Completed
Push — master ( 061d1e...31cfb3 )
by Greg
02:26
created

src/Task/Archive/Extract.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Robo\Task\Archive;
4
5
use Robo\Result;
6
use Robo\Task\BaseTask;
7
use Robo\Task\Filesystem\FilesystemStack;
8
use Robo\Task\Filesystem\DeleteDir;
9
use Robo\Contract\BuilderAwareInterface;
10
use Robo\Common\BuilderAwareTrait;
11
12
/**
13
 * Extracts an archive.
14
 *
15
 * Note that often, distributions are packaged in tar or zip archives
16
 * where the topmost folder may contain variable information, such as
17
 * the release date, or the version of the package.  This information
18
 * is very useful when unpacking by hand, but arbitrarily-named directories
19
 * are much less useful to scripts.  Therefore, by default, Extract will
20
 * remove the top-level directory, and instead store all extracted files
21
 * into the directory specified by $archivePath.
22
 *
23
 * To keep the top-level directory when extracting, use
24
 * `preserveTopDirectory(true)`.
25
 *
26
 * ``` php
27
 * <?php
28
 * $this->taskExtract($archivePath)
29
 *  ->to($destination)
30
 *  ->preserveTopDirectory(false) // the default
31
 *  ->run();
32
 * ?>
33
 * ```
34
 */
35
class Extract extends BaseTask implements BuilderAwareInterface
36
{
37
    use BuilderAwareTrait;
38
39
    /**
40
     * @var string
41
     */
42
    protected $filename;
43
44
    /**
45
     * @var string
46
     */
47
    protected $to;
48
49
    /**
50
     * @var bool
51
     */
52
    private $preserveTopDirectory = false;
53
54
    /**
55
     * @param string $filename
56
     */
57
    public function __construct($filename)
58
    {
59
        $this->filename = $filename;
60
    }
61
62
    /**
63
     * Location to store extracted files.
64
     *
65
     * @param string $to
66
     *
67
     * @return $this
68
     */
69
    public function to($to)
70
    {
71
        $this->to = $to;
72
        return $this;
73
    }
74
75
    /**
76
     * @param bool $preserve
77
     *
78
     * @return $this
79
     */
80
    public function preserveTopDirectory($preserve = true)
81
    {
82
        $this->preserveTopDirectory = $preserve;
83
        return $this;
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function run()
90
    {
91 View Code Duplication
        if (!file_exists($this->filename)) {
92
            $this->printTaskError("File {filename} does not exist", ['filename' => $this->filename]);
93
94
            return false;
95
        }
96
        if (!($mimetype = static::archiveType($this->filename))) {
97
            $this->printTaskError("Could not determine type of archive for {filename}", ['filename' => $this->filename]);
98
99
            return false;
100
        }
101
102
        // We will first extract to $extractLocation and then move to $this->to
103
        $extractLocation = static::getTmpDir();
104
        @mkdir($extractLocation);
105
        @mkdir(dirname($this->to));
106
107
        $this->startTimer();
108
109
        $this->printTaskInfo("Extracting {filename}", ['filename' => $this->filename]);
110
111
        $result = $this->extractAppropriateType($mimetype, $extractLocation);
112
        if ($result->wasSuccessful()) {
113
            $this->printTaskInfo("{filename} extracted", ['filename' => $this->filename]);
114
            // Now, we want to move the extracted files to $this->to. There
115
            // are two possibilities that we must consider:
116
            //
117
            // (1) Archived files were encapsulated in a folder with an arbitrary name
118
            // (2) There was no encapsulating folder, and all the files in the archive
119
            //     were extracted into $extractLocation
120
            //
121
            // In the case of (1), we want to move and rename the encapsulating folder
122
            // to $this->to.
123
            //
124
            // In the case of (2), we will just move and rename $extractLocation.
125
            $filesInExtractLocation = glob("$extractLocation/*");
126
            $hasEncapsulatingFolder = ((count($filesInExtractLocation) == 1) && is_dir($filesInExtractLocation[0]));
127
            if ($hasEncapsulatingFolder && !$this->preserveTopDirectory) {
128
                $result = (new FilesystemStack())
129
                    ->inflect($this)
130
                    ->rename($filesInExtractLocation[0], $this->to)
131
                    ->run();
132
                (new DeleteDir($extractLocation))
133
                    ->inflect($this)
134
                    ->run();
135
            } else {
136
                $result = (new FilesystemStack())
137
                    ->inflect($this)
138
                    ->rename($extractLocation, $this->to)
139
                    ->run();
140
            }
141
        }
142
        $this->stopTimer();
143
        $result['time'] = $this->getExecutionTime();
144
145
        return $result;
146
    }
147
148
    /**
149
     * @param string $mimetype
150
     * @param string $extractLocation
151
     *
152
     * @return \Robo\Result
153
     */
154
    protected function extractAppropriateType($mimetype, $extractLocation)
155
    {
156
        // Perform the extraction of a zip file.
157
        if (($mimetype == 'application/zip') || ($mimetype == 'application/x-zip')) {
158
            return $this->extractZip($extractLocation);
159
        }
160
        return $this->extractTar($extractLocation);
161
    }
162
163
    /**
164
     * @param string $extractLocation
165
     *
166
     * @return \Robo\Result
167
     */
168
    protected function extractZip($extractLocation)
169
    {
170
        if (!extension_loaded('zlib')) {
171
            return Result::errorMissingExtension($this, 'zlib', 'zip extracting');
172
        }
173
174
        $zip = new \ZipArchive();
175
        if (($status = $zip->open($this->filename)) !== true) {
176
            return Result::error($this, "Could not open zip archive {$this->filename}");
177
        }
178
        if (!$zip->extractTo($extractLocation)) {
179
            return Result::error($this, "Could not extract zip archive {$this->filename}");
180
        }
181
        $zip->close();
182
183
        return Result::success($this);
184
    }
185
186
    /**
187
     * @param string $extractLocation
188
     *
189
     * @return \Robo\Result
190
     */
191
    protected function extractTar($extractLocation)
192
    {
193
        if (!class_exists('Archive_Tar')) {
194
            return Result::errorMissingPackage($this, 'Archive_Tar', 'pear/archive_tar');
195
        }
196
        $tar_object = new \Archive_Tar($this->filename);
197
        if (!$tar_object->extract($extractLocation)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tar_object->extract($extractLocation) of type null|boolean is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
198
            return Result::error($this, "Could not extract tar archive {$this->filename}");
199
        }
200
201
        return Result::success($this);
202
    }
203
204
    /**
205
     * @param string $filename
206
     *
207
     * @return bool|string
208
     */
209
    protected static function archiveType($filename)
210
    {
211
        $content_type = false;
212
        if (class_exists('finfo')) {
213
            $finfo = new \finfo(FILEINFO_MIME_TYPE);
214
            $content_type = $finfo->file($filename);
215
            // If finfo cannot determine the content type, then we will try other methods
216
            if ($content_type == 'application/octet-stream') {
217
                $content_type = false;
218
            }
219
        }
220
        // Examing the file's magic header bytes.
221
        if (!$content_type) {
222
            if ($file = fopen($filename, 'rb')) {
223
                $first = fread($file, 2);
224
                fclose($file);
225
                if ($first !== false) {
226
                    // Interpret the two bytes as a little endian 16-bit unsigned int.
227
                    $data = unpack('v', $first);
228
                    switch ($data[1]) {
229
                        case 0x8b1f:
230
                            // First two bytes of gzip files are 0x1f, 0x8b (little-endian).
231
                            // See http://www.gzip.org/zlib/rfc-gzip.html#header-trailer
232
                            $content_type = 'application/x-gzip';
233
                            break;
234
235
                        case 0x4b50:
236
                            // First two bytes of zip files are 0x50, 0x4b ('PK') (little-endian).
237
                            // See http://en.wikipedia.org/wiki/Zip_(file_format)#File_headers
238
                            $content_type = 'application/zip';
239
                            break;
240
241
                        case 0x5a42:
242
                            // First two bytes of bzip2 files are 0x5a, 0x42 ('BZ') (big-endian).
243
                            // See http://en.wikipedia.org/wiki/Bzip2#File_format
244
                            $content_type = 'application/x-bzip2';
245
                            break;
246
                    }
247
                }
248
            }
249
        }
250
        // 3. Lastly if above methods didn't work, try to guess the mime type from
251
        // the file extension. This is useful if the file has no identificable magic
252
        // header bytes (for example tarballs).
253
        if (!$content_type) {
254
            // Remove querystring from the filename, if present.
255
            $filename = basename(current(explode('?', $filename, 2)));
256
            $extension_mimetype = array(
257
                '.tar.gz' => 'application/x-gzip',
258
                '.tgz' => 'application/x-gzip',
259
                '.tar' => 'application/x-tar',
260
            );
261
            foreach ($extension_mimetype as $extension => $ct) {
262
                if (substr($filename, -strlen($extension)) === $extension) {
263
                    $content_type = $ct;
264
                    break;
265
                }
266
            }
267
        }
268
269
        return $content_type;
270
    }
271
272
    /**
273
     * @return string
274
     */
275
    protected static function getTmpDir()
276
    {
277
        return getcwd().'/tmp'.rand().time();
278
    }
279
}
280