Folder.php 16 KB

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