Folder.php 15 KB

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