namespace BookStack\Uploads;
-use BookStack\Entities\Models\Book;
-use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Exceptions\ImageUploadException;
-use ErrorException;
use Exception;
-use Illuminate\Contracts\Cache\Repository as Cache;
-use Illuminate\Contracts\Filesystem\FileNotFoundException;
-use Illuminate\Contracts\Filesystem\Filesystem as StorageDisk;
-use Illuminate\Filesystem\FilesystemAdapter;
-use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
-use Intervention\Image\Exception\NotSupportedException;
-use Intervention\Image\ImageManager;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\StreamedResponse;
protected static array $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
public function __construct(
- protected ImageManager $imageTool,
- protected FilesystemManager $fileSystem,
- protected Cache $cache,
protected ImageStorage $storage,
+ protected ImageResizer $resizer,
+ protected EntityQueries $queries,
) {
}
$imageData = file_get_contents($uploadedFile->getRealPath());
if ($resizeWidth !== null || $resizeHeight !== null) {
- $imageData = $this->resizeImage($imageData, $resizeWidth, $resizeHeight, $keepRatio);
+ $imageData = $this->resizer->resizeImageData($imageData, $resizeWidth, $resizeHeight, $keepRatio);
}
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
$imagePath = '/uploads/images/' . $type . '/' . date('Y-m') . '/';
- while ($disk->exists($this->storage->adjustPathForDisk($imagePath . $fileName, $type))) {
+ while ($disk->exists($imagePath . $fileName)) {
$fileName = Str::random(3) . $fileName;
}
}
try {
- $this->storage->storeInPublicSpace($disk, $this->storage->adjustPathForDisk($fullPath, $type), $imageData);
+ $disk->put($fullPath, $imageData, true);
} catch (Exception $e) {
Log::error('Error when attempting image upload:' . $e->getMessage());
{
$imageData = file_get_contents($file->getRealPath());
$disk = $this->storage->getDisk($type);
- $adjustedPath = $this->storage->adjustPathForDisk($path, $type);
- $disk->put($adjustedPath, $imageData);
- }
-
-
- /**
- * Checks if the image is a gif. Returns true if it is, else false.
- */
- protected function isGif(Image $image): bool
- {
- return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
- }
-
- /**
- * Check if the given image and image data is apng.
- */
- protected function isApngData(Image $image, string &$imageData): bool
- {
- $isPng = strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'png';
- if (!$isPng) {
- return false;
- }
-
- $initialHeader = substr($imageData, 0, strpos($imageData, 'IDAT'));
-
- return str_contains($initialHeader, 'acTL');
- }
-
- /**
- * Get the thumbnail for an image.
- * If $keepRatio is true only the width will be used.
- * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
- *
- * @throws Exception
- */
- public function getThumbnail(
- Image $image,
- ?int $width,
- ?int $height,
- bool $keepRatio = false,
- bool $shouldCreate = false,
- bool $canCreate = false,
- ): ?string {
- // Do not resize GIF images where we're not cropping
- if ($keepRatio && $this->isGif($image)) {
- return $this->storage->getPublicUrl($image->path);
- }
-
- $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
- $imagePath = $image->path;
- $thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
-
- $thumbCacheKey = 'images::' . $image->id . '::' . $thumbFilePath;
-
- // Return path if in cache
- $cachedThumbPath = $this->cache->get($thumbCacheKey);
- if ($cachedThumbPath && !$shouldCreate) {
- return $this->storage->getPublicUrl($cachedThumbPath);
- }
-
- // If thumbnail has already been generated, serve that and cache path
- $disk = $this->storage->getDisk($image->type);
- if (!$shouldCreate && $disk->exists($this->storage->adjustPathForDisk($thumbFilePath, $image->type))) {
- $this->cache->put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
-
- return $this->storage->getPublicUrl($thumbFilePath);
- }
-
- $imageData = $disk->get($this->storage->adjustPathForDisk($imagePath, $image->type));
-
- // Do not resize apng images where we're not cropping
- if ($keepRatio && $this->isApngData($image, $imageData)) {
- $this->cache->put($thumbCacheKey, $image->path, 60 * 60 * 72);
-
- return $this->storage->getPublicUrl($image->path);
- }
-
- if (!$shouldCreate && !$canCreate) {
- return null;
- }
-
- // If not in cache and thumbnail does not exist, generate thumb and cache path
- $thumbData = $this->resizeImage($imageData, $width, $height, $keepRatio);
- $this->storage->storeInPublicSpace($disk, $this->storage->adjustPathForDisk($thumbFilePath, $image->type), $thumbData);
- $this->cache->put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
-
- return $this->storage->getPublicUrl($thumbFilePath);
+ $disk->put($path, $imageData);
}
- /**
- * Resize the image of given data to the specified size, and return the new image data.
- *
- * @throws ImageUploadException
- */
- protected function resizeImage(string $imageData, ?int $width, ?int $height, bool $keepRatio): string
- {
- try {
- $thumb = $this->imageTool->make($imageData);
- } catch (ErrorException | NotSupportedException $e) {
- throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
- }
-
- $this->orientImageToOriginalExif($thumb, $imageData);
-
- if ($keepRatio) {
- $thumb->resize($width, $height, function ($constraint) {
- $constraint->aspectRatio();
- $constraint->upsize();
- });
- } else {
- $thumb->fit($width, $height);
- }
-
- $thumbData = (string) $thumb->encode();
-
- // Use original image data if we're keeping the ratio
- // and the resizing does not save any space.
- if ($keepRatio && strlen($thumbData) > strlen($imageData)) {
- return $imageData;
- }
-
- return $thumbData;
- }
-
-
/**
* Get the raw data content from an image.
*
{
$disk = $this->storage->getDisk();
- return $disk->get($this->storage->adjustPathForDisk($image->path, $image->type));
+ return $disk->get($image->path);
}
/**
*
* @throws Exception
*/
- public function destroy(Image $image)
+ public function destroy(Image $image): void
{
- $this->destroyImagesFromPath($image->path, $image->type);
+ $disk = $this->storage->getDisk($image->type);
+ $disk->destroyAllMatchingNameFromPath($image->path);
$image->delete();
}
- /**
- * Destroys an image at the given path.
- * Searches for image thumbnails in addition to main provided path.
- */
- protected function destroyImagesFromPath(string $path, string $imageType): bool
- {
- $path = $this->storage->adjustPathForDisk($path, $imageType);
- $disk = $this->storage->getDisk($imageType);
-
- $imageFolder = dirname($path);
- $imageFileName = basename($path);
- $allImages = collect($disk->allFiles($imageFolder));
-
- // Delete image files
- $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
- return basename($imagePath) === $imageFileName;
- });
- $disk->delete($imagesToDelete->all());
-
- // Cleanup of empty folders
- $foldersInvolved = array_merge([$imageFolder], $disk->directories($imageFolder));
- foreach ($foldersInvolved as $directory) {
- if ($this->isFolderEmpty($disk, $directory)) {
- $disk->deleteDirectory($directory);
- }
- }
-
- return true;
- }
-
- /**
- * Check whether a folder is empty.
- */
- protected function isFolderEmpty(StorageDisk $storage, string $path): bool
- {
- $files = $storage->files($path);
- $folders = $storage->directories($path);
-
- return count($files) === 0 && count($folders) === 0;
- }
-
/**
* Delete gallery and drawings that are not within HTML content of pages or page revisions.
* Checks based off of only the image name.
*
* Returns the path of the images that would be/have been deleted.
*/
- public function deleteUnusedImages(bool $checkRevisions = true, bool $dryRun = true)
+ public function deleteUnusedImages(bool $checkRevisions = true, bool $dryRun = true): array
{
$types = ['gallery', 'drawio'];
$deletedPaths = [];
* Attempts to convert the URL to a system storage url then
* fetch the data from the disk or storage location.
* Returns null if the image data cannot be fetched from storage.
- *
- * @throws FileNotFoundException
*/
public function imageUrlToBase64(string $url): ?string
{
return null;
}
- $storagePath = $this->storage->adjustPathForDisk($storagePath);
-
// Apply access control when local_secure_restricted images are active
if ($this->storage->usingSecureRestrictedImages()) {
if (!$this->checkUserHasAccessToRelationOfImageAtPath($storagePath)) {
}
// Check local_secure is active
- return $this->storage->usingSecureImages()
- && $disk instanceof FilesystemAdapter
+ return $disk->usingSecureImages()
// Check the image file exists
&& $disk->exists($imagePath)
// Check the file is likely an image file
*/
protected function checkUserHasAccessToRelationOfImageAtPath(string $path): bool
{
- if (str_starts_with($path, '/uploads/images/')) {
+ if (str_starts_with($path, 'uploads/images/')) {
$path = substr($path, 15);
}
}
if ($imageType === 'gallery' || $imageType === 'drawio') {
- return Page::visible()->where('id', '=', $image->uploaded_to)->exists();
+ return $this->queries->pages->visibleForList()->where('id', '=', $image->uploaded_to)->exists();
}
if ($imageType === 'cover_book') {
- return Book::visible()->where('id', '=', $image->uploaded_to)->exists();
+ return $this->queries->books->visibleForList()->where('id', '=', $image->uploaded_to)->exists();
}
if ($imageType === 'cover_bookshelf') {
- return Bookshelf::visible()->where('id', '=', $image->uploaded_to)->exists();
+ return $this->queries->shelves->visibleForList()->where('id', '=', $image->uploaded_to)->exists();
}
return false;