Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.78% covered (success)
93.78%
181 / 193
75.68% covered (warning)
75.68%
28 / 37
CRAP
0.00% covered (danger)
0.00%
0 / 1
Directory
93.78% covered (success)
93.78%
181 / 193
75.68% covered (warning)
75.68%
28 / 37
126.64
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 list
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
7
 listByExtension
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
10.16
 index
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
6.29
 addNode
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 createNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 size
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
10
 count
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 delete
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
13
 parent
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 owner
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 permission
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 copy
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 move
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sanitize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 isExisting
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 create
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 rewind
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 current
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 next
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 valid
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 offsetSet
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 offsetExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 offsetUnset
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dirname
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dirpath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 basename
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 copyNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 moveNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetGet
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 getList
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\System\File\Local
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\System\File\Local;
16
17use phpOMS\System\File\ContainerInterface;
18use phpOMS\System\File\DirectoryInterface;
19use phpOMS\System\File\PathException;
20use phpOMS\Utils\StringUtils;
21
22/**
23 * Filesystem class.
24 *
25 * Performing operations on the file system
26 *
27 * @package phpOMS\System\File\Local
28 * @license OMS License 2.0
29 * @link    https://jingga.app
30 * @since   1.0.0
31 */
32final class Directory extends FileAbstract implements DirectoryInterface
33{
34    /**
35     * Directory list filter.
36     *
37     * @var string
38     * @since 1.0.0
39     */
40    private string $filter = '*';
41
42    /**
43     * Directory nodes (files and directories).
44     *
45     * @var array<string, ContainerInterface>
46     * @since 1.0.0
47     */
48    private array $nodes = [];
49
50    /**
51     * Constructor.
52     *
53     * @param string $path       Path
54     * @param string $filter     Filter
55     * @param bool   $initialize Should get initialized during construction
56     *
57     * @since 1.0.0
58     */
59    public function __construct(string $path, string $filter = '*', bool $initialize = true)
60    {
61        $this->filter = \ltrim($filter, '\\/');
62        parent::__construct($path);
63
64        if ($initialize && \is_dir($this->path)) {
65            $this->index();
66        }
67    }
68
69    /**
70     * List all files in directory recursively.
71     *
72     * @param string $path      Path
73     * @param string $filter    Filter
74     * @param bool   $recursive Recursive list
75     *
76     * @return string[] Array of files and directory with relative path to $path
77     *
78     * @since 1.0.0
79     */
80    public static function list(string $path, string $filter = '*', bool $recursive = false) : array
81    {
82        if (!\is_dir($path)) {
83            return [];
84        }
85
86        $list = [];
87        $path = \rtrim($path, '\\/');
88
89        $iterator = $recursive
90            ? new \RecursiveIteratorIterator(
91                new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS),
92                \RecursiveIteratorIterator::SELF_FIRST)
93            : new \DirectoryIterator($path);
94
95        if ($filter !== '*') {
96            $iterator = new \RegexIterator($iterator, '/' . $filter . '/i', \RecursiveRegexIterator::GET_MATCH);
97        }
98
99        /** @var \DirectoryIterator $iterator */
100        foreach ($iterator as $item) {
101            if (!$recursive && $item->isDot()) {
102                continue;
103            }
104
105            $list[] = \substr(\strtr($iterator->getPathname(), '\\', '/'), \strlen($path) + 1);
106        }
107
108        /** @var string[] $list */
109        return $list;
110    }
111
112    /**
113     * List all files by extension directory.
114     *
115     * @param string $path      Path
116     * @param string $extension Extension
117     * @param string $exclude   Pattern to exclude
118     * @param bool   $recursive Recursive
119     *
120     * @return array<array|string>
121     *
122     * @since 1.0.0
123     */
124    public static function listByExtension(string $path, string $extension = '', string $exclude = '', bool $recursive = true) : array
125    {
126        $list = [];
127        $path = \rtrim($path, '\\/');
128
129        if (!\is_dir($path)) {
130            return $list;
131        }
132
133        $iterator = $recursive
134            ? new \RecursiveIteratorIterator(
135                new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS),
136                \RecursiveIteratorIterator::SELF_FIRST)
137            : new \DirectoryIterator($path);
138
139        /** @var \DirectoryIterator $iterator */
140        foreach ($iterator as $item) {
141            if (!$recursive && $item->isDot()) {
142                continue;
143            }
144
145            $subPath = \substr($iterator->getPathname(), \strlen($path) + 1);
146
147            if ((empty($extension) || $item->getExtension() === $extension)
148                && (empty($exclude) || (!(bool) \preg_match('/' . $exclude . '/', $subPath)))
149            ) {
150                $list[] = \strtr($subPath, '\\', '/');
151            }
152        }
153
154        return $list;
155    }
156
157    /**
158     * {@inheritdoc}
159     */
160    public function index() : void
161    {
162        if ($this->isInitialized) {
163            return;
164        }
165
166        parent::index();
167
168        $files = \glob($this->path . \DIRECTORY_SEPARATOR . $this->filter);
169        if ($files === false) {
170            return;
171        }
172
173        foreach ($files as $filename) {
174            if (!StringUtils::endsWith(\trim($filename), '.')) {
175                $file = \is_dir($filename) ? new self($filename, '*', false) : new File($filename);
176
177                $this->addNode($file);
178            }
179        }
180    }
181
182    /**
183     * {@inheritdoc}
184     */
185    public function addNode(ContainerInterface $node) : self
186    {
187        $this->count                      += $node->getCount();
188        $this->size                       += $node->getSize();
189        $this->nodes[$node->getBasename()] = $node;
190
191        $node->createNode();
192
193        return $this;
194    }
195
196    /**
197     * Create node
198     *
199     * @return bool
200     *
201     * @since 1.0.0
202     */
203    public function createNode() : bool
204    {
205        return self::create($this->path, $this->permission, true);
206    }
207
208    /**
209     * {@inheritdoc}
210     */
211    public static function size(string $dir, bool $recursive = true) : int
212    {
213        if (!\is_dir($dir) || !\is_readable($dir)) {
214            return -1;
215        }
216
217        $countSize   = 0;
218        $directories = \scandir($dir);
219
220        if ($directories === false) {
221            return $countSize; // @codeCoverageIgnore
222        }
223
224        foreach ($directories as $key => $filename) {
225            if ($filename === '..' || $filename === '.') {
226                continue;
227            }
228
229            $path = $dir . '/' . $filename;
230            if (\is_dir($path) && $recursive) {
231                $countSize += self::size($path, $recursive);
232            } elseif (\is_file($path)) {
233                $countSize += \filesize($path);
234            }
235        }
236
237        return $countSize;
238    }
239
240    /**
241     * Get amount of sub-resources.
242     *
243     * A file will always return 1 as it doesn't have any sub-resources.
244     *
245     * @param string   $path      Path of the resource
246     * @param bool     $recursive Should count also sub-sub-resources
247     * @param string[] $ignore    Ignore files
248     *
249     * @return int
250     *
251     * @since 1.0.0
252     */
253    public static function count(string $path, bool $recursive = true, array $ignore = []) : int
254    {
255        if (!\is_dir($path)) {
256            return -1;
257        }
258
259        $size     = 0;
260        $files    = \scandir($path);
261        $ignore[] = '.';
262        $ignore[] = '..';
263
264        if ($files === false) {
265            return $size; // @codeCoverageIgnore
266        }
267
268        foreach ($files as $t) {
269            if (\in_array($t, $ignore)) {
270                continue;
271            }
272            if (\is_dir(\rtrim($path, '/') . '/' . $t)) {
273                if ($recursive) {
274                    $size += self::count(\rtrim($path, '/') . '/' . $t, true, $ignore);
275                }
276            } else {
277                ++$size;
278            }
279        }
280
281        return $size;
282    }
283
284    /**
285     * {@inheritdoc}
286     */
287    public static function delete(string $path) : bool
288    {
289        if (empty($path) || !\is_dir($path)) {
290            return false;
291        }
292
293        $counter = 0;
294        $files   = \scandir($path);
295
296        if ($files === false) {
297            return false; // @codeCoverageIgnore
298        }
299
300        do {
301            foreach ($files as $file) {
302                if ($file === '.' || $file === '..') {
303                    continue;
304                }
305
306                if (\is_dir($path . '/' . $file)) {
307                    self::delete($path . '/' . $file);
308                } elseif (\is_writable($path . '/' . $file)) {
309                    \unlink($path . '/' . $file);
310                }
311            }
312
313            ++$counter;
314            $files = \scandir($path);
315        } while ($files !== false && $counter < 3 && \count($files) > 2);
316
317        $files = \scandir($path);
318        if ($files === false || \count($files) > 2) {
319            return false;
320        }
321
322        \rmdir($path);
323
324        return true;
325    }
326
327    /**
328     * {@inheritdoc}
329     */
330    public static function parent(string $path) : string
331    {
332        $path = \explode('/', \strtr($path, '\\', '/'));
333        \array_pop($path);
334
335        return \implode('/', $path);
336    }
337
338    /**
339     * {@inheritdoc}
340     *
341     * @throws PathException
342     */
343    public static function owner(string $path) : int
344    {
345        if (!\is_dir($path)) {
346            throw new PathException($path);
347        }
348
349        return (int) \fileowner($path);
350    }
351
352    /**
353     * {@inheritdoc}
354     */
355    public static function permission(string $path) : int
356    {
357        if (!\is_dir($path)) {
358            return -1;
359        }
360
361        return (int) \fileperms($path);
362    }
363
364    /**
365     * {@inheritdoc}
366     */
367    public static function copy(string $from, string $to, bool $overwrite = false) : bool
368    {
369        if (!\is_dir($from)
370            || (!$overwrite && \is_dir($to))
371        ) {
372            return false;
373        }
374
375        if (!\is_dir($to)) {
376            self::create($to, 0755, true);
377        } elseif ($overwrite) {
378            self::delete($to);
379            self::create($to, 0755, true);
380        }
381
382        $iterator = new \RecursiveIteratorIterator(
383            new \RecursiveDirectoryIterator($from, \RecursiveDirectoryIterator::SKIP_DOTS),
384            \RecursiveIteratorIterator::SELF_FIRST);
385
386        /** @var \DirectoryIterator $item */
387        foreach ($iterator as $item) {
388            /** @var \RecursiveDirectoryIterator $iterator */
389            $subPath = $iterator->getSubPathname();
390
391            if ($item->isDir()) {
392                \mkdir($to . '/' . $subPath);
393            } else {
394                \copy($from . '/' . $subPath, $to . '/' . $subPath);
395            }
396        }
397
398        return true;
399    }
400
401    /**
402     * {@inheritdoc}
403     */
404    public static function move(string $from, string $to, bool $overwrite = false) : bool
405    {
406        if (!\is_dir($from)
407            || (!$overwrite && \is_dir($to))
408        ) {
409            return false;
410        }
411
412        if ($overwrite && \is_dir($to)) {
413            self::delete($to);
414        }
415
416        if (!self::exists(self::parent($to))) {
417            self::create(self::parent($to), 0755, true);
418        }
419
420        \rename($from, $to);
421
422        return true;
423    }
424
425    /**
426     * {@inheritdoc}
427     */
428    public static function exists(string $path) : bool
429    {
430        return \is_dir($path);
431    }
432
433    /**
434     * {@inheritdoc}
435     */
436    public static function sanitize(string $path, string $replace = '', string $invalid = '/[^\w\s\d\.\-_~,;:\[\]\(\]\/]/') : string
437    {
438        return \preg_replace($invalid, $replace, $path) ?? '';
439    }
440
441    /**
442     * {@inheritdoc}
443     */
444    public function getNode(string $name) : ?ContainerInterface
445    {
446        $name = isset($this->nodes[$name]) ? $name : $this->path . '/' . $name;
447
448        if (isset($this->nodes[$name]) && $this->nodes[$name] instanceof self) {
449            $this->nodes[$name]->index();
450        }
451
452        return $this->nodes[$name] ?? null;
453    }
454
455    /**
456     * Check if the child node exists
457     *
458     * @param string $name Child node name. If empty checks if this node exists.
459     *
460     * @return bool
461     *
462     * @since 1.0.0
463     */
464    public function isExisting(string $name = null) : bool
465    {
466        if ($name === null) {
467            return \is_dir($this->path);
468        }
469
470        $name = isset($this->nodes[$name]) ? $name : $this->path . '/' . $name;
471
472        return isset($this->nodes[$name]);
473    }
474
475    /**
476     * Create directory
477     *
478     * @param string $path       Path of the resource
479     * @param int    $permission Permission
480     * @param bool   $recursive  Create recursive in case of subdirectories
481     *
482     * @return bool
483     *
484     * @since 1.0.0
485     */
486    public static function create(string $path, int $permission = 0755, bool $recursive = false) : bool
487    {
488        if (!\is_dir($path)) {
489            if (!$recursive && !\is_dir(self::parent($path))) {
490                return false;
491            }
492
493            try {
494                \mkdir($path, $permission, $recursive);
495            } catch (\Throwable $_) {
496                return false; // @codeCoverageIgnore
497            }
498
499            return true;
500        }
501
502        return false;
503    }
504
505    /**
506     * {@inheritdoc}
507     */
508    public function rewind() : void
509    {
510        \reset($this->nodes);
511    }
512
513    /**
514     * {@inheritdoc}
515     */
516    public function current() : ContainerInterface
517    {
518        $current = \current($this->nodes);
519        if ($current instanceof self) {
520            $current->index();
521        }
522
523        return $current === false ? $this : $current;
524    }
525
526    /**
527     * {@inheritdoc}
528     */
529    public function key() : ?string
530    {
531        return \key($this->nodes);
532    }
533
534    /**
535     * {@inheritdoc}
536     */
537    public function next() : void
538    {
539        $next = \next($this->nodes);
540        if ($next instanceof self) {
541            $next->index();
542        }
543    }
544
545    /**
546     * {@inheritdoc}
547     */
548    public function valid() : bool
549    {
550        $key = \key($this->nodes);
551
552        return ($key !== null && $key !== false);
553    }
554
555    /**
556     * {@inheritdoc}
557     */
558    public function offsetSet(mixed $offset, mixed $value) : void
559    {
560        if (!($value instanceof ContainerInterface)) {
561            return;
562        }
563
564        if ($offset === null || !isset($this->nodes[$offset])) {
565            $this->addNode($value);
566        } else {
567            $this->nodes[$offset]->deleteNode();
568            $this->addNode($value);
569        }
570    }
571
572    /**
573     * {@inheritdoc}
574     */
575    public function offsetExists(mixed $offset) : bool
576    {
577        $offset = isset($this->nodes[$offset]) ? $offset : $this->path . '/' . $offset;
578
579        return isset($this->nodes[$offset]);
580    }
581
582    /**
583     * {@inheritdoc}
584     */
585    public function offsetUnset(mixed $offset) : void
586    {
587        $offset = isset($this->nodes[$offset]) ? $offset : $this->path . '/' . $offset;
588
589        if (isset($this->nodes[$offset])) {
590            $this->nodes[$offset]->deleteNode();
591
592            unset($this->nodes[$offset]);
593        }
594    }
595
596    /**
597     * {@inheritdoc}
598     */
599    public static function name(string $path) : string
600    {
601        return \basename($path);
602    }
603
604    /**
605     * {@inheritdoc}
606     */
607    public static function dirname(string $path) : string
608    {
609        return \basename($path);
610    }
611
612    /**
613     * {@inheritdoc}
614     */
615    public static function dirpath(string $path) : string
616    {
617        return $path;
618    }
619
620    /**
621     * {@inheritdoc}
622     */
623    public static function basename(string $path) : string
624    {
625        return \basename($path);
626    }
627
628    /**
629     * {@inheritdoc}
630     */
631    public function getParent() : ContainerInterface
632    {
633        return new self(self::parent($this->path));
634    }
635
636    /**
637     * {@inheritdoc}
638     */
639    public function copyNode(string $to, bool $overwrite = false) : bool
640    {
641        return self::copy($this->path, $to, $overwrite);
642    }
643
644    /**
645     * {@inheritdoc}
646     */
647    public function moveNode(string $to, bool $overwrite = false) : bool
648    {
649        return self::move($this->path, $to, $overwrite);
650    }
651
652    /**
653     * {@inheritdoc}
654     */
655    public function deleteNode() : bool
656    {
657        // @todo: update parent
658
659        return self::delete($this->path);
660    }
661
662    /**
663     * {@inheritdoc}
664     */
665    public function offsetGet(mixed $offset) : mixed
666    {
667        if (isset($this->nodes[$offset]) && $this->nodes[$offset] instanceof self) {
668            $this->nodes[$offset]->index();
669        }
670
671        return $this->nodes[$offset] ?? null;
672    }
673
674    /**
675     * {@inheritdoc}
676     */
677    public function getList() : array
678    {
679        $pathLength = \strlen($this->path);
680        $content    = [];
681
682        foreach ($this->nodes as $node) {
683            $content[] = \substr($node->getPath(), $pathLength + 1);
684        }
685
686        return $content;
687    }
688}