Folder.php 16 KB

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