From c696a8bd2e7b5a381d801e3c91ae63948376a8d4 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 29 Apr 2024 22:00:34 +1000 Subject: [PATCH] refactor file uploading and add media picker errors --- ...Exception.php => FileInvalidException.php} | 2 +- app/Exceptions/FileTooLargeException.php | 37 +++ app/Http/Controllers/MediaController.php | 214 +++++++++--------- resources/js/media-picker.js | 22 +- .../views/components/ui/filelist.blade.php | 3 + 5 files changed, 166 insertions(+), 112 deletions(-) rename app/Exceptions/{MediaServiceException.php => FileInvalidException.php} (92%) create mode 100644 app/Exceptions/FileTooLargeException.php diff --git a/app/Exceptions/MediaServiceException.php b/app/Exceptions/FileInvalidException.php similarity index 92% rename from app/Exceptions/MediaServiceException.php rename to app/Exceptions/FileInvalidException.php index e0183a6..2e122f3 100644 --- a/app/Exceptions/MediaServiceException.php +++ b/app/Exceptions/FileInvalidException.php @@ -4,7 +4,7 @@ namespace App\Exceptions; use Exception; -class MediaServiceException extends Exception +class FileInvalidException extends Exception { /** * The error code of the exception. diff --git a/app/Exceptions/FileTooLargeException.php b/app/Exceptions/FileTooLargeException.php new file mode 100644 index 0000000..59832d8 --- /dev/null +++ b/app/Exceptions/FileTooLargeException.php @@ -0,0 +1,37 @@ +message = $message; + $this->code = $code; + + parent::__construct($message, $code); + } +} diff --git a/app/Http/Controllers/MediaController.php b/app/Http/Controllers/MediaController.php index b2a40ca..0363c79 100644 --- a/app/Http/Controllers/MediaController.php +++ b/app/Http/Controllers/MediaController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Exceptions\FileInvalidException; +use App\Exceptions\FileTooLargeException; use App\Helpers; use App\Models\Media; use Illuminate\Http\Request; @@ -108,73 +110,79 @@ class MediaController extends Controller */ public function admin_store(Request $request) { - $max_size = Helpers::getMaxUploadSize(); + $file = null; - $validator = Validator::make($request->all(), [ - 'title' => 'required', - 'file' => 'required|file|max:' . (max(round($max_size / 1024),0)), - ], [ - 'title.required' => __('validation.custom_messages.title_required'), - 'file.required' => __('validation.custom_messages.file_required'), - 'file.file' => __('validation.custom_messages.file_file'), - 'file.max' => __('validation.custom_messages.file_max', ['max' => Helpers::bytesToString($max_size)]) - ]); + // Check if the endpoint received a file... + if($request->hasFile('file')) { + try { + $file = $this->upload($request); - if ($validator->fails()) { - if($request->wantsJson()) { + if($file === true) { + return response()->json([ + 'message' => 'Chunk stored', + ]); + } else if(!$file) { + return response()->json([ + 'message' => 'An error occurred processing the file.', + 'errors' => [ + 'file' => 'An error occurred processing the file.' + ] + ], 422); + } + + if(!$request->has('title')) { + return response()->json([ + 'message' => 'The file ' . $file->getClientOriginalName() . ' has been uploaded', + ]); + } + } catch(\Exception $e) { return response()->json([ - 'message' => 'The given data was invalid.', - 'errors' => $validator->errors(), - ], 422); - } else { - return redirect()->back()->withErrors($validator)->withInput(); - } - } - - $file = $request->file('file'); - $fileName = $request->input('filename', $file->getClientOriginalName()); - $fileName = Helpers::cleanFileName($fileName); - - if(($request->has('filestart') || $request->has('fileappend')) && $request->has('filesize')) { - $fileSize = $request->get('filesize'); - - if($fileSize > $max_size) { - return response()->json([ - 'message' => 'The file ' . $fileName . ' is larger than the maximum size allowed of ' . Helpers::bytesToString($max_size), + 'message' => $e->getMessage(), 'errors' => [ - 'file' => 'The file is larger than the maximum size allowed of ' . Helpers::bytesToString($max_size) + 'file' => $e->getMessage() ] ], 422); } - $tempFilePath = sys_get_temp_dir() . '/chunk-' . $fileName; - - $filemode = 'a'; - if($request->has('filestart')) { - $filemode = 'w'; - } - - // Append the chunk to the temporary file - $fp = fopen($tempFilePath, $filemode); - if ($fp) { - fwrite($fp, file_get_contents($file->getRealPath())); - fclose($fp); - } - - // Check if the upload is complete - if (filesize($tempFilePath) >= $fileSize) { - $fileMime = mime_content_type($tempFilePath); - if($fileMime === false) { - $fileMime = 'application/octet-stream'; - } - $file = new UploadedFile($tempFilePath, $fileName, $fileMime, null, true); - } else { + // else check if it received a file name of a previous upload... + } else if($request->has('file')) { + $tempFileName = sys_get_temp_dir() . '/chunk-' . Auth::id() . '-' . $request->file; + if(!file_exists($tempFileName)) { return response()->json([ - 'message' => 'Chunk stored', - ]); + 'message' => 'Could not find the referenced file on the server.', + 'errors' => [ + 'file' => 'Could not find the referenced file on the server.' + ] + ], 422); } + + $fileMime = mime_content_type($tempFileName); + if($fileMime === false) { + $fileMime = 'application/octet-stream'; + } + $file = new UploadedFile($tempFileName, $request->file, $fileMime, null, true); } + // Check there is an actual file + if(!$file) { + return response()->json([ + 'message' => 'A file is required.', + 'errors' => [ + 'file' => 'A file is required.' + ] + ], 422); + } + + if(!$request->has('title')) { + return response()->json([ + 'message' => 'A title is required', + 'errors' => [ + 'title' => 'A title is required' + ] + ], 422); + } + + $fileName = $file->getClientOriginalName(); $name = pathinfo($fileName, PATHINFO_FILENAME); $extension = pathinfo($fileName, PATHINFO_EXTENSION); $name = Helpers::cleanFileName($name); @@ -346,71 +354,61 @@ class MediaController extends Controller return redirect()->route('admin.media.index'); } - public function upload(Request $request) + + /** + * @throws FileInvalidException + * @throws FileTooLargeException + */ + private function upload(Request $request) { - $request->validate([ - 'file' => 'required|file', - ]); - - if(auth()->guest()) { - return response()->json([ - 'message' => 'You must be logged in to upload media', - ], 401); - } - - if(!auth()->user()?->admin) { - return response()->json([ - 'message' => 'You do not have permission to upload media', - ], 403); - } - - if(!$request->hasFile('file')) { - return response()->json([ - 'message' => 'No file was received by the server', - ], 422); - } - $max_size = Helpers::getMaxUploadSize(); $file = $request->file('file'); - - if($file->getSize() > $max_size) { - return response()->json([ - 'message' => 'The file ' . $file->getClientOriginalName() . ' is larger than the maximum size allowed of ' . Helpers::bytesToString($max_size) - ], 422); + if(!$file->isValid()) { + throw new FileInvalidException('The file is invalid'); } - $name = $file->getClientOriginalName(); - if(Media::find($name) !== null) { - $increment = 2; - while(Media::find($name . '-' . $increment) !== null) { - $increment++; + $fileName = $request->input('filename', $file->getClientOriginalName()); + $fileName = Helpers::cleanFileName($fileName); + + if(($request->has('filestart') || $request->has('fileappend')) && $request->has('filesize')) { + $fileSize = $request->get('filesize'); + + if($fileSize > $max_size) { + throw new FileTooLargeException('The file is larger than the maximum size allowed of ' . Helpers::bytesToString($max_size)); } - $name = $name . '-' . $increment; + $tempFilePath = sys_get_temp_dir() . '/chunk-' . Auth::id() . '-' . $fileName; + + $filemode = 'a'; + if($request->has('filestart')) { + $filemode = 'w'; + } + + // Append the chunk to the temporary file + $fp = fopen($tempFilePath, $filemode); + if ($fp) { + fwrite($fp, file_get_contents($file->getRealPath())); + fclose($fp); + } + + // Check if the upload is complete + if (filesize($tempFilePath) >= $fileSize) { + $fileMime = mime_content_type($tempFilePath); + if($fileMime === false) { + $fileMime = 'application/octet-stream'; + } + + return new UploadedFile($tempFilePath, $fileName, $fileMime, null, true); + } else { + return true; + } } - $media = Media::Create([ - 'title' => $request->get('title', $name), - 'user_id' => auth()->id(), - 'name' => $name, - 'size' => $file->getSize(), - 'mime_type' => $file->getMimeType(), - 'hash' => hash_file('sha256', $file->path()), - ]); - - $file->storeAs('/', $media->hash, 'public'); - $media->generateVariants(); - unlink($file); - - return response()->json([ - 'message' => 'File has been uploaded', - 'name' => $media->name, - 'size' => $media->size, - 'mime_type' => $media->mime_type - ]); + return $file; } + public function download(Request $request, Media $media) { $file = $media->path(); diff --git a/resources/js/media-picker.js b/resources/js/media-picker.js index 9405d85..8ce54b4 100644 --- a/resources/js/media-picker.js +++ b/resources/js/media-picker.js @@ -4,12 +4,23 @@ const SMMediaPicker = { return SM.mimeMatches(file.type, Alpine.store('media').require_mime_type); }); + if(validFiles.length === 0) { + Alpine.store('media').error = 'No files where uploaded as they do not meet the requirements.'; + } else if(validFiles.length !== files.length) { + Alpine.store('media').error = 'Some files where not uploaded as they do not meet the requirements.'; + } else { + Alpine.store('media').error = null; + } + const titles = Array.from(validFiles).map((file) => SM.toTitleCase(file.name)); SM.upload(validFiles, (response) => { - response.files.forEach((file) => { - SMMediaPicker.updateSelection(file.data.name); - }); + if(response.files) { + response.files.forEach((file) => { + SMMediaPicker.updateSelection(file.data.name); + }); + } + SMMediaPicker.open( Alpine.store('media').selected, { @@ -107,6 +118,11 @@ const SMMediaPicker = { html: `
+