Passed
Push — 0.1.x ( e6e6c9...fdd999 )
by f
01:28
created

UnifiedArchive::archiveNodes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
namespace wapmorgan\UnifiedArchive;
3
4
use Archive7z\Archive7z;
0 ignored issues
show
Bug introduced by
The type Archive7z\Archive7z was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
5
use Exception;
6
use wapmorgan\UnifiedArchive\Formats\BasicFormat;
7
use wapmorgan\UnifiedArchive\Formats\SevenZip;
8
use wapmorgan\UnifiedArchive\Formats\Zip;
9
use ZipArchive;
10
11
/**
12
 * Class which represents archive in one of supported formats.
13
 */
14
class UnifiedArchive implements AbstractArchive
15
{
16
    const VERSION = '0.1.1';
17
18
    const ZIP = 'zip';
19
    const SEVEN_ZIP = '7zip';
20
    const RAR = 'rar';
21
    const GZIP = 'gzip';
22
    const BZIP = 'bzip2';
23
    const LZMA = 'lzma2';
24
    const ISO = 'iso';
25
    const CAB = 'cab';
26
    const TAR = 'tar';
27
    const TAR_GZIP = 'tgz';
28
    const TAR_BZIP = 'tbz2';
29
    const TAR_LZMA = 'txz';
30
    const TAR_LZW = 'tar.z';
31
32
    /** @var array List of archive format handlers */
33
    protected static $formatHandlers = [
34
        self::ZIP => 'Zip',
35
        self::SEVEN_ZIP => 'SevenZip',
36
        self::RAR => 'Rar',
37
        self::GZIP => 'Gzip',
38
        self::BZIP => 'Bzip',
39
        self::LZMA => 'Lzma',
40
        self::ISO => 'Iso',
41
        self::CAB => 'Cab',
42
        self::TAR => 'Tar',
43
        self::TAR_GZIP => 'Tar',
44
        self::TAR_BZIP => 'Tar',
45
        self::TAR_LZMA => 'Tar',
46
        self::TAR_LZW => 'Tar',
47
    ];
48
49
    /** @var array List of archive formats with support state */
50
    static protected $enabledTypes = [];
51
52
    /** @var string Type of current archive */
53
    protected $type;
54
55
    /** @var BasicFormat Adapte for current archive */
56
    protected $archive;
57
58
    /** @var array List of files in current archive */
59
    protected $files;
60
61
    /** @var int Number of files */
62
    protected $filesQuantity;
63
64
    /** @var int Size of uncompressed files */
65
    protected $uncompressedFilesSize;
66
67
    /** @var int Size of compressed files */
68
    protected $compressedFilesSize;
69
70
    /** @var int Size of archive */
71
    protected $archiveSize;
72
73
    /**
74
     * Creates instance with right type.
75
     * @param  string $fileName Filename
76
     * @return UnifiedArchive|null Returns UnifiedArchive in case of successful reading of the file
77
     * @throws \Exception
78
     */
79
    public static function open($fileName)
80
    {
81
        self::checkRequirements();
82
83
        if (!file_exists($fileName) || !is_readable($fileName))
84
            throw new Exception('Could not open file: '.$fileName);
85
86
        $type = self::detectArchiveType($fileName);
87
        if (!self::canOpenType($type)) {
88
            return null;
89
        }
90
91
        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

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

434
        $result = $this->archive->addFiles(/** @scrutinizer ignore-type */ $files_list);
Loading history...
435
        $this->scanArchive();
436
        return $result;
437
    }
438
439
    /**
440
     * Creates an archive.
441
     * @param string|string[]|array $fileOrFiles
442
     * @param $archiveName
443
     * @param bool $emulate
444
     * @return array|bool|int
445
     * @throws Exception
446
     */
447
    public static function archiveFiles($fileOrFiles, $archiveName, $emulate = false)
448
    {
449
        if (file_exists($archiveName))
450
            throw new Exception('Archive '.$archiveName.' already exists!');
451
452
        self::checkRequirements();
453
454
        $archiveType = self::detectArchiveType($archiveName, false);
455
//        if (in_array($archiveType, [TarArchive::TAR, TarArchive::TAR_GZIP, TarArchive::TAR_BZIP, TarArchive::TAR_LZMA, TarArchive::TAR_LZW], true))
456
//            return TarArchive::archiveFiles($fileOrFiles, $archiveName, $emulate);
457
        if ($archiveType === false)
458
            return false;
459
460
        $files_list = self::createFilesList($fileOrFiles);
461
462
        // fake creation: return archive data
463
        if ($emulate) {
464
            $totalSize = 0;
465
            foreach ($files_list as $fn) $totalSize += filesize($fn);
466
467
            return array(
468
                'totalSize' => $totalSize,
469
                '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

469
                'numberOfFiles' => count(/** @scrutinizer ignore-type */ $files_list),
Loading history...
470
                'files' => $files_list,
471
                'type' => $archiveType,
472
            );
473
        }
474
475
        if (!isset(static::$formatHandlers[$archiveType]))
476
            throw new Exception('Unsupported archive type: '.$archiveType.' of archive '.$archiveName);
477
478
        $handler_class = __NAMESPACE__.'\\Formats\\'.static::$formatHandlers[$archiveType];
479
480
        return $handler_class::createArchive($files_list, $archiveName);
481
    }
482
483
    /**
484
     * Tests system configuration
485
     */
486
    protected static function checkRequirements()
487
    {
488
        if (empty(self::$enabledTypes)) {
489
            self::$enabledTypes[self::ZIP] = extension_loaded('zip');
490
            self::$enabledTypes[self::SEVEN_ZIP] = class_exists('\Archive7z\Archive7z');
491
            self::$enabledTypes[self::RAR] = extension_loaded('rar');
492
            self::$enabledTypes[self::GZIP] = extension_loaded('zlib');
493
            self::$enabledTypes[self::BZIP] = extension_loaded('bz2');
494
            self::$enabledTypes[self::LZMA] = extension_loaded('xz');
495
            self::$enabledTypes[self::ISO] = class_exists('\CISOFile');
496
            self::$enabledTypes[self::CAB] = class_exists('\CabArchive');
497
            self::$enabledTypes[self::TAR] = class_exists('\Archive_Tar') || class_exists('\PharData');
498
            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...
499
            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...
500
            self::$enabledTypes[self::TAR_LZMA] = class_exists('\Archive_Tar') && extension_loaded('lzma2');
501
            self::$enabledTypes[self::TAR_LZMA] = class_exists('\Archive_Tar') && LzwStreamWrapper::isBinaryAvailable();
502
        }
503
    }
504
505
    /**
506
     * @param $outputFolder
507
     * @param string|array|null $files
508
     * @deprecated 0.1.0
509
     * @see extractFiles()
510
     * @return bool|int
511
     * @throws Exception
512
     */
513
    public function extractNode($outputFolder, $files = null)
514
    {
515
        return $this->extractFiles($outputFolder, $files);
516
    }
517
518
    /**
519
     * @param $filesOrFiles
520
     * @param $archiveName
521
     * @deprecated 0.1.0
522
     * @see archiveFiles()
523
     * @return mixed
524
     * @throws Exception
525
     */
526
    public static function archiveNodes($filesOrFiles, $archiveName)
527
    {
528
        return static::archiveFiles($filesOrFiles, $archiveName);
529
    }
530
531
    /**
532
     * Expands files list
533
     * @param $archiveFiles
534
     * @param $files
535
     * @return array
536
     */
537
    protected static function expandFileList($archiveFiles, $files)
538
    {
539
        $newFiles = [];
540
        foreach ($files as $file) {
541
            foreach ($archiveFiles as $archiveFile) {
542
                if (fnmatch($file.'*', $archiveFile))
543
                    $newFiles[] = $archiveFile;
544
            }
545
        }
546
        return $newFiles;
547
    }
548
549
    /**
550
     * @param string|array $nodes
551
     * @return array|bool
552
     */
553
    protected static function createFilesList($nodes)
554
    {
555
        $files = array();
556
557
        // passed an extended list
558
        if (is_array($nodes)) {
559
            foreach ($nodes as $source => $destination) {
560
                if (is_numeric($source))
561
                    $source = $destination;
562
563
                $destination = rtrim($destination, '/\\*');
564
565
                // if is directory
566
                if (is_dir($source))
567
                    self::importFilesFromDir(rtrim($source, '/\\*').'/*',
568
                        !empty($destination) ? $destination.'/' : null, true, $files);
569
                else if (is_file($source))
570
                    $files[$destination] = $source;
571
            }
572
573
        } 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...
574
            // if is directory
575
            if (is_dir($nodes))
576
                self::importFilesFromDir(rtrim($nodes, '/\\*').'/*', null, true,
577
                    $files);
578
            else if (is_file($nodes))
579
                $files[basename($nodes)] = $nodes;
580
        }
581
582
        return $files;
583
    }
584
585
    /**
586
     * @param string $source
587
     * @param string|null $destination
588
     * @param bool $recursive
589
     * @param array $map
590
     */
591
    protected static function importFilesFromDir($source, $destination, $recursive, &$map)
592
    {
593
        // $map[$destination] = rtrim($source, '/*');
594
        // do not map root archive folder
595
596
        if ($destination !== null)
597
            $map[$destination] = null;
598
599
        foreach (glob($source, GLOB_MARK) as $node) {
600
            if (in_array(substr($node, -1), ['/', '\\'], true) && $recursive) {
601
                self::importFilesFromDir(str_replace('\\', '/', $node).'*',
602
                    $destination.basename($node).'/', $recursive, $map);
603
            } elseif (is_file($node) && is_readable($node)) {
604
                $map[$destination.basename($node)] = $node;
605
            }
606
        }
607
    }
608
609
    /**
610
     * @param string $file
611
     * @param string $archiveName
612
     * @return bool
613
     */
614
    static public function archiveFile($file, $archiveName)
615
    {
616
        if (!is_file($file))
617
            throw new \InvalidArgumentException($file.' is not a valid file to archive');
618
619
        return static::archiveFiles($file, $archiveName) === 1;
620
    }
621
622
    /**
623
     * @param string $directory
624
     * @param string $archiveName
625
     * @return bool
626
     * @throws Exception
627
     */
628
    static public function archiveDirectory($directory, $archiveName)
629
    {
630
        if (!is_dir($directory) || !is_readable($directory))
631
            throw new \InvalidArgumentException($directory.' is not a valid directory to archive');
632
633
        return static::archiveFiles($directory, $archiveName) > 0;
634
    }
635
636
    /**
637
     * @param string $file
638
     * @param string|null $inArchiveName
639
     * @return bool
640
     * @throws Exception
641
     */
642
    public function addFile($file, $inArchiveName = null)
643
    {
644
        if (!is_file($file))
645
            throw new \InvalidArgumentException($file.' is not a valid file to add in archive');
646
647
        return ($inArchiveName !== null
648
                ? $this->addFiles([$file => $inArchiveName])
649
                : $this->addFiles([$file])) === 1;
650
    }
651
652
    /**
653
     * @param string $directory
654
     * @param string|null $inArchivePath
655
     * @return bool
656
     * @throws Exception
657
     */
658
    public function addDirectory($directory, $inArchivePath = null)
659
    {
660
        if (!is_dir($directory) || !is_readable($directory))
661
            throw new \InvalidArgumentException($directory.' is not a valid directory to add in archive');
662
663
        return ($inArchivePath !== null
664
                ? $this->addFiles([$directory => $inArchivePath])
665
                : $this->addFiles([$inArchivePath])) > 0;
666
    }
667
}
668