Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 113
0.00% covered (danger)
0.00%
0 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReadOnlyFileSystem
0.00% covered (danger)
0.00%
0 / 113
0.00% covered (danger)
0.00%
0 / 22
2970
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 fileExists
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 write
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 writeStream
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 rewindStream
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 read
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 readStream
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 delete
0.00% covered (danger)
0.00%
0 / 5
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
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 listContents
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 move
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 copy
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 getAttributes
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 lastModified
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 fileSize
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 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
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 directoryExists
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 directoryExistsIn
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 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        if ($this->deletedFiles->fileExists($location)) {
49            return false;
50        }
51        return $this->inMemoryFiles->fileExists($location)
52                || $this->filesystem->fileExists($location);
53    }
54
55    /**
56     * @param array{visibility?:string} $config
57     * @throws FilesystemException
58     */
59    public function write(string $location, string $contents, array $config = []): void
60    {
61        $config = new Config($config);
62        $this->inMemoryFiles->write($location, $contents, $config);
63
64        if ($this->deletedFiles->fileExists($location)) {
65            $this->deletedFiles->delete($location);
66        }
67    }
68
69    /**
70     * @param resource $contents
71     * @param array{visibility?:string} $config
72     * @throws FilesystemException
73     */
74    public function writeStream(string $location, $contents, $config = []): void
75    {
76        $config = new Config($config);
77        $this->rewindStream($contents);
78        $this->inMemoryFiles->writeStream($location, $contents, $config);
79
80        if ($this->deletedFiles->fileExists($location)) {
81            $this->deletedFiles->delete($location);
82        }
83    }
84    /**
85     * @param resource $resource
86     */
87    private function rewindStream($resource): void
88    {
89        if (ftell($resource) !== 0 && stream_get_meta_data($resource)['seekable']) {
90            rewind($resource);
91        }
92    }
93
94    public function read(string $location): string
95    {
96        if ($this->deletedFiles->fileExists($location)) {
97            throw UnableToReadFile::fromLocation($location);
98        }
99        if ($this->inMemoryFiles->fileExists($location)) {
100            return $this->inMemoryFiles->read($location);
101        }
102        return $this->filesystem->read($location);
103    }
104
105    public function readStream(string $location)
106    {
107        if ($this->deletedFiles->fileExists($location)) {
108            throw UnableToReadFile::fromLocation($location);
109        }
110        if ($this->inMemoryFiles->fileExists($location)) {
111            return $this->inMemoryFiles->readStream($location);
112        }
113        return $this->filesystem->readStream($location);
114    }
115
116    public function delete(string $location): void
117    {
118        if ($this->fileExists($location)) {
119            $file = $this->read($location);
120            $this->deletedFiles->write($location, $file, new Config([]));
121        }
122        if ($this->inMemoryFiles->fileExists($location)) {
123            $this->inMemoryFiles->delete($location);
124        }
125    }
126
127    public function deleteDirectory(string $location): void
128    {
129        $location = $this->pathNormalizer->normalizePath($location);
130
131        $this->deletedFiles->createDirectory($location, new Config([]));
132        $this->inMemoryFiles->deleteDirectory($location);
133    }
134
135    /**
136     * @param array{visibility?:string} $config
137     * @throws FilesystemException
138     */
139    public function createDirectory(string $location, array $config = []): void
140    {
141        $this->inMemoryFiles->createDirectory($location, new Config($config));
142
143        $this->deletedFiles->deleteDirectory($location);
144    }
145
146    public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing
147    {
148        /** @var FileAttributes[] $actual */
149        $actual = $this->filesystem->listContents($location, $deep)->toArray();
150
151        $inMemoryFilesGenerator = $this->inMemoryFiles->listContents($location, $deep);
152        $inMemoryFilesArray = $inMemoryFilesGenerator instanceof Traversable
153            ? iterator_to_array($inMemoryFilesGenerator, false)
154            : (array) $inMemoryFilesGenerator;
155
156        $inMemoryFilePaths = array_map(fn($file) => $file->path(), $inMemoryFilesArray);
157
158        $deletedFilesGenerator = $this->deletedFiles->listContents($location, $deep);
159        $deletedFilesArray = $deletedFilesGenerator instanceof Traversable
160            ? iterator_to_array($deletedFilesGenerator, false)
161            : (array) $deletedFilesGenerator;
162        $deletedFilePaths = array_map(fn($file) => $file->path(), $deletedFilesArray);
163
164        $actual = array_filter($actual, fn($file) => !in_array($file->path(), $inMemoryFilePaths));
165        $actual = array_filter($actual, fn($file) => !in_array($file->path(), $deletedFilePaths));
166
167        $good = array_merge($actual, $inMemoryFilesArray);
168
169        return new DirectoryListing($good);
170    }
171
172    /**
173     * @param array{visibility?:string} $config
174     */
175    public function move(string $source, string $destination, array $config = []): void
176    {
177        throw new BadMethodCallException('Not yet implemented');
178    }
179
180    /**
181     * @param Config|array{visibility?:string}|null $config
182     * @throws FilesystemException
183     * @throws Exception
184     */
185    public function copy(string $source, string $destination, $config = null): void
186    {
187        $sourceFile = $this->read($source);
188
189        $this->inMemoryFiles->write(
190            $destination,
191            $sourceFile,
192            $config instanceof Config ? $config : new Config($config ?? [])
193        );
194
195        $a = $this->inMemoryFiles->read($destination);
196        if ($sourceFile !== $a) {
197            throw new Exception('Copy failed');
198        }
199
200        if ($this->deletedFiles->fileExists($destination)) {
201            $this->deletedFiles->delete($destination);
202        }
203    }
204
205    /**
206     * @throws FilesystemException
207     */
208    private function getAttributes(string $path): StorageAttributes
209    {
210        $parentDirectoryContents = $this->listContents(dirname($path), false);
211        /** @var FileAttributes $entry */
212        foreach ($parentDirectoryContents as $entry) {
213            if ($entry->path() == $path) {
214                return $entry;
215            }
216        }
217        throw UnableToReadFile::fromLocation($path);
218    }
219
220    public function lastModified(string $path): int
221    {
222        $attributes = $this->getAttributes($path);
223        return $attributes->lastModified() ?? 0;
224    }
225
226    public function fileSize(string $path): int
227    {
228        $filesize = 0;
229
230        if ($this->inMemoryFiles->fileExists($path)) {
231            $filesize = $this->inMemoryFiles->fileSize($path);
232        } elseif ($this->filesystem->fileExists($path)) {
233            $filesize = $this->filesystem->fileSize($path);
234        }
235
236        if ($filesize instanceof FileAttributes) {
237            return $filesize->fileSize() ?? 0;
238        }
239
240        return $filesize;
241    }
242
243    public function mimeType(string $path): string
244    {
245        throw new BadMethodCallException('Not yet implemented');
246    }
247
248    public function setVisibility(string $path, string $visibility): void
249    {
250        throw new BadMethodCallException('Not yet implemented');
251    }
252
253    public function visibility(string $path): string
254    {
255        $defaultVisibility = Visibility::PUBLIC;
256
257        $path = $this->pathNormalizer->normalizePath($path);
258
259        if (!$this->fileExists($path) && !$this->directoryExists($path)) {
260            throw UnableToRetrieveMetadata::visibility($path, 'file does not exist');
261        }
262
263        if ($this->deletedFiles->fileExists($path)) {
264            throw UnableToRetrieveMetadata::visibility($path, 'file does not exist');
265        }
266        if ($this->inMemoryFiles->fileExists($path)) {
267            $attributes = $this->inMemoryFiles->visibility($path);
268            return $attributes->visibility() ?? $defaultVisibility;
269        }
270        if ($this->filesystem->fileExists($path)) {
271            return $this->filesystem->visibility($path);
272        }
273        return $defaultVisibility;
274    }
275
276    public function directoryExists(string $location): bool
277    {
278        $location = $this->pathNormalizer->normalizePath($location);
279
280        if ($this->directoryExistsIn($location, $this->deletedFiles)) {
281            return false;
282        }
283
284        return  $this->directoryExistsIn($location, $this->inMemoryFiles)
285            || $this->directoryExistsIn($location, $this->filesystem);
286    }
287
288    /**
289     *
290     * @param string $location
291     * @param object|FilesystemReader $filesystem
292     * @return bool
293     * @throws FilesystemException
294     */
295    protected function directoryExistsIn(string $location, $filesystem): bool
296    {
297        if (method_exists($filesystem, 'directoryExists')) {
298            return $filesystem->directoryExists($location);
299        }
300
301        /** @var FileSystemReader $filesystem */
302        $parentDirectoryContents = $filesystem->listContents(dirname($location), false);
303        /** @var FileAttributes $entry */
304        foreach ($parentDirectoryContents as $entry) {
305            if ($entry->path() == $location) {
306                return $entry->isDir();
307            }
308        }
309
310        return false;
311    }
312
313    public function has(string $location): bool
314    {
315        throw new BadMethodCallException('Not yet implemented');
316    }
317}