#!/usr/bin/env php $val) { if (preg_match('#^-[^-]#', $val)) { $errorMsg = "Seems you are using short options. Short options support is disabled in this legacy release because".PHP_EOL ."it doesn't work previous PHP 5.3. Short options will be ignored, please use long options instead,".PHP_EOL ."or use the main release.".PHP_EOL; fwrite(STDERR, $errorMsg); die(3); } } // Get settings date_default_timezone_set('UTC'); $settings = array( 'help' => in_array('--help', $argv) ? true : false, 'version' => in_array('--version', $argv) ? true : false, 'deep' => in_array('--deep', $argv) ? true : false, 'flat' => in_array('--flat', $argv) ? true : false, 'access' => in_array('--access', $argv) ? true : false, 'htime' => in_array('--htime', $argv) ? true : false, 'perms' => in_array('--perms', $argv) ? true : false, 'full' => in_array('--full', $argv) ? true : false, 'same-device' => in_array('--same-device', $argv) ? true : false, 'format' => null, ); // Reading format foreach ($argv as $val) { if (preg_match('#^--format(=["\']?(.+)["\']?)?#', $val, $matches)) { $settings['format'] = isset($matches[2]) ? $matches[2] : false; break; } } // Get targets $targets = array(); foreach ($argv as $key => $val) { // Ignore first argument (script) and options if ($key == 0 || preg_match('#^--?\w+#', $val)) { continue; } $targets[] = $val; } // Make target an absolute path to get phar working $absPattern = PHP_OS === 'WINNT' ? '#^[A-Z]:\\\\#i' : '#^/#'; foreach ($targets as $key => $target) { if ($target !== null && !preg_match($absPattern, $target)) { $targets[$key] = rtrim(getcwd(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$target; } } // Format help if ($settings['format'] === false) { echo "Format tokens :\n\n"; foreach (CliReporter::$propMapping as $key => $val) { echo " ".str_pad($key, 4)." ".$val."\n"; } die(2); } // Help if ($settings['help']) { echo $usage; die(); } // Version if ($settings['version']) { echo "dirscan ".DIRSCAN_VERSION."\n"; die(); } // Target check if (empty($targets)) { fwrite(STDERR, "No target".PHP_EOL); echo $usage; die(2); } $badTarget = 0; foreach ($targets as $key => $target) { if (!is_dir($target)) { fwrite(STDERR, "TARGET".(!empty($target) ? ' ('.$target.') ' : ' ')."is not a directory".PHP_EOL); $badTarget++; } } if ($badTarget > 0) { die(1); } // Scan $reporter = new CliReporter($settings); $reporter->header($targets, $argv); $scanner = new DirScan($settings, $reporter); foreach ($targets as $key => $target) { $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()) { // Get node metadata $stat = $this->stat($path); if ($stat === false) { $msg = "lstat failed on `".$path."`"; call_user_func(array($this->reporter, 'error'), $msg, self::ERR_DIR_LOOP); return; } 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 directory 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 (false !== ($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 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']." (path: ".$path.", uniquepath: ".$uniquepath.")"; call_user_func(array($this->reporter, 'error'), $msg, self::ERR_READLINK); } else { $result['target'] = $target; } } 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) && !is_link($path)) { return false; } // Special handling of volume roots on Windows if (PHP_OS === 'WINNT' && preg_match('#^\w:\\\\$#', $path)) { return $path; } $info = pathinfo($path); $parent = realpath($info['dirname']); $uniquepath = rtrim($parent, DIRECTORY_SEPARATOR).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 Print human readble date/time beside unix timestamp (default false) protected $perms; // bool Report file permissions (default false) protected $format; // string Custom output format protected $settings; // array Raw report settings /** * Short name for file types */ public static $typeMapping = array( 'dir' => 'd', 'file' => 'f', 'link' => 'l', ); /** * Properties mapping for custom format */ public static $propMapping = array( '%u' => 'Unique path', '%t' => 'Type', '%s' => 'Size', '%c' => 'ctime', '%C' => 'Change time', '%m' => 'mtime', '%M' => 'Modify time', '%a' => 'atime', '%A' => 'Access time', '%p' => 'Permissions', '%o' => 'UID', '%O' => 'Owner', '%g' => 'GID', '%G' => 'Group', '%i' => 'Inode', '%e' => 'Extended', ); /** * 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->perms = isset($settings['perms']) ? $settings['perms'] : false; $this->format = isset($settings['format']) ? $settings['format'] : null; $this->settings = $settings; // Use format for full report if ($settings['full'] === true) { $this->format = implode(' ', array_keys(self::$propMapping)); } } /** * Print report header * * @param array $targets List of target directories * @param array $argv Command line arguments */ public function header($targets, $argv) { $header = !empty($this->format) ? $this->getRowFormatHeader($this->format) : $this->getRowHeader(); echo "date: ".date('r (U)')."\n"; echo "getenv(TZ): ".getenv('TZ')."\n"; echo getenv('DIRSCAN_CHROOT') ? "DIRSCAN_CHROOT: " . getenv('DIRSCAN_CHROOT') . "\n" : ""; echo getenv('DIRSCAN_PWD') ? "DIRSCAN_PWD: " . getenv('DIRSCAN_PWD') . "\n" : ""; echo "date_default_timezone_get: ".date_default_timezone_get()."\n"; if (defined('DIRSCAN_VERSION')) { echo "dirscan version: ".DIRSCAN_VERSION."\n"; } echo "php version: ".phpversion()."\n"; echo "uname: ".php_uname()."\n"; echo "cwd: ".getcwd()."\n"; echo "settings: ".json_encode($this->settings)."\n"; echo "argv: ".json_encode($argv)."\n"; echo "target".(count($targets) > 1 ? "s" : "").": "."\n"; foreach ($targets as $key => $target) { $targetStat = lstat($target); $targetStat['realpath'] = realpath($target); echo sprintf(" - %s (realpath: %s, device: %s)\n", $target, $targetStat['realpath'], $targetStat['dev']); } echo "=====================================\n"; 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) { $meta = $this->getMetadata($node, $scanner); $row = !empty($this->format) ? $this->getRowFormat($this->format, $node, $meta) : $this->getRow($node, $meta); echo implode("\t", $row)."\n"; } /** * Return metadata from a node * * @param array $node Data returned by DirScan::stat * @param DirScan $scanner DirScan object * @return array */ protected function getMetadata($node, DirScan $scanner) { $meta = array( 'type' => isset(self::$typeMapping[$node['type']]) ? self::$typeMapping[$node['type']] : $node['type'], 'perms' => substr(sprintf('%o', $node['mode']), -4), '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'])) { $meta['extended']['target'] = $node['target']; } if ($node['dev'] != $scanner->startDevice) { $meta['extended']['device'] = $node['dev']; } return $meta; } /** * Return the header row array * * @return array */ protected function getRowHeader() { $header = array( 'Unique path', 'Type', 'Size', ); if ($this->perms) { $header[] = 'Permissions'; } $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'; return $header; } /** * Return the header row array, using custom format * * @param string $format A format string used as row template * @return array */ protected function getRowFormatHeader($format) { $header = preg_split('# #', $format); foreach ($header as $key => $val) { $header[$key] = strtr($val, self::$propMapping); } return $header; } /** * Return the row array for a node * * @param array $node Data returned by DirScan::stat * @param array $meta Data returned by self::getMetadata * @return array */ protected function getRow($node, $meta) { $row = array( self::getPath($node), $meta['type'], $node['size'], ); if ($this->perms) { $row[] = $meta['perms']; } $row[] = $node['ctime']; if ($this->htime) { $row[] = $meta['hctime']; } $row[] = $node['mtime']; if ($this->htime) { $row[] = $meta['hmtime']; } if ($this->access) { $row[] = $node['atime']; if ($this->htime) { $row[] = $meta['hatime']; } } if (!empty($meta['extended'])) { $row[] = json_encode($meta['extended']); } return $row; } /** * Return the row array for a node, using custom format * * @param string $format A format string used as row template * @param array $node Data returned by DirScan::stat * @param array $meta Data returned by self::getMetadata * @return array */ protected function getRowFormat($format, $node, $meta) { $statMapping = array( '%u' => self::getPath($node), '%t' => $meta['type'], '%s' => $node['size'], '%c' => $node['ctime'], '%C' => $meta['hctime'], '%m' => $node['mtime'], '%M' => $meta['hmtime'], '%a' => $node['atime'], '%A' => $meta['hatime'], '%p' => $meta['perms'], '%o' => $node['uid'], '%O' => $node['owner'], '%g' => $node['gid'], '%G' => $node['group'], '%i' => $node['ino'], '%e' => empty($meta['extended']) ? '' : json_encode($meta['extended']), ); $row = preg_split('# #', $format); foreach ($row as $key => $val) { $row[$key] = strtr($val, $statMapping); } // Trim array $keys = array_reverse(array_keys($row)); foreach ($keys as $key) { if (empty($row[$key])) { unset($row[$key]); continue; } break; } return $row; } /** * Returns either node uniquepath, or path as failover if uniquepath is not available * * @param array $node Data returned by DirScan::stat * @return string */ protected static function getPath($node) { return !empty($node['uniquepath']) ? $node['uniquepath'] : $node['path'].' *UNIQUEPATH_FAILOVER'; } /** * Print error messages to stderr * * @param string $msg Error message * @param int $code Error code */ public function error($msg, $code = null) { fwrite(STDERR, "[dirscan] ".$msg.PHP_EOL); } }