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