From d1ce6d5ec9f0ccb204a722766dfd377645b2faf9 Mon Sep 17 00:00:00 2001 From: "Pedro X." Date: Fri, 31 May 2024 14:41:04 +0100 Subject: [PATCH 01/21] wip --- composer.json | 2 +- src/Uploaders/MediaUploader.php | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 659a5dc..83e886b 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "Laravel", "Backpack", "Backpack for Laravel", "Backpack Addon", "spatie medialibrary uploaders" ], "require": { - "backpack/crud": "^6.0", + "backpack/crud": "^6.8", "spatie/laravel-medialibrary": "^10.7|^11.3" }, "require-dev": { diff --git a/src/Uploaders/MediaUploader.php b/src/Uploaders/MediaUploader.php index a9462da..b18f433 100644 --- a/src/Uploaders/MediaUploader.php +++ b/src/Uploaders/MediaUploader.php @@ -90,11 +90,11 @@ public function retrieveUploadedFiles(Model $entry): Model if (! is_array($values)) { $values = json_decode($values, true); } - + $repeatableUploaders = array_merge(app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()), [$this]); foreach ($repeatableUploaders as $uploader) { $uploadValues = $uploader->getPreviousRepeatableValues($entry); - + $values = $this->mergeValuesRecursive($values, $uploadValues); } @@ -131,7 +131,7 @@ protected function get(HasMedia|Model $entry) return $media->first(); } - protected function processRepeatableUploads(Model $entry, Collection $values): Collection + protected function processRepeatableUploads(Model $entry, Collection $values): array { foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) { $uploader->uploadRepeatableFiles($values->pluck($uploader->getName())->toArray(), $uploader->getPreviousRepeatableMedia($entry), $entry); @@ -140,7 +140,7 @@ protected function processRepeatableUploads(Model $entry, Collection $values): C unset($item[$uploader->getName()]); return $item; - }); + })->toArray(); } return $values; @@ -207,9 +207,9 @@ protected function getPreviousRepeatableMedia(Model $entry) $previousMedia = $this->get($entry)->transform(function ($item) { return [$this->getName() => $item, 'order_column' => $item->getCustomProperty('repeatableRow')]; }); - $previousMedia->each(function($item) use (&$orderedMedia) { + $previousMedia->each(function ($item) use (&$orderedMedia) { $orderedMedia[] = $item[$this->getName()]; - }); + }); return $orderedMedia; } @@ -236,7 +236,6 @@ private function initFileAdder($entry, $file) if (get_class($file) === File::class) { return $entry->addMedia($file->getPathName()); } - } private function getConversionToDisplay($item) From cf971b3a0189d27f4327671874caafa6d53b1b76 Mon Sep 17 00:00:00 2001 From: "Pedro X." Date: Thu, 20 Jun 2024 17:07:25 +0100 Subject: [PATCH 02/21] refactor, add tests --- .gitignore | 9 + composer.json | 12 +- phpunit.xml | 43 ++--- readme.md | 14 +- src/AddonServiceProvider.php | 9 +- src/Uploaders/MediaAjaxUploader.php | 54 ++---- src/Uploaders/MediaDropzoneUploader.php | 139 ++++++++++++++++ src/Uploaders/MediaSingleBase64Image.php | 2 +- src/Uploaders/MediaUploader.php | 84 ++++------ src/Uploaders/Traits/AddMediaToModels.php | 34 ++++ src/Uploaders/Traits/IdentifiesMedia.php | 27 +++ .../MediaUploaderCrudController.php | 50 ++++++ .../Migrations/create_media_table.php | 36 ++++ .../create_media_uploader_table.php | 19 +++ tests/Config/Models/MediaUploader.php | 22 +++ tests/Feature/MediaUploadersTest.php | 155 ++++++++++++++++++ tests/FeatureTestCase.php | 42 +++++ 17 files changed, 618 insertions(+), 133 deletions(-) create mode 100644 .gitignore create mode 100644 src/Uploaders/MediaDropzoneUploader.php create mode 100644 src/Uploaders/Traits/AddMediaToModels.php create mode 100644 src/Uploaders/Traits/IdentifiesMedia.php create mode 100644 tests/Config/Controllers/MediaUploaderCrudController.php create mode 100644 tests/Config/Database/Migrations/create_media_table.php create mode 100644 tests/Config/Database/Migrations/create_media_uploader_table.php create mode 100644 tests/Config/Models/MediaUploader.php create mode 100644 tests/Feature/MediaUploadersTest.php create mode 100644 tests/FeatureTestCase.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c0111d --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.idea/ +vendor/ +node_modules/ +.DS_Store +.composer.lock +composer.lock +.phpunit.result.cache +src/public/packages/ +/.phpunit.cache \ No newline at end of file diff --git a/composer.json b/composer.json index 83e886b..603cc83 100644 --- a/composer.json +++ b/composer.json @@ -14,18 +14,24 @@ "Laravel", "Backpack", "Backpack for Laravel", "Backpack Addon", "spatie medialibrary uploaders" ], "require": { - "backpack/crud": "^6.8", + "backpack/crud": "dev-fix-uploaders as 6.7", "spatie/laravel-medialibrary": "^10.7|^11.3" }, "require-dev": { - "phpunit/phpunit": "^9.0|^10.0", - "orchestra/testbench": "~6|^8.0" + "phpunit/phpunit": "^10.0|^11.0", + "orchestra/testbench": "^8.0|^9.0|^10.0" }, "autoload": { "psr-4": { "Backpack\\MediaLibraryUploaders\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Backpack\\MediaLibraryUploaders\\Tests\\": "tests", + "Backpack\\CRUD\\Tests\\": "vendor/backpack/crud/tests" + } + }, "scripts": { "test": "vendor/bin/phpunit --testdox" }, diff --git a/phpunit.xml b/phpunit.xml index ce34605..1d55086 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,22 +1,25 @@ - - - - ./tests/ - - - - - src/ - - + + + + ./tests/Feature + + + + + + + + + + + + + + + + + src/ + + diff --git a/readme.md b/readme.md index 4d471dd..3673045 100644 --- a/readme.md +++ b/readme.md @@ -12,11 +12,11 @@ More exactly, it provides the `->withMedia()` helper, that will handle the file ## Requirements -**Install and use `spatie/laravel-medialibrary` v10**. If you haven't already, please make sure you've installed `spatie/laravel-medialibrary` and followed all installation steps in [their docs](https://spatie.be/docs/laravel-medialibrary/v10/installation-setup): +**Install and use `spatie/laravel-medialibrary` v10|v11**. If you haven't already, please make sure you've installed `spatie/laravel-medialibrary` and followed all installation steps in [their docs](https://spatie.be/docs/laravel-medialibrary/v11/installation-setup): ``` bash # require the package -composer require "spatie/laravel-medialibrary:^10.0.0" +composer require "spatie/laravel-medialibrary:^11.0" # prepare the database # NOTE: Spatie migration does not come with a `down()` method by default, add one now if you need it @@ -33,7 +33,7 @@ php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServicePr ``` -Then prepare your Models to use `spatie/laravel-medialibrary`, by adding the `InteractsWithMedia` trait to your model and implement the `HasMedia` interface like explained on [Media Library Documentation](https://spatie.be/docs/laravel-medialibrary/v10/basic-usage/preparing-your-model). +Then prepare your Models to use `spatie/laravel-medialibrary`, by adding the `InteractsWithMedia` trait to your model and implement the `HasMedia` interface like explained on [Media Library Documentation](https://spatie.be/docs/laravel-medialibrary/v11/basic-usage/preparing-your-model). ## Installation @@ -100,11 +100,11 @@ CRUD::field('main_image') ]); ``` -**NOTE:** Some methods will be called automatically by Backpack; You shoudn't call them inside the closure used for configuration: `toMediaCollection()`, `setName()`, `usingName()`, `setOrder()`, `toMediaCollectionFromRemote()` and `toMediaLibrary()`. They will throw an error if you manually try to call them in the closure. +**NOTE:** Some methods will be called automatically by Backpack; You shouldn't call them inside the closure used for configuration: `toMediaCollection()`, `setName()`, `usingName()`, `setOrder()`, `toMediaCollectionFromRemote()` and `toMediaLibrary()`. They will throw an error if you manually try to call them in the closure. ### Defining media collection in the model -You can also have the collection configured in your model as explained in [Spatie Documentation](https://spatie.be/docs/laravel-medialibrary/v10/working-with-media-collections/defining-media-collections), in that case, you just need to pass the `collection` configuration key. But you are still able to configure all the other options including the `whenSaving` callback. +You can also have the collection configured in your model as explained in [Spatie Documentation](https://spatie.be/docs/laravel-medialibrary/v11/working-with-media-collections/defining-media-collections), in that case, you just need to pass the `collection` configuration key. But you are still able to configure all the other options including the `whenSaving` callback. ```php // In your Model.php @@ -151,7 +151,7 @@ CRUD::field('main_image') 'displayConversions' => 'thumb' ]); -// you can also configure aditional manipulations in the `whenSaving` callback +// you can also configure additional manipulations in the `whenSaving` callback ->withMedia([ 'displayConversions' => 'thumb', 'whenSaving' => function($media) { @@ -183,7 +183,7 @@ You can normally assign custom properties to your media with `->withCustomProper ## Change log -Changes are documented here on Github. Please see the [Releases tab](https://github.com/backpack/media-library-connector/releases). +Changes are documented here on Github. Please see the [Releases tab](https://github.com/Laravel-Backpack/medialibrary-uploaders/releases). ## Testing diff --git a/src/AddonServiceProvider.php b/src/AddonServiceProvider.php index f372134..9a2fa4a 100644 --- a/src/AddonServiceProvider.php +++ b/src/AddonServiceProvider.php @@ -5,7 +5,7 @@ use Backpack\CRUD\app\Library\CrudPanel\CrudColumn; use Backpack\CRUD\app\Library\CrudPanel\CrudField; use Backpack\CRUD\app\Library\Uploaders\Support\RegisterUploadEvents; -use Backpack\MediaLibraryUploaders\Uploaders\MediaAjaxUploader; +use Backpack\MediaLibraryUploaders\Uploaders\MediaDropzoneUploader; use Backpack\MediaLibraryUploaders\Uploaders\MediaMultipleFiles; use Backpack\MediaLibraryUploaders\Uploaders\MediaSingleBase64Image; use Backpack\MediaLibraryUploaders\Uploaders\MediaSingleFile; @@ -30,9 +30,14 @@ public function boot() 'image' => MediaSingleBase64Image::class, 'upload' => MediaSingleFile::class, 'upload_multiple' => MediaMultipleFiles::class, - 'dropzone' => MediaAjaxUploader::class, ], 'withMedia'); + if (class_exists(\Backpack\Pro\Uploads\BackpackAjaxUploader::class)) { + app('UploadersRepository')->addUploaderClasses([ + 'dropzone' => MediaDropzoneUploader::class, + ], 'withMedia'); + } + // register media upload macros on crud fields and columns. if (! CrudField::hasMacro('withMedia')) { CrudField::macro('withMedia', function ($uploadDefinition = [], $subfield = null, $registerEvents = true) { diff --git a/src/Uploaders/MediaAjaxUploader.php b/src/Uploaders/MediaAjaxUploader.php index ec247a7..3a8bd28 100644 --- a/src/Uploaders/MediaAjaxUploader.php +++ b/src/Uploaders/MediaAjaxUploader.php @@ -3,49 +3,14 @@ namespace Backpack\MediaLibraryUploaders\Uploaders; use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD; -use Backpack\CRUD\app\Library\Uploaders\Support\Interfaces\UploaderInterface; -use Illuminate\Database\Eloquent\Model; +use Backpack\Pro\Uploads\BackpackAjaxUploader; use Illuminate\Support\Facades\Storage; use Symfony\Component\HttpFoundation\File\File; -class MediaAjaxUploader extends MediaUploader +class MediaAjaxUploader extends BackpackAjaxUploader { - public static function for(array $field, $configuration): UploaderInterface - { - return (new self($field, $configuration))->multiple(); - } - - public function uploadFiles(Model $entry, $value = null) - { - $temporaryDisk = CRUD::get('dropzone.temporary_disk'); - $temporaryFolder = CRUD::get('dropzone.temporary_folder'); - - $uploads = $value ?? CRUD::getRequest()->input($this->getName()) ?? []; - - $uploads = is_array($uploads) ? $uploads : (json_decode($uploads, true) ?? []); - - $uploadedFiles = array_filter($uploads, function ($value) use ($temporaryFolder, $temporaryDisk) { - return strpos($value, $temporaryFolder) !== false && Storage::disk($temporaryDisk)->exists($value); - }); - - $previousSentFiles = array_filter($uploads, function ($value) use ($temporaryFolder) { - return strpos($value, $temporaryFolder) === false; - }); - - $previousFiles = $this->get($entry); - - foreach ($previousFiles as $previousFile) { - if (! in_array($this->getMediaIdentifier($previousFile, $entry), $previousSentFiles)) { - $previousFile->delete(); - } - } - - foreach ($uploadedFiles as $key => $value) { - $file = new File(Storage::disk($temporaryDisk)->path($value)); - - $this->addMediaFile($entry, $file); - } - } + use Traits\IdentifiesMedia; + use Traits\AddMediaToModels; public function uploadRepeatableFiles($values, $previousValues, $entry = null) { @@ -83,15 +48,16 @@ public function uploadRepeatableFiles($values, $previousValues, $entry = null) $fileIdentifier = $this->getMediaIdentifier($previousFile, $entry); if (empty($sentFiles)) { $previousFile->delete(); + continue; } - + $foundInSentFiles = false; - foreach($sentFiles as $row => $sentFilesInRow) { + foreach ($sentFiles as $row => $sentFilesInRow) { $fileWasSent = array_search($fileIdentifier, $sentFilesInRow, true); - if($fileWasSent !== false) { + if ($fileWasSent !== false) { $foundInSentFiles = true; - if($row !== $previousFile->getCustomProperty('repeatableRow')) { + if ($row !== $previousFile->getCustomProperty('repeatableRow')) { $previousFile->setCustomProperty('repeatableRow', $row); $previousFile->save(); // avoid checking the same file twice. This is a performance improvement. @@ -102,7 +68,7 @@ public function uploadRepeatableFiles($values, $previousValues, $entry = null) } if ($foundInSentFiles === false) { - $previousFile->delete(); + $previousFile->delete(); } } } diff --git a/src/Uploaders/MediaDropzoneUploader.php b/src/Uploaders/MediaDropzoneUploader.php new file mode 100644 index 0000000..2c4bdf8 --- /dev/null +++ b/src/Uploaders/MediaDropzoneUploader.php @@ -0,0 +1,139 @@ +multiple(); + } + + public function uploadFiles(Model $entry, $value = null) + { + $uploads = $value ?? CRUD::getRequest()->input($this->getName()) ?? []; + + $uploads = is_array($uploads) ? $uploads : (json_decode($uploads, true) ?? []); + + $uploadedFiles = array_filter($uploads, function ($value) { + return strpos($value, $this->temporaryFolder) !== false; + }); + + $previousSentFiles = array_filter($uploads, function ($value) { + return strpos($value, $this->temporaryFolder) === false; + }); + + $previousDatabaseFiles = $this->getPreviousFiles($entry) ?? []; + + foreach ($previousDatabaseFiles as $previousFile) { + if (! in_array($this->getMediaIdentifier($previousFile, $entry), $previousSentFiles)) { + $previousFile->delete(); + } + } + + foreach ($uploadedFiles as $key => $value) { + $file = new File(Storage::disk($this->temporaryDisk)->path($value)); + + $this->addMediaFile($entry, $file); + } + } + + public function uploadRepeatableFiles($values, $previousValues, $entry = null) + { + $values = array_map(function ($value) { + return is_array($value) ? $value : (json_decode($value, true) ?? []); + }, $values); + + foreach ($values as $row => $files) { + $files = is_array($files) ? $files : (json_decode($files, true) ?? []); + + $uploadedFiles = array_filter($files, function ($value) { + return strpos($value, $this->temporaryFolder) !== false; + }); + + foreach ($uploadedFiles ?? [] as $key => $file) { + try { + $name = substr($file, strrpos($file, '/') + 1); + + $temporaryFile = $this->temporaryDisk->get($file); + + $this->permanentDisk->put($this->getPath().$name, $temporaryFile); + + $this->temporaryDisk->delete($file); + + $file = str_replace(Str::finish($this->temporaryFolder, '/'), $this->getPath(), $file); + + $values[$row][$key] = $file; + } catch (\Throwable $th) { + Log::error($th->getMessage()); + Alert::error('An error occurred uploading files. Check log files.')->flash(); + } + } + } + + $previousValuesArray = Arr::flatten(Arr::map($previousValues, function ($value) { + return ! is_array($value) ? json_decode($value, true) ?? [] : $value; + })); + + $currentValuesArray = Arr::flatten(Arr::map($values, function ($value) { + return ! is_array($value) ? json_decode($value, true) ?? [] : $value; + })); + + $filesToDelete = array_diff($previousValuesArray, $currentValuesArray); + + foreach ($filesToDelete as $key => $value) { + $this->permanentDisk->delete($this->getPath().$value); + } + + foreach ($values as $row => $value) { + if (empty($value)) { + unset($values[$row]); + } + } + + return $values; + } + + protected function ajaxEndpointSuccessResponse($files = null): \Illuminate\Http\JsonResponse + { + return $files ? + response()->json(['files' => $files, 'success' => true]) : + response()->json(['success' => true]); + } + + protected function ajaxEndpointErrorMessage(string $message = 'An error occurred while processing the file.'): \Illuminate\Http\JsonResponse + { + return response()->json([ + 'message' => $message, + 'success' => false, + ], 400); + } + + protected function buildAjaxEndpointValidationFilesArray($validationKey, $uploadedFiles, $requestInputName): array + { + $previousUploadedFiles = json_decode(CRUD::getRequest()->input('previousUploadedFiles'), true) ?? []; + + if (Str::contains($validationKey, '.*.')) { + return [ + 'validate_ajax_endpoint' => true, + Str::before($validationKey, '.*') => [ + 0 => [ + Str::after($validationKey, '*.') => array_merge($uploadedFiles[$requestInputName], $previousUploadedFiles), + ], + ], + ]; + } + + return array_merge($uploadedFiles[$requestInputName], $previousUploadedFiles, ['validate_ajax_endpoint' => true]); + } +} diff --git a/src/Uploaders/MediaSingleBase64Image.php b/src/Uploaders/MediaSingleBase64Image.php index ee66c86..e697fff 100644 --- a/src/Uploaders/MediaSingleBase64Image.php +++ b/src/Uploaders/MediaSingleBase64Image.php @@ -12,7 +12,7 @@ public function uploadFiles(Model $entry, $values = null) { $value = $value ?? CrudPanelFacade::getRequest()->get($this->getName()); - $previousImage = $this->get($entry); + $previousImage = $this->getPreviousFiles($entry); if (! $value && $previousImage) { $previousImage->delete(); diff --git a/src/Uploaders/MediaUploader.php b/src/Uploaders/MediaUploader.php index b18f433..d654fe1 100644 --- a/src/Uploaders/MediaUploader.php +++ b/src/Uploaders/MediaUploader.php @@ -3,18 +3,18 @@ namespace Backpack\MediaLibraryUploaders\Uploaders; use Backpack\CRUD\app\Library\Uploaders\Uploader; -use Backpack\MediaLibraryUploaders\ConstrainedFileAdder; -use Exception; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\UploadedFile; use Illuminate\Support\Collection; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\MediaCollections\Models\Media; -use Spatie\MediaLibrary\Support\PathGenerator\PathGeneratorFactory; use Symfony\Component\HttpFoundation\File\File; abstract class MediaUploader extends Uploader { + use Traits\IdentifiesMedia; + use Traits\AddMediaToModels; + public $mediaName; public $collection; @@ -40,11 +40,11 @@ public function __construct(array $crudObject, array $configuration) }) ->first(); - $configuration['disk'] = $modelDefinition?->diskName ?? null; - - $configuration['disk'] = empty($configuration['disk']) ? $crudObject['disk'] ?? config('media-library.disk_name') : null; + $configuration['disk'] ??= $modelDefinition?->diskName ?? null; - // read https://spatie.be/docs/laravel-medialibrary/v10/advanced-usage/using-a-custom-directory-structure#main + $configuration['disk'] = empty($configuration['disk']) ? ($crudObject['disk'] ?? config('media-library.disk_name')) : $configuration['disk']; + //dd($configuration['disk'], $crudObject); + // read https://spatie.be/docs/laravel-medialibrary/v11/advanced-usage/using-a-custom-directory-structure#main // on how to customize file directory $crudObject['prefix'] = $configuration['path'] = ''; @@ -66,15 +66,15 @@ public function storeUploadedFiles(Model $entry): Model // or using guarded in their models. $entry->offsetUnset($this->getName()); // setting the raw attributes makes sure the `attributeCastCache` property is cleared, preventing - // uploaded files from beeing re-added to the entry from the cache. + // uploaded files from being re-added to the entry from the cache. $entry = $entry->setRawAttributes($entry->getAttributes()); return $entry; } public function retrieveUploadedFiles(Model $entry): Model - { - $media = $this->get($entry); + { + $media = $this->getPreviousFiles($entry); if (! $media) { return $entry; @@ -131,6 +131,22 @@ protected function get(HasMedia|Model $entry) return $media->first(); } + public function getPreviousFiles(Model $entry): mixed + { + $media = $entry->getMedia($this->collection, function ($media) use ($entry) { + /** @var Media $media */ + return $media->getCustomProperty('name') === $this->getName() && + $media->getCustomProperty('repeatableContainerName') === $this->repeatableContainerName && + $entry->{$entry->getKeyName()} === $media->getAttribute('model_id'); + }); + + if ($this->canHandleMultipleFiles() || $this->handleRepeatableFiles) { + return $media; + } + + return $media->first(); + } + protected function processRepeatableUploads(Model $entry, Collection $values): array { foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) { @@ -140,35 +156,10 @@ protected function processRepeatableUploads(Model $entry, Collection $values): a unset($item[$uploader->getName()]); return $item; - })->toArray(); + }); } - return $values; - } - - protected function addMediaFile($entry, $file, $order = null) - { - $this->order = $order; - - $fileAdder = $this->initFileAdder($entry, $file); - - $fileAdder = $fileAdder->usingName($this->mediaName) - ->withCustomProperties($this->getCustomProperties()) - ->usingFileName($this->getFileName($file)); - - $constrainedMedia = new ConstrainedFileAdder(); - $constrainedMedia->setFileAdder($fileAdder); - $constrainedMedia->setMediaUploader($this); - - if ($this->savingEventCallback && is_callable($this->savingEventCallback)) { - $constrainedMedia = call_user_func_array($this->savingEventCallback, [$constrainedMedia, $this]); - } - - if (! $constrainedMedia) { - throw new Exception('Please return a valid class from `whenSaving` closure. Field: '.$this->getName()); - } - - $constrainedMedia->getFileAdder()->toMediaCollection($this->collection, $this->getDisk()); + return $values->toArray(); } protected function getPreviousRepeatableValues(Model $entry) @@ -260,23 +251,4 @@ public function getCustomProperties() 'repeatableRow' => $this->order, ]; } - - public function getMediaIdentifier($media, $entry = null) - { - $path = PathGeneratorFactory::create($media); - - if ($entry && ! empty($entry->mediaConversions)) { - $conversion = array_values(array_filter($entry->mediaConversions, function ($item) use ($media) { - return $item->getName() === $this->getConversionToDisplay($media); - }))[0] ?? null; - - if (! $conversion) { - return $path->getPath($media).$media->file_name; - } - - return $path->getPathForConversions($media).$conversion->getConversionFile($media); - } - - return $path->getPath($media).$media->file_name; - } } diff --git a/src/Uploaders/Traits/AddMediaToModels.php b/src/Uploaders/Traits/AddMediaToModels.php new file mode 100644 index 0000000..af9bda4 --- /dev/null +++ b/src/Uploaders/Traits/AddMediaToModels.php @@ -0,0 +1,34 @@ +order = $order; + + $fileAdder = $this->initFileAdder($entry, $file); + + $fileAdder = $fileAdder->usingName($this->mediaName) + ->withCustomProperties($this->getCustomProperties()) + ->usingFileName($this->getFileName($file)); + + $constrainedMedia = new ConstrainedFileAdder(); + $constrainedMedia->setFileAdder($fileAdder); + $constrainedMedia->setMediaUploader($this); + + if ($this->savingEventCallback && is_callable($this->savingEventCallback)) { + $constrainedMedia = call_user_func_array($this->savingEventCallback, [$constrainedMedia, $this]); + } + + if (! $constrainedMedia) { + throw new Exception('Please return a valid class from `whenSaving` closure. Field: '.$this->getName()); + } + + $constrainedMedia->getFileAdder()->toMediaCollection($this->collection, $this->getDisk()); + } +} diff --git a/src/Uploaders/Traits/IdentifiesMedia.php b/src/Uploaders/Traits/IdentifiesMedia.php new file mode 100644 index 0000000..a899ba5 --- /dev/null +++ b/src/Uploaders/Traits/IdentifiesMedia.php @@ -0,0 +1,27 @@ +mediaConversions)) { + $conversion = array_filter($entry->mediaConversions, function ($item) use ($media) { + return $item->getName() === $this->getConversionToDisplay($media); + })[0] ?? null; + + if (! $conversion) { + return $path->getPath($media).$media->file_name; + } + + return $path->getPathForConversions($media).$conversion->getConversionFile($media); + } + + return $path->getPath($media).$media->file_name; + } +} \ No newline at end of file diff --git a/tests/Config/Controllers/MediaUploaderCrudController.php b/tests/Config/Controllers/MediaUploaderCrudController.php new file mode 100644 index 0000000..ee496f6 --- /dev/null +++ b/tests/Config/Controllers/MediaUploaderCrudController.php @@ -0,0 +1,50 @@ +crud->setRoute(config('backpack.base.route_prefix').'/media-uploader'); + $this->crud->setModel(MediaUploader::class); + } + + protected function setupCreateOperation() + { + CRUD::field('upload')->type('upload')->withMedia([ + 'disk' => 'uploaders', + 'fileNamer' => fn ($file) => $file->getClientOriginalName(), + 'whenSaving' => function($spatieMedia, $backpackMediaObject) { + return $spatieMedia->preservingOriginal(); + } + ]); + CRUD::field('upload_multiple')->type('upload_multiple')->withMedia([ + 'disk' => 'uploaders', + 'fileNamer' => fn ($file) => $file->getClientOriginalName(), + 'whenSaving' => function($spatieMedia, $backpackMediaObject) { + return $spatieMedia->preservingOriginal(); + } + ]); + } + + protected function setupUpdateOperation() + { + $this->setupCreateOperation(); + } + + protected function setupDeleteOperation() + { + $this->setupCreateOperation(); + } +} diff --git a/tests/Config/Database/Migrations/create_media_table.php b/tests/Config/Database/Migrations/create_media_table.php new file mode 100644 index 0000000..ea4bd59 --- /dev/null +++ b/tests/Config/Database/Migrations/create_media_table.php @@ -0,0 +1,36 @@ +id(); + + $table->morphs('model'); + $table->uuid('uuid')->nullable()->unique(); + $table->string('collection_name'); + $table->string('name'); + $table->string('file_name'); + $table->string('mime_type')->nullable(); + $table->string('disk'); + $table->string('conversions_disk')->nullable(); + $table->unsignedBigInteger('size'); + $table->json('manipulations'); + $table->json('custom_properties'); + $table->json('generated_conversions'); + $table->json('responsive_images'); + $table->unsignedInteger('order_column')->nullable()->index(); + + $table->nullableTimestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('media'); + } +}; diff --git a/tests/Config/Database/Migrations/create_media_uploader_table.php b/tests/Config/Database/Migrations/create_media_uploader_table.php new file mode 100644 index 0000000..8fecc8f --- /dev/null +++ b/tests/Config/Database/Migrations/create_media_uploader_table.php @@ -0,0 +1,19 @@ +id(); + }); + } + + public function down(): void + { + Schema::dropIfExists('media_uploaders'); + } +}; diff --git a/tests/Config/Models/MediaUploader.php b/tests/Config/Models/MediaUploader.php new file mode 100644 index 0000000..bb88ccb --- /dev/null +++ b/tests/Config/Models/MediaUploader.php @@ -0,0 +1,22 @@ +crud(config('backpack.base.route_prefix').'/media-uploader', MediaUploaderCrudController::class); + } + + public function setUp(): void + { + parent::setUp(); + + $this->testBaseUrl = config('backpack.base.route_prefix').'/media-uploader'; + } + + public function test_it_can_access_the_uploaders_create_page() + { + $response = $this->get($this->testBaseUrl.'/create'); + $response->assertStatus(200); + } + + public function test_it_can_upload_a_single_file() + { + $response = $this->post($this->testBaseUrl, [ + 'upload' => $this->getUploadedFile('avatar1.jpg'), + ]); + + $response->assertStatus(302); + + $response->assertRedirect($this->testBaseUrl); + + $this->assertDatabaseCount('media_uploaders', 1); + + $uploader = MediaUploader::first(); + + $this->assertEquals(1, $uploader->getMedia()->count()); + + $this->assertTrue(Storage::disk('uploaders')->exists('1/avatar1.jpg')); + } + + public function test_it_can_upload_multiple_files() + { + $response = $this->post($this->testBaseUrl, [ + 'upload_multiple' => [ + $this->getUploadedFile('avatar1.jpg'), + $this->getUploadedFile('avatar2.jpg'), + ], + ]); + + $response->assertStatus(302); + + $response->assertRedirect($this->testBaseUrl); + + $this->assertDatabaseCount('media_uploaders', 1); + + $uploader = MediaUploader::first(); + + $this->assertEquals(2, $uploader->getMedia()->count()); + + $this->assertTrue(Storage::disk('uploaders')->exists('1/avatar1.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('2/avatar2.jpg')); + } + + public function test_it_can_upload_files_for_multiple_uploaders() + { + $response = $this->post($this->testBaseUrl, [ + 'upload' => $this->getUploadedFile('avatar1.jpg'), + 'upload_multiple' => [ + $this->getUploadedFile('avatar2.jpg'), + $this->getUploadedFile('avatar3.jpg'), + ], + ]); + + $response->assertStatus(302); + + $response->assertRedirect($this->testBaseUrl); + + $this->assertDatabaseCount('media_uploaders', 1); + + $uploader = MediaUploader::first(); + + $this->assertEquals(3, $uploader->getMedia()->count()); + + $this->assertTrue(Storage::disk('uploaders')->exists('1/avatar1.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('2/avatar2.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('3/avatar3.jpg')); + } + + public function test_it_display_the_edit_page_without_files() + { + self::initUploader(); + + $response = $this->get($this->testBaseUrl.'/1/edit'); + $response->assertStatus(200); + } + + /** + * Undocumented function + * + * @group fail + */ + public function test_it_display_the_upload_page_with_files() + { + self::initUploaderWithFiles(); + $response = $this->get($this->testBaseUrl.'/1/edit'); + + $response->assertStatus(200); + + $response->assertSee('avatar1.jpg'); + $response->assertSee('avatar2.jpg'); + $response->assertSee('avatar3.jpg'); + } + + + private static function initUploader() + { + $uploader = new MediaUploader(); + $uploader->save(); + } + + private function initUploaderWithFiles() + { + $uploader = new MediaUploader(); + $uploader->addMedia($this->getUploadedFile('avatar1.jpg'))->withCustomProperties([ + 'name' => 'upload', + 'repeatableContainerName' => null, + 'repeatableRow' => null, + ])->preservingOriginal()->toMediaCollection('default', 'uploaders'); + $uploader->addMedia($this->getUploadedFile('avatar2.jpg'))->withCustomProperties([ + 'name' => 'upload_multiple', + 'repeatableContainerName' => null, + 'repeatableRow' => null, + ])->preservingOriginal()->toMediaCollection('default', 'uploaders'); + $uploader->addMedia($this->getUploadedFile('avatar3.jpg'))->withCustomProperties([ + 'name' => 'upload_multiple', + 'repeatableContainerName' => null, + 'repeatableRow' => null, + ])->preservingOriginal()->toMediaCollection('default', 'uploaders'); + $uploader->save(); + } + + +} diff --git a/tests/FeatureTestCase.php b/tests/FeatureTestCase.php new file mode 100644 index 0000000..93aea1e --- /dev/null +++ b/tests/FeatureTestCase.php @@ -0,0 +1,42 @@ +loadMigrationsFrom([ + '--database' => 'testing', + '--path' => realpath(__DIR__.'/Config/Database/Migrations'), + ]); + + config(['filesystems.disks.uploaders' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => false, + ]]); + + + Storage::fake('uploaders'); + + $this->actingAs(User::find(1)); + } +} \ No newline at end of file From a8577c57e7a480193886f665fa944d94b2f183df Mon Sep 17 00:00:00 2001 From: "Pedro X." Date: Fri, 21 Jun 2024 15:20:28 +0100 Subject: [PATCH 03/21] wip --- src/Uploaders/MediaAjaxUploader.php | 13 ++ src/Uploaders/MediaDropzoneUploader.php | 3 +- src/Uploaders/MediaUploader.php | 39 +----- src/Uploaders/Traits/HasCollections.php | 9 ++ .../Traits/HasConstrainedFileAdder.php | 25 ++++ src/Uploaders/Traits/HasCustomProperties.php | 15 +++ src/Uploaders/Traits/HasMediaName.php | 12 ++ src/Uploaders/Traits/HasSavingCallback.php | 10 ++ tests/Feature/MediaUploadersTest.php | 122 +++++++++++++++--- 9 files changed, 198 insertions(+), 50 deletions(-) create mode 100644 src/Uploaders/Traits/HasCollections.php create mode 100644 src/Uploaders/Traits/HasConstrainedFileAdder.php create mode 100644 src/Uploaders/Traits/HasCustomProperties.php create mode 100644 src/Uploaders/Traits/HasMediaName.php create mode 100644 src/Uploaders/Traits/HasSavingCallback.php diff --git a/src/Uploaders/MediaAjaxUploader.php b/src/Uploaders/MediaAjaxUploader.php index 3a8bd28..e1fa575 100644 --- a/src/Uploaders/MediaAjaxUploader.php +++ b/src/Uploaders/MediaAjaxUploader.php @@ -11,6 +11,19 @@ class MediaAjaxUploader extends BackpackAjaxUploader { use Traits\IdentifiesMedia; use Traits\AddMediaToModels; + use Traits\HasConstrainedFileAdder; + use Traits\HasMediaName; + use Traits\HasCustomProperties; + use Traits\HasSavingCallback; + use Traits\HasCollections; + + public function __construct(array $crudObject, array $configuration) + { + parent::__construct($crudObject, $configuration); + $this->mediaName = $configuration['mediaName'] ?? $crudObject['name']; + $this->savingEventCallback = $configuration['whenSaving'] ?? null; + $this->collection = $configuration['collection'] ?? 'default'; + } public function uploadRepeatableFiles($values, $previousValues, $entry = null) { diff --git a/src/Uploaders/MediaDropzoneUploader.php b/src/Uploaders/MediaDropzoneUploader.php index 2c4bdf8..0f7eb0d 100644 --- a/src/Uploaders/MediaDropzoneUploader.php +++ b/src/Uploaders/MediaDropzoneUploader.php @@ -10,7 +10,6 @@ use Illuminate\Support\Str; use Prologue\Alerts\Facades\Alert; use Symfony\Component\HttpFoundation\File\File; -use Illuminate\Support\Facades\Storage; class MediaDropzoneUploader extends MediaAjaxUploader { @@ -42,7 +41,7 @@ public function uploadFiles(Model $entry, $value = null) } foreach ($uploadedFiles as $key => $value) { - $file = new File(Storage::disk($this->temporaryDisk)->path($value)); + $file = new File($this->temporaryDisk->path($value)); $this->addMediaFile($entry, $file); } diff --git a/src/Uploaders/MediaUploader.php b/src/Uploaders/MediaUploader.php index d654fe1..e0cd115 100644 --- a/src/Uploaders/MediaUploader.php +++ b/src/Uploaders/MediaUploader.php @@ -4,27 +4,24 @@ use Backpack\CRUD\app\Library\Uploaders\Uploader; use Illuminate\Database\Eloquent\Model; -use Illuminate\Http\UploadedFile; use Illuminate\Support\Collection; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\MediaCollections\Models\Media; -use Symfony\Component\HttpFoundation\File\File; abstract class MediaUploader extends Uploader { use Traits\IdentifiesMedia; use Traits\AddMediaToModels; - - public $mediaName; - - public $collection; + use Traits\HasConstrainedFileAdder; + use Traits\HasMediaName; + use Traits\HasCustomProperties; + use Traits\HasSavingCallback; + use Traits\HasCollections; public $displayConversions; public $order; - public $savingEventCallback = null; - public function __construct(array $crudObject, array $configuration) { $this->collection = $configuration['collection'] ?? 'default'; @@ -110,7 +107,7 @@ public function retrieveUploadedFiles(Model $entry): Model return $this->getMediaIdentifier($item, $entry); })->toArray(); } - + dd($entry); return $entry; } @@ -214,21 +211,6 @@ private function getModelInstance($crudObject): Model return new ($crudObject['baseModel'] ?? get_class(app('crud')->getModel())); } - private function initFileAdder($entry, $file) - { - if (is_a($file, UploadedFile::class, true)) { - return $entry->addMedia($file); - } - - if (is_string($file)) { - return $entry->addMediaFromBase64($file); - } - - if (get_class($file) === File::class) { - return $entry->addMedia($file->getPathName()); - } - } - private function getConversionToDisplay($item) { foreach ($this->displayConversions as $displayConversion) { @@ -243,12 +225,5 @@ private function getConversionToDisplay($item) /************************* * Helper methods * *************************/ - public function getCustomProperties() - { - return [ - 'name' => $this->getName(), - 'repeatableContainerName' => $this->repeatableContainerName, - 'repeatableRow' => $this->order, - ]; - } + } diff --git a/src/Uploaders/Traits/HasCollections.php b/src/Uploaders/Traits/HasCollections.php new file mode 100644 index 0000000..0ac6b2e --- /dev/null +++ b/src/Uploaders/Traits/HasCollections.php @@ -0,0 +1,9 @@ +addMedia($file); + } + + if (is_string($file)) { + return $entry->addMediaFromBase64($file); + } + + if (get_class($file) === File::class) { + return $entry->addMedia($file->getPathName()); + } + } +} \ No newline at end of file diff --git a/src/Uploaders/Traits/HasCustomProperties.php b/src/Uploaders/Traits/HasCustomProperties.php new file mode 100644 index 0000000..09e351a --- /dev/null +++ b/src/Uploaders/Traits/HasCustomProperties.php @@ -0,0 +1,15 @@ + $this->getName(), + 'repeatableContainerName' => $this->repeatableContainerName, + 'repeatableRow' => $this->order, + ]; + } +} \ No newline at end of file diff --git a/src/Uploaders/Traits/HasMediaName.php b/src/Uploaders/Traits/HasMediaName.php new file mode 100644 index 0000000..ad88310 --- /dev/null +++ b/src/Uploaders/Traits/HasMediaName.php @@ -0,0 +1,12 @@ +post($this->testBaseUrl, [ - 'upload' => $this->getUploadedFile('avatar1.jpg'), + 'upload' => $this->getUploadedFile('avatar1.jpg'), 'upload_multiple' => [ $this->getUploadedFile('avatar2.jpg'), $this->getUploadedFile('avatar3.jpg'), @@ -106,11 +105,6 @@ public function test_it_display_the_edit_page_without_files() $response->assertStatus(200); } - /** - * Undocumented function - * - * @group fail - */ public function test_it_display_the_upload_page_with_files() { self::initUploaderWithFiles(); @@ -123,6 +117,104 @@ public function test_it_display_the_upload_page_with_files() $response->assertSee('avatar3.jpg'); } + public function test_it_can_update_files() + { + self::initUploaderWithFiles(); + + $response = $this->put($this->testBaseUrl.'/1', [ + 'upload' => $this->getUploadedFile('avatar4.jpg'), + 'upload_multiple' => [ + $this->getUploadedFile('avatar5.jpg'), + $this->getUploadedFile('avatar6.jpg'), + ], + 'clear_upload_multiple' => ['2/avatar2.jpg', '3/avatar3.jpg'], + 'id' => 1, + ]); + + $response->assertStatus(302); + + $response->assertRedirect($this->testBaseUrl); + + $this->assertDatabaseCount('media_uploaders', 1); + + $uploader = MediaUploader::first(); + + $this->assertEquals(3, $uploader->getMedia()->count()); + + $this->assertTrue(Storage::disk('uploaders')->exists('4/avatar4.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('5/avatar5.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('6/avatar6.jpg')); + $this->assertFalse(Storage::disk('uploaders')->exists('2/avatar2.jpg')); + $this->assertFalse(Storage::disk('uploaders')->exists('3/avatar3.jpg')); + $this->assertFalse(Storage::disk('uploaders')->exists('1/avatar1.jpg')); + + } + + public function test_it_keeps_previous_values_unchanged_when_not_deleted() + { + self::initUploaderWithFiles(); + + $response = $this->put($this->testBaseUrl.'/1', [ + 'upload_multiple' => ['2/avatar2.jpg', '3/avatar3.jpg'], + 'id' => 1, + ]); + + $response->assertStatus(302); + + $response->assertRedirect($this->testBaseUrl); + + $this->assertDatabaseCount('media_uploaders', 1); + + $uploader = MediaUploader::first(); + + $this->assertEquals(3, $uploader->getMedia()->count()); + + $this->assertTrue(Storage::disk('uploaders')->exists('1/avatar1.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('2/avatar2.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('3/avatar3.jpg')); + } + + public function test_upload_multiple_can_delete_uploaded_files_and_add_at_the_same_time() + { + self::initUploaderWithFiles(); + + $response = $this->put($this->testBaseUrl.'/1', [ + 'upload_multiple' => $this->getUploadedFiles(['avatar4.jpg', 'avatar5.jpg']), + 'clear_upload_multiple' => ['2/avatar2.jpg'], + 'id' => 1, + ]); + + $response->assertStatus(302); + + $response->assertRedirect($this->testBaseUrl); + + $this->assertDatabaseCount('media_uploaders', 1); + + $uploader = MediaUploader::first(); + + $this->assertEquals(4, $uploader->getMedia()->count()); + + $this->assertTrue(Storage::disk('uploaders')->exists('1/avatar1.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('3/avatar3.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('4/avatar4.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('5/avatar5.jpg')); + + } + + public function test_it_can_delete_uploaded_files() + { + self::initUploaderWithFiles(); + + $response = $this->delete($this->testBaseUrl.'/1'); + + $response->assertStatus(200); + + $this->assertDatabaseCount('media_uploaders', 0); + + $files = Storage::disk('uploaders')->allFiles(); + + $this->assertEquals(0, count($files)); + } private static function initUploader() { @@ -134,22 +226,20 @@ private function initUploaderWithFiles() { $uploader = new MediaUploader(); $uploader->addMedia($this->getUploadedFile('avatar1.jpg'))->withCustomProperties([ - 'name' => 'upload', + 'name' => 'upload', 'repeatableContainerName' => null, - 'repeatableRow' => null, + 'repeatableRow' => null, ])->preservingOriginal()->toMediaCollection('default', 'uploaders'); $uploader->addMedia($this->getUploadedFile('avatar2.jpg'))->withCustomProperties([ - 'name' => 'upload_multiple', + 'name' => 'upload_multiple', 'repeatableContainerName' => null, - 'repeatableRow' => null, + 'repeatableRow' => null, ])->preservingOriginal()->toMediaCollection('default', 'uploaders'); $uploader->addMedia($this->getUploadedFile('avatar3.jpg'))->withCustomProperties([ - 'name' => 'upload_multiple', + 'name' => 'upload_multiple', 'repeatableContainerName' => null, - 'repeatableRow' => null, + 'repeatableRow' => null, ])->preservingOriginal()->toMediaCollection('default', 'uploaders'); $uploader->save(); } - - } From 76b0fe22c6f495ce7cb8bfea8048ef2d00099dd6 Mon Sep 17 00:00:00 2001 From: "Pedro X." Date: Mon, 24 Jun 2024 15:24:47 +0100 Subject: [PATCH 04/21] wip --- src/AddonServiceProvider.php | 2 + src/Uploaders/MediaAjaxUploader.php | 1 + src/Uploaders/MediaEasyMDEUploader.php | 138 ++++++++++++++++++ src/Uploaders/MediaUploader.php | 124 +--------------- .../Traits/HandleRepeatableUploads.php | 67 +++++++++ src/Uploaders/Traits/HasCollections.php | 1 - .../Traits/RetrievesUploadedFiles.php | 67 +++++++++ 7 files changed, 279 insertions(+), 121 deletions(-) create mode 100644 src/Uploaders/MediaEasyMDEUploader.php create mode 100644 src/Uploaders/Traits/HandleRepeatableUploads.php create mode 100644 src/Uploaders/Traits/RetrievesUploadedFiles.php diff --git a/src/AddonServiceProvider.php b/src/AddonServiceProvider.php index 9a2fa4a..eef437d 100644 --- a/src/AddonServiceProvider.php +++ b/src/AddonServiceProvider.php @@ -9,6 +9,7 @@ use Backpack\MediaLibraryUploaders\Uploaders\MediaMultipleFiles; use Backpack\MediaLibraryUploaders\Uploaders\MediaSingleBase64Image; use Backpack\MediaLibraryUploaders\Uploaders\MediaSingleFile; +use Backpack\MediaLibraryUploaders\Uploaders\MediaEasyMDEUploader; use Illuminate\Support\ServiceProvider; class AddonServiceProvider extends ServiceProvider @@ -35,6 +36,7 @@ public function boot() if (class_exists(\Backpack\Pro\Uploads\BackpackAjaxUploader::class)) { app('UploadersRepository')->addUploaderClasses([ 'dropzone' => MediaDropzoneUploader::class, + 'easymde' => MediaEasyMDEUploader::class, ], 'withMedia'); } diff --git a/src/Uploaders/MediaAjaxUploader.php b/src/Uploaders/MediaAjaxUploader.php index e1fa575..5ed8942 100644 --- a/src/Uploaders/MediaAjaxUploader.php +++ b/src/Uploaders/MediaAjaxUploader.php @@ -16,6 +16,7 @@ class MediaAjaxUploader extends BackpackAjaxUploader use Traits\HasCustomProperties; use Traits\HasSavingCallback; use Traits\HasCollections; + use Traits\RetrievesUploadedFiles; public function __construct(array $crudObject, array $configuration) { diff --git a/src/Uploaders/MediaEasyMDEUploader.php b/src/Uploaders/MediaEasyMDEUploader.php new file mode 100644 index 0000000..58c770c --- /dev/null +++ b/src/Uploaders/MediaEasyMDEUploader.php @@ -0,0 +1,138 @@ +multiple(); + } + + public function uploadFiles(Model $entry, $value = null) + { + $uploads = $value ?? CRUD::getRequest()->input($this->getName()) ?? []; + + $uploads = is_array($uploads) ? $uploads : (json_decode($uploads, true) ?? []); + + $uploadedFiles = array_filter($uploads, function ($value) { + return strpos($value, $this->temporaryFolder) !== false; + }); + + $previousSentFiles = array_filter($uploads, function ($value) { + return strpos($value, $this->temporaryFolder) === false; + }); + + $previousDatabaseFiles = $this->getPreviousFiles($entry) ?? []; + + foreach ($previousDatabaseFiles as $previousFile) { + if (! in_array($this->getMediaIdentifier($previousFile, $entry), $previousSentFiles)) { + $previousFile->delete(); + } + } + + foreach ($uploadedFiles as $key => $value) { + $file = new File($this->temporaryDisk->path($value)); + + $this->addMediaFile($entry, $file); + } + } + + public function uploadRepeatableFiles($values, $previousValues, $entry = null) + { + $values = array_map(function ($value) { + return is_array($value) ? $value : (json_decode($value, true) ?? []); + }, $values); + + foreach ($values as $row => $files) { + $files = is_array($files) ? $files : (json_decode($files, true) ?? []); + + $uploadedFiles = array_filter($files, function ($value) { + return strpos($value, $this->temporaryFolder) !== false; + }); + + foreach ($uploadedFiles ?? [] as $key => $file) { + try { + $name = substr($file, strrpos($file, '/') + 1); + + $temporaryFile = $this->temporaryDisk->get($file); + + $this->permanentDisk->put($this->getPath().$name, $temporaryFile); + + $this->temporaryDisk->delete($file); + + $file = str_replace(Str::finish($this->temporaryFolder, '/'), $this->getPath(), $file); + + $values[$row][$key] = $file; + } catch (\Throwable $th) { + Log::error($th->getMessage()); + Alert::error('An error occurred uploading files. Check log files.')->flash(); + } + } + } + + $previousValuesArray = Arr::flatten(Arr::map($previousValues, function ($value) { + return ! is_array($value) ? json_decode($value, true) ?? [] : $value; + })); + + $currentValuesArray = Arr::flatten(Arr::map($values, function ($value) { + return ! is_array($value) ? json_decode($value, true) ?? [] : $value; + })); + + $filesToDelete = array_diff($previousValuesArray, $currentValuesArray); + + foreach ($filesToDelete as $key => $value) { + $this->permanentDisk->delete($this->getPath().$value); + } + + foreach ($values as $row => $value) { + if (empty($value)) { + unset($values[$row]); + } + } + + return $values; + } + + protected function ajaxEndpointSuccessResponse($files = null): \Illuminate\Http\JsonResponse + { + return $files ? + response()->json(['files' => $files, 'success' => true]) : + response()->json(['success' => true]); + } + + protected function ajaxEndpointErrorMessage(string $message = 'An error occurred while processing the file.'): \Illuminate\Http\JsonResponse + { + return response()->json([ + 'message' => $message, + 'success' => false, + ], 400); + } + + protected function buildAjaxEndpointValidationFilesArray($validationKey, $uploadedFiles, $requestInputName): array + { + $previousUploadedFiles = json_decode(CRUD::getRequest()->input('previousUploadedFiles'), true) ?? []; + + if (Str::contains($validationKey, '.*.')) { + return [ + 'validate_ajax_endpoint' => true, + Str::before($validationKey, '.*') => [ + 0 => [ + Str::after($validationKey, '*.') => array_merge($uploadedFiles[$requestInputName], $previousUploadedFiles), + ], + ], + ]; + } + + return array_merge($uploadedFiles[$requestInputName], $previousUploadedFiles, ['validate_ajax_endpoint' => true]); + } +} diff --git a/src/Uploaders/MediaUploader.php b/src/Uploaders/MediaUploader.php index e0cd115..b8cbd29 100644 --- a/src/Uploaders/MediaUploader.php +++ b/src/Uploaders/MediaUploader.php @@ -17,6 +17,7 @@ abstract class MediaUploader extends Uploader use Traits\HasCustomProperties; use Traits\HasSavingCallback; use Traits\HasCollections; + use Traits\RetrievesUploadedFiles; public $displayConversions; @@ -40,7 +41,7 @@ public function __construct(array $crudObject, array $configuration) $configuration['disk'] ??= $modelDefinition?->diskName ?? null; $configuration['disk'] = empty($configuration['disk']) ? ($crudObject['disk'] ?? config('media-library.disk_name')) : $configuration['disk']; - //dd($configuration['disk'], $crudObject); + // read https://spatie.be/docs/laravel-medialibrary/v11/advanced-usage/using-a-custom-directory-structure#main // on how to customize file directory $crudObject['prefix'] = $configuration['path'] = ''; @@ -69,51 +70,7 @@ public function storeUploadedFiles(Model $entry): Model return $entry; } - public function retrieveUploadedFiles(Model $entry): Model - { - $media = $this->getPreviousFiles($entry); - - if (! $media) { - return $entry; - } - - if (empty($entry->mediaConversions)) { - $entry->registerAllMediaConversions(); - } - - if ($this->handleRepeatableFiles) { - $values = $entry->{$this->getRepeatableContainerName()} ?? []; - - if (! is_array($values)) { - $values = json_decode($values, true); - } - - $repeatableUploaders = array_merge(app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()), [$this]); - foreach ($repeatableUploaders as $uploader) { - $uploadValues = $uploader->getPreviousRepeatableValues($entry); - - $values = $this->mergeValuesRecursive($values, $uploadValues); - } - - $entry->{$this->getRepeatableContainerName()} = $values; - - return $entry; - } - - if (is_a($media, 'Spatie\MediaLibrary\MediaCollections\Models\Media')) { - $entry->{$this->getName()} = $this->getMediaIdentifier($media, $entry); - } else { - $entry->{$this->getName()} = $media->map(function ($item) use ($entry) { - return $this->getMediaIdentifier($item, $entry); - })->toArray(); - } - dd($entry); - return $entry; - } - - /***************************************************** - * Protected methods - default implementation * - *****************************************************/ + /** @deprecated - use getPreviousFiles() */ protected function get(HasMedia|Model $entry) { $media = $entry->getMedia($this->collection, function ($media) use ($entry) { @@ -128,79 +85,7 @@ protected function get(HasMedia|Model $entry) return $media->first(); } - public function getPreviousFiles(Model $entry): mixed - { - $media = $entry->getMedia($this->collection, function ($media) use ($entry) { - /** @var Media $media */ - return $media->getCustomProperty('name') === $this->getName() && - $media->getCustomProperty('repeatableContainerName') === $this->repeatableContainerName && - $entry->{$entry->getKeyName()} === $media->getAttribute('model_id'); - }); - - if ($this->canHandleMultipleFiles() || $this->handleRepeatableFiles) { - return $media; - } - - return $media->first(); - } - - protected function processRepeatableUploads(Model $entry, Collection $values): array - { - foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) { - $uploader->uploadRepeatableFiles($values->pluck($uploader->getName())->toArray(), $uploader->getPreviousRepeatableMedia($entry), $entry); - - $values->transform(function ($item) use ($uploader) { - unset($item[$uploader->getName()]); - - return $item; - }); - } - - return $values->toArray(); - } - - protected function getPreviousRepeatableValues(Model $entry) - { - if ($this->canHandleMultipleFiles()) { - return $this->get($entry) - ->groupBy(function ($item) { - return $item->getCustomProperty('repeatableRow'); - }) - ->transform(function ($media) use ($entry) { - $mediaItems = $media->map(function ($item) use ($entry) { - return $this->getMediaIdentifier($item, $entry); - }) - ->toArray(); - - return [$this->getName() => $mediaItems]; - }) - ->toArray(); - } - - return $this->get($entry) - ->transform(function ($item) use ($entry) { - return [ - $this->getName() => $this->getMediaIdentifier($item, $entry), - 'order_column' => $item->getCustomProperty('repeatableRow'), - ]; - }) - ->sortBy('order_column') - ->keyBy('order_column') - ->toArray(); - } - - protected function getPreviousRepeatableMedia(Model $entry) - { - $orderedMedia = []; - $previousMedia = $this->get($entry)->transform(function ($item) { - return [$this->getName() => $item, 'order_column' => $item->getCustomProperty('repeatableRow')]; - }); - $previousMedia->each(function ($item) use (&$orderedMedia) { - $orderedMedia[] = $item[$this->getName()]; - }); - - return $orderedMedia; - } + /************************************************** * Private methods- default implementation * @@ -225,5 +110,4 @@ private function getConversionToDisplay($item) /************************* * Helper methods * *************************/ - } diff --git a/src/Uploaders/Traits/HandleRepeatableUploads.php b/src/Uploaders/Traits/HandleRepeatableUploads.php new file mode 100644 index 0000000..2f6adb4 --- /dev/null +++ b/src/Uploaders/Traits/HandleRepeatableUploads.php @@ -0,0 +1,67 @@ +getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) { + $uploader->uploadRepeatableFiles($values->pluck($uploader->getName())->toArray(), $uploader->getPreviousRepeatableMedia($entry), $entry); + + $values->transform(function ($item) use ($uploader) { + unset($item[$uploader->getName()]); + + return $item; + }); + } + + return $values->toArray(); + } + + protected function getPreviousRepeatableValues(Model $entry) + { + if ($this->canHandleMultipleFiles()) { + return $this->get($entry) + ->groupBy(function ($item) { + return $item->getCustomProperty('repeatableRow'); + }) + ->transform(function ($media) use ($entry) { + $mediaItems = $media->map(function ($item) use ($entry) { + return $this->getMediaIdentifier($item, $entry); + }) + ->toArray(); + + return [$this->getName() => $mediaItems]; + }) + ->toArray(); + } + + return $this->get($entry) + ->transform(function ($item) use ($entry) { + return [ + $this->getName() => $this->getMediaIdentifier($item, $entry), + 'order_column' => $item->getCustomProperty('repeatableRow'), + ]; + }) + ->sortBy('order_column') + ->keyBy('order_column') + ->toArray(); + } + + protected function getPreviousRepeatableMedia(Model $entry) + { + $orderedMedia = []; + $previousMedia = $this->get($entry)->transform(function ($item) { + return [$this->getName() => $item, 'order_column' => $item->getCustomProperty('repeatableRow')]; + }); + $previousMedia->each(function ($item) use (&$orderedMedia) { + $orderedMedia[] = $item[$this->getName()]; + }); + + return $orderedMedia; + } +} \ No newline at end of file diff --git a/src/Uploaders/Traits/HasCollections.php b/src/Uploaders/Traits/HasCollections.php index 0ac6b2e..6f85a59 100644 --- a/src/Uploaders/Traits/HasCollections.php +++ b/src/Uploaders/Traits/HasCollections.php @@ -5,5 +5,4 @@ trait HasCollections { public string $collection; - } \ No newline at end of file diff --git a/src/Uploaders/Traits/RetrievesUploadedFiles.php b/src/Uploaders/Traits/RetrievesUploadedFiles.php new file mode 100644 index 0000000..6b570f2 --- /dev/null +++ b/src/Uploaders/Traits/RetrievesUploadedFiles.php @@ -0,0 +1,67 @@ +getPreviousFiles($entry); + + if (! $media) { + return $entry; + } + + if (empty($entry->mediaConversions)) { + $entry->registerAllMediaConversions(); + } + + if ($this->handleRepeatableFiles) { + $values = $entry->{$this->getRepeatableContainerName()} ?? []; + + if (! is_array($values)) { + $values = json_decode($values, true); + } + + $repeatableUploaders = array_merge(app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()), [$this]); + foreach ($repeatableUploaders as $uploader) { + $uploadValues = $uploader->getPreviousRepeatableValues($entry); + + $values = $this->mergeValuesRecursive($values, $uploadValues); + } + + $entry->{$this->getRepeatableContainerName()} = $values; + + return $entry; + } + + if (is_a($media, 'Spatie\MediaLibrary\MediaCollections\Models\Media')) { + $entry->{$this->getName()} = $this->getMediaIdentifier($media, $entry); + } else { + $entry->{$this->getName()} = $media->map(function ($item) use ($entry) { + return $this->getMediaIdentifier($item, $entry); + })->toArray(); + } + + return $entry; + } + + public function getPreviousFiles(Model $entry): mixed + { + $media = $entry->getMedia($this->collection, function ($media) use ($entry) { + /** @var Media $media */ + return $media->getCustomProperty('name') === $this->getName() && + $media->getCustomProperty('repeatableContainerName') === $this->repeatableContainerName && + $entry->{$entry->getKeyName()} === $media->getAttribute('model_id'); + }); + + if ($this->canHandleMultipleFiles() || $this->handleRepeatableFiles) { + return $media; + } + + return $media->first(); + } + +} \ No newline at end of file From 070c7e4d8b298670d3d3854c58d0515eb31fcb8e Mon Sep 17 00:00:00 2001 From: "Pedro X." Date: Mon, 24 Jun 2024 15:31:58 +0100 Subject: [PATCH 05/21] update easy mde uploader --- src/Uploaders/MediaEasyMDEUploader.php | 139 ++++++------------------- 1 file changed, 32 insertions(+), 107 deletions(-) diff --git a/src/Uploaders/MediaEasyMDEUploader.php b/src/Uploaders/MediaEasyMDEUploader.php index 58c770c..748dca4 100644 --- a/src/Uploaders/MediaEasyMDEUploader.php +++ b/src/Uploaders/MediaEasyMDEUploader.php @@ -2,137 +2,62 @@ namespace Backpack\MediaLibraryUploaders\Uploaders; -use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD; -use Backpack\CRUD\app\Library\Uploaders\Support\Interfaces\UploaderInterface; +use Backpack\CRUD\app\Library\Validation\Rules\BackpackCustomRule; +use Backpack\Pro\Uploads\Validation\ValidEasyMDE; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Arr; -use Illuminate\Support\Facades\Log; -use Illuminate\Support\Str; -use Prologue\Alerts\Facades\Alert; -use Symfony\Component\HttpFoundation\File\File; +use Illuminate\Support\Facades\Storage; class MediaEasyMDEUploader extends MediaAjaxUploader { - public static function for(array $field, $configuration): UploaderInterface - { - return (new self($field, $configuration))->multiple(); - } public function uploadFiles(Model $entry, $value = null) { - $uploads = $value ?? CRUD::getRequest()->input($this->getName()) ?? []; - - $uploads = is_array($uploads) ? $uploads : (json_decode($uploads, true) ?? []); - - $uploadedFiles = array_filter($uploads, function ($value) { - return strpos($value, $this->temporaryFolder) !== false; - }); - - $previousSentFiles = array_filter($uploads, function ($value) { - return strpos($value, $this->temporaryFolder) === false; - }); - - $previousDatabaseFiles = $this->getPreviousFiles($entry) ?? []; + // nothing to do here. The files are uploaded via the EasyMDE editor using the ajax endpoint. + return $this->isFake() ? ($entry->{$this->getFakeAttribute()}[$this->getAttributeName()] ?? null) : $entry->{$this->getAttributeName()}; + + } - foreach ($previousDatabaseFiles as $previousFile) { - if (! in_array($this->getMediaIdentifier($previousFile, $entry), $previousSentFiles)) { - $previousFile->delete(); - } - } + protected function ajaxEndpointSuccessResponse($files = null): \Illuminate\Http\JsonResponse + { + /** @var \Illuminate\Filesystem\FilesystemAdapter $disk */ + $disk = Storage::disk($this->getDisk()); - foreach ($uploadedFiles as $key => $value) { - $file = new File($this->temporaryDisk->path($value)); + $file = current($files); - $this->addMediaFile($entry, $file); - } + return response()->json([ + 'data' => ['filePath' => $disk->url($file)], + ]); } + protected function getDefaultAjaxEndpointValidation(): BackpackCustomRule + { + return ValidEasyMDE::field([])->file(['mimetypes:image/jpeg,image/png,image/jpg', 'max:1024']); + } + public function uploadRepeatableFiles($values, $previousValues, $entry = null) { - $values = array_map(function ($value) { - return is_array($value) ? $value : (json_decode($value, true) ?? []); - }, $values); - - foreach ($values as $row => $files) { - $files = is_array($files) ? $files : (json_decode($files, true) ?? []); - - $uploadedFiles = array_filter($files, function ($value) { - return strpos($value, $this->temporaryFolder) !== false; - }); - - foreach ($uploadedFiles ?? [] as $key => $file) { - try { - $name = substr($file, strrpos($file, '/') + 1); - - $temporaryFile = $this->temporaryDisk->get($file); - - $this->permanentDisk->put($this->getPath().$name, $temporaryFile); - - $this->temporaryDisk->delete($file); - - $file = str_replace(Str::finish($this->temporaryFolder, '/'), $this->getPath(), $file); - - $values[$row][$key] = $file; - } catch (\Throwable $th) { - Log::error($th->getMessage()); - Alert::error('An error occurred uploading files. Check log files.')->flash(); - } - } - } - - $previousValuesArray = Arr::flatten(Arr::map($previousValues, function ($value) { - return ! is_array($value) ? json_decode($value, true) ?? [] : $value; - })); - - $currentValuesArray = Arr::flatten(Arr::map($values, function ($value) { - return ! is_array($value) ? json_decode($value, true) ?? [] : $value; - })); - - $filesToDelete = array_diff($previousValuesArray, $currentValuesArray); - - foreach ($filesToDelete as $key => $value) { - $this->permanentDisk->delete($this->getPath().$value); - } - - foreach ($values as $row => $value) { - if (empty($value)) { - unset($values[$row]); - } - } - + // nothing to do here. The files are uploaded via ajax return $values; } - protected function ajaxEndpointSuccessResponse($files = null): \Illuminate\Http\JsonResponse + protected function getAjaxEndpointDisk(): \Illuminate\Filesystem\FilesystemAdapter { - return $files ? - response()->json(['files' => $files, 'success' => true]) : - response()->json(['success' => true]); + return $this->getPermanentDisk(); } - protected function ajaxEndpointErrorMessage(string $message = 'An error occurred while processing the file.'): \Illuminate\Http\JsonResponse + protected function getAjaxEndpointPath(): string { - return response()->json([ - 'message' => $message, - 'success' => false, - ], 400); + return $this->getPath(); } - protected function buildAjaxEndpointValidationFilesArray($validationKey, $uploadedFiles, $requestInputName): array + public function getValueWithoutPath(?string $value = null): ?string { - $previousUploadedFiles = json_decode(CRUD::getRequest()->input('previousUploadedFiles'), true) ?? []; - - if (Str::contains($validationKey, '.*.')) { - return [ - 'validate_ajax_endpoint' => true, - Str::before($validationKey, '.*') => [ - 0 => [ - Str::after($validationKey, '*.') => array_merge($uploadedFiles[$requestInputName], $previousUploadedFiles), - ], - ], - ]; - } + // don't strip the paths on easyMDE uploads + return $value; + } - return array_merge($uploadedFiles[$requestInputName], $previousUploadedFiles, ['validate_ajax_endpoint' => true]); + public function shouldDeleteFiles(): bool + { + return false; } } From c25176ed13223d4aa40a4f7798796fac94fbbff1 Mon Sep 17 00:00:00 2001 From: "Pedro X." Date: Tue, 25 Jun 2024 10:28:33 +0100 Subject: [PATCH 06/21] wip --- src/AddonServiceProvider.php | 2 - src/Uploaders/MediaDropzoneUploader.php | 4 +- src/Uploaders/MediaEasyMDEUploader.php | 63 ----------------------- src/Uploaders/MediaUploader.php | 22 -------- src/Uploaders/Traits/AddMediaToModels.php | 19 +++++++ src/Uploaders/Traits/HasMediaName.php | 12 ----- src/Uploaders/Traits/IdentifiesMedia.php | 2 + 7 files changed, 23 insertions(+), 101 deletions(-) delete mode 100644 src/Uploaders/MediaEasyMDEUploader.php delete mode 100644 src/Uploaders/Traits/HasMediaName.php diff --git a/src/AddonServiceProvider.php b/src/AddonServiceProvider.php index eef437d..9a2fa4a 100644 --- a/src/AddonServiceProvider.php +++ b/src/AddonServiceProvider.php @@ -9,7 +9,6 @@ use Backpack\MediaLibraryUploaders\Uploaders\MediaMultipleFiles; use Backpack\MediaLibraryUploaders\Uploaders\MediaSingleBase64Image; use Backpack\MediaLibraryUploaders\Uploaders\MediaSingleFile; -use Backpack\MediaLibraryUploaders\Uploaders\MediaEasyMDEUploader; use Illuminate\Support\ServiceProvider; class AddonServiceProvider extends ServiceProvider @@ -36,7 +35,6 @@ public function boot() if (class_exists(\Backpack\Pro\Uploads\BackpackAjaxUploader::class)) { app('UploadersRepository')->addUploaderClasses([ 'dropzone' => MediaDropzoneUploader::class, - 'easymde' => MediaEasyMDEUploader::class, ], 'withMedia'); } diff --git a/src/Uploaders/MediaDropzoneUploader.php b/src/Uploaders/MediaDropzoneUploader.php index 0f7eb0d..a79458f 100644 --- a/src/Uploaders/MediaDropzoneUploader.php +++ b/src/Uploaders/MediaDropzoneUploader.php @@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Prologue\Alerts\Facades\Alert; -use Symfony\Component\HttpFoundation\File\File; +use Illuminate\Http\UploadedFile; class MediaDropzoneUploader extends MediaAjaxUploader { @@ -41,7 +41,7 @@ public function uploadFiles(Model $entry, $value = null) } foreach ($uploadedFiles as $key => $value) { - $file = new File($this->temporaryDisk->path($value)); + $file = new UploadedFile($this->temporaryDisk->path($value), $value); $this->addMediaFile($entry, $file); } diff --git a/src/Uploaders/MediaEasyMDEUploader.php b/src/Uploaders/MediaEasyMDEUploader.php deleted file mode 100644 index 748dca4..0000000 --- a/src/Uploaders/MediaEasyMDEUploader.php +++ /dev/null @@ -1,63 +0,0 @@ -isFake() ? ($entry->{$this->getFakeAttribute()}[$this->getAttributeName()] ?? null) : $entry->{$this->getAttributeName()}; - - } - - protected function ajaxEndpointSuccessResponse($files = null): \Illuminate\Http\JsonResponse - { - /** @var \Illuminate\Filesystem\FilesystemAdapter $disk */ - $disk = Storage::disk($this->getDisk()); - - $file = current($files); - - return response()->json([ - 'data' => ['filePath' => $disk->url($file)], - ]); - } - - protected function getDefaultAjaxEndpointValidation(): BackpackCustomRule - { - return ValidEasyMDE::field([])->file(['mimetypes:image/jpeg,image/png,image/jpg', 'max:1024']); - } - - public function uploadRepeatableFiles($values, $previousValues, $entry = null) - { - // nothing to do here. The files are uploaded via ajax - return $values; - } - - protected function getAjaxEndpointDisk(): \Illuminate\Filesystem\FilesystemAdapter - { - return $this->getPermanentDisk(); - } - - protected function getAjaxEndpointPath(): string - { - return $this->getPath(); - } - - public function getValueWithoutPath(?string $value = null): ?string - { - // don't strip the paths on easyMDE uploads - return $value; - } - - public function shouldDeleteFiles(): bool - { - return false; - } -} diff --git a/src/Uploaders/MediaUploader.php b/src/Uploaders/MediaUploader.php index b8cbd29..5f245f8 100644 --- a/src/Uploaders/MediaUploader.php +++ b/src/Uploaders/MediaUploader.php @@ -13,7 +13,6 @@ abstract class MediaUploader extends Uploader use Traits\IdentifiesMedia; use Traits\AddMediaToModels; use Traits\HasConstrainedFileAdder; - use Traits\HasMediaName; use Traits\HasCustomProperties; use Traits\HasSavingCallback; use Traits\HasCollections; @@ -49,27 +48,6 @@ public function __construct(array $crudObject, array $configuration) parent::__construct($crudObject, $configuration); } - /************************* - * Public methods * - *************************/ - public function storeUploadedFiles(Model $entry): Model - { - if ($this->handleRepeatableFiles) { - return $this->handleRepeatableFiles($entry); - } - - $this->uploadFiles($entry); - - // make sure we remove the attribute from the model in case developer is using it in fillable - // or using guarded in their models. - $entry->offsetUnset($this->getName()); - // setting the raw attributes makes sure the `attributeCastCache` property is cleared, preventing - // uploaded files from being re-added to the entry from the cache. - $entry = $entry->setRawAttributes($entry->getAttributes()); - - return $entry; - } - /** @deprecated - use getPreviousFiles() */ protected function get(HasMedia|Model $entry) { diff --git a/src/Uploaders/Traits/AddMediaToModels.php b/src/Uploaders/Traits/AddMediaToModels.php index af9bda4..cb6a9de 100644 --- a/src/Uploaders/Traits/AddMediaToModels.php +++ b/src/Uploaders/Traits/AddMediaToModels.php @@ -3,6 +3,7 @@ namespace Backpack\MediaLibraryUploaders\Uploaders\Traits; use Backpack\MediaLibraryUploaders\ConstrainedFileAdder; +use Illuminate\Database\Eloquent\Model; use Exception; trait AddMediaToModels @@ -31,4 +32,22 @@ protected function addMediaFile($entry, $file, $order = null) $constrainedMedia->getFileAdder()->toMediaCollection($this->collection, $this->getDisk()); } + + public function storeUploadedFiles(Model $entry): Model + { + if ($this->handleRepeatableFiles) { + return $this->handleRepeatableFiles($entry); + } + + $this->uploadFiles($entry); + + // make sure we remove the attribute from the model in case developer is using it in fillable + // or using guarded in their models. + $entry->offsetUnset($this->getName()); + // setting the raw attributes makes sure the `attributeCastCache` property is cleared, preventing + // uploaded files from being re-added to the entry from the cache. + $entry = $entry->setRawAttributes($entry->getAttributes()); + + return $entry; + } } diff --git a/src/Uploaders/Traits/HasMediaName.php b/src/Uploaders/Traits/HasMediaName.php deleted file mode 100644 index ad88310..0000000 --- a/src/Uploaders/Traits/HasMediaName.php +++ /dev/null @@ -1,12 +0,0 @@ - Date: Tue, 25 Jun 2024 10:30:54 +0100 Subject: [PATCH 07/21] remove deleted trait --- src/Uploaders/MediaAjaxUploader.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Uploaders/MediaAjaxUploader.php b/src/Uploaders/MediaAjaxUploader.php index 5ed8942..4a6696c 100644 --- a/src/Uploaders/MediaAjaxUploader.php +++ b/src/Uploaders/MediaAjaxUploader.php @@ -12,7 +12,6 @@ class MediaAjaxUploader extends BackpackAjaxUploader use Traits\IdentifiesMedia; use Traits\AddMediaToModels; use Traits\HasConstrainedFileAdder; - use Traits\HasMediaName; use Traits\HasCustomProperties; use Traits\HasSavingCallback; use Traits\HasCollections; From 9d033fbae0bd4ba312bdafee958123338e5bd34d Mon Sep 17 00:00:00 2001 From: "Pedro X." Date: Tue, 25 Jun 2024 12:07:56 +0100 Subject: [PATCH 08/21] wip --- src/Uploaders/MediaUploader.php | 4 ---- .../Database/Migrations/create_media_uploader_table.php | 1 + tests/Config/Models/MediaUploader.php | 6 +++++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Uploaders/MediaUploader.php b/src/Uploaders/MediaUploader.php index 5f245f8..8ffd402 100644 --- a/src/Uploaders/MediaUploader.php +++ b/src/Uploaders/MediaUploader.php @@ -84,8 +84,4 @@ private function getConversionToDisplay($item) return false; } - - /************************* - * Helper methods * - *************************/ } diff --git a/tests/Config/Database/Migrations/create_media_uploader_table.php b/tests/Config/Database/Migrations/create_media_uploader_table.php index 8fecc8f..038a0cf 100644 --- a/tests/Config/Database/Migrations/create_media_uploader_table.php +++ b/tests/Config/Database/Migrations/create_media_uploader_table.php @@ -9,6 +9,7 @@ public function up(): void { Schema::create('media_uploaders', function (Blueprint $table) { $table->id(); + $table->json('repeatable')->nullable(); }); } diff --git a/tests/Config/Models/MediaUploader.php b/tests/Config/Models/MediaUploader.php index bb88ccb..d81cabd 100644 --- a/tests/Config/Models/MediaUploader.php +++ b/tests/Config/Models/MediaUploader.php @@ -16,7 +16,11 @@ class MediaUploader extends Model implements HasMedia * * @var array */ - protected $fillable = ['name']; + protected $fillable = ['repeatable']; public $timestamps = false; + + protected $casts = [ + 'repeatable' => 'json', + ]; } From d42c25db08a7db91747046ffd7146a0711441521 Mon Sep 17 00:00:00 2001 From: "Pedro X." Date: Tue, 25 Jun 2024 13:06:26 +0100 Subject: [PATCH 09/21] add repeatable trait --- src/Uploaders/MediaAjaxUploader.php | 1 + src/Uploaders/MediaUploader.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Uploaders/MediaAjaxUploader.php b/src/Uploaders/MediaAjaxUploader.php index 4a6696c..42f11d6 100644 --- a/src/Uploaders/MediaAjaxUploader.php +++ b/src/Uploaders/MediaAjaxUploader.php @@ -16,6 +16,7 @@ class MediaAjaxUploader extends BackpackAjaxUploader use Traits\HasSavingCallback; use Traits\HasCollections; use Traits\RetrievesUploadedFiles; + use Traits\HandleRepeatableUploads; public function __construct(array $crudObject, array $configuration) { diff --git a/src/Uploaders/MediaUploader.php b/src/Uploaders/MediaUploader.php index 8ffd402..76d7031 100644 --- a/src/Uploaders/MediaUploader.php +++ b/src/Uploaders/MediaUploader.php @@ -4,7 +4,6 @@ use Backpack\CRUD\app\Library\Uploaders\Uploader; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Collection; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\MediaCollections\Models\Media; @@ -17,6 +16,7 @@ abstract class MediaUploader extends Uploader use Traits\HasSavingCallback; use Traits\HasCollections; use Traits\RetrievesUploadedFiles; + use Traits\HandleRepeatableUploads; public $displayConversions; From 220eefbae2c0666225ee57de3cb46cbd145fe95b Mon Sep 17 00:00:00 2001 From: "Pedro X." Date: Tue, 25 Jun 2024 13:20:37 +0100 Subject: [PATCH 10/21] make functions public --- src/Uploaders/Traits/HandleRepeatableUploads.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Uploaders/Traits/HandleRepeatableUploads.php b/src/Uploaders/Traits/HandleRepeatableUploads.php index 2f6adb4..a34d927 100644 --- a/src/Uploaders/Traits/HandleRepeatableUploads.php +++ b/src/Uploaders/Traits/HandleRepeatableUploads.php @@ -7,7 +7,7 @@ trait HandleRepeatableUploads { - protected function processRepeatableUploads(Model $entry, Collection $values): array + public function processRepeatableUploads(Model $entry, Collection $values): array { foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) { $uploader->uploadRepeatableFiles($values->pluck($uploader->getName())->toArray(), $uploader->getPreviousRepeatableMedia($entry), $entry); @@ -22,7 +22,7 @@ protected function processRepeatableUploads(Model $entry, Collection $values): a return $values->toArray(); } - protected function getPreviousRepeatableValues(Model $entry) + public function getPreviousRepeatableValues(Model $entry) { if ($this->canHandleMultipleFiles()) { return $this->get($entry) @@ -52,7 +52,7 @@ protected function getPreviousRepeatableValues(Model $entry) ->toArray(); } - protected function getPreviousRepeatableMedia(Model $entry) + public function getPreviousRepeatableMedia(Model $entry) { $orderedMedia = []; $previousMedia = $this->get($entry)->transform(function ($item) { From c4e3a87c9cab1a1483993bb904d4273d5eb3115a Mon Sep 17 00:00:00 2001 From: "Pedro X." Date: Tue, 25 Jun 2024 13:23:54 +0100 Subject: [PATCH 11/21] rename get function --- src/Uploaders/Traits/HandleRepeatableUploads.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Uploaders/Traits/HandleRepeatableUploads.php b/src/Uploaders/Traits/HandleRepeatableUploads.php index a34d927..9c61b79 100644 --- a/src/Uploaders/Traits/HandleRepeatableUploads.php +++ b/src/Uploaders/Traits/HandleRepeatableUploads.php @@ -25,7 +25,7 @@ public function processRepeatableUploads(Model $entry, Collection $values): arra public function getPreviousRepeatableValues(Model $entry) { if ($this->canHandleMultipleFiles()) { - return $this->get($entry) + return $this->getPreviousFiles($entry) ->groupBy(function ($item) { return $item->getCustomProperty('repeatableRow'); }) @@ -40,7 +40,7 @@ public function getPreviousRepeatableValues(Model $entry) ->toArray(); } - return $this->get($entry) + return $this->getPreviousFiles($entry) ->transform(function ($item) use ($entry) { return [ $this->getName() => $this->getMediaIdentifier($item, $entry), @@ -55,7 +55,7 @@ public function getPreviousRepeatableValues(Model $entry) public function getPreviousRepeatableMedia(Model $entry) { $orderedMedia = []; - $previousMedia = $this->get($entry)->transform(function ($item) { + $previousMedia = $this->getPreviousFiles($entry)->transform(function ($item) { return [$this->getName() => $item, 'order_column' => $item->getCustomProperty('repeatableRow')]; }); $previousMedia->each(function ($item) use (&$orderedMedia) { From 61e1d2eb2772a5b988e005ebc06e2ccda4e16910 Mon Sep 17 00:00:00 2001 From: "Pedro X." Date: Tue, 25 Jun 2024 13:44:37 +0100 Subject: [PATCH 12/21] refactoring dropzone uploader --- src/Uploaders/MediaDropzoneUploader.php | 55 +++++++++++-------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/src/Uploaders/MediaDropzoneUploader.php b/src/Uploaders/MediaDropzoneUploader.php index a79458f..e911d51 100644 --- a/src/Uploaders/MediaDropzoneUploader.php +++ b/src/Uploaders/MediaDropzoneUploader.php @@ -42,7 +42,6 @@ public function uploadFiles(Model $entry, $value = null) foreach ($uploadedFiles as $key => $value) { $file = new UploadedFile($this->temporaryDisk->path($value), $value); - $this->addMediaFile($entry, $file); } } @@ -62,17 +61,8 @@ public function uploadRepeatableFiles($values, $previousValues, $entry = null) foreach ($uploadedFiles ?? [] as $key => $file) { try { - $name = substr($file, strrpos($file, '/') + 1); - - $temporaryFile = $this->temporaryDisk->get($file); - - $this->permanentDisk->put($this->getPath().$name, $temporaryFile); - - $this->temporaryDisk->delete($file); - - $file = str_replace(Str::finish($this->temporaryFolder, '/'), $this->getPath(), $file); - - $values[$row][$key] = $file; + $file = new UploadedFile($this->temporaryDisk->path($file), $file); + $this->addMediaFile($entry, $file, $row); } catch (\Throwable $th) { Log::error($th->getMessage()); Alert::error('An error occurred uploading files. Check log files.')->flash(); @@ -80,27 +70,32 @@ public function uploadRepeatableFiles($values, $previousValues, $entry = null) } } - $previousValuesArray = Arr::flatten(Arr::map($previousValues, function ($value) { - return ! is_array($value) ? json_decode($value, true) ?? [] : $value; - })); - - $currentValuesArray = Arr::flatten(Arr::map($values, function ($value) { - return ! is_array($value) ? json_decode($value, true) ?? [] : $value; - })); - - $filesToDelete = array_diff($previousValuesArray, $currentValuesArray); - - foreach ($filesToDelete as $key => $value) { - $this->permanentDisk->delete($this->getPath().$value); - } + foreach ($previousValues as $previousFile) { + $fileIdentifier = $this->getMediaIdentifier($previousFile, $entry); + if (empty($sentFiles)) { + $previousFile->delete(); + continue; + } + + $foundInSentFiles = false; + foreach($sentFiles as $row => $sentFilesInRow) { + $fileWasSent = array_search($fileIdentifier, $sentFilesInRow, true); + if($fileWasSent !== false) { + $foundInSentFiles = true; + if($row !== $previousFile->getCustomProperty('repeatableRow')) { + $previousFile->setCustomProperty('repeatableRow', $row); + $previousFile->save(); + // avoid checking the same file twice. This is a performance improvement. + unset($sentFiles[$row][$fileWasSent]); + break; + } + } + } - foreach ($values as $row => $value) { - if (empty($value)) { - unset($values[$row]); + if ($foundInSentFiles === false) { + $previousFile->delete(); } } - - return $values; } protected function ajaxEndpointSuccessResponse($files = null): \Illuminate\Http\JsonResponse From 5b313f753e21a842bdb1765dac0f8c1f73f61718 Mon Sep 17 00:00:00 2001 From: "Pedro X." Date: Tue, 25 Jun 2024 16:28:34 +0100 Subject: [PATCH 13/21] wip --- src/Uploaders/MediaDropzoneUploader.php | 6 ++++++ src/Uploaders/MediaSingleFile.php | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Uploaders/MediaDropzoneUploader.php b/src/Uploaders/MediaDropzoneUploader.php index e911d51..cc18796 100644 --- a/src/Uploaders/MediaDropzoneUploader.php +++ b/src/Uploaders/MediaDropzoneUploader.php @@ -52,6 +52,8 @@ public function uploadRepeatableFiles($values, $previousValues, $entry = null) return is_array($value) ? $value : (json_decode($value, true) ?? []); }, $values); + $sentFiles = []; + foreach ($values as $row => $files) { $files = is_array($files) ? $files : (json_decode($files, true) ?? []); @@ -59,6 +61,10 @@ public function uploadRepeatableFiles($values, $previousValues, $entry = null) return strpos($value, $this->temporaryFolder) !== false; }); + $sentFiles = array_merge($sentFiles, [$row => array_filter($files, function ($value) { + return strpos($value, $this->temporaryFolder) === false; + })]); + foreach ($uploadedFiles ?? [] as $key => $file) { try { $file = new UploadedFile($this->temporaryDisk->path($file), $file); diff --git a/src/Uploaders/MediaSingleFile.php b/src/Uploaders/MediaSingleFile.php index 1780bbf..5e24c92 100644 --- a/src/Uploaders/MediaSingleFile.php +++ b/src/Uploaders/MediaSingleFile.php @@ -12,7 +12,7 @@ public function uploadFiles(Model $entry, $value = null) { $value = $value ?? CRUD::getRequest()->file($this->getName()); - $previousFile = $this->get($entry); + $previousFile = $this->getPreviousFiles($entry); if ($previousFile && ($value && is_a($value, UploadedFile::class) || request()->has($this->getName()))) { $previousFile->delete(); From 31e81d8586d747c7c80e4d6e7563292c607652b0 Mon Sep 17 00:00:00 2001 From: pxpm Date: Mon, 1 Jul 2024 10:38:30 +0100 Subject: [PATCH 14/21] add media test models --- tests/Config/Models/File.php | 27 ++++++++++++++++++++++++++ tests/Config/Models/Folder.php | 27 ++++++++++++++++++++++++++ tests/Config/Models/MediaUploader.php | 17 ++++++++++++++++ tests/Config/Models/Picture.php | 27 ++++++++++++++++++++++++++ tests/Config/Models/UploadersPivot.php | 17 ++++++++++++++++ 5 files changed, 115 insertions(+) create mode 100644 tests/Config/Models/File.php create mode 100644 tests/Config/Models/Folder.php create mode 100644 tests/Config/Models/Picture.php create mode 100644 tests/Config/Models/UploadersPivot.php diff --git a/tests/Config/Models/File.php b/tests/Config/Models/File.php new file mode 100644 index 0000000..d78d69c --- /dev/null +++ b/tests/Config/Models/File.php @@ -0,0 +1,27 @@ +belongsToMany(MediaUploader::class, 'media_uploaders_pivot'); + } +} diff --git a/tests/Config/Models/Folder.php b/tests/Config/Models/Folder.php new file mode 100644 index 0000000..81c763e --- /dev/null +++ b/tests/Config/Models/Folder.php @@ -0,0 +1,27 @@ +belongsTo(MediaUploader::class); + } +} diff --git a/tests/Config/Models/MediaUploader.php b/tests/Config/Models/MediaUploader.php index d81cabd..be89745 100644 --- a/tests/Config/Models/MediaUploader.php +++ b/tests/Config/Models/MediaUploader.php @@ -23,4 +23,21 @@ class MediaUploader extends Model implements HasMedia protected $casts = [ 'repeatable' => 'json', ]; + + public function belongsToManyRelation() : BelongsToMany + { + return $this->belongsToMany(File::class, 'uploaders_pivot') + ->using(UploadersPivot::class) + ->withPivot(['dropzone', 'easymde', 'upload', 'image', 'upload_multiple']); + } + + public function hasManyRelation() : HasMany + { + return $this->hasMany(Picture::class, 'uploader_id'); + } + + public function hasOneRelation() : HasOne + { + return $this->hasOne(Folder::class, 'uploader_id'); + } } diff --git a/tests/Config/Models/Picture.php b/tests/Config/Models/Picture.php new file mode 100644 index 0000000..419078b --- /dev/null +++ b/tests/Config/Models/Picture.php @@ -0,0 +1,27 @@ +belongsTo(MediaUploader::class); + } +} diff --git a/tests/Config/Models/UploadersPivot.php b/tests/Config/Models/UploadersPivot.php new file mode 100644 index 0000000..7b35c5c --- /dev/null +++ b/tests/Config/Models/UploadersPivot.php @@ -0,0 +1,17 @@ + Date: Mon, 1 Jul 2024 10:45:06 +0100 Subject: [PATCH 15/21] fix test models --- tests/Config/Models/MediaUploader.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Config/Models/MediaUploader.php b/tests/Config/Models/MediaUploader.php index be89745..20d841a 100644 --- a/tests/Config/Models/MediaUploader.php +++ b/tests/Config/Models/MediaUploader.php @@ -5,6 +5,9 @@ use Illuminate\Database\Eloquent\Model; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; +use \Illuminate\Database\Eloquent\Relations\HasMany; +use \Illuminate\Database\Eloquent\Relations\BelongsToMany; +use \Illuminate\Database\Eloquent\Relations\HasOne; class MediaUploader extends Model implements HasMedia { From 6a937b3ebf452c70866e485eff6f8adac7c8fd84 Mon Sep 17 00:00:00 2001 From: pxpm Date: Mon, 1 Jul 2024 10:48:33 +0100 Subject: [PATCH 16/21] fix media pivot namespace --- tests/Config/Models/UploadersPivot.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Config/Models/UploadersPivot.php b/tests/Config/Models/UploadersPivot.php index 7b35c5c..2129e44 100644 --- a/tests/Config/Models/UploadersPivot.php +++ b/tests/Config/Models/UploadersPivot.php @@ -1,6 +1,6 @@ Date: Mon, 1 Jul 2024 10:49:54 +0100 Subject: [PATCH 17/21] fix test model namespace --- tests/Config/Models/Picture.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Config/Models/Picture.php b/tests/Config/Models/Picture.php index 419078b..9344024 100644 --- a/tests/Config/Models/Picture.php +++ b/tests/Config/Models/Picture.php @@ -1,6 +1,6 @@ Date: Mon, 1 Jul 2024 10:53:23 +0100 Subject: [PATCH 18/21] add fk to relation --- tests/Config/Models/MediaUploader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Config/Models/MediaUploader.php b/tests/Config/Models/MediaUploader.php index 20d841a..638eca1 100644 --- a/tests/Config/Models/MediaUploader.php +++ b/tests/Config/Models/MediaUploader.php @@ -29,7 +29,7 @@ class MediaUploader extends Model implements HasMedia public function belongsToManyRelation() : BelongsToMany { - return $this->belongsToMany(File::class, 'uploaders_pivot') + return $this->belongsToMany(File::class, 'uploaders_pivot', 'uploader_id') ->using(UploadersPivot::class) ->withPivot(['dropzone', 'easymde', 'upload', 'image', 'upload_multiple']); } From 471d18c236504950c42479e404760af6b9bcb8f3 Mon Sep 17 00:00:00 2001 From: pxpm Date: Thu, 4 Jul 2024 15:11:36 +0100 Subject: [PATCH 19/21] add media library tests --- src/Uploaders/MediaAjaxUploader.php | 1 + src/Uploaders/MediaMultipleFiles.php | 17 +++++++ src/Uploaders/MediaSingleBase64Image.php | 18 ++++++- src/Uploaders/MediaSingleFile.php | 23 +++++++++ src/Uploaders/MediaUploader.php | 2 + src/Uploaders/Traits/DeletesUploadedFiles.php | 48 +++++++++++++++++++ .../Traits/HandleRepeatableUploads.php | 32 ++++++++++++- src/Uploaders/Traits/IdentifiesMedia.php | 8 ++-- .../create_media_uploader_table.php | 8 ++-- tests/Config/Models/File.php | 6 ++- tests/Config/Models/MediaUploader.php | 8 ++-- tests/Config/Models/UploadersPivot.php | 4 ++ tests/Feature/MediaUploadersTest.php | 14 +++--- 13 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 src/Uploaders/Traits/DeletesUploadedFiles.php diff --git a/src/Uploaders/MediaAjaxUploader.php b/src/Uploaders/MediaAjaxUploader.php index 42f11d6..b5ffd5c 100644 --- a/src/Uploaders/MediaAjaxUploader.php +++ b/src/Uploaders/MediaAjaxUploader.php @@ -17,6 +17,7 @@ class MediaAjaxUploader extends BackpackAjaxUploader use Traits\HasCollections; use Traits\RetrievesUploadedFiles; use Traits\HandleRepeatableUploads; + use Traits\DeletesUploadedFiles; public function __construct(array $crudObject, array $configuration) { diff --git a/src/Uploaders/MediaMultipleFiles.php b/src/Uploaders/MediaMultipleFiles.php index 3a4b232..19d2133 100644 --- a/src/Uploaders/MediaMultipleFiles.php +++ b/src/Uploaders/MediaMultipleFiles.php @@ -74,4 +74,21 @@ public function uploadRepeatableFiles($value, $previousValues, $entry = null) } } } + + protected function hasDeletedFiles($value): bool + { + return empty($this->getFilesToDeleteFromRequest()) ? false : true; + } + + protected function getEntryAttributeValue(Model $entry) + { + $value = $entry->{$this->getAttributeName()}; + + return isset($entry->getCasts()[$this->getName()]) ? $value : json_encode($value); + } + + private function getFilesToDeleteFromRequest(): array + { + return collect(CRUD::getRequest()->get('clear_'.$this->getNameForRequest()))->flatten()->toArray(); + } } diff --git a/src/Uploaders/MediaSingleBase64Image.php b/src/Uploaders/MediaSingleBase64Image.php index e697fff..20425e6 100644 --- a/src/Uploaders/MediaSingleBase64Image.php +++ b/src/Uploaders/MediaSingleBase64Image.php @@ -5,15 +5,20 @@ use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; +use Illuminate\Support\Collection; class MediaSingleBase64Image extends MediaUploader { - public function uploadFiles(Model $entry, $values = null) + public function uploadFiles(Model $entry, $value = null) { $value = $value ?? CrudPanelFacade::getRequest()->get($this->getName()); $previousImage = $this->getPreviousFiles($entry); + if (is_a($previousImage, Collection::class, true)) { + $previousImage = null; + } + if (! $value && $previousImage) { $previousImage->delete(); @@ -24,7 +29,6 @@ public function uploadFiles(Model $entry, $values = null) if ($previousImage) { $previousImage->delete(); } - $this->addMediaFile($entry, $value); } } @@ -66,4 +70,14 @@ private function getMediaFromFileUrl($previousImages, $fileUrl, $entry) return is_array($previousImage) ? array_shift($previousImage) : null; } + + protected function shouldUploadFiles($value): bool + { + return $value && is_string($value) && Str::startsWith($value, 'data:image'); + } + + public function shouldKeepPreviousValueUnchanged(Model $entry, $entryValue): bool + { + return $entry->exists && is_string($entryValue) && ! Str::startsWith($entryValue, 'data:image'); + } } diff --git a/src/Uploaders/MediaSingleFile.php b/src/Uploaders/MediaSingleFile.php index 5e24c92..34ad93c 100644 --- a/src/Uploaders/MediaSingleFile.php +++ b/src/Uploaders/MediaSingleFile.php @@ -5,6 +5,7 @@ use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Collection; class MediaSingleFile extends MediaUploader { @@ -14,6 +15,10 @@ public function uploadFiles(Model $entry, $value = null) $previousFile = $this->getPreviousFiles($entry); + if (is_a($previousFile, Collection::class, true)) { + $previousFile = null; + } + if ($previousFile && ($value && is_a($value, UploadedFile::class) || request()->has($this->getName()))) { $previousFile->delete(); } @@ -53,4 +58,22 @@ public function uploadRepeatableFiles($values, $previousFiles, $entry = null) } } } + + /** + * Single file uploaders send no value when they are not dirty. + */ + public function shouldKeepPreviousValueUnchanged(Model $entry, $entryValue): bool + { + return is_string($entryValue); + } + + protected function hasDeletedFiles($entryValue): bool + { + return $entryValue === null; + } + + protected function shouldUploadFiles($value): bool + { + return is_a($value, 'Illuminate\Http\UploadedFile', true); + } } diff --git a/src/Uploaders/MediaUploader.php b/src/Uploaders/MediaUploader.php index 76d7031..de77efe 100644 --- a/src/Uploaders/MediaUploader.php +++ b/src/Uploaders/MediaUploader.php @@ -17,6 +17,7 @@ abstract class MediaUploader extends Uploader use Traits\HasCollections; use Traits\RetrievesUploadedFiles; use Traits\HandleRepeatableUploads; + use Traits\DeletesUploadedFiles; public $displayConversions; @@ -84,4 +85,5 @@ private function getConversionToDisplay($item) return false; } + } diff --git a/src/Uploaders/Traits/DeletesUploadedFiles.php b/src/Uploaders/Traits/DeletesUploadedFiles.php new file mode 100644 index 0000000..7009524 --- /dev/null +++ b/src/Uploaders/Traits/DeletesUploadedFiles.php @@ -0,0 +1,48 @@ +shouldDeleteFiles()) { + return; + } + + $files = $entry->media->filter(function($query) { + return $query->custom_properties['name'] == $this->getAttributeName() && + $query->custom_properties['repeatableContainerName'] == $this->getRepeatableContainerName() && + $query->custom_properties['repeatableRow'] == null; + }); + + $files->each->delete(); + + } + + protected function deletePivotFiles(Model|Pivot $model) + { + if (! $this->shouldDeleteFiles()) { + return; + } + + if (! is_a($model, Pivot::class, true)) { + $pivots = $model->{$this->getRepeatableContainerName()}; + foreach ($pivots as $pivot) { + $pivot = $pivot->pivot->loadMissing('media'); + $this->deleteFiles($pivot); + } + + return; + } + + //this is a workaround for Laravel Pivot Models, because they don't bring the primary key when eager loading + // https://github.com/laravel/framework/issues/31658 + $model->refresh(); + $model->loadMissing('media'); + + $this->deleteFiles($model); + } +} \ No newline at end of file diff --git a/src/Uploaders/Traits/HandleRepeatableUploads.php b/src/Uploaders/Traits/HandleRepeatableUploads.php index 9c61b79..a14dd8c 100644 --- a/src/Uploaders/Traits/HandleRepeatableUploads.php +++ b/src/Uploaders/Traits/HandleRepeatableUploads.php @@ -22,6 +22,36 @@ public function processRepeatableUploads(Model $entry, Collection $values): arra return $values->toArray(); } + protected function uploadRelationshipFiles(Model $entry): Model + { + $entryValue = $entry->{$this->getAttributeName()}; + + if ($this->handleMultipleFiles && is_string($entryValue)) { + try { + $entryValue = json_decode($entryValue, true); + } catch (\Exception) { + return $entry; + } + } + + if ($this->hasDeletedFiles($entryValue)) { + $entry->{$this->getAttributeName()} = $this->uploadFiles($entry, false); + $this->updatedPreviousFiles = $this->getEntryAttributeValue($entry); + } + + if ($this->shouldKeepPreviousValueUnchanged($entry, $entryValue)) { + $entry->{$this->getAttributeName()} = $this->updatedPreviousFiles ?? $this->getEntryOriginalValue($entry); + + return $entry; + } + + if ($this->shouldUploadFiles($entryValue)) { + $entry->{$this->getAttributeName()} = $this->uploadFiles($entry, $entryValue); + } + + return $entry; + } + public function getPreviousRepeatableValues(Model $entry) { if ($this->canHandleMultipleFiles()) { @@ -64,4 +94,4 @@ public function getPreviousRepeatableMedia(Model $entry) return $orderedMedia; } -} \ No newline at end of file +} diff --git a/src/Uploaders/Traits/IdentifiesMedia.php b/src/Uploaders/Traits/IdentifiesMedia.php index 843df3e..57f2fde 100644 --- a/src/Uploaders/Traits/IdentifiesMedia.php +++ b/src/Uploaders/Traits/IdentifiesMedia.php @@ -7,11 +7,10 @@ trait IdentifiesMedia { public string $mediaName; - + public function getMediaIdentifier($media, $entry = null) { $path = PathGeneratorFactory::create($media); - if ($entry && ! empty($entry->mediaConversions)) { $conversion = array_filter($entry->mediaConversions, function ($item) use ($media) { return $item->getName() === $this->getConversionToDisplay($media); @@ -23,7 +22,10 @@ public function getMediaIdentifier($media, $entry = null) return $path->getPathForConversions($media).$conversion->getConversionFile($media); } + if (is_null($path)) { + dd($media); + } return $path->getPath($media).$media->file_name; } -} \ No newline at end of file +} diff --git a/tests/Config/Database/Migrations/create_media_uploader_table.php b/tests/Config/Database/Migrations/create_media_uploader_table.php index 038a0cf..6fdbdf7 100644 --- a/tests/Config/Database/Migrations/create_media_uploader_table.php +++ b/tests/Config/Database/Migrations/create_media_uploader_table.php @@ -7,14 +7,14 @@ return new class() extends Migration { public function up(): void { - Schema::create('media_uploaders', function (Blueprint $table) { - $table->id(); + /* Schema::create('uploaders', function (Blueprint $table) { + $table->unsignedBigInteger('id')->primary(); $table->json('repeatable')->nullable(); - }); + }); */ } public function down(): void { - Schema::dropIfExists('media_uploaders'); + //Schema::dropIfExists('uploaders'); } }; diff --git a/tests/Config/Models/File.php b/tests/Config/Models/File.php index d78d69c..ce8cdd2 100644 --- a/tests/Config/Models/File.php +++ b/tests/Config/Models/File.php @@ -20,8 +20,10 @@ class File extends Model implements HasMedia public $timestamps = false; - public function belongsToManyRelation() + protected $table = 'documents'; + + public function uploaders() { - return $this->belongsToMany(MediaUploader::class, 'media_uploaders_pivot'); + return $this->belongsToMany(MediaUploader::class, 'uploaders_pivot', 'file_id', 'uploader_id'); } } diff --git a/tests/Config/Models/MediaUploader.php b/tests/Config/Models/MediaUploader.php index 638eca1..348f81c 100644 --- a/tests/Config/Models/MediaUploader.php +++ b/tests/Config/Models/MediaUploader.php @@ -21,17 +21,19 @@ class MediaUploader extends Model implements HasMedia */ protected $fillable = ['repeatable']; + protected $table = 'uploaders'; + public $timestamps = false; protected $casts = [ 'repeatable' => 'json', ]; - public function belongsToManyRelation() : BelongsToMany + public function documents() : BelongsToMany { - return $this->belongsToMany(File::class, 'uploaders_pivot', 'uploader_id') + return $this->belongsToMany(File::class, 'uploaders_pivot', 'uploader_id', 'file_id') ->using(UploadersPivot::class) - ->withPivot(['dropzone', 'easymde', 'upload', 'image', 'upload_multiple']); + ->withPivot(['id', 'dropzone', 'easymde', 'upload', 'image', 'upload_multiple']); } public function hasManyRelation() : HasMany diff --git a/tests/Config/Models/UploadersPivot.php b/tests/Config/Models/UploadersPivot.php index 2129e44..16e3c9d 100644 --- a/tests/Config/Models/UploadersPivot.php +++ b/tests/Config/Models/UploadersPivot.php @@ -13,5 +13,9 @@ class UploadersPivot extends Pivot implements HasMedia public $timestamps = false; + public $incrementing = true; + protected $table = 'uploaders_pivot'; + + protected $fillable = ['id', 'uploader_id', 'file_id', 'dropzone', 'easymde', 'upload', 'image', 'upload_multiple']; } diff --git a/tests/Feature/MediaUploadersTest.php b/tests/Feature/MediaUploadersTest.php index d70a3ab..ab822c0 100644 --- a/tests/Feature/MediaUploadersTest.php +++ b/tests/Feature/MediaUploadersTest.php @@ -40,7 +40,7 @@ public function test_it_can_upload_a_single_file() $response->assertRedirect($this->testBaseUrl); - $this->assertDatabaseCount('media_uploaders', 1); + $this->assertDatabaseCount('uploaders', 1); $uploader = MediaUploader::first(); @@ -62,7 +62,7 @@ public function test_it_can_upload_multiple_files() $response->assertRedirect($this->testBaseUrl); - $this->assertDatabaseCount('media_uploaders', 1); + $this->assertDatabaseCount('uploaders', 1); $uploader = MediaUploader::first(); @@ -86,7 +86,7 @@ public function test_it_can_upload_files_for_multiple_uploaders() $response->assertRedirect($this->testBaseUrl); - $this->assertDatabaseCount('media_uploaders', 1); + $this->assertDatabaseCount('uploaders', 1); $uploader = MediaUploader::first(); @@ -135,7 +135,7 @@ public function test_it_can_update_files() $response->assertRedirect($this->testBaseUrl); - $this->assertDatabaseCount('media_uploaders', 1); + $this->assertDatabaseCount('uploaders', 1); $uploader = MediaUploader::first(); @@ -163,7 +163,7 @@ public function test_it_keeps_previous_values_unchanged_when_not_deleted() $response->assertRedirect($this->testBaseUrl); - $this->assertDatabaseCount('media_uploaders', 1); + $this->assertDatabaseCount('uploaders', 1); $uploader = MediaUploader::first(); @@ -188,7 +188,7 @@ public function test_upload_multiple_can_delete_uploaded_files_and_add_at_the_sa $response->assertRedirect($this->testBaseUrl); - $this->assertDatabaseCount('media_uploaders', 1); + $this->assertDatabaseCount('uploaders', 1); $uploader = MediaUploader::first(); @@ -209,7 +209,7 @@ public function test_it_can_delete_uploaded_files() $response->assertStatus(200); - $this->assertDatabaseCount('media_uploaders', 0); + $this->assertDatabaseCount('uploaders', 0); $files = Storage::disk('uploaders')->allFiles(); From 16511e7a7c9f19dae112180012b358cc6d9adeb3 Mon Sep 17 00:00:00 2001 From: pxpm Date: Thu, 7 Nov 2024 10:14:15 +0000 Subject: [PATCH 20/21] save the object in the macro --- src/AddonServiceProvider.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/AddonServiceProvider.php b/src/AddonServiceProvider.php index 9a2fa4a..2dc14b0 100644 --- a/src/AddonServiceProvider.php +++ b/src/AddonServiceProvider.php @@ -41,8 +41,10 @@ public function boot() // register media upload macros on crud fields and columns. if (! CrudField::hasMacro('withMedia')) { CrudField::macro('withMedia', function ($uploadDefinition = [], $subfield = null, $registerEvents = true) { - $uploadDefinition = is_array($uploadDefinition) ? $uploadDefinition : []; /** @var CrudField $this */ + $uploadDefinition = is_array($uploadDefinition) ? $uploadDefinition : []; + $this->setAttributeValue('withMedia', $uploadDefinition); + $this->save(); RegisterUploadEvents::handle($this, $uploadDefinition, 'withMedia', $subfield, $registerEvents); return $this; @@ -51,8 +53,10 @@ public function boot() if (! CrudColumn::hasMacro('withMedia')) { CrudColumn::macro('withMedia', function ($uploadDefinition = [], $subfield = null, $registerEvents = true) { - $uploadDefinition = is_array($uploadDefinition) ? $uploadDefinition : []; /** @var CrudColumn $this */ + $uploadDefinition = is_array($uploadDefinition) ? $uploadDefinition : []; + $this->setAttributeValue('withMedia', $uploadDefinition); + $this->save(); RegisterUploadEvents::handle($this, $uploadDefinition, 'withMedia', $subfield, $registerEvents); return $this; From edfac436dbc70e03d64063d7f97a6781304880e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20T=C4=83b=C4=83citu?= Date: Thu, 7 Nov 2024 15:57:34 +0200 Subject: [PATCH 21/21] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 603cc83..2cd84cd 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "Laravel", "Backpack", "Backpack for Laravel", "Backpack Addon", "spatie medialibrary uploaders" ], "require": { - "backpack/crud": "dev-fix-uploaders as 6.7", + "backpack/crud": "^6.0|dev-next", "spatie/laravel-medialibrary": "^10.7|^11.3" }, "require-dev": {