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