]> BookStack Code Mirror - bookstack/blob - app/Repos/ImageRepo.php
Revamped image system to use driver-agnotstic storage and be more efficent
[bookstack] / app / Repos / ImageRepo.php
1 <?php namespace BookStack\Repos;
2
3
4 use BookStack\Image;
5 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
6 use Intervention\Image\ImageManager as ImageTool;
7 use Illuminate\Contracts\Filesystem\Factory as FileSystem;
8 use Illuminate\Contracts\Cache\Repository as Cache;
9 use Setting;
10 use Symfony\Component\HttpFoundation\File\UploadedFile;
11
12 class ImageRepo
13 {
14
15     protected $image;
16     protected $imageTool;
17     protected $fileSystem;
18     protected $cache;
19
20     /**
21      * @var FileSystemInstance
22      */
23     protected $storageInstance;
24     protected $storageUrl;
25
26
27     /**
28      * ImageRepo constructor.
29      * @param Image      $image
30      * @param ImageTool  $imageTool
31      * @param FileSystem $fileSystem
32      * @param Cache      $cache
33      */
34     public function __construct(Image $image, ImageTool $imageTool, FileSystem $fileSystem, Cache $cache)
35     {
36         $this->image = $image;
37         $this->imageTool = $imageTool;
38         $this->fileSystem = $fileSystem;
39         $this->cache = $cache;
40     }
41
42
43     /**
44      * Get an image with the given id.
45      * @param $id
46      * @return mixed
47      */
48     public function getById($id)
49     {
50         return $this->image->findOrFail($id);
51     }
52
53
54     /**
55      * Get all images for the standard gallery view that's used for
56      * adding images to shared content such as pages.
57      * @param int $page
58      * @param int $pageSize
59      * @return array
60      */
61     public function getAllGallery($page = 0, $pageSize = 24)
62     {
63         $images = $this->image->where('type', '=', 'gallery')
64             ->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
65         $hasMore = count($images) > $pageSize;
66
67         $returnImages = $images->take(24);
68         $returnImages->each(function ($image) {
69             $this->loadThumbs($image);
70         });
71
72         return [
73             'images' => $returnImages,
74             'hasMore' => $hasMore
75         ];
76     }
77
78     /**
79      * Save a new image into storage and return the new image.
80      * @param UploadedFile $uploadFile
81      * @param  string      $type
82      * @return Image
83      */
84     public function saveNew(UploadedFile $uploadFile, $type)
85     {
86         $storage = $this->getStorage();
87         $secureUploads = Setting::get('app-secure-images');
88         $imageName = str_replace(' ', '-', $uploadFile->getClientOriginalName());
89
90         if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
91
92         $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
93         while ($storage->exists($imagePath . $imageName)) {
94             $imageName = str_random(3) . $imageName;
95         }
96         $fullPath = $imagePath . $imageName;
97
98         $storage->put($fullPath, file_get_contents($uploadFile->getRealPath()));
99
100         $userId = auth()->user()->id;
101         $image = $this->image->forceCreate([
102             'name' => $imageName,
103             'path' => $fullPath,
104             'url' => $this->getPublicUrl($fullPath),
105             'type' => $type,
106             'created_by' => $userId,
107             'updated_by' => $userId
108         ]);
109
110         $this->loadThumbs($image);
111         return $image;
112     }
113
114     /**
115      * Update the details of an image via an array of properties.
116      * @param Image $image
117      * @param array $updateDetails
118      * @return Image
119      */
120     public function updateImageDetails(Image $image, $updateDetails)
121     {
122         $image->fill($updateDetails);
123         $image->save();
124         $this->loadThumbs($image);
125         return $image;
126     }
127
128
129     /**
130      * Destroys an Image object along with its files and thumbnails.
131      * @param Image $image
132      * @return bool
133      */
134     public function destroyImage(Image $image)
135     {
136         $storage = $this->getStorage();
137
138         $imageFolder = dirname($image->path);
139         $imageFileName = basename($image->path);
140         $allImages = collect($storage->allFiles($imageFolder));
141
142         $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
143             $expectedIndex = strlen($imagePath) - strlen($imageFileName);
144             return strpos($imagePath, $imageFileName) === $expectedIndex;
145         });
146
147         $storage->delete($imagesToDelete->all());
148
149         // Cleanup of empty folders
150         foreach ($storage->directories($imageFolder) as $directory) {
151             if ($this->isFolderEmpty($directory)) $storage->deleteDirectory($directory);
152         }
153         if ($this->isFolderEmpty($imageFolder)) $storage->deleteDirectory($imageFolder);
154
155         $image->delete();
156         return true;
157     }
158
159     /**
160      * Check whether or not a folder is empty.
161      * @param $path
162      * @return int
163      */
164     private function isFolderEmpty($path)
165     {
166         $files = $this->getStorage()->files($path);
167         $folders = $this->getStorage()->directories($path);
168         return count($files) === 0 && count($folders) === 0;
169     }
170
171     /**
172      * Load thumbnails onto an image object.
173      * @param Image $image
174      */
175     private function loadThumbs(Image $image)
176     {
177         $image->thumbs = [
178             'gallery' => $this->getThumbnail($image, 150, 150),
179             'display' => $this->getThumbnail($image, 840, 0, true)
180         ];
181     }
182
183     /**
184      * Get the thumbnail for an image.
185      * If $keepRatio is true only the width will be used.
186      * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
187      *
188      * @param Image $image
189      * @param int   $width
190      * @param int   $height
191      * @param bool  $keepRatio
192      * @return string
193      */
194     private function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
195     {
196         $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
197         $thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path);
198
199         if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
200             return $this->getPublicUrl($thumbFilePath);
201         }
202
203         $storage = $this->getStorage();
204
205         if ($storage->exists($thumbFilePath)) {
206             return $this->getPublicUrl($thumbFilePath);
207         }
208
209         // Otherwise create the thumbnail
210         $thumb = $this->imageTool->make($storage->get($image->path));
211         if ($keepRatio) {
212             $thumb->resize($width, null, function ($constraint) {
213                 $constraint->aspectRatio();
214                 $constraint->upsize();
215             });
216         } else {
217             $thumb->fit($width, $height);
218         }
219
220         $thumbData = (string)$thumb->encode();
221         $storage->put($thumbFilePath, $thumbData);
222         $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
223
224         return $this->getPublicUrl($thumbFilePath);
225     }
226
227     /**
228      * Gets a public facing url for an image by checking relevant environment variables.
229      * @param $filePath
230      * @return string
231      */
232     private function getPublicUrl($filePath)
233     {
234         if ($this->storageUrl === null) {
235             $storageUrl = env('STORAGE_URL');
236
237             // Get the standard public s3 url if s3 is set as storage type
238             if ($storageUrl == false && env('STORAGE_TYPE') === 's3') {
239                 $storageDetails = config('filesystems.disks.s3');
240                 $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'] . $filePath;
241             }
242
243             $this->storageUrl = $storageUrl;
244         }
245
246         return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath;
247     }
248
249
250     /**
251      * Get the storage that will be used for storing images.
252      * @return FileSystemInstance
253      */
254     private function getStorage()
255     {
256         if ($this->storageInstance !== null) return $this->storageInstance;
257
258         $storageType = env('STORAGE_TYPE');
259         $this->storageInstance = $this->fileSystem->disk($storageType);
260
261         return $this->storageInstance;
262     }
263
264
265 }