diff --git a/app/Uploads/Controllers/ImageController.php b/app/Uploads/Controllers/ImageController.php index fea0713a2..2c611c515 100644 --- a/app/Uploads/Controllers/ImageController.php +++ b/app/Uploads/Controllers/ImageController.php @@ -14,13 +14,10 @@ use Illuminate\Validation\ValidationException; class ImageController extends Controller { - protected ImageRepo $imageRepo; - protected ImageService $imageService; - - public function __construct(ImageRepo $imageRepo, ImageService $imageService) - { - $this->imageRepo = $imageRepo; - $this->imageService = $imageService; + public function __construct( + protected ImageRepo $imageRepo, + protected ImageService $imageService + ) { } /** @@ -65,6 +62,29 @@ class ImageController extends Controller ]); } + /** + * Update the file for an existing image. + */ + public function updateFile(Request $request, string $id) + { + $this->validate($request, [ + 'file' => ['required', 'file', ...$this->getImageValidationRules()], + ]); + + $image = $this->imageRepo->getById($id); + $this->checkImagePermission($image); + $this->checkOwnablePermission('image-update', $image); + $file = $request->file('file'); + + try { + $this->imageRepo->updateImageFile($image, $file); + } catch (ImageUploadException $exception) { + return $this->jsonError($exception->getMessage()); + } + + return response(''); + } + /** * Get the form for editing the given image. * diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php index 2d35d96ff..e28e7b794 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -11,16 +11,10 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; class ImageRepo { - protected ImageService $imageService; - protected PermissionApplicator $permissions; - - /** - * ImageRepo constructor. - */ - public function __construct(ImageService $imageService, PermissionApplicator $permissions) - { - $this->imageService = $imageService; - $this->permissions = $permissions; + public function __construct( + protected ImageService $imageService, + protected PermissionApplicator $permissions + ) { } /** @@ -164,12 +158,29 @@ class ImageRepo public function updateImageDetails(Image $image, $updateDetails): Image { $image->fill($updateDetails); + $image->updated_by = user()->id; $image->save(); $this->loadThumbs($image); return $image; } + /** + * Update the image file of an existing image in the system. + * @throws ImageUploadException + */ + public function updateImageFile(Image $image, UploadedFile $file): void + { + if ($file->getClientOriginalExtension() !== pathinfo($image->path, PATHINFO_EXTENSION)) { + throw new ImageUploadException(trans('errors.image_upload_replace_type')); + } + + $image->updated_by = user()->id; + $image->save(); + $this->imageService->replaceExistingFromUpload($image->path, $image->type, $file); + $this->loadThumbs($image, true); + } + /** * Destroys an Image object along with its revisions, files and thumbnails. * @@ -202,11 +213,11 @@ class ImageRepo /** * Load thumbnails onto an image object. */ - public function loadThumbs(Image $image): void + public function loadThumbs(Image $image, bool $forceCreate = false): void { $image->setAttribute('thumbs', [ - 'gallery' => $this->getThumbnail($image, 150, 150, false), - 'display' => $this->getThumbnail($image, 1680, null, true), + 'gallery' => $this->getThumbnail($image, 150, 150, false, $forceCreate), + 'display' => $this->getThumbnail($image, 1680, null, true, $forceCreate), ]); } @@ -215,10 +226,10 @@ class ImageRepo * If $keepRatio is true only the width will be used. * Checks the cache then storage to avoid creating / accessing the filesystem on every check. */ - protected function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio): ?string + protected function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio, bool $forceCreate): ?string { try { - return $this->imageService->getThumbnail($image, $width, $height, $keepRatio); + return $this->imageService->getThumbnail($image, $width, $height, $keepRatio, $forceCreate); } catch (Exception $exception) { return null; } diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index 5458779e9..66596a57f 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -194,6 +194,14 @@ class ImageService return $image; } + public function replaceExistingFromUpload(string $path, string $type, UploadedFile $file): void + { + $imageData = file_get_contents($file->getRealPath()); + $storage = $this->getStorageDisk($type); + $adjustedPath = $this->adjustPathForStorageDisk($path, $type); + $storage->put($adjustedPath, $imageData); + } + /** * Save image data for the given path in the public space, if possible, * for the provided storage mechanism. @@ -262,7 +270,7 @@ class ImageService * @throws Exception * @throws InvalidArgumentException */ - public function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio = false): string + public function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio = false, bool $forceCreate = false): string { // Do not resize GIF images where we're not cropping if ($keepRatio && $this->isGif($image)) { @@ -277,13 +285,13 @@ class ImageService // Return path if in cache $cachedThumbPath = $this->cache->get($thumbCacheKey); - if ($cachedThumbPath) { + if ($cachedThumbPath && !$forceCreate) { return $this->getPublicUrl($cachedThumbPath); } // If thumbnail has already been generated, serve that and cache path $storage = $this->getStorageDisk($image->type); - if ($storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) { + if (!$forceCreate && $storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) { $this->cache->put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72); return $this->getPublicUrl($thumbFilePath); diff --git a/lang/en/components.php b/lang/en/components.php index a06c26d5b..8a105096b 100644 --- a/lang/en/components.php +++ b/lang/en/components.php @@ -32,6 +32,8 @@ return [ 'image_upload_success' => 'Image uploaded successfully', 'image_update_success' => 'Image details successfully updated', 'image_delete_success' => 'Image successfully deleted', + 'image_replace' => 'Replace Image', + 'image_replace_success' => 'Image file successfully updated', // Code Editor 'code_editor' => 'Edit Code', diff --git a/lang/en/errors.php b/lang/en/errors.php index 6991f96e4..b03fb8c35 100644 --- a/lang/en/errors.php +++ b/lang/en/errors.php @@ -49,6 +49,7 @@ return [ // Drawing & Images 'image_upload_error' => 'An error occurred uploading the image', 'image_upload_type_error' => 'The image type being uploaded is invalid', + 'image_upload_replace_type' => 'Image file replacements must be of the same type', 'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.', // Attachments diff --git a/resources/js/components/dropzone.js b/resources/js/components/dropzone.js index 2b8b35081..1cac09b4a 100644 --- a/resources/js/components/dropzone.js +++ b/resources/js/components/dropzone.js @@ -15,6 +15,7 @@ export class Dropzone extends Component { this.isActive = true; this.url = this.$opts.url; + this.method = (this.$opts.method || 'post').toUpperCase(); this.successMessage = this.$opts.successMessage; this.errorMessage = this.$opts.errorMessage; this.uploadLimitMb = Number(this.$opts.uploadLimit); @@ -167,6 +168,9 @@ export class Dropzone extends Component { startXhrForUpload(upload) { const formData = new FormData(); formData.append('file', upload.file, upload.file.name); + if (this.method !== 'POST') { + formData.append('_method', this.method); + } const component = this; const req = window.$http.createXMLHttpRequest('POST', this.url, { diff --git a/resources/sass/_lists.scss b/resources/sass/_lists.scss index 33e500d6a..ad0803e71 100644 --- a/resources/sass/_lists.scss +++ b/resources/sass/_lists.scss @@ -674,6 +674,10 @@ ul.pagination { text-align: start !important; max-height: 500px; overflow-y: auto; + &.anchor-left { + inset-inline-end: auto; + inset-inline-start: 0; + } &.wide { min-width: 220px; } diff --git a/resources/views/pages/parts/image-manager-form.blade.php b/resources/views/pages/parts/image-manager-form.blade.php index 66231a356..75750ef2f 100644 --- a/resources/views/pages/parts/image-manager-form.blade.php +++ b/resources/views/pages/parts/image-manager-form.blade.php @@ -1,4 +1,14 @@ -
+
id}/file") }}" + option:dropzone:method="PUT" + option:dropzone:success-message="{{ trans('components.image_update_success') }}" + option:dropzone:upload-limit="{{ config('app.upload_limit') }}" + option:dropzone:upload-limit-message="{{ trans('errors.server_upload_limit') }}" + option:dropzone:zone-text="{{ trans('entities.attachments_dropzone') }}" + option:dropzone:file-accept="image/*" + class="image-manager-details"> + +
-
- @if(userCan('image-delete', $image)) - - @endif -
-
+ @if(userCan('image-delete', $image) || userCan('image-update', $image)) + + @endif -
@if(!is_null($dependantPages)) +
@if(count($dependantPages) > 0)

{{ trans('components.image_delete_used') }}