diff --git a/README.md b/README.md index db05d64..a2b80c8 100644 --- a/README.md +++ b/README.md @@ -271,23 +271,6 @@ return [ */ 'queue' => null, - /** - * Customize WithoutOverlapping middleware settings - */ - 'queue_overlapping' => [ - /** - * The release value should be longer than the longest conversion job that might run - * Default is: 1 minute. Increase it if your jobs are longer. - */ - 'release_after' => 60, - /** - * The expire value allows you to forget a lock in case of an unexpected job failure - * - * @see https://laravel.com/docs/10.x/queues#preventing-job-overlaps - */ - 'expire_after' => 60 * 60, - ], - ]; ``` diff --git a/config/media.php b/config/media.php index c0307d4..813c0d6 100644 --- a/config/media.php +++ b/config/media.php @@ -10,6 +10,8 @@ */ 'model' => Media::class, + 'temporary_storage_path' => 'app/tmp/media', + /** * The default disk used for storing files */ @@ -58,21 +60,4 @@ */ 'queue' => null, - /** - * Customize WithoutOverlapping middleware settings - */ - 'queue_overlapping' => [ - /** - * The release value should be longer than the longest conversion job that might run - * Default is: 1 minute. Increase it if your jobs are longer. - */ - 'release_after' => 60, - /** - * The expire value allows you to forget a lock in case of an unexpected job failure - * - * @see https://laravel.com/docs/10.x/queues#preventing-job-overlaps - */ - 'expire_after' => 60 * 60, - ], - ]; diff --git a/database/factories/MediaConversionFactory.php b/database/factories/MediaConversionFactory.php new file mode 100644 index 0000000..b682f1c --- /dev/null +++ b/database/factories/MediaConversionFactory.php @@ -0,0 +1,38 @@ + + */ +class MediaConversionFactory extends Factory +{ + protected $model = MediaConversion::class; + + public function definition() + { + return [ + 'conversion_name' => 'name', + 'state' => 'success', + 'state_set_at' => now(), + 'disk' => config('media.disk'), + 'path' => '{uuid}/conversions/name/fileName.jpg', + 'type' => MediaType::Image, + 'name' => 'fileName', + 'extension' => 'jpg', + 'file_name' => 'fileName.jpg', + 'mime_type' => 'image/jpeg', + 'width' => 16, + 'height' => 9, + 'aspect_ratio' => 16 / 9, + 'average_color' => null, + 'size' => 800, + 'duration' => null, + 'metadata' => [], + ]; + } +} diff --git a/database/factories/MediaFactory.php b/database/factories/MediaFactory.php index e63a5f8..028e29c 100644 --- a/database/factories/MediaFactory.php +++ b/database/factories/MediaFactory.php @@ -2,13 +2,13 @@ namespace Elegantly\Media\Database\Factories; -use Elegantly\Media\Casts\GeneratedConversion; use Elegantly\Media\Enums\MediaType; use Elegantly\Media\Models\Media; +use Elegantly\Media\Models\MediaConversion; use Illuminate\Database\Eloquent\Factories\Factory; /** - * @template TModel of Media + * @extends Factory */ class MediaFactory extends Factory { @@ -20,51 +20,31 @@ public function definition() 'name' => 'empty', 'file_name' => 'empty.jpg', 'size' => 10, - 'path' => '/uuid/empty.jpg', + 'path' => '{uuid}/empty.jpg', 'type' => MediaType::Image, 'collection_name' => config('media.default_collection_name'), 'disk' => config('media.disk'), - 'model_id' => 0, - 'model_type' => '\App\Models\Fake', ]; } public function withPoster(): static { - return $this->state(function (array $attributes) { - return [ - 'generated_conversions' => collect($attributes['generated_conversions'] ?? []) - ->put('poster', new GeneratedConversion( - state: 'success', - type: MediaType::Image, - file_name: 'poster.png', - name: 'poster', - path: '/uuid/poster/poster.png', - disk: $attributes['disk'], - )), - ]; - }); - } - - public static function generatedConversion(?string $disk = null) - { - return new GeneratedConversion( - state: 'success', - type: MediaType::Image, - file_name: 'poster.png', - name: 'poster', - path: '/poster/poster.png', - disk: $disk ?? config('media.disk'), - generated_conversions: collect([ - '480p' => new GeneratedConversion( - state: 'success', - type: MediaType::Image, - file_name: 'poster-480p.png', - name: 'poster-480p', - path: '/poster/generated_conversions/480p/poster-480p.png', - disk: $disk ?? config('media.disk'), - ), - ]) + return $this->has( + MediaConversion::factory() + ->state(fn ($attributes) => [ + 'conversion_name' => 'poster', + 'disk' => $attributes['disk'], + 'path' => '{uuid}/conversions/poster/poster.jpg', + 'type' => MediaType::Image, + 'name' => 'poster', + 'extension' => 'jpg', + 'file_name' => 'poster.jpg', + 'mime_type' => 'image/jpeg', + 'width' => $attributes['width'] ?? null, + 'height' => $attributes['height'] ?? null, + 'aspect_ratio' => $attributes['aspect_ratio'] ?? null, + ]), + 'conversions' ); } } diff --git a/database/migrations/create_media_conversions_table.php.stub b/database/migrations/create_media_conversions_table.php.stub new file mode 100644 index 0000000..0e14928 --- /dev/null +++ b/database/migrations/create_media_conversions_table.php.stub @@ -0,0 +1,49 @@ +id(); + $table->uuid('uuid')->unique(); + + $table->string('conversion_name'); + + $table->foreignId('media_id'); + + $table->string('state')->nullable(); + $table->dateTime('state_set_at')->nullable(); + + $table->longText('contents')->nullable(); + + $table->string('disk')->nullable(); + + $table->text('path')->nullable(); + $table->string('name')->nullable(); + $table->string('file_name')->nullable(); + $table->string('extension')->nullable(); + $table->string('mime_type')->nullable(); + $table->string('type')->nullable(); + $table->unsignedBigInteger('size')->nullable(); + $table->unsignedBigInteger('width')->nullable(); + $table->unsignedBigInteger('height')->nullable(); + $table->decimal('aspect_ratio', 8, 2, true)->nullable(); + $table->string('average_color')->nullable(); + $table->decimal('duration', 19, 2, true)->nullable(); + + $table->json('metadata')->nullable(); + + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('media_conversations'); + } +}; diff --git a/database/migrations/create_media_table.php.stub b/database/migrations/create_media_table.php.stub index 753335a..0f6347b 100644 --- a/database/migrations/create_media_table.php.stub +++ b/database/migrations/create_media_table.php.stub @@ -10,36 +10,39 @@ return new class extends Migration { Schema::create('media', function (Blueprint $table) { $table->id(); + $table->uuid('uuid')->unique(); - $table->nullableMorphs('model'); - $table->uuid('uuid')->unique()->index(); $table->string('collection_name')->index(); $table->string('collection_group')->nullable(); + $table->string('disk')->nullable(); + + $table->text('path')->nullable(); $table->string('name')->nullable(); $table->string('file_name')->nullable(); + $table->string('extension')->nullable(); $table->string('mime_type')->nullable(); - $table->string('disk')->nullable(); - $table->unsignedBigInteger('size')->nullable(); - $table->unsignedBigInteger('order_column')->nullable()->index(); - $table->json('generated_conversions')->nullable(); - - $table->string('path')->nullable(); $table->string('type')->nullable(); - $table->string('extension')->nullable(); + $table->unsignedBigInteger('size')->nullable(); $table->unsignedBigInteger('width')->nullable(); $table->unsignedBigInteger('height')->nullable(); $table->decimal('aspect_ratio', 8, 2, true)->nullable(); $table->string('average_color')->nullable(); $table->decimal('duration', 19, 2, true)->nullable(); + + $table->unsignedBigInteger('order_column')->nullable()->index(); $table->json('metadata')->nullable(); + $table->json('generated_conversions')->nullable(); + + $table->nullableMorphs('model'); + $table->timestamps(); - $table->index(['model_type', 'model_id', 'collection_name']); }); } - public function down(){ + public function down() + { Schema::dropIfExists('media'); } }; diff --git a/database/migrations/migrate_generated_conversions_to_media_conversions_table.php.stub b/database/migrations/migrate_generated_conversions_to_media_conversions_table.php.stub new file mode 100644 index 0000000..8d95675 --- /dev/null +++ b/database/migrations/migrate_generated_conversions_to_media_conversions_table.php.stub @@ -0,0 +1,102 @@ +chunkById(5_000, function ($items) { + + foreach ($items as $media) { + + if (! $media->generated_conversions) { + continue; + } + + /** @var array $generatedConversions */ + $generatedConversions = json_decode($media->generated_conversions, true); + + if (empty($generatedConversions)) { + continue; + } + + $conversions = $this->generatedConversionsToMediaConversions( + $generatedConversions, + ); + + $media->conversions()->saveMany($conversions); + + } + + }); + + Schema::create('media', function (Blueprint $table) { + $table->dropColumn('generated_conversions'); + }); + + } + + public function down() + { + Schema::create('media', function (Blueprint $table) { + $table->json('generated_conversions')->nullable(); + }); + } + + /** + * @return array + */ + public function generatedConversionsToMediaConversions( + array $generatedConversions, + ?string $parent = null, + ): array { + + return collect($generatedConversions) + ->flatMap(function (array $generatedConversion, string $conversionName) use ($parent) { + + $fullName = $parent ? "{$parent}.{$conversionName}" : $conversionName; + + $root = new MediaConversion([ + 'conversion_name' => $fullName, + ...Arr::only($generatedConversion, [ + 'state', + 'state_set_at', + 'disk', + 'path', + 'type', + 'name', + 'extension', + 'file_name', + 'mime_type', + 'width', + 'height', + 'aspect_ratio', + 'average_color', + 'size', + 'duration', + 'metadata', + 'created_at', + 'updated_at', + ]), + ]); + + if ($children = data_get($generatedConversion, 'generated_conversions')) { + return [ + $root, + ...$this->generatedConversionsToMediaConversions($children, $fullName), + ]; + } + + return [$root]; + + }) + ->toArray(); + + } +}; diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 260b5e1..9df685c 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -2,7 +2,7 @@ includes: - phpstan-baseline.neon parameters: - level: 4 + level: 9 paths: - src - config @@ -10,4 +10,3 @@ parameters: tmpDir: build/phpstan checkOctaneCompatibility: true checkModelProperties: true - diff --git a/src/Casts/GeneratedConversion.php b/src/Casts/GeneratedConversion.php deleted file mode 100644 index d9113a3..0000000 --- a/src/Casts/GeneratedConversion.php +++ /dev/null @@ -1,84 +0,0 @@ - $generated_conversions - */ -class GeneratedConversion implements Arrayable -{ - use InteractsWithMediaFiles; - - public Carbon $created_at; - - public Carbon $state_set_at; - - public function __construct( - public ?string $state = null, - public ?string $file_name = null, - public ?string $name = null, - public ?MediaType $type = null, - public ?string $disk = null, - public ?string $path = null, - public ?string $mime_type = null, - public ?string $extension = null, - public ?int $size = null, - public ?float $duration = null, - public ?int $height = null, - public ?int $width = null, - public ?float $aspect_ratio = null, - public ?string $average_color = null, - public ?string $content = null, - public array $metadata = [], - public Collection $generated_conversions = new Collection, - ?Carbon $created_at = null, - ?Carbon $state_set_at = null, - ) { - $this->created_at = $created_at ?? now(); - $this->state_set_at = $state_set_at ?? now(); - } - - public static function make(array $attributes): self - { - $state_set_at = Arr::get($attributes, 'state_set_at'); - $created_at = Arr::get($attributes, 'created_at'); - $type = Arr::get($attributes, 'type'); - - return new self( - file_name: Arr::get($attributes, 'file_name'), - name: Arr::get($attributes, 'name'), - state: Arr::get($attributes, 'state'), - state_set_at: $state_set_at ? Carbon::parse($state_set_at) : null, - type: $type ? MediaType::from($type) : null, - disk: Arr::get($attributes, 'disk'), - path: Arr::get($attributes, 'path'), - mime_type: Arr::get($attributes, 'mime_type'), - extension: Arr::get($attributes, 'extension'), - size: Arr::get($attributes, 'size'), - duration: Arr::get($attributes, 'duration'), - height: Arr::get($attributes, 'height'), - width: Arr::get($attributes, 'width'), - aspect_ratio: Arr::get($attributes, 'aspect_ratio'), - average_color: Arr::get($attributes, 'average_color'), - content: Arr::get($attributes, 'content'), - metadata: Arr::get($attributes, 'metadata', []), - generated_conversions: collect(Arr::get($attributes, 'generated_conversions', []))->map(fn ($item) => self::make($item)), - created_at: $created_at ? Carbon::parse($created_at) : null, - ); - } - - public function toArray(): array - { - return array_map( - fn ($value) => $value instanceof Arrayable ? $value->toArray() : $value, - get_object_vars($this), - ); - } -} diff --git a/src/Casts/GeneratedConversions.php b/src/Casts/GeneratedConversions.php deleted file mode 100644 index d9c1ea9..0000000 --- a/src/Casts/GeneratedConversions.php +++ /dev/null @@ -1,43 +0,0 @@ - $attributes - */ - public function get(Model $model, string $key, mixed $value, array $attributes): mixed - { - if (is_null($value)) { - return collect(); - } - - return collect(json_decode($value, true))->map(fn ($item) => GeneratedConversion::make($item)); - } - - /** - * Prepare the given value for storage. - * - * @param Collection|array|null $value - * @param array $attributes - */ - public function set(Model $model, string $key, mixed $value, array $attributes): mixed - { - if (is_null($value)) { - return json_encode([]); - } - - if ($value instanceof Collection) { - return json_encode($value->toArray()); - } - - return json_encode($value); - } -} diff --git a/src/Commands/GenerateMediaConversionsCommand.php b/src/Commands/GenerateMediaConversionsCommand.php index 25aeb79..4d87513 100644 --- a/src/Commands/GenerateMediaConversionsCommand.php +++ b/src/Commands/GenerateMediaConversionsCommand.php @@ -7,9 +7,11 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; +use function Laravel\Prompts\confirm; + class GenerateMediaConversionsCommand extends Command { - public $signature = 'media:generate-conversions {ids?*} {--force} {--pretend} {--conversions=*} {--models=*}'; + public $signature = 'media:generate-conversions {ids?*} {--force} {--pretend} {--conversions=*} {--collections=*} {--models=*}'; public $description = 'Generate all media conversions'; @@ -18,16 +20,22 @@ public function handle(): int $ids = (array) $this->argument('ids'); $force = (bool) $this->option('force'); $pretend = (bool) $this->option('pretend'); + /** @var string[] $conversions */ $conversions = (array) $this->option('conversions'); $models = (array) $this->option('models'); + $collections = (array) $this->option('collections'); + /** + * @var class-string $model + */ $model = config('media.model'); /** @var Collection */ $media = $model::query() - ->with(['model']) + ->with(['model', 'conversions']) ->when(! empty($ids), fn (Builder $query) => $query->whereIn('id', $ids)) ->when(! empty($models), fn (Builder $query) => $query->whereIn('model_type', $models)) + ->when(! empty($collections), fn (Builder $query) => $query->whereIn('collection_name', $collections)) ->get(); $mediaByModel = $media->countBy('model_type'); @@ -42,22 +50,21 @@ public function handle(): int }) ); - if ($pretend) { + if ($pretend || ! confirm('Continue?')) { return self::SUCCESS; } $this->withProgressBar($media, function (Media $media) use ($conversions, $force) { - $model = $media->model; - $modelConversions = $model->getMediaConversions($media); - $conversions = empty($conversions) ? - $modelConversions : - array_intersect($modelConversions->toArray(), $conversions); + $conversions = empty($conversions) ? array_keys($media->getConversionsDefinitions()) : $conversions; + + foreach ($conversions as $name) { + $conversion = $media->getConversion((string) $name); - foreach ($conversions as $conversion) { - if ($force || ! $media->hasGeneratedConversion($conversion)) { - $model->dispatchConversion($media, $conversion); + if ($force || ! $conversion) { + $media->dispatchConversion($name); } + } }); diff --git a/src/Concerns/HasMedia.php b/src/Concerns/HasMedia.php new file mode 100644 index 0000000..6f1a7b3 --- /dev/null +++ b/src/Concerns/HasMedia.php @@ -0,0 +1,243 @@ + $media + */ +trait HasMedia +{ + public static function bootHasMedia() + { + static::deleting(function (Model $model) { + + if (! config('media.delete_media_with_model')) { + return true; + } + + $isSoftDeleting = method_exists($model, 'isForceDeleting') && ! $model->isForceDeleting(); + + if ( + $isSoftDeleting && + ! config('media.delete_media_with_trashed_model') + ) { + return true; + } + + /** @var class-string */ + $job = config('media.delete_media_with_model_job'); + + $model->media->each(fn ($media) => dispatch(new $job($media))); + + }); + } + + /** + * @return MorphMany + */ + public function media(): MorphMany + { + return $this->morphMany(config('media.model'), 'model')->chaperone(); + } + + /** + * @return Arrayable|iterable|null + */ + public function registerMediaCollections(): Arrayable|iterable|null + { + return []; + } + + public function getMediaCollection(string $collectionName): ?MediaCollection + { + return collect($this->registerMediaCollections())->firstWhere('name', $collectionName); + } + + /** + * @return Collection + */ + public function getMedia( + ?string $collectionName = null, + ?string $collectionGroup = null + ): Collection { + return $this->media + ->when($collectionName, fn ($collection) => $collection->where('collection_name', $collectionName)) + ->when($collectionGroup, fn ($collection) => $collection->where('collection_group', $collectionGroup)) + ->values(); + } + + /** + * @return TMedia + */ + public function getFirstMedia( + ?string $collectionName = null, + ?string $collectionGroup = null + ): ?Media { + return $this->getMedia($collectionName, $collectionGroup)->first(); + } + + public function getFirstMediaUrl( + ?string $collectionName = null, + ?string $collectionGroup = null + ): ?string { + $media = $this->getFirstMedia($collectionName, $collectionGroup); + + if ($url = $media?->getUrl()) { + return $url; + } + + if ( + $collectionName && + $collection = $this->getMediaCollection($collectionName) + ) { + return value($collection->fallback); + } + + return null; + } + + /** + * @param string|resource|UploadedFile|File $file + * @return TMedia + */ + public function addMedia( + mixed $file, + ?string $collectionName = null, + ?string $collectionGroup = null, + ?string $name = null, + ?string $disk = null, + ): Media { + $collectionName ??= config('media.default_collection_name'); + + /** @var class-string */ + $model = config('media.model'); + + $media = new $model; + $media->model()->associate($this); + $media->collection_name = $collectionName; + $media->collection_group = $collectionGroup; + + $collection = $collectionName ? $this->getMediaCollection($collectionName) : null; + + $media->storeFile( + file: $file, + name: $name, + disk: $disk ?? $collection?->disk, + before: function ($file) use ($collection) { + if ($acceptedMimeTypes = $collection?->acceptedMimeTypes) { + if (! in_array( + HelpersFile::mimeType($file), + $acceptedMimeTypes + )) { + throw new Exception("Media file can't be stored: Invalid MIME type", 415); + } + } + + if ($transform = $collection?->transform) { + return $transform($file); + } + + return $file; + } + ); + + if ($this->relationLoaded('media')) { + $this->media->push($media); + } + + if ($collection?->single) { + $this->clearMediaCollection( + collectionName: $collectionName, + except: [$media->id] + ); + } + + $media->dispatchConversions(); + + return $media; + } + + /** + * @return $this + */ + public function deleteMedia(int $mediaId): static + { + $this->media->find($mediaId)?->delete(); + + $this->setRelation( + 'media', + $this->media->except([$mediaId]) + ); + + return $this; + } + + /** + * @return $this + */ + public function clearMediaCollection( + string $collectionName, + ?string $collectionGroup = null, + array $except = [], + ): static { + + $media = $this->getMedia($collectionName, $collectionGroup) + ->except($except) + ->each(fn ($media) => $media->delete()); + + $this->setRelation( + 'media', + $this->media->except($media->modelKeys()) + ); + + return $this; + } + + /** + * @param int|TMedia $media + */ + public function dispatchMediaConversion( + int|Media $media, + string $conversion + ): ?PendingDispatch { + + $media = $media instanceof Media ? $media : $this->media->find($media); + + $media->model()->associate($this); + + return $media->dispatchConversion($conversion); + } + + /** + * @param int|TMedia $media + */ + public function executeMediaConversion( + int|Media $media, + string $conversion + ): ?MediaConversion { + + $media = $media instanceof Media ? $media : $this->media->find($media); + + $media->model()->associate($this); + + return $media->executeConversion($conversion); + } +} diff --git a/src/Concerns/InteractWithFiles.php b/src/Concerns/InteractWithFiles.php new file mode 100644 index 0000000..6863018 --- /dev/null +++ b/src/Concerns/InteractWithFiles.php @@ -0,0 +1,194 @@ +disk) { + return null; + } + + return Storage::disk($this->disk); + } + + public function getUrl(): ?string + { + if (! $this->path) { + return null; + } + + return $this->getDisk()?->url($this->path); + } + + /** + * @param array $options + */ + public function getTemporaryUrl( + DateTimeInterface $expiration, + array $options = [] + ): ?string { + if (! $this->path) { + return null; + } + + return $this->getDisk()?->temporaryUrl($this->path, $expiration, $options); + } + + /** + * @return null|resource + */ + public function readStream() + { + if (! $this->path) { + return null; + } + + return $this->getDisk()?->readStream($this->path); + } + + public function deleteFile(): bool + { + if (! $this->path) { + return true; + } + + return (bool) $this->getDisk()?->delete($this->path); + } + + public function putFile( + string $disk, + string $destination, + UploadedFile|HttpFile $file, + string $name, + ): string|null|false { + $this->disk = $disk; + + $destination = Str::rtrim($destination, '/'); + $extension = File::extension($file); + + $name = File::sanitizeFilename($name); + + $fileName = $extension ? "{$name}.{$extension}" : $name; + + $path = $this->getDisk()?->putFileAs( + $destination, + $file, + $fileName, + ) ?: null; + + $this->path = $path; + $this->name = $name; + $this->extension = $extension; + $this->file_name = $fileName; + + $dimension = File::dimension($file->getPathname()); + + $this->height = $dimension?->getHeight(); + $this->width = $dimension?->getWidth(); + $this->aspect_ratio = $dimension?->getRatio(forceStandards: false)->getValue(); + $this->duration = File::duration($file->getPathname()); + $this->mime_type = File::mimeType($file); + $this->size = $file->getSize(); + $this->type = File::type($file->getPathname()); + + return $path; + } + + public function copyFileTo( + string|Filesystem $disk, + string $path, + ): ?string { + $filesystem = $disk instanceof Filesystem ? $disk : Storage::disk($disk); + + $stream = $this->readStream(); + + if (! $stream) { + return null; + } + + $result = $filesystem->writeStream( + $path, + $stream + ); + + return $result ? $path : null; + } + + public function moveFileTo( + string $disk, + string $path, + ): ?string { + + if ($disk === $this->disk && $path === $this->path) { + return $path; + } + + if ($this->copyFileTo($disk, $path)) { + try { + $this->deleteFile(); + } catch (\Throwable $th) { + report($th); + } + + $this->disk = $disk; + $this->path = $path; + $this->save(); + + return $path; + } + + return null; + + } + + public function humanReadableSize( + int $precision = 0, + ?int $maxPrecision = null + ): ?string { + if (! $this->size) { + return null; + } + + return Number::fileSize($this->size, $precision, $maxPrecision); + } + + public function humanReadableDuration( + ?int $syntax = null, + bool $short = false, + int $parts = CarbonInterval::NO_LIMIT, + ?int $options = null + ): ?string { + if (! $this->duration) { + return null; + } + + return CarbonInterval::milliseconds($this->duration)->forHumans($syntax, $short, $parts, $options); + } +} diff --git a/src/Contracts/InteractWithMedia.php b/src/Contracts/InteractWithMedia.php index e02c2d3..2dc9d78 100644 --- a/src/Contracts/InteractWithMedia.php +++ b/src/Contracts/InteractWithMedia.php @@ -3,116 +3,97 @@ namespace Elegantly\Media\Contracts; use Elegantly\Media\MediaCollection; -use Elegantly\Media\MediaConversion; use Elegantly\Media\Models\Media; +use Elegantly\Media\Models\MediaConversion; use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Relations\MorphMany; +use Illuminate\Foundation\Bus\PendingDispatch; use Illuminate\Http\File; use Illuminate\Http\UploadedFile; -use Illuminate\Support\Collection; /** + * @mixin \Illuminate\Database\Eloquent\Model + * * @template TMedia of Media + * + * @property Collection $media */ interface InteractWithMedia { /** - * @return EloquentCollection + * @return MorphMany */ - public function getMedia(?string $collection_name = null, ?string $collection_group = null): EloquentCollection; - - public function hasMedia(?string $collection_name = null, ?string $collection_group = null): bool; + public function media(): MorphMany; /** - * @return ?TMedia - */ - public function getFirstMedia(?string $collection_name = null, ?string $collection_group = null); - - public function getFirstMediaUrl( - ?string $collection_name = null, - ?string $collection_group = null, - ?string $conversion = null, - ): ?string; - - /** - * @return Arrayable|iterable|null + * @return Arrayable|iterable|null */ public function registerMediaCollections(): Arrayable|iterable|null; - /** - * @param TMedia $media - * @return Arrayable|iterable|null - */ - public function registerMediaConversions($media): Arrayable|iterable|null; + public function getMediaCollection(string $collectionName): ?MediaCollection; /** - * @param TMedia $media + * @return Collection */ - public function registerMediaTransformations($media, UploadedFile|File $file): UploadedFile|File; + public function getMedia( + ?string $collectionName = null, + ?string $collectionGroup = null + ): Collection; /** - * @return Collection + * @return ?TMedia */ - public function getMediaCollections(): Collection; + public function getFirstMedia( + ?string $collectionName = null, + ?string $collectionGroup = null + ): ?Media; - public function hasMediaCollection(string $collection_name): bool; - - public function getMediaCollection(string $collection_name): ?MediaCollection; + public function getFirstMediaUrl( + ?string $collectionName = null, + ?string $collectionGroup = null + ): ?string; /** - * @param TMedia $media - * @return Collection + * @param string|resource|UploadedFile|File $file + * @return TMedia */ - public function getMediaConversions($media): Collection; - - public function getMediaConversionKey(string $conversion): string; + public function addMedia( + mixed $file, + ?string $collectionName = null, + ?string $collectionGroup = null, + ?string $name = null, + ?string $disk = null, + ): Media; /** - * @param TMedia $media + * @return $this */ - public function getMediaConversion($media, string $conversion): ?MediaConversion; + public function deleteMedia(int $mediaId): static; /** - * @param int[] $except Array of Media Ids - * @return Collection The deleted media list + * @param array $except + * @return $this */ public function clearMediaCollection( - string $collection_name, - ?string $collection_group = null, - array $except = [] - ): Collection; - - /** - * @return ?TMedia - */ - public function deleteMedia(int $mediaId); - - /** - * @param string|UploadedFile|resource $file - * @return TMedia - */ - public function addMedia( - mixed $file, - ?string $collection_name = null, - ?string $collection_group = null, - ?string $disk = null, - ?string $name = null, - ?string $order = null, - ?array $metadata = null, - ); + string $collectionName, + ?string $collectionGroup = null, + array $except = [], + ): static; /** - * @param TMedia $media + * @param int|TMedia $media */ - public function dispatchConversion($media, string $conversionName): static; + public function dispatchMediaConversion( + int|Media $media, + string $conversion + ): ?PendingDispatch; /** - * @param TMedia $media + * @param int|TMedia $media */ - public function dispatchConversions( - $media, - ?bool $force = false, - ?array $only = null, - ?array $except = null, - ): static; + public function executeMediaConversion( + int|Media $media, + string $conversion + ): ?MediaConversion; } diff --git a/src/Definitions/MediaConversionDefinition.php b/src/Definitions/MediaConversionDefinition.php new file mode 100644 index 0000000..2238700 --- /dev/null +++ b/src/Definitions/MediaConversionDefinition.php @@ -0,0 +1,100 @@ + $conversions */ + $conversions = collect($conversions)->keyBy('name')->toArray(); + $this->conversions = $conversions; + } + + public function handle( + Media $media, + ?MediaConversion $parent, + string $file, + Filesystem $filesystem, + SpatieTemporaryDirectory $temporaryDirectory + ): ?MediaConversion { + $handle = $this->handle; + + return $handle($media, $parent, $file, $filesystem, $temporaryDirectory); + } + + public function shouldExecute(Media $media, ?MediaConversion $parent): bool + { + $when = $this->when; + + if ($when === null) { + return true; + } + + if (is_bool($when)) { + return $when; + } + + return (bool) $when($media, $parent); + } + + public function dispatch(Media $media, ?MediaConversion $parent): PendingDispatch + { + return dispatch(new MediaConversionJob( + media: $media, + conversion: $parent ? "{$parent->conversion_name}.{$this->name}" : $this->name + )); + } + + public function execute(Media $media, ?MediaConversion $parent): ?MediaConversion + { + return TemporaryDirectory::callback(function ($temporaryDirectory) use ($media, $parent) { + + $storage = Storage::build([ + 'driver' => 'local', + 'root' => $temporaryDirectory->path(), + ]); + + $source = $parent ?? $media; + + if (! $source->path) { + return null; + } + + $copy = $source->copyFileTo( + disk: $storage, + path: $source->path + ); + + if (! $copy) { + return null; + } + + return $this->handle($media, $parent, $copy, $storage, $temporaryDirectory); + + }); + } +} diff --git a/src/Definitions/MediaConversionImage.php b/src/Definitions/MediaConversionImage.php new file mode 100644 index 0000000..14c47c4 --- /dev/null +++ b/src/Definitions/MediaConversionImage.php @@ -0,0 +1,77 @@ + null, + when: $when, + immediate: $immediate, + queued: $queued, + queue: $queue, + conversions: $conversions + ); + } + + public function shouldExecute(Media $media, ?MediaConversion $parent): bool + { + if ($this->when !== null) { + return parent::shouldExecute($media, $parent); + } + + return ($parent ?? $media)->type === MediaType::Image; + } + + public function handle( + Media $media, + ?MediaConversion $parent, + string $file, + Filesystem $filesystem, + SpatieTemporaryDirectory $temporaryDirectory + ): ?MediaConversion { + + $fileName = $this->fileName ?? "{$media->name}.jpg"; + + Image::load($filesystem->path($file)) + ->fit($this->fit, $this->width, $this->height) + ->optimize($this->optimizerChain) + ->save($filesystem->path($fileName)); + + return $media->addConversion( + file: $filesystem->path($fileName), + conversionName: $this->name, + parent: $parent, + ); + + } +} diff --git a/src/Definitions/MediaConversionPoster.php b/src/Definitions/MediaConversionPoster.php new file mode 100644 index 0000000..896c15c --- /dev/null +++ b/src/Definitions/MediaConversionPoster.php @@ -0,0 +1,85 @@ + null, + when: $when, + immediate: $immediate, + queued: $queued, + queue: $queue, + conversions: $conversions + ); + } + + public function shouldExecute(Media $media, ?MediaConversion $parent): bool + { + if ($this->when !== null) { + return parent::shouldExecute($media, $parent); + } + + return ($parent ?? $media)->type === MediaType::Video; + } + + public function handle( + Media $media, + ?MediaConversion $parent, + string $file, + Filesystem $filesystem, + SpatieTemporaryDirectory $temporaryDirectory + ): ?MediaConversion { + + $fileName = $this->fileName ?? "{$media->name}.jpg"; + + FFMpeg::fromFilesystem($filesystem) + ->open($file) + ->getFrameFromSeconds($this->seconds) + ->export() + ->save($fileName); + + Image::load($filesystem->path($fileName)) + ->fit($this->fit, $this->width, $this->height) + ->optimize($this->optimizerChain) + ->save(); + + return $media->addConversion( + file: $filesystem->path($fileName), + conversionName: $this->name, + parent: $parent, + ); + + } +} diff --git a/src/Definitions/MediaConversionVideo.php b/src/Definitions/MediaConversionVideo.php new file mode 100644 index 0000000..9df4c2c --- /dev/null +++ b/src/Definitions/MediaConversionVideo.php @@ -0,0 +1,104 @@ + null, + when: $when, + immediate: $immediate, + queued: $queued, + queue: $queue, + conversions: $conversions + ); + } + + public function shouldExecute(Media $media, ?MediaConversion $parent): bool + { + if ($this->when !== null) { + return parent::shouldExecute($media, $parent); + } + + return ($parent ?? $media)->type === MediaType::Video; + } + + public function handle( + Media $media, + ?MediaConversion $parent, + string $file, + Filesystem $filesystem, + SpatieTemporaryDirectory $temporaryDirectory + ): ?MediaConversion { + + $fileName = $this->fileName ?? "{$media->name}.mp4"; + + $source = $parent ?? $media; + + $ratio = new AspectRatio($source->aspect_ratio); + + $width = $this->width; + $height = $this->height; + + if ($width && ! $height) { + $height = $ratio->calculateHeight($width); + } elseif ($height && ! $width) { + $width = $ratio->calculateWidth($height); + } + + $ffmpeg = FFMpeg::fromFilesystem($filesystem) + ->open($file) + ->export() + ->inFormat($this->format); + + if ($width && $height) { + $ffmpeg->resize( + $width, + $height, + $this->fitMethod, + $this->forceStandards + ); + } + + $ffmpeg->save($fileName); + + return $media->addConversion( + file: $filesystem->path($fileName), + conversionName: $this->name, + parent: $parent, + ); + + } +} diff --git a/src/Enums/MediaType.php b/src/Enums/MediaType.php index 55f92c0..7f68509 100644 --- a/src/Enums/MediaType.php +++ b/src/Enums/MediaType.php @@ -40,7 +40,7 @@ public static function tryFromMimeType(string $mimeType): self */ public static function tryFromStreams(string $path): self { - $type = self::tryFromMimeType(File::mimeType($path)); + $type = self::tryFromMimeType(File::mimeType($path) ?? ''); if ( $type === self::Video || diff --git a/src/Events/MediaFileStoredEvent.php b/src/Events/MediaFileStoredEvent.php index dbb81d1..57fba49 100644 --- a/src/Events/MediaFileStoredEvent.php +++ b/src/Events/MediaFileStoredEvent.php @@ -3,22 +3,21 @@ namespace Elegantly\Media\Events; use Elegantly\Media\Models\Media; +use Elegantly\Media\Models\MediaConversion; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; /** - * Disptached when any new file + * Disptached when a new file is strored */ class MediaFileStoredEvent { use Dispatchable, SerializesModels; /** - * Create a new event instance. - * * @return void */ - public function __construct(public Media $media, public string $path) + public function __construct(public Media|MediaConversion $media) { // } diff --git a/src/FileDownloaders/FileDownloader.php b/src/FileDownloaders/FileDownloader.php index cc52484..d6619ce 100644 --- a/src/FileDownloaders/FileDownloader.php +++ b/src/FileDownloaders/FileDownloader.php @@ -4,12 +4,14 @@ use Elegantly\Media\Helpers\File; use Exception; -use Spatie\TemporaryDirectory\TemporaryDirectory; +use Illuminate\Support\Facades\Storage; class FileDownloader { - public static function getTemporaryFile(string $url, ?TemporaryDirectory $temporaryDirectory = null): string - { + public static function fromUrl( + string $url, + string $destination, + ): string { $context = stream_context_create([ 'http' => [ 'header' => 'User-Agent: Elegantly laravel-media package', @@ -20,12 +22,7 @@ public static function getTemporaryFile(string $url, ?TemporaryDirectory $tempor throw new Exception("Can't reach the url: {$url}"); } - $temporaryDirectory ??= (new TemporaryDirectory) - ->location(storage_path('media-tmp')) - ->deleteWhenDestroyed() - ->create(); - - $path = tempnam($temporaryDirectory->path(), 'media-'); + $path = tempnam($destination, 'media-'); file_put_contents($path, $stream); @@ -41,4 +38,45 @@ public static function getTemporaryFile(string $url, ?TemporaryDirectory $tempor return $path; } + + /** + * @param resource $resource + */ + public static function fromResource( + $resource, + string $destination, + ): string { + + $path = tempnam($destination, 'media-'); + + $storage = Storage::build([ + 'driver' => 'local', + 'root' => $destination, + ]); + + $storage->writeStream($path, $resource); + + return $path; + } + + /** + * @param resource|string $file + */ + public static function download( + $file, + string $destination + ): string { + if (is_string($file)) { + return static::fromUrl( + url: $file, + destination: $destination + ); + } + + return static::fromResource( + resource: $file, + destination: $destination + ); + + } } diff --git a/src/Helpers/File.php b/src/Helpers/File.php index 9cd7aba..d8471d9 100644 --- a/src/Helpers/File.php +++ b/src/Helpers/File.php @@ -37,7 +37,7 @@ public static function mimeType(string|HttpFile|UploadedFile $file): ?string return $file->getMimeType(); } - return SupportFile::mimeType($file); + return SupportFile::mimeType($file) ?: null; } public static function extension(string|HttpFile|UploadedFile $file): ?string diff --git a/src/Jobs/DeleteModelMediaJob.php b/src/Jobs/DeleteModelMediaJob.php index 195bc2e..a78dcff 100644 --- a/src/Jobs/DeleteModelMediaJob.php +++ b/src/Jobs/DeleteModelMediaJob.php @@ -11,8 +11,9 @@ use Illuminate\Queue\SerializesModels; /** - * Deleting a lot of media can take some time - * In might even fail + * This job will take care of deleting Media associated with models + * Deleting a media can take some time or even fail. + * To prevent failure when a Model is deleted, the media are individually deleted by this job. */ class DeleteModelMediaJob implements ShouldBeUnique, ShouldQueue { @@ -22,26 +23,29 @@ public function __construct(public Media $media) { $this->media = $media->withoutRelations(); - $this->onConnection(config('media.queue_connection')); - $this->onQueue(config('media.queue')); + /** @var ?string $connection */ + $connection = config('media.queue_connection'); + /** @var ?string $queue */ + $queue = config('media.queue'); + + $this->onConnection($connection); + $this->onQueue($queue); } - public function uniqueId() + public function uniqueId(): string { return (string) $this->media->id; } - public function handle() + public function handle(): void { $this->media->delete(); } /** - * Get the tags that should be assigned to the job. - * - * @return array + * @return array */ - public function tags() + public function tags(): array { return [ 'media', diff --git a/src/Jobs/MediaConversionJob.php b/src/Jobs/MediaConversionJob.php index 5dbec69..b035d6a 100644 --- a/src/Jobs/MediaConversionJob.php +++ b/src/Jobs/MediaConversionJob.php @@ -2,178 +2,61 @@ namespace Elegantly\Media\Jobs; -use Elegantly\Media\Casts\GeneratedConversion; -use Elegantly\Media\MediaConversion; use Elegantly\Media\Models\Media; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Storage; -use Spatie\TemporaryDirectory\TemporaryDirectory; class MediaConversionJob implements ShouldBeUnique, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public TemporaryDirectory $temporaryDirectory; - - public $deleteWhenMissingModels = true; - - /** - * Path of the conversion - */ - public string $conversionName; + public bool $deleteWhenMissingModels = true; public function __construct( public Media $media, - ?string $queue = null, + public string $conversion, ) { - $this->media = $media->withoutRelations(); - $this->onConnection(config('media.queue_connection')); - $this->onQueue($queue ?? config('media.queue')); - } - - public function setConversionName(string $conversionName): static - { - $this->conversionName = $conversionName; + /** @var ?string $connection */ + $connection = config('media.queue_connection'); + /** @var ?string $queue */ + $queue = config('media.queue'); - return $this; + $this->onConnection($connection); + $this->onQueue($queue); } public function uniqueId(): string { - return "{$this->media->id}:{$this->conversionName}"; - } - - /** - * WithoutOverlapping middleware will cost you a try - * If you have 10 conversions for the same media, you should allow at least 10 tries in your job/queue - * Because each processing job will trigger a try to the other pending ones - * ReleaseAfter value qhould always be longer than the time it takes to proceed the job - */ - public function withoutOverlapping(): WithoutOverlapping - { - return (new WithoutOverlapping("media:{$this->media->id}")) - ->shared() - ->releaseAfter(config('media.queue_overlapping.release_after', 60)) - ->expireAfter(config('media.queue_overlapping.expire_after', 60 * 60)); - } - - public function middleware(): array - { - /** - * Skip overlapping job with sync queue or it will prevent jobs to be running - */ - if ($this->job?->getConnectionName() === 'sync') { - return []; - } - - return [ - $this->withoutOverlapping(), - ]; - } - - public function getMediaConversion(): ?MediaConversion - { - return $this->media->model?->getMediaConversion($this->media, $this->conversionName); - } - - public function isNestedConversion(): bool - { - return str($this->conversionName)->contains('.'); - } - - public function getGeneratedParentConversion(): ?GeneratedConversion - { - if ($this->isNestedConversion()) { - return $this->media->getGeneratedConversion( - str($this->conversionName)->beforeLast('.')->value() - ); - } - - return null; - } - - public function getTemporaryDisk(): \Illuminate\Contracts\Filesystem\Filesystem - { - return Storage::build([ - 'driver' => 'local', - 'root' => $this->temporaryDirectory->path(), - ]); - } - - public function makeTemporaryFileCopy(): string|false - { - if ($this->isNestedConversion()) { - return $this->getGeneratedParentConversion()->makeTemporaryFileCopy($this->temporaryDirectory); - } - - return $this->media->makeTemporaryFileCopy($this->temporaryDirectory); + return "{$this->media->id}:{$this->conversion}"; } public function handle(): void { - $this->init(); - - try { - $this->run(); - } catch (\Throwable $th) { - - $this->temporaryDirectory->delete(); - - throw $th; + if ( + $this->media->model && + $this->media->model->getMediaCollection($this->media->collection_name) + ) { + $this->media->model->executeMediaConversion($this->media, $this->conversion); + } else { + $this->media->executeConversion($this->conversion); } - $this->destroy(); - } - - public function init(): void - { - $this->temporaryDirectory = (new TemporaryDirectory) - ->location(storage_path('media-tmp')) - ->deleteWhenDestroyed() - ->create(); - } - - public function run(): void - { - // - } - - /** - * Cleanup temporary files and dispatch children conversions - */ - public function destroy(): void - { - $this->temporaryDirectory->delete(); - - $this->dispatchChildrenConversions(); - } - - protected function dispatchChildrenConversions(): void - { - $childrenConversions = $this->getMediaConversion()->getConversions($this->media); - - foreach ($childrenConversions as $childConversion) { - $childConversion->dispatch( - withConversionName: "{$this->conversionName}.{$childConversion->conversionName}" - ); - } } /** - * Get the tags that should be assigned to the job. + * @return array */ public function tags(): array { return [ 'media', - get_class($this), - $this->conversionName, + $this->media->id, + $this->conversion, + "{$this->media->model_type}:{$this->media->model_id}", ]; } } diff --git a/src/Jobs/OptimizedImageConversionJob.php b/src/Jobs/OptimizedImageConversionJob.php deleted file mode 100644 index 9ae3c18..0000000 --- a/src/Jobs/OptimizedImageConversionJob.php +++ /dev/null @@ -1,47 +0,0 @@ -fileName = $fileName ?? $this->media->file_name; - } - - public function run(): void - { - $temporaryDisk = $this->getTemporaryDisk(); - $path = $this->makeTemporaryFileCopy(); - - $newPath = $temporaryDisk->path($this->fileName); - - Image::load($path) - ->fit($this->fit, $this->width, $this->height) - ->optimize($this->optimizerChain) - ->save($newPath); - - $this->media->storeConversion( - file: $newPath, - conversion: $this->conversionName, - name: File::name($this->fileName) - ); - } -} diff --git a/src/Jobs/OptimizedVideoConversionJob.php b/src/Jobs/OptimizedVideoConversionJob.php deleted file mode 100644 index 649afa2..0000000 --- a/src/Jobs/OptimizedVideoConversionJob.php +++ /dev/null @@ -1,59 +0,0 @@ -media->id}")) - ->shared() - ->releaseAfter(now()->addMinutes(10)) - ->expireAfter(now()->addMinutes(30)); - } - - public function __construct( - public Media $media, - ?string $queue = null, - public ?int $width = null, - public ?int $height = null, - public FormatInterface $format = new X264, - public string $fitMethod = ResizeFilter::RESIZEMODE_FIT, - public bool $forceStandards = false, - ?string $fileName = null, - ) { - parent::__construct($media, $queue); - - $this->fileName = $fileName ?? $this->media->file_name; - } - - public function run(): void - { - $temporaryDisk = $this->getTemporaryDisk(); - $path = $this->makeTemporaryFileCopy(); - - // @phpstan-ignore-next-line - FFMpeg::fromDisk($temporaryDisk) - ->open(File::basename($path)) - ->export() - ->inFormat($this->format) - ->resize($this->width, $this->height, $this->fitMethod, $this->forceStandards) - ->save($this->fileName); - - $this->media->storeConversion( - file: $temporaryDisk->path($this->fileName), - conversion: $this->conversionName, - name: File::name($this->fileName) - ); - } -} diff --git a/src/Jobs/VideoPosterConversionJob.php b/src/Jobs/VideoPosterConversionJob.php deleted file mode 100644 index a3884a4..0000000 --- a/src/Jobs/VideoPosterConversionJob.php +++ /dev/null @@ -1,54 +0,0 @@ -fileName = $fileName ?? "{$this->media->name}.jpg"; - } - - public function run(): void - { - $temporaryDisk = $this->getTemporaryDisk(); - $path = $this->makeTemporaryFileCopy(); - - FFMpeg::fromDisk($temporaryDisk) - ->open(File::basename($path)) - ->getFrameFromSeconds($this->seconds) - ->export() - ->save($this->fileName); - - Image::load($temporaryDisk->path($this->fileName)) - ->fit($this->fit, $this->width, $this->height) - ->optimize($this->optimizerChain) - ->save(); - - $this->media->storeConversion( - file: $temporaryDisk->path($this->fileName), - conversion: $this->conversionName, - name: File::name($this->fileName) - ); - } -} diff --git a/src/MediaCollection.php b/src/MediaCollection.php index cd7a142..8c17159 100644 --- a/src/MediaCollection.php +++ b/src/MediaCollection.php @@ -3,14 +3,18 @@ namespace Elegantly\Media; use Closure; -use Illuminate\Support\Collection; +use Elegantly\Media\Definitions\MediaConversionDefinition; +use Illuminate\Http\File; +use Illuminate\Http\UploadedFile; -/** - * @property Collection $conversions - * @property null|string|(Closure(): string) $fallback - */ class MediaCollection { + /** + * @param null|(string[]) $acceptedMimeTypes + * @param null|string|(Closure(): null|string) $fallback + * @param null|(Closure(UploadedFile|File $file): (UploadedFile|File)) $transform + * @param MediaConversionDefinition[] $conversions + */ public function __construct( public string $name, public ?array $acceptedMimeTypes = null, @@ -18,7 +22,22 @@ public function __construct( public bool $public = false, public ?string $disk = null, public null|string|Closure $fallback = null, + public ?Closure $transform = null, + public array $conversions = [], ) { - // + /** @var array $conversions */ + $conversions = collect($conversions)->keyBy('name')->toArray(); + $this->conversions = $conversions; + } + + public function getConversionDefinition(string $name): ?MediaConversionDefinition + { + /** @var ?MediaConversionDefinition */ + $value = data_get( + target: $this->conversions, + key: str_replace('.', '.conversions.', $name) + ); + + return $value; } } diff --git a/src/MediaConversion.php b/src/MediaConversion.php deleted file mode 100644 index 49c015c..0000000 --- a/src/MediaConversion.php +++ /dev/null @@ -1,69 +0,0 @@ -|iterable)) $conversions - */ - public function __construct( - public string $conversionName, - protected MediaConversionJob $job, - public bool $sync = false, - public ?Closure $conversions = null, - ) { - // - } - - /** - * @return Collection - */ - public function getConversions(Media $media): Collection - { - $conversions = $this->conversions; - - if ($conversions instanceof Closure) { - $generatedConversion = $media->getGeneratedConversion($this->conversionName); - - if ($generatedConversion) { - $conversions = $conversions($generatedConversion); - } else { - $conversions = []; - } - } - - return collect($conversions)->keyBy('conversionName'); - } - - public function getJob(): MediaConversionJob - { - return $this->job->setConversionName($this->conversionName); - } - - public function dispatch( - ?string $withConversionName = null, - ?bool $sync = null, - ): static { - $job = $this->getJob(); - - if ($withConversionName) { - $job->setConversionName($withConversionName); - } - - if ($sync ?? $this->sync) { - dispatch_sync($job); - } else { - dispatch($job); - } - - return $this; - } -} diff --git a/src/MediaServiceProvider.php b/src/MediaServiceProvider.php index 865ae98..f7bdab0 100644 --- a/src/MediaServiceProvider.php +++ b/src/MediaServiceProvider.php @@ -19,6 +19,8 @@ public function configurePackage(Package $package): void ->name('laravel-media') ->hasConfigFile() ->hasMigration('create_media_table') + ->hasMigration('create_media_conversions_table') + ->hasMigration('migrate_generated_conversions_to_media_conversions_table') ->hasCommand(GenerateMediaConversionsCommand::class) ->hasViews(); } diff --git a/src/MediaZipper.php b/src/MediaZipper.php index 1d22427..9f4a565 100644 --- a/src/MediaZipper.php +++ b/src/MediaZipper.php @@ -3,6 +3,7 @@ namespace Elegantly\Media; use Elegantly\Media\Models\Media; +use Exception; use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Contracts\Support\Responsable; use Illuminate\Support\Collection; @@ -17,6 +18,7 @@ class MediaZipper implements Responsable { /** * @param Collection $media + * @param array $zipStreamOptions */ public function __construct( public Collection $media = new Collection, @@ -26,10 +28,17 @@ public function __construct( $this->zipStreamOptions['outputName'] = $fileName; } + /** + * @param array $options writeStream options + */ public function toFile(Filesystem $storage, string $path, array $options = []): string|false { $temporaryStream = fopen('php://memory', 'w+'); + if ($temporaryStream === false) { + throw new Exception('PHP Stream creation failed.'); + } + $zip = $this->getZipStream([ 'outputStream' => $temporaryStream, ]); @@ -45,8 +54,12 @@ public function toFile(Filesystem $storage, string $path, array $options = []): return $success ? $path : false; } + /** + * @param array $options zipStreamOptions options + */ public function getZipStream(array $options = []): ZipStream { + // @phpstan-ignore-next-line $zip = new ZipStream(...array_merge( $this->zipStreamOptions, $options @@ -55,6 +68,10 @@ public function getZipStream(array $options = []): ZipStream foreach ($this->media as $index => $item) { $stream = $item->readStream(); + if ($stream === null) { + throw new Exception("[Media:{$item->id}] Can't read stream at {$item->path} and disk {$item->disk}."); + } + $zip->addFileFromStream( fileName: "{$index}_{$item->file_name}", stream: $stream, @@ -71,7 +88,10 @@ public function getZipStream(array $options = []): ZipStream public function getSize(): int { - return (int) $this->media->sum('size'); + /** @var int $value */ + $value = $this->media->sum('size'); + + return (int) $value; } public function toResponse($request): StreamedResponse diff --git a/src/Models/Media.php b/src/Models/Media.php index 45f96f8..041e653 100644 --- a/src/Models/Media.php +++ b/src/Models/Media.php @@ -2,29 +2,31 @@ namespace Elegantly\Media\Models; +use Carbon\Carbon; use Closure; -use Elegantly\Media\Casts\GeneratedConversion; -use Elegantly\Media\Casts\GeneratedConversions; +use Elegantly\Media\Concerns\InteractWithFiles; use Elegantly\Media\Contracts\InteractWithMedia; +use Elegantly\Media\Database\Factories\MediaFactory; +use Elegantly\Media\Definitions\MediaConversionDefinition; use Elegantly\Media\Enums\MediaType; use Elegantly\Media\Events\MediaFileStoredEvent; use Elegantly\Media\FileDownloaders\FileDownloader; use Elegantly\Media\Helpers\File; +use Elegantly\Media\TemporaryDirectory; use Elegantly\Media\Traits\HasUuid; -use Elegantly\Media\Traits\InteractsWithMediaFiles; +use Exception; use Illuminate\Database\Eloquent\Casts\ArrayObject; use Illuminate\Database\Eloquent\Casts\AsArrayObject; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Collection as EloquentCollection; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Foundation\Bus\PendingDispatch; use Illuminate\Http\File as HttpFile; use Illuminate\Http\UploadedFile; -use Illuminate\Support\Arr; -use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; -use Spatie\TemporaryDirectory\TemporaryDirectory; /** * @property int $id @@ -32,6 +34,7 @@ * @property string $collection_name * @property ?string $collection_group * @property ?MediaType $type + * @property ?string $disk * @property ?string $path * @property ?string $name * @property ?string $file_name @@ -44,17 +47,22 @@ * @property ?int $size * @property ?int $order_column * @property ?float $duration - * @property ?Collection $generated_conversions * @property ?ArrayObject $metadata * @property ?InteractWithMedia $model * @property ?string $model_type * @property ?int $model_id + * @property EloquentCollection $conversions + * @property Carbon $created_at + * @property Carbon $updated_at * @property-read ?string $url */ class Media extends Model { + /** @use HasFactory */ + use HasFactory; + use HasUuid; - use InteractsWithMediaFiles; + use InteractWithFiles; /** * @var array @@ -69,509 +77,375 @@ class Media extends Model protected $casts = [ 'type' => MediaType::class, 'metadata' => AsArrayObject::class, - 'generated_conversions' => GeneratedConversions::class, ]; public static function booted() { static::deleting(function (Media $media) { - $media->generated_conversions - ?->keys() - ->each(function (string $conversion) use ($media) { - $media->deleteGeneratedConversionFiles($conversion); - }); - $media->deleteMediaFiles(); - }); - } + $media->conversions->each(fn ($conversion) => $conversion->delete()); - public function model(): MorphTo - { - return $this->morphTo(); + $media->deleteFile(); + }); } + /** + * @return Attribute + */ protected function url(): Attribute { return Attribute::get(fn () => $this->getUrl()); } - public function makeGeneratedConversionKey(string $conversion): string - { - return str_replace('.', '.generated_conversions.', $conversion); - } - /** - * Retreive a conversion or nested conversion - * Ex: $media->getGeneratedConversion('poster.480p') + * @return MorphTo */ - public function getGeneratedConversion(string $conversion, ?string $state = null): ?GeneratedConversion - { - $generatedConversion = data_get( - $this->generated_conversions, - $this->makeGeneratedConversionKey($conversion) - ); - - if ($state) { - return $generatedConversion?->state === $state ? $generatedConversion : null; - } - - return $generatedConversion; - } - - public function hasGeneratedConversion(string $conversion, ?string $state = null): bool + public function model(): MorphTo { - return (bool) $this->getGeneratedConversion($conversion, $state); + return $this->morphTo(); } /** - * Generate the default base path for storing files - * uuid/ - * files - * /generated_conversions - * /conversionName - * files + * @return HasMany */ - public function makePath( - ?string $conversion = null, - ?string $fileName = null - ): string { - $prefix = config('media.generated_path_prefix', ''); - - $root = Str::of($prefix) - ->when($prefix, fn ($string) => $string->finish('/')) - ->append($this->uuid) - ->finish('/'); - - if ($conversion) { - return $root - ->append('generated_conversions/') - ->append(str_replace('.', '/', $this->makeGeneratedConversionKey($conversion))) - ->finish('/') - ->append($fileName ?? ''); - } - - return $root->append($fileName ?? ''); - } - - public function putGeneratedConversion(string $conversion, GeneratedConversion $generatedConversion): static + public function conversions(): HasMany { - $genealogy = explode('.', $conversion); - - if (count($genealogy) > 1) { - $child = Arr::last($genealogy); - $parents = implode('.', array_slice($genealogy, 0, -1)); - $conversion = $this->getGeneratedConversion($parents); - $conversion->generated_conversions->put($child, $generatedConversion); - } else { - $this->generated_conversions->put($conversion, $generatedConversion); - } - - return $this; + return $this->hasMany(MediaConversion::class); } - public function forgetGeneratedConversion(string $conversion): static - { - $genealogy = explode('.', $conversion); + // Storing File ---------------------------------------------------------- - if (count($genealogy) > 1) { - $child = Arr::last($genealogy); - $parents = implode('.', array_slice($genealogy, 0, -1)); - $conversion = $this->getGeneratedConversion($parents); - $conversion->generated_conversions->forget($child); - } else { - $this->generated_conversions->forget($conversion); + /** + * @param string|UploadedFile|HttpFile|resource $file + * @param null|(Closure(UploadedFile|HttpFile $file):(UploadedFile|HttpFile)) $before + */ + public function storeFile( + mixed $file, + ?string $destination = null, + ?string $name = null, + ?string $disk = null, + ?Closure $before = null, + ): static { + if ($file instanceof UploadedFile || $file instanceof HttpFile) { + return $this->storeFileFromHttpFile($file, $destination, $name, $disk, $before); } - return $this; - } - - public function extractFileInformation(UploadedFile|HttpFile $file): static - { - $this->mime_type = File::mimeType($file); - $this->extension = File::extension($file); - $this->size = $file->getSize(); - $this->type = File::type($file->getPathname()); - - $dimension = File::dimension($file->getPathname()); - - $this->height = $dimension?->getHeight(); - $this->width = $dimension?->getWidth(); - $this->aspect_ratio = $dimension?->getRatio(forceStandards: false)->getValue(); - $this->duration = File::duration($file->getPathname()); - - return $this; - } - - protected function performMediaTransformations(UploadedFile|HttpFile $file): UploadedFile|HttpFile - { if ( - $this->relationLoaded('model') || - ($this->model_id && $this->model_type) + (is_string($file) && filter_var($file, FILTER_VALIDATE_URL)) || + ! is_string($file) ) { - $file = $this->model->registerMediaTransformations($this, $file); - $this->extractFileInformation($file); // refresh file informations + return TemporaryDirectory::callback(function ($temporaryDirectory) use ($file, $destination, $name, $disk, $before) { + $path = FileDownloader::download( + file: $file, + destination: $temporaryDirectory->path() + ); + + return $this->storeFileFromHttpFile(new HttpFile($path), $destination, $name, $disk, $before); + }); } - return $file; + return $this->storeFileFromHttpFile(new HttpFile($file), $destination, $name, $disk, $before); } + /** + * @param null|(Closure(UploadedFile|HttpFile $file):(UploadedFile|HttpFile)) $before + */ public function storeFileFromHttpFile( UploadedFile|HttpFile $file, - ?string $collection_name = null, - ?string $basePath = null, + ?string $destination = null, ?string $name = null, ?string $disk = null, + ?Closure $before = null, ): static { - $this->collection_name = $collection_name ?? $this->collection_name ?? config('media.default_collection_name'); - $this->disk = $disk ?? $this->disk ?? config('media.disk'); - $this->extractFileInformation($file); + $destination ??= $this->makeFreshPath(); + $name ??= File::name($file) ?? Str::random(6); + $disk ??= $this->disk ?? config()->string('media.disk', config()->string('filesystems.default', 'local')); - $file = $this->performMediaTransformations($file); - - $basePath = Str::finish($basePath ?? $this->makePath(), '/'); + if ($before) { + $file = $before($file); + } - $this->name = Str::limit( - File::sanitizeFilename($name ?? File::name($file)), - 255 - strlen($this->extension ?? '') - strlen($basePath) - 1, // 1 is for the point between the name and the extension - '' + $path = $this->putFile( + disk: $disk, + destination: $destination, + file: $file, + name: $name, ); - $this->file_name = "{$this->name}.{$this->extension}"; - $this->path = $basePath.$this->file_name; - - $path = $this->putFile($file, fileName: $this->file_name); - event(new MediaFileStoredEvent($this, $path)); + if (! $path) { + throw new Exception("Storing Media File '{$file->getPath()}' to disk '{$disk}' at '{$destination}' failed."); + } $this->save(); + event(new MediaFileStoredEvent($this)); + return $this; } - public function storeFileFromUrl( - string $url, - ?string $collection_name = null, - ?string $basePath = null, - ?string $name = null, - ?string $disk = null, - ): static { - - $temporaryDirectory = (new TemporaryDirectory) - ->location(storage_path('media-tmp')) - ->create(); - - $path = FileDownloader::getTemporaryFile($url, $temporaryDirectory); - - $this->storeFileFromHttpFile(new HttpFile($path), $collection_name, $basePath, $name, $disk); + // \ Storing File ---------------------------------------------------------- - $temporaryDirectory->delete(); - - return $this; - } + // Managing Conversions ---------------------------------------------------------- /** - * @param resource $ressource + * @return MediaConversionDefinition[] */ - public function storeFileFromRessource( - $ressource, - ?string $collection_name = null, - ?string $basePath = null, - ?string $name = null, - ?string $disk = null - ): static { - - $temporaryDirectory = (new TemporaryDirectory) - ->location(storage_path('media-tmp')) - ->create(); - - $path = tempnam($temporaryDirectory->path(), 'media-'); - - $storage = Storage::build([ - 'driver' => 'local', - 'root' => $temporaryDirectory->path(), - ]); - - $storage->writeStream($path, $ressource); - - $this->storeFileFromHttpFile(new HttpFile($path), $collection_name, $basePath, $name, $disk); - - $temporaryDirectory->delete(); - - return $this; + public function registerConversions(): array + { + return []; } /** - * @param string|UploadedFile|HttpFile|resource $file - * @param (string|UploadedFile|HttpFile)[] $otherFiles any other file to store in the same directory + * Retreive conversions defined in both the Media and the Model MediaCollection + * Model's MediaCollection definitions override the Media's definitions + * + * @return array */ - public function storeFile( - mixed $file, - ?string $collection_name = null, - ?string $basePath = null, - ?string $name = null, - ?string $disk = null, - array $otherFiles = [] - ): static { - if ($file instanceof UploadedFile || $file instanceof HttpFile) { - $this->storeFileFromHttpFile($file, $collection_name, $basePath, $name, $disk); - } elseif (filter_var($file, FILTER_VALIDATE_URL)) { - $this->storeFileFromUrl($file, $collection_name, $basePath, $name, $disk); - } elseif (is_resource($file)) { - $this->storeFileFromRessource($file, $collection_name, $basePath, $name, $disk); - } else { - $this->storeFileFromHttpFile(new HttpFile($file), $collection_name, $basePath, $name, $disk); - } + public function getConversionsDefinitions(): array + { + $conversions = collect($this->registerConversions()); - foreach ($otherFiles as $otherFile) { - $path = $this->putFile($otherFile); - event(new MediaFileStoredEvent($this, $path)); + if ( + $this->model && + $collection = $this->model->getMediaCollection($this->collection_name) + ) { + $conversions->push(...array_values($collection->conversions)); } - return $this; - } + /** @var array */ + $value = $conversions->keyBy('name')->toArray(); - /** - * @param (string|UploadedFile|HttpFile)[] $otherFiles any other file to store in the same directory - */ - public function storeConversion( - string|UploadedFile|HttpFile $file, - string $conversion, - ?string $name = null, - ?string $basePath = null, - string $state = 'success', - array $otherFiles = [] - ): GeneratedConversion { - - if ($file instanceof UploadedFile || $file instanceof HttpFile) { - $generatedConversion = $this->storeConversionFromHttpFile($file, $conversion, $name, $basePath, $state); - } elseif (filter_var($file, FILTER_VALIDATE_URL)) { - $generatedConversion = $this->storeConversionFromUrl($file, $conversion, $name, $basePath, $state); - } else { - $generatedConversion = $this->storeConversionFromHttpFile(new HttpFile($file), $conversion, $name, $basePath, $state); - } + return $value; + } - foreach ($otherFiles as $otherFile) { - $path = $this->putFile($otherFile); - event(new MediaFileStoredEvent($this, $path)); - } + public function getConversionDefinition(string $name): ?MediaConversionDefinition + { + /** @var ?MediaConversionDefinition $value */ + $value = data_get( + target: $this->getConversionsDefinitions(), + key: str_replace('.', '.conversions.', $name) + ); - return $generatedConversion; + return $value; } - public function storeConversionFromUrl( - string $url, - string $conversion, - ?string $name = null, - ?string $basePath = null, - string $state = 'success', - ): GeneratedConversion { - $temporaryDirectory = (new TemporaryDirectory) - ->location(storage_path('media-tmp')) - ->create(); + public function dispatchConversion(string $conversion): ?PendingDispatch + { - $path = FileDownloader::getTemporaryFile($url, $temporaryDirectory); + $definition = $this->getConversionDefinition($conversion); - $generatedConversion = $this->storeConversionFromHttpFile(new HttpFile($path), $conversion, $name, $basePath, $state); + if (! $definition) { + return null; + } - $temporaryDirectory->delete(); + return $definition->dispatch($this, $this->getParentConversion($conversion)); - return $generatedConversion; } - public function storeConversionFromHttpFile( - UploadedFile|HttpFile $file, - string $conversion, - ?string $name = null, - ?string $basePath = null, - string $state = 'success', - ): GeneratedConversion { - $name = File::sanitizeFilename($name ?? File::name($file->getPathname())); - - $extension = File::extension($file); - $file_name = "{$name}.{$extension}"; - $mime_type = File::mimeType($file); - $type = File::type($file->getPathname()); - $dimension = File::dimension($file->getPathname()); + public function executeConversion(string $conversion): ?MediaConversion + { - $existingConversion = $this->getGeneratedConversion($conversion); + $definition = $this->getConversionDefinition($conversion); - if ($existingConversion) { - $this->deleteGeneratedConversionFiles($conversion); + if (! $definition) { + return null; } - $generatedConversion = new GeneratedConversion( - name: $name, - extension: $extension, - file_name: $file_name, - path: Str::of($basePath ?? $this->makePath($conversion))->finish('/')->append($file_name), - mime_type: $mime_type, - type: $type, - state: $state, - disk: $this->disk, - height: $dimension?->getHeight(), - width: $dimension?->getWidth(), - aspect_ratio: $dimension?->getRatio(forceStandards: false)->getValue(), - size: $file->getSize(), - duration: File::duration($file->getPathname()), - created_at: $existingConversion?->created_at - ); + return $definition->execute($this, $this->getParentConversion($conversion)); - $this->putGeneratedConversion($conversion, $generatedConversion); + } - $path = $generatedConversion->putFile($file, fileName: $generatedConversion->file_name); - event(new MediaFileStoredEvent($this, $path)); + public function getConversion(string $name): ?MediaConversion + { + return $this->conversions->firstWhere('conversion_name', $name); + } - $this->save(); + public function getParentConversion(string $name): ?MediaConversion + { + if (! str_contains($name, '.')) { + return null; + } - return $generatedConversion; + return $this->getConversion(str($name)->beforeLast('.')); } /** - * @param null|Closure(GeneratedConversion $item):bool $when + * @return EloquentCollection */ - public function moveGeneratedConversion( - string $conversion, - ?string $disk = null, - ?string $path = null, - ?Closure $when = null - ): ?GeneratedConversion { - $generatedConversion = $this->getGeneratedConversion($conversion); + public function getChildrenConversions(string $name): EloquentCollection + { + return $this->conversions->filter(fn ($conversion) => str_starts_with($conversion->conversion_name, "{$name}.")); + } - if (! $generatedConversion) { - return null; - } + /** + * @param string|resource|UploadedFile|HttpFile $file + */ + public function addConversion( + $file, + string $conversionName, + ?MediaConversion $parent = null, + ?string $name = null, + ?string $destination = null, + ?string $disk = null, + ): MediaConversion { - if ($when && ! $when($generatedConversion)) { - return $generatedConversion; + if ( + $parent && + ! str_contains($conversionName, '.') + ) { + $conversionName = "{$parent->conversion_name}.{$conversionName}"; } - if (! $generatedConversion->disk || ! $generatedConversion->path) { - return $generatedConversion; + if ($conversion = $this->getConversion($conversionName)) { + $existingConversion = clone $conversion; + } else { + $existingConversion = null; + $conversion = new MediaConversion; } - $newDisk = $disk ?? $generatedConversion->disk; - $newPath = $path ?? $generatedConversion->path; - - if ( - $newDisk === $generatedConversion->disk && - $newPath === $generatedConversion->path - ) { - return $generatedConversion; - } + $conversion->fill([ + 'conversion_name' => $conversionName, + 'media_id' => $this->id, + 'state' => 'success', + 'state_set_at' => now(), + ]); - $generatedConversion->copyFileTo( - disk: $newDisk, - path: $newPath + $conversion = $conversion->storeFile( + file: $file, + destination: $destination ?? $this->makeFreshPath($conversionName), + name: $name, + disk: $disk ?? $this->disk ); - $generatedConversion->deleteFile(); - - $generatedConversion->disk = $newDisk; - $generatedConversion->path = $newPath; + if ($existingConversion) { + $existingConversion->deleteFile(); + $this->deleteChildrenConversion($conversionName); + } elseif ($this->relationLoaded('conversions')) { + $this->conversions->push($conversion); + } - $this->putGeneratedConversion( - $conversion, - $generatedConversion + $this->dispatchConversions( + parent: $conversion, ); - $this->save(); - - return $generatedConversion; + return $conversion; } - public function moveFile( - ?string $disk = null, - ?string $path = null, + /** + * @return $this + */ + public function dispatchConversions( + ?MediaConversion $parent = null ): static { - - if (! $this->disk || ! $this->path) { - return $this; - } - - $newDisk = $disk ?? $this->disk; - $newPath = $path ?? $this->path; - - if ( - $newDisk === $this->disk && - $newPath === $this->path - ) { - return $this; + if ($parent) { + $definitions = $this->getConversionDefinition($parent->conversion_name)?->conversions ?? []; + } else { + $definitions = $this->getConversionsDefinitions(); } - $this->copyFileTo( - disk: $newDisk, - path: $newPath - ); - - $this->deleteFile(); + foreach ($definitions as $definition) { + if (! $definition->immediate) { + continue; + } - $this->disk = $newDisk; - $this->path = $newPath; + if (! $definition->shouldExecute( + media: $this, + parent: $parent + )) { + continue; + } - $this->save(); + if ($definition->queued) { + $definition->dispatch( + media: $this, + parent: $parent, + ); + } else { + $definition->execute( + media: $this, + parent: $parent + ); + } + } return $this; + } /** - * Recursively move generated and nested conversions files to a new disk - * - * @param null|Closure(GeneratedConversion $item):bool $when + * Delete Media Conversions and its derived conversions */ - protected function moveGeneratedConversionToDisk( - string $disk, - string $conversion, - ?Closure $when = null - ): ?GeneratedConversion { - $generatedConversion = $this->moveGeneratedConversion( - conversion: $conversion, - disk: $disk, - when: $when + public function deleteConversion(string $conversionName): static + { + $deleted = $this->conversions + ->filter(function ($conversion) use ($conversionName) { + if ($conversion->conversion_name === $conversionName) { + return true; + } + + return str($conversion->conversion_name)->startsWith("{$conversionName}."); + }) + ->each(fn ($conversion) => $conversion->delete()); + + $this->setRelation( + 'conversions', + $this->conversions->except($deleted->modelKeys()) ); - if (! $generatedConversion) { - return null; - } + return $this; + } - foreach ($generatedConversion->generated_conversions->keys() as $childConversionName) { - $this->moveGeneratedConversionToDisk( - disk: $disk, - conversion: "{$conversion}.{$childConversionName}", - when: $when, - ); - } + public function deleteChildrenConversion(string $conversionName): static + { + $deleted = $this->conversions + ->filter(function ($conversion) use ($conversionName) { + return str($conversion->conversion_name)->startsWith("{$conversionName}."); + }) + ->each(fn ($conversion) => $conversion->delete()); + + $this->setRelation( + 'conversions', + $this->conversions->except($deleted->modelKeys()) + ); - return $generatedConversion; + return $this; } + // \ Managing Conversions ---------------------------------------------------------- + /** - * @param null|Closure(GeneratedConversion|static $item):bool $when + * Generate the default base path for storing files + * uuid/ + * files + * /conversions + * /conversionName + * files */ - public function moveToDisk( - string $disk, - ?Closure $when = null - ): static { + public function makeFreshPath( + ?string $conversion = null, + ?string $fileName = null + ): string { + $prefix = config()->string('media.generated_path_prefix', ''); - if ($when && ! $when($this)) { - return $this; - } + $root = Str::of($prefix) + ->when($prefix, fn ($string) => $string->finish('/')) + ->append($this->uuid) + ->finish('/'); - if ($this->generated_conversions) { - foreach ($this->generated_conversions->keys() as $conversionName) { - $this->moveGeneratedConversionToDisk( - disk: $disk, - conversion: $conversionName, - when: $when - ); - } + if ($conversion) { + return $root + ->append('conversions/') + ->append(str_replace('.', '/conversions/', $conversion)) + ->finish('/') + ->append($fileName ?? ''); } - return $this->moveFile( - disk: $disk - ); + return $root->append($fileName ?? ''); } /** + * @param array $keys * @param null|(Closure(null|int $previous): int) $sequence * @return EloquentCollection */ @@ -600,73 +474,13 @@ public static function reorder(array $keys, ?Closure $sequence = null, string $u return $models; } - public function deleteGeneratedConversion(string $conversion): ?GeneratedConversion - { - $generatedConversion = $this->getGeneratedConversion($conversion); - - if (! $generatedConversion) { - return null; - } - - $this - ->deleteGeneratedConversionFiles($conversion) - ->forgetGeneratedConversion($conversion) - ->save(); - - return $generatedConversion; - } - - public function deleteGeneratedConversions(): static - { - $this->generated_conversions - ?->keys() - ->each(function (string $conversion) { - $this->deleteGeneratedConversionFiles($conversion); - }); - - $this->generated_conversions = collect(); - $this->save(); - - return $this; - } - - /** - * You can override this function to customize how files are deleted - */ - public function deleteGeneratedConversionFiles(string $conversion): static - { - $generatedConversion = $this->getGeneratedConversion($conversion); - - if (! $generatedConversion) { - return $this; - } - - $generatedConversion->generated_conversions - ->keys() - ->each(function (string $childConversion) use ($conversion) { - $this->deleteGeneratedConversionFiles("{$conversion}.{$childConversion}"); - }); - - $generatedConversion->deleteFile(); - - return $this; - } - - /** - * You can override this function to customize how files are deleted - */ - protected function deleteMediaFiles(): static - { - $this->deleteFile(); - - return $this; - } - // Attributes Getters ---------------------------------------------------------------------- /** * Retreive the path of a conversion or nested conversion * Ex: $media->getPath('poster.480p') + * + * @param null|bool|string|array $fallback */ public function getPath( ?string $conversion = null, @@ -675,7 +489,7 @@ public function getPath( $path = null; if ($conversion) { - $path = $this->getGeneratedConversion($conversion)?->path; + $path = $this->getConversion($conversion)?->path; } elseif ($this->path) { $path = $this->path; } @@ -703,6 +517,7 @@ public function getPath( * Ex: $media->getUrl('poster.480p') * * @param null|bool|string|array $fallback + * @param null|array $parameters */ public function getUrl( ?string $conversion = null, @@ -712,7 +527,7 @@ public function getUrl( $url = null; if ($conversion) { - $url = $this->getGeneratedConversion($conversion)?->getUrl(); + $url = $this->getConversion($conversion)?->getUrl(); } elseif ($this->path) { /** @var null|string $url */ $url = $this->getDisk()?->url($this->path); @@ -750,6 +565,8 @@ public function getUrl( * Ex: $media->getTemporaryUrl('poster.480p', now()->addHour()) * * @param null|bool|string|array $fallback + * @param array $options + * @param null|array $parameters */ public function getTemporaryUrl( \DateTimeInterface $expiration, @@ -762,7 +579,7 @@ public function getTemporaryUrl( $url = null; if ($conversion) { - $url = $this->getGeneratedConversion($conversion)?->getTemporaryUrl($expiration, $options); + $url = $this->getConversion($conversion)?->getTemporaryUrl($expiration, $options); } elseif ($this->path) { /** @var null|string $url */ $url = $this->getDisk()?->temporaryUrl($this->path, $expiration, $options); @@ -801,14 +618,17 @@ public function getTemporaryUrl( return null; } + /** + * @param null|bool|string|int|array $fallback + */ public function getWidth( ?string $conversion = null, - null|bool|string|array|int $fallback = null, + null|bool|string|int|array $fallback = null, ): ?int { $width = null; if ($conversion) { - $width = $this->getGeneratedConversion($conversion)?->width; + $width = $this->getConversion($conversion)?->width; } else { $width = $this->width; } @@ -833,14 +653,17 @@ public function getWidth( return null; } + /** + * @param null|bool|string|int|array $fallback + */ public function getHeight( ?string $conversion = null, - null|bool|string|array|int $fallback = null, + null|bool|string|int|array $fallback = null, ): ?int { $height = null; if ($conversion) { - $height = $this->getGeneratedConversion($conversion)?->height; + $height = $this->getConversion($conversion)?->height; } else { $height = $this->height; } @@ -865,6 +688,9 @@ public function getHeight( return null; } + /** + * @param null|bool|string|array $fallback + */ public function getName( ?string $conversion = null, null|bool|string|array $fallback = null, @@ -872,7 +698,7 @@ public function getName( $name = null; if ($conversion) { - $name = $this->getGeneratedConversion($conversion)?->name; + $name = $this->getConversion($conversion)?->name; } else { $name = $this->name; } @@ -895,6 +721,9 @@ public function getName( return null; } + /** + * @param null|bool|string|array $fallback + */ public function getFileName( ?string $conversion = null, null|bool|string|array $fallback = null, @@ -902,7 +731,7 @@ public function getFileName( $fileName = null; if ($conversion) { - $fileName = $this->getGeneratedConversion($conversion)?->file_name; + $fileName = $this->getConversion($conversion)?->file_name; } else { $fileName = $this->file_name; } @@ -925,14 +754,17 @@ public function getFileName( return null; } + /** + * @param null|bool|string|int|array $fallback + */ public function getSize( ?string $conversion = null, - null|bool|string|array|int $fallback = null, + null|bool|string|int|array $fallback = null, ): ?int { $size = null; if ($conversion) { - $size = $this->getGeneratedConversion($conversion)?->size; + $size = $this->getConversion($conversion)?->size; } else { $size = $this->size; } @@ -957,14 +789,17 @@ public function getSize( return null; } + /** + * @param null|bool|string|float|array $fallback + */ public function getAspectRatio( ?string $conversion = null, - null|bool|string|array|float $fallback = null, + null|bool|string|float|array $fallback = null, ): ?float { $aspectRatio = null; if ($conversion) { - $aspectRatio = $this->getGeneratedConversion($conversion)?->aspect_ratio; + $aspectRatio = $this->getConversion($conversion)?->aspect_ratio; } else { $aspectRatio = $this->aspect_ratio; } @@ -989,6 +824,9 @@ public function getAspectRatio( return null; } + /** + * @param null|bool|string|array $fallback + */ public function getMimeType( ?string $conversion = null, null|bool|string|array $fallback = null, @@ -996,7 +834,7 @@ public function getMimeType( $mimeType = null; if ($conversion) { - $mimeType = $this->getGeneratedConversion($conversion)?->mime_type; + $mimeType = $this->getConversion($conversion)?->mime_type; } else { $mimeType = $this->mime_type; } diff --git a/src/Models/MediaConversion.php b/src/Models/MediaConversion.php new file mode 100644 index 0000000..3e7f45a --- /dev/null +++ b/src/Models/MediaConversion.php @@ -0,0 +1,156 @@ + */ + use HasFactory; + + use HasUuid; + use InteractWithFiles; + + /** + * @var array + */ + protected $guarded = []; + + protected $appends = ['url']; + + /** + * @var array + */ + protected $casts = [ + 'type' => MediaType::class, + 'metadata' => AsArrayObject::class, + 'state_set_at' => 'datetime', + ]; + + public static function booted() + { + static::deleting(function (MediaConversion $conversion) { + $conversion->deleteFile(); + }); + } + + /** + * @return BelongsTo + */ + public function media(): BelongsTo + { + return $this->belongsTo(Media::class); + } + + /** + * @return Attribute + */ + public function url(): Attribute + { + return Attribute::get(fn () => $this->getUrl()); + } + + /** + * @param string|UploadedFile|HttpFile|resource $file + */ + public function storeFile( + mixed $file, + string $destination, + ?string $name = null, + ?string $disk = null, + ): static { + if ($file instanceof UploadedFile || $file instanceof HttpFile) { + return $this->storeFileFromHttpFile($file, $destination, $name, $disk); + } + + if ( + (is_string($file) && filter_var($file, FILTER_VALIDATE_URL)) || + ! is_string($file) + ) { + return TemporaryDirectory::callback(function ($temporaryDirectory) use ($file, $destination, $name, $disk) { + $path = FileDownloader::download( + file: $file, + destination: $temporaryDirectory->path() + ); + + return $this->storeFileFromHttpFile(new HttpFile($path), $destination, $name, $disk); + }); + } + + return $this->storeFileFromHttpFile(new HttpFile($file), $destination, $name, $disk); + } + + public function storeFileFromHttpFile( + UploadedFile|HttpFile $file, + string $destination, + ?string $name = null, + ?string $disk = null, + ): static { + + $name ??= File::name($file) ?? Str::random(6); + $disk ??= $this->disk ?? config()->string('media.disk'); + + $path = $this->putFile( + disk: $disk, + destination: $destination, + file: $file, + name: $name, + ); + + if (! $path) { + throw new Exception("Storing Media Conversion File '{$file->getPath()}' to disk '{$disk}' at '{$destination}' failed."); + } + + $this->save(); + + event(new MediaFileStoredEvent($this)); + + return $this; + } +} diff --git a/src/Support/ResponsiveImagesConversionsPreset.php b/src/Support/ResponsiveImagesConversionsPreset.php deleted file mode 100644 index 89d1cef..0000000 --- a/src/Support/ResponsiveImagesConversionsPreset.php +++ /dev/null @@ -1,54 +0,0 @@ -name ?? $media->name; - - foreach (static::getWidths($widths, $media, $generatedConversion) as $width) { - - $name = (string) $width; - - $conversions[] = new MediaConversion( - conversionName: $name, - job: new OptimizedImageConversionJob( - media: $media, - queue: $queue, - width: $width, - fileName: "{$baseName}-{$name}.{$extension}" - ) - ); - } - - return $conversions; - } - - public static function getWidths( - array $widths, - Media $media, - ?GeneratedConversion $generatedConversion = null, - ): array { - return array_filter($widths, fn (int $width) => ($generatedConversion?->width ?? $media->width) >= $width); - } -} diff --git a/src/Support/ResponsiveVideosConversionsPreset.php b/src/Support/ResponsiveVideosConversionsPreset.php deleted file mode 100644 index ecb475b..0000000 --- a/src/Support/ResponsiveVideosConversionsPreset.php +++ /dev/null @@ -1,62 +0,0 @@ -name ?? $media->name; - - foreach (static::getWidths($widths, $media, $generatedConversion) as $width) { - - $name = (string) $width; - - $conversions[] = new MediaConversion( - conversionName: $name, - job: new OptimizedVideoConversionJob( - media: $media, - queue: $queue, - width: $width, - format: $format, - fitMethod: $fitMethod, - forceStandards: $forceStandards, - fileName: "{$baseName}-{$name}.mp4" - ) - ); - } - - return $conversions; - } - - public static function getWidths( - array $widths, - Media $media, - ?GeneratedConversion $generatedConversion = null, - ): array { - return array_filter($widths, fn (int $width) => ($generatedConversion?->width ?? $media->width) >= $width); - } -} diff --git a/src/TemporaryDirectory.php b/src/TemporaryDirectory.php new file mode 100644 index 0000000..007d7a8 --- /dev/null +++ b/src/TemporaryDirectory.php @@ -0,0 +1,39 @@ +string('media.temporary_storage_path', 'app/tmp/media')); + + $temporaryDirectory = (new self) + ->location($location) + ->create(); + + try { + $value = $callback($temporaryDirectory); + } catch (\Throwable $th) { + $temporaryDirectory->delete(); + throw $th; + } + + $temporaryDirectory->delete(); + + return $value; + + } +} diff --git a/src/Traits/HasMedia.php b/src/Traits/HasMedia.php deleted file mode 100644 index 432cdeb..0000000 --- a/src/Traits/HasMedia.php +++ /dev/null @@ -1,398 +0,0 @@ - $media ordered by order_column - */ -trait HasMedia -{ - public static function bootHasMedia() - { - static::deleting(function (Model $model) { - - if (! config('media.delete_media_with_model')) { - return true; - } - - $isSoftDeleting = method_exists($model, 'isForceDeleting') && ! $model->isForceDeleting(); - - if ( - $isSoftDeleting && ! config('media.delete_media_with_trashed_model') - ) { - return true; - } - - $job = config('media.delete_media_with_model_job'); - - foreach ($model->media as $media) { - dispatch(new $job($media)); - } - }); - } - - public function media(): MorphMany - { - return $this->morphMany(config('media.model'), 'model') - ->orderByRaw('-order_column DESC') - ->orderBy('id', 'asc'); - } - - /** - * @return EloquentCollection - */ - public function getMedia(?string $collection_name = null, ?string $collection_group = null): EloquentCollection - { - return $this->media - ->when($collection_name, fn (EloquentCollection $collection) => $collection->where('collection_name', $collection_name)) - ->when($collection_group, fn (EloquentCollection $collection) => $collection->where('collection_group', $collection_group)) - ->values(); - } - - public function hasMedia(?string $collection_name = null, ?string $collection_group = null): bool - { - return $this->getMedia($collection_name, $collection_group)->isNotEmpty(); - } - - /** - * @return TMedia - */ - public function getFirstMedia( - ?string $collection_name = null, - ?string $collection_group = null - ) { - return $this->getMedia($collection_name, $collection_group)->first(); - } - - /** - * @param null|bool|string|array $fallback - */ - public function getFirstMediaUrl( - ?string $collection_name = null, - ?string $collection_group = null, - ?string $conversion = null, - null|bool|string|array $fallback = null, - ?array $parameters = null, - ): ?string { - $media = $this->getFirstMedia($collection_name, $collection_group); - - if ($media) { - return $media->getUrl( - conversion: $conversion, - fallback: $fallback, - parameters: $parameters - ); - } - - $collection = $this->getMediaCollection($collection_name); - - return value($collection?->fallback); - } - - /** - * @return Arrayable|iterable|null - */ - public function registerMediaCollections(): Arrayable|iterable|null - { - return []; - } - - /** - * @param TMedia $media - * @return Arrayable|iterable|null - */ - public function registerMediaConversions($media): Arrayable|iterable|null - { - return []; - } - - /** - * @param TMedia $media - */ - public function registerMediaTransformations($media, UploadedFile|File $file): UploadedFile|File - { - return $file; - } - - /** - * @return Collection - */ - public function getMediaCollections(): Collection - { - return collect($this->registerMediaCollections()) - ->push(new MediaCollection( - name: config('media.default_collection_name'), - single: false, - public: false - )) - ->keyBy('name'); - } - - public function getMediaCollection(string $collectionName): ?MediaCollection - { - return $this->getMediaCollections()->get($collectionName); - } - - public function hasMediaCollection(string $collectionName): bool - { - return (bool) $this->getMediaCollection($collectionName); - } - - /** - * @param TMedia $media - * @return Collection - */ - public function getMediaConversions($media): Collection - { - return collect($this->registerMediaConversions($media))->keyBy('conversionName'); - } - - public function getMediaConversionKey(string $conversion): string - { - return str_replace('.', '.conversions.', $conversion); - } - - /** - * @param TMedia $media - */ - public function getMediaConversion($media, string $conversion): ?MediaConversion - { - $conversionsNames = explode('.', $conversion); - - $conversions = $this->getMediaConversions($media); - - return $this->getNestedMediaConversion( - $media, - $conversions->get($conversionsNames[0]), - array_slice($conversionsNames, 1), - ); - } - - /** - * @param TMedia $media - * @param string[] $conversionsNames - */ - protected function getNestedMediaConversion( - $media, - ?MediaConversion $mediaConversion, - array $conversionsNames, - ): ?MediaConversion { - - if (empty($conversionsNames) || ! $mediaConversion) { - return $mediaConversion; - } - - $conversionName = $conversionsNames[0]; - - $conversions = $mediaConversion->getConversions($media); - - return $this->getNestedMediaConversion( - $media, - $conversions->get($conversionName), - array_slice($conversionsNames, 1), - ); - } - - /** - * @param int[] $except Array of Media Ids - * @return Collection The deleted media list - */ - public function clearMediaCollection( - string $collection_name, - ?string $collection_group = null, - array $except = [] - ): Collection { - $media = $this->getMedia($collection_name, $collection_group) - ->except($except) - ->each(function (Media $model) { - $model->delete(); - }); - - $this->setRelation( - 'media', - $this->media->except($media->modelKeys()) - ); - - return $media; - } - - /** - * @return ?TMedia - */ - public function deleteMedia(int $mediaId) - { - $media = $this->media->find($mediaId); - - if (! $media) { - return null; - } - - $media->delete(); - - $this->setRelation( - 'media', - $this->media->except([$mediaId]) - ); - - return $media; - } - - /** - * @param string|UploadedFile|resource $file - * @return TMedia - */ - public function addMedia( - mixed $file, - ?string $collection_name = null, - ?string $collection_group = null, - ?string $disk = null, - ?string $name = null, - ?string $order = null, - ?array $metadata = null, - ) { - $collection_name ??= config('media.default_collection_name'); - - $collection = $this->getMediaCollection($collection_name); - - if (! $collection) { - $class = static::class; - throw new Exception("[Media collection not registered] {$collection_name} is not registered for the model {$class}."); - } - - $model = config('media.model'); - /** @var TMedia $media */ - $media = new $model; - - $media->model()->associate($this); - - $media->collection_group = $collection_group; - $media->order_column = $order; - $media->metadata = $metadata; - - $media->storeFile( - file: $file, - collection_name: $collection_name, - name: $name, - disk: $disk ?? $collection->disk - ); - - if ($this->relationLoaded('media')) { - $this->setRelation( - 'media', - $this->media->push($media->withoutRelations()) - ); - } - - if ($collection->single) { - $this->clearMediaCollection($collection_name, except: [$media->id]); - } - - $this->dispatchConversions($media); - - return $media; - } - - /** - * @param TMedia $media - */ - public function dispatchConversion($media, string $conversionName): static - { - $conversion = $this->getMediaConversion($media, $conversionName); - - if (! $conversion) { - return $this; - } - - $media->deleteGeneratedConversion($conversion->conversionName); - - $media - ->putGeneratedConversion($conversion->conversionName, new GeneratedConversion(state: 'pending')) - ->save(); - - $conversion->dispatch(); - - return $this; - } - - /** - * Dispatch media conversions for a specific media collection - * - * @param bool $sync Overrides Conversion sync attribute - */ - public function dispatchCollectionConversions( - string $collectionName, - ?bool $force = false, - ?array $only = null, - ?array $except = null, - ?bool $sync = null, - ): static { - - foreach ($this->getMedia($collectionName) as $media) { - $this->dispatchConversions( - media: $media, - force: $force, - only: $only, - except: $except, - sync: $sync, - ); - } - - return $this; - } - - /** - * @param TMedia $media - * @param bool $sync Overrides Conversion sync attribute - */ - public function dispatchConversions( - $media, - ?bool $force = false, - ?array $only = null, - ?array $except = null, - ?bool $sync = null, - ): static { - $conversions = $this->getMediaConversions($media) - ->only($only) - ->except($except); - - if (! $force) { - $conversions = $conversions->filter(function (MediaConversion $conversion) use ($media) { - return ! $media->hasGeneratedConversion($conversion->conversionName); - }); - } - - if ($conversions->isEmpty()) { - return $this; - } - - foreach ($conversions as $conversion) { - $media->deleteGeneratedConversionFiles($conversion->conversionName); - $media->putGeneratedConversion($conversion->conversionName, new GeneratedConversion(state: 'pending')); - } - - $media->save(); - - foreach ($conversions as $conversion) { - $conversion->dispatch( - sync: $sync, - ); - } - - return $this; - } -} diff --git a/src/Traits/HasUuid.php b/src/Traits/HasUuid.php index 0b7e221..89342ee 100644 --- a/src/Traits/HasUuid.php +++ b/src/Traits/HasUuid.php @@ -9,7 +9,7 @@ */ trait HasUuid { - public function initializeHasUuid() + public function initializeHasUuid(): void { if (blank($this->uuid)) { $this->uuid = (string) Str::uuid(); diff --git a/src/Traits/InteractsWithMediaFiles.php b/src/Traits/InteractsWithMediaFiles.php index a57dfd6..1d8ed4a 100644 --- a/src/Traits/InteractsWithMediaFiles.php +++ b/src/Traits/InteractsWithMediaFiles.php @@ -40,12 +40,15 @@ public function getUrl(): ?string return $this->getDisk()?->url($this->path); } - public function getTemporaryUrl(\DateTimeInterface $expiration, array $options = []): ?string - { + public function getTemporaryUrl( + \DateTimeInterface $expiration, + array $options = [] + ): ?string { if (! $this->path) { return null; } + // @phpstan-ignore-next-line return $this->getDisk()?->temporaryUrl($this->path, $expiration, $options); } diff --git a/tests/Feature/HasMediaTest.php b/tests/Feature/HasMediaTest.php index e6c3b26..98c549a 100644 --- a/tests/Feature/HasMediaTest.php +++ b/tests/Feature/HasMediaTest.php @@ -1,216 +1,150 @@ getMediaCollections()->toArray())->toHaveKeys(['files', 'avatar', 'fallback']); + $collection = $model->getMediaCollection('single'); + expect($collection)->toBeInstanceOf(MediaCollection::class); + expect($collection->name)->toBe('single'); }); -it('keys media conversion by conversionName', function () { - $model = new TestWithNestedConversions; - - /** @var Media $media */ - $media = MediaFactory::new()->make([ - 'type' => MediaType::Image, - ]); +it('gets the fallback value when no media extist', function () { + $model = new Test; - expect($model->getMediaConversions($media)->toArray())->toHaveKeys(['optimized', '360']); + expect($model->getFirstMediaUrl('fallback'))->toBe('fallback-value'); }); -it('gets the correct media conversion', function () { - $model = new TestWithNestedConversions; - - /** @var Media $media */ - $media = MediaFactory::new()->make([ - 'type' => MediaType::Image, - ]); - - expect($model->getMediaConversion($media, 'optimized')?->conversionName)->toBe('optimized'); - expect($model->getMediaConversion($media, '360')?->conversionName)->toBe('360'); -}); +it('retreives the media url', function () { + Storage::fake('media'); + $model = new Test; + $model->save(); -it('gets the correct nested media conversion', function () { - $model = new TestWithNestedConversions; - - /** @var Media $media */ - $media = MediaFactory::new()->make([ - 'type' => MediaType::Image, - /** - * In order to access the nested '360' conversion, the parent one must be already generated - */ - 'generated_conversions' => [ - 'optimized' => new GeneratedConversion( - name: 'optimized', - file_name: 'optimized.jpg', - ), - ], - ]); + $model->addMedia( + file: UploadedFile::fake()->image('foo.jpg'), + disk: 'media', + collectionName: 'files', + ); - expect($media->getGeneratedConversion('optimized'))->not->toBe(null); + expect($model->getFirstMediaUrl('files'))->not->toBe(null); - expect($model->getMediaConversion($media, 'optimized.webp')?->conversionName)->toBe('webp'); }); -it('creates a media, store files and generate conversions', function () { +it('adds a new media to the default collection', function () { Storage::fake('media'); - $model = new Test; $model->save(); - $file = UploadedFile::fake()->image('foo.jpg'); + $file = UploadedFile::fake()->image('foo.jpg', width: 16, height: 9); $media = $model->addMedia( file: $file, - collection_name: 'files', disk: 'media' ); - $media->refresh(); - - expect($model->getMediaConversions($media)->count())->toBe(1); + expect($media->model_id)->toBe($model->id); + expect($media->model_type)->toBe(get_class($model)); + expect($media->exists)->toBe(true); + expect($media->name)->toBe('foo'); + expect($media->extension)->toBe('jpg'); + expect($media->file_name)->toBe('foo.jpg'); - expect($media->collection_name)->toBe('files'); + expect($media->collection_name)->toBe(config('media.default_collection_name')); + expect($media->collection_group)->toBe(null); Storage::disk('media')->assertExists($media->path); - expect($media->generated_conversions->count())->toBe(1); - - $generatedConversion = $media->getGeneratedConversion('optimized'); + expect($model->media)->toHaveLength(1); - expect($generatedConversion)->not->toBe(null); + $modelMedia = $model->getFirstMedia(); - Storage::disk('media')->assertExists($generatedConversion->path); + expect($modelMedia)->toBeInstanceOf(Media::class); }); -it('generates nested conversions', function () { +it('adds a new media to a collection and group', function () { Storage::fake('media'); - - $model = new TestWithNestedConversions; + $model = new Test; $model->save(); - $file = UploadedFile::fake()->image('foo.jpg'); + $file = UploadedFile::fake()->image('foo.jpg', width: 16, height: 9); $media = $model->addMedia( file: $file, + collectionName: 'files', + collectionGroup: 'group', disk: 'media' ); - $media->refresh(); - expect($media->collection_name)->toBe(config('media.default_collection_name')); - - $mediaConversions = $model->getMediaConversions($media); - - expect($media->generated_conversions->toArray()) - ->toHaveLength($mediaConversions->count()) - ->toHaveKeys($mediaConversions->keys()->toArray()); - - // Parent conversion - $generatedConversion = $media->getGeneratedConversion('optimized'); - - expect($generatedConversion)->toBeInstanceOf(GeneratedConversion::class); - expect($generatedConversion->name)->toBe('optimized'); - - Storage::disk('media')->assertExists($generatedConversion->path); - - expect($generatedConversion->generated_conversions->toArray()) - ->toHaveLength(1) - ->toHaveKeys(['webp']); + Storage::disk('media')->assertExists($media->path); - // Child conversion - $childGeneratedConversion = $media->getGeneratedConversion('optimized.webp'); - expect($childGeneratedConversion)->toBeInstanceOf(GeneratedConversion::class); - expect($childGeneratedConversion->extension)->toBe('webp'); - expect($childGeneratedConversion->name)->toBe('optimized'); + expect($media->model_id)->toBe($model->id); + expect($media->model_type)->toBe(get_class($model)); + expect($media->exists)->toBe(true); + expect($media->collection_name)->toBe('files'); + expect($media->collection_group)->toBe('group'); - Storage::disk('media')->assertExists($childGeneratedConversion->path); -}); + expect($model->media)->toHaveLength(1); -it('gets the fallback value when no media extist', function () { - $model = new Test; + $modelMedia = $model->getFirstMedia(); - expect($model->getFirstMediaUrl('fallback'))->toBe('fallback-value'); + expect($modelMedia)->toBeInstanceOf(Media::class); }); -it('gets the media url when a media exists in a collection', function () { +it('generates conversions and nested conversions when adding media', function () { Storage::fake('media'); - $model = new Test; $model->save(); - $file = UploadedFile::fake()->image('foo.jpg'); - $media = $model->addMedia( - file: $file, - collection_name: 'fallback', + file: $this->getTestFile('videos/horizontal.mp4'), + collectionName: 'conversions', disk: 'media' ); - expect($model->getFirstMedia()->id)->toBe($media->id); - expect($model->getFirstMediaUrl())->toBe($media->getUrl()); + expect( + $media->conversions->pluck('conversion_name')->toArray() + )->toBe([ + 'poster', + 'poster.360', + // 'small' video conversion is queued + ]); + }); -it('adds the new added media to the model relation', function () { +it('deletes old media when adding to single collection', function () { Storage::fake('media'); - $model = new Test; $model->save(); - $model->load('media'); - - expect($model->media)->toHaveLength(0); - - $model->addMedia( + $firstMedia = $model->addMedia( file: UploadedFile::fake()->image('foo.jpg'), - collection_name: 'files', - disk: 'media' + disk: 'media', + collectionName: 'single', ); - expect($model->media)->toHaveLength(1); - - $model->addMedia( - file: UploadedFile::fake()->image('bar.jpg'), - collection_name: 'fallback', - disk: 'media' - ); + Storage::disk('media')->assertExists($firstMedia->path); - expect($model->media)->toHaveLength(2); -}); - -it('removes media from the model when clearing media collection', function () { - Storage::fake('media'); - - $model = new Test; - $model->save(); + expect($model->media)->toHaveLength(1); - $model->addMedia( + $secondMedia = $model->addMedia( file: UploadedFile::fake()->image('foo.jpg'), - collection_name: 'files', - disk: 'media' + disk: 'media', + collectionName: 'single', ); - $model->addMedia( - file: UploadedFile::fake()->image('bar.jpg'), - collection_name: 'fallback', - disk: 'media' - ); + expect($model->media)->toHaveLength(1); - expect($model->media)->toHaveLength(2); - expect($model->getMedia('files'))->toHaveLength(1); + Storage::disk('media')->assertExists($secondMedia->path); - $model->clearMediaCollection('files'); + Storage::disk('media')->assertMissing($firstMedia->path); + expect(Media::query()->find($firstMedia->id))->toBe(null); - expect($model->media)->toHaveLength(1); - expect($model->getMedia('files'))->toHaveLength(0); }); it('deletes media and its files with the model when delete_media_with_model is true', function () { @@ -221,11 +155,8 @@ $model = new Test; $model->save(); - $file = UploadedFile::fake()->image('foo.jpg'); - $media = $model->addMedia( - file: $file, - collection_name: 'fallback', + file: UploadedFile::fake()->image('foo.jpg'), disk: 'media' ); @@ -250,7 +181,6 @@ $media = $model->addMedia( file: $file, - collection_name: 'fallback', disk: 'media' ); @@ -276,7 +206,6 @@ $media = $model->addMedia( file: $file, - collection_name: 'fallback', disk: 'media' ); @@ -302,7 +231,6 @@ $media = $model->addMedia( file: $file, - collection_name: 'fallback', disk: 'media' ); @@ -328,7 +256,6 @@ $media = $model->addMedia( file: $file, - collection_name: 'fallback', disk: 'media' ); diff --git a/tests/Feature/HasMediaTransformationsTest.php b/tests/Feature/HasMediaTransformationsTest.php index af1ee62..c11536b 100644 --- a/tests/Feature/HasMediaTransformationsTest.php +++ b/tests/Feature/HasMediaTransformationsTest.php @@ -1,19 +1,26 @@ save(); - $file = $this->getTestFile('images/800x900.jpg'); + $original = $this->getTestFile('images/800x900.png'); + + $path = Storage::disk('media') + ->putFileAs( + 'copy', + $original, + '800x900.jpg' + ); $media = $model->addMedia( - file: $file, - collection_name: 'avatar', + file: Storage::disk('media')->path($path), + collectionName: 'transform', disk: 'media' ); diff --git a/tests/Feature/InteractWithFilesTest.php b/tests/Feature/InteractWithFilesTest.php new file mode 100644 index 0000000..1cebda4 --- /dev/null +++ b/tests/Feature/InteractWithFilesTest.php @@ -0,0 +1,31 @@ +make(); + + Storage::fake('media'); + + $file = UploadedFile::fake()->image('foo.jpg', width: 16, height: 9); + + $media->storeFile( + file: $file, + disk: 'media' + ); + + Storage::disk('media')->assertExists($media->path); + + Storage::fake('media-copy'); + + $copy = $media->copyFileTo( + disk: 'media-copy', + path: $media->path + ); + + Storage::disk('media-copy')->assertExists($copy); +}); diff --git a/tests/Feature/InteractsWithMediaFilesTest.php b/tests/Feature/InteractsWithMediaFilesTest.php deleted file mode 100644 index 2b0d8d3..0000000 --- a/tests/Feature/InteractsWithMediaFilesTest.php +++ /dev/null @@ -1,234 +0,0 @@ -make(); - - Storage::fake('media'); - - $file = UploadedFile::fake()->image('foo.jpg'); - - $media->storeFile( - file: $file, - disk: 'media' - ); - - expect($media->getDisk()->exists($media->path))->toBe(true); - - $temporaryDirectory = (new TemporaryDirectory) - ->location(storage_path('media-tmp')) - ->create(); - - $path = $media->makeTemporaryFileCopy($temporaryDirectory); - - expect($path)->toBeString(); - - expect(is_file($path))->tobe(true); - - $temporaryDirectory->delete(); - - expect(is_file($path))->tobe(false); -}); - -it('copy the GeneratedConversion file to a temporary directory', function () { - - /** @var Media $media */ - $media = MediaFactory::new()->make(); - - Storage::fake('media'); - - $file = UploadedFile::fake()->image('foo.jpg'); - - $media->storeFile( - file: $file, - disk: 'media' - ); - - $poster = UploadedFile::fake()->image('foo-poster.jpg'); - - $generatedConversion = $media->storeConversion( - file: $poster->getPathname(), - conversion: 'poster', - name: 'avatar-poster' - ); - - expect($generatedConversion->getDisk()->exists($generatedConversion->path))->toBe(true); - - $temporaryDirectory = (new TemporaryDirectory) - ->location(storage_path('media-tmp')) - ->create(); - - $path = $generatedConversion->makeTemporaryFileCopy($temporaryDirectory); - - expect($path)->toBeString(); - - expect(is_file($path))->tobe(true); - - $temporaryDirectory->delete(); - - expect(is_file($path))->tobe(false); -}); - -it('put file to the Media path', function () { - - /** @var Media $media */ - $media = MediaFactory::new()->make(); - - Storage::fake('media'); - - $file = UploadedFile::fake()->image('foo.jpg'); - - $media->storeFile( - file: $file, - disk: 'media' - ); - - $otherFile = UploadedFile::fake()->image('foo-other.jpg'); - - $path = $media->putFile($otherFile); - - Storage::disk('media')->assertExists($path); - - expect(SupportFile::dirname($path))->toBe($media->getDirname()); -}); - -it('put file to the Generated conversion path', function () { - - /** @var Media $media */ - $media = MediaFactory::new()->make(); - - Storage::fake('media'); - - $file = UploadedFile::fake()->image('foo.jpg'); - - $media->storeFile( - file: $file, - disk: 'media' - ); - - $poster = UploadedFile::fake()->image('foo-poster.jpg'); - - $generatedConversion = $media->storeConversion( - file: $poster->getPathname(), - conversion: 'poster', - name: 'avatar-poster' - ); - - $otherFile = UploadedFile::fake()->image('foo-other.jpg'); - - $path = $generatedConversion->putFile($otherFile); - - Storage::disk('media')->assertExists($path); - - expect(SupportFile::dirname($path))->toBe($generatedConversion->getDirname()); -}); - -it('moves the main file to a new path', function () { - - /** @var Media $media */ - $media = MediaFactory::new()->make(); - - Storage::fake('media-from'); - Storage::fake('media-to'); - - $file = UploadedFile::fake()->image('foo.jpg'); - - $media->storeFile( - file: $file, - disk: 'media-from' - ); - - Storage::disk('media-from')->assertExists($media->path); - - $media->moveFile( - disk: 'media-to' - ); - - expect($media->disk)->toBe('media-to'); - - Storage::disk('media-from')->assertMissing($media->path); - Storage::disk('media-to')->assertExists($media->path); -}); - -it('moves a conversion file to a new path', function () { - - /** @var Media $media */ - $media = MediaFactory::new()->make(); - - Storage::fake('media-from'); - Storage::fake('media-to'); - - $file = UploadedFile::fake()->image('foo.jpg'); - - $media->storeFile( - file: $file, - disk: 'media-from' - ); - - $poster = UploadedFile::fake()->image('foo-poster.jpg'); - - $generatedConversion = $media->storeConversion( - file: $poster->getPathname(), - conversion: 'poster', - name: 'avatar-poster' - ); - - Storage::disk('media-from')->assertExists($generatedConversion->path); - - $media->moveGeneratedConversion( - conversion: 'poster', - disk: 'media-to' - ); - - expect($generatedConversion->disk)->toBe('media-to'); - - Storage::disk('media-from')->assertMissing($media->getPath('poster')); - Storage::disk('media-to')->assertExists($media->getPath('poster')); -}); - -it('moves media to a new disk', function () { - - /** @var Media $media */ - $media = MediaFactory::new()->make(); - - Storage::fake('media-from'); - Storage::fake('media-to'); - - $file = UploadedFile::fake()->image('foo.jpg'); - - $media->storeFile( - file: $file, - disk: 'media-from' - ); - - $poster = UploadedFile::fake()->image('foo-poster.jpg'); - - $generatedConversion = $media->storeConversion( - file: $poster->getPathname(), - conversion: 'poster', - name: 'avatar-poster' - ); - - Storage::disk('media-from')->assertExists($media->path); - Storage::disk('media-from')->assertExists($generatedConversion->path); - - $media->moveToDisk( - disk: 'media-to' - ); - - expect($media->disk)->toBe('media-to'); - expect($generatedConversion->disk)->toBe('media-to'); - - Storage::disk('media-from')->assertMissing($media->path); - Storage::disk('media-from')->assertMissing($media->getPath('poster')); - Storage::disk('media-to')->assertExists($media->path); - Storage::disk('media-to')->assertExists($media->getPath('poster')); -}); diff --git a/tests/Feature/MediaCollectionTest.php b/tests/Feature/MediaCollectionTest.php new file mode 100644 index 0000000..9aeb93c --- /dev/null +++ b/tests/Feature/MediaCollectionTest.php @@ -0,0 +1,26 @@ + null, + conversions: [ + new MediaConversionDefinition( + name: '360', + handle: fn () => null, + conversions: [], + ), + ], + ), + ], + ); + + expect($collection->getConversionDefinition('poster')?->name)->tobe('poster'); + expect($collection->getConversionDefinition('poster.360')?->name)->tobe('360'); +}); diff --git a/tests/Feature/MediaConversionTest.php b/tests/Feature/MediaConversionTest.php new file mode 100644 index 0000000..2eaf933 --- /dev/null +++ b/tests/Feature/MediaConversionTest.php @@ -0,0 +1,58 @@ +make([ + 'disk' => 'media', + ]); + + Storage::fake('media'); + + $media->save(); + + $file = UploadedFile::fake()->image('foo.jpg', width: 16, height: 9); + + $conversion = $media->addConversion( + conversionName: 'poster', + file: $file, + name: 'poster', + ); + + Storage::disk('media')->assertExists($conversion->path); + + $conversion->deleteFile(); + + Storage::disk('media')->assertMissing($conversion->path); + +}); + +it('On MediaConversion deletion, it deletes the files', function () { + /** @var Media $media */ + $media = MediaFactory::new()->make([ + 'disk' => 'media', + ]); + + Storage::fake('media'); + + $media->save(); + + $file = UploadedFile::fake()->image('foo.jpg', width: 16, height: 9); + + $conversion = $media->addConversion( + conversionName: 'poster', + file: $file, + name: 'poster', + ); + + Storage::disk('media')->assertExists($conversion->path); + + $conversion->delete(); + + Storage::disk('media')->assertMissing($conversion->path); + +}); diff --git a/tests/Feature/MediaConversionVideoTest.php b/tests/Feature/MediaConversionVideoTest.php new file mode 100644 index 0000000..98255f7 --- /dev/null +++ b/tests/Feature/MediaConversionVideoTest.php @@ -0,0 +1,43 @@ +save(); + + $model->addMedia( + file: $this->getTestFile('videos/horizontal.mp4'), + collectionName: 'conversions', + disk: 'media' + ); + + Queue::assertPushed(MediaConversionJob::class, 1); +}); + +it('generates video conversion when adding media', function () { + Storage::fake('media'); + $model = new Test; + $model->save(); + + $media = $model->addMedia( + file: $this->getTestFile('videos/horizontal.mp4'), + collectionName: 'conversions', + disk: 'media' + ); + + // because some conversions are queued + $media->refresh(); + + $conversion = $media->getConversion('small'); + + expect($conversion)->not->toBe(null); + expect($conversion->width)->toBe(100); + expect($conversion->aspect_ratio)->toBe($media->aspect_ratio); + +}); diff --git a/tests/Feature/MediaTest.php b/tests/Feature/MediaTest.php index 19f73d2..b949e63 100644 --- a/tests/Feature/MediaTest.php +++ b/tests/Feature/MediaTest.php @@ -1,207 +1,44 @@ make(); - - expect($media->getUrl())->toBe('/storage//uuid/empty.jpg'); - expect($media->getUrl( - parameters: [ - 'width' => 200, - 'height' => 200, - ] - ))->toBe('/storage//uuid/empty.jpg?width=200&height=200'); - -}); - -it('retrieve the fallback url', function () { - /** @var Media $media */ - $media = MediaFactory::new()->make(); - - expect($media->getUrl( - conversion: 'poster', - fallback: true, - ))->toBe('/storage//uuid/empty.jpg'); - - expect($media->getUrl( - conversion: 'poster', - fallback: true, - parameters: [ - 'width' => 200, - 'height' => 200, - ] - ))->toBe('/storage//uuid/empty.jpg?width=200&height=200'); - -}); - -it('retrieve the conversion url', function () { - - /** @var Media $media */ - $media = MediaFactory::new()->withPoster()->make(); - - expect($media->getUrl( - conversion: 'poster', - fallback: true, - ))->toBe('/storage//uuid/poster/poster.png'); - - expect($media->getUrl( - conversion: 'poster', - fallback: true, - parameters: [ - 'width' => 200, - 'height' => 200, - ] - ))->toBe('/storage//uuid/poster/poster.png?width=200&height=200'); - -}); - -it('retrieve the generated conversion key', function () { +it('stores an uploaded image', function () { /** @var Media $media */ $media = MediaFactory::new()->make(); - expect($media->makeGeneratedConversionKey('poster'))->toBe('poster'); - expect($media->makeGeneratedConversionKey('poster.480p'))->toBe('poster.generated_conversions.480p'); - expect($media->makeGeneratedConversionKey('poster.square.480p'))->toBe('poster.generated_conversions.square.generated_conversions.480p'); -}); - -it('retrieve the generated conversion', function () { - /** @var Media $media */ - $media = MediaFactory::new()->make(); - - $media->generated_conversions = collect([ - 'poster' => MediaFactory::generatedConversion(), - ]); - - expect($media->hasGeneratedConversion('poster'))->toBe(true); - expect($media->hasGeneratedConversion('poster.480p'))->toBe(true); - expect($media->hasGeneratedConversion('poster.480p.foo'))->toBe(false); - - expect($media->getGeneratedConversion('poster'))->toBeInstanceof(GeneratedConversion::class); - expect($media->getGeneratedConversion('poster.480p'))->toBeInstanceof(GeneratedConversion::class); - expect($media->getGeneratedConversion('poster.480p.foo'))->toBe(null); -}); - -it('retrieve the generated conversion path', function () { - /** @var Media $media */ - $media = MediaFactory::new()->make(); - - $media->generated_conversions = collect([ - 'poster' => MediaFactory::generatedConversion(), - ]); - - expect($media->getPath('poster'))->toBe('/poster/poster.png'); - expect($media->getPath('poster.480p'))->toBe('/poster/generated_conversions/480p/poster-480p.png'); -}); - -it('retrieve the generated conversion url', function () { - Storage::fake('media'); - - /** @var Media $media */ - $media = MediaFactory::new()->make(); - - $media->generated_conversions = collect([ - 'poster' => MediaFactory::generatedConversion(disk: 'media'), - ]); - - expect($media->getUrl('poster'))->toBe('/storage//poster/poster.png'); - expect($media->getUrl('poster.480p'))->toBe('/storage//poster/generated_conversions/480p/poster-480p.png'); -}); - -it('retrieve the generated conversion url fallback', function () { Storage::fake('media'); - /** @var Media $media */ - $media = MediaFactory::new()->withPoster()->make([ - 'path' => '/uuid/empty.jpg', - ]); - - expect($media->getUrl( - conversion: 'missing', - fallback: true - ))->toBe('/storage//uuid/empty.jpg'); - - expect($media->getUrl( - conversion: 'missing', - fallback: 'poster' - ))->toBe('/storage//uuid/poster/poster.png'); - - expect($media->getUrl( - conversion: 'missing', - fallback: 'foo' - ))->toBe('foo'); - - expect($media->getUrl( - conversion: 'missing' - ))->toBe(null); -}); - -it('add the generated conversion', function () { + $file = UploadedFile::fake()->image('foo.jpg', width: 16, height: 9); - /** @var Media $media */ - $media = MediaFactory::new()->make(); + $media->storeFile( + file: $file, + disk: 'media' + ); - $media->generated_conversions = collect([ - 'poster' => MediaFactory::generatedConversion(), - ]); + expect($media->path)->toBe("{$media->uuid}/foo.jpg"); + expect($media->name)->toBe('foo'); + expect($media->extension)->toBe('jpg'); + expect($media->file_name)->toBe('foo.jpg'); + expect($media->mime_type)->toBe('image/jpeg'); + expect($media->width)->toBe(16); + expect($media->height)->toBe(9); + expect($media->aspect_ratio)->toBe((new Dimension(16, 9))->getRatio(false)->getValue()); + expect($media->duration)->toBe(null); + expect($media->size)->toBe(695); - $media->putGeneratedConversion('optimized', new GeneratedConversion( - file_name: 'optimized.png', - name: 'optimized', - state: 'pending', - path: '/optimized/optimized.png', - type: MediaType::Image, - disk: config('media.disk') - )); - - $media->putGeneratedConversion('poster.poster-optimized', new GeneratedConversion( - file_name: 'poster-optimized.png', - name: 'poster-optimized', - state: 'pending', - path: 'poster/generated_conversions/optimized/poster-optimized.png', - type: MediaType::Image, - disk: config('media.disk') - )); - - expect($media->hasGeneratedConversion('optimized'))->toBe(true); - expect($media->hasGeneratedConversion('poster.poster-optimized'))->toBe(true); + Storage::disk('media')->assertExists($media->path); }); -it('update a generated conversion', function () { - /** @var Media $media */ - $media = MediaFactory::new()->make(); - - $media - ->putGeneratedConversion('poster', new GeneratedConversion( - file_name: 'poster.png', - name: 'poster', - state: 'success', - path: '/optimized/poster.png', - type: MediaType::Image, - disk: config('media.disk') - )) - ->save(); - - $generatedConversion = $media->getGeneratedConversion('poster'); - - expect($generatedConversion->state)->toBe('success'); - - $generatedConversion->state = 'failed'; - $media->save(); - - $media->refresh(); - - expect($generatedConversion->state)->tobe('failed'); -}); +it('stores an uploaded image using a prefixed path', function () { + config()->set('media.generated_path_prefix', 'prefix'); -it('store an uploaded image', function () { /** @var Media $media */ $media = MediaFactory::new()->make(); @@ -214,14 +51,21 @@ disk: 'media' ); + expect($media->path)->toBe("prefix/{$media->uuid}/foo.jpg"); expect($media->name)->toBe('foo'); + expect($media->extension)->toBe('jpg'); expect($media->file_name)->toBe('foo.jpg'); - expect($media->path)->toBe("{$media->uuid}/foo.jpg"); + expect($media->mime_type)->toBe('image/jpeg'); + expect($media->width)->toBe(16); + expect($media->height)->toBe(9); + expect($media->aspect_ratio)->toBe((new Dimension(16, 9))->getRatio(false)->getValue()); + expect($media->duration)->toBe(null); + expect($media->size)->toBe(695); Storage::disk('media')->assertExists($media->path); }); -it('store an uploaded image with a custom name', function () { +it('stores an uploaded image with a custom name', function () { /** @var Media $media */ $media = MediaFactory::new()->make(); @@ -231,277 +75,312 @@ $media->storeFile( file: $file, - collection_name: 'avatar', name: 'avatar', disk: 'media' ); + expect($media->path)->toBe("{$media->uuid}/avatar.jpg"); + expect($media->name)->toBe('avatar'); + expect($media->extension)->toBe('jpg'); + expect($media->file_name)->toBe('avatar.jpg'); + expect($media->mime_type)->toBe('image/jpeg'); expect($media->width)->toBe(16); expect($media->height)->toBe(9); expect($media->aspect_ratio)->toBe((new Dimension(16, 9))->getRatio(false)->getValue()); - expect($media->collection_name)->toBe('avatar'); - expect($media->name)->toBe('avatar'); - expect($media->file_name)->toBe('avatar.jpg'); - expect($media->type)->toBe(MediaType::Image); - expect($media->path)->toBe("{$media->uuid}/avatar.jpg"); + expect($media->duration)->toBe(null); + expect($media->size)->toBe(695); Storage::disk('media')->assertExists($media->path); }); -it('store a pdf from an url with a custom name', function () { +it('stores an svg file', function () { /** @var Media $media */ $media = MediaFactory::new()->make(); Storage::fake('media'); $media->storeFile( - file: $this->dummy_pdf_url, + file: $this->getTestFile('images/svg.svg'), disk: 'media', - name: 'foo' ); - expect($media->name)->toBe('foo'); - expect($media->file_name)->toBe('foo.pdf'); - expect($media->path)->toBe("{$media->uuid}/foo.pdf"); + expect($media->path)->toBe("{$media->uuid}/svg.svg"); + expect($media->name)->toBe('svg'); + expect($media->extension)->toBe('svg'); + expect($media->file_name)->toBe('svg.svg'); + expect($media->mime_type)->toBe('image/svg+xml'); + expect($media->width)->toBe(279); + expect($media->height)->toBe(279); + expect($media->aspect_ratio)->toBe(1); + expect($media->duration)->toBe(null); + expect($media->size)->toBe(1853); Storage::disk('media')->assertExists($media->path); }); -it('store a svg file', function () { +it('stores a pdf from an url', function () { /** @var Media $media */ $media = MediaFactory::new()->make(); Storage::fake('media'); - $file = $this->getTestFile('images/svg.svg'); - $media->storeFile( - file: $file, + file: $this->dummy_pdf_url, disk: 'media', - name: 'foo' + name: 'document' ); - expect($media->name)->toBe('foo'); - expect($media->file_name)->toBe('foo.svg'); - expect($media->path)->toBe("{$media->uuid}/foo.svg"); + expect($media->path)->toBe("{$media->uuid}/document.pdf"); + expect($media->name)->toBe('document'); + expect($media->extension)->toBe('pdf'); + expect($media->file_name)->toBe('document.pdf'); + expect($media->mime_type)->toBe('application/pdf'); + expect($media->width)->toBe(null); + expect($media->height)->toBe(null); + expect($media->aspect_ratio)->toBe(null); + expect($media->duration)->toBe(null); + expect($media->size)->toBe(13264); Storage::disk('media')->assertExists($media->path); }); -it('store a file within a prefixed path', function () { - config()->set('media.generated_path_prefix', 'media'); - +it('stores a conversion file', function () { /** @var Media $media */ - $media = MediaFactory::new()->make(); + $media = MediaFactory::new()->make([ + 'disk' => 'media', + ]); Storage::fake('media'); - $file = $this->getTestFile('images/svg.svg'); + $media->save(); - $media->storeFile( + $file = UploadedFile::fake()->image('foo.jpg', width: 16, height: 9); + + $conversion = $media->addConversion( file: $file, - disk: 'media', - name: 'foo' + conversionName: 'poster', + name: 'poster', ); - expect($media->name)->toBe('foo'); - expect($media->file_name)->toBe('foo.svg'); - expect($media->path)->toBe("media/{$media->uuid}/foo.svg"); - - Storage::disk('media')->assertExists($media->path); + expect($conversion->media_id)->toBe($media->id); + expect($conversion->id)->not->toBe(null); + expect($conversion->exists)->toBe(true); + expect($conversion->path)->toBe("{$media->uuid}/conversions/poster/poster.jpg"); + expect($conversion->name)->toBe('poster'); + expect($conversion->extension)->toBe('jpg'); + expect($conversion->file_name)->toBe('poster.jpg'); + expect($conversion->mime_type)->toBe('image/jpeg'); + expect($conversion->width)->toBe(16); + expect($conversion->height)->toBe(9); + expect($conversion->aspect_ratio)->toBe((new Dimension(16, 9))->getRatio(false)->getValue()); + expect($conversion->duration)->toBe(null); + expect($conversion->size)->toBe(695); + + Storage::disk('media')->assertExists($conversion->path); + + expect($media->conversions()->count())->toBe(1); + expect($media->conversions)->toHaveLength(1); + expect($media->getConversion('poster'))->toBeInstanceof(MediaConversion::class); }); -it('limit the name length to 255', function () { - config()->set('media.generated_path_prefix', 'media'); +it('retrieves conversions definitions from the associated model', function () { - /** @var Media $media */ - $media = MediaFactory::new()->make(); + $media = Media::factory()->make([ + 'collection_name' => 'conversions', + ]); + $media->model()->associate(new Test); - Storage::fake('media'); + $definitions = $media->getConversionsDefinitions(); - $file = $this->getTestFile('images/svg.svg'); + expect($definitions)->toHaveLength(2); + expect($definitions['poster'])->toBeInstanceOf(MediaConversionDefinition::class); + expect($definitions['small'])->toBeInstanceOf(MediaConversionDefinition::class); - $media->storeFile( - file: $file, - disk: 'media', - name: 'aaaaaaaa10aaaaaaaa20aaaaaaaa30aaaaaaaa40aaaaaaaa50aaaaaaaa60aaaaaaaa70aaaaaaaa80aaaaaaaa90aaaaaaaa10aaaaaaaa20aaaaaaaa30aaaaaaaa40aaaaaaaa40aaaaaaaa50aaaaaaaa60aaaaaaaa70aaaaaaaa80aaaaaaaa90aaaaaaaa10aaaaaaaa20aaaaaaaa30aaaaaaaa40aaaaaaaa50aaaaaaaa60aaaaa', - ); +}); - expect($media->name)->toBe('aaaaaaaa10aaaaaaaa20aaaaaaaa30aaaaaaaa40aaaaaaaa50aaaaaaaa60aaaaaaaa70aaaaaaaa80aaaaaaaa90aaaaaaaa10aaaaaaaa20aaaaaaaa30aaaaaaaa40aaaaaaaa40aaaaaaaa50aaaaaaaa60aaaaaaaa70aaaaaaaa80aaaaaaaa90aaaaaaaa10aaaaaaaa'); - expect($media->file_name)->toBe('aaaaaaaa10aaaaaaaa20aaaaaaaa30aaaaaaaa40aaaaaaaa50aaaaaaaa60aaaaaaaa70aaaaaaaa80aaaaaaaa90aaaaaaaa10aaaaaaaa20aaaaaaaa30aaaaaaaa40aaaaaaaa40aaaaaaaa50aaaaaaaa60aaaaaaaa70aaaaaaaa80aaaaaaaa90aaaaaaaa10aaaaaaaa.svg'); - expect($media->path)->toBe("media/{$media->uuid}/aaaaaaaa10aaaaaaaa20aaaaaaaa30aaaaaaaa40aaaaaaaa50aaaaaaaa60aaaaaaaa70aaaaaaaa80aaaaaaaa90aaaaaaaa10aaaaaaaa20aaaaaaaa30aaaaaaaa40aaaaaaaa40aaaaaaaa50aaaaaaaa60aaaaaaaa70aaaaaaaa80aaaaaaaa90aaaaaaaa10aaaaaaaa.svg"); +it('retrieves a conversion definition from the associated model', function () { + $media = Media::factory()->make([ + 'collection_name' => 'conversions', + ]); + $media->model()->associate(new Test); - Storage::disk('media')->assertExists($media->path); + expect($media->getConversionDefinition('poster'))->not->toBe(null); + expect($media->getConversionDefinition('small'))->not->toBe(null); }); -it('store a conversion image of a media', function () { +it('deletes old conversion files when adding the same conversion', function () { /** @var Media $media */ - $media = MediaFactory::new()->make(); + $media = MediaFactory::new()->make([ + 'disk' => 'media', + ]); + $media->save(); Storage::fake('media'); - $orginial = UploadedFile::fake()->image('foo.jpg', width: 16, height: 9); + $file = UploadedFile::fake()->image('foo.jpg', width: 16, height: 9); - $media->storeFile( - file: $orginial, - collection_name: 'avatar', - name: 'avatar', - disk: 'media' + $conversion = $media->addConversion( + file: $file, + conversionName: 'poster', + name: 'poster', ); - $poster = UploadedFile::fake()->image('foo-poster.jpg', width: 16, height: 9); + $path = $conversion->path; - $media->storeConversion( - file: $poster->getPathname(), - conversion: 'poster', - name: 'avatar-poster' + Storage::disk('media')->assertExists($path); + + $newConversion = $media->addConversion( + file: $file, + conversionName: 'poster', + name: 'new-poster', ); - $generatedConversion = $media->getGeneratedConversion('poster'); + $newPath = $newConversion->path; - expect($generatedConversion)->toBeInstanceof(GeneratedConversion::class); + Storage::disk('media')->assertExists($newPath); + Storage::disk('media')->assertMissing($path); - expect($generatedConversion->width)->toBe(16); - expect($generatedConversion->height)->toBe(9); - expect($generatedConversion->aspect_ratio)->toBe((new Dimension(16, 9))->getRatio(false)->getValue()); - expect($generatedConversion->name)->toBe('avatar-poster'); - expect($generatedConversion->file_name)->toBe('avatar-poster.jpg'); - expect($generatedConversion->type)->toBe(MediaType::Image); - expect($generatedConversion->path)->toBe("{$media->uuid}/generated_conversions/poster/avatar-poster.jpg"); - expect($generatedConversion->path)->toBe($media->getPath('poster')); + expect($media->conversions)->toHaveLength(1); - Storage::disk('media')->assertExists($generatedConversion->path); }); -it('store a conversion image of a conversion', function () { +it('deletes children conversions when adding the same conversion', function () { /** @var Media $media */ - $media = MediaFactory::new()->make(); + $media = Media::factory()->make([ + 'disk' => 'media', + ]); + $media->save(); Storage::fake('media'); - $media->storeFile( - file: UploadedFile::fake()->image('foo.jpg', width: 16, height: 9), - collection_name: 'avatar', - name: 'avatar', - disk: 'media' + $file = UploadedFile::fake()->image('foo.jpg', width: 16, height: 9); + + $media->addConversion( + file: $file, + conversionName: 'foo', + name: 'foo', ); - $poster = UploadedFile::fake()->image('foo-poster.jpg', width: 16, height: 9); + $conversion = $media->addConversion( + file: $file, + conversionName: 'poster', + name: 'poster', + ); - $media->storeConversion( - file: $poster->getPathname(), - conversion: 'poster', - name: 'avatar-poster' + $media->addConversion( + file: $file, + conversionName: '360', + parent: $conversion, + name: '360', ); - $small = UploadedFile::fake()->image('foo-poster-small.jpg', width: 16, height: 9); + expect($media->conversions)->toHaveLength(3); + expect($media->getConversion('foo'))->not->toBe(null); + expect($media->getConversion('poster'))->not->toBe(null); + expect($media->getConversion('poster.360'))->not->toBe(null); - $media->storeConversion( - file: $small->getPathname(), - conversion: 'poster.small', - name: 'avatar-poster-small' - ); + $children = $media->getChildrenConversions('poster'); - $generatedConversion = $media->getGeneratedConversion('poster.small'); + expect($children)->toHaveLength(1); + + $newConversion = $media->addConversion( + file: $file, + conversionName: 'poster', + name: 'new-poster', + ); - expect($generatedConversion)->toBeInstanceof(GeneratedConversion::class); + foreach ($children as $child) { + expect($child->fresh())->toBe(null); + } - expect($generatedConversion->width)->toBe(16); - expect($generatedConversion->height)->toBe(9); - expect($generatedConversion->aspect_ratio)->toBe((new Dimension(16, 9))->getRatio(false)->getValue()); - expect($generatedConversion->name)->toBe('avatar-poster-small'); - expect($generatedConversion->file_name)->toBe('avatar-poster-small.jpg'); - expect($generatedConversion->type)->toBe(MediaType::Image); - expect($generatedConversion->path)->toBe("{$media->uuid}/generated_conversions/poster/generated_conversions/small/avatar-poster-small.jpg"); - expect($generatedConversion->path)->toBe($media->getPath('poster.small')); + expect($media->conversions)->toHaveLength(2); + expect($media->getConversion('foo'))->not->toBe(null); + expect($media->getConversion('poster'))->not->toBe(null); + expect($media->getConversion('poster.360'))->toBe(null); - Storage::disk('media')->assertExists($generatedConversion->path); }); -it('delete a media generated conversion with its own conversions', function () { +it('retrieve the url', function () { /** @var Media $media */ $media = MediaFactory::new()->make(); - Storage::fake('media'); + expect($media->getUrl())->toBe('/storage/{uuid}/empty.jpg'); + expect($media->getUrl( + parameters: [ + 'width' => 200, + 'height' => 200, + ] + ))->toBe('/storage/{uuid}/empty.jpg?width=200&height=200'); - $media->storeFile( - file: UploadedFile::fake()->image('foo.jpg', width: 16, height: 9), - collection_name: 'avatar', - name: 'avatar', - disk: 'media' - ); +}); - $poster = UploadedFile::fake()->image('foo-poster.jpg', width: 16, height: 9); +it('retrieve the fallback url', function () { + /** @var Media $media */ + $media = MediaFactory::new()->make(); - $media->storeConversion( - file: $poster->getPathname(), + expect($media->getUrl( conversion: 'poster', - name: 'avatar-poster' - ); + fallback: false, + ))->toBe(null); - $small = UploadedFile::fake()->image('foo-poster-small.jpg', width: 16, height: 9); + expect($media->getUrl( + conversion: 'poster', + fallback: true, + ))->toBe('/storage/{uuid}/empty.jpg'); - $media->storeConversion( - file: $small->getPathname(), - conversion: 'poster.small', - name: 'avatar-poster-small' - ); + expect($media->getUrl( + conversion: 'poster', + fallback: true, + parameters: [ + 'width' => 200, + 'height' => 200, + ] + ))->toBe('/storage/{uuid}/empty.jpg?width=200&height=200'); - $generatedConversion = $media->getGeneratedConversion('poster'); - $nestedGeneratedConversion = $media->getGeneratedConversion('poster.small'); +}); - Storage::disk('media')->assertExists($generatedConversion->path); - Storage::disk('media')->assertExists($nestedGeneratedConversion->path); +it('retrieve the conversion url', function () { - $media->deleteGeneratedConversion('poster'); + /** @var Media $media */ + $media = MediaFactory::new()->withPoster()->create(); - expect($media->getGeneratedConversion('poster'))->toBe(null); + expect($media->getUrl( + conversion: 'poster', + fallback: false, + ))->toBe('/storage/{uuid}/conversions/poster/poster.jpg'); + + expect($media->getUrl( + conversion: 'poster', + fallback: false, + parameters: [ + 'width' => 200, + 'height' => 200, + ] + ))->toBe('/storage/{uuid}/conversions/poster/poster.jpg?width=200&height=200'); - Storage::disk('media')->assertMissing($generatedConversion->path); - Storage::disk('media')->assertMissing($nestedGeneratedConversion->path); }); -it('delete all files when model deleted', function () { +it('store a file within a prefixed path', function () { + config()->set('media.generated_path_prefix', 'media'); + /** @var Media $media */ $media = MediaFactory::new()->make(); Storage::fake('media'); - $media->storeFile( - file: UploadedFile::fake()->image('foo.jpg', width: 16, height: 9), - collection_name: 'avatar', - name: 'avatar', - disk: 'media' - ); - - $poster = UploadedFile::fake()->image('foo-poster.jpg', width: 16, height: 9); - - $media->storeConversion( - file: $poster->getPathname(), - conversion: 'poster', - name: 'avatar-poster' - ); - - $small = UploadedFile::fake()->image('foo-poster-small.jpg', width: 16, height: 9); + $file = $this->getTestFile('images/svg.svg'); - $media->storeConversion( - file: $small->getPathname(), - conversion: 'poster.small', - name: 'avatar-poster-small' + $media->storeFile( + file: $file, + disk: 'media', + name: 'foo' ); - $generatedConversion = $media->getGeneratedConversion('poster'); - $nestedGeneratedConversion = $media->getGeneratedConversion('poster.small'); + expect($media->name)->toBe('foo'); + expect($media->file_name)->toBe('foo.svg'); + expect($media->path)->toBe("media/{$media->uuid}/foo.svg"); Storage::disk('media')->assertExists($media->path); - Storage::disk('media')->assertExists($generatedConversion->path); - Storage::disk('media')->assertExists($nestedGeneratedConversion->path); - - $media->delete(); - - Storage::disk('media')->assertMissing($media->path); - Storage::disk('media')->assertMissing($generatedConversion->path); - Storage::disk('media')->assertMissing($nestedGeneratedConversion->path); }); it('reorder models', function () { diff --git a/tests/Feature/MediaZipperTest.php b/tests/Feature/MediaZipperTest.php index 9f2d802..9fd0882 100644 --- a/tests/Feature/MediaZipperTest.php +++ b/tests/Feature/MediaZipperTest.php @@ -15,7 +15,6 @@ ])->each(function (Media $media) { $media->storeFile( file: UploadedFile::fake()->image('foo.jpg'), - collection_name: 'avatar', name: 'avatar', disk: 'media' ); diff --git a/tests/Feature/OptimizedImageConversionJobTest.php b/tests/Feature/OptimizedImageConversionJobTest.php deleted file mode 100644 index 0bff357..0000000 --- a/tests/Feature/OptimizedImageConversionJobTest.php +++ /dev/null @@ -1,28 +0,0 @@ -save(); - - $file = UploadedFile::fake()->image('foo.jpg'); - $model->addMedia( - file: $file, - collection_name: 'files', - disk: 'media' - ); - - $media = $model->getMedia('files')->first(); - - $generatedConversion = $media->getGeneratedConversion('webp'); - - expect($generatedConversion)->not->toBe(null); - expect($generatedConversion->extension)->toBe('webp'); - - Storage::disk('media')->assertExists($generatedConversion->path); -}); diff --git a/tests/Feature/ResponsiveImagesConversionsPresetTest.php b/tests/Feature/ResponsiveImagesConversionsPresetTest.php deleted file mode 100644 index f15ec33..0000000 --- a/tests/Feature/ResponsiveImagesConversionsPresetTest.php +++ /dev/null @@ -1,37 +0,0 @@ -save(); - - $orginial = UploadedFile::fake()->image('original.jpg', width: 1920, height: 1920); - - $media = $model->addMedia( - file: $orginial, - collection_name: 'images', - disk: 'media' - ); - - $media->refresh(); - - expect($model->getMediaConversions($media))->toHaveLength(4); - expect($media->generated_conversions)->toHaveLength(4); - - Storage::disk('media')->assertExists($media->path); - - foreach (ResponsiveImagesConversionsPreset::DEFAULT_WIDTH as $width) { - $generatedConversion = $media->getGeneratedConversion((string) $width); - expect($generatedConversion)->not->toBe(null); - expect($generatedConversion->width)->toBe($width); - } - - Storage::disk('media')->assertExists($generatedConversion->path); -}); diff --git a/tests/Feature/VideoPosterConversionJobTest.php b/tests/Feature/VideoPosterConversionJobTest.php deleted file mode 100644 index 2ed97a8..0000000 --- a/tests/Feature/VideoPosterConversionJobTest.php +++ /dev/null @@ -1,64 +0,0 @@ -save(); - - $file = $this->getTestFile('videos/horizontal.mp4'); - - $model->addMedia( - file: $file, - collection_name: 'videos', - disk: 'media' - ); - - $media = $model->getMedia('videos')->first(); - - Storage::disk('media')->assertExists($media->path); - - $generatedConversion = $media->getGeneratedConversion('poster'); - - expect($generatedConversion)->not->toBe(null); - expect($generatedConversion->extension)->toBe('jpg'); -}); - -it('generate a poster with its responsive images from a video', function () { - Storage::fake('media'); - - $model = new TestWithVideoConversions; - $model->save(); - - $file = $this->getTestFile('videos/horizontal.mp4'); - - $model->addMedia( - file: $file, - collection_name: 'videos', - disk: 'media' - ); - - $media = $model->getMedia('videos')->first(); - - expect($model->getMediaConversion($media, 'poster.360'))->not->toBe(null); - - Storage::disk('media')->assertExists($media->path); - - $generatedConversion = $media->getGeneratedConversion('poster'); - - expect($generatedConversion)->not->toBe(null); - expect($generatedConversion->extension)->toBe('jpg'); - - foreach (ResponsiveImagesConversionsPreset::getWidths( - ResponsiveImagesConversionsPreset::DEFAULT_WIDTH, - $media - ) as $width) { - $generatedConversion = $media->getGeneratedConversion("poster.{$width}"); - expect($generatedConversion)->not->toBe(null); - expect($generatedConversion->width)->toBe($width); - } -}); diff --git a/tests/Models/Test.php b/tests/Models/Test.php index 0a46683..9af2bd7 100644 --- a/tests/Models/Test.php +++ b/tests/Models/Test.php @@ -2,17 +2,19 @@ namespace Elegantly\Media\Tests\Models; -use Elegantly\Media\Contracts\InteractWithMedia; +use Elegantly\Media\Concerns\HasMedia; +use Elegantly\Media\Definitions\MediaConversionImage; +use Elegantly\Media\Definitions\MediaConversionPoster; +use Elegantly\Media\Definitions\MediaConversionVideo; use Elegantly\Media\Enums\MediaType; -use Elegantly\Media\Jobs\OptimizedImageConversionJob; +use Elegantly\Media\Helpers\File; use Elegantly\Media\MediaCollection; -use Elegantly\Media\MediaConversion; -use Elegantly\Media\Traits\HasMedia; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Model; use Spatie\Image\Enums\Fit; +use Spatie\Image\Image; -class Test extends Model implements InteractWithMedia +class Test extends Model { use HasMedia; @@ -29,7 +31,7 @@ public function registerMediaCollections(): Arrayable|iterable|null public: false, ), new MediaCollection( - name: 'avatar', + name: 'single', single: true, public: true, ), @@ -39,34 +41,49 @@ public function registerMediaCollections(): Arrayable|iterable|null public: true, fallback: fn () => 'fallback-value' ), - ]; - } + new MediaCollection( + name: 'transform', + single: false, + public: true, + transform: function ($file) { + $path = $file->getRealPath(); + $type = File::type($path); - public function registerMediaConversions($media): Arrayable|iterable|null - { - $conversions = collect(); + if ($type === MediaType::Image) { - if ($media->type === MediaType::Image) { - $conversions->push(new MediaConversion( - conversionName: 'optimized', - job: new OptimizedImageConversionJob( - media: $media, - ) - )); + Image::load($path) + ->fit(Fit::Crop, 500, 500) + ->optimize() + ->save(); - if ($media->collection_name === 'avatar') { - $conversions->push(new MediaConversion( - conversionName: 'small', - job: new OptimizedImageConversionJob( - media: $media, - width: 5, - height: 5, - fit: Fit::Crop - ) - )); - } - } + } - return $conversions; + return $file; + } + ), + new MediaCollection( + name: 'conversions', + single: false, + public: false, + conversions: [ + new MediaConversionPoster( + name: 'poster', + queued: false, + conversions: [ + new MediaConversionImage( + name: '360', + width: 360, + queued: false, + ), + ] + ), + new MediaConversionVideo( + name: 'small', + queued: true, + width: 100, + ), + ] + ), + ]; } } diff --git a/tests/Models/TestSoftDelete.php b/tests/Models/TestSoftDelete.php index 9751241..cdc935f 100644 --- a/tests/Models/TestSoftDelete.php +++ b/tests/Models/TestSoftDelete.php @@ -2,10 +2,9 @@ namespace Elegantly\Media\Tests\Models; -use Elegantly\Media\Contracts\InteractWithMedia; use Illuminate\Database\Eloquent\SoftDeletes; -class TestSoftDelete extends Test implements InteractWithMedia +class TestSoftDelete extends Test { use SoftDeletes; } diff --git a/tests/Models/TestWithMediaTransformations.php b/tests/Models/TestWithMediaTransformations.php deleted file mode 100644 index 1a1728f..0000000 --- a/tests/Models/TestWithMediaTransformations.php +++ /dev/null @@ -1,49 +0,0 @@ -collection_name === 'avatar') { - Image::load($file->getRealPath()) - ->fit(Fit::Crop, 500, 500) - ->optimize() - ->save(); - } elseif ($media->collection_name === 'video') { - // @phpstan-ignore-next-line - FFMpeg::open($file) - ->export() - ->inFormat(new X264) - ->resize(500, null, ResizeFilter::RESIZEMODE_FIT, false) - ->save(); - } - - return $file; - } -} diff --git a/tests/Models/TestWithMultipleConversions.php b/tests/Models/TestWithMultipleConversions.php deleted file mode 100644 index 744e6f8..0000000 --- a/tests/Models/TestWithMultipleConversions.php +++ /dev/null @@ -1,56 +0,0 @@ -type === MediaType::Image) { - return [ - new MediaConversion( - conversionName: 'optimized', - job: new OptimizedImageConversionJob( - media: $media, - ) - ), - new MediaConversion( - conversionName: 'webp', - job: new OptimizedImageConversionJob( - media: $media, - fileName: "{$media->name}.webp" - ) - ), - ]; - } - - return collect(); - } -} diff --git a/tests/Models/TestWithNestedConversions.php b/tests/Models/TestWithNestedConversions.php deleted file mode 100644 index ebca42f..0000000 --- a/tests/Models/TestWithNestedConversions.php +++ /dev/null @@ -1,56 +0,0 @@ -type === MediaType::Image) { - return [ - new MediaConversion( - conversionName: 'optimized', - job: new OptimizedImageConversionJob( - media: $media, - fileName: 'optimized.jpg' - ), - conversions: fn (GeneratedConversion $generatedConversion) => [ - new MediaConversion( - conversionName: 'webp', - job: new OptimizedImageConversionJob( - media: $media, - fileName: "{$generatedConversion->name}.webp" // expected to be optimized.webp - ), - ), - ] - ), - new MediaConversion( - conversionName: '360', - job: new OptimizedImageConversionJob( - media: $media, - width: 360, - fileName: '360.jpg' - ), - ), - ]; - } - - return []; - } -} diff --git a/tests/Models/TestWithResponsiveImages.php b/tests/Models/TestWithResponsiveImages.php deleted file mode 100644 index c98a55c..0000000 --- a/tests/Models/TestWithResponsiveImages.php +++ /dev/null @@ -1,43 +0,0 @@ -type === MediaType::Image) { - return ResponsiveImagesConversionsPreset::make( - media: $media, - ); - } - - return collect(); - } -} diff --git a/tests/Models/TestWithVideoConversions.php b/tests/Models/TestWithVideoConversions.php deleted file mode 100644 index 1fb55ec..0000000 --- a/tests/Models/TestWithVideoConversions.php +++ /dev/null @@ -1,58 +0,0 @@ -type === MediaType::Video) { - return [ - new MediaConversion( - conversionName: 'poster', - job: new VideoPosterConversionJob( - media: $media, - seconds: 0, - fileName: "{$media->name}.jpg", - ), - conversions: function (GeneratedConversion $generatedConversion) use ($media) { - return ResponsiveImagesConversionsPreset::make( - media: $media, - generatedConversion: $generatedConversion - ); - } - ), - ]; - } - - return []; - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index e0b0d65..506202c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -37,6 +37,8 @@ public function getEnvironmentSetUp($app) $migration = include __DIR__.'/../database/migrations/create_media_table.php.stub'; $migration->up(); + $migration = include __DIR__.'/../database/migrations/create_media_conversions_table.php.stub'; + $migration->up(); $app['db']->connection()->getSchemaBuilder()->create('tests', function (Blueprint $table) { $table->increments('id'); diff --git a/tests/Unit/FileDownloaderTest.php b/tests/Unit/FileDownloaderTest.php index a999771..0bcc33c 100644 --- a/tests/Unit/FileDownloaderTest.php +++ b/tests/Unit/FileDownloaderTest.php @@ -9,7 +9,10 @@ ->location(storage_path('media-tmp')) ->create(); - $path = FileDownloader::getTemporaryFile($this->dummy_pdf_url, $temporaryDirectory); + $path = FileDownloader::fromUrl( + $this->dummy_pdf_url, + $temporaryDirectory->path() + ); expect(is_file($path))->toBe(true); @@ -24,9 +27,9 @@ ->location(storage_path('media-tmp')) ->create(); - $path = FileDownloader::getTemporaryFile( + $path = FileDownloader::fromUrl( 'https://icon.horse/icon/discord.com', - $temporaryDirectory + $temporaryDirectory->path() ); expect(is_file($path))->toBe(true); diff --git a/tests/files/images/800x900.jpg b/tests/files/images/800x900.jpg deleted file mode 100644 index 9ab8f6d..0000000 Binary files a/tests/files/images/800x900.jpg and /dev/null differ diff --git a/tests/files/images/800x900.png b/tests/files/images/800x900.png new file mode 100644 index 0000000..6873cf8 Binary files /dev/null and b/tests/files/images/800x900.png differ