bug fixes
This commit is contained in:
@@ -33,9 +33,9 @@ class MediaConductor extends Conductor
|
|||||||
*
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $defaultFilters = [
|
// protected $defaultFilters = [
|
||||||
'status' => 'OK'
|
// 'status' => 'OK'
|
||||||
];
|
// ];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,60 +29,6 @@ class MediaJobConductor extends Conductor
|
|||||||
protected $includes = ['user'];
|
protected $includes = ['user'];
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an array of model fields visible to the current user.
|
|
||||||
*
|
|
||||||
* @param Model $model The model in question.
|
|
||||||
* @return array The array of field names.
|
|
||||||
*/
|
|
||||||
public function fields(Model $model): array
|
|
||||||
{
|
|
||||||
$fields = parent::fields($model);
|
|
||||||
|
|
||||||
/** @var \App\Models\User */
|
|
||||||
$user = auth()->user();
|
|
||||||
if ($user === null || $user->hasPermission('admin/media') === false) {
|
|
||||||
$fields = arrayRemoveItem($fields, ['permission', 'storage']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a scope query on the collection before anything else.
|
|
||||||
*
|
|
||||||
* @param Builder $builder The builder in use.
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function scope(Builder $builder): void
|
|
||||||
{
|
|
||||||
$user = auth()->user();
|
|
||||||
if ($user === null) {
|
|
||||||
$builder->where('permission', '');
|
|
||||||
} else {
|
|
||||||
$builder->where('permission', '')->orWhereIn('permission', $user->permissions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return if the current model is visible.
|
|
||||||
*
|
|
||||||
* @param Model $model The model.
|
|
||||||
* @return boolean Allow model to be visible.
|
|
||||||
*/
|
|
||||||
public static function viewable(Model $model): bool
|
|
||||||
{
|
|
||||||
if ($model->permission !== '') {
|
|
||||||
/** @var \App\Models\User */
|
|
||||||
$user = auth()->user();
|
|
||||||
if ($user === null || $user->hasPermission($model->permission) === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return if the current model is creatable.
|
* Return if the current model is creatable.
|
||||||
*
|
*
|
||||||
@@ -90,8 +36,7 @@ class MediaJobConductor extends Conductor
|
|||||||
*/
|
*/
|
||||||
public static function creatable(): bool
|
public static function creatable(): bool
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
return false;
|
||||||
return ($user !== null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,10 +47,7 @@ class MediaJobConductor extends Conductor
|
|||||||
*/
|
*/
|
||||||
public static function updatable(Model $model): bool
|
public static function updatable(Model $model): bool
|
||||||
{
|
{
|
||||||
/** @var \App\Models\User */
|
return false;
|
||||||
$user = auth()->user();
|
|
||||||
return ($user !== null && (strcasecmp($model->user_id, $user->id) === 0 ||
|
|
||||||
$user->hasPermission('admin/media') === true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,9 +58,7 @@ class MediaJobConductor extends Conductor
|
|||||||
*/
|
*/
|
||||||
public static function destroyable(Model $model): bool
|
public static function destroyable(Model $model): bool
|
||||||
{
|
{
|
||||||
/** @var \App\Models\User */
|
return false;
|
||||||
$user = auth()->user();
|
|
||||||
return ($user !== null && ($model->user_id === $user->id || $user->hasPermission('admin/media') === true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -82,88 +82,7 @@ class MediaController extends ApiController
|
|||||||
return $this->respondWithErrors(['file' => 'The browser did not upload the file correctly to the server.']);
|
return $this->respondWithErrors(['file' => 'The browser did not upload the file correctly to the server.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate file object
|
return $this->storeOrUpdate($request, null);
|
||||||
if ($file->isValid() !== true) {
|
|
||||||
switch ($file->getError()) {
|
|
||||||
case UPLOAD_ERR_INI_SIZE:
|
|
||||||
case UPLOAD_ERR_FORM_SIZE:
|
|
||||||
return $this->respondTooLarge();
|
|
||||||
case UPLOAD_ERR_PARTIAL:
|
|
||||||
return $this->respondWithErrors([$file => 'The file upload was interrupted.']);
|
|
||||||
default:
|
|
||||||
return $this->respondWithErrors([$file => 'An error occurred uploading the file to the server.']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($file->getSize() > Media::getMaxUploadSize()) {
|
|
||||||
return $this->respondTooLarge();
|
|
||||||
}
|
|
||||||
|
|
||||||
// create/get media job
|
|
||||||
$mediaJob = null;
|
|
||||||
$filename = '';
|
|
||||||
$data = [];
|
|
||||||
|
|
||||||
if($request->missing('job_id') === true) {
|
|
||||||
/** @var \App\Models\User */
|
|
||||||
$user = auth()->user();
|
|
||||||
|
|
||||||
$mediaJob = new MediaJob();
|
|
||||||
$mediaJob->user_id = $user->id;
|
|
||||||
|
|
||||||
$data['title'] = $request->get('title', '');
|
|
||||||
$data['name'] = $request->has('chunk') === true ? $request->get('name', '') : $file->getClientOriginalName();
|
|
||||||
$data['size'] = $request->has('chunk') === true ? 0 : $file->getSize();
|
|
||||||
$data['mime_type'] = $request->has('chunk') === true ? '' : $file->getMimeType();
|
|
||||||
$data['storage'] = $request->get('storage', '');
|
|
||||||
|
|
||||||
if($request->has('transform') === true) {
|
|
||||||
$data['transform'] = $request->get('transform');
|
|
||||||
}
|
|
||||||
|
|
||||||
$filename = $request->get('name', '');
|
|
||||||
$mediaJob->setStatusWaiting();
|
|
||||||
} else {
|
|
||||||
$mediaJob = MediaJob::find($request->get('job_id'));
|
|
||||||
if($mediaJob === null || $mediaJob->exists() === false) {
|
|
||||||
$this->respondNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode($mediaJob->data);
|
|
||||||
if($data === null) {
|
|
||||||
Log::error(`{$mediaJob->id} contains no data`);
|
|
||||||
return $this->respondServerError();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(array_key_exists('name', $data) === false) {
|
|
||||||
Log::error(`{$mediaJob->id} data does not contain the name key`);
|
|
||||||
return $this->respondServerError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if($mediaJob === null || $filename === '') {
|
|
||||||
Log::error(`media job or filename does not exist`);
|
|
||||||
return $this->respondServerError();
|
|
||||||
}
|
|
||||||
|
|
||||||
// save uploaded file
|
|
||||||
$temporaryFilePath = generateTempFilePath(pathinfo($filename, PATHINFO_EXTENSION), $request->get('chunk', ''));
|
|
||||||
copy($file->path(), $temporaryFilePath);
|
|
||||||
|
|
||||||
if($request->has('chunk') === true) {
|
|
||||||
$data['chunks'][$request->get('chunk', '1')] = $temporaryFilePath;
|
|
||||||
} else {
|
|
||||||
$data['file'] = $temporaryFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
$mediaJob->data = json_encode($data, true);
|
|
||||||
$mediaJob->save();
|
|
||||||
$mediaJob->process();
|
|
||||||
|
|
||||||
return $this->respondAsResource(
|
|
||||||
MediaJobConductor::model($request, $mediaJob),
|
|
||||||
['respondCode' => HttpResponseCodes::HTTP_ACCEPTED]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,45 +94,150 @@ class MediaController extends ApiController
|
|||||||
*/
|
*/
|
||||||
public function update(MediaRequest $request, Media $medium)
|
public function update(MediaRequest $request, Media $medium)
|
||||||
{
|
{
|
||||||
|
// allowed to update a media item
|
||||||
if (MediaConductor::updatable($medium) === false) {
|
if (MediaConductor::updatable($medium) === false) {
|
||||||
return $this->respondForbidden();
|
return $this->respondForbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this->storeOrUpdate($request, $medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a new media resource
|
||||||
|
*
|
||||||
|
* @param \App\Http\Requests\MediaRequest $request The uploaded media.
|
||||||
|
* @param \App\Models\Media|null $medium The specified media.
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function storeOrUpdate(MediaRequest $request, Media|null $medium)
|
||||||
|
{
|
||||||
$file = $request->file('file');
|
$file = $request->file('file');
|
||||||
if ($file !== null) {
|
if ($file !== null) {
|
||||||
$jsonResult = $this->validateFileItem($file);
|
// validate file object
|
||||||
if ($jsonResult !== null) {
|
if ($file->isValid() !== true) {
|
||||||
return $jsonResult;
|
switch ($file->getError()) {
|
||||||
|
case UPLOAD_ERR_INI_SIZE:
|
||||||
|
case UPLOAD_ERR_FORM_SIZE:
|
||||||
|
return $this->respondTooLarge();
|
||||||
|
case UPLOAD_ERR_PARTIAL:
|
||||||
|
return $this->respondWithErrors([$file => 'The file upload was interrupted.']);
|
||||||
|
default:
|
||||||
|
return $this->respondWithErrors([$file => 'An error occurred uploading the file to the server.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($file->getSize() > Media::getMaxUploadSize()) {
|
||||||
|
return $this->respondTooLarge();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$medium->status('Updating Media');
|
// create/get media job
|
||||||
$medium->update($request->except(['file','transform']));
|
$mediaJob = null;
|
||||||
|
$data = [];
|
||||||
|
|
||||||
$transformData = [];
|
if ($request->missing('job_id') === true) {
|
||||||
|
/** @var \App\Models\User */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$mediaJob = new MediaJob();
|
||||||
|
$mediaJob->user_id = $user->id;
|
||||||
|
if ($medium !== null) {
|
||||||
|
$mediaJob->media_id = $medium->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('title') === true || $file !== null) {
|
||||||
|
$data['title'] = $request->get('title', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('name') === true || $file !== null) {
|
||||||
|
$data['name'] = $request->has('chunk') === true ? $request->get('name', '') : $file->getClientOriginalName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($file !== null) {
|
||||||
|
$data['size'] = $request->has('chunk') === true ? 0 : $file->getSize();
|
||||||
|
$data['mime_type'] = $request->has('chunk') === true ? '' : $file->getMimeType();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('storage') === true || $file !== null) {
|
||||||
|
$data['storage'] = $request->get('storage', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('transform') === true) {
|
||||||
|
$transform = [];
|
||||||
|
|
||||||
|
foreach ($request->get('transform') as $key => $value) {
|
||||||
|
if (is_string($value) === true) {
|
||||||
|
if (preg_match('/^rotate-(-?\d+)$/', $value, $matches) !== false) {
|
||||||
|
unset($transform[$key]);
|
||||||
|
$transform['rotate'] = $matches[1];
|
||||||
|
} elseif (preg_match('/^flip-([vh]|vh|hv)$/', $value, $matches) !== false) {
|
||||||
|
unset($transform[$key]);
|
||||||
|
$transform['flip'] = $matches[1];
|
||||||
|
} elseif (preg_match('/^crop-(\d+)-(\d+)$/', $value, $matches) !== false) {
|
||||||
|
unset($transform[$key]);
|
||||||
|
$transform['crop'] = ['width' => $matches[1], 'height' => $matches[2]];
|
||||||
|
} elseif (preg_match('/^crop-(\d+)-(\d+)-(\d+)-(\d+)$/', $value, $matches) !== false) {
|
||||||
|
unset($transform[$key]);
|
||||||
|
$transform['crop'] = ['width' => $matches[1], 'height' => $matches[2], 'x' => $matches[3], 'y' => $matches[4]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($transform) > 0) {
|
||||||
|
$data['transform'] = $transform;
|
||||||
|
}
|
||||||
|
}//end if
|
||||||
|
|
||||||
|
$mediaJob->setStatusWaiting();
|
||||||
|
} else {
|
||||||
|
$mediaJob = MediaJob::find($request->get('job_id'));
|
||||||
|
if ($mediaJob === null || $mediaJob->exists() === false) {
|
||||||
|
$this->respondNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($mediaJob->data, true);
|
||||||
|
if ($data === null) {
|
||||||
|
Log::error(`{$mediaJob->id} contains no data`);
|
||||||
|
return $this->respondServerError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('name', $data) === false) {
|
||||||
|
Log::error(`{$mediaJob->id} data does not contain the name key`);
|
||||||
|
return $this->respondServerError();
|
||||||
|
}
|
||||||
|
}//end if
|
||||||
|
|
||||||
|
if ($mediaJob === null) {
|
||||||
|
Log::error(`media job does not exist`);
|
||||||
|
return $this->respondServerError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// save uploaded file
|
||||||
if ($file !== null) {
|
if ($file !== null) {
|
||||||
$temporaryFilePath = generateTempFilePath();
|
if ($data['name'] === '') {
|
||||||
|
Log::error(`filename does not exist`);
|
||||||
|
return $this->respondServerError();
|
||||||
|
}
|
||||||
|
|
||||||
|
$temporaryFilePath = generateTempFilePath(pathinfo($data['name'], PATHINFO_EXTENSION), $request->get('chunk', ''));
|
||||||
copy($file->path(), $temporaryFilePath);
|
copy($file->path(), $temporaryFilePath);
|
||||||
|
|
||||||
$transformData = array_merge($transformData, ['file' => [
|
if ($request->has('chunk') === true) {
|
||||||
'path' => $temporaryFilePath,
|
$data['chunks'][$request->get('chunk', '1')] = $temporaryFilePath;
|
||||||
'size' => $file->getSize(),
|
$data['chunk_count'] = $request->get('chunk_count', 1);
|
||||||
'mime_type' => $file->getMimeType(),
|
} else {
|
||||||
]
|
$data['file'] = $temporaryFilePath;
|
||||||
]);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->has('transform') === true) {
|
$mediaJob->data = json_encode($data, true);
|
||||||
$transformData = array_merge($transformData, array_map('trim', explode(',', $request->get('transform'))));
|
$mediaJob->save();
|
||||||
}
|
$mediaJob->process();
|
||||||
|
|
||||||
if (count($transformData) > 0) {
|
return $this->respondAsResource(
|
||||||
$medium->transform($transformData);
|
MediaJobConductor::model($request, $mediaJob),
|
||||||
} else {
|
['resourceName' => 'media_job', 'respondCode' => HttpResponseCodes::HTTP_ACCEPTED]
|
||||||
$medium->ok();
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return $this->respondAsResource(MediaConductor::model($request, $medium));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Conductors\MediaJobConductor;
|
||||||
|
use App\Http\Controllers\Api\ApiController;
|
||||||
|
use App\Models\MediaJob;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class MediaJobController extends Controller
|
class MediaJobController extends ApiController
|
||||||
{
|
{
|
||||||
//
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request The endpoint request.
|
||||||
|
* @param \App\Models\MediaJob $mediaJob The request media job.
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function show(Request $request, MediaJob $mediaJob)
|
||||||
|
{
|
||||||
|
if (MediaJobConductor::viewable($mediaJob) === true) {
|
||||||
|
return $this->respondAsResource(MediaJobConductor::model($request, $mediaJob), ['resourceName' => 'media_job']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->respondForbidden();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ class MediaRequest extends BaseRequest
|
|||||||
Rule::requiredIf(function () {
|
Rule::requiredIf(function () {
|
||||||
return request()->has('chunk') && request('chunk') != 1;
|
return request()->has('chunk') && request('chunk') != 1;
|
||||||
}),
|
}),
|
||||||
Rule::forbiddenUnless(function () {
|
|
||||||
return request()->has('chunk') && request('chunk') != 1;
|
|
||||||
}),
|
|
||||||
'string',
|
'string',
|
||||||
],
|
],
|
||||||
'name' => [
|
'name' => [
|
||||||
@@ -24,16 +21,7 @@ class MediaRequest extends BaseRequest
|
|||||||
}),
|
}),
|
||||||
'string',
|
'string',
|
||||||
],
|
],
|
||||||
'chunk' => [
|
'chunk' => 'required_with:chunk_count|integer|min:1|max:99|lte:chunk_count',
|
||||||
'required_with:chunk_count',
|
|
||||||
'integer',
|
|
||||||
'min:1',
|
|
||||||
'max:99',
|
|
||||||
Rule::passes(function ($attribute, $value) {
|
|
||||||
$chunkCount = request('chunk_count');
|
|
||||||
return $value <= $chunkCount;
|
|
||||||
})->withMessage('The chunk must be less than or equal to chunk_count.'),
|
|
||||||
],
|
|
||||||
'chunk_count' => 'required_with:chunk|integer|min:1',
|
'chunk_count' => 'required_with:chunk|integer|min:1',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use FFMpeg\FFProbe;
|
|||||||
use FFMpeg\Format\VideoInterface;
|
use FFMpeg\Format\VideoInterface;
|
||||||
use Intervention\Image\Facades\Image;
|
use Intervention\Image\Facades\Image;
|
||||||
|
|
||||||
|
/** @property on $format */
|
||||||
class MediaWorkerJob implements ShouldQueue
|
class MediaWorkerJob implements ShouldQueue
|
||||||
{
|
{
|
||||||
use Dispatchable;
|
use Dispatchable;
|
||||||
@@ -32,25 +33,16 @@ class MediaWorkerJob implements ShouldQueue
|
|||||||
*/
|
*/
|
||||||
protected $mediaJob;
|
protected $mediaJob;
|
||||||
|
|
||||||
/**
|
|
||||||
* Actions should be silent (not update the status field)
|
|
||||||
*
|
|
||||||
* @var boolean
|
|
||||||
*/
|
|
||||||
protected $silent;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new job instance.
|
* Create a new job instance.
|
||||||
*
|
*
|
||||||
* @param MediaJob $mediaJob The mediaJob model.
|
* @param MediaJob $mediaJob The mediaJob model.
|
||||||
* @param array $actions The media actions to make.
|
|
||||||
* @param boolean $silent Update the media status with progress.
|
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(MediaJob $mediaJob, bool $silent = false)
|
public function __construct(MediaJob $mediaJob)
|
||||||
{
|
{
|
||||||
$mediaJob = $mediaJob;
|
$this->mediaJob = $mediaJob;
|
||||||
$this->silent = $silent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,29 +52,21 @@ class MediaWorkerJob implements ShouldQueue
|
|||||||
*/
|
*/
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
$media = null;
|
$media = $this->mediaJob->media()->first();
|
||||||
|
$newMedia = false;
|
||||||
$data = json_decode($this->mediaJob->data, true);
|
$data = json_decode($this->mediaJob->data, true);
|
||||||
|
|
||||||
// new Media();
|
|
||||||
// $this->mediaJob->media_id = $media->id;
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// FILE
|
// FILE
|
||||||
if (array_key_exists('file', $data) === true) {
|
if (array_key_exists('file', $data) === true) {
|
||||||
if(file_exists($data['file']) === false) {
|
if (file_exists($data['file']) === false) {
|
||||||
$errorStr = 'temporary upload file no longer exists';
|
$this->throwMediaJobFailure('temporary upload file no longer exists');
|
||||||
$this->mediaJob->setStatusFailed($errorStr);
|
|
||||||
Log::info($errorStr);
|
|
||||||
throw new \Exception($errorStr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert HEIC files to JPG
|
// convert HEIC files to JPG
|
||||||
$fileExtension = File::extension($data['file']);
|
$fileExtension = File::extension($data['file']);
|
||||||
if ($fileExtension === 'heic') {
|
if ($fileExtension === 'heic') {
|
||||||
if ($this->silent === false) {
|
$this->mediaJob->setStatusProcessing(0, 'converting image');
|
||||||
$this->mediaJob->setStatusProcessing(0, 'converting image');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the path without the file name
|
// Get the path without the file name
|
||||||
$uploadedFileDirectory = dirname($data['file']);
|
$uploadedFileDirectory = dirname($data['file']);
|
||||||
@@ -91,22 +75,26 @@ class MediaWorkerJob implements ShouldQueue
|
|||||||
$jpgFileName = pathinfo($data['file'], PATHINFO_FILENAME) . '.jpg';
|
$jpgFileName = pathinfo($data['file'], PATHINFO_FILENAME) . '.jpg';
|
||||||
$jpgFilePath = $uploadedFileDirectory . '/' . $jpgFileName;
|
$jpgFilePath = $uploadedFileDirectory . '/' . $jpgFileName;
|
||||||
if (file_exists($jpgFilePath) === true) {
|
if (file_exists($jpgFilePath) === true) {
|
||||||
$errorStr = 'file already exists on server';
|
$this->throwMediaJobFailure('file already exists on server');
|
||||||
$this->mediaJob->setStatusFailed($errorStr);
|
|
||||||
Log::info($errorStr);
|
|
||||||
throw new \Exception($errorStr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Image::make($data['file'])->save($jpgFilePath);
|
Image::make($data['file'])->save($jpgFilePath);
|
||||||
|
|
||||||
// Update the uploaded file path and file name
|
// Update the uploaded file path and file name
|
||||||
unlink($data['file']);
|
unlink($data['file']);
|
||||||
$filePath = $jpgFilePath;
|
|
||||||
$data['file'] = $jpgFileName;
|
$data['file'] = $jpgFileName;
|
||||||
}//end if
|
}//end if
|
||||||
|
|
||||||
// get storage
|
// get storage
|
||||||
$storage = $data['storage'];
|
$storage = '';
|
||||||
|
if ($media === null) {
|
||||||
|
if (array_key_exists('storage', $data) === true) {
|
||||||
|
$storage = $data['storage'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$storage = $media->storage;
|
||||||
|
}
|
||||||
|
|
||||||
if ($storage === '') {
|
if ($storage === '') {
|
||||||
if (strpos($data['mime_type'], 'image/') === 0) {
|
if (strpos($data['mime_type'], 'image/') === 0) {
|
||||||
$storage = 'local';
|
$storage = 'local';
|
||||||
@@ -118,46 +106,31 @@ class MediaWorkerJob implements ShouldQueue
|
|||||||
// Check if file already exists
|
// Check if file already exists
|
||||||
if (Storage::disk($storage)->exists($data['name']) === true) {
|
if (Storage::disk($storage)->exists($data['name']) === true) {
|
||||||
if (array_key_exists('replace', $data) === false || isTrue($data['replace']) === false) {
|
if (array_key_exists('replace', $data) === false || isTrue($data['replace']) === false) {
|
||||||
$this->mediaJob->setStatusFailed('file already exists on server');
|
$this->throwMediaJobFailure('file already exists on server');
|
||||||
$errorStr = "cannot upload file " . $storage . " " . // phpcs:ignore
|
|
||||||
"/ " . $data['name'] . " as it already exists";
|
|
||||||
Log::info($errorStr);
|
|
||||||
throw new \Exception($errorStr);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$media = new Media([
|
if ($media === null) {
|
||||||
'user_id' => $this->mediaJob->user_id,
|
$newMedia = true;
|
||||||
'title' => $data['title'],
|
$media = new Media([
|
||||||
'name' => $data['name'],
|
'user_id' => $this->mediaJob->user_id,
|
||||||
'mime_type' => $data['mime_type'],
|
'title' => $data['title'],
|
||||||
'size' => $data['size'],
|
'name' => $data['name'],
|
||||||
'storage' => $storage
|
'mime_type' => $data['mime_type'],
|
||||||
]);
|
'size' => $data['size'],
|
||||||
|
'storage' => $storage
|
||||||
|
]);
|
||||||
|
}//end if
|
||||||
|
|
||||||
$media->setStagingFile($filePath);
|
$media->setStagingFile($data['file']);
|
||||||
} else {
|
} else {
|
||||||
$media = $this->mediaJob->media()->first();
|
if ($media === null) {
|
||||||
if($media === null || $media->exists() === false) {
|
$this->throwMediaJobFailure('The media item no longer exists');
|
||||||
$errorStr = 'The media item no longer exists';
|
|
||||||
$this->mediaJob->setStatusFailed($errorStr);
|
|
||||||
Log::info($errorStr);
|
|
||||||
throw new \Exception($errorStr);
|
|
||||||
}
|
}
|
||||||
}
|
}//end if
|
||||||
|
|
||||||
// TODO:
|
if (array_key_exists('transform', $data) === true) {
|
||||||
// mime_type may not be in data if we are just doing a transform...
|
|
||||||
// if fails, need to delete the staging file
|
|
||||||
// do not delete the file straight away incase we fail the transform
|
|
||||||
// delete the media object if we fail and it is a new media object
|
|
||||||
// UPDATE IN CONTROLLER NEEDS TO BE FIXED
|
|
||||||
// STATUS field can be removed in Media object
|
|
||||||
// Front end needs to support non status field and media jobs
|
|
||||||
|
|
||||||
if(array_key_exists('transform', $data) === true) {
|
|
||||||
$media->createStagingFile();
|
$media->createStagingFile();
|
||||||
$media->deleteFile();
|
|
||||||
|
|
||||||
// Modifications
|
// Modifications
|
||||||
if (strpos($media->mime_type, 'image/') === 0) {
|
if (strpos($media->mime_type, 'image/') === 0) {
|
||||||
@@ -168,9 +141,7 @@ class MediaWorkerJob implements ShouldQueue
|
|||||||
if (array_key_exists("rotate", $data['transform']) === true) {
|
if (array_key_exists("rotate", $data['transform']) === true) {
|
||||||
$rotate = intval($data['transform']['rotate']);
|
$rotate = intval($data['transform']['rotate']);
|
||||||
if ($rotate !== 0) {
|
if ($rotate !== 0) {
|
||||||
if ($this->silent === false) {
|
$this->mediaJob->setStatusProcessing(0, 'rotating image');
|
||||||
$this->mediaJob->setStatusProcessing(0, 'rotating image');
|
|
||||||
}
|
|
||||||
$image = $image->rotate($rotate);
|
$image = $image->rotate($rotate);
|
||||||
$modified = true;
|
$modified = true;
|
||||||
}
|
}
|
||||||
@@ -179,17 +150,13 @@ class MediaWorkerJob implements ShouldQueue
|
|||||||
// FLIP-H/V
|
// FLIP-H/V
|
||||||
if (array_key_exists('flip', $data['transform']) === true) {
|
if (array_key_exists('flip', $data['transform']) === true) {
|
||||||
if (stripos($data['transform']['flip'], 'h') !== false) {
|
if (stripos($data['transform']['flip'], 'h') !== false) {
|
||||||
if ($this->silent === false) {
|
$this->mediaJob->setStatusProcessing(0, 'flipping image');
|
||||||
$this->mediaJob->setStatusProcessing(0, 'flipping image');
|
|
||||||
}
|
|
||||||
$image = $image->flip('h');
|
$image = $image->flip('h');
|
||||||
$modified = true;
|
$modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stripos($data['transform']['flip'], 'v') !== false) {
|
if (stripos($data['transform']['flip'], 'v') !== false) {
|
||||||
if ($this->silent === false) {
|
$this->mediaJob->setStatusProcessing(0, 'flipping image');
|
||||||
$this->mediaJob->setStatusProcessing(0, 'flipping image');
|
|
||||||
}
|
|
||||||
$image = $image->flip('v');
|
$image = $image->flip('v');
|
||||||
$modified = true;
|
$modified = true;
|
||||||
}
|
}
|
||||||
@@ -203,9 +170,7 @@ class MediaWorkerJob implements ShouldQueue
|
|||||||
$x = intval(arrayDefaultValue("x", $cropData, 0));
|
$x = intval(arrayDefaultValue("x", $cropData, 0));
|
||||||
$y = intval(arrayDefaultValue("y", $cropData, 0));
|
$y = intval(arrayDefaultValue("y", $cropData, 0));
|
||||||
|
|
||||||
if ($this->silent === false) {
|
$this->mediaJob->setStatusProcessing(0, 'cropping image');
|
||||||
$this->mediaJob->setStatusProcessing(0, 'cropping image');
|
|
||||||
}
|
|
||||||
$image = $image->crop($width, $height, $x, $y);
|
$image = $image->crop($width, $height, $x, $y);
|
||||||
$modified = true;
|
$modified = true;
|
||||||
}//end if
|
}//end if
|
||||||
@@ -253,17 +218,13 @@ class MediaWorkerJob implements ShouldQueue
|
|||||||
// FLIP-H/V
|
// FLIP-H/V
|
||||||
if (array_key_exists('flip', $data['transform']) === true) {
|
if (array_key_exists('flip', $data['transform']) === true) {
|
||||||
if (stripos($data['transform']['flip'], 'h') !== false) {
|
if (stripos($data['transform']['flip'], 'h') !== false) {
|
||||||
if ($this->silent === false) {
|
$this->mediaJob->setStatusProcessing(0, 'flipping video');
|
||||||
$media->status('Flipping video');
|
|
||||||
}
|
|
||||||
$filters->hflip()->synchronize();
|
$filters->hflip()->synchronize();
|
||||||
$modified = true;
|
$modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stripos($data['transform']['flip'], 'v') !== false) {
|
if (stripos($data['transform']['flip'], 'v') !== false) {
|
||||||
if ($this->silent === false) {
|
$this->mediaJob->setStatusProcessing(0, 'flipping video');
|
||||||
$media->status('Flipping video');
|
|
||||||
}
|
|
||||||
$filters->vflip()->synchronize();
|
$filters->vflip()->synchronize();
|
||||||
$modified = true;
|
$modified = true;
|
||||||
}
|
}
|
||||||
@@ -281,52 +242,67 @@ class MediaWorkerJob implements ShouldQueue
|
|||||||
|
|
||||||
$cropDimension = new Dimension($width, $height);
|
$cropDimension = new Dimension($width, $height);
|
||||||
|
|
||||||
if ($this->silent === false) {
|
$this->mediaJob->setStatusProcessing(0, 'cropping video');
|
||||||
$media->status('Cropping video');
|
|
||||||
}
|
|
||||||
$filters->crop($cropDimension, $x, $y)->synchronize();
|
$filters->crop($cropDimension, $x, $y)->synchronize();
|
||||||
$modified = true;
|
$modified = true;
|
||||||
}//end if
|
}//end if
|
||||||
|
|
||||||
$tempFilePath = generateTempFilePath(pathinfo($stagingFilePath, PATHINFO_EXTENSION));
|
$tempFilePath = generateTempFilePath(pathinfo($stagingFilePath, PATHINFO_EXTENSION));
|
||||||
if (method_exists($format, 'on') === true) {
|
if (method_exists($format, 'on') === true) {
|
||||||
$media = $media;
|
$mediaJob = $this->mediaJob;
|
||||||
$format->on('progress', function ($video, $format, $percentage) use ($media) {
|
$format->on('progress', function ($video, $format, $percentage) use ($mediaJob) {
|
||||||
$media->status("{$percentage}% transcoded");
|
$mediaJob->setStatusProcessing($percentage, 'transcoded');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if($modified === true) {
|
if ($modified === true) {
|
||||||
$video->save($format, $tempFilePath);
|
$video->save($format, $tempFilePath);
|
||||||
$media->changeStagingFile($tempFilePath);
|
$media->changeStagingFile($tempFilePath);
|
||||||
}
|
}
|
||||||
}//end if
|
}//end if
|
||||||
|
|
||||||
// Move file
|
// Move file
|
||||||
if (array_key_exists("move", $data['transform']) === true) {
|
if (array_key_exists('move', $data['transform']) === true) {
|
||||||
if (array_key_exists("storage", $data['transform']['move']) === true) {
|
if (array_key_exists('storage', $data['transform']['move']) === true) {
|
||||||
$newStorage = $data['transform']['move"]["storage'];
|
$newStorage = $data['transform']['move']['storage'];
|
||||||
if ($media->storage !== $newStorage) {
|
if ($media->storage !== $newStorage) {
|
||||||
if (Storage::has($newStorage) === true) {
|
if (Storage::has($newStorage) === true) {
|
||||||
|
$media->createStagingFile();
|
||||||
$media->storage = $newStorage;
|
$media->storage = $newStorage;
|
||||||
} else {
|
} else {
|
||||||
$media->error("Cannot move file to '{$newStorage}' as it does not exist");
|
$this->throwMediaJobFailure("Cannot move file to '{$newStorage}' as it does not exist");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}//end if
|
||||||
|
|
||||||
|
// Update attributes
|
||||||
|
if (array_key_exists('title', $data) === true) {
|
||||||
|
$media->title = $data['title'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finish media object
|
// Finish media object
|
||||||
$media->saveStagingFile(true);
|
if ($media->hasStagingFile() === true) {
|
||||||
|
$this->mediaJob->setStatusProcessing(-1, 'uploading to cdn');
|
||||||
|
$media->deleteFile();
|
||||||
|
$media->saveStagingFile(true);
|
||||||
|
}
|
||||||
|
|
||||||
$media->save();
|
$media->save();
|
||||||
|
$this->mediaJob->media_id = $media->id;
|
||||||
$this->mediaJob->setStatusComplete();
|
$this->mediaJob->setStatusComplete();
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$media->deleteStagingFile();
|
if ($this->mediaJob->status !== 'failed') {
|
||||||
|
$this->mediaJob->setStatusFailed('Unexpected server error occurred');
|
||||||
|
}
|
||||||
|
|
||||||
// if (strpos($media->status, 'Error') !== 0) {
|
if ($media !== null) {
|
||||||
// $media->error('Failed to process the file');
|
$media->deleteStagingFile();
|
||||||
// }
|
if ($newMedia === true) {
|
||||||
|
$media->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Log::error($e->getMessage() . "\n" . $e->getFile() . " - " . $e->getLine() . "\n" . $e->getTraceAsString());
|
Log::error($e->getMessage() . "\n" . $e->getFile() . " - " . $e->getLine() . "\n" . $e->getTraceAsString());
|
||||||
$this->fail($e);
|
$this->fail($e);
|
||||||
@@ -373,4 +349,16 @@ class MediaWorkerJob implements ShouldQueue
|
|||||||
|
|
||||||
return new $formatClassName();
|
return new $formatClassName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set failure status of MediaJob and throw exception.
|
||||||
|
*
|
||||||
|
* @param string $error The failure message.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function throwMediaJobFailure(string $error): void
|
||||||
|
{
|
||||||
|
$this->mediaJob->setStatusFailed($error);
|
||||||
|
throw new \Exception($error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,35 +3,24 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Enum\HttpResponseCodes;
|
use App\Enum\HttpResponseCodes;
|
||||||
use App\Jobs\MediaJob;
|
use App\Jobs\MediaWorkerJob;
|
||||||
use App\Jobs\MoveMediaJob;
|
use App\Jobs\MoveMediaJob;
|
||||||
use App\Jobs\StoreUploadedFileJob;
|
|
||||||
use App\Traits\Uuids;
|
use App\Traits\Uuids;
|
||||||
use Exception;
|
|
||||||
use FFMpeg\Coordinate\TimeCode;
|
use FFMpeg\Coordinate\TimeCode;
|
||||||
use FFMpeg\FFMpeg;
|
use FFMpeg\FFMpeg;
|
||||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\InvalidCastException;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Config;
|
use Illuminate\Support\Facades\Config;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Intervention\Image\Exception\NotSupportedException;
|
|
||||||
use Intervention\Image\Exception\NotWritableException;
|
|
||||||
use ImagickException;
|
|
||||||
use Intervention\Image\Facades\Image;
|
use Intervention\Image\Facades\Image;
|
||||||
use InvalidArgumentException;
|
|
||||||
use Psr\Container\NotFoundExceptionInterface;
|
|
||||||
use Psr\Container\ContainerExceptionInterface;
|
|
||||||
use SplFileInfo;
|
use SplFileInfo;
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
class Media extends Model
|
class Media extends Model
|
||||||
@@ -59,7 +48,6 @@ class Media extends Model
|
|||||||
'description',
|
'description',
|
||||||
'name',
|
'name',
|
||||||
'size',
|
'size',
|
||||||
'status',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -367,7 +355,7 @@ class Media extends Model
|
|||||||
public function moveToStorage(string $storage): void
|
public function moveToStorage(string $storage): void
|
||||||
{
|
{
|
||||||
if ($storage !== $this->storage && Config::has("filesystems.disks.$storage") === true) {
|
if ($storage !== $this->storage && Config::has("filesystems.disks.$storage") === true) {
|
||||||
$this->status = "Processing media";
|
// $this->status = "Processing media";
|
||||||
MoveMediaJob::dispatch($this, $storage)->onQueue('media');
|
MoveMediaJob::dispatch($this, $storage)->onQueue('media');
|
||||||
$this->save();
|
$this->save();
|
||||||
}
|
}
|
||||||
@@ -376,36 +364,26 @@ class Media extends Model
|
|||||||
/**
|
/**
|
||||||
* Transform the media through the Media Job Queue
|
* Transform the media through the Media Job Queue
|
||||||
*
|
*
|
||||||
* @param array $transform The transform data.
|
* @param array $transform The transform data.
|
||||||
* @param boolean $silent Update the medium progress through its status field.
|
* @return MediaJob
|
||||||
* @return void
|
|
||||||
*/
|
*/
|
||||||
public function transform(array $transform, bool $silent = false): void
|
public function transform(array $transform): MediaJob
|
||||||
{
|
{
|
||||||
foreach ($transform as $key => $value) {
|
$mediaJob = new MediaJob([
|
||||||
if (is_string($value) === true) {
|
'media_id' => $this->media,
|
||||||
if (preg_match('/^rotate-(-?\d+)$/', $value, $matches) !== false) {
|
'user_id' => auth()->user()?->id,
|
||||||
unset($transform[$key]);
|
'data' => json_encode(['transform' => $transform]),
|
||||||
$transform['rotate'] = $matches[1];
|
]);
|
||||||
} elseif (preg_match('/^flip-([vh]|vh|hv)$/', $value, $matches) !== false) {
|
|
||||||
unset($transform[$key]);
|
|
||||||
$transform['flip'] = $matches[1];
|
|
||||||
} elseif (preg_match('/^crop-(\d+)-(\d+)$/', $value, $matches) !== false) {
|
|
||||||
unset($transform[$key]);
|
|
||||||
$transform['crop'] = ['width' => $matches[1], 'height' => $matches[2]];
|
|
||||||
} elseif (preg_match('/^crop-(\d+)-(\d+)-(\d+)-(\d+)$/', $value, $matches) !== false) {
|
|
||||||
unset($transform[$key]);
|
|
||||||
$transform['crop'] = ['width' => $matches[1], 'height' => $matches[2], 'x' => $matches[3], 'y' => $matches[4]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MediaJob::dispatch($this, $transform, $silent)->onQueue('media');
|
MediaWorkerJob::dispatch($mediaJob)->onQueue('media');
|
||||||
|
return $mediaJob;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->error('Failed to transform media');
|
$this->error('Failed to transform media');
|
||||||
throw $e;
|
throw $e;
|
||||||
}//end try
|
}//end try
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -498,8 +476,8 @@ class Media extends Model
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
static::fileNameHasSuffix($fileName) === true ||
|
static::fileNameHasSuffix($fileName) === true ||
|
||||||
static::fileExistsInStorage("$fileName.$extension") === true ||
|
static::fileExistsInStorage("$fileName.$extension") === true //||
|
||||||
Media::where('name', "$fileName.$extension")->where('status', 'not like', 'failed%')->exists() === true
|
// Media::where('name', "$fileName.$extension")->where('status', 'not like', 'failed%')->exists() === true
|
||||||
) {
|
) {
|
||||||
$fileName .= '-';
|
$fileName .= '-';
|
||||||
for ($i = 1; $i < $maxTries; $i++) {
|
for ($i = 1; $i < $maxTries; $i++) {
|
||||||
@@ -507,7 +485,7 @@ class Media extends Model
|
|||||||
if (
|
if (
|
||||||
static::fileExistsInStorage("$fileNameIndex.$extension") !== true &&
|
static::fileExistsInStorage("$fileNameIndex.$extension") !== true &&
|
||||||
Media::where('name', "$fileNameIndex.$extension")
|
Media::where('name', "$fileNameIndex.$extension")
|
||||||
->where('status', 'not like', 'Failed%')
|
// ->where('status', 'not like', 'Failed%')
|
||||||
->exists() !== true
|
->exists() !== true
|
||||||
) {
|
) {
|
||||||
return "$fileNameIndex.$extension";
|
return "$fileNameIndex.$extension";
|
||||||
@@ -721,32 +699,34 @@ class Media extends Model
|
|||||||
*/
|
*/
|
||||||
public function saveStagingFile(bool $delete = true, bool $silent = false): void
|
public function saveStagingFile(bool $delete = true, bool $silent = false): void
|
||||||
{
|
{
|
||||||
if (strlen($this->storage) > 0 && strlen($this->name) > 0) {
|
if ($this->stagingFilePath !== '') {
|
||||||
if (Storage::disk($this->storage)->exists($this->name) === true) {
|
if (strlen($this->storage) > 0 && strlen($this->name) > 0) {
|
||||||
Storage::disk($this->storage)->delete($this->name);
|
if (Storage::disk($this->storage)->exists($this->name) === true) {
|
||||||
|
Storage::disk($this->storage)->delete($this->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Illuminate\Filesystem\FilesystemAdapter */
|
||||||
|
$fileSystem = Storage::disk($this->storage);
|
||||||
|
// if ($silent === false) {
|
||||||
|
// $this->status('Uploading to CDN');
|
||||||
|
// }
|
||||||
|
$fileSystem->putFileAs('/', $this->stagingFilePath, $this->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var Illuminate\Filesystem\FilesystemAdapter */
|
// if ($silent === false) {
|
||||||
$fileSystem = Storage::disk($this->storage);
|
// $this->status('Generating Thumbnail');
|
||||||
if ($silent === false) {
|
// }
|
||||||
$this->status('Uploading to CDN');
|
$this->generateThumbnail();
|
||||||
|
|
||||||
|
// if ($silent === false) {
|
||||||
|
// $this->status('Generating Variants');
|
||||||
|
// }
|
||||||
|
$this->generateVariants();
|
||||||
|
|
||||||
|
if ($delete === true) {
|
||||||
|
$this->deleteStagingFile();
|
||||||
}
|
}
|
||||||
$fileSystem->putFileAs('/', $this->stagingFilePath, $this->name);
|
}//end if
|
||||||
}
|
|
||||||
|
|
||||||
if ($silent === false) {
|
|
||||||
$this->status('Generating Thumbnail');
|
|
||||||
}
|
|
||||||
$this->generateThumbnail();
|
|
||||||
|
|
||||||
if ($silent === false) {
|
|
||||||
$this->status('Generating Variants');
|
|
||||||
}
|
|
||||||
$this->generateVariants();
|
|
||||||
|
|
||||||
if ($delete === true) {
|
|
||||||
$this->deleteStagingFile();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -777,6 +757,16 @@ class Media extends Model
|
|||||||
$this->stagingFilePath = $newFile;
|
$this->stagingFilePath = $newFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is a staging file present
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function hasStagingFile(): bool
|
||||||
|
{
|
||||||
|
return $this->stagingFilePath !== "";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a Thumbnail for this media.
|
* Generate a Thumbnail for this media.
|
||||||
*
|
*
|
||||||
@@ -1019,7 +1009,7 @@ class Media extends Model
|
|||||||
*/
|
*/
|
||||||
public function ok(): void
|
public function ok(): void
|
||||||
{
|
{
|
||||||
$this->status = "OK";
|
// $this->status = "OK";
|
||||||
$this->save();
|
$this->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1030,7 +1020,7 @@ class Media extends Model
|
|||||||
*/
|
*/
|
||||||
public function error(string $error = ""): void
|
public function error(string $error = ""): void
|
||||||
{
|
{
|
||||||
$this->status = "Error" . ($error !== "" ? ": {$error}" : "");
|
// $this->status = "Error" . ($error !== "" ? ": {$error}" : "");
|
||||||
$this->save();
|
$this->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1041,7 +1031,7 @@ class Media extends Model
|
|||||||
*/
|
*/
|
||||||
public function status(string $status = ""): void
|
public function status(string $status = ""): void
|
||||||
{
|
{
|
||||||
$this->status = "Info: " . $status;
|
// $this->status = "Info: " . $status;
|
||||||
$this->save();
|
$this->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,73 +13,147 @@ class MediaJob extends Model
|
|||||||
use HasFactory;
|
use HasFactory;
|
||||||
use Uuids;
|
use Uuids;
|
||||||
|
|
||||||
public function setStatusFailed(string $statusText = ''): void {
|
|
||||||
|
/**
|
||||||
|
* The default attributes.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected $attributes = [
|
||||||
|
'user_id' => null,
|
||||||
|
'media_id' => null,
|
||||||
|
'status' => '',
|
||||||
|
'status_text' => '',
|
||||||
|
'progress' => 0,
|
||||||
|
'data' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set MediaJob status to failed.
|
||||||
|
*
|
||||||
|
* @param string $statusText The failed reason.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setStatusFailed(string $statusText = ''): void
|
||||||
|
{
|
||||||
$this->setStatus('failed', $statusText, 0);
|
$this->setStatus('failed', $statusText, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setStatusQueued(): void {
|
/**
|
||||||
|
* Set MediaJob status to queued.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setStatusQueued(): void
|
||||||
|
{
|
||||||
$this->setStatus('queued', '', 0);
|
$this->setStatus('queued', '', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setStatusWaiting(): void {
|
/**
|
||||||
|
* Set MediaJob status to waiting.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setStatusWaiting(): void
|
||||||
|
{
|
||||||
$this->setStatus('waiting', '', 0);
|
$this->setStatus('waiting', '', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setStatusProcessing(int $progress = 0, string $statusText = ''): void {
|
/**
|
||||||
if($statusText === '') {
|
* Set MediaJob status to processing.
|
||||||
|
*
|
||||||
|
* @param integer $progress The processing percentage.
|
||||||
|
* @param string $statusText The processing status text.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setStatusProcessing(int $progress = 0, string $statusText = ''): void
|
||||||
|
{
|
||||||
|
if ($statusText === '') {
|
||||||
$statusText = $this->status_text;
|
$statusText = $this->status_text;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->setStatus('processing', $statusText, $progress);
|
$this->setStatus('processing', $statusText, $progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setStatusComplete(): void {
|
/**
|
||||||
|
* Set MediaJob status to complete.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setStatusComplete(): void
|
||||||
|
{
|
||||||
$this->setStatus('complete');
|
$this->setStatus('complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setStatusInvalid(): void {
|
/**
|
||||||
$this->setStatus('invalid');
|
* Set MediaJon status to invalid.
|
||||||
|
*
|
||||||
|
* @param string $text The status text.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setStatusInvalid(string $text = ''): void
|
||||||
|
{
|
||||||
|
$this->setStatus('invalid', $text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setStatus(string $status, string $text = '', int $progress = 0): void {
|
/**
|
||||||
|
* Set MediaJob status details.
|
||||||
|
*
|
||||||
|
* @param string $status The status string.
|
||||||
|
* @param string $text The status text.
|
||||||
|
* @param integer $progress The status percentage.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function setStatus(string $status, string $text = '', int $progress = 0): void
|
||||||
|
{
|
||||||
$this->status = $status;
|
$this->status = $status;
|
||||||
$this->status_text = $text;
|
$this->status_text = $text;
|
||||||
$this->progress = $progress;
|
$this->progress = $progress;
|
||||||
$this->save();
|
$this->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the MediaJob.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
public function process(): void
|
public function process(): void
|
||||||
{
|
{
|
||||||
$data = json_decode($this->data, true);
|
$data = json_decode($this->data, true);
|
||||||
if($data !== null) {
|
if ($data !== null) {
|
||||||
if(array_key_exists('chunks', $data) === true) {
|
if (array_key_exists('chunks', $data) === true) {
|
||||||
if(array_key_exists('chunk_count', $data) === false || array_key_exists('name', $data) === false) {
|
if (array_key_exists('chunk_count', $data) === false) {
|
||||||
$this->setStatusInvalid();
|
$this->setStatusInvalid('chunk_count is missing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('name', $data) === false) {
|
||||||
|
$this->setStatusInvalid('name is missing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$numChunks = count($data['chunks']);
|
$numChunks = count($data['chunks']);
|
||||||
$maxChunks = intval($data['chunk_count']);
|
$maxChunks = intval($data['chunk_count']);
|
||||||
if($numChunks >= $maxChunks) {
|
if ($numChunks >= $maxChunks) {
|
||||||
// merge file and dispatch
|
// merge file and dispatch
|
||||||
$percentage = 0;
|
$percentage = 0;
|
||||||
$percentageStep = 100 / $maxChunks;
|
$percentageStep = (100 / $maxChunks);
|
||||||
$this->setStatusProcessing($percentage, 'combining chunks');
|
$this->setStatusProcessing($percentage, 'combining chunks');
|
||||||
|
|
||||||
$newFile = generateTempFilePath(pathinfo($data['name'], PATHINFO_EXTENSION));
|
$newFile = generateTempFilePath(pathinfo($data['name'], PATHINFO_EXTENSION));
|
||||||
$failed = false;
|
$failed = false;
|
||||||
|
|
||||||
for($index = 1; $index <= $maxChunks; $index++) {
|
for ($index = 1; $index <= $maxChunks; $index++) {
|
||||||
if(array_key_exists($index, $data['chunks']) === false) {
|
if (array_key_exists($index, $data['chunks']) === false) {
|
||||||
$failed = true;
|
$failed = `{$index} chunk is missing`;
|
||||||
} else {
|
} else {
|
||||||
$tempFileName = $data['chunks'][$index];
|
$tempFileName = $data['chunks'][$index];
|
||||||
|
|
||||||
if($failed === false) {
|
if ($failed === false) {
|
||||||
$chunkContents = file_get_contents($tempFileName);
|
$chunkContents = file_get_contents($tempFileName);
|
||||||
if($chunkContents === false) {
|
if ($chunkContents === false) {
|
||||||
$failed = true;
|
$failed = `{$index} chunk is empty`;
|
||||||
} else {
|
} else {
|
||||||
file_put_contents($newFile, $chunkContents, FILE_APPEND);
|
file_put_contents($newFile, $chunkContents, FILE_APPEND);
|
||||||
}
|
}
|
||||||
@@ -93,67 +167,29 @@ class MediaJob extends Model
|
|||||||
unset($data['chunks']);
|
unset($data['chunks']);
|
||||||
$this->data = json_encode($data);
|
$this->data = json_encode($data);
|
||||||
|
|
||||||
if($failed === false) {
|
if ($failed !== false) {
|
||||||
$this->setStatusInvalid();
|
$this->setStatusInvalid($failed);
|
||||||
} else {
|
} else {
|
||||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
$mime = finfo_file($finfo, $newFile);
|
$mime = finfo_file($finfo, $newFile);
|
||||||
finfo_close($finfo);
|
finfo_close($finfo);
|
||||||
|
|
||||||
$data['file']['path'] = $newFile;
|
$data['file'] = $newFile;
|
||||||
$data['file']['size'] = filesize($newFile);
|
$data['size'] = filesize($newFile);
|
||||||
$data['file']['mime_type'] = $mime;
|
$data['mime_type'] = $mime;
|
||||||
|
|
||||||
|
$this->data = json_encode($data);
|
||||||
$this->setStatusQueued();
|
$this->setStatusQueued();
|
||||||
MediaWorkerJob::dispatch($this);
|
MediaWorkerJob::dispatch($this)->onQueue('media');
|
||||||
}
|
}
|
||||||
}
|
}//end if
|
||||||
} else if(array_key_exists('file', $data) || array_key_exists('transform', $data)) {
|
} else {
|
||||||
$this->setStatusQueued();
|
$this->setStatusQueued();
|
||||||
MediaWorkerJob::dispatch($this);
|
MediaWorkerJob::dispatch($this)->onQueue('media');
|
||||||
}
|
}//end if
|
||||||
}
|
}//end if
|
||||||
}
|
}
|
||||||
|
|
||||||
// public const INVALID_FILE_ERROR = 1;
|
|
||||||
// public const FILE_SIZE_EXCEEDED_ERROR = 2;
|
|
||||||
// public const FILE_NAME_EXISTS_ERROR = 3;
|
|
||||||
// public const TEMP_FILE_ERROR = 4;
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Set the Media Job to failed
|
|
||||||
// *
|
|
||||||
// * @var string $msg The status message to save.
|
|
||||||
// * @return void
|
|
||||||
// */
|
|
||||||
// public function failed(string $msg = ''): void
|
|
||||||
// {
|
|
||||||
// $data = [];
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// $data = json_decode($this->data, true);
|
|
||||||
// } catch(\Exception $e) {
|
|
||||||
// /* empty */
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if(array_key_exists('chunks', $data) === true) {
|
|
||||||
// foreach($data['chunks'] as $num => $path) {
|
|
||||||
// if(file_exists($path) === true) {
|
|
||||||
// unlink($path);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// unset($data['chunks']);
|
|
||||||
// $this->data = json_encode($data);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// $this->status = 'failed';
|
|
||||||
// $this->status_text = $msg;
|
|
||||||
// $this->progress = 0;
|
|
||||||
// $this->save();
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the job owner
|
* Return the job owner
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ return new class extends Migration
|
|||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('media_jobs', function (Blueprint $table) {
|
Schema::create('media_jobs', function (Blueprint $table) {
|
||||||
$table->uuid()->primary();
|
$table->uuid('id')->primary();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
$table->uuid('user_id');
|
$table->uuid('user_id')->nullable();
|
||||||
$table->uuid('media_id'); // Add a foreign key for the media model
|
$table->uuid('media_id')->nullable(); // Add a foreign key for the media model
|
||||||
$table->string('status');
|
$table->string('status');
|
||||||
$table->string('status_text');
|
$table->string('status_text');
|
||||||
$table->text('data');
|
$table->text('data');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div v-if="value" class="flex flex-justify-center mb-4">
|
<div v-if="value" class="flex flex-justify-center mb-4">
|
||||||
<SMLoading v-if="!imgError && !imgLoaded" class="w-48 h-48" small />
|
<SMLoading v-if="!imgError && !imgLoaded" class="w-48 h-48" small />
|
||||||
<svg
|
<svg
|
||||||
v-if="imgError"
|
v-if="imgError && imgLoaded"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
class="h-48 text-gray">
|
class="h-48 text-gray">
|
||||||
@@ -13,11 +13,11 @@
|
|||||||
fill="currentColor" />
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
<img
|
<img
|
||||||
v-if="!imgError"
|
|
||||||
class="max-w-48 max-h-48 w-full h-full"
|
class="max-w-48 max-h-48 w-full h-full"
|
||||||
@load="imgLoaded = true"
|
@load="handleImageLoaded"
|
||||||
@error="imgError = true"
|
@error="handleImageError"
|
||||||
:src="mediaGetThumbnail(value, 'medium')" />
|
:style="{ display: image == '' ? 'none' : 'block' }"
|
||||||
|
:src="image" />
|
||||||
</div>
|
</div>
|
||||||
<svg
|
<svg
|
||||||
v-else
|
v-else
|
||||||
@@ -38,17 +38,24 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="slots.help"><slot name="help"></slot></template>
|
<template v-if="slots.help"><slot name="help"></slot></template>
|
||||||
|
<input
|
||||||
|
id="file"
|
||||||
|
ref="refUploadInput"
|
||||||
|
type="file"
|
||||||
|
style="display: none"
|
||||||
|
:accept="props.accepts"
|
||||||
|
@change="handleChangeSelectFile" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, watch, ref, useSlots } from "vue";
|
import { inject, watch, ref, useSlots, onMounted } from "vue";
|
||||||
import { isEmpty, generateRandomElementId } from "../helpers/utils";
|
import { isEmpty, generateRandomElementId } from "../helpers/utils";
|
||||||
import { toTitleCase } from "../helpers/string";
|
import { toTitleCase } from "../helpers/string";
|
||||||
import { mediaGetThumbnail } from "../helpers/media";
|
import { mediaGetThumbnail } from "../helpers/media";
|
||||||
import { openDialog } from "./SMDialog";
|
import { openDialog } from "./SMDialog";
|
||||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||||
import SMDialogUpload from "./dialogs/SMDialogUpload.vue";
|
// import SMDialogUpload from "./dialogs/SMDialogUpload.vue";
|
||||||
import { Media } from "../helpers/api.types";
|
import { Media } from "../helpers/api.types";
|
||||||
import SMLoading from "./SMLoading.vue";
|
import SMLoading from "./SMLoading.vue";
|
||||||
|
|
||||||
@@ -168,6 +175,8 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
|
const refUploadInput = ref(null);
|
||||||
|
const image = ref("");
|
||||||
|
|
||||||
const form = inject(props.formId, props.form);
|
const form = inject(props.formId, props.form);
|
||||||
const control =
|
const control =
|
||||||
@@ -232,6 +241,16 @@ watch(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => value.value,
|
||||||
|
(newValue) => {
|
||||||
|
mediaGetThumbnail(newValue, "medium", (e) => {
|
||||||
|
image.value = e;
|
||||||
|
imgLoaded.value = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (typeof control === "object" && control !== null) {
|
if (typeof control === "object" && control !== null) {
|
||||||
watch(
|
watch(
|
||||||
() => control.validation.result.valid,
|
() => control.validation.result.valid,
|
||||||
@@ -269,21 +288,58 @@ const handleMediaSelect = async () => {
|
|||||||
allowUpload: props.allowUpload,
|
allowUpload: props.allowUpload,
|
||||||
accepts: props.accepts,
|
accepts: props.accepts,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
result = await openDialog(SMDialogUpload, {
|
|
||||||
accepts: props.accepts,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const mediaResult = result as Media;
|
const mediaResult = result as Media;
|
||||||
emits("update:modelValue", mediaResult);
|
emits("update:modelValue", mediaResult);
|
||||||
|
if (control) {
|
||||||
|
control.value = mediaResult;
|
||||||
|
feedbackInvalid.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (refUploadInput.value != null) {
|
||||||
|
refUploadInput.value.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeSelectFile = async () => {
|
||||||
|
if (refUploadInput.value != null && refUploadInput.value.files != null) {
|
||||||
|
imgLoaded.value = false;
|
||||||
|
imgError.value = false;
|
||||||
|
|
||||||
|
const fileList = Array.from(refUploadInput.value.files);
|
||||||
|
|
||||||
|
let file = fileList.length > 0 ? fileList[0] : null;
|
||||||
|
|
||||||
|
emits("update:modelValue", file);
|
||||||
if (control) {
|
if (control) {
|
||||||
control.value = mediaResult;
|
control.value = file;
|
||||||
feedbackInvalid.value = "";
|
feedbackInvalid.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
mediaGetThumbnail(value.value, "medium", (e) => {
|
||||||
|
image.value = e;
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleImageLoaded = () => {
|
||||||
|
imgLoaded.value = true;
|
||||||
|
imgError.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageError = () => {
|
||||||
|
if (image.value !== "") {
|
||||||
|
imgLoaded.value = true;
|
||||||
|
imgError.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ interface ApiOptions {
|
|||||||
signal?: AbortSignal | null;
|
signal?: AbortSignal | null;
|
||||||
progress?: ApiProgressCallback;
|
progress?: ApiProgressCallback;
|
||||||
callback?: ApiResultCallback;
|
callback?: ApiResultCallback;
|
||||||
|
chunk?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiResponse {
|
export interface ApiResponse {
|
||||||
@@ -322,7 +323,7 @@ export const api = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apiOptions.method = "POST";
|
apiOptions.method = "POST";
|
||||||
return await this.send(options);
|
return await this.send(apiOptions);
|
||||||
},
|
},
|
||||||
|
|
||||||
put: async function (options: ApiOptions | string): Promise<ApiResponse> {
|
put: async function (options: ApiOptions | string): Promise<ApiResponse> {
|
||||||
@@ -335,7 +336,7 @@ export const api = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apiOptions.method = "PUT";
|
apiOptions.method = "PUT";
|
||||||
return await this.send(options);
|
return await this.send(apiOptions);
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async function (
|
delete: async function (
|
||||||
@@ -350,7 +351,104 @@ export const api = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apiOptions.method = "DELETE";
|
apiOptions.method = "DELETE";
|
||||||
return await this.send(options);
|
return await this.send(apiOptions);
|
||||||
|
},
|
||||||
|
|
||||||
|
chunk: async function (options: ApiOptions | string): Promise<ApiResponse> {
|
||||||
|
let apiOptions = {} as ApiOptions;
|
||||||
|
|
||||||
|
// setup api options
|
||||||
|
if (typeof options == "string") {
|
||||||
|
apiOptions.url = options;
|
||||||
|
} else {
|
||||||
|
apiOptions = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set method to post by default
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(apiOptions, "method")) {
|
||||||
|
apiOptions.method = "POST";
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for chunk option
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(apiOptions, "chunk") &&
|
||||||
|
Object.prototype.hasOwnProperty.call(apiOptions, "body") &&
|
||||||
|
apiOptions.body instanceof FormData
|
||||||
|
) {
|
||||||
|
if (apiOptions.body.has(apiOptions.chunk)) {
|
||||||
|
const file = apiOptions.body.get(apiOptions.chunk);
|
||||||
|
|
||||||
|
if (file instanceof File) {
|
||||||
|
const chunkSize = 50 * 1024 * 1024;
|
||||||
|
let chunk = 0;
|
||||||
|
let chunkCount = 1;
|
||||||
|
let job_id = -1;
|
||||||
|
|
||||||
|
if (file.size > chunkSize) {
|
||||||
|
chunkCount = Math.ceil(file.size / chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = null;
|
||||||
|
for (chunk = 0; chunk < chunkCount; chunk++) {
|
||||||
|
const offset = chunk * chunkSize;
|
||||||
|
const fileChunk = file.slice(
|
||||||
|
offset,
|
||||||
|
offset + chunkSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
const chunkFormData = new FormData();
|
||||||
|
if (job_id == -1) {
|
||||||
|
for (const [field, value] of apiOptions.body) {
|
||||||
|
chunkFormData.append(field, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkFormData.append("name", file.name);
|
||||||
|
chunkFormData.append("size", file.size.toString());
|
||||||
|
chunkFormData.append("mime_type", file.type);
|
||||||
|
} else {
|
||||||
|
chunkFormData.append("job_id", job_id.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkFormData.set(apiOptions.chunk, fileChunk);
|
||||||
|
chunkFormData.append("chunk", (chunk + 1).toString());
|
||||||
|
chunkFormData.append(
|
||||||
|
"chunk_count",
|
||||||
|
chunkCount.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const chunkOptions = {
|
||||||
|
method: apiOptions.method,
|
||||||
|
url: apiOptions.url,
|
||||||
|
params: apiOptions.params || {},
|
||||||
|
body: chunkFormData,
|
||||||
|
headers: apiOptions.headers || {},
|
||||||
|
progress: (progressEvent) => {
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(
|
||||||
|
apiOptions,
|
||||||
|
"progress",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
apiOptions.progress({
|
||||||
|
loaded:
|
||||||
|
chunk * chunkSize +
|
||||||
|
progressEvent.loaded,
|
||||||
|
total: file.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
result = await this.send(chunkOptions);
|
||||||
|
job_id = result.data.media_job.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.send(apiOptions);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,19 @@ export interface MediaCollection {
|
|||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MediaJob {
|
||||||
|
id: string;
|
||||||
|
media_id: string;
|
||||||
|
user_id: string;
|
||||||
|
status: string;
|
||||||
|
status_text: string;
|
||||||
|
progress: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaJobResponse {
|
||||||
|
media_job: MediaJob;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Article {
|
export interface Article {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -39,48 +39,55 @@ export const mimeMatches = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const mediaGetThumbnail = (
|
export const mediaGetThumbnail = (
|
||||||
media: Media,
|
media: Media | File,
|
||||||
useVariant: string | null = "",
|
useVariant: string | null = "",
|
||||||
forceRefresh: boolean = false,
|
callback = null,
|
||||||
): string => {
|
): string => {
|
||||||
let url: string = "";
|
let url: string = "";
|
||||||
|
|
||||||
if (!media) {
|
if (media) {
|
||||||
|
if (media instanceof File) {
|
||||||
|
if (callback != null) {
|
||||||
|
if (mimeMatches("image/*", media.type) == true) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function (e) {
|
||||||
|
callback(e.target.result.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(media);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
useVariant &&
|
||||||
|
useVariant != "" &&
|
||||||
|
useVariant != null &&
|
||||||
|
media.variants &&
|
||||||
|
media.variants[useVariant]
|
||||||
|
) {
|
||||||
|
url = media.url.replace(media.name, media.variants[useVariant]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.thumbnail && media.thumbnail.length > 0) {
|
||||||
|
url = media.thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.variants && media.variants["thumb"]) {
|
||||||
|
url = media.url.replace(media.name, media.variants["thumb"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === "") {
|
||||||
|
url = "/assets/fileicons/unknown.webp";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback != null) {
|
||||||
|
callback(url);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
useVariant &&
|
|
||||||
useVariant != "" &&
|
|
||||||
useVariant != null &&
|
|
||||||
media.variants &&
|
|
||||||
media.variants[useVariant]
|
|
||||||
) {
|
|
||||||
url = media.url.replace(media.name, media.variants[useVariant]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media.thumbnail && media.thumbnail.length > 0) {
|
|
||||||
url = media.thumbnail;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media.variants && media.variants["thumb"]) {
|
|
||||||
url = media.url.replace(media.name, media.variants["thumb"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url === "") {
|
|
||||||
return "/assets/fileicons/unknown.webp";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceRefresh == true) {
|
|
||||||
// Generate a random string
|
|
||||||
const randomString = Math.random().toString(36).substring(7);
|
|
||||||
|
|
||||||
// Check if the URL already has query parameters
|
|
||||||
const separator = url.includes("?") ? "&" : "?";
|
|
||||||
|
|
||||||
// Append the random string as a query parameter
|
|
||||||
url = `${url}${separator}_random=${randomString}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
:title="pageHeading"
|
:title="pageHeading"
|
||||||
:back-link="{ name: 'dashboard-media-list' }"
|
:back-link="{ name: 'dashboard-media-list' }"
|
||||||
back-title="Back to Media" />
|
back-title="Back to Media" />
|
||||||
<SMLoading v-if="form.loading()" />
|
<SMLoading v-if="form.loading()">{{ progressText }}</SMLoading>
|
||||||
<div v-else class="max-w-4xl mx-auto px-4 mt-8">
|
<div v-else class="max-w-4xl mx-auto px-4 mt-8">
|
||||||
<SMForm
|
<SMForm
|
||||||
:model-value="form"
|
:model-value="form"
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
<SMSelectFile
|
<SMSelectFile
|
||||||
v-if="!editMultiple"
|
v-if="!editMultiple"
|
||||||
control="file"
|
control="file"
|
||||||
allow-upload
|
|
||||||
upload-only
|
upload-only
|
||||||
accepts="*"
|
accepts="*"
|
||||||
class="mb-4" />
|
class="mb-4" />
|
||||||
@@ -84,7 +83,11 @@ import { api } from "../../helpers/api";
|
|||||||
import { Form, FormControl } from "../../helpers/form";
|
import { Form, FormControl } from "../../helpers/form";
|
||||||
import { bytesReadable } from "../../helpers/types";
|
import { bytesReadable } from "../../helpers/types";
|
||||||
import { And, Required } from "../../helpers/validate";
|
import { And, Required } from "../../helpers/validate";
|
||||||
import { Media, MediaResponse } from "../../helpers/api.types";
|
import {
|
||||||
|
Media,
|
||||||
|
MediaJobResponse,
|
||||||
|
MediaResponse,
|
||||||
|
} from "../../helpers/api.types";
|
||||||
import { openDialog } from "../../components/SMDialog";
|
import { openDialog } from "../../components/SMDialog";
|
||||||
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||||
import SMForm from "../../components/SMForm.vue";
|
import SMForm from "../../components/SMForm.vue";
|
||||||
@@ -96,6 +99,7 @@ import SMPageStatus from "../../components/SMPageStatus.vue";
|
|||||||
import SMSelectFile from "../../components/SMSelectFile.vue";
|
import SMSelectFile from "../../components/SMSelectFile.vue";
|
||||||
import { userHasPermission } from "../../helpers/utils";
|
import { userHasPermission } from "../../helpers/utils";
|
||||||
import SMImageGallery from "../../components/SMImageGallery.vue";
|
import SMImageGallery from "../../components/SMImageGallery.vue";
|
||||||
|
import { toTitleCase } from "../../helpers/string";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -190,8 +194,10 @@ const handleLoad = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (enableFormCallBack) => {
|
const handleSubmit = async (enableFormCallBack) => {
|
||||||
|
let processing = false;
|
||||||
|
form.loading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
form.loading(true);
|
|
||||||
if (editMultiple === false) {
|
if (editMultiple === false) {
|
||||||
let submitData = new FormData();
|
let submitData = new FormData();
|
||||||
|
|
||||||
@@ -210,8 +216,9 @@ const handleSubmit = async (enableFormCallBack) => {
|
|||||||
form.controls.description.value as string,
|
form.controls.description.value as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let result = null;
|
||||||
if (route.params.id) {
|
if (route.params.id) {
|
||||||
await api.put({
|
result = await api.put({
|
||||||
url: "/media/{id}",
|
url: "/media/{id}",
|
||||||
params: {
|
params: {
|
||||||
id: route.params.id,
|
id: route.params.id,
|
||||||
@@ -226,12 +233,13 @@ const handleSubmit = async (enableFormCallBack) => {
|
|||||||
)}%`),
|
)}%`),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await api.post({
|
result = await api.chunk({
|
||||||
url: "/media",
|
url: "/media",
|
||||||
body: submitData,
|
body: submitData,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
|
chunk: "file",
|
||||||
progress: (progressEvent) =>
|
progress: (progressEvent) =>
|
||||||
(progressText.value = `Uploading File: ${Math.floor(
|
(progressText.value = `Uploading File: ${Math.floor(
|
||||||
(progressEvent.loaded / progressEvent.total) * 100,
|
(progressEvent.loaded / progressEvent.total) * 100,
|
||||||
@@ -239,13 +247,79 @@ const handleSubmit = async (enableFormCallBack) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useToastStore().addToast({
|
const mediaJobId = result.data.media_job.id;
|
||||||
title: route.params.id ? "Media Updated" : "Media Created",
|
const mediaJobUpdate = async () => {
|
||||||
content: route.params.id
|
api.get({
|
||||||
? "The media item has been updated."
|
url: "/media/job/{id}",
|
||||||
: "The media item been created.",
|
params: {
|
||||||
type: "success",
|
id: mediaJobId,
|
||||||
});
|
},
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
const data = result.data as MediaJobResponse;
|
||||||
|
|
||||||
|
// queued
|
||||||
|
// complete
|
||||||
|
// waiting
|
||||||
|
// processing - txt - prog
|
||||||
|
|
||||||
|
// invalid - err
|
||||||
|
// failed - err
|
||||||
|
|
||||||
|
if (data.media_job.status != "complete") {
|
||||||
|
if (data.media_job.status == "queued") {
|
||||||
|
progressText.value = "Queued for processing";
|
||||||
|
} else if (data.media_job.status == "processing") {
|
||||||
|
if (data.media_job.progress != -1) {
|
||||||
|
progressText.value = `${toTitleCase(
|
||||||
|
data.media_job.status_text,
|
||||||
|
)} ${data.media_job.progress}%`;
|
||||||
|
} else {
|
||||||
|
progressText.value = `${toTitleCase(
|
||||||
|
data.media_job.status_text,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
data.media_job.status == "invalid" ||
|
||||||
|
data.media_job.status == "failed"
|
||||||
|
) {
|
||||||
|
useToastStore().addToast({
|
||||||
|
title: "Error Processing Media",
|
||||||
|
content: toTitleCase(
|
||||||
|
data.media_job.status_text,
|
||||||
|
),
|
||||||
|
type: "danger",
|
||||||
|
});
|
||||||
|
|
||||||
|
progressText.value = "";
|
||||||
|
form.loading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setTimeout(mediaJobUpdate, 500);
|
||||||
|
} else {
|
||||||
|
useToastStore().addToast({
|
||||||
|
title: route.params.id
|
||||||
|
? "Media Updated"
|
||||||
|
: "Media Created",
|
||||||
|
content: route.params.id
|
||||||
|
? "The media item has been updated."
|
||||||
|
: "The media item been created.",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
progressText.value = "";
|
||||||
|
form.loading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log("error", e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
processing = true;
|
||||||
|
mediaJobUpdate();
|
||||||
} else {
|
} else {
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
@@ -292,14 +366,16 @@ const handleSubmit = async (enableFormCallBack) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
// const urlParams = new URLSearchParams(window.location.search);
|
||||||
const returnUrl = urlParams.get("return");
|
// const returnUrl = urlParams.get("return");
|
||||||
if (returnUrl) {
|
// if (returnUrl) {
|
||||||
router.push(decodeURIComponent(returnUrl));
|
// router.push(decodeURIComponent(returnUrl));
|
||||||
} else {
|
// } else {
|
||||||
router.push({ name: "dashboard-media-list" });
|
// router.push({ name: "dashboard-media-list" });
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
processing = false;
|
||||||
|
|
||||||
useToastStore().addToast({
|
useToastStore().addToast({
|
||||||
title: "Server error",
|
title: "Server error",
|
||||||
content: "An error occurred saving the media.",
|
content: "An error occurred saving the media.",
|
||||||
@@ -308,8 +384,10 @@ const handleSubmit = async (enableFormCallBack) => {
|
|||||||
|
|
||||||
enableFormCallBack();
|
enableFormCallBack();
|
||||||
} finally {
|
} finally {
|
||||||
progressText.value = "";
|
if (processing == false) {
|
||||||
form.loading(false);
|
progressText.value = "";
|
||||||
|
form.loading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Http\Controllers\Api\EventController;
|
|||||||
use App\Http\Controllers\Api\InfoController;
|
use App\Http\Controllers\Api\InfoController;
|
||||||
use App\Http\Controllers\Api\LogController;
|
use App\Http\Controllers\Api\LogController;
|
||||||
use App\Http\Controllers\Api\MediaController;
|
use App\Http\Controllers\Api\MediaController;
|
||||||
|
use App\Http\Controllers\Api\MediaJobController;
|
||||||
use App\Http\Controllers\Api\OCRController;
|
use App\Http\Controllers\Api\OCRController;
|
||||||
use App\Http\Controllers\Api\ArticleController;
|
use App\Http\Controllers\Api\ArticleController;
|
||||||
use App\Http\Controllers\Api\ShortlinkController;
|
use App\Http\Controllers\Api\ShortlinkController;
|
||||||
@@ -40,6 +41,7 @@ Route::post('/users/resendVerifyEmailCode', [UserController::class, 'resendVerif
|
|||||||
Route::post('/users/verifyEmail', [UserController::class, 'verifyEmail']);
|
Route::post('/users/verifyEmail', [UserController::class, 'verifyEmail']);
|
||||||
Route::get('/users/{user}/events', [UserController::class, 'eventList']);
|
Route::get('/users/{user}/events', [UserController::class, 'eventList']);
|
||||||
|
|
||||||
|
Route::get('media/job/{mediaJob}', [MediaJobController::class, 'show']);
|
||||||
Route::apiResource('media', MediaController::class);
|
Route::apiResource('media', MediaController::class);
|
||||||
Route::get('media/{medium}/download', [MediaController::class, 'download']);
|
Route::get('media/{medium}/download', [MediaController::class, 'download']);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user