Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46.15% covered (danger)
46.15%
18 / 39
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
LocalFileStorage
46.15% covered (danger)
46.15%
18 / 39
57.14% covered (warning)
57.14%
4 / 7
17.99
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createNewUpload
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getProxy
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 loadFromStorage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 psrToPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 normalizePath
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 pathToPsr
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\Core\FileStorage;
3
4use Apie\Core\Identifiers\KebabCaseSlug;
5use Apie\Core\ValueObjects\Utils;
6use Nyholm\Psr7\Factory\Psr17Factory;
7use Psr\Http\Message\UploadedFileInterface;
8use Symfony\Component\Mime\MimeTypes;
9use WeakMap;
10
11final class LocalFileStorage implements PsrAwareStorageInterface
12{
13    private string $path;
14
15    /**
16     * @var WeakMap<UploadedFileInterface, string> $mappedPaths
17     */
18    private WeakMap $mappedPaths;
19
20    /**
21     * @param array<string, string> $options
22     */
23    public function __construct(array $options)
24    {
25        $this->mappedPaths = new WeakMap();
26        $this->path = rtrim(Utils::toString($options['path']), '\\/') . DIRECTORY_SEPARATOR;
27    }
28
29    public function createNewUpload(
30        UploadedFileInterface $fileUpload,
31        string $className = StoredFile::class
32    ): StoredFile {
33        $storagePath =
34            uniqid(KebabCaseSlug::fromClass($className)->toNative(), true)
35            . ltrim($this->normalizePath($fileUpload->getClientFilename()));
36        $target = fopen($this->path . $storagePath, 'w+');
37        if ($fileUpload instanceof StoredFile) {
38            stream_copy_to_stream($fileUpload->getStream()->detach(), $target);
39        } else {
40            fputs($target, $fileUpload->getStream()->__toString());
41        }
42        fclose($target);
43
44        return $className::createFromLocalFile(
45            $this->path . $storagePath,
46            $fileUpload->getClientFilename()
47        )->markBeingStored($this, $storagePath);
48    }
49
50    public function getProxy(
51        string $storagePath,
52        string $className = StoredFile::class
53    ): StoredFile {
54        return $className::createFromStorage(
55            $this,
56            $storagePath
57        );
58    }
59
60    public function loadFromStorage(
61        string $storagePath,
62        string $className = StoredFile::class
63    ): StoredFile {
64        $path = ltrim($this->normalizePath($storagePath), '\\/');
65        $fullPath = $this->path . $path;
66        return StoredFile::createFromLocalFile(
67            $fullPath
68        )->markBeingStored($this, $storagePath);
69    }
70
71    public function psrToPath(UploadedFileInterface $uploadedFile): string
72    {
73        return $this->mappedPaths[$uploadedFile];
74    }
75
76    private function normalizePath(string $path): string
77    {
78        $patterns =['~/{2,}~', '~/(\./)+~', '~([^/\.]+/(?R)*\.{2,}/)~', '~\.\./~'];
79        $replacements = ['/', '/', '', ''];
80        return preg_replace($patterns, $replacements, $path);
81    }
82
83    public function pathToPsr(string $path): UploadedFileInterface
84    {
85        $path = ltrim($this->normalizePath($path), '\\/');
86        $fullPath = $this->path . $path;
87        $factory = new Psr17Factory();
88        $result = $factory->createUploadedFile(
89            $factory->createStreamFromFile($fullPath),
90            null,
91            UPLOAD_ERR_OK,
92            basename($fullPath),
93            MimeTypes::getDefault()->guessMimeType($fullPath),
94        );
95        $this->mappedPaths[$result] = $path;
96        return $result;
97    }
98}