3 namespace BookStack\Uploads;
5 use BookStack\Exceptions\FileUploadException;
7 use Symfony\Component\HttpFoundation\File\UploadedFile;
9 class AttachmentService
11 public function __construct(
12 protected FileStorage $storage,
17 * Stream an attachment from storage.
19 * @return resource|null
21 public function streamAttachmentFromStorage(Attachment $attachment)
23 return $this->storage->getReadStream($attachment->path);
27 * Read the file size of an attachment from storage, in bytes.
29 public function getAttachmentFileSize(Attachment $attachment): int
31 return $this->storage->getSize($attachment->path);
35 * Store a new attachment upon user upload.
37 * @throws FileUploadException
39 public function saveNewUpload(UploadedFile $uploadedFile, int $pageId): Attachment
41 $attachmentName = $uploadedFile->getClientOriginalName();
42 $attachmentPath = $this->putFileInStorage($uploadedFile);
43 $largestExistingOrder = Attachment::query()->where('uploaded_to', '=', $pageId)->max('order');
45 /** @var Attachment $attachment */
46 $attachment = Attachment::query()->forceCreate([
47 'name' => $attachmentName,
48 'path' => $attachmentPath,
49 'extension' => $uploadedFile->getClientOriginalExtension(),
50 'uploaded_to' => $pageId,
51 'created_by' => user()->id,
52 'updated_by' => user()->id,
53 'order' => $largestExistingOrder + 1,
60 * Store an upload, saving to a file and deleting any existing uploads
61 * attached to that file.
63 * @throws FileUploadException
65 public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment): Attachment
67 if (!$attachment->external) {
68 $this->deleteFileInStorage($attachment);
71 $attachmentName = $uploadedFile->getClientOriginalName();
72 $attachmentPath = $this->putFileInStorage($uploadedFile);
74 $attachment->name = $attachmentName;
75 $attachment->path = $attachmentPath;
76 $attachment->external = false;
77 $attachment->extension = $uploadedFile->getClientOriginalExtension();
84 * Save a new File attachment from a given link and name.
86 public function saveNewFromLink(string $name, string $link, int $page_id): Attachment
88 $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
90 return Attachment::forceCreate([
95 'uploaded_to' => $page_id,
96 'created_by' => user()->id,
97 'updated_by' => user()->id,
98 'order' => $largestExistingOrder + 1,
103 * Updates the ordering for a listing of attached files.
105 public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId)
107 foreach ($attachmentOrder as $index => $attachmentId) {
108 Attachment::query()->where('uploaded_to', '=', $pageId)
109 ->where('id', '=', $attachmentId)
110 ->update(['order' => $index]);
115 * Update the details of a file.
117 public function updateFile(Attachment $attachment, array $requestData): Attachment
119 if (isset($requestData['name'])) {
120 $attachment->name = $requestData['name'];
123 $link = trim($requestData['link'] ?? '');
125 if (!$attachment->external) {
126 $this->deleteFileInStorage($attachment);
127 $attachment->external = true;
128 $attachment->extension = '';
130 $attachment->path = $link;
135 return $attachment->refresh();
139 * Delete a File from the database and storage.
143 public function deleteFile(Attachment $attachment)
145 if (!$attachment->external) {
146 $this->deleteFileInStorage($attachment);
149 $attachment->delete();
153 * Delete a file from the filesystem it sits on.
154 * Cleans any empty leftover folders.
156 public function deleteFileInStorage(Attachment $attachment): void
158 $this->storage->delete($attachment->path);
162 * Store a file in storage with the given filename.
164 * @throws FileUploadException
166 protected function putFileInStorage(UploadedFile $uploadedFile): string
168 $basePath = 'uploads/files/' . date('Y-m-M') . '/';
170 return $this->storage->uploadFile(
173 $uploadedFile->getClientOriginalExtension(),
179 * Get the file validation rules for attachments.
181 public static function getFileValidationRules(): array
183 return ['file', 'max:' . (config('app.upload_limit') * 1000)];