refactor file uploading and add media picker errors
This commit is contained in:
@@ -4,7 +4,7 @@ namespace App\Exceptions;
|
|||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class MediaServiceException extends Exception
|
class FileInvalidException extends Exception
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The error code of the exception.
|
* The error code of the exception.
|
||||||
37
app/Exceptions/FileTooLargeException.php
Normal file
37
app/Exceptions/FileTooLargeException.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class FileTooLargeException extends Exception
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The error code of the exception.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The error message of the exception.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new exception instance.
|
||||||
|
*
|
||||||
|
* @param string $message
|
||||||
|
* @param int $code
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(string $message, int $code = 0)
|
||||||
|
{
|
||||||
|
$this->message = $message;
|
||||||
|
$this->code = $code;
|
||||||
|
|
||||||
|
parent::__construct($message, $code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Exceptions\FileInvalidException;
|
||||||
|
use App\Exceptions\FileTooLargeException;
|
||||||
use App\Helpers;
|
use App\Helpers;
|
||||||
use App\Models\Media;
|
use App\Models\Media;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -108,73 +110,79 @@ class MediaController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function admin_store(Request $request)
|
public function admin_store(Request $request)
|
||||||
{
|
{
|
||||||
$max_size = Helpers::getMaxUploadSize();
|
$file = null;
|
||||||
|
|
||||||
$validator = Validator::make($request->all(), [
|
// Check if the endpoint received a file...
|
||||||
'title' => 'required',
|
if($request->hasFile('file')) {
|
||||||
'file' => 'required|file|max:' . (max(round($max_size / 1024),0)),
|
try {
|
||||||
], [
|
$file = $this->upload($request);
|
||||||
'title.required' => __('validation.custom_messages.title_required'),
|
|
||||||
'file.required' => __('validation.custom_messages.file_required'),
|
if($file === true) {
|
||||||
'file.file' => __('validation.custom_messages.file_file'),
|
return response()->json([
|
||||||
'file.max' => __('validation.custom_messages.file_max', ['max' => Helpers::bytesToString($max_size)])
|
'message' => 'Chunk stored',
|
||||||
]);
|
]);
|
||||||
|
} else if(!$file) {
|
||||||
if ($validator->fails()) {
|
|
||||||
if($request->wantsJson()) {
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'The given data was invalid.',
|
'message' => 'An error occurred processing the file.',
|
||||||
'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),
|
|
||||||
'errors' => [
|
'errors' => [
|
||||||
'file' => 'The file is larger than the maximum size allowed of ' . Helpers::bytesToString($max_size)
|
'file' => 'An error occurred processing the file.'
|
||||||
]
|
]
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
$tempFilePath = sys_get_temp_dir() . '/chunk-' . $fileName;
|
if(!$request->has('title')) {
|
||||||
|
return response()->json([
|
||||||
$filemode = 'a';
|
'message' => 'The file ' . $file->getClientOriginalName() . ' has been uploaded',
|
||||||
if($request->has('filestart')) {
|
]);
|
||||||
$filemode = 'w';
|
}
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'errors' => [
|
||||||
|
'file' => $e->getMessage()
|
||||||
|
]
|
||||||
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the chunk to the temporary file
|
// else check if it received a file name of a previous upload...
|
||||||
$fp = fopen($tempFilePath, $filemode);
|
} else if($request->has('file')) {
|
||||||
if ($fp) {
|
$tempFileName = sys_get_temp_dir() . '/chunk-' . Auth::id() . '-' . $request->file;
|
||||||
fwrite($fp, file_get_contents($file->getRealPath()));
|
if(!file_exists($tempFileName)) {
|
||||||
fclose($fp);
|
return response()->json([
|
||||||
|
'message' => 'Could not find the referenced file on the server.',
|
||||||
|
'errors' => [
|
||||||
|
'file' => 'Could not find the referenced file on the server.'
|
||||||
|
]
|
||||||
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the upload is complete
|
$fileMime = mime_content_type($tempFileName);
|
||||||
if (filesize($tempFilePath) >= $fileSize) {
|
|
||||||
$fileMime = mime_content_type($tempFilePath);
|
|
||||||
if($fileMime === false) {
|
if($fileMime === false) {
|
||||||
$fileMime = 'application/octet-stream';
|
$fileMime = 'application/octet-stream';
|
||||||
}
|
}
|
||||||
$file = new UploadedFile($tempFilePath, $fileName, $fileMime, null, true);
|
$file = new UploadedFile($tempFileName, $request->file, $fileMime, null, true);
|
||||||
} else {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Chunk stored',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
$name = pathinfo($fileName, PATHINFO_FILENAME);
|
||||||
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
|
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
|
||||||
$name = Helpers::cleanFileName($name);
|
$name = Helpers::cleanFileName($name);
|
||||||
@@ -346,71 +354,61 @@ class MediaController extends Controller
|
|||||||
return redirect()->route('admin.media.index');
|
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();
|
$max_size = Helpers::getMaxUploadSize();
|
||||||
|
|
||||||
$file = $request->file('file');
|
$file = $request->file('file');
|
||||||
|
if(!$file->isValid()) {
|
||||||
if($file->getSize() > $max_size) {
|
throw new FileInvalidException('The file is invalid');
|
||||||
return response()->json([
|
|
||||||
'message' => 'The file ' . $file->getClientOriginalName() . ' is larger than the maximum size allowed of ' . Helpers::bytesToString($max_size)
|
|
||||||
], 422);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = $file->getClientOriginalName();
|
$fileName = $request->input('filename', $file->getClientOriginalName());
|
||||||
if(Media::find($name) !== null) {
|
$fileName = Helpers::cleanFileName($fileName);
|
||||||
$increment = 2;
|
|
||||||
while(Media::find($name . '-' . $increment) !== null) {
|
if(($request->has('filestart') || $request->has('fileappend')) && $request->has('filesize')) {
|
||||||
$increment++;
|
$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';
|
||||||
}
|
}
|
||||||
|
|
||||||
$media = Media::Create([
|
// Append the chunk to the temporary file
|
||||||
'title' => $request->get('title', $name),
|
$fp = fopen($tempFilePath, $filemode);
|
||||||
'user_id' => auth()->id(),
|
if ($fp) {
|
||||||
'name' => $name,
|
fwrite($fp, file_get_contents($file->getRealPath()));
|
||||||
'size' => $file->getSize(),
|
fclose($fp);
|
||||||
'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
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function download(Request $request, Media $media)
|
public function download(Request $request, Media $media)
|
||||||
{
|
{
|
||||||
$file = $media->path();
|
$file = $media->path();
|
||||||
|
|||||||
@@ -4,12 +4,23 @@ const SMMediaPicker = {
|
|||||||
return SM.mimeMatches(file.type, Alpine.store('media').require_mime_type);
|
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));
|
const titles = Array.from(validFiles).map((file) => SM.toTitleCase(file.name));
|
||||||
|
|
||||||
SM.upload(validFiles, (response) => {
|
SM.upload(validFiles, (response) => {
|
||||||
|
if(response.files) {
|
||||||
response.files.forEach((file) => {
|
response.files.forEach((file) => {
|
||||||
SMMediaPicker.updateSelection(file.data.name);
|
SMMediaPicker.updateSelection(file.data.name);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
SMMediaPicker.open(
|
SMMediaPicker.open(
|
||||||
Alpine.store('media').selected,
|
Alpine.store('media').selected,
|
||||||
{
|
{
|
||||||
@@ -107,6 +118,11 @@ const SMMediaPicker = {
|
|||||||
|
|
||||||
html: `
|
html: `
|
||||||
<div class="flex flex-col h-full w-full" x-data="{tab: 'browser', showFileDrop: false}">
|
<div class="flex flex-col h-full w-full" x-data="{tab: 'browser', showFileDrop: false}">
|
||||||
|
<template x-if="$store.media.error">
|
||||||
|
<div class="flex justify-center" role="alert">
|
||||||
|
<p class="relative bg-red-100 border border-red-400 text-red-700 py-2 pl-4 pr-8 text-xs rounded mb-4"><span x-text="$store.media.error"></span><i class="fa-solid fa-close text-red-900 hover:text-red-700 cursor-pointer absolute top-2 right-2" x-on:click="$store.media.error=null;"></i></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<ul class="flex -mb-[1px] z-10">
|
<ul class="flex -mb-[1px] z-10">
|
||||||
<li x-show="$store.media.allow_uploads" class="cursor-pointer border px-3 py-2 rounded-t-lg hover:border-t-gray-300 hover:border-x-gray-300" :class="{ 'border-gray-300': tab === 'upload', 'border-b-white': tab === 'upload', 'border-transparent': tab !== 'upload' }" x-on:click.prevent="tab='upload'">Upload</li>
|
<li x-show="$store.media.allow_uploads" class="cursor-pointer border px-3 py-2 rounded-t-lg hover:border-t-gray-300 hover:border-x-gray-300" :class="{ 'border-gray-300': tab === 'upload', 'border-b-white': tab === 'upload', 'border-transparent': tab !== 'upload' }" x-on:click.prevent="tab='upload'">Upload</li>
|
||||||
<li class="cursor-pointer border px-3 py-2 rounded-t-lg hover:border-t-gray-300 hover:border-x-gray-300" :class="{ 'border-gray-300': tab === 'browser', 'border-b-white': tab === 'browser', 'border-transparent': tab !== 'browser' }" x-on:click.prevent="tab='browser'">Browser</li>
|
<li class="cursor-pointer border px-3 py-2 rounded-t-lg hover:border-t-gray-300 hover:border-x-gray-300" :class="{ 'border-gray-300': tab === 'browser', 'border-b-white': tab === 'browser', 'border-transparent': tab !== 'browser' }" x-on:click.prevent="tab='browser'">Browser</li>
|
||||||
|
|||||||
@@ -15,9 +15,12 @@
|
|||||||
<li class="flex items-center min-h-10">
|
<li class="flex items-center min-h-10">
|
||||||
<img class="w-10 mr-2" :src="file.thumbnail" />
|
<img class="w-10 mr-2" :src="file.thumbnail" />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
|
<div>
|
||||||
<a class="link" :href="file.url" x-text="file.title" target="_blank"></a>
|
<a class="link" :href="file.url" x-text="file.title" target="_blank"></a>
|
||||||
<i x-show="file.password" x-cloak class="fa-solid fa-lock text-xs text-gray-400 -translate-x-0.5 -translate-y-1.5 scale-75"></i>
|
<i x-show="file.password" x-cloak class="fa-solid fa-lock text-xs text-gray-400 -translate-x-0.5 -translate-y-1.5 scale-75"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="text-xs text-gray-400" x-text="file.file_type"></span>
|
||||||
|
</div>
|
||||||
<a class="cursor-pointer text-gray-400 w-7 text-center hover:text-primary-color" :href="file.url + '?download=1'"><i class="fa-solid fa-download"></i></a>
|
<a class="cursor-pointer text-gray-400 w-7 text-center hover:text-primary-color" :href="file.url + '?download=1'"><i class="fa-solid fa-download"></i></a>
|
||||||
@if($editor)
|
@if($editor)
|
||||||
<i class="text-gray-400 w-7 text-center fa-solid fa-trash hover:text-red-500 cursor-pointer" x-on:click.prevent="removeFile(file.name)"></i>
|
<i class="text-gray-400 w-7 text-center fa-solid fa-trash hover:text-red-500 cursor-pointer" x-on:click.prevent="removeFile(file.name)"></i>
|
||||||
|
|||||||
Reference in New Issue
Block a user