Folder.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. <?php
  2. /**
  3. * @package Grav.Common.FileSystem
  4. *
  5. * @copyright Copyright (C) 2015 - 2018 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 $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. $base = preg_replace('![\\\/]+!', '/', $base);
  127. $path = preg_replace('![\\\/]+!', '/', $path);
  128. if ($path === $base) {
  129. return '';
  130. }
  131. $baseParts = explode('/', isset($base[0]) && '/' === $base[0] ? substr($base, 1) : $base);
  132. $pathParts = explode('/', isset($path[0]) && '/' === $path[0] ? substr($path, 1) : $path);
  133. array_pop($baseParts);
  134. $lastPart = array_pop($pathParts);
  135. foreach ($baseParts as $i => $directory) {
  136. if (isset($pathParts[$i]) && $pathParts[$i] === $directory) {
  137. unset($baseParts[$i], $pathParts[$i]);
  138. } else {
  139. break;
  140. }
  141. }
  142. $pathParts[] = $lastPart;
  143. $path = str_repeat('../', count($baseParts)) . implode('/', $pathParts);
  144. return '' === $path
  145. || '/' === $path[0]
  146. || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
  147. ? "./$path" : $path;
  148. }
  149. /**
  150. * Shift first directory out of the path.
  151. *
  152. * @param string $path
  153. * @return string
  154. */
  155. public static function shift(&$path)
  156. {
  157. $parts = explode('/', trim($path, '/'), 2);
  158. $result = array_shift($parts);
  159. $path = array_shift($parts);
  160. return $result ?: null;
  161. }
  162. /**
  163. * Return recursive list of all files and directories under given path.
  164. *
  165. * @param string $path
  166. * @param array $params
  167. * @return array
  168. * @throws \RuntimeException
  169. */
  170. public static function all($path, array $params = [])
  171. {
  172. if ($path === false) {
  173. throw new \RuntimeException("Path doesn't exist.");
  174. }
  175. $compare = isset($params['compare']) ? 'get' . $params['compare'] : null;
  176. $pattern = isset($params['pattern']) ? $params['pattern'] : null;
  177. $filters = isset($params['filters']) ? $params['filters'] : null;
  178. $recursive = isset($params['recursive']) ? $params['recursive'] : true;
  179. $levels = isset($params['levels']) ? $params['levels'] : -1;
  180. $key = isset($params['key']) ? 'get' . $params['key'] : null;
  181. $value = isset($params['value']) ? 'get' . $params['value'] : ($recursive ? 'getSubPathname' : 'getFilename');
  182. $folders = isset($params['folders']) ? $params['folders'] : true;
  183. $files = isset($params['files']) ? $params['files'] : true;
  184. /** @var UniformResourceLocator $locator */
  185. $locator = Grav::instance()['locator'];
  186. if ($recursive) {
  187. $flags = \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS
  188. + \FilesystemIterator::CURRENT_AS_SELF + \FilesystemIterator::FOLLOW_SYMLINKS;
  189. if ($locator->isStream($path)) {
  190. $directory = $locator->getRecursiveIterator($path, $flags);
  191. } else {
  192. $directory = new \RecursiveDirectoryIterator($path, $flags);
  193. }
  194. $iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
  195. $iterator->setMaxDepth(max($levels, -1));
  196. } else {
  197. if ($locator->isStream($path)) {
  198. $iterator = $locator->getIterator($path);
  199. } else {
  200. $iterator = new \FilesystemIterator($path);
  201. }
  202. }
  203. $results = [];
  204. /** @var \RecursiveDirectoryIterator $file */
  205. foreach ($iterator as $file) {
  206. // Ignore hidden files.
  207. if ($file->getFilename()[0] === '.') {
  208. continue;
  209. }
  210. if (!$folders && $file->isDir()) {
  211. continue;
  212. }
  213. if (!$files && $file->isFile()) {
  214. continue;
  215. }
  216. if ($compare && $pattern && !preg_match($pattern, $file->{$compare}())) {
  217. continue;
  218. }
  219. $fileKey = $key ? $file->{$key}() : null;
  220. $filePath = $file->{$value}();
  221. if ($filters) {
  222. if (isset($filters['key'])) {
  223. $pre = !empty($filters['pre-key']) ? $filters['pre-key'] : '';
  224. $fileKey = $pre . preg_replace($filters['key'], '', $fileKey);
  225. }
  226. if (isset($filters['value'])) {
  227. $filter = $filters['value'];
  228. if (is_callable($filter)) {
  229. $filePath = call_user_func($filter, $file);
  230. } else {
  231. $filePath = preg_replace($filter, '', $filePath);
  232. }
  233. }
  234. }
  235. if ($fileKey !== null) {
  236. $results[$fileKey] = $filePath;
  237. } else {
  238. $results[] = $filePath;
  239. }
  240. }
  241. return $results;
  242. }
  243. /**
  244. * Recursively copy directory in filesystem.
  245. *
  246. * @param string $source
  247. * @param string $target
  248. * @param string $ignore Ignore files matching pattern (regular expression).
  249. * @throws \RuntimeException
  250. */
  251. public static function copy($source, $target, $ignore = null)
  252. {
  253. $source = rtrim($source, '\\/');
  254. $target = rtrim($target, '\\/');
  255. if (!is_dir($source)) {
  256. throw new \RuntimeException('Cannot copy non-existing folder.');
  257. }
  258. // Make sure that path to the target exists before copying.
  259. self::create($target);
  260. $success = true;
  261. // Go through all sub-directories and copy everything.
  262. $files = self::all($source);
  263. foreach ($files as $file) {
  264. if ($ignore && preg_match($ignore, $file)) {
  265. continue;
  266. }
  267. $src = $source .'/'. $file;
  268. $dst = $target .'/'. $file;
  269. if (is_dir($src)) {
  270. // Create current directory (if it doesn't exist).
  271. if (!is_dir($dst)) {
  272. $success &= @mkdir($dst, 0777, true);
  273. }
  274. } else {
  275. // Or copy current file.
  276. $success &= @copy($src, $dst);
  277. }
  278. }
  279. if (!$success) {
  280. $error = error_get_last();
  281. throw new \RuntimeException($error['message']);
  282. }
  283. // Make sure that the change will be detected when caching.
  284. @touch(dirname($target));
  285. }
  286. /**
  287. * Move directory in filesystem.
  288. *
  289. * @param string $source
  290. * @param string $target
  291. * @throws \RuntimeException
  292. */
  293. public static function move($source, $target)
  294. {
  295. if (!file_exists($source) || !is_dir($source)) {
  296. // Rename fails if source folder does not exist.
  297. throw new \RuntimeException('Cannot move non-existing folder.');
  298. }
  299. // Don't do anything if the source is the same as the new target
  300. if ($source === $target) {
  301. return;
  302. }
  303. if (file_exists($target)) {
  304. // Rename fails if target folder exists.
  305. throw new \RuntimeException('Cannot move files to existing folder/file.');
  306. }
  307. // Make sure that path to the target exists before moving.
  308. self::create(dirname($target));
  309. // Silence warnings (chmod failed etc).
  310. @rename($source, $target);
  311. // Rename function can fail while still succeeding, so let's check if the folder exists.
  312. if (!file_exists($target) || !is_dir($target)) {
  313. // In some rare cases rename() creates file, not a folder. Get rid of it.
  314. if (file_exists($target)) {
  315. @unlink($target);
  316. }
  317. // Rename doesn't support moving folders across filesystems. Use copy instead.
  318. self::copy($source, $target);
  319. self::delete($source);
  320. }
  321. // Make sure that the change will be detected when caching.
  322. @touch(dirname($source));
  323. @touch(dirname($target));
  324. @touch($target);
  325. }
  326. /**
  327. * Recursively delete directory from filesystem.
  328. *
  329. * @param string $target
  330. * @param bool $include_target
  331. * @return bool
  332. * @throws \RuntimeException
  333. */
  334. public static function delete($target, $include_target = true)
  335. {
  336. if (!is_dir($target)) {
  337. return false;
  338. }
  339. $success = self::doDelete($target, $include_target);
  340. if (!$success) {
  341. $error = error_get_last();
  342. throw new \RuntimeException($error['message']);
  343. }
  344. // Make sure that the change will be detected when caching.
  345. if ($include_target) {
  346. @touch(dirname($target));
  347. } else {
  348. @touch($target);
  349. }
  350. return $success;
  351. }
  352. /**
  353. * @param string $folder
  354. * @throws \RuntimeException
  355. */
  356. public static function mkdir($folder)
  357. {
  358. self::create($folder);
  359. }
  360. /**
  361. * @param string $folder
  362. * @throws \RuntimeException
  363. */
  364. public static function create($folder)
  365. {
  366. if (is_dir($folder)) {
  367. return;
  368. }
  369. $success = @mkdir($folder, 0777, true);
  370. if (!$success) {
  371. $error = error_get_last();
  372. throw new \RuntimeException($error['message']);
  373. }
  374. }
  375. /**
  376. * Recursive copy of one directory to another
  377. *
  378. * @param $src
  379. * @param $dest
  380. *
  381. * @return bool
  382. * @throws \RuntimeException
  383. */
  384. public static function rcopy($src, $dest)
  385. {
  386. // If the src is not a directory do a simple file copy
  387. if (!is_dir($src)) {
  388. copy($src, $dest);
  389. return true;
  390. }
  391. // If the destination directory does not exist create it
  392. if (!is_dir($dest)) {
  393. static::mkdir($dest);
  394. }
  395. // Open the source directory to read in files
  396. $i = new \DirectoryIterator($src);
  397. /** @var \DirectoryIterator $f */
  398. foreach ($i as $f) {
  399. if ($f->isFile()) {
  400. copy($f->getRealPath(), "{$dest}/" . $f->getFilename());
  401. } else {
  402. if (!$f->isDot() && $f->isDir()) {
  403. static::rcopy($f->getRealPath(), "{$dest}/{$f}");
  404. }
  405. }
  406. }
  407. return true;
  408. }
  409. /**
  410. * @param string $folder
  411. * @param bool $include_target
  412. * @return bool
  413. * @internal
  414. */
  415. protected static function doDelete($folder, $include_target = true)
  416. {
  417. // Special case for symbolic links.
  418. if (is_link($folder)) {
  419. return @unlink($folder);
  420. }
  421. // Go through all items in filesystem and recursively remove everything.
  422. $files = array_diff(scandir($folder, SCANDIR_SORT_NONE), array('.', '..'));
  423. foreach ($files as $file) {
  424. $path = "{$folder}/{$file}";
  425. is_dir($path) ? self::doDelete($path) : @unlink($path);
  426. }
  427. return $include_target ? @rmdir($folder) : true;
  428. }
  429. }