123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548 |
- <?php
- /**
- * @package Grav\Common\Filesystem
- *
- * @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
- * @license MIT License; see LICENSE file for details.
- */
- namespace Grav\Common\Filesystem;
- use DirectoryIterator;
- use Exception;
- use FilesystemIterator;
- use Grav\Common\Grav;
- use RecursiveDirectoryIterator;
- use RecursiveIteratorIterator;
- use RegexIterator;
- use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
- use RuntimeException;
- use function count;
- use function dirname;
- use function is_callable;
- /**
- * Class Folder
- * @package Grav\Common\Filesystem
- */
- abstract class Folder
- {
- /**
- * Recursively find the last modified time under given path.
- *
- * @param array $paths
- * @return int
- */
- public static function lastModifiedFolder(array $paths): int
- {
- $last_modified = 0;
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- $flags = RecursiveDirectoryIterator::SKIP_DOTS;
- foreach ($paths as $path) {
- if (!file_exists($path)) {
- return 0;
- }
- if ($locator->isStream($path)) {
- $directory = $locator->getRecursiveIterator($path, $flags);
- } else {
- $directory = new RecursiveDirectoryIterator($path, $flags);
- }
- $filter = new RecursiveFolderFilterIterator($directory);
- $iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST);
- foreach ($iterator as $dir) {
- $dir_modified = $dir->getMTime();
- if ($dir_modified > $last_modified) {
- $last_modified = $dir_modified;
- }
- }
- }
- return $last_modified;
- }
- /**
- * Recursively find the last modified time under given path by file.
- *
- * @param array $paths
- * @param string $extensions which files to search for specifically
- * @return int
- */
- public static function lastModifiedFile(array $paths, $extensions = 'md|yaml'): int
- {
- $last_modified = 0;
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- $flags = RecursiveDirectoryIterator::SKIP_DOTS;
- foreach($paths as $path) {
- if (!file_exists($path)) {
- return 0;
- }
- if ($locator->isStream($path)) {
- $directory = $locator->getRecursiveIterator($path, $flags);
- } else {
- $directory = new RecursiveDirectoryIterator($path, $flags);
- }
- $recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
- $iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
- /** @var RecursiveDirectoryIterator $file */
- foreach ($iterator as $file) {
- try {
- $file_modified = $file->getMTime();
- if ($file_modified > $last_modified) {
- $last_modified = $file_modified;
- }
- } catch (Exception $e) {
- Grav::instance()['log']->error('Could not process file: ' . $e->getMessage());
- }
- }
- }
- return $last_modified;
- }
- /**
- * Recursively md5 hash all files in a path
- *
- * @param array $paths
- * @return string
- */
- public static function hashAllFiles(array $paths): string
- {
- $files = [];
- foreach ($paths as $path) {
- if (file_exists($path)) {
- $flags = RecursiveDirectoryIterator::SKIP_DOTS;
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- if ($locator->isStream($path)) {
- $directory = $locator->getRecursiveIterator($path, $flags);
- } else {
- $directory = new RecursiveDirectoryIterator($path, $flags);
- }
- $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
- foreach ($iterator as $file) {
- $files[] = $file->getPathname() . '?'. $file->getMTime();
- }
- }
- }
- return md5(serialize($files));
- }
- /**
- * Get relative path between target and base path. If path isn't relative, return full path.
- *
- * @param string $path
- * @param string $base
- * @return string
- */
- public static function getRelativePath($path, $base = GRAV_ROOT)
- {
- if ($base) {
- $base = preg_replace('![\\\/]+!', '/', $base);
- $path = preg_replace('![\\\/]+!', '/', $path);
- if (strpos($path, $base) === 0) {
- $path = ltrim(substr($path, strlen($base)), '/');
- }
- }
- return $path;
- }
- /**
- * Get relative path between target and base path. If path isn't relative, return full path.
- *
- * @param string $path
- * @param string $base
- * @return string
- */
- public static function getRelativePathDotDot($path, $base)
- {
- // Normalize paths.
- $base = preg_replace('![\\\/]+!', '/', $base);
- $path = preg_replace('![\\\/]+!', '/', $path);
- if ($path === $base) {
- return '';
- }
- $baseParts = explode('/', ltrim($base, '/'));
- $pathParts = explode('/', ltrim($path, '/'));
- array_pop($baseParts);
- $lastPart = array_pop($pathParts);
- foreach ($baseParts as $i => $directory) {
- if (isset($pathParts[$i]) && $pathParts[$i] === $directory) {
- unset($baseParts[$i], $pathParts[$i]);
- } else {
- break;
- }
- }
- $pathParts[] = $lastPart;
- $path = str_repeat('../', count($baseParts)) . implode('/', $pathParts);
- return '' === $path
- || strpos($path, '/') === 0
- || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
- ? "./$path" : $path;
- }
- /**
- * Shift first directory out of the path.
- *
- * @param string $path
- * @return string|null
- */
- public static function shift(&$path)
- {
- $parts = explode('/', trim($path, '/'), 2);
- $result = array_shift($parts);
- $path = array_shift($parts);
- return $result ?: null;
- }
- /**
- * Return recursive list of all files and directories under given path.
- *
- * @param string $path
- * @param array $params
- * @return array
- * @throws RuntimeException
- */
- public static function all($path, array $params = [])
- {
- if (!$path) {
- throw new RuntimeException("Path doesn't exist.");
- }
- if (!file_exists($path)) {
- return [];
- }
- $compare = isset($params['compare']) ? 'get' . $params['compare'] : null;
- $pattern = $params['pattern'] ?? null;
- $filters = $params['filters'] ?? null;
- $recursive = $params['recursive'] ?? true;
- $levels = $params['levels'] ?? -1;
- $key = isset($params['key']) ? 'get' . $params['key'] : null;
- $value = 'get' . ($params['value'] ?? ($recursive ? 'SubPathname' : 'Filename'));
- $folders = $params['folders'] ?? true;
- $files = $params['files'] ?? true;
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- if ($recursive) {
- $flags = RecursiveDirectoryIterator::SKIP_DOTS + FilesystemIterator::UNIX_PATHS
- + FilesystemIterator::CURRENT_AS_SELF + FilesystemIterator::FOLLOW_SYMLINKS;
- if ($locator->isStream($path)) {
- $directory = $locator->getRecursiveIterator($path, $flags);
- } else {
- $directory = new RecursiveDirectoryIterator($path, $flags);
- }
- $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
- $iterator->setMaxDepth(max($levels, -1));
- } else {
- if ($locator->isStream($path)) {
- $iterator = $locator->getIterator($path);
- } else {
- $iterator = new FilesystemIterator($path);
- }
- }
- $results = [];
- /** @var RecursiveDirectoryIterator $file */
- foreach ($iterator as $file) {
- // Ignore hidden files.
- if (strpos($file->getFilename(), '.') === 0 && $file->isFile()) {
- continue;
- }
- if (!$folders && $file->isDir()) {
- continue;
- }
- if (!$files && $file->isFile()) {
- continue;
- }
- if ($compare && $pattern && !preg_match($pattern, $file->{$compare}())) {
- continue;
- }
- $fileKey = $key ? $file->{$key}() : null;
- $filePath = $file->{$value}();
- if ($filters) {
- if (isset($filters['key'])) {
- $pre = !empty($filters['pre-key']) ? $filters['pre-key'] : '';
- $fileKey = $pre . preg_replace($filters['key'], '', $fileKey);
- }
- if (isset($filters['value'])) {
- $filter = $filters['value'];
- if (is_callable($filter)) {
- $filePath = $filter($file);
- } else {
- $filePath = preg_replace($filter, '', $filePath);
- }
- }
- }
- if ($fileKey !== null) {
- $results[$fileKey] = $filePath;
- } else {
- $results[] = $filePath;
- }
- }
- return $results;
- }
- /**
- * Recursively copy directory in filesystem.
- *
- * @param string $source
- * @param string $target
- * @param string|null $ignore Ignore files matching pattern (regular expression).
- * @return void
- * @throws RuntimeException
- */
- public static function copy($source, $target, $ignore = null)
- {
- $source = rtrim($source, '\\/');
- $target = rtrim($target, '\\/');
- if (!is_dir($source)) {
- throw new RuntimeException('Cannot copy non-existing folder.');
- }
- // Make sure that path to the target exists before copying.
- self::create($target);
- $success = true;
- // Go through all sub-directories and copy everything.
- $files = self::all($source);
- foreach ($files as $file) {
- if ($ignore && preg_match($ignore, $file)) {
- continue;
- }
- $src = $source .'/'. $file;
- $dst = $target .'/'. $file;
- if (is_dir($src)) {
- // Create current directory (if it doesn't exist).
- if (!is_dir($dst)) {
- $success &= @mkdir($dst, 0777, true);
- }
- } else {
- // Or copy current file.
- $success &= @copy($src, $dst);
- }
- }
- if (!$success) {
- $error = error_get_last();
- throw new RuntimeException($error['message'] ?? 'Unknown error');
- }
- // Make sure that the change will be detected when caching.
- @touch(dirname($target));
- }
- /**
- * Move directory in filesystem.
- *
- * @param string $source
- * @param string $target
- * @return void
- * @throws RuntimeException
- */
- public static function move($source, $target)
- {
- if (!file_exists($source) || !is_dir($source)) {
- // Rename fails if source folder does not exist.
- throw new RuntimeException('Cannot move non-existing folder.');
- }
- // Don't do anything if the source is the same as the new target
- if ($source === $target) {
- return;
- }
- if (strpos($target, $source . '/') === 0) {
- throw new RuntimeException('Cannot move folder to itself');
- }
- if (file_exists($target)) {
- // Rename fails if target folder exists.
- throw new RuntimeException('Cannot move files to existing folder/file.');
- }
- // Make sure that path to the target exists before moving.
- self::create(dirname($target));
- // Silence warnings (chmod failed etc).
- @rename($source, $target);
- // Rename function can fail while still succeeding, so let's check if the folder exists.
- if (is_dir($source)) {
- // Rename doesn't support moving folders across filesystems. Use copy instead.
- self::copy($source, $target);
- self::delete($source);
- }
- // Make sure that the change will be detected when caching.
- @touch(dirname($source));
- @touch(dirname($target));
- @touch($target);
- }
- /**
- * Recursively delete directory from filesystem.
- *
- * @param string $target
- * @param bool $include_target
- * @return bool
- * @throws RuntimeException
- */
- public static function delete($target, $include_target = true)
- {
- if (!is_dir($target)) {
- return false;
- }
- $success = self::doDelete($target, $include_target);
- if (!$success) {
- $error = error_get_last();
- throw new RuntimeException($error['message'] ?? 'Unknown error');
- }
- // Make sure that the change will be detected when caching.
- if ($include_target) {
- @touch(dirname($target));
- } else {
- @touch($target);
- }
- return $success;
- }
- /**
- * @param string $folder
- * @return void
- * @throws RuntimeException
- */
- public static function mkdir($folder)
- {
- self::create($folder);
- }
- /**
- * @param string $folder
- * @return void
- * @throws RuntimeException
- */
- public static function create($folder)
- {
- // Silence error for open_basedir; should fail in mkdir instead.
- if (@is_dir($folder)) {
- return;
- }
- $success = @mkdir($folder, 0777, true);
- if (!$success) {
- // Take yet another look, make sure that the folder doesn't exist.
- clearstatcache(true, $folder);
- if (!@is_dir($folder)) {
- throw new RuntimeException(sprintf('Unable to create directory: %s', $folder));
- }
- }
- }
- /**
- * Recursive copy of one directory to another
- *
- * @param string $src
- * @param string $dest
- * @return bool
- * @throws RuntimeException
- */
- public static function rcopy($src, $dest)
- {
- // If the src is not a directory do a simple file copy
- if (!is_dir($src)) {
- copy($src, $dest);
- return true;
- }
- // If the destination directory does not exist create it
- if (!is_dir($dest)) {
- static::create($dest);
- }
- // Open the source directory to read in files
- $i = new DirectoryIterator($src);
- foreach ($i as $f) {
- if ($f->isFile()) {
- copy($f->getRealPath(), "{$dest}/" . $f->getFilename());
- } else {
- if (!$f->isDot() && $f->isDir()) {
- static::rcopy($f->getRealPath(), "{$dest}/{$f}");
- }
- }
- }
- return true;
- }
- /**
- * Does a directory contain children
- *
- * @param string $directory
- * @return int|false
- */
- public static function countChildren($directory)
- {
- if (!is_dir($directory)) {
- return false;
- }
- $directories = glob($directory . '/*', GLOB_ONLYDIR);
- return $directories ? count($directories) : false;
- }
- /**
- * @param string $folder
- * @param bool $include_target
- * @return bool
- * @internal
- */
- protected static function doDelete($folder, $include_target = true)
- {
- // Special case for symbolic links.
- if ($include_target && is_link($folder)) {
- return @unlink($folder);
- }
- // Go through all items in filesystem and recursively remove everything.
- $files = scandir($folder, SCANDIR_SORT_NONE);
- $files = $files ? array_diff($files, ['.', '..']) : [];
- foreach ($files as $file) {
- $path = "{$folder}/{$file}";
- is_dir($path) ? self::doDelete($path) : @unlink($path);
- }
- return $include_target ? @rmdir($folder) : true;
- }
- }
|