From d7529cef80503b72a254957cec252ff8e2c28ef7 Mon Sep 17 00:00:00 2001 From: James Collins Date: Thu, 24 Aug 2023 22:14:43 +1000 Subject: [PATCH] updated to media management --- app/Helpers/Array.php | 17 + app/Helpers/TypeValue.php | 27 ++ app/Http/Controllers/Api/MediaController.php | 175 +++++---- app/Jobs/MediaJob.php | 224 +++++++++++ app/Jobs/MoveMediaJob.php | 2 + app/Jobs/StoreUploadedFileJob.php | 11 +- app/Models/Media.php | 359 +++++++++++------- composer.json | 3 +- .../js/components/dialogs/SMDialogMedia.vue | 102 ++++- 9 files changed, 718 insertions(+), 202 deletions(-) create mode 100644 app/Helpers/TypeValue.php create mode 100644 app/Jobs/MediaJob.php diff --git a/app/Helpers/Array.php b/app/Helpers/Array.php index 17a169b..5c7ac8c 100644 --- a/app/Helpers/Array.php +++ b/app/Helpers/Array.php @@ -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; +} diff --git a/app/Helpers/TypeValue.php b/app/Helpers/TypeValue.php new file mode 100644 index 0000000..fac3573 --- /dev/null +++ b/app/Helpers/TypeValue.php @@ -0,0 +1,27 @@ +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; + } } diff --git a/app/Jobs/MediaJob.php b/app/Jobs/MediaJob.php new file mode 100644 index 0000000..c3e2453 --- /dev/null +++ b/app/Jobs/MediaJob.php @@ -0,0 +1,224 @@ +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 + } +} diff --git a/app/Jobs/MoveMediaJob.php b/app/Jobs/MoveMediaJob.php index 9c33b57..9221cd7 100644 --- a/app/Jobs/MoveMediaJob.php +++ b/app/Jobs/MoveMediaJob.php @@ -48,6 +48,8 @@ class MoveMediaJob implements ShouldQueue /** * Execute the job. + * + * @return void */ public function handle(): void { diff --git a/app/Jobs/StoreUploadedFileJob.php b/app/Jobs/StoreUploadedFileJob.php index 3796133..ef114a6 100644 --- a/app/Jobs/StoreUploadedFileJob.php +++ b/app/Jobs/StoreUploadedFileJob.php @@ -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; } /** diff --git a/app/Models/Media.php b/app/Models/Media.php index 7bbed9d..3df33c8 100644 --- a/app/Models/Media.php +++ b/app/Models/Media.php @@ -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(); + } } diff --git a/composer.json b/composer.json index db59d18..3d59874 100644 --- a/composer.json +++ b/composer.json @@ -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/", diff --git a/resources/js/components/dialogs/SMDialogMedia.vue b/resources/js/components/dialogs/SMDialogMedia.vue index ffe6af9..16bc8e5 100644 --- a/resources/js/components/dialogs/SMDialogMedia.vue +++ b/resources/js/components/dialogs/SMDialogMedia.vue @@ -234,7 +234,7 @@ -
+

{{ lastSelected.title }}

@@ -260,13 +260,49 @@

- + + Rotate Left + + + + Rotate Right + + + Delete Permanently + "> + + Delete Permanently + + +

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