1048 lines
33 KiB
PHP
1048 lines
33 KiB
PHP
<?php
|
|
|
|
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 FFMpeg\Coordinate\TimeCode;
|
|
use FFMpeg\FFMpeg;
|
|
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;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
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;
|
|
|
|
class Media extends Model
|
|
{
|
|
use HasFactory;
|
|
use Uuids;
|
|
use DispatchesJobs;
|
|
|
|
public const INVALID_FILE_ERROR = 1;
|
|
public const FILE_SIZE_EXCEEDED_ERROR = 2;
|
|
public const FILE_NAME_EXISTS_ERROR = 3;
|
|
public const TEMP_FILE_ERROR = 4;
|
|
|
|
/**
|
|
* The attributes that are mass assignable.
|
|
*
|
|
* @var array<int, string>
|
|
*/
|
|
protected $fillable = [
|
|
'title',
|
|
'user_id',
|
|
'mime_type',
|
|
'permission',
|
|
'storage',
|
|
'description',
|
|
'name',
|
|
'size',
|
|
'status',
|
|
];
|
|
|
|
/**
|
|
* The attributes that are appended.
|
|
*
|
|
* @var array<string>
|
|
*/
|
|
protected $appends = [
|
|
'url',
|
|
];
|
|
|
|
/**
|
|
* The default attributes.
|
|
*
|
|
* @var string[]
|
|
*/
|
|
protected $attributes = [
|
|
'storage' => 'cdn',
|
|
'variants' => '[]',
|
|
'description' => '',
|
|
'dimensions' => '',
|
|
'permission' => '',
|
|
'thumbnail' => '',
|
|
];
|
|
|
|
/**
|
|
* The storage file list cache.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected static $storageFileListCache = [];
|
|
|
|
/**
|
|
* Object variant details.
|
|
*
|
|
* @var int[][][]
|
|
*/
|
|
protected static $objectVariants = [
|
|
'image' => [
|
|
'small' => ['width' => 300, 'height' => 225],
|
|
'medium' => ['width' => 768, 'height' => 576],
|
|
'large' => ['width' => 1024, 'height' => 768],
|
|
'xlarge' => ['width' => 1536, 'height' => 1152],
|
|
'xxlarge' => ['width' => 2048, 'height' => 1536],
|
|
'scaled' => ['width' => 2560, 'height' => 1920]
|
|
]
|
|
];
|
|
|
|
/**
|
|
* Staging file path of asset for processing.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $stagingFilePath = "";
|
|
|
|
|
|
/**
|
|
* Model Boot
|
|
*
|
|
* @return void
|
|
*/
|
|
protected static function boot(): void
|
|
{
|
|
parent::boot();
|
|
|
|
$clearCache = function ($media) {
|
|
Cache::forget("media:{$media->id}");
|
|
};
|
|
|
|
static::updating(function ($media) use ($clearCache) {
|
|
$clearCache($media);
|
|
|
|
if (array_key_exists('permission', $media->getChanges()) === true) {
|
|
$origPermission = $media->getOriginal()['permission'];
|
|
$newPermission = $media->permission;
|
|
|
|
$newPermissionLen = strlen($newPermission);
|
|
|
|
if ($newPermissionLen !== strlen($origPermission)) {
|
|
if ($newPermissionLen === 0) {
|
|
$this->moveToStorage('cdn');
|
|
} else {
|
|
$this->moveToStorage('private');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
static::deleting(function ($media) use ($clearCache) {
|
|
$clearCache($media);
|
|
$media->deleteFile();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get Object Variants.
|
|
*
|
|
* @param string $type The variant object to get.
|
|
* @return array The variant data.
|
|
*/
|
|
public static function getObjectVariants(string $type): array
|
|
{
|
|
if (isset(self::$objectVariants[$type]) === true) {
|
|
return self::$objectVariants[$type];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Variants Get Mutator.
|
|
*
|
|
* @param mixed $value The value to mutate.
|
|
* @return array|null The mutated value.
|
|
*/
|
|
public function getVariantsAttribute(mixed $value): array|null
|
|
{
|
|
if (is_string($value) === true) {
|
|
return json_decode($value, true);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Variants Set Mutator.
|
|
*
|
|
* @param mixed $value The value to mutate.
|
|
* @return void
|
|
*/
|
|
public function setVariantsAttribute(mixed $value): void
|
|
{
|
|
if (is_array($value) !== true) {
|
|
$value = [];
|
|
}
|
|
|
|
$this->attributes['variants'] = json_encode(($value ?? []));
|
|
}
|
|
|
|
/**
|
|
* Get previous variant.
|
|
*
|
|
* @param string $type The variant type.
|
|
* @param string $variant The initial variant.
|
|
* @return string The previous variant name (or '').
|
|
*/
|
|
public function getPreviousVariant(string $type, string $variant): string
|
|
{
|
|
if (isset(self::$objectVariants[$type]) === false) {
|
|
return '';
|
|
}
|
|
|
|
$variants = self::$objectVariants[$type];
|
|
$keys = array_keys($variants);
|
|
|
|
$currentIndex = array_search($variant, $keys);
|
|
if ($currentIndex === false || $currentIndex === 0) {
|
|
return '';
|
|
}
|
|
|
|
return $keys[($currentIndex - 1)];
|
|
}
|
|
|
|
/**
|
|
* Get next variant.
|
|
*
|
|
* @param string $type The variant type.
|
|
* @param string $variant The initial variant.
|
|
* @return string The next variant name (or '').
|
|
*/
|
|
public function getNextVariant(string $type, string $variant): string
|
|
{
|
|
if (isset(self::$objectVariants[$type]) === false) {
|
|
return '';
|
|
}
|
|
|
|
$variants = self::$objectVariants[$type];
|
|
$keys = array_keys($variants);
|
|
|
|
$currentIndex = array_search($variant, $keys);
|
|
if ($currentIndex === false || $currentIndex === (count($keys) - 1)) {
|
|
return '';
|
|
}
|
|
|
|
return $keys[($currentIndex + 1)];
|
|
}
|
|
|
|
/**
|
|
* Get variant URL.
|
|
*
|
|
* @param string $variant The variant to find.
|
|
* @param boolean $returnNearest Return the nearest variant if request is not found.
|
|
* @return string The URL.
|
|
*/
|
|
public function getVariantURL(string $variant, bool $returnNearest = true): string
|
|
{
|
|
$variants = $this->variants;
|
|
if (isset($variants[$variant]) === true) {
|
|
return self::getUrlPath() . $variants[$variant];
|
|
}
|
|
|
|
if ($returnNearest === true) {
|
|
$variantType = explode('/', $this->mime_type)[0];
|
|
$previousVariant = $variant;
|
|
while (empty($previousVariant) === false) {
|
|
$previousVariant = $this->getPreviousVariant($variantType, $previousVariant);
|
|
if (empty($previousVariant) === false && isset($variants[$previousVariant]) === true) {
|
|
return self::getUrlPath() . $variants[$previousVariant];
|
|
}
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Delete file and associated files with the modal.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function deleteFile(): void
|
|
{
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Invalidate Cloudflare Cache.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function invalidateCFCache(): void
|
|
{
|
|
$zone_id = env("CLOUDFLARE_ZONE_ID");
|
|
$api_key = env("CLOUDFLARE_API_KEY");
|
|
if ($zone_id !== null && $api_key !== null && $this->url !== "") {
|
|
$urls = [$this->url];
|
|
|
|
foreach ($this->variants as $variant => $name) {
|
|
$urls[] = str_replace($this->name, $name, $this->url);
|
|
}
|
|
|
|
$curl = curl_init();
|
|
curl_setopt_array($curl, [
|
|
CURLOPT_URL => "https://api.cloudflare.com/client/v4/zones/" . $zone_id . "/purge_cache",
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_CUSTOMREQUEST => "DELETE",
|
|
CURLOPT_POSTFIELDS => json_encode(["files" => $urls]),
|
|
CURLOPT_HTTPHEADER => [
|
|
"Content-Type: application/json",
|
|
"Authorization: Bearer " . $api_key
|
|
],
|
|
]);
|
|
curl_exec($curl);
|
|
curl_close($curl);
|
|
}//end if
|
|
}
|
|
|
|
/**
|
|
* Get URL path
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getUrlPath(): string
|
|
{
|
|
$url = config("filesystems.disks.$this->storage.url");
|
|
return "$url/";
|
|
}
|
|
|
|
/**
|
|
* Return the file URL
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getUrlAttribute(): string
|
|
{
|
|
if (isset($this->attributes['name']) === true) {
|
|
return self::getUrlPath() . $this->name;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Return the file owner
|
|
*
|
|
* @return BelongsTo
|
|
*/
|
|
public function user(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class);
|
|
}
|
|
|
|
/**
|
|
* Move files to new storage device.
|
|
*
|
|
* @param string $storage The storage ID to move to.
|
|
* @return void
|
|
*/
|
|
public function moveToStorage(string $storage): void
|
|
{
|
|
if ($storage !== $this->storage && Config::has("filesystems.disks.$storage") === true) {
|
|
$this->status = "Processing media";
|
|
MoveMediaJob::dispatch($this, $storage)->onQueue('media');
|
|
$this->save();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transform the media through the Media Job Queue
|
|
*
|
|
* @param array $transform The transform data.
|
|
* @param boolean $silent Update the medium progress through its status field.
|
|
* @return void
|
|
*/
|
|
public function transform(array $transform, bool $silent = false): void
|
|
{
|
|
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]];
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
MediaJob::dispatch($this, $transform, $silent)->onQueue('media');
|
|
} catch (\Exception $e) {
|
|
$this->error('Failed to transform media');
|
|
throw $e;
|
|
}//end try
|
|
}
|
|
|
|
/**
|
|
* Download the file from the storage to the user.
|
|
*
|
|
* @param string $variant The variant to download or null if none.
|
|
* @param boolean $fallback Fallback to the original file if the variant is not found.
|
|
* @return JsonResponse|StreamedResponse The response.
|
|
* @throws BindingResolutionException The Exception.
|
|
*/
|
|
public function download(string $variant = null, bool $fallback = true)
|
|
{
|
|
$path = $this->name;
|
|
if ($variant !== null) {
|
|
if (array_key_exists($variant, $this->variant) === true) {
|
|
$path = $this->variant[$variant];
|
|
} else {
|
|
return response()->json(
|
|
['message' => 'The resource was not found.'],
|
|
HttpResponseCodes::HTTP_NOT_FOUND
|
|
);
|
|
}
|
|
}
|
|
|
|
$disk = Storage::disk($this->storage);
|
|
if ($disk->exists($path) === true) {
|
|
$stream = $disk->readStream($path);
|
|
$response = response()->stream(
|
|
function () use ($stream) {
|
|
fpassthru($stream);
|
|
},
|
|
200,
|
|
[
|
|
'Content-Type' => $this->mime_type,
|
|
'Content-Length' => $disk->size($path),
|
|
'Content-Disposition' => 'attachment; filename="' . basename($path) . '"',
|
|
]
|
|
);
|
|
|
|
return $response;
|
|
}
|
|
|
|
return response()->json(['message' => 'The resource was not found.'], HttpResponseCodes::HTTP_NOT_FOUND);
|
|
}
|
|
|
|
/**
|
|
* Get the server maximum upload size
|
|
*
|
|
* @return integer
|
|
*/
|
|
public static function getMaxUploadSize(): int
|
|
{
|
|
$sizes = [
|
|
ini_get('upload_max_filesize'),
|
|
ini_get('post_max_size'),
|
|
ini_get('memory_limit')
|
|
];
|
|
|
|
foreach ($sizes as &$size) {
|
|
$size = trim($size);
|
|
$last = strtolower($size[(strlen($size) - 1)]);
|
|
switch ($last) {
|
|
case 'g':
|
|
$size = (intval($size) * 1024);
|
|
// Size is in MB - fallthrough
|
|
case 'm':
|
|
$size = (intval($size) * 1024);
|
|
// Size is in KB - fallthrough
|
|
case 'k':
|
|
$size = (intval($size) * 1024);
|
|
// Size is in B - fallthrough
|
|
}
|
|
}
|
|
|
|
return min($sizes);
|
|
}
|
|
|
|
/**
|
|
* Generate a file name that is available within storage.
|
|
*
|
|
* @param string $fileName The proposed file name.
|
|
* @return string|boolean The available file name or false if failed.
|
|
*/
|
|
public static function generateUniqueFileName(string $fileName)
|
|
{
|
|
$index = 1;
|
|
$maxTries = 100;
|
|
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
|
|
$fileName = static::sanitizeFilename(pathinfo($fileName, PATHINFO_FILENAME));
|
|
|
|
if (
|
|
static::fileNameHasSuffix($fileName) === true ||
|
|
static::fileExistsInStorage("$fileName.$extension") === true ||
|
|
Media::where('name', "$fileName.$extension")->where('status', 'not like', 'failed%')->exists() === true
|
|
) {
|
|
$fileName .= '-';
|
|
for ($i = 1; $i < $maxTries; $i++) {
|
|
$fileNameIndex = $fileName . $index;
|
|
if (
|
|
static::fileExistsInStorage("$fileNameIndex.$extension") !== true &&
|
|
Media::where('name', "$fileNameIndex.$extension")
|
|
->where('status', 'not like', 'Failed%')
|
|
->exists() !== true
|
|
) {
|
|
return "$fileNameIndex.$extension";
|
|
}
|
|
|
|
++$index;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return "$fileName.$extension";
|
|
}
|
|
|
|
/**
|
|
* Determines if the file name exists in any of the storage disks.
|
|
*
|
|
* @param string $fileName The file name to check.
|
|
* @param boolean $ignoreCache Ignore the file list cache.
|
|
* @return boolean If the file exists on any storage disks.
|
|
*/
|
|
public static function fileExistsInStorage(string $fileName, bool $ignoreCache = false): bool
|
|
{
|
|
$disks = array_keys(Config::get('filesystems.disks'));
|
|
|
|
if ($ignoreCache === false) {
|
|
if (count(static::$storageFileListCache) === 0) {
|
|
$disks = array_keys(Config::get('filesystems.disks'));
|
|
|
|
foreach ($disks as $disk) {
|
|
try {
|
|
static::$storageFileListCache[$disk] = Storage::disk($disk)->allFiles();
|
|
} catch (\Exception $e) {
|
|
Log::error("Cannot get a file list for storage device '$disk'. Error: " . $e->getMessage());
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (static::$storageFileListCache as $disk => $files) {
|
|
if (in_array($fileName, $files) === true) {
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
$disks = array_keys(Config::get('filesystems.disks'));
|
|
|
|
foreach ($disks as $disk) {
|
|
try {
|
|
if (Storage::disk($disk)->exists($fileName) === true) {
|
|
return true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
Log::error($e->getMessage());
|
|
throw new \Exception("Cannot verify if file '$fileName' already exists in storage device '$disk'");
|
|
}
|
|
}
|
|
}//end if
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Test if the file name contains a special suffix.
|
|
*
|
|
* @param string $fileName The file name to test.
|
|
* @return boolean If the file name contains the special suffix.
|
|
*/
|
|
public static function fileNameHasSuffix(string $fileName): bool
|
|
{
|
|
$suffix = '/(-\d+x\d+|-scaled|-thumb)$/i';
|
|
$fileNameWithoutExtension = pathinfo($fileName, PATHINFO_FILENAME);
|
|
|
|
return preg_match($suffix, $fileNameWithoutExtension) === 1;
|
|
}
|
|
|
|
/**
|
|
* Sanitize fileName for upload
|
|
*
|
|
* @param string $fileName Filename to sanitize.
|
|
* @return string
|
|
*/
|
|
private static function sanitizeFilename(string $fileName): string
|
|
{
|
|
/*
|
|
# file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
|
|
[<>:"/\\\|?*]|
|
|
|
|
# control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
|
|
[\x00-\x1F]|
|
|
|
|
# non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
|
|
[\x7F\xA0\xAD]|
|
|
|
|
# URI reserved https://www.rfc-editor.org/rfc/rfc3986#section-2.2
|
|
[#\[\]@!$&\'()+,;=]|
|
|
|
|
# URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
|
|
[{}^\~`]
|
|
*/
|
|
|
|
$fileName = preg_replace(
|
|
'~
|
|
[<>:"/\\\|?*]|
|
|
[\x00-\x1F]|
|
|
[\x7F\xA0\xAD]|
|
|
[#\[\]@!$&\'()+,;=]|
|
|
[{}^\~`]
|
|
~x',
|
|
'-',
|
|
$fileName
|
|
);
|
|
|
|
$fileName = ltrim($fileName, '.-');
|
|
|
|
$fileName = preg_replace([
|
|
// "file name.zip" becomes "file-name.zip"
|
|
'/ +/',
|
|
// "file___name.zip" becomes "file-name.zip"
|
|
'/_+/',
|
|
// "file---name.zip" becomes "file-name.zip"
|
|
'/-+/'
|
|
], '-', $fileName);
|
|
$fileName = preg_replace([
|
|
// "file--.--.-.--name.zip" becomes "file.name.zip"
|
|
'/-*\.-*/',
|
|
// "file...name..zip" becomes "file.name.zip"
|
|
'/\.{2,}/'
|
|
], '.', $fileName);
|
|
// lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625
|
|
$fileName = mb_strtolower($fileName, mb_detect_encoding($fileName));
|
|
// ".file-name.-" becomes "file-name"
|
|
$fileName = trim($fileName, '.-');
|
|
|
|
$ext = pathinfo($fileName, PATHINFO_EXTENSION);
|
|
$fileName = mb_strcut(
|
|
pathinfo($fileName, PATHINFO_FILENAME),
|
|
0,
|
|
(255 - ($ext !== '' ? strlen($ext) + 1 : 0)),
|
|
mb_detect_encoding($fileName)
|
|
) . ($ext !== '' ? '.' . $ext : '');
|
|
return $fileName;
|
|
}
|
|
|
|
/**
|
|
* Get the Staging File path.
|
|
*
|
|
* @param boolean $create Create staging file if doesn't exist.
|
|
* @return 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->storage)->readStream($this->name);
|
|
$filePath = generateTempFilePath(pathinfo($this->name, PATHINFO_EXTENSION));
|
|
|
|
$writeStream = fopen($filePath, 'w');
|
|
while (feof($readStream) !== true) {
|
|
fwrite($writeStream, fread($readStream, 8192));
|
|
}
|
|
fclose($readStream);
|
|
fclose($writeStream);
|
|
|
|
$this->stagingFilePath = $filePath;
|
|
}//end if
|
|
|
|
return $this->stagingFilePath !== "";
|
|
}
|
|
|
|
/**
|
|
* Save the Staging File to storage
|
|
*
|
|
* @param boolean $delete Delete the existing staging file.
|
|
* @param boolean $silent Update the status field with the progress.
|
|
* @return void
|
|
*/
|
|
public function saveStagingFile(bool $delete = true, bool $silent = false): 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);
|
|
if ($silent === false) {
|
|
$this->status('Uploading to CDN');
|
|
}
|
|
$fileSystem->putFileAs('/', $this->stagingFilePath, $this->name);
|
|
}
|
|
|
|
if ($silent === false) {
|
|
$this->status('Generating Thumbnail');
|
|
}
|
|
$this->generateThumbnail();
|
|
|
|
if ($silent === false) {
|
|
$this->status('Generating Variants');
|
|
}
|
|
$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.
|
|
*
|
|
* @return boolean If generation was successful.
|
|
*/
|
|
public function generateThumbnail(): bool
|
|
{
|
|
$thumbnailWidth = 200;
|
|
$thumbnailHeight = 200;
|
|
|
|
// delete existing thumbnail
|
|
if (strlen($this->thumbnail) !== 0) {
|
|
$path = substr($this->thumbnail, strlen($this->getUrlPath()));
|
|
if (strlen($path) > 0 && Storage::disk($this->storage)->exists($path) === true) {
|
|
Storage::disk($this->storage)->delete($path);
|
|
}
|
|
}
|
|
|
|
$filePath = $this->getStagingFilePath();
|
|
|
|
$fileExtension = File::extension($this->name);
|
|
$tempImagePath = tempnam(sys_get_temp_dir(), 'thumb');
|
|
$newFilename = pathinfo($this->name, PATHINFO_FILENAME) . "-thumb.webp";
|
|
$success = false;
|
|
|
|
if (strpos($this->mime_type, 'image/') === 0) {
|
|
$image = Image::make($filePath);
|
|
$image->resize($thumbnailWidth, $thumbnailHeight, function ($constraint) {
|
|
$constraint->aspectRatio();
|
|
});
|
|
$image->fit($thumbnailWidth, $thumbnailHeight);
|
|
$image->encode('webp', 75)->save($tempImagePath);
|
|
$success = true;
|
|
} elseif ($this->mime_type === 'application/pdf' && extension_loaded('imagick') === true) {
|
|
$pdfPreview = new \Imagick();
|
|
$pdfPreview->setResolution(300, 300);
|
|
$pdfPreview->readImage($filePath . '[0]');
|
|
$pdfPreview->setImageFormat('webp');
|
|
$pdfPreview->thumbnailImage($thumbnailWidth, $thumbnailHeight, true);
|
|
file_put_contents($tempImagePath, $pdfPreview);
|
|
|
|
$success = true;
|
|
} elseif ($this->mime_type === 'text/plain') {
|
|
$image = Image::canvas($thumbnailWidth, $thumbnailHeight, '#FFFFFF');
|
|
|
|
// Read the first few lines of the text file
|
|
$numLines = 5;
|
|
$text = file_get_contents($filePath);
|
|
$lines = explode("\n", $text);
|
|
$previewText = implode("\n", array_slice($lines, 0, $numLines));
|
|
|
|
// Center the text on the image
|
|
$fontSize = 8;
|
|
$textColor = '#000000'; // Black text color
|
|
|
|
// Calculate the position to start drawing the text
|
|
$x = 10; // Left padding
|
|
$y = 10; // Top padding
|
|
|
|
// Draw the text on the canvas with text wrapping
|
|
$lines = explode("\n", wordwrap($previewText, 30, "\n", true));
|
|
foreach ($lines as $line) {
|
|
$image->text($line, $x, $y, function ($font) use ($fontSize, $textColor) {
|
|
$font->file(1);
|
|
$font->size($fontSize);
|
|
$font->color($textColor);
|
|
});
|
|
|
|
// Move to the next line
|
|
$y += ($fontSize + 4); // Add some vertical spacing between lines (adjust as needed)
|
|
}
|
|
|
|
$image->encode('webp', 75)->save($tempImagePath);
|
|
|
|
$success = true;
|
|
} elseif (strpos($this->mime_type, 'video/') === 0) {
|
|
$tempImagePath .= '.webp';
|
|
|
|
try {
|
|
$ffmpeg = FFMpeg::create();
|
|
$video = $ffmpeg->open($filePath);
|
|
$frame = $video->frame(TimeCode::fromSeconds(5));
|
|
$frame->save($tempImagePath);
|
|
} catch(\Exception $e) {
|
|
Log::error($e);
|
|
}
|
|
|
|
$success = true;
|
|
}//end if
|
|
|
|
if ($success === true && file_exists($tempImagePath) === true) {
|
|
/** @var Illuminate\Filesystem\FilesystemAdapter */
|
|
$fileSystem = Storage::disk($this->storage);
|
|
$fileSystem->putFileAs('/', new SplFileInfo($tempImagePath), $newFilename);
|
|
unlink($tempImagePath);
|
|
|
|
$this->thumbnail = $this->getUrlPath() . $newFilename;
|
|
} else {
|
|
$iconExtension = 'unknown';
|
|
if ($fileExtension !== '') {
|
|
$iconPath = public_path('assets/fileicons/' . $fileExtension . '.webp');
|
|
if (file_exists($iconPath) === true) {
|
|
$iconExtension = $fileExtension;
|
|
}
|
|
}
|
|
|
|
$this->thumbnail = asset('/assets/fileicons/' . $iconExtension . '.webp');
|
|
}
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* Delete Media Thumbnail from storage.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function deleteThumbnail(): void
|
|
{
|
|
if (strlen($this->thumbnail) > 0) {
|
|
$path = substr($this->thumbnail, strlen($this->getUrlPath()));
|
|
|
|
if (strlen($path) > 0 && Storage::disk($this->storage)->exists($path) === true) {
|
|
Storage::disk($this->storage)->delete($path);
|
|
$this->thumbnail = ''; // Clear the thumbnail property
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate variants for this media.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function generateVariants(): void
|
|
{
|
|
if (strpos($this->mime_type, 'image/') === 0) {
|
|
// Generate additional image sizes
|
|
$sizes = Media::getObjectVariants('image');
|
|
|
|
// download original from CDN if no local file
|
|
$filePath = $this->getStagingFilePath();
|
|
|
|
// delete existing variants
|
|
if (is_array($this->variants) === true) {
|
|
foreach ($this->variants as $variantName => $variantFile) {
|
|
if (Storage::disk($this->storage)->exists($variantFile) === true) {
|
|
Storage::disk($this->storage)->delete($variantFile);
|
|
}
|
|
}
|
|
}
|
|
$this->variants = [];
|
|
|
|
$originalImage = Image::make($filePath);
|
|
|
|
$imageSize = $originalImage->getSize();
|
|
$isPortrait = $imageSize->getHeight() > $imageSize->getWidth();
|
|
|
|
// Swap width and height values for portrait images
|
|
foreach ($sizes as $variantName => &$size) {
|
|
if ($isPortrait === true) {
|
|
$temp = $size['width'];
|
|
$size['width'] = $size['height'];
|
|
$size['height'] = $temp;
|
|
}
|
|
}
|
|
|
|
$dimensions = [$originalImage->getWidth(), $originalImage->getHeight()];
|
|
$this->dimensions = implode('x', $dimensions);
|
|
|
|
foreach ($sizes as $variantName => $size) {
|
|
$postfix = "{$size['width']}x{$size['height']}";
|
|
if ($variantName === 'scaled') {
|
|
$postfix = 'scaled';
|
|
}
|
|
|
|
$newFilename = pathinfo($this->name, PATHINFO_FILENAME) . "-$postfix.webp";
|
|
|
|
// Get the largest available variant
|
|
if ($dimensions[0] >= $size['width'] && $dimensions[1] >= $size['height']) {
|
|
// Store the variant in the variants array
|
|
$variants[$variantName] = $newFilename;
|
|
|
|
// Resize the image to the variant size if its dimensions are greater than the
|
|
// specified size
|
|
$image = clone $originalImage;
|
|
|
|
$imageSize = $image->getSize();
|
|
if ($imageSize->getWidth() > $size['width'] || $imageSize->getHeight() > $size['height']) {
|
|
$image->resize($size['width'], $size['height'], function ($constraint) {
|
|
$constraint->aspectRatio();
|
|
$constraint->upsize();
|
|
});
|
|
$image->resizeCanvas($size['width'], $size['height'], 'center', false, 'rgba(0,0,0,0)');
|
|
}
|
|
|
|
$image->orientate();
|
|
|
|
// Optimize and store image
|
|
$tempImagePath = tempnam(sys_get_temp_dir(), 'optimize');
|
|
$image->encode('webp', 75)->save($tempImagePath);
|
|
/** @var Illuminate\Filesystem\FilesystemAdapter */
|
|
$fileSystem = Storage::disk($this->storage);
|
|
$fileSystem->putFileAs('/', new SplFileInfo($tempImagePath), $newFilename);
|
|
unlink($tempImagePath);
|
|
}//end if
|
|
}//end foreach
|
|
|
|
// Set missing variants to the largest available variant
|
|
foreach ($sizes as $variantName => $size) {
|
|
if (isset($variants[$variantName]) === false) {
|
|
$variants[$variantName] = $this->name;
|
|
}
|
|
}
|
|
|
|
$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();
|
|
}
|
|
}
|