2023-09-30 14:12:22 -04:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace BookStack\Uploads;
|
|
|
|
|
|
|
|
use Illuminate\Contracts\Filesystem\Filesystem;
|
|
|
|
use Illuminate\Filesystem\FilesystemAdapter;
|
|
|
|
use League\Flysystem\WhitespacePathNormalizer;
|
|
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
|
|
|
|
class ImageStorageDisk
|
|
|
|
{
|
|
|
|
public function __construct(
|
|
|
|
protected string $diskName,
|
|
|
|
protected Filesystem $filesystem,
|
|
|
|
) {
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if local secure image storage (Fetched behind authentication)
|
|
|
|
* is currently active in the instance.
|
|
|
|
*/
|
|
|
|
public function usingSecureImages(): bool
|
|
|
|
{
|
|
|
|
return $this->diskName === 'local_secure_images';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Change the originally provided path to fit any disk-specific requirements.
|
|
|
|
* This also ensures the path is kept to the expected root folders.
|
|
|
|
*/
|
|
|
|
protected function adjustPathForDisk(string $path): string
|
|
|
|
{
|
|
|
|
$path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/images/', '', $path));
|
|
|
|
|
|
|
|
if ($this->usingSecureImages()) {
|
|
|
|
return $path;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 'uploads/images/' . $path;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a file at the given path exists.
|
|
|
|
*/
|
|
|
|
public function exists(string $path): bool
|
|
|
|
{
|
|
|
|
return $this->filesystem->exists($this->adjustPathForDisk($path));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the file at the given path.
|
|
|
|
*/
|
2023-09-30 15:00:48 -04:00
|
|
|
public function get(string $path): ?string
|
2023-09-30 14:12:22 -04:00
|
|
|
{
|
|
|
|
return $this->filesystem->get($this->adjustPathForDisk($path));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Save the given image data at the given path. Can choose to set
|
|
|
|
* the image as public which will update its visibility after saving.
|
|
|
|
*/
|
|
|
|
public function put(string $path, string $data, bool $makePublic = false): void
|
|
|
|
{
|
|
|
|
$path = $this->adjustPathForDisk($path);
|
|
|
|
$this->filesystem->put($path, $data);
|
|
|
|
|
|
|
|
// Set visibility when a non-AWS-s3, s3-like storage option is in use.
|
|
|
|
// Done since this call can break s3-like services but desired for other image stores.
|
|
|
|
// Attempting to set ACL during above put request requires different permissions
|
|
|
|
// hence would technically be a breaking change for actual s3 usage.
|
|
|
|
if ($makePublic && !$this->isS3Like()) {
|
|
|
|
$this->filesystem->setVisibility($path, 'public');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Destroys an image at the given path.
|
|
|
|
* Searches for image thumbnails in addition to main provided path.
|
|
|
|
*/
|
|
|
|
public function destroyAllMatchingNameFromPath(string $path): void
|
|
|
|
{
|
|
|
|
$path = $this->adjustPathForDisk($path);
|
|
|
|
|
|
|
|
$imageFolder = dirname($path);
|
|
|
|
$imageFileName = basename($path);
|
|
|
|
$allImages = collect($this->filesystem->allFiles($imageFolder));
|
|
|
|
|
|
|
|
// Delete image files
|
|
|
|
$imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
|
|
|
|
return basename($imagePath) === $imageFileName;
|
|
|
|
});
|
|
|
|
$this->filesystem->delete($imagesToDelete->all());
|
|
|
|
|
|
|
|
// Cleanup of empty folders
|
|
|
|
$foldersInvolved = array_merge([$imageFolder], $this->filesystem->directories($imageFolder));
|
|
|
|
foreach ($foldersInvolved as $directory) {
|
|
|
|
if ($this->isFolderEmpty($directory)) {
|
|
|
|
$this->filesystem->deleteDirectory($directory);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the mime type of the file at the given path.
|
|
|
|
* Only works for local filesystem adapters.
|
|
|
|
*/
|
|
|
|
public function mimeType(string $path): string
|
|
|
|
{
|
2023-09-30 15:00:48 -04:00
|
|
|
$path = $this->adjustPathForDisk($path);
|
2023-09-30 14:12:22 -04:00
|
|
|
return $this->filesystem instanceof FilesystemAdapter ? $this->filesystem->mimeType($path) : '';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a stream response for the image at the given path.
|
|
|
|
*/
|
|
|
|
public function response(string $path): StreamedResponse
|
|
|
|
{
|
2023-09-30 15:00:48 -04:00
|
|
|
return $this->filesystem->response($this->adjustPathForDisk($path));
|
2023-09-30 14:12:22 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the image storage in use is an S3-like (but not likely S3) external system.
|
|
|
|
*/
|
|
|
|
protected function isS3Like(): bool
|
|
|
|
{
|
|
|
|
$usingS3 = $this->diskName === 's3';
|
|
|
|
return $usingS3 && !is_null(config('filesystems.disks.s3.endpoint'));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check whether a folder is empty.
|
|
|
|
*/
|
|
|
|
protected function isFolderEmpty(string $path): bool
|
|
|
|
{
|
|
|
|
$files = $this->filesystem->files($path);
|
|
|
|
$folders = $this->filesystem->directories($path);
|
|
|
|
|
|
|
|
return count($files) === 0 && count($folders) === 0;
|
|
|
|
}
|
|
|
|
}
|