Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.00% covered (warning)
75.00%
93 / 124
22.73% covered (danger)
22.73%
5 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReadOnlyFileSystem
75.00% covered (warning)
75.00%
93 / 124
22.73% covered (danger)
22.73%
5 / 22
99.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 fileExists
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 write
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 writeStream
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 rewindStream
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 read
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 readStream
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 delete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 deleteDirectory
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 createDirectory
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 listContents
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 move
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 copy
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
4.06
 getAttributes
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 lastModified
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 fileSize
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
4.59
 mimeType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setVisibility
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 visibility
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
6.56
 directoryExists
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 directoryExistsIn
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 has
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * When running with `--dry-run` the filesystem should be read-only.
4 *
5 * This should work with read operations working as normal but write operations should be
6 * cached so they appear to have been successful but are not actually written to disk.
7 */
8
9namespace BrianHenryIE\Strauss\Helpers;
10
11use BadMethodCallException;
12use Exception;
13use League\Flysystem\Config;
14use League\Flysystem\DirectoryListing;
15use League\Flysystem\FileAttributes;
16use League\Flysystem\FilesystemException;
17use League\Flysystem\FilesystemOperator;
18use League\Flysystem\FilesystemReader;
19use League\Flysystem\PathNormalizer;
20use League\Flysystem\StorageAttributes;
21use League\Flysystem\UnableToReadFile;
22use League\Flysystem\UnableToRetrieveMetadata;
23use League\Flysystem\Visibility;
24use League\Flysystem\WhitespacePathNormalizer;
25use Traversable;
26
27class ReadOnlyFileSystem implements FilesystemOperator, FlysystemBackCompatInterface
28{
29//  use FlysystemBackCompatTrait;
30    protected FilesystemOperator $filesystem;
31    protected InMemoryFilesystemAdapter $inMemoryFiles;
32    protected InMemoryFilesystemAdapter $deletedFiles;
33
34    protected PathNormalizer $pathNormalizer;
35
36    public function __construct(FilesystemOperator $filesystem, ?PathNormalizer $pathNormalizer = null)
37    {
38        $this->filesystem = $filesystem;
39
40        $this->inMemoryFiles = new InMemoryFilesystemAdapter();
41        $this->deletedFiles = new InMemoryFilesystemAdapter();
42
43        $this->pathNormalizer = $pathNormalizer ?? new WhitespacePathNormalizer();
44    }
45
46    public function fileExists(string $location): bool
47    {
48        $location = $this->pathNormalizer->normalizePath($location);
49
50        if ($this->deletedFiles->fileExists($location)) {
51            return false;
52        }
53        return $this->inMemoryFiles->fileExists($location)
54                || $this->filesystem->fileExists($location);
55    }
56
57    /**
58     * @param array{visibility?:string} $config
59     * @throws FilesystemException
60     */
61    public function write(string $location, string $contents, array $config = []): void
62    {
63        $location = $this->pathNormalizer->normalizePath($location);
64
65        $config = new Config($config);
66        $this->inMemoryFiles->write($location, $contents, $config);
67
68        if ($this->deletedFiles->fileExists($location)) {
69            $this->deletedFiles->delete($location);
70        }
71    }
72
73    /**
74     * @param resource $contents
75     * @param array{visibility?:string} $config
76     * @throws FilesystemException
77     */
78    public function writeStream(string $location, $contents, $config = []): void
79    {
80        $location = $this->pathNormalizer->normalizePath($location);
81
82        $config = new Config($config);
83        $this->rewindStream($contents);
84        $this->inMemoryFiles->writeStream($location, $contents, $config);
85
86        if ($this->deletedFiles->fileExists($location)) {
87            $this->deletedFiles->delete($location);
88        }
89    }
90    /**
91     * @param resource $resource
92     */
93    private function rewindStream($resource): void
94    {
95        if (ftell($resource) !== 0 && stream_get_meta_data($resource)['seekable']) {
96            rewind($resource);
97        }
98    }
99
100    public function read(string $location): string
101    {
102        $location = $this->pathNormalizer->normalizePath($location);
103
104        if ($this->deletedFiles->fileExists($location)) {
105            throw UnableToReadFile::fromLocation($location);
106        }
107        if ($this->inMemoryFiles->fileExists($location)) {
108            return $this->inMemoryFiles->read($location);
109        }
110        return $this->filesystem->read($location);
111    }
112
113    public function readStream(string $location)
114    {
115        $location = $this->pathNormalizer->normalizePath($location);
116
117        if ($this->deletedFiles->fileExists($location)) {
118            throw UnableToReadFile::fromLocation($location);
119        }
120        if ($this->inMemoryFiles->fileExists($location)) {
121            return $this->inMemoryFiles->readStream($location);
122        }
123        return $this->filesystem->readStream($location);
124    }
125
126    public function delete(string $location): void
127    {
128        $location = $this->pathNormalizer->normalizePath($location);
129
130        if ($this->fileExists($location)) {
131            $file = $this->read($location);
132            $this->deletedFiles->write($location, $file, new Config([]));
133        }
134        if ($this->inMemoryFiles->fileExists($location)) {
135            $this->inMemoryFiles->delete($location);
136        }
137    }
138
139    public function deleteDirectory(string $location): void
140    {
141        $location = $this->pathNormalizer->normalizePath($location);
142
143        $this->deletedFiles->createDirectory($location, new Config([]));
144        $this->inMemoryFiles->deleteDirectory($location);
145    }
146
147    /**
148     * @param array{visibility?:string} $config
149     * @throws FilesystemException
150     */
151    public function createDirectory(string $location, array $config = []): void
152    {
153        $location = $this->pathNormalizer->normalizePath($location);
154
155        $this->inMemoryFiles->createDirectory($location, new Config($config));
156
157        $this->deletedFiles->deleteDirectory($location);
158    }
159
160    public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing
161    {
162        $location = $this->pathNormalizer->normalizePath($location);
163
164        /** @var FileAttributes[] $actual */
165        $actual = $this->filesystem->listContents($location, $deep)->toArray();
166
167        $inMemoryFilesGenerator = $this->inMemoryFiles->listContents($location, $deep);
168        $inMemoryFilesArray = $inMemoryFilesGenerator instanceof Traversable
169            ? iterator_to_array($inMemoryFilesGenerator, false)
170            : (array) $inMemoryFilesGenerator;
171
172        $inMemoryFilePaths = array_map(fn($file) => $file->path(), $inMemoryFilesArray);
173
174        $deletedFilesGenerator = $this->deletedFiles->listContents($location, $deep);
175        $deletedFilesArray = $deletedFilesGenerator instanceof Traversable
176            ? iterator_to_array($deletedFilesGenerator, false)
177            : (array) $deletedFilesGenerator;
178        $deletedFilePaths = array_map(fn($file) => $file->path(), $deletedFilesArray);
179
180        $actual = array_filter($actual, fn($file) => !in_array($file->path(), $inMemoryFilePaths));
181        $actual = array_filter($actual, fn($file) => !in_array($file->path(), $deletedFilePaths));
182
183        $good = array_merge($actual, $inMemoryFilesArray);
184
185        return new DirectoryListing($good);
186    }
187
188    /**
189     * @param array{visibility?:string} $config
190     */
191    public function move(string $source, string $destination, array $config = []): void
192    {
193        throw new BadMethodCallException('Not yet implemented');
194    }
195
196    /**
197     * @param Config|array{visibility?:string}|null $config
198     * @throws FilesystemException
199     * @throws Exception
200     */
201    public function copy(string $source, string $destination, $config = null): void
202    {
203        $source = $this->pathNormalizer->normalizePath($source);
204        $destination = $this->pathNormalizer->normalizePath($destination);
205
206        $sourceFile = $this->read($source);
207
208        $this->inMemoryFiles->write(
209            $destination,
210            $sourceFile,
211            $config instanceof Config ? $config : new Config($config ?? [])
212        );
213
214        $a = $this->inMemoryFiles->read($destination);
215        if ($sourceFile !== $a) {
216            throw new Exception('Copy failed');
217        }
218
219        if ($this->deletedFiles->fileExists($destination)) {
220            $this->deletedFiles->delete($destination);
221        }
222    }
223
224    /**
225     * @throws FilesystemException
226     */
227    private function getAttributes(string $path): StorageAttributes
228    {
229        $path = $this->pathNormalizer->normalizePath($path);
230
231        $parentDirectoryContents = $this->listContents(dirname($path), false);
232        /** @var FileAttributes $entry */
233        foreach ($parentDirectoryContents as $entry) {
234            if ($entry->path() == $path) {
235                return $entry;
236            }
237        }
238        throw UnableToReadFile::fromLocation($path);
239    }
240
241    public function lastModified(string $path): int
242    {
243        $attributes = $this->getAttributes($this->pathNormalizer->normalizePath($path));
244        return $attributes->lastModified() ?? 0;
245    }
246
247    public function fileSize(string $path): int
248    {
249        $path = $this->pathNormalizer->normalizePath($path);
250
251        $filesize = 0;
252
253        if ($this->inMemoryFiles->fileExists($path)) {
254            $filesize = $this->inMemoryFiles->fileSize($path);
255        } elseif ($this->filesystem->fileExists($path)) {
256            $filesize = $this->filesystem->fileSize($path);
257        }
258
259        if ($filesize instanceof FileAttributes) {
260            return $filesize->fileSize() ?? 0;
261        }
262
263        return $filesize;
264    }
265
266    public function mimeType(string $path): string
267    {
268        throw new BadMethodCallException('Not yet implemented');
269    }
270
271    public function setVisibility(string $path, string $visibility): void
272    {
273        throw new BadMethodCallException('Not yet implemented');
274    }
275
276    public function visibility(string $path): string
277    {
278        $defaultVisibility = Visibility::PUBLIC;
279
280        $path = $this->pathNormalizer->normalizePath($path);
281
282        if (!$this->fileExists($path) && !$this->directoryExists($path)) {
283            throw UnableToRetrieveMetadata::visibility($path, 'file does not exist');
284        }
285
286        if ($this->deletedFiles->fileExists($path)) {
287            throw UnableToRetrieveMetadata::visibility($path, 'file does not exist');
288        }
289        if ($this->inMemoryFiles->fileExists($path)) {
290            $attributes = $this->inMemoryFiles->visibility($path);
291            return $attributes->visibility() ?? $defaultVisibility;
292        }
293        if ($this->filesystem->fileExists($path)) {
294            return $this->filesystem->visibility($path);
295        }
296        return $defaultVisibility;
297    }
298
299    public function directoryExists(string $location): bool
300    {
301        $location = $this->pathNormalizer->normalizePath($location);
302
303        if ($this->directoryExistsIn($location, $this->deletedFiles)) {
304            return false;
305        }
306
307        return  $this->directoryExistsIn($location, $this->inMemoryFiles)
308            || $this->directoryExistsIn($location, $this->filesystem);
309    }
310
311    /**
312     *
313     * @param string $location
314     * @param object|FilesystemReader $filesystem
315     * @return bool
316     * @throws FilesystemException
317     */
318    protected function directoryExistsIn(string $location, $filesystem): bool
319    {
320        $location = $this->pathNormalizer->normalizePath($location);
321
322        if (method_exists($filesystem, 'directoryExists')) {
323            return $filesystem->directoryExists($location);
324        }
325
326        /** @var FileSystemReader $filesystem */
327        $parentDirectoryContents = $filesystem->listContents(
328            $this->pathNormalizer->normalizePath(dirname($location)),
329            false
330        );
331
332        /** @var FileAttributes $entry */
333        foreach ($parentDirectoryContents as $entry) {
334            if ($entry->path() == $location) {
335                return $entry->isDir();
336            }
337        }
338
339        return false;
340    }
341
342    public function has(string $location): bool
343    {
344        throw new BadMethodCallException('Not yet implemented');
345    }
346}