From 3229d2371e9391111792c23567ef9026258d7712 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 11 Jun 2021 13:56:39 -0400 Subject: [PATCH] Upgrades and improvements to $files->find() method and add $files->exists() --- wire/core/WireFileTools.php | 177 ++++++++++++++++++++++++++++++++---- 1 file changed, 161 insertions(+), 16 deletions(-) diff --git a/wire/core/WireFileTools.php b/wire/core/WireFileTools.php index df9ee7a2..38a2e477 100644 --- a/wire/core/WireFileTools.php +++ b/wire/core/WireFileTools.php @@ -444,6 +444,111 @@ class WireFileTools extends Wire { $options['copy'] = true; return $this->rename($oldName, $newName, $options); } + + /** + * Does the given file/link/dir exist? + * + * Thie method accepts an `$options` argument that can be specified as an array + * or a string (space or comma separated). The examples here demonstrate usage as + * a string since it is the simplest for readability. + * + * - This function may return false for symlinks pointing to non-existing + * files, unless you specify `link` as the `type`. + * - Specifying `false` for the `readable` or `writable` argument disables the + * option from being used, it doesn’t perform a NOT condition. + * - The `writable` option may also be written as `writeable`, if preferred. + * + * ~~~~~ + * // 1. check if exists + * $exists = $files->exists('/path/file.ext'); + * + * // 2. check if exists and is readable (or writable) + * $exists = $files->exists('/path/file.ext', 'readable'); + * $exists = $files->exists('/path/file.ext', 'writable'); + * + * // 3. check if exists and is file, link or dir + * $exists = $files->exists('/path/file.ext', 'file'); + * $exists = $files->exists('/path/file.ext', 'link'); + * $exists = $files->exists('/path/file.ext', 'dir'); + * + * // 4. check if exists and is writable file or dir + * $exists = $files->exists('/path/file.ext', 'writable file'); + * $exists = $files->exists('/path/dir/', 'writable dir'); + * + * // 5. check if exists and is readable and writable file + * $exists = $files->exists('/path/file.ext', 'readable writable file'); + * ~~~~~ + * + * @param string $filename + * @param array|string $options Can be specified as array or string: + * - `type` (string): Verify it is of type: 'file', 'link', 'dir' (default='') + * - `readable` (bool): Verify it is readable? (default=false) + * - `writable` (bool): Also verify the file is writable? (default=false) + * - `writeable` (bool): Alias of writable (default=false) + * - When specified as string, you can use any combination of the words: + * `readable, writable, file, link, dir` (separated by space or comma). + * @return bool + * @throws WireException if given invalid or unrecognized $options + * @since 3.0.180 + * + * + */ + public function exists($filename, $options = '') { + + $defaults = array( + 'type' => '', + 'readable' => false, + 'writable' => false, + 'writeable' => false, // alias of writable + ); + + if($options === '') { + $options = $defaults; + + } else if(is_array($options)) { + $options = array_merge($defaults, $options); + if(!empty($options['type'])) $options['type'] = strtolower(trim($options['type'])); + + } else if(is_string($options)) { + $types = array('file', 'link', 'dir'); + if(strpos($options, ',') !== false) $options = str_replace(',', ' ', $options); + foreach(explode(' ', $options) as $option) { + $option = strtolower(trim($option)); + if(empty($option)) continue; + if(isset($defaults[$option])) { + // readable, writable + $defaults[$option] = true; + } else if(in_array($option, $types, true)) { + // file, dir, link + if(empty($defaults['type'])) $defaults['type'] = $option; + } else { + throw new WireException("Unrecognized option: $option"); + } + } + $options = $defaults; + + } else { + throw new WireException('Invalid $options argument'); + } + + if($options['readable'] && !is_readable($filename)) { + $exists = false; + } else if(($options['writable'] || $options['writeable']) && !is_writable($filename)) { + $exists = false; + } else if($options['type'] === '') { + $exists = $options['readable'] ? true : file_exists($filename); + } else if($options['type'] === 'file') { + $exists = is_file($filename); + } else if($options['type'] === 'link') { + $exists = is_link($filename); + } else if($options['type'] === 'dir') { + $exists = is_dir($filename); + } else { + throw new WireException("Unrecognized 'type' option: $options[type]"); + } + + return $exists; + } /** * Allow path or filename to to be used for file manipulation actions? @@ -588,10 +693,15 @@ class WireFileTools extends Wire { * * @param string $path Path to start from (required). * @param array $options Options to affect what is returned (optional): - * - `recursive` (int): How many levels of subdirectories this method should descend into (default=10). - * - `extensions` (array): Only include files having these extensions, or omit to include all (default=[]). + * - `recursive` (int|bool): How many levels of subdirectories this method should descend into beyond the 1 given. + * Specify 1 to remain at the one directory level given, or 2+ to descend into subdirectories. (default=10) + * In 3.0.180+ you may also specify true for no limit, or false to disable descending into any subdirectories. + * - `extensions` (array|string): Only include files having these extensions, or omit to include all (default=[]). + * In 3.0.180+ the extensions argument may also be a string (space or comma separated). * - `excludeDirNames` (array): Do not descend into directories having these names (default=[]). * - `excludeHidden` (bool): Exclude hidden files? (default=false). + * - `allowDirs` (bool): Allow directories in returned files (except for '.' and '..')? Note that returned + * directories have a trailing slash. (default=false) 3.0.180+ * - `returnRelative` (bool): Make returned array have filenames relative to given start $path? (default=false) * @return array Flat array of filenames * @since 3.0.96 @@ -605,6 +715,7 @@ class WireFileTools extends Wire { 'excludeExtensions' => array(), 'excludeDirNames' => array(), 'excludeHidden' => false, + 'allowDirs' => false, 'returnRelative' => false, ); @@ -612,48 +723,82 @@ class WireFileTools extends Wire { if(!is_dir($path) || !is_readable($path)) return array(); $options = array_merge($defaults, $options); + if(empty($options['_level'])) { // this is a non-recursive call $options['_startPath'] = $path; $options['_level'] = 0; - foreach($options['extensions'] as $k => $v) $options['extensions'][$k] = strtolower($v); + if(!is_array($options['extensions'])) { + if($options['extensions']) { + $options['extensions'] = preg_replace('/[,;\.\s]+/', ' ', $options['extensions']); + $options['extensions'] = explode(' ', $options['extensions']); + } else { + $options['extensions'] = array(); + } + } + foreach($options['extensions'] as $k => $v) { + $options['extensions'][$k] = strtolower(trim($v)); + } } + $options['_level']++; - if($options['recursive'] && $options['_level'] > $options['recursive']) return array(); - + if($options['recursive'] && $options['recursive'] !== true) { + if($options['_level'] > $options['recursive']) return array(); + } + $dirs = array(); $files = array(); foreach(new \DirectoryIterator($path) as $file) { + if($file->isDot()) continue; + $basename = $file->getBasename(); - if($options['excludeHidden'] && strpos($basename, '.') === 0) continue; + $ext = strtolower($file->getExtension()); + if($file->isDir()) { - if(!in_array($basename, $options['excludeDirNames'])) $dirs[] = $file->getPathname(); + $dir = $this->unixDirName($file->getPathname()); + if($options['allowDirs']) { + if($options['returnRelative'] && strpos($dir, $options['_startPath']) === 0) { + $dir = substr($dir, strlen($options['_startPath'])); + } + $files[$dir] = $dir; + } + if($options['recursive'] === false || $options['recursive'] < 1) continue; + if(!in_array($basename, $options['excludeDirNames'])) $dirs[$dir] = $file->getPathname(); continue; } - $ext = strtolower($file->getExtension()); + + if($options['excludeHidden'] && strpos($basename, '.') === 0) continue; if(!empty($options['extensions']) && !in_array($ext, $options['extensions'])) continue; if(!empty($options['excludeExtensions']) && in_array($ext, $options['excludeExtensions'])) continue; + $filename = $this->unixFileName($file->getPathname()); - // make relative to provided path - if($options['returnRelative']) { - $filename = str_replace($options['_startPath'], '', $filename); + if($options['returnRelative'] && strpos($filename, $options['_startPath']) === 0) { + $filename = substr($filename, strlen($options['_startPath'])); } $files[] = $filename; } - sort($files); - - foreach($dirs as $dir) { + foreach($dirs as $key => $dir) { $_files = $this->find($dir, $options); - foreach($_files as $name) { - $files[] = $name; + if(count($_files)) { + foreach($_files as $name) { + $files[] = $name; + } + } else { + // no files found in directory + if($options['allowDirs'] && count($options['extensions']) && isset($files[$key])) { + // remove directory if it didn't match any files having requested extension + unset($files[$key]); + } } } $options['_level']--; + + if(!$options['_level']) sort($files); return $files; }