updated to media management
This commit is contained in:
@@ -38,3 +38,20 @@ function arrayLimitKeys(array $arr, array $keys): array
|
||||
{
|
||||
return array_intersect_key($arr, array_flip($keys));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array value or default value if it does not exist
|
||||
*
|
||||
* @param string $key The key value to return if exists.
|
||||
* @param array $arr The array to check.
|
||||
* @param mixed $value The value to return if key does not exist.
|
||||
* @return mixed
|
||||
*/
|
||||
function arrayDefaultValue(string $key, array $arr, mixed $value): mixed
|
||||
{
|
||||
if (array_key_exists($key, $arr) === true) {
|
||||
return $arr[$key];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
27
app/Helpers/TypeValue.php
Normal file
27
app/Helpers/TypeValue.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/* Type Value Helper Functions */
|
||||
|
||||
|
||||
/**
|
||||
* Is value true
|
||||
*
|
||||
* @param mixed $value Value to check.
|
||||
* @return boolean
|
||||
*/
|
||||
function isTrue(mixed $value): bool
|
||||
{
|
||||
if (is_bool($value) === true && $value === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_numeric($value) === true && intval($value) === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_string($value) === true && in_array(strtolower($value), ['true', '1'], true) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -6,9 +6,10 @@ use App\Conductors\MediaConductor;
|
||||
use App\Enum\HttpResponseCodes;
|
||||
use App\Http\Requests\MediaRequest;
|
||||
use App\Models\Media;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
class MediaController extends ApiController
|
||||
@@ -67,45 +68,62 @@ class MediaController extends ApiController
|
||||
*/
|
||||
public function store(MediaRequest $request)
|
||||
{
|
||||
if (MediaConductor::creatable() === true) {
|
||||
if (MediaConductor::creatable() === false) {
|
||||
return $this->respondForbidden();
|
||||
}
|
||||
|
||||
$file = $request->file('file');
|
||||
if ($file === null) {
|
||||
return $this->respondWithErrors(['file' => 'The browser did not upload the file correctly to the server.']);
|
||||
}
|
||||
|
||||
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.']);
|
||||
}
|
||||
$jsonResult = $this->validateFileItem($file);
|
||||
if ($jsonResult !== null) {
|
||||
return $jsonResult;
|
||||
}
|
||||
|
||||
if ($file->getSize() > Media::getMaxUploadSize()) {
|
||||
return $this->respondTooLarge();
|
||||
}
|
||||
$request->merge([
|
||||
'title' => $request->get('title', ''),
|
||||
'name' => '',
|
||||
'size' => $file->getSize(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'status' => '',
|
||||
]);
|
||||
|
||||
try {
|
||||
$media = Media::createFromUploadedFile($request, $file);
|
||||
} catch (\Exception $e) {
|
||||
if ($e->getCode() === Media::FILE_SIZE_EXCEEDED_ERROR) {
|
||||
return $this->respondTooLarge();
|
||||
// We store images by default locally
|
||||
if ($request->get('storage') === null) {
|
||||
if (strpos($file->getMimeType(), 'image/') === 0) {
|
||||
$request->merge([
|
||||
'storage' => 'local',
|
||||
]);
|
||||
} else {
|
||||
return $this->respondWithErrors(['file' => $e->getMessage()]);
|
||||
$request->merge([
|
||||
'storage' => 'cdn',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$mediaItem = $request->user()->media()->create($request->except(['file','transform']));
|
||||
|
||||
$temporaryFilePath = generateTempFilePath();
|
||||
copy($file->path(), $temporaryFilePath);
|
||||
|
||||
$transformData = ['file' => [
|
||||
'path' => $temporaryFilePath,
|
||||
'size' => $file->getSize(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
]
|
||||
];
|
||||
if ($request->has('transform') === true) {
|
||||
$transformData = array_merge($transformData, array_map('trim', explode(',', $request->get('transform'))));
|
||||
}
|
||||
|
||||
$mediaItem->transform($transformData);
|
||||
|
||||
return $this->respondAsResource(
|
||||
MediaConductor::model($request, $media),
|
||||
MediaConductor::model($request, $mediaItem),
|
||||
['respondCode' => HttpResponseCodes::HTTP_ACCEPTED]
|
||||
);
|
||||
}//end if
|
||||
|
||||
return $this->respondForbidden();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,43 +135,42 @@ class MediaController extends ApiController
|
||||
*/
|
||||
public function update(MediaRequest $request, Media $medium)
|
||||
{
|
||||
if (MediaConductor::updatable($medium) === true) {
|
||||
if (MediaConductor::updatable($medium) === false) {
|
||||
return $this->respondForbidden();
|
||||
}
|
||||
|
||||
$file = $request->file('file');
|
||||
if ($file !== 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.']);
|
||||
$jsonResult = $this->validateFileItem($file);
|
||||
if ($jsonResult !== null) {
|
||||
return $jsonResult;
|
||||
}
|
||||
}
|
||||
|
||||
if ($file->getSize() > Media::getMaxUploadSize()) {
|
||||
return $this->respondTooLarge();
|
||||
}
|
||||
}
|
||||
|
||||
$medium->update($request->all());
|
||||
$medium->update($request->except(['file','transform']));
|
||||
|
||||
$transformData = [];
|
||||
if ($file !== null) {
|
||||
try {
|
||||
$medium->updateWithUploadedFile($file);
|
||||
} catch (\Exception $e) {
|
||||
return $this->respondWithErrors(
|
||||
['file' => $e->getMessage()],
|
||||
HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR
|
||||
);
|
||||
$temporaryFilePath = generateTempFilePath();
|
||||
copy($file->path(), $temporaryFilePath);
|
||||
|
||||
$transformData = array_merge($transformData, ['file' => [
|
||||
'path' => $temporaryFilePath,
|
||||
'size' => $file->getSize(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($request->has('transform') === true) {
|
||||
$transformData = array_merge($transformData, array_map('trim', explode(',', $request->get('transform'))));
|
||||
}
|
||||
|
||||
if (count($transformData) > 0) {
|
||||
$medium->transform($transformData);
|
||||
}
|
||||
|
||||
return $this->respondAsResource(MediaConductor::model($request, $medium));
|
||||
}//end if
|
||||
|
||||
return $this->respondForbidden();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,4 +268,32 @@ class MediaController extends ApiController
|
||||
|
||||
return response()->file($path, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a File item in a request is valid
|
||||
*
|
||||
* @param UploadedFile $file The file to validate.
|
||||
* @param string $errorKey The error key to use.
|
||||
* @return JsonResponse|null
|
||||
*/
|
||||
private function validateFileItem(UploadedFile $file, string $errorKey = 'file'): JsonResponse|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([$errorKey => 'The file upload was interrupted.']);
|
||||
default:
|
||||
return $this->respondWithErrors([$errorKey => 'An error occurred uploading the file to the server.']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($file->getSize() > Media::getMaxUploadSize()) {
|
||||
return $this->respondTooLarge();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
224
app/Jobs/MediaJob.php
Normal file
224
app/Jobs/MediaJob.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Media;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use FFMpeg;
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use Intervention\Image\Facades\Image;
|
||||
|
||||
class MediaJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Media item
|
||||
*
|
||||
* @var Media
|
||||
*/
|
||||
protected $media;
|
||||
|
||||
/**
|
||||
* Actions to make on the Media
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $actions;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param Media $media The media model.
|
||||
* @param array $actions The media actions to make.
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Media $media, array $actions)
|
||||
{
|
||||
$this->media = $media;
|
||||
$this->actions = $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
// FILE
|
||||
if (array_key_exists("file", $this->actions) === true) {
|
||||
$uploadData = $this->actions["file"];
|
||||
|
||||
if (array_key_exists("path", $uploadData) === false || file_exists($uploadData["path"]) === false) {
|
||||
$this->media->error("Upload file does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
$filePath = $uploadData["path"];
|
||||
|
||||
// convert HEIC files to JPG
|
||||
$fileExtension = File::extension($filePath);
|
||||
if ($fileExtension === 'heic') {
|
||||
// Get the path without the file name
|
||||
$uploadedFileDirectory = dirname($filePath);
|
||||
|
||||
// Convert the HEIC file to JPG
|
||||
$jpgFileName = pathinfo($filePath, PATHINFO_FILENAME) . '.jpg';
|
||||
$jpgFilePath = $uploadedFileDirectory . '/' . $jpgFileName;
|
||||
if (file_exists($jpgFilePath) === true) {
|
||||
$this->media->error("File already exists in storage");
|
||||
return;
|
||||
}
|
||||
|
||||
Image::make($filePath)->save($jpgFilePath);
|
||||
|
||||
// Update the uploaded file path and file name
|
||||
unlink($filePath);
|
||||
$filePath = $jpgFilePath;
|
||||
$this->media->name = $jpgFileName;
|
||||
$this->media->save();
|
||||
}
|
||||
|
||||
// Check if file already exists
|
||||
if (Storage::disk($this->media->storage)->exists($this->media->name) === true) {
|
||||
if (array_key_exists('replace', $uploadData) === false || isTrue($uploadData['replace']) === false) {
|
||||
$errorStr = "cannot upload file {$this->media->storage} " . // phpcs:ignore
|
||||
"/ {$this->media->name} as it already exists";
|
||||
Log::info($errorStr);
|
||||
throw new \Exception($errorStr);
|
||||
}
|
||||
}
|
||||
|
||||
$this->media->setStagingFile($filePath);
|
||||
}//end if
|
||||
|
||||
$this->media->createStagingFile();
|
||||
$this->media->deleteFile();
|
||||
|
||||
// Modifications
|
||||
if (strpos($this->media->mime_type, 'image/') === 0) {
|
||||
$image = Image::make($filePath);
|
||||
|
||||
// ROTATE
|
||||
if (array_key_exists("rotate", $this->actions) === true) {
|
||||
$rotate = intval($this->actions["rotate"]);
|
||||
if ($rotate !== 0) {
|
||||
$image = $image->rotate($rotate);
|
||||
}
|
||||
}
|
||||
|
||||
// FLIP-H/V
|
||||
if (array_key_exists('flip', $this->actions) === true) {
|
||||
if (stripos($this->actions['flip'], 'h') !== false) {
|
||||
$image = $image->flip('h');
|
||||
}
|
||||
|
||||
if (stripos($this->actions['flip'], 'v') !== false) {
|
||||
$image = $image->flip('v');
|
||||
}
|
||||
}
|
||||
|
||||
// CROP
|
||||
if (array_key_exists("crop", $this->actions) === true) {
|
||||
$cropData = $this->actions["crop"];
|
||||
$width = intval(arrayDefaultValue("width", $cropData, $image->getWidth()));
|
||||
$height = intval(arrayDefaultValue("height", $cropData, $image->getHeight()));
|
||||
$x = intval(arrayDefaultValue("x", $cropData, 0));
|
||||
$y = intval(arrayDefaultValue("y", $cropData, 0));
|
||||
|
||||
$image = $image->crop($width, $height, $x, $y);
|
||||
}//end if
|
||||
|
||||
$image->save($filePath);
|
||||
} elseif (strpos($this->media->mime_type, 'video/') === 0) {
|
||||
$ffmpeg = FFMpeg\FFMpeg::create();
|
||||
$video = $ffmpeg->open($this->media->getStagingFilePath());
|
||||
|
||||
/** @var FFMpeg\Media\Video::filters */
|
||||
$filters = $video->filters();
|
||||
|
||||
// ROTATE
|
||||
if (array_key_exists("rotate", $this->actions) === true) {
|
||||
$rotate = intval($this->actions["rotate"]);
|
||||
$rotate = (($rotate % 360 + 360) % 360); // remove excess rotations
|
||||
$rotate = (round($rotate / 90) * 90); // round to nearest 90%
|
||||
|
||||
if ($rotate > 0) {
|
||||
if ($rotate === 90) {
|
||||
$filters->rotate(FFMpeg\Filters\Video\RotateFilter::ROTATE_90);
|
||||
} elseif ($rotate === 190) {
|
||||
$filters->rotate(FFMpeg\Filters\Video\RotateFilter::ROTATE_180);
|
||||
} elseif ($rotate === 270) {
|
||||
$filters->rotate(FFMpeg\Filters\Video\RotateFilter::ROTATE_270);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FLIP-H/V
|
||||
if (array_key_exists('flip', $this->actions) === true) {
|
||||
if (stripos($this->actions['flip'], 'h') !== false) {
|
||||
$filters->hflip()->synchronize();
|
||||
}
|
||||
|
||||
if (stripos($this->actions['flip'], 'v') !== false) {
|
||||
$filters->vflip()->synchronize();
|
||||
}
|
||||
}
|
||||
|
||||
// CROP
|
||||
if (array_key_exists("crop", $this->actions) === true) {
|
||||
$cropData = $this->actions["crop"];
|
||||
$videoStream = $video->getStreams()->videos()->first();
|
||||
|
||||
$width = intval(arrayDefaultValue("width", $cropData, $videoStream->get('width')));
|
||||
$height = intval(arrayDefaultValue("height", $cropData, $videoStream->get('height')));
|
||||
$x = intval(arrayDefaultValue("x", $cropData, 0));
|
||||
$y = intval(arrayDefaultValue("y", $cropData, 0));
|
||||
|
||||
$cropDimension = new Dimension($width, $height);
|
||||
|
||||
$filters->crop($cropDimension, $x, $y)->synchronize();
|
||||
}//end if
|
||||
|
||||
$tempFilePath = tempnam(sys_get_temp_dir(), 'video-');
|
||||
$video->save(null, $tempFilePath);
|
||||
$this->media->changeStagingFile($tempFilePath);
|
||||
}//end if
|
||||
|
||||
// Move file
|
||||
if (array_key_exists("move", $this->actions) === true) {
|
||||
if (array_key_exists("storage", $this->actions["move"]) === true) {
|
||||
$newStorage = $this->actions["move"]["storage"];
|
||||
if ($this->media->storage !== $newStorage) {
|
||||
if (Storage::has($newStorage) === true) {
|
||||
$this->media->storage = $newStorage;
|
||||
} else {
|
||||
$this->media->error("Cannot move file to '{$newStorage}' as it does not exist");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finish media object
|
||||
$this->media->saveStagingFile();
|
||||
$this->media->ok();
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
$this->media->error("Failed");
|
||||
$this->fail($e);
|
||||
}//end try
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,8 @@ class MoveMediaJob implements ShouldQueue
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
|
||||
@@ -48,6 +48,13 @@ class StoreUploadedFileJob implements ShouldQueue
|
||||
*/
|
||||
protected $replaceExisting;
|
||||
|
||||
/**
|
||||
* Modifications to make on the Media
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $modifications;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
@@ -55,13 +62,15 @@ class StoreUploadedFileJob implements ShouldQueue
|
||||
* @param Media $media The media model.
|
||||
* @param string $filePath The uploaded file.
|
||||
* @param boolean $replaceExisting Replace existing files.
|
||||
* @param array $modifications The modifications to make on the media.
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Media $media, string $filePath, bool $replaceExisting = true)
|
||||
public function __construct(Media $media, string $filePath, bool $replaceExisting = true, array $modifications = [])
|
||||
{
|
||||
$this->media = $media;
|
||||
$this->uploadedFilePath = $filePath;
|
||||
$this->replaceExisting = $replaceExisting;
|
||||
$this->modifications = $modifications;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enum\HttpResponseCodes;
|
||||
use App\Jobs\MediaJob;
|
||||
use App\Jobs\MoveMediaJob;
|
||||
use App\Jobs\StoreUploadedFileJob;
|
||||
use App\Traits\Uuids;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\InvalidCastException;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
@@ -18,7 +21,13 @@ use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\Exception\NotSupportedException;
|
||||
use Intervention\Image\Exception\NotWritableException;
|
||||
use ImagickException;
|
||||
use Intervention\Image\Facades\Image;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
@@ -82,13 +91,12 @@ class Media extends Model
|
||||
protected static $storageFileListCache = [];
|
||||
|
||||
/**
|
||||
* The variant types.
|
||||
* Object variant details.
|
||||
*
|
||||
* @var int[][][]
|
||||
*/
|
||||
protected static $variantTypes = [
|
||||
protected static $objectVariants = [
|
||||
'image' => [
|
||||
'thumb' => ['width' => 150, 'height' => 150],
|
||||
'small' => ['width' => 300, 'height' => 225],
|
||||
'medium' => ['width' => 768, 'height' => 576],
|
||||
'large' => ['width' => 1024, 'height' => 768],
|
||||
@@ -98,6 +106,13 @@ class Media extends Model
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Staging file path of asset for processing.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $stagingFilePath = "";
|
||||
|
||||
|
||||
/**
|
||||
* Model Boot
|
||||
@@ -138,15 +153,15 @@ class Media extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type Variants.
|
||||
* Get Object Variants.
|
||||
*
|
||||
* @param string $type The variant type to get.
|
||||
* @param string $type The variant object to get.
|
||||
* @return array The variant data.
|
||||
*/
|
||||
public static function getTypeVariants(string $type): array
|
||||
public static function getObjectVariants(string $type): array
|
||||
{
|
||||
if (isset(self::$variantTypes[$type]) === true) {
|
||||
return self::$variantTypes[$type];
|
||||
if (isset(self::$objectVariants[$type]) === true) {
|
||||
return self::$objectVariants[$type];
|
||||
}
|
||||
|
||||
return [];
|
||||
@@ -191,11 +206,11 @@ class Media extends Model
|
||||
*/
|
||||
public function getPreviousVariant(string $type, string $variant): string
|
||||
{
|
||||
if (isset(self::$variantTypes[$type]) === false) {
|
||||
if (isset(self::$objectVariants[$type]) === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$variants = self::$variantTypes[$type];
|
||||
$variants = self::$objectVariants[$type];
|
||||
$keys = array_keys($variants);
|
||||
|
||||
$currentIndex = array_search($variant, $keys);
|
||||
@@ -215,11 +230,11 @@ class Media extends Model
|
||||
*/
|
||||
public function getNextVariant(string $type, string $variant): string
|
||||
{
|
||||
if (isset(self::$variantTypes[$type]) === false) {
|
||||
if (isset(self::$objectVariants[$type]) === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$variants = self::$variantTypes[$type];
|
||||
$variants = self::$objectVariants[$type];
|
||||
$keys = array_keys($variants);
|
||||
|
||||
$currentIndex = array_search($variant, $keys);
|
||||
@@ -265,18 +280,12 @@ class Media extends Model
|
||||
*/
|
||||
public function deleteFile(): void
|
||||
{
|
||||
$fileName = $this->name;
|
||||
$baseName = pathinfo($fileName, PATHINFO_FILENAME);
|
||||
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
|
||||
|
||||
$files = Storage::disk($this->storage)->files();
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (preg_match("/{$baseName}(-[a-zA-Z0-9]+)?\.{$extension}/", $file) === 1) {
|
||||
Storage::disk($this->storage)->delete($file);
|
||||
}
|
||||
if (strlen($this->storage) > 0 && strlen($this->name) > 0 && Storage::disk($this->storage)->exists($this->name) === true) {
|
||||
Storage::disk($this->storage)->delete($this->name);
|
||||
}
|
||||
|
||||
$this->deleteThumbnail();
|
||||
$this->deleteVariants();
|
||||
$this->invalidateCFCache();
|
||||
}
|
||||
|
||||
@@ -363,96 +372,37 @@ class Media extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new Media from UploadedFile data.
|
||||
* Transform the media through the Media Job Queue
|
||||
*
|
||||
* @param App\Models\Request $request The request data.
|
||||
* @param Illuminate\Http\UploadedFile $file The file.
|
||||
* @return null|Media The result or null if not successful.
|
||||
* @param array $transform The transform data.
|
||||
* @return void
|
||||
*/
|
||||
public static function createFromUploadedFile(Request $request, UploadedFile $file): ?Media
|
||||
public function transform(array $transform): void
|
||||
{
|
||||
$request->merge([
|
||||
'title' => $request->get('title', ''),
|
||||
'name' => '',
|
||||
'size' => 0,
|
||||
'mime_type' => '',
|
||||
'status' => '',
|
||||
]);
|
||||
|
||||
if ($request->get('storage') === null) {
|
||||
// We store images by default locally
|
||||
if (strpos($file->getMimeType(), 'image/') === 0) {
|
||||
$request->merge([
|
||||
'storage' => 'local',
|
||||
]);
|
||||
} else {
|
||||
$request->merge([
|
||||
'storage' => 'cdn',
|
||||
]);
|
||||
foreach ($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]];
|
||||
}
|
||||
}
|
||||
|
||||
$mediaItem = $request->user()->media()->create($request->all());
|
||||
$mediaItem->updateWithUploadedFile($file);
|
||||
|
||||
return $mediaItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Media with UploadedFile data.
|
||||
*
|
||||
* @param Illuminate\Http\UploadedFile $file The file.
|
||||
* @return null|Media The media item.
|
||||
*/
|
||||
public function updateWithUploadedFile(UploadedFile $file): ?Media
|
||||
{
|
||||
if ($file === null || $file->isValid() !== true) {
|
||||
throw new \Exception('The file is invalid.', self::INVALID_FILE_ERROR);
|
||||
}
|
||||
|
||||
if ($file->getSize() > static::getMaxUploadSize()) {
|
||||
throw new \Exception('The file size is larger then permitted.', self::FILE_SIZE_EXCEEDED_ERROR);
|
||||
}
|
||||
|
||||
$name = static::generateUniqueFileName($file->getClientOriginalName());
|
||||
if ($name === false) {
|
||||
throw new \Exception('The file name already exists in storage.', self::FILE_NAME_EXISTS_ERROR);
|
||||
}
|
||||
|
||||
// remove file if there is an existing entry in this medium item
|
||||
if (strlen($this->name) > 0 && strlen($this->storage) > 0) {
|
||||
Storage::disk($this->storage)->delete($this->name);
|
||||
foreach ($this->variants as $variantName => $fileName) {
|
||||
Storage::disk($this->storage)->delete($fileName);
|
||||
}
|
||||
|
||||
$this->name = '';
|
||||
$this->variants = [];
|
||||
}
|
||||
|
||||
if (strlen($this->title) === 0) {
|
||||
$this->title = $name;
|
||||
}
|
||||
|
||||
$this->name = $name;
|
||||
$this->size = $file->getSize();
|
||||
$this->mime_type = $file->getMimeType();
|
||||
$this->status = 'Processing media';
|
||||
$this->save();
|
||||
|
||||
$temporaryFilePath = generateTempFilePath();
|
||||
copy($file->path(), $temporaryFilePath);
|
||||
|
||||
try {
|
||||
StoreUploadedFileJob::dispatch($this, $temporaryFilePath)->onQueue('media');
|
||||
MediaJob::dispatch($this, $transform)->onQueue('media');
|
||||
} catch (\Exception $e) {
|
||||
$this->status = 'Error';
|
||||
$this->save();
|
||||
|
||||
$this->error('Failed to transform media');
|
||||
throw $e;
|
||||
}//end try
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -625,7 +575,7 @@ class Media extends Model
|
||||
*/
|
||||
public static function fileNameHasSuffix(string $fileName): bool
|
||||
{
|
||||
$suffix = '/(-\d+x\d+|-scaled)$/i';
|
||||
$suffix = '/(-\d+x\d+|-scaled|-thumb)$/i';
|
||||
$fileNameWithoutExtension = pathinfo($fileName, PATHINFO_FILENAME);
|
||||
|
||||
return preg_match($suffix, $fileNameWithoutExtension) === 1;
|
||||
@@ -700,12 +650,49 @@ class Media extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Download temporary copy of the storage file.
|
||||
* Get the Staging File path.
|
||||
*
|
||||
* @return string File path
|
||||
* @param boolean $create Create staging file if doesn't exist.
|
||||
* @return string
|
||||
*/
|
||||
private function downloadTempFile(): string
|
||||
public function getStagingFilePath(bool $create = true): string
|
||||
{
|
||||
if ($this->stagingFilePath === "" && $create === true) {
|
||||
$this->createStagingFile();
|
||||
}
|
||||
|
||||
return $this->stagingFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Staging File for processing.
|
||||
*
|
||||
* @param string $path The path if the new staging file.
|
||||
* @param boolean $overwrite Overwrite existing file.
|
||||
* @return void
|
||||
*/
|
||||
public function setStagingFile(string $path, bool $overwrite = false): void
|
||||
{
|
||||
if ($this->stagingFilePath !== "") {
|
||||
if ($overwrite === true) {
|
||||
unlink($this->stagingFilePath);
|
||||
} else {
|
||||
// ignore request
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->stagingFilePath = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download temporary copy of the storage file for staging.
|
||||
*
|
||||
* @return boolean If download was successful.
|
||||
*/
|
||||
public function createStagingFile(): bool
|
||||
{
|
||||
if ($this->stagingFilePath !== "") {
|
||||
$readStream = Storage::disk($this->storageDisk)->readStream($this->name);
|
||||
$filePath = tempnam(sys_get_temp_dir(), 'download-');
|
||||
$writeStream = fopen($filePath, 'w');
|
||||
@@ -715,16 +702,72 @@ class Media extends Model
|
||||
fclose($readStream);
|
||||
fclose($writeStream);
|
||||
|
||||
return $filePath;
|
||||
$this->stagingFilePath = $filePath;
|
||||
}
|
||||
|
||||
return $this->stagingFilePath !== "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the Staging File to storage
|
||||
*
|
||||
* @param boolean $delete Delete the existing staging file.
|
||||
* @return void
|
||||
*/
|
||||
public function saveStagingFile(bool $delete = true): void
|
||||
{
|
||||
if (strlen($this->storage) > 0 && strlen($this->name) > 0) {
|
||||
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);
|
||||
$fileSystem->putFileAs('/', $this->stagingFilePath, $this->name);
|
||||
}
|
||||
|
||||
$this->generateThumbnail();
|
||||
$this->generateVariants();
|
||||
|
||||
if ($delete === true) {
|
||||
$this->deleteStagingFile();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temporary file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteStagingFile(): void
|
||||
{
|
||||
if ($this->stagingFilePath !== "") {
|
||||
unlink($this->stagingFilePath);
|
||||
$this->stagingFilePath = "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change staging file, removing the old file if present
|
||||
*
|
||||
* @param string $newFile The new staging file.
|
||||
* @return void
|
||||
*/
|
||||
public function changeStagingFile(string $newFile): void
|
||||
{
|
||||
if ($this->stagingFilePath !== "") {
|
||||
unlink($this->stagingFilePath);
|
||||
}
|
||||
|
||||
$this->stagingFilePath = $newFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a Thumbnail for this media.
|
||||
* @param string $uploadedFilePath The local file, if present (else download from storage).
|
||||
*
|
||||
* @return boolean If generation was successful.
|
||||
*/
|
||||
public function generateThumbnail(string $uploadedFilePath = ""): bool
|
||||
public function generateThumbnail(): bool
|
||||
{
|
||||
$thumbnailWidth = 200;
|
||||
$thumbnailHeight = 200;
|
||||
@@ -737,11 +780,7 @@ class Media extends Model
|
||||
}
|
||||
}
|
||||
|
||||
// download original from CDN if no local file
|
||||
$filePath = $uploadedFilePath;
|
||||
if ($uploadedFilePath === "") {
|
||||
$filePath = $this->downloadTempFile();
|
||||
}
|
||||
$filePath = $this->createStagingFile();
|
||||
|
||||
$fileExtension = File::extension($this->name);
|
||||
$tempImagePath = tempnam(sys_get_temp_dir(), 'thumb');
|
||||
@@ -832,22 +871,35 @@ class Media extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate variants for this media.
|
||||
* @param string $uploadedFilePath The local file, if present (else download from storage).
|
||||
* Delete Media Thumbnail from storage.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function generateVariants(string $uploadedFilePath = ""): void
|
||||
public function deleteThumbnail(): void
|
||||
{
|
||||
if (strlen($this->thumbnail) > 0) {
|
||||
$path = substr($this->thumbnail, strlen($this->getUrlPath()));
|
||||
|
||||
if (strlen($path) > 0 && Storage::disk($this->storageDisk)->exists($path) === true) {
|
||||
Storage::disk($this->storageDisk)->delete($path);
|
||||
$this->thumbnail = ''; // Clear the thumbnail property
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate variants for this media.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function generateVariants(): void
|
||||
{
|
||||
if (strpos($this->media->mime_type, 'image/') === 0) {
|
||||
// Generate additional image sizes
|
||||
$sizes = Media::getTypeVariants('image');
|
||||
$sizes = Media::getObjectVariants('image');
|
||||
|
||||
// download original from CDN if no local file
|
||||
$filePath = $uploadedFilePath;
|
||||
if ($uploadedFilePath === "") {
|
||||
$filePath = $this->downloadTempFile();
|
||||
}
|
||||
$filePath = $this->createStagingFile();
|
||||
|
||||
// delete existing variants
|
||||
if (is_array($this->variants) === true) {
|
||||
@@ -924,4 +976,53 @@ class Media extends Model
|
||||
$this->variants = $variants;
|
||||
}//end if
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the Media variants from storage.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteVariants(): void
|
||||
{
|
||||
if (strlen($this->name) > 0 && strlen($this->storage) > 0) {
|
||||
foreach ($this->variants as $variantName => $fileName) {
|
||||
Storage::disk($this->storage)->delete($fileName);
|
||||
}
|
||||
|
||||
$this->variants = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Media status to OK
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ok(): void
|
||||
{
|
||||
$this->status = "OK";
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Media status to an error
|
||||
* @param string $error The error to set.
|
||||
* @return void
|
||||
*/
|
||||
public function error(string $error = ""): void
|
||||
{
|
||||
$this->status = "Error" . ($error !== "" ? ": {$error}" : "");
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Media status
|
||||
* @param string $status The status to set.
|
||||
* @return void
|
||||
*/
|
||||
public function status(string $status = ""): void
|
||||
{
|
||||
$this->status = "Info: " . $status;
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"autoload": {
|
||||
"files": [
|
||||
"app/Helpers/Array.php",
|
||||
"app/Helpers/Temp.php"
|
||||
"app/Helpers/Temp.php",
|
||||
"app/Helpers/TypeValue.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
<img
|
||||
:src="mediaGetThumbnail(lastSelected)"
|
||||
class="max-h-20 max-w-20 mr-2" />
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col w-100">
|
||||
<p class="m-0 text-bold">
|
||||
{{ lastSelected.title }}
|
||||
</p>
|
||||
@@ -260,13 +260,49 @@
|
||||
</p>
|
||||
<p
|
||||
v-if="allowEditSelected"
|
||||
class="m-0 italic text-red-6 small cursor-pointer hover:underline">
|
||||
<span
|
||||
class="flex gap-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 cursor-pointer text-gray-6 hover:text-gray-4"
|
||||
viewBox="0 0 24 24"
|
||||
@click="
|
||||
handleRotateLeft(
|
||||
lastSelected,
|
||||
)
|
||||
">
|
||||
<title>Rotate Left</title>
|
||||
<path
|
||||
d="M4 11C4 6.58 7.58 3 12 3L13 3.06V5.08L12 5C8.69 5 6 7.69 6 11H9L5 15L1 11H4M17 7H13C11.9 7 11 7.9 11 9V18C11 19.11 11.9 20 13 20H19C20.11 20 21 19.11 21 18V11L17 7M19 18H13V9H16V12H19V18Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 cursor-pointer text-gray-6 hover:text-gray-4"
|
||||
viewBox="0 0 24 24"
|
||||
@click="
|
||||
handleRotateRight(
|
||||
lastSelected,
|
||||
)
|
||||
">
|
||||
<title>Rotate Right</title>
|
||||
<path
|
||||
d="M20 11H23L19 15L15 11H18C18 7.69 15.31 5 12 5L11 5.08V3.06L12 3C16.42 3 20 6.58 20 11M9 7H5C3.9 7 3 7.9 3 9V18C3 19.11 3.9 20 5 20H11C12.11 20 13 19.11 13 18V11L9 7M11 18H5V9H8V12H11V18Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 cursor-pointer text-red-6 hover:text-red-4 ml-auto"
|
||||
viewBox="0 0 24 24"
|
||||
@click="
|
||||
handleDelete(lastSelected)
|
||||
"
|
||||
>Delete Permanently</span
|
||||
>
|
||||
">
|
||||
<title>
|
||||
Delete Permanently
|
||||
</title>
|
||||
<path
|
||||
d="M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1133,6 +1169,60 @@ const handleUpdate = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRotateLeft = async (item: Media) => {
|
||||
api.put({
|
||||
url: "/media/{id}",
|
||||
params: {
|
||||
id: item.id,
|
||||
},
|
||||
body: {
|
||||
transform: "rotate-270",
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data) {
|
||||
const data = result.data as MediaResponse;
|
||||
const index = mediaItems.value.findIndex(
|
||||
(mediaItem) => mediaItem.id === item.id,
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
mediaItems.value[index] = data.medium;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
};
|
||||
|
||||
const handleRotateRight = async (item: Media) => {
|
||||
api.put({
|
||||
url: "/media/{id}",
|
||||
params: {
|
||||
id: item.id,
|
||||
},
|
||||
body: {
|
||||
transform: "rotate-90",
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data) {
|
||||
const data = result.data as MediaResponse;
|
||||
const index = mediaItems.value.findIndex(
|
||||
(mediaItem) => mediaItem.id === item.id,
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
mediaItems.value[index] = data.medium;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (item: Media) => {
|
||||
let result = await openDialog(SMDialogConfirm, {
|
||||
title: "Delete File?",
|
||||
|
||||
Reference in New Issue
Block a user