Passed
Push — main ( e5d18b...282b2b )
by Sugeng
03:08
created

ZipArchiveAdapter::isAtRootDirectory()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 9
rs 10
cc 3
nc 2
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace diecoding\flysystem\adapter;
6
7
use diecoding\flysystem\traits\ChecksumAdapterTrait;
8
use diecoding\flysystem\traits\UrlGeneratorAdapterTrait;
9
use Generator;
10
use League\Flysystem\ChecksumProvider;
11
use League\Flysystem\Config;
12
use League\Flysystem\DirectoryAttributes;
13
use League\Flysystem\FileAttributes;
14
use League\Flysystem\FilesystemAdapter;
15
use League\Flysystem\PathPrefixer;
16
use League\Flysystem\UnableToCopyFile;
17
use League\Flysystem\UnableToCreateDirectory;
18
use League\Flysystem\UnableToDeleteDirectory;
19
use League\Flysystem\UnableToDeleteFile;
20
use League\Flysystem\UnableToMoveFile;
21
use League\Flysystem\UnableToReadFile;
22
use League\Flysystem\UnableToRetrieveMetadata;
23
use League\Flysystem\UnableToSetVisibility;
24
use League\Flysystem\UnableToWriteFile;
25
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
26
use League\Flysystem\UnixVisibility\VisibilityConverter;
27
use League\Flysystem\UrlGeneration\PublicUrlGenerator;
28
use League\Flysystem\UrlGeneration\TemporaryUrlGenerator;
29
use League\Flysystem\ZipArchive\ZipArchiveProvider;
30
use League\MimeTypeDetection\FinfoMimeTypeDetector;
31
use League\MimeTypeDetection\MimeTypeDetector;
32
use Throwable;
33
use ZipArchive;
34
35
use function fclose;
36
use function fopen;
37
use function rewind;
38
use function stream_copy_to_stream;
39
40
/**
41
 * This class override \League\Flysystem\ZipArchive\ZipArchiveAdapter
42
 */
43
final class ZipArchiveAdapter implements FilesystemAdapter, ChecksumProvider, PublicUrlGenerator, TemporaryUrlGenerator
44
{
45
    use UrlGeneratorAdapterTrait, ChecksumAdapterTrait;
0 ignored issues
show
Bug introduced by
The trait diecoding\flysystem\trai...rlGeneratorAdapterTrait requires the property $action which is not provided by diecoding\flysystem\adapter\ZipArchiveAdapter.
Loading history...
46
47
    private PathPrefixer $pathPrefixer;
48
    private MimeTypeDetector $mimeTypeDetector;
49
    private VisibilityConverter $visibility;
50
51
    public function __construct(
52
        private ZipArchiveProvider $zipArchiveProvider,
53
        string $root = '',
54
        ?MimeTypeDetector $mimeTypeDetector = null,
55
        ?VisibilityConverter $visibility = null,
56
        private bool $detectMimeTypeUsingPath = false,
57
    ) {
58
        $this->pathPrefixer = new PathPrefixer(ltrim($root, '/'));
59
        $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();
60
        $this->visibility = $visibility ?? new PortableVisibilityConverter();
61
    }
62
63
    public function fileExists(string $path): bool
64
    {
65
        $archive = $this->zipArchiveProvider->createZipArchive();
66
        $fileExists = $archive->locateName($this->pathPrefixer->prefixPath($path)) !== false;
67
        $archive->close();
68
69
        return $fileExists;
70
    }
71
72
    public function write(string $path, string $contents, Config $config): void
73
    {
74
        try {
75
            $this->ensureParentDirectoryExists($path, $config);
76
        } catch (Throwable $exception) {
77
            throw UnableToWriteFile::atLocation($path, 'creating parent directory failed', $exception);
78
        }
79
80
        $archive = $this->zipArchiveProvider->createZipArchive();
81
        $prefixedPath = $this->pathPrefixer->prefixPath($path);
82
83
        if (!$archive->addFromString($prefixedPath, $contents)) {
84
            throw UnableToWriteFile::atLocation($path, 'writing the file failed');
85
        }
86
87
        $archive->close();
88
        $archive = $this->zipArchiveProvider->createZipArchive();
89
90
        $visibility = $config->get(Config::OPTION_VISIBILITY);
91
        $visibilityResult = $visibility === null
92
            || $this->setVisibilityAttribute($prefixedPath, $visibility, $archive);
93
        $archive->close();
94
95
        if ($visibilityResult === false) {
96
            throw UnableToWriteFile::atLocation($path, 'setting visibility failed');
97
        }
98
    }
99
100
    public function writeStream(string $path, $contents, Config $config): void
101
    {
102
        $contents = stream_get_contents($contents);
103
104
        if ($contents === false) {
105
            throw UnableToWriteFile::atLocation($path, 'Could not get contents of given resource.');
106
        }
107
108
        $this->write($path, $contents, $config);
109
    }
110
111
    public function read(string $path): string
112
    {
113
        $archive = $this->zipArchiveProvider->createZipArchive();
114
        $contents = $archive->getFromName($this->pathPrefixer->prefixPath($path));
115
        $statusString = $archive->getStatusString();
116
        $archive->close();
117
118
        if ($contents === false) {
119
            throw UnableToReadFile::fromLocation($path, $statusString);
120
        }
121
122
        return $contents;
123
    }
124
125
    public function readStream(string $path)
126
    {
127
        $archive = $this->zipArchiveProvider->createZipArchive();
128
        $resource = $archive->getStream($this->pathPrefixer->prefixPath($path));
129
130
        if ($resource === false) {
131
            $status = $archive->getStatusString();
132
            $archive->close();
133
            throw UnableToReadFile::fromLocation($path, $status);
134
        }
135
136
        $stream = fopen('php://temp', 'w+b');
137
        stream_copy_to_stream($resource, $stream);
138
        rewind($stream);
139
        fclose($resource);
140
141
        return $stream;
142
    }
143
144
    public function delete(string $path): void
145
    {
146
        $prefixedPath = $this->pathPrefixer->prefixPath($path);
147
        $zipArchive = $this->zipArchiveProvider->createZipArchive();
148
        $success = $zipArchive->locateName($prefixedPath) === false || $zipArchive->deleteName($prefixedPath);
149
        $statusString = $zipArchive->getStatusString();
150
        $zipArchive->close();
151
152
        if (!$success) {
153
            throw UnableToDeleteFile::atLocation($path, $statusString);
154
        }
155
    }
156
157
    public function deleteDirectory(string $path): void
158
    {
159
        $archive = $this->zipArchiveProvider->createZipArchive();
160
        $prefixedPath = $this->pathPrefixer->prefixDirectoryPath($path);
161
162
        for ($i = $archive->numFiles; $i > 0; $i--) {
163
            if (($stats = $archive->statIndex($i)) === false) {
164
                continue;
165
            }
166
167
            $itemPath = $stats['name'];
168
169
            if (strpos($itemPath, $prefixedPath) !== 0) {
170
                continue;
171
            }
172
173
            if (!$archive->deleteIndex($i)) {
174
                $statusString = $archive->getStatusString();
175
                $archive->close();
176
                throw UnableToDeleteDirectory::atLocation($path, $statusString);
177
            }
178
        }
179
180
        $archive->deleteName($prefixedPath);
181
182
        $archive->close();
183
    }
184
185
    public function createDirectory(string $path, Config $config): void
186
    {
187
        try {
188
            $this->ensureDirectoryExists($path, $config);
189
        } catch (Throwable $exception) {
190
            throw UnableToCreateDirectory::dueToFailure($path, $exception);
191
        }
192
    }
193
194
    public function directoryExists(string $path): bool
195
    {
196
        $archive = $this->zipArchiveProvider->createZipArchive();
197
        $location = $this->pathPrefixer->prefixDirectoryPath($path);
198
199
        return $archive->statName($location) !== false;
200
    }
201
202
    public function setVisibility(string $path, string $visibility): void
203
    {
204
        $archive = $this->zipArchiveProvider->createZipArchive();
205
        $location = $this->pathPrefixer->prefixPath($path);
206
        $stats = $archive->statName($location) ?: $archive->statName($location . '/');
207
208
        if ($stats === false) {
0 ignored issues
show
introduced by
The condition $stats === false is always false.
Loading history...
209
            $statusString = $archive->getStatusString();
210
            $archive->close();
211
            throw UnableToSetVisibility::atLocation($path, $statusString);
212
        }
213
214
        if (!$this->setVisibilityAttribute($stats['name'], $visibility, $archive)) {
215
            $statusString1 = $archive->getStatusString();
216
            $archive->close();
217
            throw UnableToSetVisibility::atLocation($path, $statusString1);
218
        }
219
220
        $archive->close();
221
    }
222
223
    public function visibility(string $path): FileAttributes
224
    {
225
        /** @var int|null $opsys */
226
        $opsys = null;
227
        /** @var int|null $attr */
228
        $attr = null;
229
        $archive = $this->zipArchiveProvider->createZipArchive();
230
        $archive->getExternalAttributesName(
231
            $this->pathPrefixer->prefixPath($path),
232
            $opsys,
233
            $attr
234
        );
235
        $archive->close();
236
237
        if ($opsys !== ZipArchive::OPSYS_UNIX || $attr === null) {
238
            throw UnableToRetrieveMetadata::visibility($path);
239
        }
240
241
        return new FileAttributes(
242
            $path,
243
            null,
244
            $this->visibility->inverseForFile($attr >> 16)
245
        );
246
    }
247
248
    public function mimeType(string $path): FileAttributes
249
    {
250
        try {
251
            $mimetype = $this->detectMimeTypeUsingPath
252
                ? $this->mimeTypeDetector->detectMimeTypeFromPath($path)
253
                : $this->mimeTypeDetector->detectMimeType($path, $this->read($path));
254
        } catch (Throwable $exception) {
255
            throw UnableToRetrieveMetadata::mimeType($path, $exception->getMessage(), $exception);
256
        }
257
258
        if ($mimetype === null) {
259
            throw UnableToRetrieveMetadata::mimeType($path, 'Unknown.');
260
        }
261
262
        return new FileAttributes($path, null, null, null, $mimetype);
263
    }
264
265
    public function lastModified(string $path): FileAttributes
266
    {
267
        $zipArchive = $this->zipArchiveProvider->createZipArchive();
268
        $stats = $zipArchive->statName($this->pathPrefixer->prefixPath($path));
269
        $statusString = $zipArchive->getStatusString();
270
        $zipArchive->close();
271
272
        if ($stats === false) {
273
            throw UnableToRetrieveMetadata::lastModified($path, $statusString);
274
        }
275
276
        return new FileAttributes($path, null, null, $stats['mtime']);
277
    }
278
279
    public function fileSize(string $path): FileAttributes
280
    {
281
        $archive = $this->zipArchiveProvider->createZipArchive();
282
        $stats = $archive->statName($this->pathPrefixer->prefixPath($path));
283
        $statusString = $archive->getStatusString();
284
        $archive->close();
285
286
        if ($stats === false) {
287
            throw UnableToRetrieveMetadata::fileSize($path, $statusString);
288
        }
289
290
        if ($this->isDirectoryPath($stats['name'])) {
291
            throw UnableToRetrieveMetadata::fileSize($path, 'It\'s a directory.');
292
        }
293
294
        return new FileAttributes($path, $stats['size'], null, null);
295
    }
296
297
    public function listContents(string $path, bool $deep): iterable
298
    {
299
        $archive = $this->zipArchiveProvider->createZipArchive();
300
        $location = $this->pathPrefixer->prefixDirectoryPath($path);
301
        $items = [];
302
303
        for ($i = 0; $i < $archive->numFiles; $i++) {
304
            $stats = $archive->statIndex($i);
305
            // @codeCoverageIgnoreStart
306
            if ($stats === false) {
307
                continue;
308
            }
309
            // @codeCoverageIgnoreEnd
310
311
            $itemPath = $stats['name'];
312
313
            if (
314
                $location === $itemPath
315
                || ($deep && $location !== '' && strpos($itemPath, $location) !== 0)
316
                || ($deep === false && !$this->isAtRootDirectory($location, $itemPath))
317
            ) {
318
                continue;
319
            }
320
321
            $items[] = $this->isDirectoryPath($itemPath)
322
                ? new DirectoryAttributes(
323
                    $this->pathPrefixer->stripDirectoryPrefix($itemPath),
324
                    null,
325
                    $stats['mtime']
326
                )
327
                : new FileAttributes(
328
                    $this->pathPrefixer->stripPrefix($itemPath),
329
                    $stats['size'],
330
                    null,
331
                    $stats['mtime']
332
                );
333
        }
334
335
        $archive->close();
336
337
        return $this->yieldItemsFrom($items);
338
    }
339
340
    private function yieldItemsFrom(array $items): Generator
341
    {
342
        yield from $items;
343
    }
344
345
    public function move(string $source, string $destination, Config $config): void
346
    {
347
        try {
348
            $this->ensureParentDirectoryExists($destination, $config);
349
        } catch (Throwable $exception) {
350
            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);
351
        }
352
353
        $archive = $this->zipArchiveProvider->createZipArchive();
354
355
        if ($archive->locateName($this->pathPrefixer->prefixPath($destination)) !== false) {
356
            $this->delete($destination);
357
            $this->copy($source, $destination, $config);
358
            $this->delete($source);
359
            return;
360
        }
361
362
        $renamed = $archive->renameName(
363
            $this->pathPrefixer->prefixPath($source),
364
            $this->pathPrefixer->prefixPath($destination)
365
        );
366
        if ($renamed === false) {
367
            throw UnableToMoveFile::fromLocationTo($source, $destination);
368
        }
369
    }
370
371
    public function copy(string $source, string $destination, Config $config): void
372
    {
373
        try {
374
            $readStream = $this->readStream($source);
375
            $this->writeStream($destination, $readStream, $config);
376
        } catch (Throwable $exception) {
377
            if (isset($readStream)) {
378
                @fclose($readStream);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fclose(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

378
                /** @scrutinizer ignore-unhandled */ @fclose($readStream);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
379
            }
380
381
            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);
382
        }
383
    }
384
385
    private function ensureParentDirectoryExists(string $path, Config $config): void
386
    {
387
        $dirname = dirname($path);
388
389
        if ($dirname === '' || $dirname === '.') {
390
            return;
391
        }
392
393
        $this->ensureDirectoryExists($dirname, $config);
394
    }
395
396
    private function ensureDirectoryExists(string $dirname, Config $config): void
397
    {
398
        $visibility = $config->get(Config::OPTION_DIRECTORY_VISIBILITY);
399
        $archive = $this->zipArchiveProvider->createZipArchive();
400
        $prefixedDirname = $this->pathPrefixer->prefixDirectoryPath($dirname);
401
        $parts = array_filter(explode('/', trim($prefixedDirname, '/')));
402
        $dirPath = '';
403
404
        foreach ($parts as $part) {
405
            $dirPath .= $part . '/';
406
            $info = $archive->statName($dirPath);
407
408
            if ($info === false && $archive->addEmptyDir($dirPath) === false) {
409
                throw UnableToCreateDirectory::atLocation($dirname);
410
            }
411
412
            if ($visibility === null) {
413
                continue;
414
            }
415
416
            if (!$this->setVisibilityAttribute($dirPath, $visibility, $archive)) {
417
                $archive->close();
418
                throw UnableToCreateDirectory::atLocation($dirname, 'Unable to set visibility.');
419
            }
420
        }
421
422
        $archive->close();
423
    }
424
425
    private function isDirectoryPath(string $path): bool
426
    {
427
        return substr($path, -1) === '/';
428
    }
429
430
    private function isAtRootDirectory(string $directoryRoot, string $path): bool
431
    {
432
        $dirname = dirname($path);
433
434
        if ('' === $directoryRoot && '.' === $dirname) {
435
            return true;
436
        }
437
438
        return $directoryRoot === (rtrim($dirname, '/') . '/');
439
    }
440
441
    private function setVisibilityAttribute(string $statsName, string $visibility, ZipArchive $archive): bool
442
    {
443
        $visibility = $this->isDirectoryPath($statsName)
444
            ? $this->visibility->forDirectory($visibility)
445
            : $this->visibility->forFile($visibility);
446
447
        return $archive->setExternalAttributesName($statsName, ZipArchive::OPSYS_UNIX, $visibility << 16);
448
    }
449
}