mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Thumbnails: Added OOM handling and regen endpoint
- Added some level of app out-of-memory handling so we can show a proper error message upon OOM events. - Added endpoint and image-manager button/action for regenerating thumbnails for an image so they can be re-created upon failure.
This commit is contained in:
parent
cc0827ff28
commit
5af3041b9b
@ -10,6 +10,7 @@ use Illuminate\Http\Exceptions\PostTooLargeException;
|
|||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Symfony\Component\ErrorHandler\Error\FatalError;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
@ -36,6 +37,15 @@ class Handler extends ExceptionHandler
|
|||||||
'password_confirmation',
|
'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.
|
* Report or log an exception.
|
||||||
*
|
*
|
||||||
@ -60,6 +70,13 @@ class Handler extends ExceptionHandler
|
|||||||
*/
|
*/
|
||||||
public function render($request, Throwable $e)
|
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) {
|
if ($e instanceof PostTooLargeException) {
|
||||||
$e = new NotifyException(trans('errors.server_post_limit'), '/', 413);
|
$e = new NotifyException(trans('errors.server_post_limit'), '/', 413);
|
||||||
}
|
}
|
||||||
@ -71,6 +88,24 @@ class Handler extends ExceptionHandler
|
|||||||
return parent::render($request, $e);
|
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.
|
* Check if the given request is an API request.
|
||||||
*/
|
*/
|
||||||
|
@ -8,6 +8,7 @@ use BookStack\Http\Controller;
|
|||||||
use BookStack\Uploads\Image;
|
use BookStack\Uploads\Image;
|
||||||
use BookStack\Uploads\ImageRepo;
|
use BookStack\Uploads\ImageRepo;
|
||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
|
use BookStack\Util\OutOfMemoryHandler;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
@ -121,6 +122,24 @@ class ImageController extends Controller
|
|||||||
return response('');
|
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.
|
* Check related page permission and ensure type is drawio or gallery.
|
||||||
*/
|
*/
|
||||||
|
58
app/Util/OutOfMemoryHandler.php
Normal file
58
app/Util/OutOfMemoryHandler.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
@ -34,6 +34,8 @@ return [
|
|||||||
'image_delete_success' => 'Image successfully deleted',
|
'image_delete_success' => 'Image successfully deleted',
|
||||||
'image_replace' => 'Replace Image',
|
'image_replace' => 'Replace Image',
|
||||||
'image_replace_success' => 'Image file successfully updated',
|
'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
|
||||||
'code_editor' => 'Edit Code',
|
'code_editor' => 'Edit Code',
|
||||||
|
@ -51,6 +51,7 @@ return [
|
|||||||
'image_upload_error' => 'An error occurred uploading the image',
|
'image_upload_error' => 'An error occurred uploading the image',
|
||||||
'image_upload_type_error' => 'The image type being uploaded is invalid',
|
'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_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.',
|
'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
|
// Attachments
|
||||||
|
@ -90,6 +90,15 @@ export class ImageManager extends Component {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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
|
// Edit form submit
|
||||||
this.formContainer.addEventListener('ajax-form-success', () => {
|
this.formContainer.addEventListener('ajax-form-success', () => {
|
||||||
this.refreshGallery();
|
this.refreshGallery();
|
||||||
@ -268,4 +277,14 @@ export class ImageManager extends Component {
|
|||||||
return this.loadMore.querySelector('button') && !this.loadMore.hasAttribute('hidden');
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,9 @@
|
|||||||
id="image-manager-replace"
|
id="image-manager-replace"
|
||||||
refs="dropzone@select-button"
|
refs="dropzone@select-button"
|
||||||
class="text-item">{{ trans('components.image_replace') }}</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
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -142,6 +142,7 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::post('/images/drawio', [UploadControllers\DrawioImageController::class, 'create']);
|
Route::post('/images/drawio', [UploadControllers\DrawioImageController::class, 'create']);
|
||||||
Route::get('/images/edit/{id}', [UploadControllers\ImageController::class, 'edit']);
|
Route::get('/images/edit/{id}', [UploadControllers\ImageController::class, 'edit']);
|
||||||
Route::put('/images/{id}/file', [UploadControllers\ImageController::class, 'updateFile']);
|
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::put('/images/{id}', [UploadControllers\ImageController::class, 'update']);
|
||||||
Route::delete('/images/{id}', [UploadControllers\ImageController::class, 'destroy']);
|
Route::delete('/images/{id}', [UploadControllers\ImageController::class, 'destroy']);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user