Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 112
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 / 112
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 / 11
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 League\Flysystem\Config;
12use League\Flysystem\DirectoryListing;
13use League\Flysystem\FileAttributes;
14use League\Flysystem\FilesystemOperator;
15use League\Flysystem\PathNormalizer;
16use League\Flysystem\StorageAttributes;
17use League\Flysystem\UnableToReadFile;
18use League\Flysystem\UnableToRetrieveMetadata;
19use League\Flysystem\WhitespacePathNormalizer;
20use Traversable;
21
22class ReadOnlyFileSystem implements FilesystemOperator, FlysystemBackCompatInterface
23{
24//  use FlysystemBackCompatTrait;
25    protected FilesystemOperator $filesystem;
26    protected InMemoryFilesystemAdapter $inMemoryFiles;
27    protected InMemoryFilesystemAdapter $deletedFiles;
28
29    protected PathNormalizer $pathNormalizer;
30
31    public function __construct(FilesystemOperator $filesystem, ?PathNormalizer $pathNormalizer = null)
32    {
33        $this->filesystem = $filesystem;
34
35        $this->inMemoryFiles = new InMemoryFilesystemAdapter();
36        $this->deletedFiles = new InMemoryFilesystemAdapter();
37
38        $this->pathNormalizer = $pathNormalizer ?? new WhitespacePathNormalizer();
39    }
40
41    public function fileExists(string $location): bool
42    {
43        if ($this->deletedFiles->fileExists($location)) {
44            return false;
45        }
46        return $this->inMemoryFiles->fileExists($location)
47                || $this->filesystem->fileExists($location);
48    }
49
50    public function write(string $location, string $contents, array $config = []): void
51    {
52        $config = new \League\Flysystem\Config($config);
53        $this->inMemoryFiles->write($location, $contents, $config);
54
55        if ($this->deletedFiles->fileExists($location)) {
56            $this->deletedFiles->delete($location);
57        }
58    }
59
60    public function writeStream(string $location, $contents, $config = []): void
61    {
62        $config = new \League\Flysystem\Config($config);
63        $this->rewindStream($contents);
64        $this->inMemoryFiles->writeStream($location, $contents, $config);
65
66        if ($this->deletedFiles->fileExists($location)) {
67            $this->deletedFiles->delete($location);
68        }
69    }
70    /**
71     * @param resource $resource
72     */
73    private function rewindStream($resource): void
74    {
75        if (ftell($resource) !== 0 && stream_get_meta_data($resource)['seekable']) {
76            rewind($resource);
77        }
78    }
79
80    public function read(string $location): string
81    {
82        if ($this->deletedFiles->fileExists($location)) {
83            throw UnableToReadFile::fromLocation($location);
84        }
85        if ($this->inMemoryFiles->fileExists($location)) {
86            return $this->inMemoryFiles->read($location);
87        }
88        return $this->filesystem->read($location);
89    }
90
91    public function readStream(string $location)
92    {
93        if ($this->deletedFiles->fileExists($location)) {
94            throw UnableToReadFile::fromLocation($location);
95        }
96        if ($this->inMemoryFiles->fileExists($location)) {
97            return $this->inMemoryFiles->readStream($location);
98        }
99        return $this->filesystem->readStream($location);
100    }
101
102    public function delete(string $location): void
103    {
104        if ($this->fileExists($location)) {
105            $file = $this->read($location);
106            $this->deletedFiles->write($location, $file, new Config([]));
107        }
108        if ($this->inMemoryFiles->fileExists($location)) {
109            $this->inMemoryFiles->delete($location);
110        }
111    }
112
113    public function deleteDirectory(string $location): void
114    {
115        $location = $this->pathNormalizer->normalizePath($location);
116
117        $this->deletedFiles->createDirectory($location, new Config([]));
118        $this->inMemoryFiles->deleteDirectory($location);
119    }
120
121
122    public function createDirectory(string $location, array $config = []): void
123    {
124        $this->inMemoryFiles->createDirectory($location, new Config($config));
125
126        $this->deletedFiles->deleteDirectory($location);
127    }
128
129    public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing
130    {
131        /** @var FileAttributes[] $actual */
132        $actual = $this->filesystem->listContents($location, $deep)->toArray();
133
134        $inMemoryFilesGenerator = $this->inMemoryFiles->listContents($location, $deep);
135        $inMemoryFilesArray = $inMemoryFilesGenerator instanceof Traversable
136            ? iterator_to_array($inMemoryFilesGenerator, false)
137            : (array) $inMemoryFilesGenerator;
138
139        $inMemoryFilePaths = array_map(fn($file) => $file->path(), $inMemoryFilesArray);
140
141        $deletedFilesGenerator = $this->deletedFiles->listContents($location, $deep);
142        $deletedFilesArray = $deletedFilesGenerator instanceof Traversable
143            ? iterator_to_array($deletedFilesGenerator, false)
144            : (array) $deletedFilesGenerator;
145        $deletedFilePaths = array_map(fn($file) => $file->path(), $deletedFilesArray);
146
147        $actual = array_filter($actual, fn($file) => !in_array($file->path(), $inMemoryFilePaths));
148        $actual = array_filter($actual, fn($file) => !in_array($file->path(), $deletedFilePaths));
149
150        $good = array_merge($actual, $inMemoryFilesArray);
151
152        return new DirectoryListing($good);
153    }
154
155    public function move(string $source, string $destination, array $config = []): void
156    {
157        throw new \BadMethodCallException('Not yet implemented');
158    }
159
160    public function copy(string $source, string $destination, $config = null): void
161    {
162        $sourceFile = $this->read($source);
163
164        $this->inMemoryFiles->write(
165            $destination,
166            $sourceFile,
167            $config instanceof Config ? $config : new Config($config ?? [])
168        );
169
170        $a = $this->inMemoryFiles->read($destination);
171        if ($sourceFile !== $a) {
172            throw new \Exception('Copy failed');
173        }
174
175        if ($this->deletedFiles->fileExists($destination)) {
176            $this->deletedFiles->delete($destination);
177        }
178    }
179
180    private function getAttributes(string $path): StorageAttributes
181    {
182        $parentDirectoryContents = $this->listContents(dirname($path), false);
183        /** @var FileAttributes $entry */
184        foreach ($parentDirectoryContents as $entry) {
185            if ($entry->path() == $path) {
186                return $entry;
187            }
188        }
189        throw UnableToReadFile::fromLocation($path);
190    }
191
192    public function lastModified(string $path): int
193    {
194        $attributes = $this->getAttributes($path);
195        return $attributes->lastModified() ?? 0;
196    }
197
198    public function fileSize(string $path): int
199    {
200        $filesize = 0;
201
202        if ($this->inMemoryFiles->fileExists($path)) {
203            $filesize = $this->inMemoryFiles->fileSize($path);
204        } elseif ($this->filesystem->fileExists($path)) {
205            $filesize = $this->filesystem->fileSize($path);
206        }
207
208        if ($filesize instanceof FileAttributes) {
209            return $filesize->fileSize();
210        }
211
212        return $filesize;
213    }
214
215    public function mimeType(string $path): string
216    {
217        throw new \BadMethodCallException('Not yet implemented');
218    }
219
220    public function setVisibility(string $path, string $visibility): void
221    {
222        throw new \BadMethodCallException('Not yet implemented');
223    }
224
225    public function visibility(string $path): string
226    {
227        $path = $this->pathNormalizer->normalizePath($path);
228
229        if (!$this->fileExists($path) && !$this->directoryExists($path)) {
230            throw UnableToRetrieveMetadata::visibility($path, 'file does not exist');
231        }
232
233        if ($this->deletedFiles->fileExists($path)) {
234            throw UnableToRetrieveMetadata::visibility($path, 'file does not exist');
235        }
236        if ($this->inMemoryFiles->fileExists($path)) {
237            $attribtes = $this->inMemoryFiles->visibility($path);
238            return $attribtes->visibility();
239        }
240        if ($this->filesystem->fileExists($path)) {
241            return $this->filesystem->visibility($path);
242        }
243        return \League\Flysystem\Visibility::PUBLIC;
244    }
245
246    public function directoryExists(string $location): bool
247    {
248        $location = $this->pathNormalizer->normalizePath($location);
249
250        if ($this->directoryExistsIn($location, $this->deletedFiles)) {
251            return false;
252        }
253
254        return  $this->directoryExistsIn($location, $this->inMemoryFiles)
255            || $this->directoryExistsIn($location, $this->filesystem);
256    }
257
258    protected function directoryExistsIn(string $location, $filesystem): bool
259    {
260        if (method_exists($filesystem, 'directoryExists')) {
261            return $filesystem->directoryExists($location);
262        }
263
264        $parentDirectoryContents = $filesystem->listContents(dirname($location), false);
265        /** @var FileAttributes $entry */
266        foreach ($parentDirectoryContents as $entry) {
267            if ($entry->path() == $location) {
268                return $entry->isDir();
269            }
270        }
271
272        return false;
273    }
274
275    public function has(string $location): bool
276    {
277        throw new \BadMethodCallException('Not yet implemented');
278    }
279}