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