Fixed upload system

This commit is contained in:
Jérémy Munsch 2022-07-28 09:55:16 +02:00
parent ba2b9dcde2
commit d2236fe041
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
10 changed files with 233 additions and 147 deletions

View file

@ -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());
});
}
}

41
config/chunk-upload.php Normal file
View file

@ -0,0 +1,41 @@
<?php
return [
/*
* The storage config
*/
'storage' => [
/*
* 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
],
],
];

View file

@ -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'),

View file

@ -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);

View file

@ -61,8 +61,12 @@
class="media"
:style="`background-image:url('${media.url}')`"
></div>
<div v-if="media.type_media_id === 3" class="media video"></div>
<div v-if="media.type_media_id === 4" class="media son"></div>
<div v-if="media.type_media_id === 3" class="media video">
<font-awesome-icon icon="file-video" />
</div>
<div v-if="media.type_media_id === 4" class="media son">
<font-awesome-icon icon="file-audio" />
</div>
<p class="libelle">{{ media.libelle }}</p>
<div class="info">

View file

@ -14,7 +14,7 @@
</button>
<button
v-if="(storeRole === 1 || storeRole === 4) && !selectionContainsLockedMedias"
v-on:click.prevent="deleteMedias"
v-on:click.prevent="deleteMedia"
class="action_delete_media"
>
<font-awesome-icon icon="trash-alt" />
@ -84,7 +84,7 @@
<button
type="submit"
class="btn btn_form_download ml-10"
@click="performDownload"
v-on:click.prevent="performDownload"
>
<font-awesome-icon icon="arrow-circle-down" />
<span> Télécharger</span>
@ -93,11 +93,11 @@
</div>
<div class="upload_container high-level">
<span class="fileinput-button" @click="addFiles">
<a class="btn btn-primary basic-style">
<span class="fileinput-button" v-on:click.prevent="addFiles">
<button class="btn btn-primary basic-style">
<font-awesome-icon icon="plus-circle" style="background-color: unset" />
<span> Ajouter vos médias</span>
</a>
</button>
</span>
<div class="content_wrapper">
<!-- The container for the uploaded errors -->
@ -119,12 +119,16 @@
</div>
</div>
<div class="upload_bar">
<a id="submit" class="btn basic-style" href="#" @click="createMedias">
Transférer
</a>
<a id="close_upload_form" class="basic-style" href="#" @click="closeUploadFrom">
Fermer
</a>
<button id="submit" class="btn basic-style" v-on:click.prevent="createMedia">
<span>Transférer</span>
</button>
<button
id="close_upload_form"
class="btn btn-red basic-style"
v-on:click.prevent="closeUploadFrom"
>
<span>Fermer</span>
</button>
</div>
<br />
<input style="display: none" ref="fileupload" type="file" name="files[]" multiple />
@ -136,14 +140,23 @@
</h3>
<hr />
<div class="list_uploaded_files">
<p class="list_uploaded_files_p" v-for="file in uploadedFilesErrors">
<span> {{ file.libelle }}</span>
<span> {{ file.extension }}</span>
<span> {{ file.message }}</span>
</p>
<p class="list_uploaded_files_p" v-for="file in uploadedFiles">
{{ file.libelle }}.{{ file.extension }}
<button class="btn btn-sm btn-primary" @click="openMedia(file)">Editer</button>
<button class="btn btn-sm btn-primary" v-on:click.prevent="openMedia(file)">
<span>Editer</span>
</button>
</p>
</div>
<hr />
<div style="margin-top: 20px; text-align: center">
<a @click="CloseListUploadedFiles" class="btn" href="#">Fermer</a>
<button v-on:click.prevent="CloseListUploadedFiles" class="btn">
<span>Fermer</span>
</button>
</div>
</div>
</div>
@ -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(),

View file

@ -62,8 +62,12 @@
class="media"
:style="`background-image:url('${media.url}')`"
></div>
<div v-if="media.type_media_id === 3" class="media video"></div>
<div v-if="media.type_media_id === 4" class="media son"></div>
<div v-if="media.type_media_id === 3" class="media video">
<font-awesome-icon icon="file-video" />
</div>
<div v-if="media.type_media_id === 4" class="media son">
<font-awesome-icon icon="file-audio" />
</div>
<div class="bar">
<a href="#" class="media_info" @click="openMedia(media)">
<font-awesome-icon icon="info-circle" />

View file

@ -77,8 +77,12 @@
class="media"
:style="`background-image:url('${media.url}')`"
></div>
<div v-if="media.type_media_id === 3" class="media video"></div>
<div v-if="media.type_media_id === 4" class="media son"></div>
<div v-if="media.type_media_id === 3" class="media video">
<font-awesome-icon icon="file-video" />
</div>
<div v-if="media.type_media_id === 4" class="media son">
<font-awesome-icon icon="file-audio" />
</div>
<p class="libelle">{{ media.libelle }}</p>

View file

@ -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 {

View file

@ -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;