use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
+use Symfony\Component\ErrorHandler\Error\FatalError;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;
'password_confirmation',
];
+ /**
+ * A function to run upon out of memory.
+ * If it returns a response, that will be provided back to the request
+ * upon an out of memory event.
+ *
+ * @var ?callable<?\Illuminate\Http\Response>
+ */
+ protected $onOutOfMemory = null;
+
/**
* Report or log an exception.
*
*/
public function render($request, Throwable $e)
{
+ if ($e instanceof FatalError && str_contains($e->getMessage(), 'bytes exhausted (tried to allocate') && $this->onOutOfMemory) {
+ $response = call_user_func($this->onOutOfMemory);
+ if ($response) {
+ return $response;
+ }
+ }
+
if ($e instanceof PostTooLargeException) {
$e = new NotifyException(trans('errors.server_post_limit'), '/', 413);
}
return parent::render($request, $e);
}
+ /**
+ * Provide a function to be called when an out of memory event occurs.
+ * If the callable returns a response, this response will be returned
+ * to the request upon error.
+ */
+ public function prepareForOutOfMemory(callable $onOutOfMemory)
+ {
+ $this->onOutOfMemory = $onOutOfMemory;
+ }
+
+ /**
+ * Forget the current out of memory handler, if existing.
+ */
+ public function forgetOutOfMemoryHandler()
+ {
+ $this->onOutOfMemory = null;
+ }
+
/**
* Check if the given request is an API request.
*/
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageService;
+use BookStack\Util\OutOfMemoryHandler;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
return response('');
}
+ /**
+ * Rebuild the thumbnails for the given image.
+ */
+ public function rebuildThumbnails(string $id)
+ {
+ $image = $this->imageRepo->getById($id);
+ $this->checkImagePermission($image);
+ $this->checkOwnablePermission('image-update', $image);
+
+ new OutOfMemoryHandler(function () {
+ return $this->jsonError(trans('errors.image_thumbnail_memory_limit'));
+ });
+
+ $this->imageRepo->loadThumbs($image, true);
+
+ return response(trans('components.image_rebuild_thumbs_success'));
+ }
+
/**
* Check related page permission and ensure type is drawio or gallery.
*/
--- /dev/null
+<?php
+
+namespace BookStack\Util;
+
+use BookStack\Exceptions\Handler;
+use Illuminate\Contracts\Debug\ExceptionHandler;
+
+/**
+ * Create a handler which runs the provided actions upon an
+ * out-of-memory event. This allows reserving of memory to allow
+ * the desired action to run as needed.
+ *
+ * Essentially provides a wrapper and memory reserving around the
+ * memory handling added to the default app error handler.
+ */
+class OutOfMemoryHandler
+{
+ protected $onOutOfMemory;
+ protected string $memoryReserve = '';
+
+ public function __construct(callable $onOutOfMemory, int $memoryReserveMB = 4)
+ {
+ $this->onOutOfMemory = $onOutOfMemory;
+
+ $this->memoryReserve = str_repeat('x', $memoryReserveMB * 1_000_000);
+ $this->getHandler()->prepareForOutOfMemory(function () {
+ return $this->handle();
+ });
+ }
+
+ protected function handle(): mixed
+ {
+ $result = null;
+ $this->memoryReserve = '';
+
+ if ($this->onOutOfMemory) {
+ $result = call_user_func($this->onOutOfMemory);
+ $this->forget();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Forget the handler so no action is taken place on out of memory.
+ */
+ public function forget(): void
+ {
+ $this->memoryReserve = '';
+ $this->onOutOfMemory = null;
+ $this->getHandler()->forgetOutOfMemoryHandler();
+ }
+
+ protected function getHandler(): Handler
+ {
+ return app()->make(ExceptionHandler::class);
+ }
+}
'image_delete_success' => 'Image successfully deleted',
'image_replace' => 'Replace Image',
'image_replace_success' => 'Image file successfully updated',
+ 'image_rebuild_thumbs' => 'Regenerate Size Variations',
+ 'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',
// Code Editor
'code_editor' => 'Edit Code',
'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',
+ 'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits',
'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
}
});
+ // Rebuild thumbs click
+ onChildEvent(this.formContainer, '#image-manager-rebuild-thumbs', 'click', async (_, button) => {
+ button.disabled = true;
+ if (this.lastSelected) {
+ await this.rebuildThumbnails(this.lastSelected.id);
+ }
+ button.disabled = false;
+ });
+
// Edit form submit
this.formContainer.addEventListener('ajax-form-success', () => {
this.refreshGallery();
return this.loadMore.querySelector('button') && !this.loadMore.hasAttribute('hidden');
}
+ async rebuildThumbnails(imageId) {
+ try {
+ const response = await window.$http.put(`/images/${imageId}/rebuild-thumbnails`);
+ window.$events.success(response.data);
+ this.refreshGallery();
+ } catch (err) {
+ window.$events.showResponseError(err);
+ }
+ }
+
}
id="image-manager-replace"
refs="dropzone@select-button"
class="text-item">{{ trans('components.image_replace') }}</button>
+ <button type="button"
+ id="image-manager-rebuild-thumbs"
+ class="text-item">{{ trans('components.image_rebuild_thumbs') }}</button>
@endif
</div>
</div>
Route::post('/images/drawio', [UploadControllers\DrawioImageController::class, 'create']);
Route::get('/images/edit/{id}', [UploadControllers\ImageController::class, 'edit']);
Route::put('/images/{id}/file', [UploadControllers\ImageController::class, 'updateFile']);
+ Route::put('/images/{id}/rebuild-thumbnails', [UploadControllers\ImageController::class, 'rebuildThumbnails']);
Route::put('/images/{id}', [UploadControllers\ImageController::class, 'update']);
Route::delete('/images/{id}', [UploadControllers\ImageController::class, 'destroy']);