]> BookStack Code Mirror - bookstack/blob - app/Uploads/AttachmentService.php
ZIP Import: Finished base import process & error handling
[bookstack] / app / Uploads / AttachmentService.php
1 <?php
2
3 namespace BookStack\Uploads;
4
5 use BookStack\Exceptions\FileUploadException;
6 use Exception;
7 use Symfony\Component\HttpFoundation\File\UploadedFile;
8
9 class AttachmentService
10 {
11     public function __construct(
12         protected FileStorage $storage,
13     ) {
14     }
15
16     /**
17      * Stream an attachment from storage.
18      *
19      * @return resource|null
20      */
21     public function streamAttachmentFromStorage(Attachment $attachment)
22     {
23         return $this->storage->getReadStream($attachment->path);
24     }
25
26     /**
27      * Read the file size of an attachment from storage, in bytes.
28      */
29     public function getAttachmentFileSize(Attachment $attachment): int
30     {
31         return $this->storage->getSize($attachment->path);
32     }
33
34     /**
35      * Store a new attachment upon user upload.
36      *
37      * @throws FileUploadException
38      */
39     public function saveNewUpload(UploadedFile $uploadedFile, int $pageId): Attachment
40     {
41         $attachmentName = $uploadedFile->getClientOriginalName();
42         $attachmentPath = $this->putFileInStorage($uploadedFile);
43         $largestExistingOrder = Attachment::query()->where('uploaded_to', '=', $pageId)->max('order');
44
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,
54         ]);
55
56         return $attachment;
57     }
58
59     /**
60      * Store an upload, saving to a file and deleting any existing uploads
61      * attached to that file.
62      *
63      * @throws FileUploadException
64      */
65     public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment): Attachment
66     {
67         if (!$attachment->external) {
68             $this->deleteFileInStorage($attachment);
69         }
70
71         $attachmentName = $uploadedFile->getClientOriginalName();
72         $attachmentPath = $this->putFileInStorage($uploadedFile);
73
74         $attachment->name = $attachmentName;
75         $attachment->path = $attachmentPath;
76         $attachment->external = false;
77         $attachment->extension = $uploadedFile->getClientOriginalExtension();
78         $attachment->save();
79
80         return $attachment;
81     }
82
83     /**
84      * Save a new File attachment from a given link and name.
85      */
86     public function saveNewFromLink(string $name, string $link, int $page_id): Attachment
87     {
88         $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
89
90         return Attachment::forceCreate([
91             'name'        => $name,
92             'path'        => $link,
93             'external'    => true,
94             'extension'   => '',
95             'uploaded_to' => $page_id,
96             'created_by'  => user()->id,
97             'updated_by'  => user()->id,
98             'order'       => $largestExistingOrder + 1,
99         ]);
100     }
101
102     /**
103      * Updates the ordering for a listing of attached files.
104      */
105     public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId)
106     {
107         foreach ($attachmentOrder as $index => $attachmentId) {
108             Attachment::query()->where('uploaded_to', '=', $pageId)
109                 ->where('id', '=', $attachmentId)
110                 ->update(['order' => $index]);
111         }
112     }
113
114     /**
115      * Update the details of a file.
116      */
117     public function updateFile(Attachment $attachment, array $requestData): Attachment
118     {
119         $attachment->name = $requestData['name'];
120         $link = trim($requestData['link'] ?? '');
121
122         if (!empty($link)) {
123             if (!$attachment->external) {
124                 $this->deleteFileInStorage($attachment);
125                 $attachment->external = true;
126                 $attachment->extension = '';
127             }
128             $attachment->path = $requestData['link'];
129         }
130
131         $attachment->save();
132
133         return $attachment->refresh();
134     }
135
136     /**
137      * Delete a File from the database and storage.
138      *
139      * @throws Exception
140      */
141     public function deleteFile(Attachment $attachment)
142     {
143         if (!$attachment->external) {
144             $this->deleteFileInStorage($attachment);
145         }
146
147         $attachment->delete();
148     }
149
150     /**
151      * Delete a file from the filesystem it sits on.
152      * Cleans any empty leftover folders.
153      */
154     public function deleteFileInStorage(Attachment $attachment): void
155     {
156         $this->storage->delete($attachment->path);
157     }
158
159     /**
160      * Store a file in storage with the given filename.
161      *
162      * @throws FileUploadException
163      */
164     protected function putFileInStorage(UploadedFile $uploadedFile): string
165     {
166         $basePath = 'uploads/files/' . date('Y-m-M') . '/';
167
168         return $this->storage->uploadFile(
169             $uploadedFile,
170             $basePath,
171             $uploadedFile->getClientOriginalExtension(),
172             ''
173         );
174     }
175
176     /**
177      * Get the file validation rules for attachments.
178      */
179     public static function getFileValidationRules(): array
180     {
181         return ['file', 'max:' . (config('app.upload_limit') * 1000)];
182     }
183 }