updated to media management

This commit is contained in:
2023-08-24 22:14:43 +10:00
parent 60a15c2227
commit d7529cef80
9 changed files with 718 additions and 202 deletions

View File

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

View File

@@ -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) {
$file = $request->file('file');
if ($file === null) {
return $this->respondWithErrors(['file' => 'The browser did not upload the file correctly to the server.']);
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.']);
}
$jsonResult = $this->validateFileItem($file);
if ($jsonResult !== null) {
return $jsonResult;
}
$request->merge([
'title' => $request->get('title', ''),
'name' => '',
'size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
'status' => '',
]);
// We store images by default locally
if ($request->get('storage') === null) {
if (strpos($file->getMimeType(), 'image/') === 0) {
$request->merge([
'storage' => 'local',
]);
} else {
$request->merge([
'storage' => 'cdn',
]);
}
}
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.']);
}
}
$mediaItem = $request->user()->media()->create($request->except(['file','transform']));
if ($file->getSize() > Media::getMaxUploadSize()) {
return $this->respondTooLarge();
}
$temporaryFilePath = generateTempFilePath();
copy($file->path(), $temporaryFilePath);
try {
$media = Media::createFromUploadedFile($request, $file);
} catch (\Exception $e) {
if ($e->getCode() === Media::FILE_SIZE_EXCEEDED_ERROR) {
return $this->respondTooLarge();
} else {
return $this->respondWithErrors(['file' => $e->getMessage()]);
}
}
$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'))));
}
return $this->respondAsResource(
MediaConductor::model($request, $media),
['respondCode' => HttpResponseCodes::HTTP_ACCEPTED]
);
}//end if
$mediaItem->transform($transformData);
return $this->respondForbidden();
return $this->respondAsResource(
MediaConductor::model($request, $mediaItem),
['respondCode' => HttpResponseCodes::HTTP_ACCEPTED]
);
}
/**
@@ -117,43 +135,42 @@ class MediaController extends ApiController
*/
public function update(MediaRequest $request, Media $medium)
{
if (MediaConductor::updatable($medium) === true) {
$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.']);
}
}
if (MediaConductor::updatable($medium) === false) {
return $this->respondForbidden();
}
if ($file->getSize() > Media::getMaxUploadSize()) {
return $this->respondTooLarge();
}
$file = $request->file('file');
if ($file !== null) {
$jsonResult = $this->validateFileItem($file);
if ($jsonResult !== null) {
return $jsonResult;
}
}
$medium->update($request->all());
$medium->update($request->except(['file','transform']));
if ($file !== null) {
try {
$medium->updateWithUploadedFile($file);
} catch (\Exception $e) {
return $this->respondWithErrors(
['file' => $e->getMessage()],
HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR
);
}
}
$transformData = [];
if ($file !== null) {
$temporaryFilePath = generateTempFilePath();
copy($file->path(), $temporaryFilePath);
return $this->respondAsResource(MediaConductor::model($request, $medium));
}//end if
$transformData = array_merge($transformData, ['file' => [
'path' => $temporaryFilePath,
'size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
]
]);
}
return $this->respondForbidden();
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));
}
/**
@@ -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
View 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
}
}

View File

@@ -48,6 +48,8 @@ class MoveMediaJob implements ShouldQueue
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{

View File

@@ -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;
}
/**

View File

@@ -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,31 +650,124 @@ 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
{
$readStream = Storage::disk($this->storageDisk)->readStream($this->name);
$filePath = tempnam(sys_get_temp_dir(), 'download-');
$writeStream = fopen($filePath, 'w');
while (feof($readStream) !== true) {
fwrite($writeStream, fread($readStream, 8192));
if ($this->stagingFilePath === "" && $create === true) {
$this->createStagingFile();
}
fclose($readStream);
fclose($writeStream);
return $filePath;
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');
while (feof($readStream) !== true) {
fwrite($writeStream, fread($readStream, 8192));
}
fclose($readStream);
fclose($writeStream);
$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();
}
}

View File

@@ -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/",

View File

@@ -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?",