diff --git a/src/filesystem/FileFinder.php b/src/filesystem/FileFinder.php index d75449f..56a8007 100644 --- a/src/filesystem/FileFinder.php +++ b/src/filesystem/FileFinder.php @@ -1,268 +1,285 @@ 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 * @group filesystem */ final class FileFinder { private $root; private $exclude = array(); private $paths = array(); + private $name = array(); private $suffix = 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; } /** * @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) { - $matches = (count($this->suffix) == 0); + $matches = !count($this->name) && !count($this->suffix); + foreach ($this->name as $curr_name) { + if (basename($file) === $curr_name) { + $matches = true; + break; + } + } foreach ($this->suffix as $curr_suffix) { if (fnmatch($curr_suffix, $file)) { $matches = true; break; } } if (!$matches) { return false; } $matches = (count($this->paths) == 0); foreach ($this->paths as $path) { if (fnmatch($path, $this->root.'/'.$file)) { $matches = true; break; } } $fullpath = $this->root.'/'.ltrim($file, '/'); if (($this->type == 'f' && is_dir($fullpath)) || ($this->type == 'd' && !is_dir($fullpath))) { $matches = false; } return $matches; } /** * @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( "Invalid FileFinder root directory specified ('{$this->root}'). ". "Root directory must be a directory, be readable, and be specified ". "with an absolute path."); } 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->suffix) { - $command[] = $this->generateList('name', $this->suffix); + if ($this->name || $this->suffix) { + $command[] = $this->generateList('name', array_merge( + $this->name, + $this->suffix)); } if ($this->paths) { $command[] = $this->generateList('path', $this->paths); } $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. for ($i = 0; $i < count($files); $i++) { if (substr($files[$i], 0, 2) == './') { $files[$i] = substr($files[$i], 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) { $items = array_map('escapeshellarg', $items); foreach ($items as $key => $item) { $items[$key] = '-'.$flag.' '.$item; } $items = implode(' -o ', $items); return '"(" '.$items.' ")"'; } } diff --git a/src/filesystem/__tests__/FileFinderTestCase.php b/src/filesystem/__tests__/FileFinderTestCase.php index d3c4f69..5d1b837 100644 --- a/src/filesystem/__tests__/FileFinderTestCase.php +++ b/src/filesystem/__tests__/FileFinderTestCase.php @@ -1,112 +1,141 @@ setGenerateChecksums($checksums) - ->excludePath('./exclude') - ->excludePath('subdir.txt') - ->withType($type) - ->withPath($path) - ->withSuffix('txt') - ->setForceMode($mode); - $files = $finder->find(); - return $files; + protected function getFinder() { + $finder = new FileFinder(dirname(__FILE__) . '/data'); + $finder->excludePath('./exclude') + ->excludePath('subdir.txt'); + return $finder; } public function testFinderWithChecksums() { - $root = dirname(__FILE__) . '/data'; foreach (array('php', 'shell') as $mode) { - $files = $this->findFiles($root, true, 'f', '*', $mode); + $files = $this->getFinder() + ->setGenerateChecksums(true) + ->withType('f') + ->withPath('*') + ->withSuffix('txt') + ->setForceMode($mode) + ->find(); // Test whether correct files were found. $this->assertTrue(array_key_exists('test.txt', $files)); $this->assertTrue(array_key_exists('file.txt', $files)); $this->assertTrue( array_key_exists('include_dir.txt/subdir.txt/alsoinclude.txt', $files)); $this->assertFalse(array_key_exists('test', $files)); $this->assertTrue(array_key_exists('.hidden.txt', $files)); $this->assertFalse(array_key_exists('exclude/file.txt', $files)); $this->assertFalse(array_key_exists('include_dir.txt', $files)); foreach ($files as $file => $checksum) { $this->assertFalse(is_dir($file)); } // Test checksums. $this->assertEqual($files['test.txt'], 'aea46212fa8b8d0e0e6aa34a15c9e2f5'); $this->assertEqual($files['file.txt'], '725130ba6441eadb4e5d807898e0beae'); $this->assertEqual($files['.hidden.txt'], 'b6cfc9ce9afe12b258ee1c19c235aa27'); $this->assertEqual($files['include_dir.txt/subdir.txt/alsoinclude.txt'], '91e5c1ad76ff229c6456ac92e74e1d9f'); } } public function testFinderWithoutChecksums() { - $root = dirname(__FILE__) . '/data'; foreach (array('php', 'shell') as $mode) { - $files = $this->findFiles($root, false, 'f', '*', $mode); + $files = $this->getFinder() + ->withType('f') + ->withPath('*') + ->withSuffix('txt') + ->setForceMode($mode) + ->find(); // Test whether correct files were found. $this->assertTrue(in_array('test.txt', $files)); $this->assertTrue(in_array('file.txt', $files)); $this->assertTrue(in_array('.hidden.txt', $files)); $this->assertTrue( in_array('include_dir.txt/subdir.txt/alsoinclude.txt', $files)); $this->assertFalse(in_array('test', $files)); $this->assertFalse(in_array('exclude/file.txt', $files)); $this->assertFalse(in_array('include_dir.txt', $files)); foreach ($files as $file => $checksum) { $this->assertFalse(is_dir($file)); } } } public function testFinderWithDirectories() { - $root = dirname(__FILE__) . '/data'; foreach (array('php', 'shell') as $mode) { - $files = $this->findFiles($root, true, '', '*', $mode); + $files = $this->getFinder() + ->setGenerateChecksums(true) + ->withPath('*') + ->withSuffix('txt') + ->setForceMode($mode) + ->find(); // Test whether the correct files were found. $this->assertTrue(array_key_exists('test.txt', $files)); $this->assertTrue(array_key_exists('file.txt', $files)); $this->assertTrue( array_key_exists('include_dir.txt/subdir.txt/alsoinclude.txt', $files)); $this->assertFalse(array_key_exists('test', $files)); $this->assertTrue(array_key_exists('.hidden.txt', $files)); $this->assertFalse(array_key_exists('exclude/file.txt', $files)); $this->assertTrue(array_key_exists('include_dir.txt', $files)); // Test checksums. $this->assertEqual($files['test.txt'], 'aea46212fa8b8d0e0e6aa34a15c9e2f5'); $this->assertEqual($files['include_dir.txt'], null); } } public function testFinderWithPath() { - $root = dirname(__FILE__) . '/data'; foreach (array('php', 'shell') as $mode) { - $files = $this->findFiles($root, true, 'f', - '*/include_dir.txt/subdir.txt/alsoinclude.txt', $mode); + $files = $this->getFinder() + ->setGenerateChecksums(true) + ->withType('f') + ->withPath('*/include_dir.txt/subdir.txt/alsoinclude.txt') + ->withSuffix('txt') + ->setForceMode($mode) + ->find(); // Test whether the correct files were found. $this->assertTrue( array_key_exists('include_dir.txt/subdir.txt/alsoinclude.txt', $files)); // Ensure that only the one file was found. $this->assertEqual(1, count($files)); } } + public function testFinderWithNames() { + foreach (array('php', 'shell') as $mode) { + $files = $this->getFinder() + ->withType('f') + ->withPath('*') + ->withName('test') + ->setForceMode($mode) + ->find(); + + // Test whether the correct files were found. + $this->assertTrue(in_array('test', $files)); + $this->assertFalse(in_array('exclude/test', $files)); + $this->assertTrue(in_array('include_dir.txt/test', $files)); + $this->assertTrue(in_array('include_dir.txt/subdir.txt/test', $files)); + $this->assertEqual(3, count($files)); + } + } + } diff --git a/src/filesystem/__tests__/data/exclude/test b/src/filesystem/__tests__/data/exclude/test new file mode 100644 index 0000000..bce1946 --- /dev/null +++ b/src/filesystem/__tests__/data/exclude/test @@ -0,0 +1 @@ +Test file. diff --git a/src/filesystem/__tests__/data/include_dir.txt/subdir.txt/test b/src/filesystem/__tests__/data/include_dir.txt/subdir.txt/test new file mode 100644 index 0000000..bce1946 --- /dev/null +++ b/src/filesystem/__tests__/data/include_dir.txt/subdir.txt/test @@ -0,0 +1 @@ +Test file. diff --git a/src/filesystem/__tests__/data/include_dir.txt/test b/src/filesystem/__tests__/data/include_dir.txt/test new file mode 100644 index 0000000..bce1946 --- /dev/null +++ b/src/filesystem/__tests__/data/include_dir.txt/test @@ -0,0 +1 @@ +Test file.