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