Folder.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. <?php
  2. /**
  3. * @package Grav\Common\Filesystem
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Filesystem;
  9. use DirectoryIterator;
  10. use Exception;
  11. use FilesystemIterator;
  12. use Grav\Common\Grav;
  13. use RecursiveDirectoryIterator;
  14. use RecursiveIteratorIterator;
  15. use RegexIterator;
  16. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  17. use RuntimeException;
  18. use function count;
  19. use function dirname;
  20. use function is_callable;
  21. /**
  22. * Class Folder
  23. * @package Grav\Common\Filesystem
  24. */
  25. abstract class Folder
  26. {
  27. /**
  28. * Recursively find the last modified time under given path.
  29. *
  30. * @param array $paths
  31. * @return int
  32. */
  33. public static function lastModifiedFolder(array $paths): int
  34. {
  35. $last_modified = 0;
  36. /** @var UniformResourceLocator $locator */
  37. $locator = Grav::instance()['locator'];
  38. $flags = RecursiveDirectoryIterator::SKIP_DOTS;
  39. foreach ($paths as $path) {
  40. if (!file_exists($path)) {
  41. return 0;
  42. }
  43. if ($locator->isStream($path)) {
  44. $directory = $locator->getRecursiveIterator($path, $flags);
  45. } else {
  46. $directory = new RecursiveDirectoryIterator($path, $flags);
  47. }
  48. $filter = new RecursiveFolderFilterIterator($directory);
  49. $iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST);
  50. foreach ($iterator as $dir) {
  51. $dir_modified = $dir->getMTime();
  52. if ($dir_modified > $last_modified) {
  53. $last_modified = $dir_modified;
  54. }
  55. }
  56. }
  57. return $last_modified;
  58. }
  59. /**
  60. * Recursively find the last modified time under given path by file.
  61. *
  62. * @param array $paths
  63. * @param string $extensions which files to search for specifically
  64. * @return int
  65. */
  66. public static function lastModifiedFile(array $paths, $extensions = 'md|yaml'): int
  67. {
  68. $last_modified = 0;
  69. /** @var UniformResourceLocator $locator */
  70. $locator = Grav::instance()['locator'];
  71. $flags = RecursiveDirectoryIterator::SKIP_DOTS;
  72. foreach($paths as $path) {
  73. if (!file_exists($path)) {
  74. return 0;
  75. }
  76. if ($locator->isStream($path)) {
  77. $directory = $locator->getRecursiveIterator($path, $flags);
  78. } else {
  79. $directory = new RecursiveDirectoryIterator($path, $flags);
  80. }
  81. $recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
  82. $iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/i');
  83. /** @var RecursiveDirectoryIterator $file */
  84. foreach ($iterator as $file) {
  85. try {
  86. $file_modified = $file->getMTime();
  87. if ($file_modified > $last_modified) {
  88. $last_modified = $file_modified;
  89. }
  90. } catch (Exception $e) {
  91. Grav::instance()['log']->error('Could not process file: ' . $e->getMessage());
  92. }
  93. }
  94. }
  95. return $last_modified;
  96. }
  97. /**
  98. * Recursively md5 hash all files in a path
  99. *
  100. * @param array $paths
  101. * @return string
  102. */
  103. public static function hashAllFiles(array $paths): string
  104. {
  105. $files = [];
  106. foreach ($paths as $path) {
  107. if (file_exists($path)) {
  108. $flags = RecursiveDirectoryIterator::SKIP_DOTS;
  109. /** @var UniformResourceLocator $locator */
  110. $locator = Grav::instance()['locator'];
  111. if ($locator->isStream($path)) {
  112. $directory = $locator->getRecursiveIterator($path, $flags);
  113. } else {
  114. $directory = new RecursiveDirectoryIterator($path, $flags);
  115. }
  116. $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
  117. foreach ($iterator as $file) {
  118. $files[] = $file->getPathname() . '?'. $file->getMTime();
  119. }
  120. }
  121. }
  122. return md5(serialize($files));
  123. }
  124. /**
  125. * Get relative path between target and base path. If path isn't relative, return full path.
  126. *
  127. * @param string $path
  128. * @param string $base
  129. * @return string
  130. */
  131. public static function getRelativePath($path, $base = GRAV_ROOT)
  132. {
  133. if ($base) {
  134. $base = preg_replace('![\\\/]+!', '/', $base);
  135. $path = preg_replace('![\\\/]+!', '/', $path);
  136. if (strpos($path, $base) === 0) {
  137. $path = ltrim(substr($path, strlen($base)), '/');
  138. }
  139. }
  140. return $path;
  141. }
  142. /**
  143. * Get relative path between target and base path. If path isn't relative, return full path.
  144. *
  145. * @param string $path
  146. * @param string $base
  147. * @return string
  148. */
  149. public static function getRelativePathDotDot($path, $base)
  150. {
  151. // Normalize paths.
  152. $base = preg_replace('![\\\/]+!', '/', $base);
  153. $path = preg_replace('![\\\/]+!', '/', $path);
  154. if ($path === $base) {
  155. return '';
  156. }
  157. $baseParts = explode('/', ltrim($base, '/'));
  158. $pathParts = explode('/', ltrim($path, '/'));
  159. array_pop($baseParts);
  160. $lastPart = array_pop($pathParts);
  161. foreach ($baseParts as $i => $directory) {
  162. if (isset($pathParts[$i]) && $pathParts[$i] === $directory) {
  163. unset($baseParts[$i], $pathParts[$i]);
  164. } else {
  165. break;
  166. }
  167. }
  168. $pathParts[] = $lastPart;
  169. $path = str_repeat('../', count($baseParts)) . implode('/', $pathParts);
  170. return '' === $path
  171. || strpos($path, '/') === 0
  172. || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
  173. ? "./$path" : $path;
  174. }
  175. /**
  176. * Shift first directory out of the path.
  177. *
  178. * @param string $path
  179. * @return string|null
  180. */
  181. public static function shift(&$path)
  182. {
  183. $parts = explode('/', trim($path, '/'), 2);
  184. $result = array_shift($parts);
  185. $path = array_shift($parts);
  186. return $result ?: null;
  187. }
  188. /**
  189. * Return recursive list of all files and directories under given path.
  190. *
  191. * @param string $path
  192. * @param array $params
  193. * @return array
  194. * @throws RuntimeException
  195. */
  196. public static function all($path, array $params = [])
  197. {
  198. if (!$path) {
  199. throw new RuntimeException("Path doesn't exist.");
  200. }
  201. if (!file_exists($path)) {
  202. return [];
  203. }
  204. $compare = isset($params['compare']) ? 'get' . $params['compare'] : null;
  205. $pattern = $params['pattern'] ?? null;
  206. $filters = $params['filters'] ?? null;
  207. $recursive = $params['recursive'] ?? true;
  208. $levels = $params['levels'] ?? -1;
  209. $key = isset($params['key']) ? 'get' . $params['key'] : null;
  210. $value = 'get' . ($params['value'] ?? ($recursive ? 'SubPathname' : 'Filename'));
  211. $folders = $params['folders'] ?? true;
  212. $files = $params['files'] ?? true;
  213. /** @var UniformResourceLocator $locator */
  214. $locator = Grav::instance()['locator'];
  215. if ($recursive) {
  216. $flags = RecursiveDirectoryIterator::SKIP_DOTS + FilesystemIterator::UNIX_PATHS
  217. + FilesystemIterator::CURRENT_AS_SELF + FilesystemIterator::FOLLOW_SYMLINKS;
  218. if ($locator->isStream($path)) {
  219. $directory = $locator->getRecursiveIterator($path, $flags);
  220. } else {
  221. $directory = new RecursiveDirectoryIterator($path, $flags);
  222. }
  223. $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
  224. $iterator->setMaxDepth(max($levels, -1));
  225. } else {
  226. if ($locator->isStream($path)) {
  227. $iterator = $locator->getIterator($path);
  228. } else {
  229. $iterator = new FilesystemIterator($path);
  230. }
  231. }
  232. $results = [];
  233. /** @var RecursiveDirectoryIterator $file */
  234. foreach ($iterator as $file) {
  235. // Ignore hidden files.
  236. if (strpos($file->getFilename(), '.') === 0 && $file->isFile()) {
  237. continue;
  238. }
  239. if (!$folders && $file->isDir()) {
  240. continue;
  241. }
  242. if (!$files && $file->isFile()) {
  243. continue;
  244. }
  245. if ($compare && $pattern && !preg_match($pattern, $file->{$compare}())) {
  246. continue;
  247. }
  248. $fileKey = $key ? $file->{$key}() : null;
  249. $filePath = $file->{$value}();
  250. if ($filters) {
  251. if (isset($filters['key'])) {
  252. $pre = !empty($filters['pre-key']) ? $filters['pre-key'] : '';
  253. $fileKey = $pre . preg_replace($filters['key'], '', $fileKey);
  254. }
  255. if (isset($filters['value'])) {
  256. $filter = $filters['value'];
  257. if (is_callable($filter)) {
  258. $filePath = $filter($file);
  259. } else {
  260. $filePath = preg_replace($filter, '', $filePath);
  261. }
  262. }
  263. }
  264. if ($fileKey !== null) {
  265. $results[$fileKey] = $filePath;
  266. } else {
  267. $results[] = $filePath;
  268. }
  269. }
  270. return $results;
  271. }
  272. /**
  273. * Recursively copy directory in filesystem.
  274. *
  275. * @param string $source
  276. * @param string $target
  277. * @param string|null $ignore Ignore files matching pattern (regular expression).
  278. * @return void
  279. * @throws RuntimeException
  280. */
  281. public static function copy($source, $target, $ignore = null)
  282. {
  283. $source = rtrim($source, '\\/');
  284. $target = rtrim($target, '\\/');
  285. if (!is_dir($source)) {
  286. throw new RuntimeException('Cannot copy non-existing folder.');
  287. }
  288. // Make sure that path to the target exists before copying.
  289. self::create($target);
  290. $success = true;
  291. // Go through all sub-directories and copy everything.
  292. $files = self::all($source);
  293. foreach ($files as $file) {
  294. if ($ignore && preg_match($ignore, $file)) {
  295. continue;
  296. }
  297. $src = $source .'/'. $file;
  298. $dst = $target .'/'. $file;
  299. if (is_dir($src)) {
  300. // Create current directory (if it doesn't exist).
  301. if (!is_dir($dst)) {
  302. $success &= @mkdir($dst, 0777, true);
  303. }
  304. } else {
  305. // Or copy current file.
  306. $success &= @copy($src, $dst);
  307. }
  308. }
  309. if (!$success) {
  310. $error = error_get_last();
  311. throw new RuntimeException($error['message'] ?? 'Unknown error');
  312. }
  313. // Make sure that the change will be detected when caching.
  314. @touch(dirname($target));
  315. }
  316. /**
  317. * Move directory in filesystem.
  318. *
  319. * @param string $source
  320. * @param string $target
  321. * @return void
  322. * @throws RuntimeException
  323. */
  324. public static function move($source, $target)
  325. {
  326. if (!file_exists($source) || !is_dir($source)) {
  327. // Rename fails if source folder does not exist.
  328. throw new RuntimeException('Cannot move non-existing folder.');
  329. }
  330. // Don't do anything if the source is the same as the new target
  331. if ($source === $target) {
  332. return;
  333. }
  334. if (strpos($target, $source . '/') === 0) {
  335. throw new RuntimeException('Cannot move folder to itself');
  336. }
  337. if (file_exists($target)) {
  338. // Rename fails if target folder exists.
  339. throw new RuntimeException('Cannot move files to existing folder/file.');
  340. }
  341. // Make sure that path to the target exists before moving.
  342. self::create(dirname($target));
  343. // Silence warnings (chmod failed etc).
  344. @rename($source, $target);
  345. // Rename function can fail while still succeeding, so let's check if the folder exists.
  346. if (is_dir($source)) {
  347. // Rename doesn't support moving folders across filesystems. Use copy instead.
  348. self::copy($source, $target);
  349. self::delete($source);
  350. }
  351. // Make sure that the change will be detected when caching.
  352. @touch(dirname($source));
  353. @touch(dirname($target));
  354. @touch($target);
  355. }
  356. /**
  357. * Recursively delete directory from filesystem.
  358. *
  359. * @param string $target
  360. * @param bool $include_target
  361. * @return bool
  362. * @throws RuntimeException
  363. */
  364. public static function delete($target, $include_target = true)
  365. {
  366. if (!is_dir($target)) {
  367. return false;
  368. }
  369. $success = self::doDelete($target, $include_target);
  370. if (!$success) {
  371. $error = error_get_last();
  372. throw new RuntimeException($error['message'] ?? 'Unknown error');
  373. }
  374. // Make sure that the change will be detected when caching.
  375. if ($include_target) {
  376. @touch(dirname($target));
  377. } else {
  378. @touch($target);
  379. }
  380. return $success;
  381. }
  382. /**
  383. * @param string $folder
  384. * @return void
  385. * @throws RuntimeException
  386. */
  387. public static function mkdir($folder)
  388. {
  389. self::create($folder);
  390. }
  391. /**
  392. * @param string $folder
  393. * @return void
  394. * @throws RuntimeException
  395. */
  396. public static function create($folder)
  397. {
  398. // Silence error for open_basedir; should fail in mkdir instead.
  399. if (@is_dir($folder)) {
  400. return;
  401. }
  402. $success = @mkdir($folder, 0777, true);
  403. if (!$success) {
  404. // Take yet another look, make sure that the folder doesn't exist.
  405. clearstatcache(true, $folder);
  406. if (!@is_dir($folder)) {
  407. throw new RuntimeException(sprintf('Unable to create directory: %s', $folder));
  408. }
  409. }
  410. }
  411. /**
  412. * Recursive copy of one directory to another
  413. *
  414. * @param string $src
  415. * @param string $dest
  416. * @return bool
  417. * @throws RuntimeException
  418. */
  419. public static function rcopy($src, $dest)
  420. {
  421. // If the src is not a directory do a simple file copy
  422. if (!is_dir($src)) {
  423. copy($src, $dest);
  424. return true;
  425. }
  426. // If the destination directory does not exist create it
  427. if (!is_dir($dest)) {
  428. static::create($dest);
  429. }
  430. // Open the source directory to read in files
  431. $i = new DirectoryIterator($src);
  432. foreach ($i as $f) {
  433. if ($f->isFile()) {
  434. copy($f->getRealPath(), "{$dest}/" . $f->getFilename());
  435. } else {
  436. if (!$f->isDot() && $f->isDir()) {
  437. static::rcopy($f->getRealPath(), "{$dest}/{$f}");
  438. }
  439. }
  440. }
  441. return true;
  442. }
  443. /**
  444. * Does a directory contain children
  445. *
  446. * @param string $directory
  447. * @return int|false
  448. */
  449. public static function countChildren($directory)
  450. {
  451. if (!is_dir($directory)) {
  452. return false;
  453. }
  454. $directories = glob($directory . '/*', GLOB_ONLYDIR);
  455. return $directories ? count($directories) : false;
  456. }
  457. /**
  458. * @param string $folder
  459. * @param bool $include_target
  460. * @return bool
  461. * @internal
  462. */
  463. protected static function doDelete($folder, $include_target = true)
  464. {
  465. // Special case for symbolic links.
  466. if ($include_target && is_link($folder)) {
  467. return @unlink($folder);
  468. }
  469. // Go through all items in filesystem and recursively remove everything.
  470. $files = scandir($folder, SCANDIR_SORT_NONE);
  471. $files = $files ? array_diff($files, ['.', '..']) : [];
  472. foreach ($files as $file) {
  473. $path = "{$folder}/{$file}";
  474. is_dir($path) ? self::doDelete($path) : @unlink($path);
  475. }
  476. return $include_target ? @rmdir($folder) : true;
  477. }
  478. }