src/filesystem/FileFinder.php (247 lines of code) (raw):

<?php /** * Find files on disk matching criteria, like the 'find' system utility. Use of * this class is straightforward: * * // Find PHP files in /tmp * $files = id(new FileFinder('/tmp')) * ->withType('f') * ->withSuffix('php') * ->find(); * * @task create Creating a File Query * @task config Configuring File Queries * @task exec Executing the File Query * @task internal Internal */ final class FileFinder extends Phobject { private $root; private $exclude = array(); private $paths = array(); private $name = array(); private $suffix = array(); private $nameGlobs = array(); private $type; private $generateChecksums = false; private $followSymlinks; private $forceMode; /** * Create a new FileFinder. * * @param string Root directory to find files beneath. * @return this * @task create */ public function __construct($root) { $this->root = rtrim($root, '/'); } /** * @task config */ public function excludePath($path) { $this->exclude[] = $path; return $this; } /** * @task config */ public function withName($name) { $this->name[] = $name; return $this; } /** * @task config */ public function withSuffix($suffix) { $this->suffix[] = $suffix; return $this; } /** * @task config */ public function withPath($path) { $this->paths[] = $path; return $this; } /** * @task config */ public function withType($type) { $this->type = $type; return $this; } /** * @task config */ public function withFollowSymlinks($follow) { $this->followSymlinks = $follow; return $this; } /** * @task config */ public function setGenerateChecksums($generate) { $this->generateChecksums = $generate; return $this; } public function getGenerateChecksums() { return $this->generateChecksums; } public function withNameGlob($pattern) { $this->nameGlobs[] = $pattern; return $this; } /** * @task config * @param string Either "php", "shell", or the empty string. */ public function setForceMode($mode) { $this->forceMode = $mode; return $this; } /** * @task internal */ public function validateFile($file) { if ($this->name) { $matches = false; foreach ($this->name as $curr_name) { if (basename($file) === $curr_name) { $matches = true; break; } } if (!$matches) { return false; } } if ($this->nameGlobs) { $name = basename($file); $matches = false; foreach ($this->nameGlobs as $glob) { $glob = addcslashes($glob, '\\'); if (fnmatch($glob, $name)) { $matches = true; break; } } if (!$matches) { return false; } } if ($this->suffix) { $matches = false; foreach ($this->suffix as $suffix) { $suffix = addcslashes($suffix, '\\?*'); $suffix = '*.'.$suffix; if (fnmatch($suffix, $file)) { $matches = true; break; } } if (!$matches) { return false; } } if ($this->paths) { $matches = false; foreach ($this->paths as $path) { if (fnmatch($path, $this->root.'/'.$file)) { $matches = true; break; } } if (!$matches) { return false; } } $fullpath = $this->root.'/'.ltrim($file, '/'); if (($this->type == 'f' && is_dir($fullpath)) || ($this->type == 'd' && !is_dir($fullpath))) { return false; } return true; } /** * @task internal */ private function getFiles($dir) { $found = Filesystem::listDirectory($this->root.'/'.$dir, true); $files = array(); if (strlen($dir) > 0) { $dir = rtrim($dir, '/').'/'; } foreach ($found as $filename) { // Only exclude files whose names match relative to the root. if ($dir == '') { $matches = true; foreach ($this->exclude as $exclude_path) { if (fnmatch(ltrim($exclude_path, './'), $dir.$filename)) { $matches = false; break; } } if (!$matches) { continue; } } if ($this->validateFile($dir.$filename)) { $files[] = $dir.$filename; } if (is_dir($this->root.'/'.$dir.$filename)) { foreach ($this->getFiles($dir.$filename) as $file) { $files[] = $file; } } } return $files; } /** * @task exec */ public function find() { $files = array(); if (!is_dir($this->root) || !is_readable($this->root)) { throw new Exception( pht( "Invalid %s root directory specified ('%s'). Root directory ". "must be a directory, be readable, and be specified with an ". "absolute path.", __CLASS__, $this->root)); } if ($this->forceMode == 'shell') { $php_mode = false; } else if ($this->forceMode == 'php') { $php_mode = true; } else { $php_mode = (phutil_is_windows() || !Filesystem::binaryExists('find')); } if ($php_mode) { $files = $this->getFiles(''); } else { $args = array(); $command = array(); $command[] = 'find'; if ($this->followSymlinks) { $command[] = '-L'; } $command[] = '.'; if ($this->exclude) { $command[] = $this->generateList('path', $this->exclude).' -prune'; $command[] = '-o'; } if ($this->type) { $command[] = '-type %s'; $args[] = $this->type; } if ($this->name) { $command[] = $this->generateList('name', $this->name, 'name'); } if ($this->suffix) { $command[] = $this->generateList('name', $this->suffix, 'suffix'); } if ($this->paths) { $command[] = $this->generateList('path', $this->paths); } if ($this->nameGlobs) { $command[] = $this->generateList('name', $this->nameGlobs); } $command[] = '-print0'; array_unshift($args, implode(' ', $command)); list($stdout) = newv('ExecFuture', $args) ->setCWD($this->root) ->resolvex(); $stdout = trim($stdout); if (!strlen($stdout)) { return array(); } $files = explode("\0", $stdout); // On OSX/BSD, find prepends a './' to each file. foreach ($files as $key => $file) { // When matching directories, we can get "." back in the result set, // but this isn't an interesting result. if ($file == '.') { unset($files[$key]); continue; } if (substr($files[$key], 0, 2) == './') { $files[$key] = substr($files[$key], 2); } } } if (!$this->generateChecksums) { return $files; } else { $map = array(); foreach ($files as $line) { $fullpath = $this->root.'/'.ltrim($line, '/'); if (is_dir($fullpath)) { $map[$line] = null; } else { $map[$line] = md5_file($fullpath); } } return $map; } } /** * @task internal */ private function generateList( $flag, array $items, $mode = 'glob') { foreach ($items as $key => $item) { // If the mode is not "glob" mode, we're going to escape glob characters // in the pattern. Otherwise, we escape only backslashes. if ($mode === 'glob') { $item = addcslashes($item, '\\'); } else { $item = addcslashes($item, '\\*?'); } if ($mode === 'suffix') { $item = '*.'.$item; } $item = (string)csprintf('%s %s', '-'.$flag, $item); $items[$key] = $item; } $items = implode(' -o ', $items); return '"(" '.$items.' ")"'; } }