3 namespace BookStack\Uploads;
5 use Illuminate\Contracts\Filesystem\Filesystem;
6 use Illuminate\Filesystem\FilesystemAdapter;
7 use League\Flysystem\WhitespacePathNormalizer;
8 use Symfony\Component\HttpFoundation\StreamedResponse;
10 class ImageStorageDisk
12 public function __construct(
13 protected string $diskName,
14 protected Filesystem $filesystem,
19 * Check if local secure image storage (Fetched behind authentication)
20 * is currently active in the instance.
22 public function usingSecureImages(): bool
24 return $this->diskName === 'local_secure_images';
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.
31 protected function adjustPathForDisk(string $path): string
33 $path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/images/', '', $path));
35 if ($this->usingSecureImages()) {
39 return 'uploads/images/' . $path;
43 * Check if a file at the given path exists.
45 public function exists(string $path): bool
47 return $this->filesystem->exists($this->adjustPathForDisk($path));
51 * Get the file at the given path.
53 public function get(string $path): ?string
55 return $this->filesystem->get($this->adjustPathForDisk($path));
59 * Get a stream to the file at the given path.
62 public function stream(string $path): mixed
64 return $this->filesystem->readStream($this->adjustPathForDisk($path));
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.
71 public function put(string $path, string $data, bool $makePublic = false): void
73 $path = $this->adjustPathForDisk($path);
74 $this->filesystem->put($path, $data);
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');
86 * Destroys an image at the given path.
87 * Searches for image thumbnails in addition to main provided path.
89 public function destroyAllMatchingNameFromPath(string $path): void
91 $path = $this->adjustPathForDisk($path);
93 $imageFolder = dirname($path);
94 $imageFileName = basename($path);
95 $allImages = collect($this->filesystem->allFiles($imageFolder));
98 $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
99 return basename($imagePath) === $imageFileName;
101 $this->filesystem->delete($imagesToDelete->all());
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);
113 * Get the mime type of the file at the given path.
114 * Only works for local filesystem adapters.
116 public function mimeType(string $path): string
118 $path = $this->adjustPathForDisk($path);
119 return $this->filesystem instanceof FilesystemAdapter ? $this->filesystem->mimeType($path) : '';
123 * Get a stream response for the image at the given path.
125 public function response(string $path): StreamedResponse
127 return $this->filesystem->response($this->adjustPathForDisk($path));
131 * Check if the image storage in use is an S3-like (but not likely S3) external system.
133 protected function isS3Like(): bool
135 $usingS3 = $this->diskName === 's3';
136 return $usingS3 && !is_null(config('filesystems.disks.s3.endpoint'));
140 * Check whether a folder is empty.
142 protected function isFolderEmpty(string $path): bool
144 $files = $this->filesystem->files($path);
145 $folders = $this->filesystem->directories($path);
147 return count($files) === 0 && count($folders) === 0;