From db3acabc662e976f253db47f82162c9f00667446 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 9 Dec 2015 19:50:17 +0000 Subject: [PATCH] Added an image service and facade, Cleaned Image Model --- app/Entity.php | 18 +-- app/Image.php | 20 +-- app/Ownable.php | 23 +++ app/Providers/CustomFacadeProvider.php | 8 ++ app/Repos/ImageRepo.php | 152 ++------------------ app/Services/Facades/Images.php | 14 ++ app/Services/ImageService.php | 189 +++++++++++++++++++++++++ config/app.php | 27 ++-- 8 files changed, 268 insertions(+), 183 deletions(-) create mode 100644 app/Ownable.php create mode 100644 app/Services/Facades/Images.php create mode 100644 app/Services/ImageService.php diff --git a/app/Entity.php b/app/Entity.php index 26878042e..5ccc016a3 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -7,23 +7,7 @@ use Illuminate\Database\Eloquent\Model; abstract class Entity extends Model { - /** - * Relation for the user that created this entity. - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function createdBy() - { - return $this->belongsTo('BookStack\User', 'created_by'); - } - - /** - * Relation for the user that updated this entity. - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function updatedBy() - { - return $this->belongsTo('BookStack\User', 'updated_by'); - } + use Ownable; /** * Compares this entity to another given entity. diff --git a/app/Image.php b/app/Image.php index 7c77440f9..66d54ba30 100644 --- a/app/Image.php +++ b/app/Image.php @@ -3,22 +3,22 @@ namespace BookStack; -class Image extends Entity +use Images; + +class Image { + use Ownable; protected $fillable = ['name']; - public function getFilePath() - { - return storage_path() . $this->url; - } - /** - * Get the url for this item. - * @return string + * Get a thumbnail for this image. + * @param int $width + * @param int $height + * @param bool|false $hardCrop */ - public function getUrl() + public function getThumb($width, $height, $hardCrop = false) { - return public_path() . $this->url; + Images::getThumbnail($this, $width, $height, $hardCrop); } } diff --git a/app/Ownable.php b/app/Ownable.php new file mode 100644 index 000000000..d6505b746 --- /dev/null +++ b/app/Ownable.php @@ -0,0 +1,23 @@ +belongsTo('BookStack\User', 'created_by'); + } + + /** + * Relation for the user that updated this entity. + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function updatedBy() + { + return $this->belongsTo('BookStack\User', 'updated_by'); + } +} \ No newline at end of file diff --git a/app/Providers/CustomFacadeProvider.php b/app/Providers/CustomFacadeProvider.php index bd4b2b515..1df14a076 100644 --- a/app/Providers/CustomFacadeProvider.php +++ b/app/Providers/CustomFacadeProvider.php @@ -2,6 +2,7 @@ namespace BookStack\Providers; +use BookStack\Services\ImageService; use BookStack\Services\ViewService; use Illuminate\Support\ServiceProvider; use BookStack\Services\ActivityService; @@ -40,5 +41,12 @@ class CustomFacadeProvider extends ServiceProvider $this->app->make('Illuminate\Contracts\Cache\Repository') ); }); + $this->app->bind('images', function() { + return new ImageService( + $this->app->make('Intervention\Image\ImageManager'), + $this->app->make('Illuminate\Contracts\Filesystem\Factory'), + $this->app->make('Illuminate\Contracts\Cache\Repository') + ); + }); } } diff --git a/app/Repos/ImageRepo.php b/app/Repos/ImageRepo.php index 0da243f7c..56b0ba98d 100644 --- a/app/Repos/ImageRepo.php +++ b/app/Repos/ImageRepo.php @@ -2,10 +2,7 @@ use BookStack\Image; -use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; -use Intervention\Image\ImageManager as ImageTool; -use Illuminate\Contracts\Filesystem\Factory as FileSystem; -use Illuminate\Contracts\Cache\Repository as Cache; +use BookStack\Services\ImageService; use Setting; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -13,30 +10,17 @@ class ImageRepo { protected $image; - protected $imageTool; - protected $fileSystem; - protected $cache; - - /** - * @var FileSystemInstance - */ - protected $storageInstance; - protected $storageUrl; - + protected $imageService; /** * ImageRepo constructor. - * @param Image $image - * @param ImageTool $imageTool - * @param FileSystem $fileSystem - * @param Cache $cache + * @param Image $image + * @param ImageService $imageService */ - public function __construct(Image $image, ImageTool $imageTool, FileSystem $fileSystem, Cache $cache) + public function __construct(Image $image,ImageService $imageService) { $this->image = $image; - $this->imageTool = $imageTool; - $this->fileSystem = $fileSystem; - $this->cache = $cache; + $this->imageService = $imageService; } @@ -83,30 +67,7 @@ class ImageRepo */ public function saveNew(UploadedFile $uploadFile, $type) { - $storage = $this->getStorage(); - $secureUploads = Setting::get('app-secure-images'); - $imageName = str_replace(' ', '-', $uploadFile->getClientOriginalName()); - - if ($secureUploads) $imageName = str_random(16) . '-' . $imageName; - - $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/'; - while ($storage->exists($imagePath . $imageName)) { - $imageName = str_random(3) . $imageName; - } - $fullPath = $imagePath . $imageName; - - $storage->put($fullPath, file_get_contents($uploadFile->getRealPath())); - - $userId = auth()->user()->id; - $image = $this->image->forceCreate([ - 'name' => $imageName, - 'path' => $fullPath, - 'url' => $this->getPublicUrl($fullPath), - 'type' => $type, - 'created_by' => $userId, - 'updated_by' => $userId - ]); - + $image = $this->imageService->saveNew($this->image, $uploadFile, $type); $this->loadThumbs($image); return $image; } @@ -133,40 +94,10 @@ class ImageRepo */ public function destroyImage(Image $image) { - $storage = $this->getStorage(); - - $imageFolder = dirname($image->path); - $imageFileName = basename($image->path); - $allImages = collect($storage->allFiles($imageFolder)); - - $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) { - $expectedIndex = strlen($imagePath) - strlen($imageFileName); - return strpos($imagePath, $imageFileName) === $expectedIndex; - }); - - $storage->delete($imagesToDelete->all()); - - // Cleanup of empty folders - foreach ($storage->directories($imageFolder) as $directory) { - if ($this->isFolderEmpty($directory)) $storage->deleteDirectory($directory); - } - if ($this->isFolderEmpty($imageFolder)) $storage->deleteDirectory($imageFolder); - - $image->delete(); + $this->imageService->destroyImage($image); return true; } - /** - * Check whether or not a folder is empty. - * @param $path - * @return int - */ - private function isFolderEmpty($path) - { - $files = $this->getStorage()->files($path); - $folders = $this->getStorage()->directories($path); - return count($files) === 0 && count($folders) === 0; - } /** * Load thumbnails onto an image object. @@ -193,72 +124,7 @@ class ImageRepo */ public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) { - $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/'; - $thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path); - - if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) { - return $this->getPublicUrl($thumbFilePath); - } - - $storage = $this->getStorage(); - - if ($storage->exists($thumbFilePath)) { - return $this->getPublicUrl($thumbFilePath); - } - - // Otherwise create the thumbnail - $thumb = $this->imageTool->make($storage->get($image->path)); - if ($keepRatio) { - $thumb->resize($width, null, function ($constraint) { - $constraint->aspectRatio(); - $constraint->upsize(); - }); - } else { - $thumb->fit($width, $height); - } - - $thumbData = (string)$thumb->encode(); - $storage->put($thumbFilePath, $thumbData); - $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72); - - return $this->getPublicUrl($thumbFilePath); - } - - /** - * Gets a public facing url for an image by checking relevant environment variables. - * @param $filePath - * @return string - */ - private function getPublicUrl($filePath) - { - if ($this->storageUrl === null) { - $storageUrl = env('STORAGE_URL'); - - // Get the standard public s3 url if s3 is set as storage type - if ($storageUrl == false && env('STORAGE_TYPE') === 's3') { - $storageDetails = config('filesystems.disks.s3'); - $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket']; - } - - $this->storageUrl = $storageUrl; - } - - return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath; - } - - - /** - * Get the storage that will be used for storing images. - * @return FileSystemInstance - */ - private function getStorage() - { - if ($this->storageInstance !== null) return $this->storageInstance; - - $storageType = env('STORAGE_TYPE'); - $this->storageInstance = $this->fileSystem->disk($storageType); - - return $this->storageInstance; + return $this->imageService->getThumbnail($image, $width, $height, $keepRatio); } diff --git a/app/Services/Facades/Images.php b/app/Services/Facades/Images.php new file mode 100644 index 000000000..219f069a0 --- /dev/null +++ b/app/Services/Facades/Images.php @@ -0,0 +1,14 @@ +imageTool = $imageTool; + $this->fileSystem = $fileSystem; + $this->cache = $cache; + } + + public function saveNew(Image $image, UploadedFile $uploadedFile, $type) + { + $storage = $this->getStorage(); + $secureUploads = Setting::get('app-secure-images'); + $imageName = str_replace(' ', '-', $uploadedFile->getClientOriginalName()); + + if ($secureUploads) $imageName = str_random(16) . '-' . $imageName; + + $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/'; + while ($storage->exists($imagePath . $imageName)) { + $imageName = str_random(3) . $imageName; + } + $fullPath = $imagePath . $imageName; + + $storage->put($fullPath, file_get_contents($uploadedFile->getRealPath())); + + $userId = auth()->user()->id; + $image = $image->forceCreate([ + 'name' => $imageName, + 'path' => $fullPath, + 'url' => $this->getPublicUrl($fullPath), + 'type' => $type, + 'created_by' => $userId, + 'updated_by' => $userId + ]); + + return $image; + } + + /** + * 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. + * + * @param Image $image + * @param int $width + * @param int $height + * @param bool $keepRatio + * @return string + */ + public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) + { + $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/'; + $thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path); + + if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) { + return $this->getPublicUrl($thumbFilePath); + } + + $storage = $this->getStorage(); + + if ($storage->exists($thumbFilePath)) { + return $this->getPublicUrl($thumbFilePath); + } + + // Otherwise create the thumbnail + $thumb = $this->imageTool->make($storage->get($image->path)); + if ($keepRatio) { + $thumb->resize($width, null, function ($constraint) { + $constraint->aspectRatio(); + $constraint->upsize(); + }); + } else { + $thumb->fit($width, $height); + } + + $thumbData = (string)$thumb->encode(); + $storage->put($thumbFilePath, $thumbData); + $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72); + + return $this->getPublicUrl($thumbFilePath); + } + + /** + * Destroys an Image object along with its files and thumbnails. + * @param Image $image + * @return bool + */ + public function destroyImage(Image $image) + { + $storage = $this->getStorage(); + + $imageFolder = dirname($image->path); + $imageFileName = basename($image->path); + $allImages = collect($storage->allFiles($imageFolder)); + + $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) { + $expectedIndex = strlen($imagePath) - strlen($imageFileName); + return strpos($imagePath, $imageFileName) === $expectedIndex; + }); + + $storage->delete($imagesToDelete->all()); + + // Cleanup of empty folders + foreach ($storage->directories($imageFolder) as $directory) { + if ($this->isFolderEmpty($directory)) $storage->deleteDirectory($directory); + } + if ($this->isFolderEmpty($imageFolder)) $storage->deleteDirectory($imageFolder); + + $image->delete(); + return true; + } + + /** + * Get the storage that will be used for storing images. + * @return FileSystemInstance + */ + private function getStorage() + { + if ($this->storageInstance !== null) return $this->storageInstance; + + $storageType = env('STORAGE_TYPE'); + $this->storageInstance = $this->fileSystem->disk($storageType); + + return $this->storageInstance; + } + + /** + * Check whether or not a folder is empty. + * @param $path + * @return int + */ + private function isFolderEmpty($path) + { + $files = $this->getStorage()->files($path); + $folders = $this->getStorage()->directories($path); + return count($files) === 0 && count($folders) === 0; + } + + /** + * Gets a public facing url for an image by checking relevant environment variables. + * @param $filePath + * @return string + */ + private function getPublicUrl($filePath) + { + if ($this->storageUrl === null) { + $storageUrl = env('STORAGE_URL'); + + // Get the standard public s3 url if s3 is set as storage type + if ($storageUrl == false && env('STORAGE_TYPE') === 's3') { + $storageDetails = config('filesystems.disks.s3'); + $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket']; + } + + $this->storageUrl = $storageUrl; + } + + return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath; + } + + +} \ No newline at end of file diff --git a/config/app.php b/config/app.php index aa7c0b561..b9c207632 100644 --- a/config/app.php +++ b/config/app.php @@ -13,7 +13,7 @@ return [ | */ - 'debug' => env('APP_DEBUG', false), + 'debug' => env('APP_DEBUG', false), /* |-------------------------------------------------------------------------- @@ -26,7 +26,7 @@ return [ | */ - 'url' => env('APP_URL', 'http://localhost'), + 'url' => env('APP_URL', 'http://localhost'), /* |-------------------------------------------------------------------------- @@ -39,7 +39,7 @@ return [ | */ - 'timezone' => 'UTC', + 'timezone' => 'UTC', /* |-------------------------------------------------------------------------- @@ -52,7 +52,7 @@ return [ | */ - 'locale' => 'en', + 'locale' => 'en', /* |-------------------------------------------------------------------------- @@ -78,9 +78,9 @@ return [ | */ - 'key' => env('APP_KEY', 'AbAZchsay4uBTU33RubBzLKw203yqSqr'), + 'key' => env('APP_KEY', 'AbAZchsay4uBTU33RubBzLKw203yqSqr'), - 'cipher' => 'AES-256-CBC', + 'cipher' => 'AES-256-CBC', /* |-------------------------------------------------------------------------- @@ -95,7 +95,7 @@ return [ | */ - 'log' => 'single', + 'log' => 'single', /* |-------------------------------------------------------------------------- @@ -108,7 +108,7 @@ return [ | */ - 'providers' => [ + 'providers' => [ /* * Laravel Framework Service Providers... @@ -167,7 +167,7 @@ return [ | */ - 'aliases' => [ + 'aliases' => [ 'App' => Illuminate\Support\Facades\App::class, 'Artisan' => Illuminate\Support\Facades\Artisan::class, @@ -208,15 +208,16 @@ return [ */ 'ImageTool' => Intervention\Image\Facades\Image::class, - 'Debugbar' => Barryvdh\Debugbar\Facade::class, + 'Debugbar' => Barryvdh\Debugbar\Facade::class, /** * Custom */ - 'Activity' => BookStack\Services\Facades\Activity::class, - 'Setting' => BookStack\Services\Facades\Setting::class, - 'Views' => BookStack\Services\Facades\Views::class, + 'Activity' => BookStack\Services\Facades\Activity::class, + 'Setting' => BookStack\Services\Facades\Setting::class, + 'Views' => BookStack\Services\Facades\Views::class, + 'Images' => \BookStack\Services\Facades\Images::class, ],