]> BookStack Code Mirror - bookstack/blob - app/Uploads/AttachmentService.php
Apply fixes from StyleCI
[bookstack] / app / Uploads / AttachmentService.php
1 <?php
2
3 namespace BookStack\Uploads;
4
5 use BookStack\Exceptions\FileUploadException;
6 use Exception;
7 use Illuminate\Contracts\Filesystem\Factory as FileSystem;
8 use Illuminate\Contracts\Filesystem\FileNotFoundException;
9 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
10 use Illuminate\Support\Str;
11 use Log;
12 use Symfony\Component\HttpFoundation\File\UploadedFile;
13
14 class AttachmentService
15 {
16     protected $fileSystem;
17
18     /**
19      * AttachmentService constructor.
20      */
21     public function __construct(FileSystem $fileSystem)
22     {
23         $this->fileSystem = $fileSystem;
24     }
25
26     /**
27      * Get the storage that will be used for storing files.
28      */
29     protected function getStorage(): FileSystemInstance
30     {
31         $storageType = config('filesystems.attachments');
32
33         // Override default location if set to local public to ensure not visible.
34         if ($storageType === 'local') {
35             $storageType = 'local_secure';
36         }
37
38         return $this->fileSystem->disk($storageType);
39     }
40
41     /**
42      * Get an attachment from storage.
43      *
44      * @throws FileNotFoundException
45      */
46     public function getAttachmentFromStorage(Attachment $attachment): string
47     {
48         return $this->getStorage()->get($attachment->path);
49     }
50
51     /**
52      * Store a new attachment upon user upload.
53      *
54      * @param UploadedFile $uploadedFile
55      * @param int          $page_id
56      *
57      * @throws FileUploadException
58      *
59      * @return Attachment
60      */
61     public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
62     {
63         $attachmentName = $uploadedFile->getClientOriginalName();
64         $attachmentPath = $this->putFileInStorage($uploadedFile);
65         $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
66
67         $attachment = Attachment::forceCreate([
68             'name'        => $attachmentName,
69             'path'        => $attachmentPath,
70             'extension'   => $uploadedFile->getClientOriginalExtension(),
71             'uploaded_to' => $page_id,
72             'created_by'  => user()->id,
73             'updated_by'  => user()->id,
74             'order'       => $largestExistingOrder + 1,
75         ]);
76
77         return $attachment;
78     }
79
80     /**
81      * Store a upload, saving to a file and deleting any existing uploads
82      * attached to that file.
83      *
84      * @param UploadedFile $uploadedFile
85      * @param Attachment   $attachment
86      *
87      * @throws FileUploadException
88      *
89      * @return Attachment
90      */
91     public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment)
92     {
93         if (!$attachment->external) {
94             $this->deleteFileInStorage($attachment);
95         }
96
97         $attachmentName = $uploadedFile->getClientOriginalName();
98         $attachmentPath = $this->putFileInStorage($uploadedFile);
99
100         $attachment->name = $attachmentName;
101         $attachment->path = $attachmentPath;
102         $attachment->external = false;
103         $attachment->extension = $uploadedFile->getClientOriginalExtension();
104         $attachment->save();
105
106         return $attachment;
107     }
108
109     /**
110      * Save a new File attachment from a given link and name.
111      */
112     public function saveNewFromLink(string $name, string $link, int $page_id): Attachment
113     {
114         $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
115
116         return Attachment::forceCreate([
117             'name'        => $name,
118             'path'        => $link,
119             'external'    => true,
120             'extension'   => '',
121             'uploaded_to' => $page_id,
122             'created_by'  => user()->id,
123             'updated_by'  => user()->id,
124             'order'       => $largestExistingOrder + 1,
125         ]);
126     }
127
128     /**
129      * Updates the ordering for a listing of attached files.
130      */
131     public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId)
132     {
133         foreach ($attachmentOrder as $index => $attachmentId) {
134             Attachment::query()->where('uploaded_to', '=', $pageId)
135                 ->where('id', '=', $attachmentId)
136                 ->update(['order' => $index]);
137         }
138     }
139
140     /**
141      * Update the details of a file.
142      */
143     public function updateFile(Attachment $attachment, array $requestData): Attachment
144     {
145         $attachment->name = $requestData['name'];
146
147         if (isset($requestData['link']) && trim($requestData['link']) !== '') {
148             $attachment->path = $requestData['link'];
149             if (!$attachment->external) {
150                 $this->deleteFileInStorage($attachment);
151                 $attachment->external = true;
152             }
153         }
154
155         $attachment->save();
156
157         return $attachment;
158     }
159
160     /**
161      * Delete a File from the database and storage.
162      *
163      * @param Attachment $attachment
164      *
165      * @throws Exception
166      */
167     public function deleteFile(Attachment $attachment)
168     {
169         if ($attachment->external) {
170             $attachment->delete();
171
172             return;
173         }
174
175         $this->deleteFileInStorage($attachment);
176         $attachment->delete();
177     }
178
179     /**
180      * Delete a file from the filesystem it sits on.
181      * Cleans any empty leftover folders.
182      *
183      * @param Attachment $attachment
184      */
185     protected function deleteFileInStorage(Attachment $attachment)
186     {
187         $storage = $this->getStorage();
188         $dirPath = dirname($attachment->path);
189
190         $storage->delete($attachment->path);
191         if (count($storage->allFiles($dirPath)) === 0) {
192             $storage->deleteDirectory($dirPath);
193         }
194     }
195
196     /**
197      * Store a file in storage with the given filename.
198      *
199      * @param UploadedFile $uploadedFile
200      *
201      * @throws FileUploadException
202      *
203      * @return string
204      */
205     protected function putFileInStorage(UploadedFile $uploadedFile)
206     {
207         $attachmentData = file_get_contents($uploadedFile->getRealPath());
208
209         $storage = $this->getStorage();
210         $basePath = 'uploads/files/' . date('Y-m-M') . '/';
211
212         $uploadFileName = Str::random(16) . '.' . $uploadedFile->getClientOriginalExtension();
213         while ($storage->exists($basePath . $uploadFileName)) {
214             $uploadFileName = Str::random(3) . $uploadFileName;
215         }
216
217         $attachmentPath = $basePath . $uploadFileName;
218
219         try {
220             $storage->put($attachmentPath, $attachmentData);
221         } catch (Exception $e) {
222             Log::error('Error when attempting file upload:' . $e->getMessage());
223
224             throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath]));
225         }
226
227         return $attachmentPath;
228     }
229 }