1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\Filesystem;
16:
17: use DirectoryIterator;
18: use Exception;
19: use InvalidArgumentException;
20: use RecursiveDirectoryIterator;
21: use RecursiveIteratorIterator;
22:
23: 24: 25: 26: 27: 28:
29: class Folder
30: {
31: 32: 33: 34: 35: 36:
37: const MERGE = 'merge';
38:
39: 40: 41: 42: 43: 44:
45: const OVERWRITE = 'overwrite';
46:
47: 48: 49: 50: 51: 52:
53: const SKIP = 'skip';
54:
55: 56: 57: 58: 59:
60: const SORT_NAME = 'name';
61:
62: 63: 64: 65: 66:
67: const SORT_TIME = 'time';
68:
69: 70: 71: 72: 73:
74: public $path;
75:
76: 77: 78: 79: 80: 81:
82: public $sort = false;
83:
84: 85: 86: 87: 88: 89:
90: public $mode = 0755;
91:
92: 93: 94:
95: protected $_fsorts = [
96: self::SORT_NAME => 'getPathname',
97: self::SORT_TIME => 'getCTime'
98: ];
99:
100: 101: 102: 103: 104:
105: protected $_messages = [];
106:
107: 108: 109: 110: 111:
112: protected $_errors = [];
113:
114: 115: 116: 117: 118:
119: protected $_directories;
120:
121: 122: 123: 124: 125:
126: protected $_files;
127:
128: 129: 130: 131: 132: 133: 134:
135: public function __construct($path = null, $create = false, $mode = false)
136: {
137: if (empty($path)) {
138: $path = TMP;
139: }
140: if ($mode) {
141: $this->mode = $mode;
142: }
143:
144: if (!file_exists($path) && $create === true) {
145: $this->create($path, $this->mode);
146: }
147: if (!Folder::isAbsolute($path)) {
148: $path = realpath($path);
149: }
150: if (!empty($path)) {
151: $this->cd($path);
152: }
153: }
154:
155: 156: 157: 158: 159:
160: public function pwd()
161: {
162: return $this->path;
163: }
164:
165: 166: 167: 168: 169: 170:
171: public function cd($path)
172: {
173: $path = $this->realpath($path);
174: if ($path !== false && is_dir($path)) {
175: return $this->path = $path;
176: }
177:
178: return false;
179: }
180:
181: 182: 183: 184: 185: 186: 187: 188: 189: 190:
191: public function read($sort = self::SORT_NAME, $exceptions = false, $fullPath = false)
192: {
193: $dirs = $files = [];
194:
195: if (!$this->pwd()) {
196: return [$dirs, $files];
197: }
198: if (is_array($exceptions)) {
199: $exceptions = array_flip($exceptions);
200: }
201: $skipHidden = isset($exceptions['.']) || $exceptions === true;
202:
203: try {
204: $iterator = new DirectoryIterator($this->path);
205: } catch (Exception $e) {
206: return [$dirs, $files];
207: }
208:
209: if (!is_bool($sort) && isset($this->_fsorts[$sort])) {
210: $methodName = $this->_fsorts[$sort];
211: } else {
212: $methodName = $this->_fsorts[self::SORT_NAME];
213: }
214:
215: foreach ($iterator as $item) {
216: if ($item->isDot()) {
217: continue;
218: }
219: $name = $item->getFilename();
220: if ($skipHidden && $name[0] === '.' || isset($exceptions[$name])) {
221: continue;
222: }
223: if ($fullPath) {
224: $name = $item->getPathname();
225: }
226:
227: if ($item->isDir()) {
228: $dirs[$item->{$methodName}()][] = $name;
229: } else {
230: $files[$item->{$methodName}()][] = $name;
231: }
232: }
233:
234: if ($sort || $this->sort) {
235: ksort($dirs);
236: ksort($files);
237: }
238:
239: if ($dirs) {
240: $dirs = array_merge(...array_values($dirs));
241: }
242:
243: if ($files) {
244: $files = array_merge(...array_values($files));
245: }
246:
247: return [$dirs, $files];
248: }
249:
250: 251: 252: 253: 254: 255: 256:
257: public function find($regexpPattern = '.*', $sort = false)
258: {
259: list(, $files) = $this->read($sort);
260:
261: return array_values(preg_grep('/^' . $regexpPattern . '$/i', $files));
262: }
263:
264: 265: 266: 267: 268: 269: 270:
271: public function findRecursive($pattern = '.*', $sort = false)
272: {
273: if (!$this->pwd()) {
274: return [];
275: }
276: $startsOn = $this->path;
277: $out = $this->_findRecursive($pattern, $sort);
278: $this->cd($startsOn);
279:
280: return $out;
281: }
282:
283: 284: 285: 286: 287: 288: 289:
290: protected function _findRecursive($pattern, $sort = false)
291: {
292: list($dirs, $files) = $this->read($sort);
293: $found = [];
294:
295: foreach ($files as $file) {
296: if (preg_match('/^' . $pattern . '$/i', $file)) {
297: $found[] = Folder::addPathElement($this->path, $file);
298: }
299: }
300: $start = $this->path;
301:
302: foreach ($dirs as $dir) {
303: $this->cd(Folder::addPathElement($start, $dir));
304: $found = array_merge($found, $this->findRecursive($pattern, $sort));
305: }
306:
307: return $found;
308: }
309:
310: 311: 312: 313: 314: 315:
316: public static function isWindowsPath($path)
317: {
318: return (preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) === '\\\\');
319: }
320:
321: 322: 323: 324: 325: 326:
327: public static function isAbsolute($path)
328: {
329: if (empty($path)) {
330: return false;
331: }
332:
333: return $path[0] === '/' ||
334: preg_match('/^[A-Z]:\\\\/i', $path) ||
335: substr($path, 0, 2) === '\\\\' ||
336: self::isRegisteredStreamWrapper($path);
337: }
338:
339: 340: 341: 342: 343: 344:
345: public static function isRegisteredStreamWrapper($path)
346: {
347: return preg_match('/^[^:\/\/]+?(?=:\/\/)/', $path, $matches) &&
348: in_array($matches[0], stream_get_wrappers());
349: }
350:
351: 352: 353: 354: 355: 356: 357: 358:
359: public static function normalizePath($path)
360: {
361: deprecationWarning('Folder::normalizePath() is deprecated. Use Folder::correctSlashFor() instead.');
362:
363: return Folder::correctSlashFor($path);
364: }
365:
366: 367: 368: 369: 370: 371:
372: public static function normalizeFullPath($path)
373: {
374: $to = Folder::correctSlashFor($path);
375: $from = ($to == '/' ? '\\' : '/');
376:
377: return str_replace($from, $to, $path);
378: }
379:
380: 381: 382: 383: 384: 385:
386: public static function correctSlashFor($path)
387: {
388: return Folder::isWindowsPath($path) ? '\\' : '/';
389: }
390:
391: 392: 393: 394: 395: 396:
397: public static function slashTerm($path)
398: {
399: if (Folder::isSlashTerm($path)) {
400: return $path;
401: }
402:
403: return $path . Folder::correctSlashFor($path);
404: }
405:
406: 407: 408: 409: 410: 411: 412:
413: public static function addPathElement($path, $element)
414: {
415: $element = (array)$element;
416: array_unshift($element, rtrim($path, DIRECTORY_SEPARATOR));
417:
418: return implode(DIRECTORY_SEPARATOR, $element);
419: }
420:
421: 422: 423: 424: 425: 426: 427:
428: public function inCakePath($path = '')
429: {
430: deprecationWarning('Folder::inCakePath() is deprecated. Use Folder::inPath() instead.');
431: $dir = substr(Folder::slashTerm(ROOT), 0, -1);
432: $newdir = $dir . $path;
433:
434: return $this->inPath($newdir);
435: }
436:
437: 438: 439: 440: 441: 442: 443: 444:
445: public function inPath($path, $reverse = false)
446: {
447: if (!Folder::isAbsolute($path)) {
448: throw new InvalidArgumentException('The $path argument is expected to be an absolute path.');
449: }
450:
451: $dir = Folder::slashTerm($path);
452: $current = Folder::slashTerm($this->pwd());
453:
454: if (!$reverse) {
455: $return = preg_match('/^' . preg_quote($dir, '/') . '(.*)/', $current);
456: } else {
457: $return = preg_match('/^' . preg_quote($current, '/') . '(.*)/', $dir);
458: }
459:
460: return (bool)$return;
461: }
462:
463: 464: 465: 466: 467: 468: 469: 470: 471:
472: public function chmod($path, $mode = false, $recursive = true, array $exceptions = [])
473: {
474: if (!$mode) {
475: $mode = $this->mode;
476: }
477:
478: if ($recursive === false && is_dir($path)) {
479:
480: if (@chmod($path, intval($mode, 8))) {
481:
482: $this->_messages[] = sprintf('%s changed to %s', $path, $mode);
483:
484: return true;
485: }
486:
487: $this->_errors[] = sprintf('%s NOT changed to %s', $path, $mode);
488:
489: return false;
490: }
491:
492: if (is_dir($path)) {
493: $paths = $this->tree($path);
494:
495: foreach ($paths as $type) {
496: foreach ($type as $fullpath) {
497: $check = explode(DIRECTORY_SEPARATOR, $fullpath);
498: $count = count($check);
499:
500: if (in_array($check[$count - 1], $exceptions)) {
501: continue;
502: }
503:
504:
505: if (@chmod($fullpath, intval($mode, 8))) {
506:
507: $this->_messages[] = sprintf('%s changed to %s', $fullpath, $mode);
508: } else {
509: $this->_errors[] = sprintf('%s NOT changed to %s', $fullpath, $mode);
510: }
511: }
512: }
513:
514: if (empty($this->_errors)) {
515: return true;
516: }
517: }
518:
519: return false;
520: }
521:
522: 523: 524: 525: 526: 527: 528:
529: public function subdirectories($path = null, $fullPath = true)
530: {
531: if (!$path) {
532: $path = $this->path;
533: }
534: $subdirectories = [];
535:
536: try {
537: $iterator = new DirectoryIterator($path);
538: } catch (Exception $e) {
539: return [];
540: }
541:
542: foreach ($iterator as $item) {
543: if (!$item->isDir() || $item->isDot()) {
544: continue;
545: }
546: $subdirectories[] = $fullPath ? $item->getRealPath() : $item->getFilename();
547: }
548:
549: return $subdirectories;
550: }
551:
552: 553: 554: 555: 556: 557: 558: 559: 560:
561: public function tree($path = null, $exceptions = false, $type = null)
562: {
563: if (!$path) {
564: $path = $this->path;
565: }
566: $files = [];
567: $directories = [$path];
568:
569: if (is_array($exceptions)) {
570: $exceptions = array_flip($exceptions);
571: }
572: $skipHidden = false;
573: if ($exceptions === true) {
574: $skipHidden = true;
575: } elseif (isset($exceptions['.'])) {
576: $skipHidden = true;
577: unset($exceptions['.']);
578: }
579:
580: try {
581: $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::KEY_AS_PATHNAME | RecursiveDirectoryIterator::CURRENT_AS_SELF);
582: $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
583: } catch (Exception $e) {
584: if ($type === null) {
585: return [[], []];
586: }
587:
588: return [];
589: }
590:
591: 592: 593: 594:
595: foreach ($iterator as $itemPath => $fsIterator) {
596: if ($skipHidden) {
597: $subPathName = $fsIterator->getSubPathname();
598: if ($subPathName[0] === '.' || strpos($subPathName, DIRECTORY_SEPARATOR . '.') !== false) {
599: continue;
600: }
601: }
602:
603: $item = $fsIterator->current();
604: if (!empty($exceptions) && isset($exceptions[$item->getFilename()])) {
605: continue;
606: }
607:
608: if ($item->isFile()) {
609: $files[] = $itemPath;
610: } elseif ($item->isDir() && !$item->isDot()) {
611: $directories[] = $itemPath;
612: }
613: }
614: if ($type === null) {
615: return [$directories, $files];
616: }
617: if ($type === 'dir') {
618: return $directories;
619: }
620:
621: return $files;
622: }
623:
624: 625: 626: 627: 628: 629: 630: 631: 632: 633: 634:
635: public function create($pathname, $mode = false)
636: {
637: if (is_dir($pathname) || empty($pathname)) {
638: return true;
639: }
640:
641: if (!self::isAbsolute($pathname)) {
642: $pathname = self::addPathElement($this->pwd(), $pathname);
643: }
644:
645: if (!$mode) {
646: $mode = $this->mode;
647: }
648:
649: if (is_file($pathname)) {
650: $this->_errors[] = sprintf('%s is a file', $pathname);
651:
652: return false;
653: }
654: $pathname = rtrim($pathname, DIRECTORY_SEPARATOR);
655: $nextPathname = substr($pathname, 0, strrpos($pathname, DIRECTORY_SEPARATOR));
656:
657: if ($this->create($nextPathname, $mode)) {
658: if (!file_exists($pathname)) {
659: $old = umask(0);
660: if (mkdir($pathname, $mode, true)) {
661: umask($old);
662: $this->_messages[] = sprintf('%s created', $pathname);
663:
664: return true;
665: }
666: umask($old);
667: $this->_errors[] = sprintf('%s NOT created', $pathname);
668:
669: return false;
670: }
671: }
672:
673: return false;
674: }
675:
676: 677: 678: 679: 680:
681: public function dirsize()
682: {
683: $size = 0;
684: $directory = Folder::slashTerm($this->path);
685: $stack = [$directory];
686: $count = count($stack);
687: for ($i = 0, $j = $count; $i < $j; $i++) {
688: if (is_file($stack[$i])) {
689: $size += filesize($stack[$i]);
690: } elseif (is_dir($stack[$i])) {
691: $dir = dir($stack[$i]);
692: if ($dir) {
693: while (($entry = $dir->read()) !== false) {
694: if ($entry === '.' || $entry === '..') {
695: continue;
696: }
697: $add = $stack[$i] . $entry;
698:
699: if (is_dir($stack[$i] . $entry)) {
700: $add = Folder::slashTerm($add);
701: }
702: $stack[] = $add;
703: }
704: $dir->close();
705: }
706: }
707: $j = count($stack);
708: }
709:
710: return $size;
711: }
712:
713: 714: 715: 716: 717: 718:
719: public function delete($path = null)
720: {
721: if (!$path) {
722: $path = $this->pwd();
723: }
724: if (!$path) {
725: return false;
726: }
727: $path = Folder::slashTerm($path);
728: if (is_dir($path)) {
729: try {
730: $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::CURRENT_AS_SELF);
731: $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::CHILD_FIRST);
732: } catch (Exception $e) {
733: return false;
734: }
735:
736: foreach ($iterator as $item) {
737: $filePath = $item->getPathname();
738: if ($item->isFile() || $item->isLink()) {
739:
740: if (@unlink($filePath)) {
741:
742: $this->_messages[] = sprintf('%s removed', $filePath);
743: } else {
744: $this->_errors[] = sprintf('%s NOT removed', $filePath);
745: }
746: } elseif ($item->isDir() && !$item->isDot()) {
747:
748: if (@rmdir($filePath)) {
749:
750: $this->_messages[] = sprintf('%s removed', $filePath);
751: } else {
752: $this->_errors[] = sprintf('%s NOT removed', $filePath);
753:
754: return false;
755: }
756: }
757: }
758:
759: $path = rtrim($path, DIRECTORY_SEPARATOR);
760:
761: if (@rmdir($path)) {
762:
763: $this->_messages[] = sprintf('%s removed', $path);
764: } else {
765: $this->_errors[] = sprintf('%s NOT removed', $path);
766:
767: return false;
768: }
769: }
770:
771: return true;
772: }
773:
774: 775: 776: 777: 778: 779: 780: 781: 782: 783: 784: 785: 786: 787: 788:
789: public function copy($options)
790: {
791: if (!$this->pwd()) {
792: return false;
793: }
794: $to = null;
795: if (is_string($options)) {
796: $to = $options;
797: $options = [];
798: }
799: $options += [
800: 'to' => $to,
801: 'from' => $this->path,
802: 'mode' => $this->mode,
803: 'skip' => [],
804: 'scheme' => Folder::MERGE,
805: 'recursive' => true
806: ];
807:
808: $fromDir = $options['from'];
809: $toDir = $options['to'];
810: $mode = $options['mode'];
811:
812: if (!$this->cd($fromDir)) {
813: $this->_errors[] = sprintf('%s not found', $fromDir);
814:
815: return false;
816: }
817:
818: if (!is_dir($toDir)) {
819: $this->create($toDir, $mode);
820: }
821:
822: if (!is_writable($toDir)) {
823: $this->_errors[] = sprintf('%s not writable', $toDir);
824:
825: return false;
826: }
827:
828: $exceptions = array_merge(['.', '..', '.svn'], $options['skip']);
829:
830: if ($handle = @opendir($fromDir)) {
831:
832: while (($item = readdir($handle)) !== false) {
833: $to = Folder::addPathElement($toDir, $item);
834: if (($options['scheme'] != Folder::SKIP || !is_dir($to)) && !in_array($item, $exceptions)) {
835: $from = Folder::addPathElement($fromDir, $item);
836: if (is_file($from) && (!is_file($to) || $options['scheme'] != Folder::SKIP)) {
837: if (copy($from, $to)) {
838: chmod($to, intval($mode, 8));
839: touch($to, filemtime($from));
840: $this->_messages[] = sprintf('%s copied to %s', $from, $to);
841: } else {
842: $this->_errors[] = sprintf('%s NOT copied to %s', $from, $to);
843: }
844: }
845:
846: if (is_dir($from) && file_exists($to) && $options['scheme'] === Folder::OVERWRITE) {
847: $this->delete($to);
848: }
849:
850: if (is_dir($from) && $options['recursive'] === false) {
851: continue;
852: }
853:
854: if (is_dir($from) && !file_exists($to)) {
855: $old = umask(0);
856: if (mkdir($to, $mode, true)) {
857: umask($old);
858: $old = umask(0);
859: chmod($to, $mode);
860: umask($old);
861: $this->_messages[] = sprintf('%s created', $to);
862: $options = ['to' => $to, 'from' => $from] + $options;
863: $this->copy($options);
864: } else {
865: $this->_errors[] = sprintf('%s not created', $to);
866: }
867: } elseif (is_dir($from) && $options['scheme'] === Folder::MERGE) {
868: $options = ['to' => $to, 'from' => $from] + $options;
869: $this->copy($options);
870: }
871: }
872: }
873: closedir($handle);
874: } else {
875: return false;
876: }
877:
878: return empty($this->_errors);
879: }
880:
881: 882: 883: 884: 885: 886: 887: 888: 889: 890: 891: 892: 893: 894: 895:
896: public function move($options)
897: {
898: $to = null;
899: if (is_string($options)) {
900: $to = $options;
901: $options = (array)$options;
902: }
903: $options += ['to' => $to, 'from' => $this->path, 'mode' => $this->mode, 'skip' => [], 'recursive' => true];
904:
905: if ($this->copy($options) && $this->delete($options['from'])) {
906: return (bool)$this->cd($options['to']);
907: }
908:
909: return false;
910: }
911:
912: 913: 914: 915: 916: 917:
918: public function messages($reset = true)
919: {
920: $messages = $this->_messages;
921: if ($reset) {
922: $this->_messages = [];
923: }
924:
925: return $messages;
926: }
927:
928: 929: 930: 931: 932: 933:
934: public function errors($reset = true)
935: {
936: $errors = $this->_errors;
937: if ($reset) {
938: $this->_errors = [];
939: }
940:
941: return $errors;
942: }
943:
944: 945: 946: 947: 948: 949:
950: public function realpath($path)
951: {
952: if (strpos($path, '..') === false) {
953: if (!Folder::isAbsolute($path)) {
954: $path = Folder::addPathElement($this->path, $path);
955: }
956:
957: return $path;
958: }
959: $path = str_replace('/', DIRECTORY_SEPARATOR, trim($path));
960: $parts = explode(DIRECTORY_SEPARATOR, $path);
961: $newparts = [];
962: $newpath = '';
963: if ($path[0] === DIRECTORY_SEPARATOR) {
964: $newpath = DIRECTORY_SEPARATOR;
965: }
966:
967: while (($part = array_shift($parts)) !== null) {
968: if ($part === '.' || $part === '') {
969: continue;
970: }
971: if ($part === '..') {
972: if (!empty($newparts)) {
973: array_pop($newparts);
974: continue;
975: }
976:
977: return false;
978: }
979: $newparts[] = $part;
980: }
981: $newpath .= implode(DIRECTORY_SEPARATOR, $newparts);
982:
983: return Folder::slashTerm($newpath);
984: }
985:
986: 987: 988: 989: 990: 991:
992: public static function isSlashTerm($path)
993: {
994: $lastChar = $path[strlen($path) - 1];
995:
996: return $lastChar === '/' || $lastChar === '\\';
997: }
998: }
999: