Changeset View
Changeset View
Standalone View
Standalone View
src/filesystem/FileFinder.php
- This file was added.
| <?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.' ")"'; | |||||
| } | |||||
| } | |||||