Passed
Push — 0.1.x ( fbf5b2...bbe9c4 )
by f
01:36
created

UnifiedArchive::archiveFiles()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 36
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 21
nc 7
nop 3
dl 0
loc 36
rs 8.6506
c 0
b 0
f 0
1
<?php
2
namespace wapmorgan\UnifiedArchive;
3
4
use Exception;
5
use InvalidArgumentException;
6
use wapmorgan\UnifiedArchive\Formats\BasicFormat;
7
8
/**
9
 * Class which represents archive in one of supported formats.
10
 */
11
class UnifiedArchive implements AbstractArchive
12
{
13
    const VERSION = '0.1.1';
14
15
    const ZIP = 'zip';
16
    const SEVEN_ZIP = '7zip';
17
    const RAR = 'rar';
18
    const GZIP = 'gzip';
19
    const BZIP = 'bzip2';
20
    const LZMA = 'lzma2';
21
    const ISO = 'iso';
22
    const CAB = 'cab';
23
    const TAR = 'tar';
24
    const TAR_GZIP = 'tgz';
25
    const TAR_BZIP = 'tbz2';
26
    const TAR_LZMA = 'txz';
27
    const TAR_LZW = 'tar.z';
28
29
    /** @var array List of archive format handlers */
30
    protected static $formatHandlers = [
31
        self::ZIP => 'Zip',
32
        self::SEVEN_ZIP => 'SevenZip',
33
        self::RAR => 'Rar',
34
        self::GZIP => 'Gzip',
35
        self::BZIP => 'Bzip',
36
        self::LZMA => 'Lzma',
37
        self::ISO => 'Iso',
38
        self::CAB => 'Cab',
39
        self::TAR => 'Tar',
40
        self::TAR_GZIP => 'Tar',
41
        self::TAR_BZIP => 'Tar',
42
        self::TAR_LZMA => 'Tar',
43
        self::TAR_LZW => 'Tar',
44
    ];
45
46
    /** @var array List of archive formats with support state */
47
    static protected $enabledTypes = [];
48
49
    /** @var string Type of current archive */
50
    protected $type;
51
52
    /** @var BasicFormat Adapter for current archive */
53
    protected $archive;
54
55
    /** @var array List of files in current archive */
56
    protected $files;
57
58
    /** @var int Number of files */
59
    protected $filesQuantity;
60
61
    /** @var int Size of uncompressed files */
62
    protected $uncompressedFilesSize;
63
64
    /** @var int Size of compressed files */
65
    protected $compressedFilesSize;
66
67
    /** @var int Size of archive */
68
    protected $archiveSize;
69
70
    /**
71
     * Creates instance with right type.
72
     * @param  string $fileName Filename
73
     * @return UnifiedArchive|null Returns UnifiedArchive in case of successful reading of the file
74
     * @throws \Exception
75
     */
76
    public static function open($fileName)
77
    {
78
        self::checkRequirements();
79
80
        if (!file_exists($fileName) || !is_readable($fileName))
81
            throw new Exception('Could not open file: '.$fileName);
82
83
        $type = self::detectArchiveType($fileName);
84
        if (!self::canOpenType($type)) {
85
            return null;
86
        }
87
88
        return new self($fileName, $type);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type false; however, parameter $type of wapmorgan\UnifiedArchive...dArchive::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
        return new self($fileName, /** @scrutinizer ignore-type */ $type);
Loading history...
89
    }
90
91
    /**
92
     * Checks whether archive can be opened with current system configuration
93
     * @param string $fileName
94
     * @return boolean
95
     */
96
    public static function canOpenArchive($fileName)
97
    {
98
        self::checkRequirements();
99
100
        $type = self::detectArchiveType($fileName);
101
        if ($type !== false && self::canOpenType($type)) {
102
            return true;
103
        }
104
105
        return false;
106
    }
107
108
    /**
109
     * Checks whether specific archive type can be opened with current system configuration
110
     *
111
     * @param $type
112
     *
113
     * @param bool $onOwn
114
     * @return boolean
115
     */
116
    public static function canOpenType($type)
117
    {
118
        self::checkRequirements();
119
120
        return (isset(self::$enabledTypes[$type]))
121
            ? self::$enabledTypes[$type]
122
            : false;
123
    }
124
125
    /**
126
     * Detect archive type by its filename or content.
127
     *
128
     * @param string $fileName
129
     * @param bool $contentCheck
130
     * @return string|boolean One of UnifiedArchive type constants OR false if type is not detected
131
     */
132
    public static function detectArchiveType($fileName, $contentCheck = true)
133
    {
134
        // by file name
135
        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
136
137
        // by file name
138
        if (stripos($fileName, '.tar.') !== false && preg_match('~\.(?<ext>tar\.(gz|bz2|xz|z))$~', strtolower($fileName), $match)) {
139
            switch ($match['ext']) {
140
                case 'tar.gz':
141
                    return self::TAR_GZIP;
142
                case 'tar.bz2':
143
                    return self::TAR_BZIP;
144
                case 'tar.xz':
145
                    return self::TAR_LZMA;
146
                case 'tar.z':
147
                    return self::TAR_LZW;
148
            }
149
        }
150
151
        switch ($ext) {
152
            case 'zip':
153
                return self::ZIP;
154
            case '7z':
155
                return self::SEVEN_ZIP;
156
            case 'rar':
157
                return self::RAR;
158
            case 'gz':
159
                return self::GZIP;
160
            case 'bz2':
161
                return self::BZIP;
162
            case 'xz':
163
                return self::LZMA;
164
            case 'iso':
165
                return self::ISO;
166
            case 'cab':
167
                return self::CAB;
168
            case 'tar':
169
                return self::TAR;
170
            case 'tgz':
171
                return self::TAR_GZIP;
172
            case 'tbz2':
173
                return self::TAR_BZIP;
174
            case 'txz':
175
                return self::TAR_LZMA;
176
177
        }
178
179
        // by content
180
        if ($contentCheck) {
181
            $mime_type = mime_content_type($fileName);
182
            switch ($mime_type) {
183
                case 'application/zip':
184
                    return self::ZIP;
185
                case 'application/x-7z-compressed':
186
                    return self::SEVEN_ZIP;
187
                case 'application/x-rar':
188
                    return self::RAR;
189
                case 'application/zlib':
190
                    return self::GZIP;
191
                case 'application/x-bzip2':
192
                    return self::BZIP;
193
                case 'application/x-lzma':
194
                    return self::LZMA;
195
                case 'application/x-iso9660-image':
196
                    return self::ISO;
197
                case 'application/vnd.ms-cab-compressed':
198
                    return self::CAB;
199
                case 'application/x-tar':
200
                    return self::TAR;
201
                case 'application/x-gtar':
202
                    return self::TAR_GZIP;
203
204
            }
205
        }
206
207
        return false;
208
    }
209
210
    /**
211
     * Opens the file as one of supported formats.
212
     *
213
     * @param string $fileName Filename
214
     * @param string $type Archive type.
215
     * @throws Exception If archive can not be opened
216
     */
217
    public function __construct($fileName, $type)
218
    {
219
        self::checkRequirements();
220
221
        $this->type = $type;
222
        $this->archiveSize = filesize($fileName);
223
224
        if (!isset(static::$formatHandlers[$type]))
225
            throw new Exception('Unsupported archive type: '.$type.' of archive '.$fileName);
226
227
        $handler_class = __NAMESPACE__.'\\Formats\\'.static::$formatHandlers[$type];
228
229
        $this->archive = new $handler_class($fileName);
230
        $this->scanArchive();
231
    }
232
233
    /**
234
     * Rescans array after modification
235
     */
236
    protected function scanArchive()
237
    {
238
        $information = $this->archive->getArchiveInformation();
239
        $this->files = $information->files;
240
        $this->compressedFilesSize = $information->compressedFilesSize;
241
        $this->uncompressedFilesSize = $information->uncompressedFilesSize;
242
        $this->filesQuantity = count($information->files);
243
    }
244
245
    /**
246
     * Closes archive.
247
     */
248
    public function __destruct()
249
    {
250
        unset($this->archive);
251
    }
252
253
    /**
254
     * Returns an instance of class implementing PclZipOriginalInterface
255
     * interface.
256
     *
257
     * @return \wapmorgan\UnifiedArchive\PclzipZipInterface Returns an instance of a class
258
     * implementing PclZipOriginalInterface
259
     * @throws Exception
260
     */
261
    public function getPclZipInterface()
262
    {
263
        if ($this->type !== self::ZIP)
264
            throw new UnsupportedOperationException();
265
266
        return new $this->archive->getPclZip();
0 ignored issues
show
Bug introduced by
The property getPclZip does not seem to exist on wapmorgan\UnifiedArchive\Formats\BasicFormat.
Loading history...
267
    }
268
269
    /**
270
     * Counts number of files
271
     * @return int
272
     */
273
    public function countFiles()
274
    {
275
        return $this->filesQuantity;
276
    }
277
278
    /**
279
     * Counts size of all uncompressed data (bytes)
280
     * @return int
281
     */
282
    public function countUncompressedFilesSize()
283
    {
284
        return $this->uncompressedFilesSize;
285
    }
286
287
    /**
288
     * Returns size of archive
289
     * @return int
290
     */
291
    public function getArchiveSize()
292
    {
293
        return $this->archiveSize;
294
    }
295
296
    /**
297
     * Returns type of archive
298
     * @return string
299
     */
300
    public function getArchiveType()
301
    {
302
        return $this->type;
303
    }
304
305
    /**
306
     * Counts size of all compressed data (in bytes)
307
     * @return int
308
     */
309
    public function countCompressedFilesSize()
310
    {
311
        return $this->compressedFilesSize;
312
    }
313
314
    /**
315
     * Returns list of files
316
     * @return array List of files
317
     */
318
    public function getFileNames()
319
    {
320
        return array_values($this->files);
321
    }
322
323
    /**
324
     * Checks that file exists in archive
325
     * @param string $fileName
326
     * @return bool
327
     */
328
    public function isFileExists($fileName)
329
    {
330
        return in_array($fileName, $this->files, true);
331
    }
332
333
    /**
334
     * Returns file metadata
335
     * @param string $fileName
336
     * @return ArchiveEntry|bool
337
     */
338
    public function getFileData($fileName)
339
    {
340
        if (!in_array($fileName, $this->files, true))
341
            return false;
342
343
        return $this->archive->getFileData($fileName);
344
    }
345
346
    /**
347
     * Returns file content
348
     *
349
     * @param $fileName
350
     *
351
     * @return bool|string
352
     * @throws \Exception
353
     */
354
    public function getFileContent($fileName)
355
    {
356
        if (!in_array($fileName, $this->files, true))
357
            return false;
358
359
        return $this->archive->getFileContent($fileName);
360
    }
361
362
    /**
363
     * Returns a resource for reading file from archive
364
     * @param string $fileName
365
     * @return bool|resource|string
366
     */
367
    public function getFileResource($fileName)
368
    {
369
        if (!in_array($fileName, $this->files, true))
370
            return false;
371
372
        return $this->archive->getFileResource($fileName);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->archive->getFileResource($fileName) also could return the type boolean|string which is incompatible with the return type mandated by wapmorgan\UnifiedArchive...hive::getFileResource() of false|resource.
Loading history...
373
    }
374
375
    /**
376
     * Unpacks files to disk.
377
     * @param string $outputFolder Extraction output dir.
378
     * @param string|array|null $files One files or list of files or null to extract all content.
379
     * @param bool $expandFilesList Should be expanded paths like 'src/' to all files inside 'src/' dir or not.
380
     * @return false|int
381
     * @throws Exception If files can not be extracted
382
     */
383
    public function extractFiles($outputFolder, $files = null, $expandFilesList = false)
384
    {
385
        if ($files !== null) {
386
            if (is_string($files)) $files = [$files];
387
388
            if ($expandFilesList)
389
                $files = self::expandFileList($this->files, $files);
390
391
            return $this->archive->extractFiles($outputFolder, $files);
392
        } else {
393
            return $this->archive->extractArchive($outputFolder);
394
        }
395
    }
396
397
    /**
398
     * Updates existing archive by removing files from it.
399
     * Only 7zip and zip types support deletion.
400
     * @param string|string[] $fileOrFiles
401
     * @param bool $expandFilesList
402
     *
403
     * @return bool|int
404
     * @throws Exception
405
     */
406
    public function deleteFiles($fileOrFiles, $expandFilesList = false)
407
    {
408
        $fileOrFiles = is_string($fileOrFiles) ? [$fileOrFiles] : $fileOrFiles;
409
410
        if ($expandFilesList && $fileOrFiles !== null)
411
            $fileOrFiles = self::expandFileList($this->files, $fileOrFiles);
412
413
        $result = $this->archive->deleteFiles($fileOrFiles);
414
        $this->scanArchive();
415
        return $result;
416
    }
417
418
    /**
419
     * Updates existing archive by adding new files
420
     * @param string[] $fileOrFiles See [[archiveFiles]] method for file list format.
421
     * @return int|bool False if failed, number of added files if success
422
     * @throws Exception
423
     */
424
    public function addFiles($fileOrFiles)
425
    {
426
        $files_list = self::createFilesList($fileOrFiles);
427
        if (empty($files_list))
428
            throw new InvalidArgumentException('Files list is empty!');
429
        $result = $this->archive->addFiles($files_list);
0 ignored issues
show
Bug introduced by
It seems like $files_list can also be of type boolean; however, parameter $files of wapmorgan\UnifiedArchive...BasicFormat::addFiles() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

429
        $result = $this->archive->addFiles(/** @scrutinizer ignore-type */ $files_list);
Loading history...
430
        $this->scanArchive();
431
        return $result;
432
    }
433
434
    /**
435
     * Adds file into archive
436
     * @param string $file
437
     * @param string|null $inArchiveName If not passed, full path will be preserved.
438
     * @return bool
439
     * @throws Exception
440
     */
441
    public function addFile($file, $inArchiveName = null)
442
    {
443
        if (!is_file($file))
444
            throw new InvalidArgumentException($file.' is not a valid file to add in archive');
445
446
        return ($inArchiveName !== null
447
                ? $this->addFiles([$file => $inArchiveName])
448
                : $this->addFiles([$file])) === 1;
449
    }
450
451
    /**
452
     * Adds directory contents to archive.
453
     * @param string $directory
454
     * @param string|null $inArchivePath If not passed, full paths will be preserved.
455
     * @return bool
456
     * @throws Exception
457
     */
458
    public function addDirectory($directory, $inArchivePath = null)
459
    {
460
        if (!is_dir($directory) || !is_readable($directory))
461
            throw new InvalidArgumentException($directory.' is not a valid directory to add in archive');
462
463
        return ($inArchivePath !== null
464
                ? $this->addFiles([$directory => $inArchivePath])
465
                : $this->addFiles([$inArchivePath])) > 0;
466
    }
467
468
    /**
469
     * Creates an archive with passed files list
470
     *
471
     * @param string|string[]|array<string,string> $fileOrFiles List of files. Can be one of three formats:
472
     *                             1. A string containing path to file or directory.
473
     *                                  File will have it's basename.
474
     *                                  `UnifiedArchive::archiveFiles(['/etc/php.ini'], 'archive.zip)` will store
475
     * file with 'php.ini' name.
476
     *                                  Directory contents will be stored in archive root.
477
     *                                  `UnifiedArchive::archiveFiles(['/var/log/'], 'archive.zip')` will store all
478
     * directory contents in archive root.
479
     *                             2. An array with strings containing pats to files or directories.
480
     *                                  Files and directories will be stored with full paths.
481
     *                                  `UnifiedArchive::archiveFiles(['/etc/php.ini', '/var/log/'], 'archive.zip)`
482
     * will preserve full paths.
483
     *                             3. An array with strings where keys are strings.
484
     *                                  Files will have name from key.
485
     *                                  Directories contents will have prefix from key.
486
     *                                  `UnifiedArchive::archiveFiles(['doc.txt' => 'very_long_name_of_document.txt',
487
     *  'static' => '/var/www/html/static/'], 'archive.zip')`
488
     *
489
     * @param string $archiveName File name of archive. Type of archive will be determined via it's name.
490
     * @param bool $emulate If true, emulation mode is performed instead of real archiving.
491
     *
492
     * @return array|bool|int
493
     * @throws Exception
494
     */
495
    public static function archiveFiles($fileOrFiles, $archiveName, $emulate = false)
496
    {
497
        if (file_exists($archiveName))
498
            throw new Exception('Archive '.$archiveName.' already exists!');
499
500
        self::checkRequirements();
501
502
        $archiveType = self::detectArchiveType($archiveName, false);
503
        //        if (in_array($archiveType, [TarArchive::TAR, TarArchive::TAR_GZIP, TarArchive::TAR_BZIP, TarArchive::TAR_LZMA, TarArchive::TAR_LZW], true))
504
        //            return TarArchive::archiveFiles($fileOrFiles, $archiveName, $emulate);
505
        if ($archiveType === false)
506
            return false;
507
508
        $files_list = self::createFilesList($fileOrFiles);
509
        if (empty($files_list))
510
            throw new InvalidArgumentException('Files list is empty!');
511
512
        // fake creation: return archive data
513
        if ($emulate) {
514
            $totalSize = 0;
515
            foreach ($files_list as $fn) $totalSize += filesize($fn);
516
517
            return array(
518
                'totalSize' => $totalSize,
519
                'numberOfFiles' => count($files_list),
0 ignored issues
show
Bug introduced by
It seems like $files_list can also be of type boolean; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

519
                'numberOfFiles' => count(/** @scrutinizer ignore-type */ $files_list),
Loading history...
520
                'files' => $files_list,
521
                'type' => $archiveType,
522
            );
523
        }
524
525
        if (!isset(static::$formatHandlers[$archiveType]))
526
            throw new Exception('Unsupported archive type: '.$archiveType.' of archive '.$archiveName);
527
528
        $handler_class = __NAMESPACE__.'\\Formats\\'.static::$formatHandlers[$archiveType];
529
530
        return $handler_class::createArchive($files_list, $archiveName);
531
    }
532
533
    /**
534
     * Creates an archive with one file
535
     * @param string $file
536
     * @param string $archiveName
537
     * @return bool
538
     * @throws \Exception
539
     */
540
    public static function archiveFile($file, $archiveName)
541
    {
542
        if (!is_file($file))
543
            throw new InvalidArgumentException($file.' is not a valid file to archive');
544
545
        return static::archiveFiles($file, $archiveName) === 1;
546
    }
547
548
    /**
549
     * Creates an archive with full directory contents
550
     * @param string $directory
551
     * @param string $archiveName
552
     * @return bool
553
     * @throws Exception
554
     */
555
    public static function archiveDirectory($directory, $archiveName)
556
    {
557
        if (!is_dir($directory) || !is_readable($directory))
558
            throw new InvalidArgumentException($directory.' is not a valid directory to archive');
559
560
        return static::archiveFiles($directory, $archiveName) > 0;
561
    }
562
563
    /**
564
     * Tests system configuration
565
     */
566
    protected static function checkRequirements()
567
    {
568
        if (empty(self::$enabledTypes)) {
569
            self::$enabledTypes[self::ZIP] = extension_loaded('zip');
570
            self::$enabledTypes[self::SEVEN_ZIP] = class_exists('\Archive7z\Archive7z');
571
            self::$enabledTypes[self::RAR] = extension_loaded('rar');
572
            self::$enabledTypes[self::GZIP] = extension_loaded('zlib');
573
            self::$enabledTypes[self::BZIP] = extension_loaded('bz2');
574
            self::$enabledTypes[self::LZMA] = extension_loaded('xz');
575
            self::$enabledTypes[self::ISO] = class_exists('\CISOFile');
576
            self::$enabledTypes[self::CAB] = class_exists('\CabArchive');
577
            self::$enabledTypes[self::TAR] = class_exists('\Archive_Tar') || class_exists('\PharData');
578
            self::$enabledTypes[self::TAR_GZIP] = (class_exists('\Archive_Tar') || class_exists('\PharData')) && extension_loaded('zlib');
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: self::enabledTypes[self:...tension_loaded('zlib')), Probably Intended Meaning: self::enabledTypes[self:...tension_loaded('zlib'))
Loading history...
579
            self::$enabledTypes[self::TAR_BZIP] = (class_exists('\Archive_Tar') || class_exists('\PharData')) && extension_loaded('bz2');
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: self::enabledTypes[self:...xtension_loaded('bz2')), Probably Intended Meaning: self::enabledTypes[self:...xtension_loaded('bz2'))
Loading history...
580
            self::$enabledTypes[self::TAR_LZMA] = class_exists('\Archive_Tar') && extension_loaded('lzma2');
581
            self::$enabledTypes[self::TAR_LZMA] = class_exists('\Archive_Tar') && LzwStreamWrapper::isBinaryAvailable();
582
        }
583
    }
584
585
    /**
586
     * Deprecated method for extracting files
587
     * @param $outputFolder
588
     * @param string|array|null $files
589
     * @deprecated 0.1.0
590
     * @see extractFiles()
591
     * @return bool|int
592
     * @throws Exception
593
     */
594
    public function extractNode($outputFolder, $files = null)
595
    {
596
        return $this->extractFiles($outputFolder, $files);
597
    }
598
599
    /**
600
     * Deprecated method for archiving files
601
     * @param $filesOrFiles
602
     * @param $archiveName
603
     * @deprecated 0.1.0
604
     * @see archiveFiles()
605
     * @return mixed
606
     * @throws Exception
607
     */
608
    public static function archiveNodes($filesOrFiles, $archiveName)
609
    {
610
        return static::archiveFiles($filesOrFiles, $archiveName);
611
    }
612
613
    /**
614
     * Expands files list
615
     * @param $archiveFiles
616
     * @param $files
617
     * @return array
618
     */
619
    protected static function expandFileList($archiveFiles, $files)
620
    {
621
        $newFiles = [];
622
        foreach ($files as $file) {
623
            foreach ($archiveFiles as $archiveFile) {
624
                if (fnmatch($file.'*', $archiveFile))
625
                    $newFiles[] = $archiveFile;
626
            }
627
        }
628
        return $newFiles;
629
    }
630
631
    /**
632
     * @param string|array $nodes
633
     * @return array|bool
634
     */
635
    protected static function createFilesList($nodes)
636
    {
637
        $files = [];
638
639
        // passed an extended list
640
        if (is_array($nodes)) {
641
            foreach ($nodes as $source => $destination) {
642
                if (is_numeric($source))
643
                    $source = $destination;
644
645
                $destination = rtrim($destination, '/\\*');
646
647
                // if is directory
648
                if (is_dir($source))
649
                    self::importFilesFromDir(rtrim($source, '/\\*').'/*',
650
                        !empty($destination) ? $destination.'/' : null, true, $files);
651
                else if (is_file($source))
652
                    $files[$destination] = $source;
653
            }
654
655
        } else if (is_string($nodes)) { // passed one file or directory
0 ignored issues
show
introduced by
The condition is_string($nodes) is always true.
Loading history...
656
            // if is directory
657
            if (is_dir($nodes))
658
                self::importFilesFromDir(rtrim($nodes, '/\\*').'/*', null, true,
659
                    $files);
660
            else if (is_file($nodes))
661
                $files[basename($nodes)] = $nodes;
662
        }
663
664
        return $files;
665
    }
666
667
    /**
668
     * @param string $source
669
     * @param string|null $destination
670
     * @param bool $recursive
671
     * @param array $map
672
     */
673
    protected static function importFilesFromDir($source, $destination, $recursive, &$map)
674
    {
675
        // $map[$destination] = rtrim($source, '/*');
676
        // do not map root archive folder
677
678
        if ($destination !== null)
679
            $map[$destination] = null;
680
681
        foreach (glob($source, GLOB_MARK) as $node) {
682
            if (in_array(substr($node, -1), ['/', '\\'], true) && $recursive) {
683
                self::importFilesFromDir(str_replace('\\', '/', $node).'*',
684
                    $destination.basename($node).'/', $recursive, $map);
685
            } elseif (is_file($node) && is_readable($node)) {
686
                $map[$destination.basename($node)] = $node;
687
            }
688
        }
689
    }
690
}
691