From e23ef878945a80e902e2d9142a2f7fe38b36d90b Mon Sep 17 00:00:00 2001 From: Finalclap Date: Mon, 18 May 2015 00:35:25 +0200 Subject: [PATCH] First commit Version 1.0.0 + phpunit tests --- .gitignore | 8 +- LICENSE | 16 +- README.md | 46 +++- build/build.php | 30 +++ phpunit.xml.dist | 8 + src/CliReporter.php | 148 +++++++++++++ src/DirScan.php | 178 +++++++++++++++ src/Reporter.php | 32 +++ src/TestReporter.php | 37 ++++ src/dirscan | 67 ++++++ src/legacy/dirscan | 403 ++++++++++++++++++++++++++++++++++ tests/DirScan/DirScanTest.php | 179 +++++++++++++++ tests/bootstrap.php | 34 +++ tests/setup.sh | 41 ++++ 14 files changed, 1205 insertions(+), 22 deletions(-) create mode 100644 build/build.php create mode 100644 phpunit.xml.dist create mode 100644 src/CliReporter.php create mode 100644 src/DirScan.php create mode 100644 src/Reporter.php create mode 100644 src/TestReporter.php create mode 100644 src/dirscan create mode 100644 src/legacy/dirscan create mode 100644 tests/DirScan/DirScanTest.php create mode 100644 tests/bootstrap.php create mode 100644 tests/setup.sh diff --git a/.gitignore b/.gitignore index 3a693c9..56b9bfc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,2 @@ -composer.phar -vendor/ - -# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file -# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file -# composer.lock +/build/*.phar +/tests/sandbox \ No newline at end of file diff --git a/LICENSE b/LICENSE index 8f71f43..10ccc5a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ - Apache License + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -175,18 +175,7 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} + Copyright 2015 Vincent Paré Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -199,4 +188,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - diff --git a/README.md b/README.md index 66865c4..2cde01f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,44 @@ -# DirScan -File system inventory +DirScan - file system inventory +------------------------------- +DirScan is a file cataloging command line tool that lists all files inside a directory and its sub-directories to a text file, with file attributes like timestamps and permissions. + +It can be used to compare all files in an entire partition at different times and see what changed between. + +Example : + +``` +dirscan --deep --same-device / > "drive-content.txt" +``` + +DirScan is bundled with a text reporter, but you can customize its output by creating a custom reporting class, allowing to save data to, say, CSV, XML or an SQLite database. + +### Download ### +Download dirscan.phar, make it executable (chmod +x dirscan.phar) and rename it if you want. On Linux, store it to `/usr/local/bin` to make it available everywhere : + +``` +wget https://github.com/finalclap/DirScan/releases/download/1.0.0/dirscan.phar +chmod +x dirscan.phar +mv dirscan.phar /usr/local/bin/dirscan +``` + +### Requirement ### +Tested on PHP 5.3, 5.4, 5.5 & 5.6. There is also a [legacy release](https://raw.githubusercontent.com/finalclap/DirScan/master/src/legacy/dirscan) that works on PHP 5.2 with some limitations. + +### Usage ### +``` +Usage : + dirscan [OPTIONS] TARGET + +Options : + --help, -h This help message + --deep, -d Explore symbolic links (default : skip) + --flat, -f Do not explore subdirectories + --access, -a Report access time + --htime Report user friendly date nearby unix timestamps + --same-device Explore only directories on the same device as the start directory + Useful on Linux, to ignore special mount points like /sys or /proc +``` + +### About windows ### + +DirScan is designed to work a Unix environment (Linux or Mac OS) but you can also use it on Windows. In this case, beware of NTFS junction points and symbolic links that are not handled properly by old php releases (see [readlink](http://php.net/manual/en/function.readlink.php) & [is_link](http://php.net/manual/fr/function.is-link.php)). But you'd better use other tools like [WhereIsIt](http://www.whereisit-soft.com/). diff --git a/build/build.php b/build/build.php new file mode 100644 index 0000000..0987975 --- /dev/null +++ b/build/build.php @@ -0,0 +1,30 @@ +startBuffering(); + +// Removing shebang from dirscan +$dirscan = file_get_contents($srcRoot.'/dirscan'); +$dirscan = preg_replace('/^#!.*?[\n\r]+/', '', $dirscan); + +// Adding files +$phar['dirscan'] = $dirscan; +$phar['DirScan.php'] = file_get_contents($srcRoot.'/DirScan.php'); +$phar['Reporter.php'] = file_get_contents($srcRoot.'/Reporter.php'); +$phar['CliReporter.php'] = file_get_contents($srcRoot.'/CliReporter.php'); + +// Get the stub +$defaultStub = $phar->createDefaultStub('dirscan'); +$stub = '#!/usr/bin/env php'."\n".$defaultStub; +$phar->setStub($stub); + +$phar->stopBuffering(); +echo "Build done (dirscan.phar)"."\n"; diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d5be96c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,8 @@ + + + + + tests/DirScan + + + diff --git a/src/CliReporter.php b/src/CliReporter.php new file mode 100644 index 0000000..5fd6845 --- /dev/null +++ b/src/CliReporter.php @@ -0,0 +1,148 @@ +access = isset($settings['access']) ? $settings['access'] : false; + $this->htime = isset($settings['htime']) ? $settings['htime'] : false; + $this->typeMapping = array( + 'dir' => 'd', + 'file' => 'f', + 'link' => 'l' + ); + } + + /** + * Print report header + * + * @param string $target Target directory + * @param array $settings List of report settings + * @param array $argv Command line arguments + */ + public function header($target, $settings, $argv) + { + $targetStat = DirScan::stat($target); + + echo "time: ".time()."\n"; + echo "date: ".date('r')."\n"; + echo "getenv(TZ): ".getenv('TZ')."\n"; + echo "date_default_timezone_get: ".date_default_timezone_get()."\n"; + echo "php version: ".phpversion()."\n"; + echo "cwd: ".getcwd()."\n"; + echo "target: ".$target." (realpath: ".$targetStat['realpath'].")\n"; + echo "start device: ".$targetStat['dev']."\n"; + echo "settings: ".json_encode($settings)."\n"; + echo "argv: ".json_encode($argv)."\n"; + echo "=====================================\n"; + + $header = array( + 'Unique path', + 'Type', + 'Size', + ); + + $header[] = 'ctime'; + if ($this->htime) { + $header[] = 'Change time'; + } + + $header[] = 'mtime'; + if ($this->htime) { + $header[] = 'Modify time'; + } + + if ($this->access) { + $header[] = 'atime'; + if ($this->htime) { + $header[] = 'Access time'; + } + } + + $header[] = 'Extended'; + echo implode("\t", $header)."\n"; + } + + /** + * Print node data + * + * @param array $node Data returned by DirScan::stat + * @param DirScan $scanner DirScan object + */ + public function push($node, DirScan $scanner) + { + $type = isset($this->typeMapping[$node['type']]) ? $this->typeMapping[$node['type']] : $node['type']; + $perms = substr(sprintf('%o', $node['mode']), -4); + if ($this->htime) { + $hctime = date('d/m/Y H:i:s', $node['ctime']); + $hmtime = date('d/m/Y H:i:s', $node['mtime']); + $hatime = date('d/m/Y H:i:s', $node['atime']); + } + + $extended = array(); + if (isset($node['target'])) { + $extended['target'] = $node['target']; + } + $startDevice = $scanner->startDevice; + if ($node['dev'] != $startDevice) { + $extended['device'] = $node['dev']; + } + + $row = array( + $node['uniquepath'], + $type, + $node['size'], + ); + + $row[] = $node['ctime']; + if ($this->htime) { + $row[] = $hctime; + } + + $row[] = $node['mtime']; + if ($this->htime) { + $row[] = $hmtime; + } + + if ($this->access) { + $row[] = $node['atime']; + if ($this->htime) { + $row[] = $hatime; + } + } + + if (!empty($extended)) { + $row[] = json_encode($extended); + } + + echo implode("\t", $row)."\n"; + } + + /** + * Print error messages on stderr + * + * @param string $msg Error message + * @param int $code Error code + */ + public function error($msg, $code = null) + { + file_put_contents('php://stderr', $msg."\n"); + } +} diff --git a/src/DirScan.php b/src/DirScan.php new file mode 100644 index 0000000..3236602 --- /dev/null +++ b/src/DirScan.php @@ -0,0 +1,178 @@ +reporter = $reporter; + $this->deep = isset($settings['deep']) ? $settings['deep'] : false; + $this->flat = isset($settings['flat']) ? $settings['flat'] : false; + $this->sameDevice = isset($settings['same-device']) ? $settings['same-device'] : false; + } + + /** + * Scan $path and its content if $path is a directory + * + * @param string $directory Path of a node to scan + * @param array $pathstack List of parents directories, used for recursive directory loop detection + */ + public function scan($path, $pathstack = array()) + { + $stat = self::stat($path); + call_user_func(array($this->reporter, 'push'), $stat, $this); + + // Saving start directory device + if (empty($pathstack)) { + $this->startDevice = $stat['dev']; + } + + // Exit now if path is not a directory or flat is enabled (except for the 1st directory (target)) + if (!is_dir($path) || ($this->flat && !empty($pathstack))) { + return; + } + + // Skip symlink if deep is disabled + if (!$this->deep && $stat['type'] === 'link') { + return; + } + + // Do not explore direcotry content if it's not on the start device + if ($this->sameDevice && $stat['dev'] != $this->startDevice) { + return; + } + + // Directory loop prevention + if (in_array($stat['realpath'], $pathstack)) { + $msg = "Infinite loop : ".$path." (".$stat['uniquepath'].")"; + call_user_func(array($this->reporter, 'error'), $msg, self::ERR_DIR_LOOP); + return; + } + + // Add current directory to path stack + $pathstack[] = $stat['realpath']; + + // Directory content scan + $childs = @opendir($path); + if ($childs === false) { + $error = error_get_last(); + call_user_func(array($this->reporter, 'error'), $error['message'], self::ERR_DIR_READ); + return; + } + while ($child = readdir($childs)) { + // Skip . & .. + if ($child === '.' || $child === '..') { + continue; + } + + $childPath = rtrim($path, '/').DIRECTORY_SEPARATOR.$child; + $this->scan($childPath, $pathstack); + } + closedir($childs); + } + + /** + * Get file system node metadata + * + * @param string $path File system path of the node + * @return array + */ + public static function stat($path) + { + $lstat = lstat($path); + if ($lstat === false) { + return false; + } + $type = filetype($path); + $owner = function_exists('posix_getpwuid') ? posix_getpwuid($lstat['uid']) : null; + $group = function_exists('posix_getgrgid') ? posix_getgrgid($lstat['gid']) : null; + $uniquepath = self::uniquepath($path); + + clearstatcache(true); + $result = array( + 'path' => $path, + 'uniquepath' => $uniquepath, + 'realpath' => realpath($path), + 'type' => $type, + 'ino' => $lstat['ino'], + 'dev' => $lstat['dev'], + 'size' => $lstat['size'], + 'atime' => $lstat['atime'], + 'mtime' => $lstat['mtime'], + 'ctime' => $lstat['ctime'], + 'mode' => $lstat['mode'], + 'uid' => $lstat['uid'], + 'gid' => $lstat['gid'], + 'owner' => $owner['name'], + 'group' => $group['name'], + ); + + if ($type === 'link') { + $target = @readlink($path); + if ($target === false) { + $error = error_get_last(); + $msg = $error['message']." (".$uniquepath.")"; + call_user_func(array($this->reporter, 'error'), $msg, self::ERR_READLINK); + return; + } else { + $result['target'] = readlink($path); + } + } + + return $result; + } + + /** + * Get a unique path to directory, file and symbolic links + * Returns false when path does not exists + * + * @param string $path + * @return string|false + */ + public static function uniquepath($path) + { + if (!file_exists($path)) { + return false; + } + $info = pathinfo($path); + clearstatcache(true); + $parent = realpath($info['dirname']); + $uniquepath = rtrim($parent, '/').DIRECTORY_SEPARATOR.$info['basename']; + return $uniquepath; + } + + /** + * This is used to fetch readonly variables + * + * @param string $attr Attribute name to read + * @return mixed Attribute value (or null if attribute does not exists) + */ + public function __get($attr) + { + return ($attr != "instance" && isset($this->$attr)) ? $this->$attr : null; + } +} diff --git a/src/Reporter.php b/src/Reporter.php new file mode 100644 index 0000000..f1c0d86 --- /dev/null +++ b/src/Reporter.php @@ -0,0 +1,32 @@ +pushStack[] = $node; + } + + /** + * Print error messages on stderr + * + * @param string $msg Error message + * @param int $code Error code + */ + public function error($msg, $code = null) + { + $this->errorStack[] = array('msg' => $msg, $code => $code); + } +} diff --git a/src/dirscan b/src/dirscan new file mode 100644 index 0000000..df774c6 --- /dev/null +++ b/src/dirscan @@ -0,0 +1,67 @@ +#!/usr/bin/env php + isset($options['help']) || isset($options['h']) ? true : false, + 'deep' => isset($options['deep']) || isset($options['d']) ? true : false, + 'flat' => isset($options['flat']) || isset($options['f']) ? true : false, + 'access' => isset($options['access']) || isset($options['a']) ? true : false, + 'htime' => isset($options['htime']) ? true : false, + 'same-device' => isset($options['same-device']) ? true : false, +); +$target = isset($argv[1]) ? end($argv) : null; + +// Make target an absolute path to get phar working +if ($target !== null && !preg_match('#^/#', $target)) { + $target = rtrim(getcwd(), '/').'/'.$target; +} + +if ($settings['help']) { + echo $usage; + die(); +} elseif (!is_dir($target)) { + file_put_contents('php://stderr', "TARGET".(!empty($target) ? ' ('.$target.') ' : ' ')."is not a directory\n"); + echo $usage; + die(1); +} + +// Scan +$reporter = new CliReporter($settings); +$reporter->header($target, $settings, $argv); +$scanner = new DirScan($settings, $reporter); +$scanner->scan($target); diff --git a/src/legacy/dirscan b/src/legacy/dirscan new file mode 100644 index 0000000..2e8d704 --- /dev/null +++ b/src/legacy/dirscan @@ -0,0 +1,403 @@ +#!/usr/bin/env php + in_array('--help', $argv) || isset($options['h']) ? true : false, + 'deep' => in_array('--deep', $argv) || isset($options['d']) ? true : false, + 'flat' => in_array('--flat', $argv) || isset($options['f']) ? true : false, + 'access' => in_array('--access', $argv) || isset($options['a']) ? true : false, + 'htime' => in_array('--htime', $argv) ? true : false, + 'same-device' => in_array('--same-device', $argv) ? true : false, +); + +$target = isset($argv[1]) ? end($argv) : null; + +if ($settings['help']) { + echo $usage; + die(); +} elseif (!is_dir($target)) { + file_put_contents('php://stderr', "TARGET".(!empty($target) ? ' ('.$target.') ' : ' ')."is not a directory\n"); + echo $usage; + die(1); +} + +// Scan +$reporter = new CliReporter($settings); +$reporter->header($target, $settings, $argv); +$scanner = new DirScan($settings, $reporter); +$scanner->scan($target); + +/** + * File system scanning class + */ +class DirScan +{ + // Error codes + const ERR_DIR_LOOP = 1000; + const ERR_DIR_READ = 1001; + const ERR_READLINK = 1002; + + protected $deep; // bool Explore symlinks if true (default false) + protected $flat; // bool Do not explore subdirectories if true (default false) + protected $reporter; // Reporter Reporter object to handle DirScan output + protected $sameDevice; // bool Scan only directories on the same device as the start directory (default false) + protected $startDevice; // int Device ID of the start directory (provided by lstat) + + /** + * Create a new DirScan object + * + * @param array $settings List of scan settings + * @param Reporter $callback Called for every scanned file system node + */ + public function __construct($settings, Reporter $reporter) + { + $this->reporter = $reporter; + $this->deep = isset($settings['deep']) ? $settings['deep'] : false; + $this->flat = isset($settings['flat']) ? $settings['flat'] : false; + $this->sameDevice = isset($settings['same-device']) ? $settings['same-device'] : false; + } + + /** + * Scan $path and its content if $path is a directory + * + * @param string $directory Path of a node to scan + * @param array $pathstack List of parents directories, used for recursive directory loop detection + */ + public function scan($path, $pathstack = array()) + { + $stat = self::stat($path); + call_user_func(array($this->reporter, 'push'), $stat, $this); + + // Saving start directory device + if (empty($pathstack)) { + $this->startDevice = $stat['dev']; + } + + // Exit now if path is not a directory or flat is enabled (except for the 1st directory (target)) + if (!is_dir($path) || ($this->flat && !empty($pathstack))) { + return; + } + + // Skip symlink if deep is disabled + if (!$this->deep && $stat['type'] === 'link') { + return; + } + + // Do not explore direcotry content if it's not on the start device + if ($this->sameDevice && $stat['dev'] != $this->startDevice) { + return; + } + + // Directory loop prevention + if (in_array($stat['realpath'], $pathstack)) { + $msg = "Infinite loop : ".$path." (".$stat['uniquepath'].")"; + call_user_func(array($this->reporter, 'error'), $msg, self::ERR_DIR_LOOP); + return; + } + + // Add current directory to path stack + $pathstack[] = $stat['realpath']; + + // Directory content scan + $childs = @opendir($path); + if ($childs === false) { + $error = error_get_last(); + call_user_func(array($this->reporter, 'error'), $error['message'], self::ERR_DIR_READ); + return; + } + while ($child = readdir($childs)) { + // Skip . & .. + if ($child === '.' || $child === '..') { + continue; + } + + $childPath = rtrim($path, '/').DIRECTORY_SEPARATOR.$child; + $this->scan($childPath, $pathstack); + } + closedir($childs); + } + + /** + * Get file system node metadata + * + * @param string $path File system path of the node + * @return array + */ + public static function stat($path) + { + $lstat = lstat($path); + if ($lstat === false) { + return false; + } + $type = filetype($path); + $owner = function_exists('posix_getpwuid') ? posix_getpwuid($lstat['uid']) : null; + $group = function_exists('posix_getgrgid') ? posix_getgrgid($lstat['gid']) : null; + $uniquepath = self::uniquepath($path); + + $result = array( + 'path' => $path, + 'uniquepath' => $uniquepath, + 'realpath' => realpath($path), + 'type' => $type, + 'ino' => $lstat['ino'], + 'dev' => $lstat['dev'], + 'size' => $lstat['size'], + 'atime' => $lstat['atime'], + 'mtime' => $lstat['mtime'], + 'ctime' => $lstat['ctime'], + 'mode' => $lstat['mode'], + 'uid' => $lstat['uid'], + 'gid' => $lstat['gid'], + 'owner' => $owner['name'], + 'group' => $group['name'], + ); + + if ($type === 'link') { + $target = @readlink($path); + if ($target === false) { + $error = error_get_last(); + $msg = $error['message']." (".$uniquepath.")"; + call_user_func(array($this->reporter, 'error'), $msg, self::ERR_READLINK); + return; + } else { + $result['target'] = readlink($path); + } + } + + return $result; + } + + /** + * Get a unique path to directory, file and symbolic links + * Returns false when path does not exists + * + * @param string $path + * @return string|false + */ + public static function uniquepath($path) + { + if (!file_exists($path)) { + return false; + } + $info = pathinfo($path); + $parent = realpath($info['dirname']); + $uniquepath = rtrim($parent, '/').DIRECTORY_SEPARATOR.$info['basename']; + return $uniquepath; + } + + /** + * This is used to fetch readonly variables + * + * @param string $attr Attribute name to read + * @return mixed Attribute value (or null if attribute does not exists) + */ + public function __get($attr) + { + return ($attr != "instance" && isset($this->$attr)) ? $this->$attr : null; + } +} + +abstract class Reporter +{ + /** + * DirScan entry handler + * + * @param array $node Data returned by DirScan::stat + * @param DirScan $scanner DirScan object + */ + public function push($node, DirScan $scanner) + { + } + + /** + * DirScan error handler + * + * @param string $msg Error message + * @param int $code Error code + */ + public function error($msg, $code = null) + { + trigger_error($msg, E_USER_WARNING); + } +} + +/** + * Displays scanned files path and attributes (sent by DirScan::scan) on standard output + */ +class CliReporter extends Reporter +{ + protected $access; // bool Report node access time (default false) + protected $htime; // bool Display human readble date/time beside unix timestamp (default false) + protected $typeMapping; // array Short name for file types + + /** + * Set report settings + * + * @param array $settings List of report settings + */ + public function __construct($settings) + { + $this->access = isset($settings['access']) ? $settings['access'] : false; + $this->htime = isset($settings['htime']) ? $settings['htime'] : false; + $this->typeMapping = array( + 'dir' => 'd', + 'file' => 'f', + 'link' => 'l' + ); + } + + /** + * Print report header + * + * @param string $target Target directory + * @param array $settings List of report settings + * @param array $argv Command line arguments + */ + public function header($target, $settings, $argv) + { + $targetStat = DirScan::stat($target); + + echo "time: ".time()."\n"; + echo "date: ".date('r')."\n"; + echo "getenv(TZ): ".getenv('TZ')."\n"; + echo "date_default_timezone_get: ".date_default_timezone_get()."\n"; + echo "php version: ".phpversion()."\n"; + echo "cwd: ".getcwd()."\n"; + echo "target: ".$target." (realpath: ".$targetStat['realpath'].")\n"; + echo "start device: ".$targetStat['dev']."\n"; + echo "settings: ".json_encode($settings)."\n"; + echo "argv: ".json_encode($argv)."\n"; + echo "=====================================\n"; + + $header = array( + 'Unique path', + 'Type', + 'Size', + ); + + $header[] = 'ctime'; + if ($this->htime) { + $header[] = 'Change time'; + } + + $header[] = 'mtime'; + if ($this->htime) { + $header[] = 'Modify time'; + } + + if ($this->access) { + $header[] = 'atime'; + if ($this->htime) { + $header[] = 'Access time'; + } + } + + $header[] = 'Extended'; + echo implode("\t", $header)."\n"; + } + + /** + * Print node data + * + * @param array $node Data returned by DirScan::stat + * @param DirScan $scanner DirScan object + */ + public function push($node, DirScan $scanner) + { + $type = isset($this->typeMapping[$node['type']]) ? $this->typeMapping[$node['type']] : $node['type']; + $perms = substr(sprintf('%o', $node['mode']), -4); + if ($this->htime) { + $hctime = date('d/m/Y H:i:s', $node['ctime']); + $hmtime = date('d/m/Y H:i:s', $node['mtime']); + $hatime = date('d/m/Y H:i:s', $node['atime']); + } + + $extended = array(); + if (isset($node['target'])) { + $extended['target'] = $node['target']; + } + $startDevice = $scanner->startDevice; + if ($node['dev'] != $startDevice) { + $extended['device'] = $node['dev']; + } + + $row = array( + $node['uniquepath'], + $type, + $node['size'], + ); + + $row[] = $node['ctime']; + if ($this->htime) { + $row[] = $hctime; + } + + $row[] = $node['mtime']; + if ($this->htime) { + $row[] = $hmtime; + } + + if ($this->access) { + $row[] = $node['atime']; + if ($this->htime) { + $row[] = $hatime; + } + } + + if (!empty($extended)) { + $row[] = json_encode($extended); + } + + echo implode("\t", $row)."\n"; + } + + /** + * Print error messages on stderr + * + * @param string $msg Error message + * @param int $code Error code + */ + public function error($msg, $code = null) + { + file_put_contents('php://stderr', $msg."\n"); + } +} diff --git a/tests/DirScan/DirScanTest.php b/tests/DirScan/DirScanTest.php new file mode 100644 index 0000000..5ac82d7 --- /dev/null +++ b/tests/DirScan/DirScanTest.php @@ -0,0 +1,179 @@ +sandbox = $_ENV['DIRSCAN_TEST_SANDBOX']; + } + + /** + * Check uniquepath on a symlink + * uniquepah should not resolve the symlink itself + * + * @covers \Finalclap\DirScan\DirScan::uniquepath + */ + public function testUniquePath() + { + $symlinkpath = $this->sandbox.'/hulls/ln-dir-absolute-seaplane'; + $uniquepath = DirScan::uniquepath($symlinkpath); + $this->assertEquals($symlinkpath, $uniquepath); + } + + /** + * uniquepath on a non existing path + * + * @covers \Finalclap\DirScan\DirScan::uniquepath + */ + public function testUniquePathNotExists() + { + $uniquepath = DirScan::uniquepath($this->sandbox.'/the-no-file'); + $this->assertEquals(false, $uniquepath); + $uniquepath = DirScan::uniquepath($this->sandbox.'/no-directory/no-file'); + $this->assertEquals(false, $uniquepath); + $uniquepath = DirScan::uniquepath($this->sandbox.'/hulls'); + $this->assertNotEquals(false, $uniquepath); + $uniquepath = DirScan::uniquepath($this->sandbox.'/wings/seaplane/fuel.txt'); + $this->assertNotEquals(false, $uniquepath); + $uniquepath = DirScan::uniquepath($this->sandbox.'/ln-file-absolute-fuel'); + $this->assertNotEquals(false, $uniquepath); + $uniquepath = DirScan::uniquepath($this->sandbox.'/ln-file-relative-fuel'); + $this->assertNotEquals(false, $uniquepath); + } + + /** + * Directory exploration + */ + public function testDirectory() + { + $settings = array(); + $reporter = new TestReporter(); + $scanner = new DirScan($settings, $reporter); + $scanner->scan($this->sandbox.'/wheels/car'); + $this->assertEmpty($reporter->errorStack); + $this->assertCount(8, $reporter->pushStack); + } + + /** + * Directory exploration with symbolic links + */ + public function testFullScan() + { + $settings = array(); + $reporter = new TestReporter(); + $scanner = new DirScan($settings, $reporter); + $scanner->scan($this->sandbox); + + $pattern = '#'.preg_quote($this->sandbox, '#').'#'; + $observed = array(); + foreach ($reporter->pushStack as $key => $val) { + $observed[] = preg_replace($pattern, '', $val['uniquepath']); + } + sort($observed); + + $expected = array( + '', + '/hulls', + '/hulls/boat', + '/hulls/jet ski', + '/hulls/ln-dir-absolute-seaplane', + '/hulls/ln-dir-relative-seaplane', + '/ln-file-absolute-fuel', + '/ln-file-relative-fuel', + '/wheels', + '/wheels/bike', + '/wheels/bike/mountain bike', + '/wheels/bike/sidecar', + '/wheels/bike/sidecar/ln-dir-loop-absolute', + '/wheels/bike/sidecar/ln-dir-loop-relative', + '/wheels/car', + '/wheels/car/convertible', + '/wheels/car/convertible/fuel.txt', + '/wheels/car/off-road', + '/wheels/car/off-road/buggy', + '/wheels/car/off-road/buggy/fuel.txt', + '/wheels/car/off-road/monster truck', + '/wheels/car/off-road/monster truck/fuel.txt', + '/wings', + '/wings/helicopter', + '/wings/plane', + '/wings/seaplane', + '/wings/seaplane/canadair', + '/wings/seaplane/canadair/fuel.txt', + '/wings/seaplane/fuel.txt', + ); + sort($expected); + + $this->assertEmpty($reporter->errorStack); + $this->assertCount(29, $reporter->pushStack); + $this->assertSame($expected, $observed); + } + + /** + * Flat option test (do not explore subdirectories) + */ + public function testFlat() + { + $settings = array('flat' => true); + $reporter = new TestReporter(); + $scanner = new DirScan($settings, $reporter); + $scanner->scan($this->sandbox.'/hulls'); + $this->assertEmpty($reporter->errorStack); + $this->assertCount(5, $reporter->pushStack); + } + + /** + * Symlink exploration + */ + public function testDeep() + { + $settings = array('deep' => true); + $reporter = new TestReporter(); + $scanner = new DirScan($settings, $reporter); + $scanner->scan($this->sandbox.'/hulls'); + $this->assertEmpty($reporter->errorStack); + $this->assertCount(11, $reporter->pushStack); + } + + /** + * Symlink exploration with trap inside (symlink directory loop) ^^ + */ + public function testDeepDirectoryLoopFull() + { + $settings = array('deep' => true); + $reporter = new TestReporter(); + $scanner = new DirScan($settings, $reporter); + $scanner->scan($this->sandbox); + $this->assertCount(2, $reporter->errorStack); + $this->assertCount(35, $reporter->pushStack); + $this->assertStringStartsWith('Infinite loop :', $reporter->errorStack[0]['msg']); + $this->assertStringStartsWith('Infinite loop :', $reporter->errorStack[1]['msg']); + } + + /** + * Symlink exploration, starting inside symlink target directory + */ + public function testDeepDirectoryLoopFromChild() + { + $settings = array('deep' => true); + $reporter = new TestReporter(); + $scanner = new DirScan($settings, $reporter); + $scanner->scan($this->sandbox.'/wheels/bike'); + $this->assertCount(2, $reporter->errorStack); + $this->assertCount(23, $reporter->pushStack); + $this->assertStringStartsWith('Infinite loop :', $reporter->errorStack[0]['msg']); + $this->assertStringStartsWith('Infinite loop :', $reporter->errorStack[1]['msg']); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..2cb83d3 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,34 @@ + fuel.txt +touch -d '1950-04-01 12:00:00' fuel.txt +cp -p fuel.txt wheels/car/convertible +cp -p fuel.txt wheels/car/off-road/buggy +cp -p fuel.txt wheels/car/off-road/"monster truck" +cp -p fuel.txt wings/seaplane +cp -p fuel.txt wings/seaplane/canadair +rm fuel.txt + +# Creating symlinks +ln -sv "$PWD/wheels/car/convertible/fuel.txt" ln-file-absolute-fuel +ln -sv wheels/car/convertible/fuel.txt ln-file-relative-fuel +ln -sv "$PWD/wings/seaplane" hulls/ln-dir-absolute-seaplane +(cd hulls && ln -sv ../wings/seaplane ln-dir-relative-seaplane) + +# Directory loop symlinks +ln -sv "$PWD/wheels" wheels/bike/sidecar/ln-dir-loop-absolute +(cd wheels/bike/sidecar && ln -sv ../.. ln-dir-loop-relative) + +# Directories timestamp +find ./* -type d | xargs -I{} touch -d '1948-01-01 06:00:00' {}