Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 275
0.00% covered (danger)
0.00%
0 / 45
CRAP
0.00% covered (danger)
0.00%
0 / 1
Directory
0.00% covered (danger)
0.00%
0 / 275
0.00% covered (danger)
0.00%
0 / 45
21756
0.00% covered (danger)
0.00%
0 / 1
 ftpConnect
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 index
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 list
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 exists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 create
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
 size
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 count
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 delete
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 parent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 created
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 changed
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 owner
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getOwner
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 parseRawList
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
42
 permission
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getPermission
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 copy
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 get
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 put
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
90
 move
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
 sanitize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 dirname
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 dirpath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 basename
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNode
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 createNode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 addNode
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getParent
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 copyNode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 moveNode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 deleteNode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 rewind
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 current
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 key
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 next
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 valid
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 offsetSet
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 offsetExists
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 offsetUnset
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 offsetGet
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 isExisting
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getList
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 listByExtension
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\System\File\Ftp
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\Ftp;
16
17use phpOMS\System\File\ContainerInterface;
18use phpOMS\System\File\DirectoryInterface;
19use phpOMS\System\File\FileUtils;
20use phpOMS\System\File\Local\Directory as LocalDirectory;
21use phpOMS\System\File\PathException;
22use phpOMS\Uri\HttpUri;
23use phpOMS\Utils\StringUtils;
24
25/**
26 * Filesystem class.
27 *
28 * Performing operations on the file system
29 *
30 * @package phpOMS\System\File\Ftp
31 * @license OMS License 2.0
32 * @link    https://jingga.app
33 * @since   1.0.0
34 */
35class Directory extends FileAbstract implements DirectoryInterface
36{
37    /**
38     * Filter for directory listing
39     *
40     * @var string
41     * @since 1.0.0
42     */
43    //private string $filter = '*';
44
45    /**
46     * Directory nodes (files and directories).
47     *
48     * @var array<string, ContainerInterface>
49     * @since 1.0.0
50     */
51    private array $nodes = [];
52
53    /**
54     * Create ftp connection.
55     *
56     * @param HttpUri $http Uri
57     *
58     * @return null|\FTP\Connection
59     *
60     * @since 1.0.0
61     */
62    public static function ftpConnect(HttpUri $http) : ?\FTP\Connection
63    {
64        $con = \ftp_connect($http->host, $http->port, 10);
65        if ($con === false) {
66            return null;
67        }
68
69        $status = \ftp_login($con, $http->user, $http->pass);
70        if ($status === false) {
71            return null;
72        }
73
74        if ($http->getPath() !== '') {
75            @\ftp_chdir($con, $http->getPath());
76        }
77
78        return $con;
79    }
80
81    /**
82     * Constructor.
83     *
84     * @param HttpUri         $uri        Uri
85     * @param bool            $initialize Should get initialized during construction
86     * @param \FTP\Connection $con        Connection
87     *
88     * @since 1.0.0
89     */
90    public function __construct(HttpUri $uri, bool $initialize = true, \FTP\Connection $con = null)
91    {
92        $this->uri = $uri;
93        $this->con = $con ?? self::ftpConnect($uri);
94
95        //$this->filter = \ltrim($filter, '\\/');
96        parent::__construct($uri->getPath());
97
98        if ($initialize && $this->con !== null && self::exists($this->con, $this->path)) {
99            $this->index();
100        }
101    }
102
103    /**
104     * {@inheritdoc}
105     */
106    public function index() : void
107    {
108        if ($this->isInitialized) {
109            return;
110        }
111
112        $this->isInitialized = true;
113        parent::index();
114
115        if ($this->con === null) {
116            return;
117        }
118
119        $list = self::list($this->con, $this->path);
120
121        foreach ($list as $filename) {
122            if (!StringUtils::endsWith(\trim($filename), '.')) {
123                $uri = clone $this->uri;
124                $uri->setPath($filename);
125
126                $file = \ftp_size($this->con, $filename) === -1 ? new self($uri, false, $this->con) : new File($uri, $this->con);
127
128                $this->addNode($file);
129            }
130        }
131    }
132
133    /**
134     * List all files in directory.
135     *
136     * @param \FTP\Connection $con       FTP connection
137     * @param string          $path      Path
138     * @param string          $filter    Filter
139     * @param bool            $recursive Recursive list
140     *
141     * @return string[]
142     *
143     * @since 1.0.0
144     */
145    public static function list(\FTP\Connection $con, string $path, string $filter = '*', bool $recursive = false) : array
146    {
147        if (!self::exists($con, $path)) {
148            return [];
149        }
150
151        $list     = [];
152        $path     = \rtrim($path, '\\/');
153        $detailed = self::parseRawList($con, $path);
154
155        foreach ($detailed as $key => $item) {
156            if ($item['type'] === 'dir' && $recursive) {
157                $list = \array_merge($list, self::list($con, $key, $filter, $recursive));
158            }
159
160            if ($filter !== '*' && \preg_match($filter, $key) !== 1) {
161                continue;
162            }
163
164            $list[] = $key;
165        }
166
167        /** @var string[] $list */
168        return $list;
169    }
170
171    /**
172     * {@inheritdoc}
173     */
174    public static function exists(\FTP\Connection $con, string $path) : bool
175    {
176        return File::exists($con, $path);
177    }
178
179    /**
180     * Create directory
181     *
182     * @param \FTP\Connection $con        FTP connection
183     * @param string          $path       Path of the resource
184     * @param int             $permission Permission
185     * @param bool            $recursive  Create recursive in case of subdirectories
186     *
187     * @return bool
188     *
189     * @since 1.0.0
190     */
191    public static function create(\FTP\Connection $con, string $path, int $permission = 0755, bool $recursive = false) : bool
192    {
193        if (self::exists($con, $path)) {
194            return false;
195        }
196
197        $parts = \explode('/', $path);
198        if ($parts[0] === '') {
199            $parts[0] = '/';
200        }
201
202        $depth = \count($parts);
203
204        $currentPath = '';
205        foreach ($parts as $key => $part) {
206            $currentPath .= ($currentPath !== '' && $currentPath !== '/' ? '/' : '') . $part;
207
208            if (!self::exists($con, $currentPath)) {
209                if (!$recursive && $key < $depth - 1) {
210                    return false;
211                }
212
213                $status = @\ftp_mkdir($con, $part);
214                if ($status === false) {
215                    return false;
216                }
217
218                \ftp_chmod($con, $permission, $part);
219            }
220
221            \ftp_chdir($con, $part);
222        }
223
224        return self::exists($con, $path);
225    }
226
227    /**
228     * {@inheritdoc}
229     */
230    public static function size(\FTP\Connection $con, string $dir, bool $recursive = true) : int
231    {
232        if (!self::exists($con, $dir)) {
233            return -1;
234        }
235
236        $countSize   = 0;
237        $directories = self::parseRawList($con, $dir);
238
239        foreach ($directories as $key => $filename) {
240            if ($key === '..' || $key === '.') {
241                continue;
242            }
243
244            if ($filename['type'] === 'dir' && $recursive) {
245                $countSize += self::size($con, $key, $recursive);
246            } elseif ($filename['type'] === 'file') {
247                $countSize += \ftp_size($con, $key);
248            }
249        }
250
251        return $countSize;
252    }
253
254    /**
255     * {@inheritdoc}
256     */
257    public static function count(\FTP\Connection $con, string $path, bool $recursive = true, array $ignore = []) : int
258    {
259        if (!self::exists($con, $path)) {
260            return -1;
261        }
262
263        $size     = 0;
264        $files    = self::parseRawList($con, $path);
265        $ignore[] = '.';
266        $ignore[] = '..';
267
268        foreach ($files as $key => $t) {
269            if (\in_array($key, $ignore)) {
270                continue;
271            }
272            if ($t['type'] === 'dir') {
273                if ($recursive) {
274                    $size += self::count($con, $key, true, $ignore);
275                }
276            } else {
277                ++$size;
278            }
279        }
280
281        return $size;
282    }
283
284    /**
285     * {@inheritdoc}
286     */
287    public static function delete(\FTP\Connection $con, string $path) : bool
288    {
289        $path = \rtrim($path, '\\/');
290
291        if (!self::exists($con, $path)) {
292            return false;
293        }
294
295        $list = self::parseRawList($con, $path);
296
297        foreach ($list as $key => $item) {
298            if ($item['type'] === 'dir') {
299                self::delete($con, $key);
300            } else {
301                File::delete($con, $key);
302            }
303        }
304
305        return \ftp_rmdir($con, $path);
306    }
307
308    /**
309     * {@inheritdoc}
310     */
311    public static function parent(string $path) : string
312    {
313        return LocalDirectory::parent($path);
314    }
315
316    /**
317     * {@inheritdoc}
318     */
319    public static function created(\FTP\Connection $con, string $path) : \DateTime
320    {
321        return self::changed($con, $path);
322    }
323
324    /**
325     * {@inheritdoc}
326     *
327     * @throws PathException
328     */
329    public static function changed(\FTP\Connection $con, string $path) : \DateTime
330    {
331        if (!self::exists($con, $path)) {
332            throw new PathException($path);
333        }
334
335        $changed = new \DateTime();
336        $time    = \ftp_mdtm($con, $path);
337
338        $changed->setTimestamp($time === false ? 0 : $time);
339
340        return $changed;
341    }
342
343    /**
344     * {@inheritdoc}
345     *
346     * @throws PathException
347     */
348    public static function owner(\FTP\Connection $con, string $path) : string
349    {
350        if (!self::exists($con, $path)) {
351            throw new PathException($path);
352        }
353
354        return self::parseRawList($con, self::parent($path))[$path]['user'];
355    }
356
357    /**
358     * {@inheritdoc}
359     */
360    public function getOwner() : string
361    {
362        if ($this->con === null) {
363            return '';
364        }
365
366        $this->owner = self::parseRawList($this->con, self::parent($this->path))[$this->path]['user'];
367
368        return $this->owner;
369    }
370
371    /**
372     * Get detailed file/dir list.
373     *
374     * @param \FTP\Connection $con  FTP connection
375     * @param string          $path Path of the resource
376     *
377     * @return array<string, array{permission:int, number:string, user:string, group:string, size:string, month:string, day:string, time:string, type:string}>
378     *
379     * @since 1.0.0
380     */
381    public static function parseRawList(\FTP\Connection $con, string $path) : array
382    {
383        $listData = \ftp_rawlist($con, $path);
384        $names    = \ftp_nlist($con, $path);
385        $data     = [];
386
387        if ($names === false || $listData === false) {
388            return [];
389        }
390
391        foreach ($listData as $key => $item) {
392            $chunks = \preg_split("/\s+/", $item);
393
394            if ($chunks === false) {
395                continue;
396            }
397
398            $e = [
399                'permission' => '',
400                'number'     => '',
401                'user'       => '',
402                'group'      => '',
403                'size'       => '',
404                'month'      => '',
405                'day'        => '',
406                'time'       => '',
407            ];
408
409            list(
410                $e['permission'],
411                $e['number'],
412                $e['user'],
413                $e['group'],
414                $e['size'],
415                $e['month'],
416                $e['day'],
417                $e['time']
418            ) = $chunks;
419
420            $e['permission'] = FileUtils::permissionToOctal(\substr($e['permission'], 1));
421            $e['type']       = $chunks[0][0] === 'd' ? 'dir' : 'file';
422
423            $data[$names[$key]] = $e;
424        }
425
426        /** @var array<string, array{permission:int, number:string, user:string, group:string, size:string, month:string, day:string, time:string, type:string}> */
427        return $data;
428    }
429
430    /**
431     * {@inheritdoc}
432     */
433    public static function permission(\FTP\Connection $con, string $path) : int
434    {
435        if (!self::exists($con, $path)) {
436            return -1;
437        }
438
439        return self::parseRawList($con, self::parent($path))[$path]['permission'];
440    }
441
442    /**
443     * {@inheritdoc}
444     */
445    public function getPermission() : int
446    {
447        if ($this->con === null) {
448            return 0;
449        }
450
451        $this->permission = self::parseRawList($this->con, self::parent($this->path))[$this->path]['permission'];
452
453        return $this->permission;
454    }
455
456    /**
457     * {@inheritdoc}
458     */
459    public static function copy(\FTP\Connection $con, string $from, string $to, bool $overwrite = false) : bool
460    {
461        if (!self::exists($con, $from)
462            || (!$overwrite && self::exists($con, $to))
463        ) {
464            return false;
465        }
466
467        $tempName = \sys_get_temp_dir() . '/' . \uniqid('omsftp_');
468        $status   = @\mkdir($tempName);
469
470        if ($status === false) {
471            return false;
472        }
473
474        $download = self::get($con, $from, $tempName . '/' . self::name($from));
475        if (!$download) {
476            LocalDirectory::delete($tempName);
477
478            return false;
479        }
480
481        $upload = self::put($con, $tempName . '/' . self::name($from), $to);
482        if (!$upload) {
483            LocalDirectory::delete($tempName);
484
485            return false;
486        }
487
488        LocalDirectory::delete($tempName);
489
490        return self::exists($con, $to);
491    }
492
493    /**
494     * Download file.
495     *
496     * @param \FTP\Connection $con  FTP connection
497     * @param string          $from Path of the resource to copy
498     * @param string          $to   Path of the resource to copy to
499     *
500     * @return bool True on success and false on failure
501     *
502     * @since 1.0.0
503     */
504    public static function get(\FTP\Connection $con, string $from, string $to) : bool
505    {
506        if (!self::exists($con, $from)) {
507            return false;
508        }
509
510        if (!\is_dir($to)) {
511            \mkdir($to);
512        }
513
514        $list = self::parseRawList($con, $from);
515        foreach ($list as $key => $item) {
516            if ($item['type'] === 'dir') {
517                self::get($con, $key, $to . '/' . self::name($key));
518            } else {
519                \file_put_contents($to . '/' . self::name($key), File::get($con, $key));
520            }
521        }
522
523        return \is_dir($to);
524    }
525
526    /**
527     * Upload file.
528     *
529     * @param \FTP\Connection $con  FTP connection
530     * @param string          $from Path of the resource to copy
531     * @param string          $to   Path of the resource to copy to
532     *
533     * @return bool True on success and false on failure
534     *
535     * @since 1.0.0
536     */
537    public static function put(\FTP\Connection $con, string $from, string $to) : bool
538    {
539        if (!\is_dir($from)) {
540            return false;
541        }
542
543        if (!self::exists($con, $to)) {
544            self::create($con, $to, 0755, true);
545        }
546
547        $list = \scandir($from);
548        if ($list === false) {
549            return false;
550        }
551
552        foreach ($list as $item) {
553            if ($item === '.' || $item === '..') {
554                continue;
555            }
556
557            $item = $from . '/' . \ltrim($item, '/');
558
559            if (\is_dir($item)) {
560                self::put($con, $item, $to . '/' . self::name($item));
561            } else {
562                $content = \file_get_contents($item);
563
564                if ($content !== false) {
565                    File::put($con, $to . '/' . self::name($item), $content);
566                }
567            }
568        }
569
570        return self::exists($con, $to);
571    }
572
573    /**
574     * Move resource to different location.
575     *
576     * @param \FTP\Connection $con       FTP connection
577     * @param string          $from      Path of the resource to move
578     * @param string          $to        Path of the resource to move to
579     * @param bool            $overwrite Overwrite/replace existing file
580     *
581     * @return bool True on success and false on failure
582     *
583     * @since 1.0.0
584     */
585    public static function move(\FTP\Connection $con, string $from, string $to, bool $overwrite = false) : bool
586    {
587        if (!self::exists($con, $from)
588            || (!$overwrite && self::exists($con, $to))
589        ) {
590            return false;
591        }
592
593        if ($overwrite && self::exists($con, $to)) {
594            self::delete($con, $to);
595        }
596
597        $copy = self::copy($con, $from, $to);
598
599        if (!$copy) {
600            return false;
601        }
602
603        self::delete($con, $from);
604
605        return true;
606    }
607
608    /**
609     * {@inheritdoc}
610     */
611    public static function sanitize(string $path, string $replace = '', string $invalid = '/[^\w\s\d\.\-_~,;:\[\]\(\]\/]/') : string
612    {
613        return \preg_replace($invalid, $replace, $path) ?? '';
614    }
615
616    /**
617     * {@inheritdoc}
618     */
619    public static function dirname(string $path) : string
620    {
621        return \basename($path);
622    }
623
624    /**
625     * {@inheritdoc}
626     */
627    public static function dirpath(string $path) : string
628    {
629        return $path;
630    }
631
632    /**
633     * {@inheritdoc}
634     */
635    public static function name(string $path) : string
636    {
637        return \basename($path);
638    }
639
640    /**
641     * {@inheritdoc}
642     */
643    public static function basename(string $path) : string
644    {
645        return \basename($path);
646    }
647
648    /**
649     * {@inheritdoc}
650     */
651    public function getNode(string $name) : ?ContainerInterface
652    {
653        $name = isset($this->nodes[$name]) ? $name : $this->path . '/' . $name;
654
655        if (isset($this->nodes[$name]) && $this->nodes[$name] instanceof self) {
656            $this->nodes[$name]->index();
657        }
658
659        return $this->nodes[$name] ?? null;
660    }
661
662    /**
663     * {@inheritdoc}
664     */
665    public function createNode() : bool
666    {
667        if ($this->con === null) {
668            return false;
669        }
670
671        return self::create($this->con, $this->path, $this->permission, true);
672    }
673
674    /**
675     * {@inheritdoc}
676     */
677    public function addNode(ContainerInterface $node) : self
678    {
679        $this->count                      += $node->getCount();
680        $this->size                       += $node->getSize();
681        $this->nodes[$node->getBasename()] = $node;
682
683        $node->createNode();
684
685        return $this;
686    }
687
688    /**
689     * {@inheritdoc}
690     */
691    public function getParent() : ContainerInterface
692    {
693        $uri = clone $this->uri;
694        $uri->setPath(self::parent($this->path));
695
696        return new self($uri, true, $this->con);
697    }
698
699    /**
700     * {@inheritdoc}
701     */
702    public function copyNode(string $to, bool $overwrite = false) : bool
703    {
704        if ($this->con === null) {
705            return false;
706        }
707
708        return self::copy($this->con, $this->path, $to, $overwrite);
709    }
710
711    /**
712     * {@inheritdoc}
713     */
714    public function moveNode(string $to, bool $overwrite = false) : bool
715    {
716        if ($this->con === null) {
717            return false;
718        }
719
720        return self::move($this->con, $this->path, $to, $overwrite);
721    }
722
723    /**
724     * {@inheritdoc}
725     */
726    public function deleteNode() : bool
727    {
728        if ($this->con === null) {
729            return false;
730        }
731
732        // @todo: update parent
733
734        return self::delete($this->con, $this->path);
735    }
736
737    /**
738     * {@inheritdoc}
739     */
740    public function rewind() : void
741    {
742        \reset($this->nodes);
743    }
744
745    /**
746     * {@inheritdoc}
747     */
748    public function current() : ContainerInterface
749    {
750        $current = \current($this->nodes);
751        if ($current instanceof self) {
752            $current->index();
753        }
754
755        return $current === false ? $this : $current;
756    }
757
758    /**
759     * {@inheritdoc}
760     */
761    public function key() : ?string
762    {
763        return \key($this->nodes);
764    }
765
766    /**
767     * {@inheritdoc}
768     */
769    public function next() : void
770    {
771        $next = \next($this->nodes);
772        if ($next instanceof self) {
773            $next->index();
774        }
775    }
776
777    /**
778     * {@inheritdoc}
779     */
780    public function valid() : bool
781    {
782        $key = \key($this->nodes);
783
784        return ($key !== null && $key !== false);
785    }
786
787    /**
788     * {@inheritdoc}
789     */
790    public function offsetSet(mixed $offset, mixed $value) : void
791    {
792        /** @var \phpOMS\System\File\ContainerInterface $value */
793        if ($offset === null || !isset($this->nodes[$offset])) {
794            $this->addNode($value);
795        } else {
796            $this->nodes[$offset]->deleteNode();
797            $this->addNode($value);
798        }
799    }
800
801    /**
802     * {@inheritdoc}
803     */
804    public function offsetExists(mixed $offset) : bool
805    {
806        $offset = isset($this->nodes[$offset]) ? $offset : $this->path . '/' . $offset;
807
808        return isset($this->nodes[$offset]);
809    }
810
811    /**
812     * {@inheritdoc}
813     */
814    public function offsetUnset(mixed $offset) : void
815    {
816        $offset = isset($this->nodes[$offset]) ? $offset : $this->path . '/' . $offset;
817
818        if (isset($this->nodes[$offset])) {
819            $this->nodes[$offset]->deleteNode();
820
821            unset($this->nodes[$offset]);
822        }
823    }
824
825    /**
826     * {@inheritdoc}
827     */
828    public function offsetGet(mixed $offset) : mixed
829    {
830        if (isset($this->nodes[$offset]) && $this->nodes[$offset] instanceof self) {
831            $this->nodes[$offset]->index();
832        }
833
834        return $this->nodes[$offset] ?? null;
835    }
836
837    /**
838     * Check if the child node exists
839     *
840     * @param string $name Child node name. If empty checks if this node exists.
841     *
842     * @return bool
843     *
844     * @since 1.0.0
845     */
846    public function isExisting(string $name = null) : bool
847    {
848        if ($name === null) {
849            return \is_dir($this->path);
850        }
851
852        $name = isset($this->nodes[$name]) ? $name : $this->path . '/' . $name;
853
854        return isset($this->nodes[$name]);
855    }
856
857    /**
858     * {@inheritdoc}
859     */
860    public function getList() : array
861    {
862        $pathLength = \strlen($this->path);
863        $content    = [];
864
865        foreach ($this->nodes as $node) {
866            $content[] = \substr($node->getPath(), $pathLength + 1);
867        }
868
869        return $content;
870    }
871
872    /**
873     * List all files by extension directory.
874     *
875     * @param \FTP\Connection $con       FTP connection
876     * @param string          $path      Path
877     * @param string          $extension Extension
878     * @param string          $exclude   Pattern to exclude
879     * @param bool            $recursive Recursive
880     *
881     * @return array<array|string>
882     *
883     * @since 1.0.0
884     */
885    public static function listByExtension(\FTP\Connection $con, string $path, string $extension = '', string $exclude = '', bool $recursive = false) : array
886    {
887        $list = [];
888        $path = \rtrim($path, '\\/');
889
890        if (!\is_dir($path)) {
891            return $list;
892        }
893
894        $files = self::list($con, $path, empty($extension) ? '*' : '/.*\.' . $extension . '$/', $recursive);
895
896        foreach ($files as $file) {
897            if (!empty($exclude) && \preg_match('/' . $exclude . '/', $file) === 1) {
898                continue;
899            }
900
901            $list[] = $file;
902        }
903
904        return $list;
905    }
906}