3 namespace BookStack\Uploads;
5 use BookStack\Util\FilePathNormalizer;
6 use Illuminate\Contracts\Filesystem\Filesystem;
7 use Illuminate\Filesystem\FilesystemAdapter;
8 use Illuminate\Support\Facades\Log;
9 use League\Flysystem\UnableToSetVisibility;
10 use Symfony\Component\HttpFoundation\StreamedResponse;
12 class ImageStorageDisk
14 public function __construct(
15 protected string $diskName,
16 protected Filesystem $filesystem,
21 * Check if local secure image storage (Fetched behind authentication)
22 * is currently active in the instance.
24 public function usingSecureImages(): bool
26 return $this->diskName === 'local_secure_images';
30 * Change the originally provided path to fit any disk-specific requirements.
31 * This also ensures the path is kept to the expected root folders.
33 protected function adjustPathForDisk(string $path): string
35 $trimmed = str_replace('uploads/images/', '', $path);
36 $normalized = FilePathNormalizer::normalize($trimmed);
38 if ($this->usingSecureImages()) {
42 return 'uploads/images/' . $normalized;
46 * Check if a file at the given path exists.
48 public function exists(string $path): bool
50 return $this->filesystem->exists($this->adjustPathForDisk($path));
54 * Get the file at the given path.
56 public function get(string $path): ?string
58 return $this->filesystem->get($this->adjustPathForDisk($path));
62 * Get a stream to the file at the given path.
65 public function stream(string $path): mixed
67 return $this->filesystem->readStream($this->adjustPathForDisk($path));
71 * Save the given image data at the given path. Can choose to set
72 * the image as public which will update its visibility after saving.
74 public function put(string $path, string $data, bool $makePublic = false): void
76 $path = $this->adjustPathForDisk($path);
77 $this->filesystem->put($path, $data);
79 // Set public visibility to ensure public access on S3, or that the file is accessible
80 // to other processes (like web-servers) for local file storage options.
81 // We avoid attempting this for (non-AWS) s3-like systems (even in a try-catch) as
82 // we've always avoided setting permissions for s3-like due to potential issues,
83 // with docs advising setting pre-configured permissions instead.
84 // We also don't do this as the default filesystem/driver level as that can technically
85 // require different ACLs for S3, and this provides us more logical control.
86 if ($makePublic && !$this->isS3Like()) {
88 $this->filesystem->setVisibility($path, 'public');
89 } catch (UnableToSetVisibility $e) {
90 Log::warning("Unable to set visibility for image upload with relative path: {$path}");
96 * Destroys an image at the given path.
97 * Searches for image thumbnails in addition to main provided path.
99 public function destroyAllMatchingNameFromPath(string $path): void
101 $path = $this->adjustPathForDisk($path);
103 $imageFolder = dirname($path);
104 $imageFileName = basename($path);
105 $allImages = collect($this->filesystem->allFiles($imageFolder));
107 // Delete image files
108 $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
109 return basename($imagePath) === $imageFileName;
111 $this->filesystem->delete($imagesToDelete->all());
113 // Cleanup of empty folders
114 $foldersInvolved = array_merge([$imageFolder], $this->filesystem->directories($imageFolder));
115 foreach ($foldersInvolved as $directory) {
116 if ($this->isFolderEmpty($directory)) {
117 $this->filesystem->deleteDirectory($directory);
123 * Get the mime type of the file at the given path.
124 * Only works for local filesystem adapters.
126 public function mimeType(string $path): string
128 $path = $this->adjustPathForDisk($path);
129 return $this->filesystem instanceof FilesystemAdapter ? $this->filesystem->mimeType($path) : '';
133 * Get a stream response for the image at the given path.
135 public function response(string $path): StreamedResponse
137 return $this->filesystem->response($this->adjustPathForDisk($path));
141 * Check if the image storage in use is an S3-like (but not likely S3) external system.
143 protected function isS3Like(): bool
145 $usingS3 = $this->diskName === 's3';
146 return $usingS3 && !is_null(config('filesystems.disks.s3.endpoint'));
150 * Check whether a folder is empty.
152 protected function isFolderEmpty(string $path): bool
154 $files = $this->filesystem->files($path);
155 $folders = $this->filesystem->directories($path);
157 return count($files) === 0 && count($folders) === 0;