From d2236fe041db7f67a409cf334dfa4a92d9a525f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Thu, 28 Jul 2022 09:55:16 +0200 Subject: [PATCH] Fixed upload system --- app/Http/Controllers/UploadController.php | 85 +++++++---- config/chunk-upload.php | 41 ++++++ config/filesystems.php | 7 + resources/js/components/DepotComponent.vue | 20 ++- .../components/StandAlonePanierComponent.vue | 8 +- .../media-manager/MmActionsComponent.vue | 137 ++++++++++++------ .../media-manager/MmBasketComponent.vue | 8 +- .../media-manager/MmDisplayComponent.vue | 8 +- resources/scss/partials/media.scss | 46 +----- resources/scss/partials/panier.scss | 20 --- 10 files changed, 233 insertions(+), 147 deletions(-) create mode 100644 config/chunk-upload.php diff --git a/app/Http/Controllers/UploadController.php b/app/Http/Controllers/UploadController.php index cc13409..81799ec 100644 --- a/app/Http/Controllers/UploadController.php +++ b/app/Http/Controllers/UploadController.php @@ -25,6 +25,7 @@ use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException; use Pion\Laravel\ChunkUpload\Receiver\FileReceiver; +use Symfony\Component\Mime\MimeTypes; class UploadController extends Controller { @@ -80,17 +81,22 @@ class UploadController extends Controller $uplodedFile = $save->getFile(); - /** @throws \Illuminate\Validation\ValidationException */ - $this->validateUploadedFile($uplodedFile); + try { + /** @throws \Illuminate\Validation\ValidationException */ + $this->validateUploadedFile($uplodedFile); - /** @var \App\Models\Media */ - $media = Media::whereId($request->media_id) - ->whereHas('dossier', function ($q) use ($dossier) { - $q->whereId($dossier->id); - })->firstOrNew(); + /** @var \App\Models\Media */ + $media = Media::whereId($request->media_id) + ->whereHas('dossier', function ($q) use ($dossier) { + $q->whereId($dossier->id); + })->firstOrNew(); - // Save the file and return any response you need. - return $this->createOrUpdateMedia($user, $uplodedFile, $media, $dossier); + // Save the file and return any response you need. + return $this->createOrUpdateMedia($user, $uplodedFile, $media, $dossier); + } catch (ValidationException $e) { + // 400 instead of 422 is for resumablejs to handle properly. + return \response()->json($e->errors(), 400); + } } // We are in chunk mode, lets send the current progress. @@ -121,6 +127,7 @@ class UploadController extends Controller if ($receiver->isUploaded() === false) { throw new UploadMissingFileException(); } + // Receive the file. $save = $receiver->receive(); @@ -131,19 +138,24 @@ class UploadController extends Controller $uplodedFile = $save->getFile(); - /** @throws \Illuminate\Validation\ValidationException */ - $this->validateUploadedFile($uplodedFile); + try { + /** @throws \Illuminate\Validation\ValidationException */ + $this->validateUploadedFile($uplodedFile); - /** @var \App\Models\Media */ - $media = Media::whereId($request->media_id) - ->whereHas('sujet', function ($q) use ($sujet) { - $q->whereId($sujet->id); - })->whereHas('dossier', function ($q) use ($dossier) { - $q->whereId($dossier->id); - })->firstOrNew(); + /** @var \App\Models\Media */ + $media = Media::whereId($request->media_id) + ->whereHas('sujet', function ($q) use ($sujet) { + $q->whereId($sujet->id); + })->whereHas('dossier', function ($q) use ($dossier) { + $q->whereId($dossier->id); + })->firstOrNew(); - // Save the file and return any response you need. - return $this->createOrUpdateMedia($user, $uplodedFile, $media, $dossier, $sujet); + // Save the file and return any response you need. + return $this->createOrUpdateMedia($user, $uplodedFile, $media, $dossier, $sujet); + } catch (ValidationException $e) { + // 400 instead of 422 is for resumablejs to handle properly. + return \response()->json($e->errors(), 400); + } } // We are in chunk mode, lets send the current progress. @@ -170,22 +182,40 @@ class UploadController extends Controller ], [ 'files.mimes' => trans('Ce type de fichier n\'est pas accepté.') ])->validate(); - $this->validateExtension($file->getClientOriginalExtension()); + $this->validateExtension($file); } /** * Validate file extension matches allowed extensions enum * - * @param string $extension + * @param \Illuminate\Http\UploadedFile $file * @return void */ - private function validateExtension(string $extension) + private function validateExtension(UploadedFile $file) { - if (!\is_null(MediaExtension::tryFrom($extension))) { - ValidationException::withMessages([ + if (\is_null(MediaExtension::tryFrom($file->getClientOriginalExtension()))) { + throw ValidationException::withMessages([ 'file' => trans('Ce type de fichier n\'est pas accepté.') ]); } + + /** @throws \ValueError If value is not defined in enum */ + $mediaExtension = MediaExtension::from($file->getClientOriginalExtension()); + + if ($file->guessExtension()) { + $mt = new MimeTypes(); + $guessedExts = $mt->getExtensions($mt->guessMimeType($file->path())); + + if (!\in_array($mediaExtension->value, $guessedExts)) { + $guessedExts = \implode(',', $guessedExts); + throw ValidationException::withMessages([ + 'file' => trans('File guessed extensions :guessedExts does not match file name extension :extension', [ + 'guessedExt' => $guessedExts, + 'extension' => $mediaExtension, + ]) + ]); + } + } } /** @@ -232,7 +262,6 @@ class UploadController extends Controller /** @var string */ $sanitizedFileExtension = \pathinfo($sanitizedFileName, \PATHINFO_EXTENSION); - // * Validate Extension /** @throws \ValueError If value is not defined in enum */ $mediaExtension = MediaExtension::from($sanitizedFileExtension); @@ -309,9 +338,7 @@ class UploadController extends Controller $media->saveOrFail(); // * Get props inserted by DB. - $media->refresh(); - - return response()->json($media); + return response()->json($media->refresh()); }); } } diff --git a/config/chunk-upload.php b/config/chunk-upload.php new file mode 100644 index 0000000..3e42b9e --- /dev/null +++ b/config/chunk-upload.php @@ -0,0 +1,41 @@ + [ + /* + * Returns the folder name of the chunks. The location is in storage/app/{folder_name} + */ + 'chunks' => 'chunks', + 'disk' => 'chunk_upload', + ], + 'clear' => [ + /* + * How old chunks we should delete + */ + 'timestamp' => '-3 HOURS', + 'schedule' => [ + 'enabled' => true, + 'cron' => '25 * * * *', // run every hour on the 25th minute + ], + ], + 'chunk' => [ + // setup for the chunk naming setup to ensure same name upload at same time + 'name' => [ + 'use' => [ + 'session' => true, // should the chunk name use the session id? The uploader must send cookie!, + 'browser' => false, // instead of session we can use the ip and browser? + ], + ], + ], + 'handlers' => [ + // A list of handlers/providers that will be appended to existing list of handlers + 'custom' => [], + // Overrides the list of handlers - use only what you really want + 'override' => [ + // \Pion\Laravel\ChunkUpload\Handler\DropZoneUploadHandler::class + ], + ], +]; diff --git a/config/filesystems.php b/config/filesystems.php index 2d05174..41e0849 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -63,12 +63,19 @@ return [ 'root' => storage_path('arcadia_link/send'), 'throw' => true, ], + 'arcadia_link_r' => [ 'driver' => 'local', 'root' => storage_path('arcadia_link/receive'), 'throw' => true, ], + 'chunk_upload' => [ + 'driver' => 'local', + 'root' => storage_path('upload'), + 'throw' => true, + ], + 's3' => [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), diff --git a/resources/js/components/DepotComponent.vue b/resources/js/components/DepotComponent.vue index 004aa64..426e26e 100644 --- a/resources/js/components/DepotComponent.vue +++ b/resources/js/components/DepotComponent.vue @@ -316,10 +316,22 @@ export default { this.setupResumable(); }); }, - uploadFileError(file, message) { - this.uploadErrors.push( - `Une erreur durant le transfert du fichier ${file.fileName}` - ); + uploadFileError(file, httpMessage) { + let message = `Une erreur durant le transfert du fichier ${file.fileName}`; + + const error = JSON.parse(httpMessage); + if (error.file) { + const errors = error.file; + if (errors instanceof String) { + message = errors; + } + if (errors instanceof Object) { + message = Object.values(errors).reduce(function (previous, current) { + return String(previous) + String(current); + }); + } + } + this.uploadErrors.push(message); }, removeFile(fileToRemove) { this.r.removeFile(fileToRemove); diff --git a/resources/js/components/StandAlonePanierComponent.vue b/resources/js/components/StandAlonePanierComponent.vue index f692d7e..d4a8e95 100644 --- a/resources/js/components/StandAlonePanierComponent.vue +++ b/resources/js/components/StandAlonePanierComponent.vue @@ -61,8 +61,12 @@ class="media" :style="`background-image:url('${media.url}')`" > -
-
+
+ +
+
+ +

{{ media.libelle }}

diff --git a/resources/js/components/media-manager/MmActionsComponent.vue b/resources/js/components/media-manager/MmActionsComponent.vue index 87d87f6..984ef2d 100644 --- a/resources/js/components/media-manager/MmActionsComponent.vue +++ b/resources/js/components/media-manager/MmActionsComponent.vue @@ -14,7 +14,7 @@
- - + +
@@ -119,12 +119,16 @@
- - Transférer - - - Fermer - + +

@@ -136,14 +140,23 @@
+

+ {{ file.libelle }} + {{ file.extension }} + {{ file.message }} +

{{ file.libelle }}.{{ file.extension }} - +


- Fermer +
@@ -187,9 +200,11 @@ export default { etiquettes: [], files: [], uploadedFiles: [], + uploadedFilesErrors: [], selectionContainsLockedMedias: true, format_id: null, waitingRequest: false, + resumable: null, }; }, methods: { @@ -245,7 +260,7 @@ export default { }); } }, - deleteMedias() { + deleteMedia() { const self = this, mediaList = this.storeGrabbed; if (Object.keys(mediaList).length === 0) { @@ -429,20 +444,21 @@ export default { type: "error", }); } else { - if (this.r === undefined) this.setupResumable(); + if (!this.resumable) this.setupResumable(); $(".upload_container").show("fast"); } }, closeUploadFrom() { this.files = []; + this.resumable.files = []; $(".upload_container").hide("fast"); }, addFiles() { this.$refs.fileupload.click(); }, removeFile(file) { - this.r.removeFile(file); - this.files = this.r.files; + this.resumable.removeFile(file); + this.files = this.resumable.files; }, /** * @return {string} @@ -462,8 +478,7 @@ export default { } }, setupResumable() { - this.r = null; - this.r = new Resumable({ + this.resumable = new Resumable({ target: `/ajax/upload-media/${this.storeSujet}/${this.storeFolder}`, simultaneousUploads: 3, maxFiles: 8, @@ -476,11 +491,11 @@ export default { }, }); - this.r.on("fileAdded", this.uploadFileAdded); - this.r.on("fileSuccess", this.uploadFileSuccess); - this.r.on("fileError", this.uploadFileError); - this.r.on("complete", this.uploadFileComplete); - this.r.assignBrowse(this.$refs.fileupload); + this.resumable.on("fileAdded", this.uploadFileAdded); + this.resumable.on("fileSuccess", this.uploadFileSuccess); + this.resumable.on("fileError", this.uploadFileError); + this.resumable.on("complete", this.uploadFileComplete); + this.resumable.assignBrowse(this.$refs.fileupload); }, uploadFileAdded(file, event) { const acceptFileTypes = /^(gif|jpe?g|png|mp4|mp3|psd|ai|svg|pdf)$/i; @@ -489,13 +504,23 @@ export default { file.fileName.split(".").pop().length && !acceptFileTypes.test(file.fileName.split(".").pop()) ) { - this.r.removeFile(file); + this.resumable.removeFile(file); + $.fancyMessage({ + title: "Attention", + message: file.fileName + " ce type de fichier n'est pas pris en charge.", + type: "warning", + }); } if (file.size.length && file.size > 5000000) { - this.r.removeFile(file); + this.resumable.removeFile(file); + $.fancyMessage({ + title: "Attention", + message: file.fileName + " ce type de fichier est trop grand.", + type: "warning", + }); } - this.files = this.r.files; + this.files = this.resumable.files; }, uploadFileSuccess(file, message) { this.uploadedFiles.push(JSON.parse(message)); @@ -515,34 +540,51 @@ export default { uploadFileComplete() { this.closeUploadFrom(); this.$loading.hide(); - //$.fancybox.close(); - // $.fancybox.open({ - // src : '#ListUploadedFiles', - // type : 'inline', - // opts : { - // modal: true, - // animationDuration : 350, - // animationEffect : 'material', - // } - // }); + $.fancybox.close(); + $.fancybox.open({ + src: "#ListUploadedFiles", + type: "inline", + opts: { + modal: true, + animationDuration: 350, + animationEffect: "material", + }, + }); console.log("complete"); }, - uploadFileError(file, message) { + uploadFileError(file, httpMessage) { + let message = "Erreur lors de l'opération, merci d'actualiser la page."; this.closeUploadFrom(); this.$loading.hide(); $.fancybox.close(); - const error = JSON.parse(message); - console.log(error); - console.log(error.files); + + const error = JSON.parse(httpMessage); + if (error.file) { + const errors = error.file; + if (errors instanceof String) { + message = errors; + } + if (errors instanceof Object) { + message = Object.values(errors).reduce(function (previous, current) { + return String(previous) + String(current); + }); + } + this.uploadedFilesErrors.push({ + libelle: file.fileName, + extension: file.file.type, + message: message, + }); + console.warn(errors); + } $.fancyMessage({ - title: "Erreur lors du téléchargement", - message: error.files[0], + title: "Erreur", + message: message, type: "error", }); }, - createMedias() { + createMedia() { this.$loading.show({ delay: 0 }); - this.r.upload(); + this.resumable.upload(); }, openMedia(media) { $.fancybox.close(); @@ -553,12 +595,17 @@ export default { }, CloseListUploadedFiles() { if ( + !this.uploadedFiles.length || confirm( "En cliquant sur OK, vous confirmez que vous avez édité tous vos fichiers." ) ) { $.fancybox.close(); this.uploadedFiles = []; + this.uploadedFilesErrors = []; + // * Reset input file. + this.$refs.fileupload.files = new DataTransfer().files; + this.$refs.fileupload.value = ""; this.$store.commit({ type: "Reload", reload: Math.random(), diff --git a/resources/js/components/media-manager/MmBasketComponent.vue b/resources/js/components/media-manager/MmBasketComponent.vue index 5509f0f..a260f87 100644 --- a/resources/js/components/media-manager/MmBasketComponent.vue +++ b/resources/js/components/media-manager/MmBasketComponent.vue @@ -62,8 +62,12 @@ class="media" :style="`background-image:url('${media.url}')`" > -
-
+
+ +
+
+ +
diff --git a/resources/js/components/media-manager/MmDisplayComponent.vue b/resources/js/components/media-manager/MmDisplayComponent.vue index f1be4dd..879f615 100644 --- a/resources/js/components/media-manager/MmDisplayComponent.vue +++ b/resources/js/components/media-manager/MmDisplayComponent.vue @@ -77,8 +77,12 @@ class="media" :style="`background-image:url('${media.url}')`" >
-
-
+
+ +
+
+ +

{{ media.libelle }}

diff --git a/resources/scss/partials/media.scss b/resources/scss/partials/media.scss index a032e30..cdb8264 100644 --- a/resources/scss/partials/media.scss +++ b/resources/scss/partials/media.scss @@ -119,7 +119,7 @@ & > #actionsBar { float: left; - button { + & > div > button { @include resetButton; font-size: 13px; padding: 15px 13px; @@ -365,7 +365,7 @@ background-position: center; position: relative; - &::before { + & svg { transform: translate(-50%, -50%); display: block; position: absolute; @@ -377,26 +377,6 @@ font-size: 59px; z-index: 1; } - &.video { - &::before { - content: "\f167"; - font-family: "Font Awesome 6 Brands"; - } - } - &.son { - &::before { - content: "\f58f"; - font-family: "Font Awesome 6 Free"; - font-weight: 900; - } - } - &.doc { - &::before { - content: "\f15c"; - font-family: "Font Awesome 6 Free"; - font-weight: 900; - } - } } .libelle { padding: 4px; @@ -758,7 +738,7 @@ background-position: center; position: relative; - &::before { + & svg { transform: translate(-50%, -50%); display: block; position: absolute; @@ -770,26 +750,6 @@ font-size: 59px; z-index: 1; } - &.video { - &::before { - content: "\f167"; - font-family: "Font Awesome 6 Brands"; - } - } - &.son { - &::before { - content: "\f58f"; - font-family: "Font Awesome 6 Free"; - font-weight: 900; - } - } - &.doc { - &::before { - content: "\f15c"; - font-family: "Font Awesome 6 Free"; - font-weight: 900; - } - } } &:hover { diff --git a/resources/scss/partials/panier.scss b/resources/scss/partials/panier.scss index f17c196..b80c9be 100644 --- a/resources/scss/partials/panier.scss +++ b/resources/scss/partials/panier.scss @@ -185,26 +185,6 @@ font-size: 59px; z-index: 1; } - &.video { - &::before { - content: "\f167"; - font-family: "Font Awesome 6 Brands"; - } - } - &.son { - &::before { - content: "\f58f"; - font-family: "Font Awesome 6 Free"; - font-weight: 900; - } - } - &.doc { - &::before { - content: "\f15c"; - font-family: "Font Awesome 6 Free"; - font-weight: 900; - } - } } .libelle { padding: 4px;