Skip to content

Commit

Permalink
Merge pull request #4578 from BookStackApp/upload_handling
Browse files Browse the repository at this point in the history
Improvements to file/image upload handling UX
  • Loading branch information
ssddanbrown authored Oct 1, 2023
2 parents 21badde + ffb04a8 commit 8bba5dd
Show file tree
Hide file tree
Showing 29 changed files with 854 additions and 525 deletions.
15 changes: 4 additions & 11 deletions app/Entities/Models/Book.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,19 @@ public function getUrl(string $path = ''): string

/**
* Returns book cover image, if book cover not exists return default cover image.
*
* @param int $width - Width of the image
* @param int $height - Height of the image
*
* @return string
*/
public function getBookCover($width = 440, $height = 250)
public function getBookCover(int $width = 440, int $height = 250): string
{
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
if (!$this->image_id) {
if (!$this->image_id || !$this->cover) {
return $default;
}

try {
$cover = $this->cover ? url($this->cover->getThumb($width, $height, false)) : $default;
return $this->cover->getThumb($width, $height, false) ?? $default;
} catch (Exception $err) {
$cover = $default;
return $default;
}

return $cover;
}

/**
Expand Down
20 changes: 7 additions & 13 deletions app/Entities/Models/Bookshelf.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace BookStack\Entities\Models;

use BookStack\Uploads\Image;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
Expand Down Expand Up @@ -49,28 +50,21 @@ public function getUrl(string $path = ''): string
}

/**
* Returns BookShelf cover image, if cover does not exists return default cover image.
*
* @param int $width - Width of the image
* @param int $height - Height of the image
*
* @return string
* Returns shelf cover image, if cover not exists return default cover image.
*/
public function getBookCover($width = 440, $height = 250)
public function getBookCover(int $width = 440, int $height = 250): string
{
// TODO - Make generic, focused on books right now, Perhaps set-up a better image
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
if (!$this->image_id) {
if (!$this->image_id || !$this->cover) {
return $default;
}

try {
$cover = $this->cover ? url($this->cover->getThumb($width, $height, false)) : $default;
} catch (\Exception $err) {
$cover = $default;
return $this->cover->getThumb($width, $height, false) ?? $default;
} catch (Exception $err) {
return $default;
}

return $cover;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion app/Entities/Tools/ExportFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ protected function containHtml(string $htmlContent): string
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
$oldImgTagString = $imgMatch;
$srcString = $imageTagsOutput[2][$index];
$imageEncoded = $this->imageService->imageUriToBase64($srcString);
$imageEncoded = $this->imageService->imageUrlToBase64($srcString);
if ($imageEncoded === null) {
$imageEncoded = $srcString;
}
Expand Down
42 changes: 41 additions & 1 deletion app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\PostTooLargeException;
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;

Expand All @@ -35,6 +37,15 @@ class Handler extends ExceptionHandler
'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.
*
Expand All @@ -59,19 +70,48 @@ public function report(Throwable $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);
}

if ($this->isApiRequest($request)) {
return $this->renderApiException($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.
*/
protected function isApiRequest(Request $request): bool
{
return strpos($request->path(), 'api/') === 0;
return str_starts_with($request->path(), 'api/');
}

/**
Expand Down
27 changes: 18 additions & 9 deletions app/Uploads/Controllers/DrawioImageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,44 @@
use BookStack\Exceptions\ImageUploadException;
use BookStack\Http\Controller;
use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageResizer;
use BookStack\Util\OutOfMemoryHandler;
use Exception;
use Illuminate\Http\Request;

class DrawioImageController extends Controller
{
protected $imageRepo;

public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
public function __construct(
protected ImageRepo $imageRepo
) {
}

/**
* Get a list of gallery images, in a list.
* Can be paged and filtered by entity.
*/
public function list(Request $request)
public function list(Request $request, ImageResizer $resizer)
{
$page = $request->get('page', 1);
$searchTerm = $request->get('search', null);
$uploadedToFilter = $request->get('uploaded_to', null);
$parentTypeFilter = $request->get('filter_type', null);

$imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);

return view('pages.parts.image-manager-list', [
$viewData = [
'warning' => '',
'images' => $imgData['images'],
'hasMore' => $imgData['has_more'],
]);
];

new OutOfMemoryHandler(function () use ($viewData) {
$viewData['warning'] = trans('errors.image_gallery_thumbnail_memory_limit');
return response()->view('pages.parts.image-manager-list', $viewData, 200);
});

$resizer->loadGalleryThumbnailsForMany($imgData['images']);

return view('pages.parts.image-manager-list', $viewData);
}

/**
Expand Down
25 changes: 21 additions & 4 deletions app/Uploads/Controllers/GalleryImageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
use BookStack\Exceptions\ImageUploadException;
use BookStack\Http\Controller;
use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageResizer;
use BookStack\Util\OutOfMemoryHandler;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;

class GalleryImageController extends Controller
Expand All @@ -19,19 +23,28 @@ public function __construct(
* Get a list of gallery images, in a list.
* Can be paged and filtered by entity.
*/
public function list(Request $request)
public function list(Request $request, ImageResizer $resizer)
{
$page = $request->get('page', 1);
$searchTerm = $request->get('search', null);
$uploadedToFilter = $request->get('uploaded_to', null);
$parentTypeFilter = $request->get('filter_type', null);

$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 30, $uploadedToFilter, $searchTerm);

return view('pages.parts.image-manager-list', [
$viewData = [
'warning' => '',
'images' => $imgData['images'],
'hasMore' => $imgData['has_more'],
]);
];

new OutOfMemoryHandler(function () use ($viewData) {
$viewData['warning'] = trans('errors.image_gallery_thumbnail_memory_limit');
return response()->view('pages.parts.image-manager-list', $viewData, 200);
});

$resizer->loadGalleryThumbnailsForMany($imgData['images']);

return view('pages.parts.image-manager-list', $viewData);
}

/**
Expand All @@ -51,6 +64,10 @@ public function create(Request $request)
return $this->jsonError(implode("\n", $exception->errors()['file']));
}

new OutOfMemoryHandler(function () {
return $this->jsonError(trans('errors.image_upload_memory_limit'));
});

try {
$imageUpload = $request->file('file');
$uploadedTo = $request->get('uploaded_to', 0);
Expand Down
57 changes: 43 additions & 14 deletions app/Uploads/Controllers/ImageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@

use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Http\Controller;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageResizer;
use BookStack\Uploads\ImageService;
use BookStack\Util\OutOfMemoryHandler;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class ImageController extends Controller
{
public function __construct(
protected ImageRepo $imageRepo,
protected ImageService $imageService
protected ImageService $imageService,
protected ImageResizer $imageResizer,
) {
}

Expand All @@ -38,23 +41,18 @@ public function showImage(string $path)

/**
* Update image details.
*
* @throws ImageUploadException
* @throws ValidationException
*/
public function update(Request $request, string $id)
{
$this->validate($request, [
$data = $this->validate($request, [
'name' => ['required', 'min:2', 'string'],
]);

$image = $this->imageRepo->getById($id);
$this->checkImagePermission($image);
$this->checkOwnablePermission('image-update', $image);

$image = $this->imageRepo->updateImageDetails($image, $request->all());

$this->imageRepo->loadThumbs($image);
$image = $this->imageRepo->updateImageDetails($image, $data);

return view('pages.parts.image-manager-form', [
'image' => $image,
Expand All @@ -76,6 +74,10 @@ public function updateFile(Request $request, string $id)
$this->checkOwnablePermission('image-update', $image);
$file = $request->file('file');

new OutOfMemoryHandler(function () {
return $this->jsonError(trans('errors.image_upload_memory_limit'));
});

try {
$this->imageRepo->updateImageFile($image, $file);
} catch (ImageUploadException $exception) {
Expand All @@ -99,12 +101,20 @@ public function edit(Request $request, string $id)
$dependantPages = $this->imageRepo->getPagesUsingImage($image);
}

$this->imageRepo->loadThumbs($image);

return view('pages.parts.image-manager-form', [
$viewData = [
'image' => $image,
'dependantPages' => $dependantPages ?? null,
]);
'warning' => '',
];

new OutOfMemoryHandler(function () use ($viewData) {
$viewData['warning'] = trans('errors.image_thumbnail_memory_limit');
return response()->view('pages.parts.image-manager-form', $viewData);
});

$this->imageResizer->loadGalleryThumbnailsForImage($image, false);

return view('pages.parts.image-manager-form', $viewData);
}

/**
Expand All @@ -123,10 +133,29 @@ public function destroy(string $id)
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->imageResizer->loadGalleryThumbnailsForImage($image, true);

return response(trans('components.image_rebuild_thumbs_success'));
}

/**
* Check related page permission and ensure type is drawio or gallery.
* @throws NotifyException
*/
protected function checkImagePermission(Image $image)
protected function checkImagePermission(Image $image): void
{
if ($image->type !== 'drawio' && $image->type !== 'gallery') {
$this->showPermissionError();
Expand Down
Loading

0 comments on commit 8bba5dd

Please sign in to comment.