mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
FEATURE: Added the SS_FileFinder class for finding files within a directory true that match a set of rules.
This commit is contained in:
parent
9caf597aee
commit
a0f66099ed
222
filesystem/FileFinder.php
Normal file
222
filesystem/FileFinder.php
Normal file
@ -0,0 +1,222 @@
|
||||
<?php
|
||||
/**
|
||||
* A utility class that finds any files matching a set of rules that are
|
||||
* present within a directory tree.
|
||||
*
|
||||
* Each file finder instance can have several options set on it:
|
||||
* - name_regex (string): A regular expression that file basenames must match.
|
||||
* - accept_callback (callback): A callback that is called to accept a file.
|
||||
* If it returns false the item will be skipped. The callback is passed the
|
||||
* basename, pathname and depth.
|
||||
* - accept_dir_callback (callback): The same as accept_callback, but only
|
||||
* called for directories.
|
||||
* - accept_file_callback (callback): The same as accept_callback, but only
|
||||
* called for files.
|
||||
* - file_callback (callback): A callback that is called when a file i
|
||||
* succesfully matched. It is passed the basename, pathname and depth.
|
||||
* - dir_callback (callback): The same as file_callback, but called for
|
||||
* directories.
|
||||
* - ignore_files (array): An array of file names to skip.
|
||||
* - ignore_dirs (array): An array of directory names to skip.
|
||||
* - ignore_vcs (bool): Skip over commonly used VCS dirs (svn, git, hg, bzr).
|
||||
* This is enabled by default. The names of VCS directories to skip over
|
||||
* are defined in {@link SS_FileFInder::$vcs_dirs}.
|
||||
* - max_depth (int): The maxmium depth to traverse down the folder tree,
|
||||
* default to unlimited.
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage filesystem
|
||||
*/
|
||||
class SS_FileFinder {
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $vcs_dirs = array(
|
||||
'.git', '.svn', '.hg', '.bzr'
|
||||
);
|
||||
|
||||
/**
|
||||
* The default options that are set on a new finder instance. Options not
|
||||
* present in this array cannot be set.
|
||||
*
|
||||
* Any default_option statics defined on child classes are also taken into
|
||||
* account.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $default_options = array(
|
||||
'name_regex' => null,
|
||||
'accept_callback' => null,
|
||||
'accept_dir_callback' => null,
|
||||
'accept_file_callback' => null,
|
||||
'file_callback' => null,
|
||||
'dir_callback' => null,
|
||||
'ignore_files' => null,
|
||||
'ignore_dirs' => null,
|
||||
'ignore_vcs' => true,
|
||||
'min_depth' => null,
|
||||
'max_depth' => null
|
||||
);
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
public function __construct() {
|
||||
$this->options = Object::combined_static(get_class($this), 'default_options');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an option value set on this instance.
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function getOption($name) {
|
||||
if (!array_key_exists($name, $this->options)) {
|
||||
throw new InvalidArgumentException("The option $name doesn't exist.");
|
||||
}
|
||||
|
||||
return $this->options[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an option on this finder instance. See {@link SS_FileFinder} for the
|
||||
* list of options available.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function setOption($name, $value) {
|
||||
if (!array_key_exists($name, $this->options)) {
|
||||
throw new InvalidArgumentException("The option $name doesn't exist.");
|
||||
}
|
||||
|
||||
$this->options[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets several options at once.
|
||||
*
|
||||
* @param array $options
|
||||
*/
|
||||
public function setOptions(array $options) {
|
||||
foreach ($options as $k => $v) $this->setOption($k, $v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all files matching the options within a directory. The search is
|
||||
* performed depth first.
|
||||
*
|
||||
* @param string $base
|
||||
* @return array
|
||||
*/
|
||||
public function find($base) {
|
||||
$paths = array(array(rtrim($base, '/'), 0));
|
||||
$found = array();
|
||||
|
||||
$fileCallback = $this->getOption('file_callback');
|
||||
$dirCallback = $this->getOption('dir_callback');
|
||||
|
||||
while ($path = array_shift($paths)) {
|
||||
list($path, $depth) = $path;
|
||||
|
||||
foreach (scandir($path) as $basename) {
|
||||
if ($basename == '.' || $basename == '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_dir("$path/$basename")) {
|
||||
if (!$this->acceptDir($basename, "$path/$basename", $depth + 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dirCallback) {
|
||||
call_user_func(
|
||||
$dirCallback, $basename, "$path/$basename", $depth + 1
|
||||
);
|
||||
}
|
||||
|
||||
$paths[] = array("$path/$basename", $depth + 1);
|
||||
} else {
|
||||
if (!$this->acceptFile($basename, "$path/$basename", $depth)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($fileCallback) {
|
||||
call_user_func(
|
||||
$fileCallback, $basename, "$path/$basename", $depth
|
||||
);
|
||||
}
|
||||
|
||||
$found[] = "$path/$basename";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if the directory should be traversed. This can be overloaded
|
||||
* to customise functionality, or extended with callbacks.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function acceptDir($basename, $pathname, $depth) {
|
||||
if ($this->getOption('ignore_vcs') && in_array($basename, self::$vcs_dirs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($ignore = $this->getOption('ignore_dirs')) {
|
||||
if (in_array($basename, $ignore)) return false;
|
||||
}
|
||||
|
||||
if ($max = $this->getOption('max_depth')) {
|
||||
if ($depth > $max) return false;
|
||||
}
|
||||
|
||||
if ($callback = $this->getOption('accept_callback')) {
|
||||
if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
|
||||
}
|
||||
|
||||
if ($callback = $this->getOption('accept_dir_callback')) {
|
||||
if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if the file should be included in the results. This can be
|
||||
* overloaded to customise functionality, or extended via callbacks.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function acceptFile($basename, $pathname, $depth) {
|
||||
if ($regex = $this->getOption('name_regex')) {
|
||||
if (!preg_match($regex, $basename)) return false;
|
||||
}
|
||||
|
||||
if ($ignore = $this->getOption('ignore_files')) {
|
||||
if (in_array($basename, $ignore)) return false;
|
||||
}
|
||||
|
||||
if ($minDepth = $this->getOption('min_depth')) {
|
||||
if ($depth < $minDepth) return false;
|
||||
}
|
||||
|
||||
if ($callback = $this->getOption('accept_callback')) {
|
||||
if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
|
||||
}
|
||||
|
||||
if ($callback = $this->getOption('accept_file_callback')) {
|
||||
if (!call_user_func($callback, $basename, $pathname, $depth)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
114
tests/filesystem/FileFinderTest.php
Normal file
114
tests/filesystem/FileFinderTest.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for the {@link SS_FileFinder} class.
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage tests
|
||||
*/
|
||||
class FileFinderTest extends SapphireTest {
|
||||
|
||||
protected $base;
|
||||
|
||||
public function __construct() {
|
||||
$this->base = dirname(__FILE__) . '/fixtures/filefinder';
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function testBasicOperation() {
|
||||
$this->assertFinderFinds(new SS_FileFinder(), array(
|
||||
'file1.txt',
|
||||
'file2.txt',
|
||||
'dir1/dir1file1.txt',
|
||||
'dir1/dir1file2.txt',
|
||||
'dir1/dir2/dir2file1.txt',
|
||||
'dir1/dir2/dir3/dir3file1.txt'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testInvalidOptionThrowsException() {
|
||||
$finder = new SS_FileFinder();
|
||||
$finder->setOption('this_doesnt_exist', 'ok');
|
||||
}
|
||||
|
||||
public function testFilenameRegex() {
|
||||
$finder = new SS_FileFinder();
|
||||
$finder->setOption('name_regex', '/file2\.txt$/');
|
||||
|
||||
$this->assertFinderFinds(
|
||||
$finder,
|
||||
array(
|
||||
'file2.txt',
|
||||
'dir1/dir1file2.txt'),
|
||||
'The finder only returns files matching the name regex.');
|
||||
}
|
||||
|
||||
public function testIgnoreFiles() {
|
||||
$finder = new SS_FileFinder();
|
||||
$finder->setOption('ignore_files', array('file1.txt', 'dir1file1.txt', 'dir2file1.txt'));
|
||||
|
||||
$this->assertFinderFinds(
|
||||
$finder,
|
||||
array(
|
||||
'file2.txt',
|
||||
'dir1/dir1file2.txt',
|
||||
'dir1/dir2/dir3/dir3file1.txt'),
|
||||
'The finder ignores files with the basename in the ignore_files setting.');
|
||||
}
|
||||
|
||||
public function testIgnoreDirs() {
|
||||
$finder = new SS_FileFinder();
|
||||
$finder->setOption('ignore_dirs', array('dir2'));
|
||||
|
||||
$this->assertFinderFinds(
|
||||
$finder,
|
||||
array(
|
||||
'file1.txt',
|
||||
'file2.txt',
|
||||
'dir1/dir1file1.txt',
|
||||
'dir1/dir1file2.txt'),
|
||||
'The finder ignores directories in ignore_dirs.');
|
||||
}
|
||||
|
||||
public function testMinDepth() {
|
||||
$finder = new SS_FileFinder();
|
||||
$finder->setOption('min_depth', 2);
|
||||
|
||||
$this->assertFinderFinds(
|
||||
$finder,
|
||||
array(
|
||||
'dir1/dir2/dir2file1.txt',
|
||||
'dir1/dir2/dir3/dir3file1.txt'),
|
||||
'The finder respects the min depth setting.');
|
||||
}
|
||||
|
||||
public function testMaxDepth() {
|
||||
$finder = new SS_FileFinder();
|
||||
$finder->setOption('max_depth', 1);
|
||||
|
||||
$this->assertFinderFinds(
|
||||
$finder,
|
||||
array(
|
||||
'file1.txt',
|
||||
'file2.txt',
|
||||
'dir1/dir1file1.txt',
|
||||
'dir1/dir1file2.txt'),
|
||||
'The finder respects the max depth setting.');
|
||||
}
|
||||
|
||||
public function assertFinderFinds($finder, $expect, $message = null) {
|
||||
$found = $finder->find($this->base);
|
||||
|
||||
foreach ($expect as $k => $file) {
|
||||
$expect[$k] = "{$this->base}/$file";
|
||||
}
|
||||
|
||||
sort($expect);
|
||||
sort($found);
|
||||
|
||||
$this->assertEquals($expect, $found, $message);
|
||||
}
|
||||
|
||||
}
|
0
tests/filesystem/fixtures/filefinder/file1.txt
Normal file
0
tests/filesystem/fixtures/filefinder/file1.txt
Normal file
0
tests/filesystem/fixtures/filefinder/file2.txt
Normal file
0
tests/filesystem/fixtures/filefinder/file2.txt
Normal file
Loading…
Reference in New Issue
Block a user