Add deferred upload
This commit is contained in:
parent
c00ee87082
commit
8bf17784cc
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'temp_storage' => 'temp',
|
'temp_disk' => 'temp',
|
||||||
'middleware' => ['web', 'auth:web'],
|
'middleware' => ['web', 'auth:web'],
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Zoomyboy\MedialibraryHelper;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Spatie\LaravelData\Attributes\MapInputName;
|
||||||
|
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||||
|
use Spatie\LaravelData\Data;
|
||||||
|
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||||
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
|
use Spatie\MediaLibrary\MediaCollections\MediaCollection;
|
||||||
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||||
|
|
||||||
|
#[MapInputName(SnakeCaseMapper::class)]
|
||||||
|
#[MapOutputName(SnakeCaseMapper::class)]
|
||||||
|
class DeferredMediaData extends Data
|
||||||
|
{
|
||||||
|
public ?int $id;
|
||||||
|
|
||||||
|
public string $originalUrl;
|
||||||
|
|
||||||
|
public int $size;
|
||||||
|
|
||||||
|
public string $name;
|
||||||
|
|
||||||
|
public string $collectionName;
|
||||||
|
|
||||||
|
public string $fileName;
|
||||||
|
|
||||||
|
public string $mimeType;
|
||||||
|
|
||||||
|
public bool $fallback = false;
|
||||||
|
|
||||||
|
public bool $isDeferred = true;
|
||||||
|
|
||||||
|
public static function fromPath(string $path, MediaCollection $collection): self
|
||||||
|
{
|
||||||
|
$file = new MediaFile(Storage::disk(config('media-library.temp_disk'))->path($path));
|
||||||
|
return static::withoutMagicalCreationFrom([
|
||||||
|
'collection_name' => $collection->name,
|
||||||
|
'original_url' => Storage::disk(config('media-library.temp_disk'))->url($path),
|
||||||
|
'size' => $file->getSize(),
|
||||||
|
'name' => $file->getBasename(),
|
||||||
|
'file_name' => $file->getFilename(),
|
||||||
|
'mime_type' => Storage::disk(config('media-library.temp_disk'))->mimeType($path),
|
||||||
|
'path' => $path,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,12 +7,12 @@ use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Spatie\Image\Image;
|
use Spatie\Image\Image;
|
||||||
use Spatie\LaravelData\DataCollection;
|
use Spatie\LaravelData\DataCollection;
|
||||||
use Spatie\MediaLibrary\HasMedia;
|
|
||||||
use Spatie\MediaLibrary\MediaCollections\Exceptions\InvalidBase64Data;
|
use Spatie\MediaLibrary\MediaCollections\Exceptions\InvalidBase64Data;
|
||||||
use Spatie\MediaLibrary\MediaCollections\FileAdder;
|
use Spatie\MediaLibrary\MediaCollections\FileAdder;
|
||||||
use Spatie\MediaLibrary\MediaCollections\MediaCollection;
|
use Spatie\MediaLibrary\MediaCollections\MediaCollection;
|
||||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||||
use Zoomyboy\MedialibraryHelper\Rules\ModelRule;
|
use Zoomyboy\MedialibraryHelper\Rules\ModelRule;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class MediaController
|
class MediaController
|
||||||
{
|
{
|
||||||
|
@ -24,6 +24,10 @@ class MediaController
|
||||||
'parent' => ['required', new ModelRule()],
|
'parent' => ['required', new ModelRule()],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (is_null($request->input('parent.id'))) {
|
||||||
|
return $this->storeDeferred($request);
|
||||||
|
}
|
||||||
|
|
||||||
$model = ModelRule::getModel($request->input('parent'));
|
$model = ModelRule::getModel($request->input('parent'));
|
||||||
$collection = ModelRule::getCollection($request->input('parent'));
|
$collection = ModelRule::getCollection($request->input('parent'));
|
||||||
$isSingle = 1 === $collection->collectionSizeLimit;
|
$isSingle = 1 === $collection->collectionSizeLimit;
|
||||||
|
@ -62,6 +66,37 @@ class MediaController
|
||||||
return $isSingle ? MediaData::from($medias->first()) : MediaData::collection($medias);
|
return $isSingle ? MediaData::from($medias->first()) : MediaData::collection($medias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function storeDeferred(Request $request)
|
||||||
|
{
|
||||||
|
$modelName = ModelRule::getModelClassName($request->input('parent'));
|
||||||
|
$collection = ModelRule::getCollection($request->input('parent'));
|
||||||
|
$isSingle = 1 === $collection->collectionSizeLimit;
|
||||||
|
|
||||||
|
$request->validate($isSingle ? [
|
||||||
|
'payload' => 'array',
|
||||||
|
'payload.name' => 'required|string|regex:/\..*$/|max:255',
|
||||||
|
'payload.content' => 'required|string',
|
||||||
|
] : [
|
||||||
|
'payload' => 'required|array|min:1',
|
||||||
|
'payload.*' => 'array',
|
||||||
|
'payload.*.name' => 'required|string|regex:/\..*$/|max:255',
|
||||||
|
'payload.*.content' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$content = $isSingle ? [$request->input('payload')] : $request->input('payload');
|
||||||
|
|
||||||
|
$tempPaths = collect($content)->map(function ($c) use ($collection, $modelName) {
|
||||||
|
$file = new MediaFile($c['name']);
|
||||||
|
|
||||||
|
$tmpFile = $this->storeTemporaryFile($c['content'], $collection);
|
||||||
|
Storage::disk(config('media-library.temp_disk'))->move($tmpFile, 'media-library/' . $file->getFilename());
|
||||||
|
return 'media-library/' . $file->getFilename();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->authorize('storeMedia', [$modelName, null, $collection->name]);
|
||||||
|
return $isSingle ? DeferredMediaData::fromPath($tempPaths->first(), $collection) : DeferredMediaData::collectionFromPaths($tempPaths, $collection);
|
||||||
|
}
|
||||||
|
|
||||||
public function update(Request $request, Media $media): MediaData
|
public function update(Request $request, Media $media): MediaData
|
||||||
{
|
{
|
||||||
$this->authorize('updateMedia', [$media->model, $media->collection_name]);
|
$this->authorize('updateMedia', [$media->model, $media->collection_name]);
|
||||||
|
@ -113,7 +148,7 @@ class MediaController
|
||||||
return property_exists($collection, $callback);
|
return property_exists($collection, $callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function fileAdderFromData($model, $data, $collection): FileAdder
|
private function storeTemporaryFile(string $data, MediaCollection $collection): string
|
||||||
{
|
{
|
||||||
$maxWidth = $collection->runCallback('maxWidth', 9);
|
$maxWidth = $collection->runCallback('maxWidth', 9);
|
||||||
if (str_contains($data, ';base64')) {
|
if (str_contains($data, ';base64')) {
|
||||||
|
@ -133,13 +168,20 @@ class MediaController
|
||||||
throw InvalidBase64Data::create();
|
throw InvalidBase64Data::create();
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmpFile = tempnam(sys_get_temp_dir(), 'media-library');
|
$tmpFile = 'media-library/' . str()->uuid()->toString();
|
||||||
file_put_contents($tmpFile, $binaryData);
|
Storage::disk(config('media-library.temp_disk'))->put($tmpFile, $binaryData);
|
||||||
|
|
||||||
if (null !== $maxWidth && 'image/jpeg' === mime_content_type($tmpFile)) {
|
if (null !== $maxWidth && 'image/jpeg' === Storage::disk(config('media-library.temp_disk'))->mimeType($tmpFile)) {
|
||||||
Image::load($tmpFile)->width($maxWidth)->save();
|
Image::load(Storage::disk(config('media-library.temp_disk'))->path($tmpFile))->width($maxWidth)->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $model->addMedia($tmpFile);
|
return $tmpFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fileAdderFromData($model, $data, $collection): FileAdder
|
||||||
|
{
|
||||||
|
$tmpFile = $this->storeTemporaryFile($data, $collection);
|
||||||
|
|
||||||
|
return $model->addMediaFromDisk($tmpFile, config('media-library.temp_disk'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,9 +37,12 @@ class MediaData extends Data
|
||||||
|
|
||||||
public bool $fallback = false;
|
public bool $fallback = false;
|
||||||
|
|
||||||
|
public bool $isDeferred = false;
|
||||||
|
|
||||||
public static function fromMedia(Media $media): self
|
public static function fromMedia(Media $media): self
|
||||||
{
|
{
|
||||||
$conversions = collect($media->getMediaConversionNames())->flip()->map(fn ($integer, $conversion) => $media->hasGeneratedConversion($conversion)
|
$conversions = collect($media->getMediaConversionNames())->flip()->map(
|
||||||
|
fn ($integer, $conversion) => $media->hasGeneratedConversion($conversion)
|
||||||
? ['original_url' => $media->getFullUrl($conversion)]
|
? ['original_url' => $media->getFullUrl($conversion)]
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,15 +21,20 @@ class ModelRule implements InvokableRule
|
||||||
public function __invoke($attribute, $value, $fail)
|
public function __invoke($attribute, $value, $fail)
|
||||||
{
|
{
|
||||||
app(Factory::class)->make([$attribute => $value], [
|
app(Factory::class)->make([$attribute => $value], [
|
||||||
"{$attribute}.id" => 'required|integer|gt:0',
|
"{$attribute}.id" => 'nullable|integer|gt:0',
|
||||||
"{$attribute}.model" => ['required', 'string', Rule::in(app('media-library-helpers')->keys())],
|
"{$attribute}.model" => ['required', 'string', Rule::in(app('media-library-helpers')->keys())],
|
||||||
"{$attribute}.collection" => 'required|string',
|
"{$attribute}.collection" => 'required|string',
|
||||||
])->validate();
|
])->validate();
|
||||||
|
|
||||||
|
|
||||||
$this->model = data_get($value, 'model');
|
$this->model = data_get($value, 'model');
|
||||||
$this->id = data_get($value, 'id');
|
$this->id = data_get($value, 'id');
|
||||||
$this->collection = data_get($value, 'collection');
|
$this->collection = data_get($value, 'collection');
|
||||||
|
|
||||||
|
if (is_null($this->id)) {
|
||||||
|
$this->validateDeferred($attribute, $value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$model = app('media-library-helpers')->get($this->model);
|
$model = app('media-library-helpers')->get($this->model);
|
||||||
app(Factory::class)->make([$attribute => $value], [
|
app(Factory::class)->make([$attribute => $value], [
|
||||||
|
@ -38,22 +43,47 @@ class ModelRule implements InvokableRule
|
||||||
])->validate();
|
])->validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function validateDeferred($attribute, $value): void
|
||||||
|
{
|
||||||
|
app(Factory::class)->make([$attribute => $value], [
|
||||||
|
"{$attribute}.model" => ['required', 'string', Rule::in(app('media-library-helpers')->keys())],
|
||||||
|
"{$attribute}.collection" => 'required|string',
|
||||||
|
])->validate();
|
||||||
|
|
||||||
|
$model = app('media-library-helpers')->get($this->model);
|
||||||
|
|
||||||
|
app(Factory::class)->make([$attribute => $value], [
|
||||||
|
"{$attribute}.collection" => ['required', Rule::in((new $model())->getRegisteredMediaCollections()->pluck('name'))],
|
||||||
|
])->validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{?id: int, ?collection: string, ?model: string} $modelParam
|
* @param array{?id: int, ?collection: string, ?model: string} $modelParam
|
||||||
*/
|
*/
|
||||||
public static function getModel($modelParam): HasMedia
|
public static function getModel($modelParam): HasMedia
|
||||||
{
|
{
|
||||||
$model = app('media-library-helpers')->get($modelParam['model']);
|
$model = static::getModelClassName($modelParam);
|
||||||
|
|
||||||
return $model::find($modelParam['id']);
|
return $model::find($modelParam['id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{?id: int, ?collection: string, ?model: string} $modelParam
|
||||||
|
* @return class-string<HasMedia>
|
||||||
|
*/
|
||||||
|
public static function getModelClassName($modelParam): string
|
||||||
|
{
|
||||||
|
return app('media-library-helpers')->get($modelParam['model']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{?id: int, ?collection: string, ?model: string} $modelParam
|
* @param array{?id: int, ?collection: string, ?model: string} $modelParam
|
||||||
*/
|
*/
|
||||||
public static function getCollection($modelParam): MediaCollection
|
public static function getCollection($modelParam): MediaCollection
|
||||||
{
|
{
|
||||||
return static::getModel($modelParam)->getMediaCollection($modelParam['collection']);
|
$className = static::getModelClassName($modelParam);
|
||||||
|
|
||||||
|
return (new $className)->getMediaCollection($modelParam['collection']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
test('it uploads a deferred file to a collection', function () {
|
||||||
|
$this->auth()->registerModel()->withoutExceptionHandling();
|
||||||
|
$content = base64_encode($this->pdfFile()->getContent());
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => null],
|
||||||
|
'payload' => [
|
||||||
|
'content' => $content,
|
||||||
|
'name' => 'beispiel bild.jpg',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->postJson('/mediaupload', $payload)
|
||||||
|
->assertStatus(201)
|
||||||
|
->assertJson([
|
||||||
|
'is_deferred' => true,
|
||||||
|
'original_url' => Storage::disk('temp')->url('media-library/beispiel bild.jpg'),
|
||||||
|
'name' => 'beispiel bild',
|
||||||
|
'collection_name' => 'defaultSingleFile',
|
||||||
|
'size' => 3028,
|
||||||
|
'file_name' => 'beispiel bild.jpg',
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
]);
|
||||||
|
Storage::disk('temp')->assertExists('media-library/beispiel bild.jpg');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it handles authorization with collection', function () {
|
||||||
|
$this->auth(['storeMedia' => ['collection' => 'rtrt']])->registerModel();
|
||||||
|
$content = base64_encode($this->pdfFile()->getContent());
|
||||||
|
|
||||||
|
$this->postJson('/mediaupload', [
|
||||||
|
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => null],
|
||||||
|
'payload' => [
|
||||||
|
'content' => $content,
|
||||||
|
'name' => 'beispiel bild.jpg',
|
||||||
|
],
|
||||||
|
])->assertStatus(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it handles authorization with collection correctly', function () {
|
||||||
|
$this->auth(['storeMedia' => ['collection' => 'defaultSingleFile']])->registerModel();
|
||||||
|
$content = base64_encode($this->pdfFile()->getContent());
|
||||||
|
|
||||||
|
$this->postJson('/mediaupload', [
|
||||||
|
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => null],
|
||||||
|
'payload' => [
|
||||||
|
'content' => $content,
|
||||||
|
'name' => 'beispiel bild.jpg',
|
||||||
|
],
|
||||||
|
])->assertStatus(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it needs a collection', function ($key, $value) {
|
||||||
|
$this->auth()->registerModel();
|
||||||
|
$content = base64_encode($this->pdfFile()->getContent());
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'parent' => ['model' => 'post', 'collection' => '', 'id' => null],
|
||||||
|
'payload' => [
|
||||||
|
'content' => $content,
|
||||||
|
'name' => 'beispiel bild.jpg',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
data_set($payload, $key, $value);
|
||||||
|
|
||||||
|
$this->postJson('/mediaupload', $payload)->assertJsonValidationErrors($key);
|
||||||
|
})->with(function () {
|
||||||
|
yield ['parent.collection', ''];
|
||||||
|
yield ['parent.collection', -1];
|
||||||
|
yield ['parent.collection', 'missingcollection'];
|
||||||
|
yield ['parent.model', 'lalala'];
|
||||||
|
yield ['parent.model', -1];
|
||||||
|
yield ['parent.model', ''];
|
||||||
|
});
|
|
@ -20,5 +20,5 @@ test('modifies config file', function () {
|
||||||
$this->setUpTheTestEnvironment();
|
$this->setUpTheTestEnvironment();
|
||||||
|
|
||||||
$this->assertEquals('lala', config('media-library.image_driver'));
|
$this->assertEquals('lala', config('media-library.image_driver'));
|
||||||
$this->assertEquals('temp', config('media-library.temp_storage'));
|
$this->assertEquals('temp', config('media-library.temp_disk'));
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,12 +29,13 @@ test('it uploads a single file to a single file collection', function () {
|
||||||
$response->assertJsonPath('name', 'beispiel bild');
|
$response->assertJsonPath('name', 'beispiel bild');
|
||||||
$response->assertJsonPath('collection_name', 'defaultSingleFile');
|
$response->assertJsonPath('collection_name', 'defaultSingleFile');
|
||||||
$response->assertJsonPath('file_name', 'beispiel-bild.jpg');
|
$response->assertJsonPath('file_name', 'beispiel-bild.jpg');
|
||||||
|
$response->assertJsonPath('is_deferred', false);
|
||||||
$response->assertJsonMissingPath('model_type');
|
$response->assertJsonMissingPath('model_type');
|
||||||
$response->assertJsonMissingPath('model_id');
|
$response->assertJsonMissingPath('model_id');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it uploads a single image to a single file collection', function () {
|
test('it uploads a single image to a single file collection', function () {
|
||||||
$this->auth()->registerModel();
|
$this->auth()->registerModel()->withoutExceptionHandling();
|
||||||
$post = $this->newPost();
|
$post = $this->newPost();
|
||||||
$content = base64_encode($this->jpgFile()->getContent());
|
$content = base64_encode($this->jpgFile()->getContent());
|
||||||
|
|
||||||
|
@ -289,7 +290,6 @@ test('it needs validation for single files', function (array $payloadOverwrites,
|
||||||
})->with(function () {
|
})->with(function () {
|
||||||
yield [['parent.model' => 'missingmodel'], 'parent.model'];
|
yield [['parent.model' => 'missingmodel'], 'parent.model'];
|
||||||
yield [['parent.id' => -1], 'parent.id'];
|
yield [['parent.id' => -1], 'parent.id'];
|
||||||
yield [['parent.id' => ''], 'parent.id'];
|
|
||||||
yield [['parent.collection' => 'missingcollection'], 'parent.collection'];
|
yield [['parent.collection' => 'missingcollection'], 'parent.collection'];
|
||||||
yield [['payload.content' => []], 'payload.content'];
|
yield [['payload.content' => []], 'payload.content'];
|
||||||
yield [['payload.content' => ['UU']], 'payload.content'];
|
yield [['payload.content' => ['UU']], 'payload.content'];
|
||||||
|
|
|
@ -7,5 +7,5 @@ use Workbench\App\Policies\PostPolicy;
|
||||||
|
|
||||||
uses(Zoomyboy\MedialibraryHelper\Tests\TestCase::class)->in('Feature');
|
uses(Zoomyboy\MedialibraryHelper\Tests\TestCase::class)->in('Feature');
|
||||||
uses(Illuminate\Foundation\Testing\RefreshDatabase::class)->in('Feature');
|
uses(Illuminate\Foundation\Testing\RefreshDatabase::class)->in('Feature');
|
||||||
uses()->beforeEach(fn () => Storage::fake('media'))->in('Feature');
|
uses()->beforeEach(fn () => Storage::fake('media') && Storage::fake('temp'))->in('Feature');
|
||||||
uses()->beforeEach(fn () => Gate::policy(Post::class, PostPolicy::class))->in('Feature');
|
uses()->beforeEach(fn () => Gate::policy(Post::class, PostPolicy::class))->in('Feature');
|
||||||
|
|
|
@ -20,12 +20,16 @@ class PostPolicy
|
||||||
&& data_get($user->policies, 'listMedia.collection') === $collection;
|
&& data_get($user->policies, 'listMedia.collection') === $collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function storeMedia(User $user, HasMedia $model, string $collection): bool
|
public function storeMedia(User $user, ?HasMedia $model, ?string $collection = null): bool
|
||||||
{
|
{
|
||||||
if (is_bool($user->policies['storeMedia'])) {
|
if (is_bool($user->policies['storeMedia'])) {
|
||||||
return $user->policies['storeMedia'] === true;
|
return $user->policies['storeMedia'] === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_null($model)) {
|
||||||
|
return data_get($user->policies, 'storeMedia.collection') === $collection;
|
||||||
|
}
|
||||||
|
|
||||||
return data_get($user->policies, 'storeMedia.id') === $model->id
|
return data_get($user->policies, 'storeMedia.id') === $model->id
|
||||||
&& data_get($user->policies, 'storeMedia.collection') === $collection;
|
&& data_get($user->policies, 'storeMedia.collection') === $collection;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue