Passed
Push — 0.1.x ( af61a4...c46ae2 )
by f
01:28
created

src/UnifiedArchive.php (1 issue)

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.2';
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);
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 string $type One of predefined archive types
112
     * @return boolean
113
     */
114
    public static function canOpenType($type)
115
    {
116
        self::checkRequirements();
117
118
        return (isset(self::$enabledTypes[$type]))
119
            ? self::$enabledTypes[$type]
120
            : false;
121
    }
122
123
    /**
124
     * Detect archive type by its filename or content.
125
     *
126
     * @param string $fileName
127
     * @param bool $contentCheck
128
     * @return string|boolean One of UnifiedArchive type constants OR false if type is not detected
129
     */
130
    public static function detectArchiveType($fileName, $contentCheck = true)
131
    {
132
        // by file name
133
        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
134
135
        // by file name
136
        if (stripos($fileName, '.tar.') !== false && preg_match('~\.(?<ext>tar\.(gz|bz2|xz|z))$~', strtolower($fileName), $match)) {
137
            switch ($match['ext']) {
138
                case 'tar.gz':
139
                    return self::TAR_GZIP;
140
                case 'tar.bz2':
141
                    return self::TAR_BZIP;
142
                case 'tar.xz':
143
                    return self::TAR_LZMA;
144
                case 'tar.z':
145
                    return self::TAR_LZW;
146
            }
147
        }
148
149
        switch ($ext) {
150
            case 'zip':
151
                return self::ZIP;
152
            case '7z':
153
                return self::SEVEN_ZIP;
154
            case 'rar':
155
                return self::RAR;
156
            case 'gz':
157
                return self::GZIP;
158
            case 'bz2':
159
                return self::BZIP;
160
            case 'xz':
161
                return self::LZMA;
162
            case 'iso':
163
                return self::ISO;
164
            case 'cab':
165
                return self::CAB;
166
            case 'tar':
167
                return self::TAR;
168
            case 'tgz':
169
                return self::TAR_GZIP;
170
            case 'tbz2':
171
                return self::TAR_BZIP;
172
            case 'txz':
173
                return self::TAR_LZMA;
174
175
        }
176
177
        // by content
178
        if ($contentCheck) {
179
            $mime_type = mime_content_type($fileName);
180
            switch ($mime_type) {
181
                case 'application/zip':
182
                    return self::ZIP;
183
                case 'application/x-7z-compressed':
184
                    return self::SEVEN_ZIP;
185
                case 'application/x-rar':
186
                    return self::RAR;
187
                case 'application/zlib':
188
                    return self::GZIP;
189
                case 'application/x-bzip2':
190
                    return self::BZIP;
191
                case 'application/x-lzma':
192
                    return self::LZMA;
193
                case 'application/x-iso9660-image':
194
                    return self::ISO;
195
                case 'application/vnd.ms-cab-compressed':
196
                    return self::CAB;
197
                case 'application/x-tar':
198
                    return self::TAR;
199
                case 'application/x-gtar':
200
                    return self::TAR_GZIP;
201
202
            }
203
        }
204
205
        return false;
206
    }
207
208
    /**
209
     * Opens the file as one of supported formats.
210
     *
211
     * @param string $fileName Filename
212
     * @param string $type Archive type.
213
     * @throws Exception If archive can not be opened
214
     */
215
    public function __construct($fileName, $type)
216
    {
217
        self::checkRequirements();
218
219
        $this->type = $type;
220
        $this->archiveSize = filesize($fileName);
221
222
        if (!isset(static::$formatHandlers[$type]))
223
            throw new Exception('Unsupported archive type: '.$type.' of archive '.$fileName);
224
225
        $handler_class = __NAMESPACE__.'\\Formats\\'.static::$formatHandlers[$type];
226
227
        $this->archive = new $handler_class($fileName);
228
        $this->scanArchive();
229
    }
230
231
    /**
232
     * Rescans array after modification
233
     */
234
    protected function scanArchive()
235
    {
236
        $information = $this->archive->getArchiveInformation();
237
        $this->files = $information->files;
238
        $this->compressedFilesSize = $information->compressedFilesSize;
239
        $this->uncompressedFilesSize = $information->uncompressedFilesSize;
240
        $this->filesQuantity = count($information->files);
241
    }
242
243
    /**
244
     * Closes archive.
245
     */
246
    public function __destruct()
247
    {
248
        unset($this->archive);
249
    }
250
251
    /**
252
     * Returns an instance of class implementing PclZipOriginalInterface
253
     * interface.
254
     *
255
     * @return \wapmorgan\UnifiedArchive\PclzipZipInterface Returns an instance of a class
256
     * implementing PclZipOriginalInterface
257
     * @throws Exception
258
     */
259
    public function getPclZipInterface()
260
    {
261
        if ($this->type !== self::ZIP)
262
            throw new UnsupportedOperationException();
263
264
        return new $this->archive->getPclZip();
265
    }
266
267
    /**
268
     * Counts number of files
269
     * @return int
270
     */
271
    public function countFiles()
272
    {
273
        return $this->filesQuantity;
274
    }
275
276
    /**
277
     * Counts size of all uncompressed data (bytes)
278
     * @return int
279
     */
280
    public function countUncompressedFilesSize()
281
    {
282
        return $this->uncompressedFilesSize;
283
    }
284
285
    /**
286
     * Returns size of archive
287
     * @return int
288
     */
289
    public function getArchiveSize()
290
    {
291
        return $this->archiveSize;
292
    }
293
294
    /**
295
     * Returns type of archive
296
     * @return string
297
     */
298
    public function getArchiveType()
299
    {
300
        return $this->type;
301
    }
302
303
    /**
304
     * Counts size of all compressed data (in bytes)
305
     * @return int
306
     */
307
    public function countCompressedFilesSize()
308
    {
309
        return $this->compressedFilesSize;
310
    }
311
312
    /**
313
     * Returns list of files
314
     * @return array List of files
315
     */
316
    public function getFileNames()
317
    {
318
        return array_values($this->files);
319
    }
320
321
    /**
322
     * Checks that file exists in archive
323
     * @param string $fileName
324
     * @return bool
325
     */
326
    public function isFileExists($fileName)
327
    {
328
        return in_array($fileName, $this->files, true);
329
    }
330
331
    /**
332
     * Returns file metadata
333
     * @param string $fileName
334
     * @return ArchiveEntry|bool
335
     */
336
    public function getFileData($fileName)
337
    {
338
        if (!in_array($fileName, $this->files, true))
339
            return false;
340
341
        return $this->archive->getFileData($fileName);
342
    }
343
344
    /**
345
     * Returns file content
346
     *
347
     * @param $fileName
348
     *
349
     * @return bool|string
350
     * @throws \Exception
351
     */
352
    public function getFileContent($fileName)
353
    {
354
        if (!in_array($fileName, $this->files, true))
355
            return false;
356
357
        return $this->archive->getFileContent($fileName);
358
    }
359
360
    /**
361
     * Returns a resource for reading file from archive
362
     * @param string $fileName
363
     * @return bool|resource|string
364
     */
365
    public function getFileResource($fileName)
366
    {
367
        if (!in_array($fileName, $this->files, true))
368
            return false;
369
370
        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...
371
    }
372
373
    /**
374
     * Unpacks files to disk.
375
     * @param string $outputFolder Extraction output dir.
376
     * @param string|array|null $files One files or list of files or null to extract all content.
377
     * @param bool $expandFilesList Should be expanded paths like 'src/' to all files inside 'src/' dir or not.
378
     * @return false|int
379
     * @throws Exception If files can not be extracted
380
     */
381
    public function extractFiles($outputFolder, $files = null, $expandFilesList = false)
382
    {
383
        if ($files !== null) {
384
            if (is_string($files)) $files = [$files];
385
386
            if ($expandFilesList)
387
                $files = self::expandFileList($this->files, $files);
388
389
            return $this->archive->extractFiles($outputFolder, $files);
390
        } else {
391
            return $this->archive->extractArchive($outputFolder);
392
        }
393
    }
394
395
    /**
396
     * Updates existing archive by removing files from it.
397
     * Only 7zip and zip types support deletion.
398
     * @param string|string[] $fileOrFiles
399
     * @param bool $expandFilesList
400
     *
401
     * @return bool|int
402
     * @throws Exception
403
     */
404
    public function deleteFiles($fileOrFiles, $expandFilesList = false)
405
    {
406
        $fileOrFiles = is_string($fileOrFiles) ? [$fileOrFiles] : $fileOrFiles;
407
408
        if ($expandFilesList && $fileOrFiles !== null)
409
            $fileOrFiles = self::expandFileList($this->files, $fileOrFiles);
410
411
        $result = $this->archive->deleteFiles($fileOrFiles);
412
        $this->scanArchive();
413
        return $result;
414
    }
415
416
    /**
417
     * Updates existing archive by adding new files
418
     * @param string[] $fileOrFiles See [[archiveFiles]] method for file list format.
419
     * @return int|bool False if failed, number of added files if success
420
     * @throws Exception
421
     */
422
    public function addFiles($fileOrFiles)
423
    {
424
        $files_list = self::createFilesList($fileOrFiles);
425
        if (empty($files_list))
426
            throw new InvalidArgumentException('Files list is empty!');
427
        $result = $this->archive->addFiles($files_list);
428
        $this->scanArchive();
429
        return $result;
430
    }
431
432
    /**
433
     * Adds file into archive
434
     * @param string $file
435
     * @param string|null $inArchiveName If not passed, full path will be preserved.
436
     * @return bool
437
     * @throws Exception
438
     */
439
    public function addFile($file, $inArchiveName = null)
440
    {
441
        if (!is_file($file))
442
            throw new InvalidArgumentException($file.' is not a valid file to add in archive');
443
444
        return ($inArchiveName !== null
445
                ? $this->addFiles([$file => $inArchiveName])
446
                : $this->addFiles([$file])) === 1;
447
    }
448
449
    /**
450
     * Adds directory contents to archive.
451
     * @param string $directory
452
     * @param string|null $inArchivePath If not passed, full paths will be preserved.
453
     * @return bool
454
     * @throws Exception
455
     */
456
    public function addDirectory($directory, $inArchivePath = null)
457
    {
458
        if (!is_dir($directory) || !is_readable($directory))
459
            throw new InvalidArgumentException($directory.' is not a valid directory to add in archive');
460
461
        return ($inArchivePath !== null
462
                ? $this->addFiles([$directory => $inArchivePath])
463
                : $this->addFiles([$inArchivePath])) > 0;
464
    }
465
466
    /**
467
     * Creates an archive with passed files list
468
     *
469
     * @param string|string[]|array<string,string> $fileOrFiles List of files. Can be one of three formats:
470
     *                             1. A string containing path to file or directory.
471
     *                                  File will have it's basename.
472
     *                                  `UnifiedArchive::archiveFiles(['/etc/php.ini'], 'archive.zip)` will store
473
     * file with 'php.ini' name.
474
     *                                  Directory contents will be stored in archive root.
475
     *                                  `UnifiedArchive::archiveFiles(['/var/log/'], 'archive.zip')` will store all
476
     * directory contents in archive root.
477
     *                             2. An array with strings containing pats to files or directories.
478
     *                                  Files and directories will be stored with full paths.
479
     *                                  `UnifiedArchive::archiveFiles(['/etc/php.ini', '/var/log/'], 'archive.zip)`
480
     * will preserve full paths.
481
     *                             3. An array with strings where keys are strings.
482
     *                                  Files will have name from key.
483
     *                                  Directories contents will have prefix from key.
484
     *                                  `UnifiedArchive::archiveFiles(['doc.txt' => 'very_long_name_of_document.txt',
485
     *  'static' => '/var/www/html/static/'], 'archive.zip')`
486
     *
487
     * @param string $archiveName File name of archive. Type of archive will be determined via it's name.
488
     * @param bool $emulate If true, emulation mode is performed instead of real archiving.
489
     *
490
     * @return array|bool|int
491
     * @throws Exception
492
     */
493
    public static function archiveFiles($fileOrFiles, $archiveName, $emulate = false)
494
    {
495
        if (file_exists($archiveName))
496
            throw new Exception('Archive '.$archiveName.' already exists!');
497
498
        self::checkRequirements();
499
500
        $archiveType = self::detectArchiveType($archiveName, false);
501
        //        if (in_array($archiveType, [TarArchive::TAR, TarArchive::TAR_GZIP, TarArchive::TAR_BZIP, TarArchive::TAR_LZMA, TarArchive::TAR_LZW], true))
502
        //            return TarArchive::archiveFiles($fileOrFiles, $archiveName, $emulate);
503
        if ($archiveType === false)
504
            return false;
505
506
        $files_list = self::createFilesList($fileOrFiles);
507
        if (empty($files_list))
508
            throw new InvalidArgumentException('Files list is empty!');
509
510
        // fake creation: return archive data
511
        if ($emulate) {
512
            $totalSize = 0;
513
            foreach ($files_list as $fn) $totalSize += filesize($fn);
514
515
            return array(
516
                'totalSize' => $totalSize,
517
                'numberOfFiles' => count($files_list),
518
                'files' => $files_list,
519
                'type' => $archiveType,
520
            );
521
        }
522
523
        if (!isset(static::$formatHandlers[$archiveType]))
524
            throw new Exception('Unsupported archive type: '.$archiveType.' of archive '.$archiveName);
525
526
        $handler_class = __NAMESPACE__.'\\Formats\\'.static::$formatHandlers[$archiveType];
527
528
        return $handler_class::createArchive($files_list, $archiveName);
529
    }
530
531
    /**
532
     * Creates an archive with one file
533
     * @param string $file
534
     * @param string $archiveName
535
     * @return bool
536
     * @throws \Exception
537
     */
538
    public static function archiveFile($file, $archiveName)
539
    {
540
        if (!is_file($file))
541
            throw new InvalidArgumentException($file.' is not a valid file to archive');
542
543
        return static::archiveFiles($file, $archiveName) === 1;
544
    }
545
546
    /**
547
     * Creates an archive with full directory contents
548
     * @param string $directory
549
     * @param string $archiveName
550
     * @return bool
551
     * @throws Exception
552
     */
553
    public static function archiveDirectory($directory, $archiveName)
554
    {
555
        if (!is_dir($directory) || !is_readable($directory))
556
            throw new InvalidArgumentException($directory.' is not a valid directory to archive');
557
558
        return static::archiveFiles($directory, $archiveName) > 0;
559
    }
560
561
    /**
562
     * Tests system configuration
563
     */
564
    protected static function checkRequirements()
565
    {
566
        if (empty(self::$enabledTypes)) {
567
            self::$enabledTypes[self::ZIP] = extension_loaded('zip');
568
            self::$enabledTypes[self::SEVEN_ZIP] = class_exists('\Archive7z\Archive7z');
569
            self::$enabledTypes[self::RAR] = extension_loaded('rar');
570
            self::$enabledTypes[self::GZIP] = extension_loaded('zlib');
571
            self::$enabledTypes[self::BZIP] = extension_loaded('bz2');
572
            self::$enabledTypes[self::LZMA] = extension_loaded('xz');
573
            self::$enabledTypes[self::ISO] = class_exists('\CISOFile');
574
            self::$enabledTypes[self::CAB] = class_exists('\CabArchive');
575
            self::$enabledTypes[self::TAR] = class_exists('\Archive_Tar') || class_exists('\PharData');
576
            self::$enabledTypes[self::TAR_GZIP] = (class_exists('\Archive_Tar') || class_exists('\PharData')) && extension_loaded('zlib');
577
            self::$enabledTypes[self::TAR_BZIP] = (class_exists('\Archive_Tar') || class_exists('\PharData')) && extension_loaded('bz2');
578
            self::$enabledTypes[self::TAR_LZMA] = class_exists('\Archive_Tar') && extension_loaded('lzma2');
579
            self::$enabledTypes[self::TAR_LZMA] = class_exists('\Archive_Tar') && LzwStreamWrapper::isBinaryAvailable();
580
        }
581
    }
582
583
    /**
584
     * Deprecated method for extracting files
585
     * @param $outputFolder
586
     * @param string|array|null $files
587
     * @deprecated 0.1.0
588
     * @see extractFiles()
589
     * @return bool|int
590
     * @throws Exception
591
     */
592
    public function extractNode($outputFolder, $files = null)
593
    {
594
        return $this->extractFiles($outputFolder, $files);
595
    }
596
597
    /**
598
     * Deprecated method for archiving files
599
     * @param $filesOrFiles
600
     * @param $archiveName
601
     * @deprecated 0.1.0
602
     * @see archiveFiles()
603
     * @return mixed
604
     * @throws Exception
605
     */
606
    public static function archiveNodes($filesOrFiles, $archiveName)
607
    {
608
        return static::archiveFiles($filesOrFiles, $archiveName);
609
    }
610
611
    /**
612
     * Expands files list
613
     * @param $archiveFiles
614
     * @param $files
615
     * @return array
616
     */
617
    protected static function expandFileList($archiveFiles, $files)
618
    {
619
        $newFiles = [];
620
        foreach ($files as $file) {
621
            foreach ($archiveFiles as $archiveFile) {
622
                if (fnmatch($file.'*', $archiveFile))
623
                    $newFiles[] = $archiveFile;
624
            }
625
        }
626
        return $newFiles;
627
    }
628
629
    /**
630
     * @param string|array $nodes
631
     * @return array|bool
632
     */
633
    protected static function createFilesList($nodes)
634
    {
635
        $files = [];
636
637
        // passed an extended list
638
        if (is_array($nodes)) {
639
            foreach ($nodes as $source => $destination) {
640
                if (is_numeric($source))
641
                    $source = $destination;
642
643
                $destination = rtrim($destination, '/\\*');
644
645
                // if is directory
646
                if (is_dir($source))
647
                    self::importFilesFromDir(rtrim($source, '/\\*').'/*',
648
                        !empty($destination) ? $destination.'/' : null, true, $files);
649
                else if (is_file($source))
650
                    $files[$destination] = $source;
651
            }
652
653
        } else if (is_string($nodes)) { // passed one file or directory
654
            // if is directory
655
            if (is_dir($nodes))
656
                self::importFilesFromDir(rtrim($nodes, '/\\*').'/*', null, true,
657
                    $files);
658
            else if (is_file($nodes))
659
                $files[basename($nodes)] = $nodes;
660
        }
661
662
        return $files;
663
    }
664
665
    /**
666
     * @param string $source
667
     * @param string|null $destination
668
     * @param bool $recursive
669
     * @param array $map
670
     */
671
    protected static function importFilesFromDir($source, $destination, $recursive, &$map)
672
    {
673
        // $map[$destination] = rtrim($source, '/*');
674
        // do not map root archive folder
675
676
        if ($destination !== null)
677
            $map[$destination] = null;
678
679
        foreach (glob($source, GLOB_MARK) as $node) {
680
            if (in_array(substr($node, -1), ['/', '\\'], true) && $recursive) {
681
                self::importFilesFromDir(str_replace('\\', '/', $node).'*',
682
                    $destination.basename($node).'/', $recursive, $map);
683
            } elseif (is_file($node) && is_readable($node)) {
684
                $map[$destination.basename($node)] = $node;
685
            }
686
        }
687
    }
688
}
689