Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.90% covered (warning)
56.90%
66 / 116
60.00% covered (warning)
60.00%
15 / 25
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileSystem
56.90% covered (warning)
56.90%
66 / 116
60.00% covered (warning)
60.00%
15 / 25
153.64
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 findAllFilesAbsolutePaths
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 getAttributes
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 fileExists
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 read
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 readStream
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 listContents
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 lastModified
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 fileSize
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 mimeType
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 visibility
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 write
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 writeStream
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 setVisibility
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 delete
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 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%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 move
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 copy
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getRelativePath
58.82% covered (warning)
58.82%
10 / 17
0.00% covered (danger)
0.00%
0 / 1
8.51
 getProjectRelativePath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 isSymlinkedFile
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isSubDirOf
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 normalize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * This class extends Flysystem's Filesystem class to add some additional functionality, particularly around
4 * symlinks which are not supported by Flysystem.
5 *
6 * TODO: Delete and modify operations on files in symlinked directories should fail with a warning.
7 *
8 * @see https://github.com/thephpleague/flysystem/issues/599
9 */
10
11namespace BrianHenryIE\Strauss\Helpers;
12
13use BrianHenryIE\Strauss\Files\FileBase;
14use Elazar\Flystream\StripProtocolPathNormalizer;
15use League\Flysystem\DirectoryAttributes;
16use League\Flysystem\DirectoryListing;
17use League\Flysystem\FileAttributes;
18use League\Flysystem\FilesystemException;
19use League\Flysystem\FilesystemOperator;
20use League\Flysystem\FilesystemReader;
21use League\Flysystem\PathNormalizer;
22use League\Flysystem\StorageAttributes;
23
24class FileSystem implements FilesystemOperator, FlysystemBackCompatInterface
25{
26    use FlysystemBackCompatTrait;
27
28    protected FilesystemOperator $flysystem;
29
30    protected PathNormalizer $normalizer;
31
32    protected string $workingDir;
33
34    /**
35     * TODO: maybe restrict the constructor to only accept a LocalFilesystemAdapter.
36     *
37     * TODO: Check are any of these methods unused
38     */
39    public function __construct(FilesystemOperator $flysystem, string $workingDir)
40    {
41        $this->flysystem = $flysystem;
42        $this->normalizer = new StripProtocolPathNormalizer('mem');
43
44        $this->workingDir = $workingDir;
45    }
46
47    /**
48     * @param string[] $fileAndDirPaths
49     *
50     * @return string[]
51     * @throws FilesystemException
52     */
53    public function findAllFilesAbsolutePaths(array $fileAndDirPaths, bool $excludeDirectories = false): array
54    {
55        $files = [];
56
57        foreach ($fileAndDirPaths as $path) {
58            if (!$this->directoryExists($path)) {
59                $files[] = $path;
60                continue;
61            }
62
63            $directoryListing = $this->listContents(
64                $path,
65                FilesystemReader::LIST_DEEP
66            );
67
68            /** @var FileAttributes[] $fileAttributesArray */
69            $fileAttributesArray = $directoryListing->toArray();
70
71
72            $f = array_map(
73                fn(StorageAttributes $attributes): string => '/'.$attributes->path(),
74                $fileAttributesArray
75            );
76
77            if ($excludeDirectories) {
78                $f = array_filter($f, fn($path) => !$this->directoryExists($path));
79            }
80
81            $files = array_merge($files, $f);
82        }
83
84        return $files;
85    }
86
87    /**
88     * @throws FilesystemException
89     */
90    public function getAttributes(string $absolutePath): ?StorageAttributes
91    {
92        // TODO: check if `realpath()` is a bad idea here.
93        $fileDirectory = realpath(dirname($absolutePath)) ?: dirname($absolutePath);
94
95        $absolutePath = $this->normalizer->normalizePath($absolutePath);
96
97        // Unsupported symbolic link encountered at location //home
98        // \League\Flysystem\SymbolicLinkEncountered
99        $dirList = $this->listContents($fileDirectory)->toArray();
100        foreach ($dirList as $file) { // TODO: use the generator.
101            if ($file->path() === $absolutePath) {
102                return $file;
103            }
104        }
105
106        return null;
107    }
108
109    /**
110     * @throws FilesystemException
111     */
112    public function exists(string $location): bool
113    {
114        return $this->fileExists($location) || $this->directoryExists($location);
115    }
116
117    public function fileExists(string $location): bool
118    {
119        return $this->flysystem->fileExists(
120            $this->normalizer->normalizePath($location)
121        );
122    }
123
124    public function read(string $location): string
125    {
126        return $this->flysystem->read(
127            $this->normalizer->normalizePath($location)
128        );
129    }
130
131    public function readStream(string $location)
132    {
133        return $this->flysystem->readStream(
134            $this->normalizer->normalizePath($location)
135        );
136    }
137
138    public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing
139    {
140        return $this->flysystem->listContents(
141            $this->normalizer->normalizePath($location),
142            $deep
143        );
144    }
145
146    public function lastModified(string $path): int
147    {
148        return $this->flysystem->lastModified(
149            $this->normalizer->normalizePath($path)
150        );
151    }
152
153    public function fileSize(string $path): int
154    {
155        return $this->flysystem->fileSize(
156            $this->normalizer->normalizePath($path)
157        );
158    }
159
160    public function mimeType(string $path): string
161    {
162        return $this->flysystem->mimeType(
163            $this->normalizer->normalizePath($path)
164        );
165    }
166
167    public function visibility(string $path): string
168    {
169        return $this->flysystem->visibility(
170            $this->normalizer->normalizePath($path)
171        );
172    }
173
174    /**
175     * @param array{visibility?:string} $config
176     * @throws FilesystemException
177     */
178    public function write(string $location, string $contents, array $config = []): void
179    {
180        $this->flysystem->write(
181            $this->normalizer->normalizePath($location),
182            $contents,
183            $config
184        );
185    }
186
187    /**
188     * @param array{visibility?:string} $config
189     * @throws FilesystemException
190     */
191    public function writeStream(string $location, $contents, array $config = []): void
192    {
193        $this->flysystem->writeStream(
194            $this->normalizer->normalizePath($location),
195            $contents,
196            $config
197        );
198    }
199
200    public function setVisibility(string $path, string $visibility): void
201    {
202        $this->flysystem->setVisibility(
203            $this->normalizer->normalizePath($path),
204            $visibility
205        );
206    }
207
208    public function delete(string $location): void
209    {
210        $this->flysystem->delete(
211            $this->normalizer->normalizePath($location)
212        );
213    }
214
215    public function deleteDirectory(string $location): void
216    {
217        $this->flysystem->deleteDirectory(
218            $this->normalizer->normalizePath($location)
219        );
220    }
221
222    /**
223     * @param array{visibility?:string} $config
224     * @throws FilesystemException
225     */
226    public function createDirectory(string $location, array $config = []): void
227    {
228        $this->flysystem->createDirectory(
229            $this->normalizer->normalizePath($location),
230            $config
231        );
232    }
233
234    /**
235     * @param array{visibility?:string} $config
236     * @throws FilesystemException
237     */
238    public function move(string $source, string $destination, array $config = []): void
239    {
240        $this->flysystem->move(
241            $this->normalizer->normalizePath($source),
242            $this->normalizer->normalizePath($destination),
243            $config
244        );
245    }
246
247    /**
248     * @param array{visibility?:string} $config
249     * @throws FilesystemException
250     */
251    public function copy(string $source, string $destination, array $config = []): void
252    {
253        $this->flysystem->copy(
254            $this->normalizer->normalizePath($source),
255            $this->normalizer->normalizePath($destination),
256            $config
257        );
258    }
259
260    /**
261     *
262     * /path/to/this/dir, /path/to/file.php => ../../file.php
263     * /path/to/here, /path/to/here/dir/file.php => dir/file.php
264     *
265     * @param string $fromAbsoluteDirectory
266     * @param string $toAbsolutePath
267     * @return string
268     */
269    public function getRelativePath(string $fromAbsoluteDirectory, string $toAbsolutePath): string
270    {
271        $fromAbsoluteDirectory = $this->normalizer->normalizePath($fromAbsoluteDirectory);
272        $toAbsolutePath = $this->normalizer->normalizePath($toAbsolutePath);
273
274        $fromDirectoryParts = array_filter(explode('/', $fromAbsoluteDirectory));
275        $toPathParts = array_filter(explode('/', $toAbsolutePath));
276        foreach ($fromDirectoryParts as $key => $part) {
277            if ($part === $toPathParts[$key]) {
278                unset($toPathParts[$key]);
279                unset($fromDirectoryParts[$key]);
280            } else {
281                break;
282            }
283            if (count($fromDirectoryParts) === 0 || count($toPathParts) === 0) {
284                break;
285            }
286        }
287
288        $relativePath =
289            str_repeat('../', count($fromDirectoryParts))
290            . implode('/', $toPathParts);
291
292        if ($this->directoryExists($toAbsolutePath)) {
293            $relativePath .= '/';
294        }
295
296        return $relativePath;
297    }
298
299    public function getProjectRelativePath(string $absolutePath): string
300    {
301
302        // What will happen with strings that are not paths?!
303
304        return $this->getRelativePath(
305            $this->workingDir,
306            $absolutePath
307        );
308    }
309
310    /**
311     * Check does the filepath point to a file outside the working directory.
312     * If `realpath()` fails to resolve the path, assume it's a symlink.
313     */
314    public function isSymlinkedFile(FileBase $file): bool
315    {
316        $realpath = realpath($file->getSourcePath());
317
318        return ! $realpath || ! str_starts_with($realpath, $this->workingDir);
319    }
320
321    /**
322     * Does the subDir path start with the dir path?
323     */
324    public function isSubDirOf(string $dir, string $subDir): bool
325    {
326        return str_starts_with(
327            $this->normalizer->normalizePath($subDir),
328            $this->normalizer->normalizePath($dir)
329        );
330    }
331
332    public function normalize(string $path): string
333    {
334        return $this->normalizer->normalizePath($path);
335    }
336}