]> BookStack Code Mirror - bookstack/blob - app/Uploads/ImageStorageDisk.php
Exports: Added rate limits for UI exports
[bookstack] / app / Uploads / ImageStorageDisk.php
1 <?php
2
3 namespace BookStack\Uploads;
4
5 use Illuminate\Contracts\Filesystem\Filesystem;
6 use Illuminate\Filesystem\FilesystemAdapter;
7 use League\Flysystem\WhitespacePathNormalizer;
8 use Symfony\Component\HttpFoundation\StreamedResponse;
9
10 class ImageStorageDisk
11 {
12     public function __construct(
13         protected string $diskName,
14         protected Filesystem $filesystem,
15     ) {
16     }
17
18     /**
19      * Check if local secure image storage (Fetched behind authentication)
20      * is currently active in the instance.
21      */
22     public function usingSecureImages(): bool
23     {
24         return $this->diskName === 'local_secure_images';
25     }
26
27     /**
28      * Change the originally provided path to fit any disk-specific requirements.
29      * This also ensures the path is kept to the expected root folders.
30      */
31     protected function adjustPathForDisk(string $path): string
32     {
33         $path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/images/', '', $path));
34
35         if ($this->usingSecureImages()) {
36             return $path;
37         }
38
39         return 'uploads/images/' . $path;
40     }
41
42     /**
43      * Check if a file at the given path exists.
44      */
45     public function exists(string $path): bool
46     {
47         return $this->filesystem->exists($this->adjustPathForDisk($path));
48     }
49
50     /**
51      * Get the file at the given path.
52      */
53     public function get(string $path): ?string
54     {
55         return $this->filesystem->get($this->adjustPathForDisk($path));
56     }
57
58     /**
59      * Get a stream to the file at the given path.
60      * @returns ?resource
61      */
62     public function stream(string $path): mixed
63     {
64         return $this->filesystem->readStream($this->adjustPathForDisk($path));
65     }
66
67     /**
68      * Save the given image data at the given path. Can choose to set
69      * the image as public which will update its visibility after saving.
70      */
71     public function put(string $path, string $data, bool $makePublic = false): void
72     {
73         $path = $this->adjustPathForDisk($path);
74         $this->filesystem->put($path, $data);
75
76         // Set visibility when a non-AWS-s3, s3-like storage option is in use.
77         // Done since this call can break s3-like services but desired for other image stores.
78         // Attempting to set ACL during above put request requires different permissions
79         // hence would technically be a breaking change for actual s3 usage.
80         if ($makePublic && !$this->isS3Like()) {
81             $this->filesystem->setVisibility($path, 'public');
82         }
83     }
84
85     /**
86      * Destroys an image at the given path.
87      * Searches for image thumbnails in addition to main provided path.
88      */
89     public function destroyAllMatchingNameFromPath(string $path): void
90     {
91         $path = $this->adjustPathForDisk($path);
92
93         $imageFolder = dirname($path);
94         $imageFileName = basename($path);
95         $allImages = collect($this->filesystem->allFiles($imageFolder));
96
97         // Delete image files
98         $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
99             return basename($imagePath) === $imageFileName;
100         });
101         $this->filesystem->delete($imagesToDelete->all());
102
103         // Cleanup of empty folders
104         $foldersInvolved = array_merge([$imageFolder], $this->filesystem->directories($imageFolder));
105         foreach ($foldersInvolved as $directory) {
106             if ($this->isFolderEmpty($directory)) {
107                 $this->filesystem->deleteDirectory($directory);
108             }
109         }
110     }
111
112     /**
113      * Get the mime type of the file at the given path.
114      * Only works for local filesystem adapters.
115      */
116     public function mimeType(string $path): string
117     {
118         $path = $this->adjustPathForDisk($path);
119         return $this->filesystem instanceof FilesystemAdapter ? $this->filesystem->mimeType($path) : '';
120     }
121
122     /**
123      * Get a stream response for the image at the given path.
124      */
125     public function response(string $path): StreamedResponse
126     {
127         return $this->filesystem->response($this->adjustPathForDisk($path));
128     }
129
130     /**
131      * Check if the image storage in use is an S3-like (but not likely S3) external system.
132      */
133     protected function isS3Like(): bool
134     {
135         $usingS3 = $this->diskName === 's3';
136         return $usingS3 && !is_null(config('filesystems.disks.s3.endpoint'));
137     }
138
139     /**
140      * Check whether a folder is empty.
141      */
142     protected function isFolderEmpty(string $path): bool
143     {
144         $files = $this->filesystem->files($path);
145         $folders = $this->filesystem->directories($path);
146
147         return count($files) === 0 && count($folders) === 0;
148     }
149 }