]> BookStack Code Mirror - bookstack/blob - app/Services/ImageService.php
Added an image service and facade, Cleaned Image Model
[bookstack] / app / Services / ImageService.php
1 <?php namespace BookStack\Services;
2
3 use BookStack\Image;
4 use Intervention\Image\ImageManager;
5 use Illuminate\Contracts\Filesystem\Factory as FileSystem;
6 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
7 use Illuminate\Contracts\Cache\Repository as Cache;
8 use Setting;
9 use Symfony\Component\HttpFoundation\File\UploadedFile;
10
11 class ImageService
12 {
13
14     protected $imageTool;
15     protected $fileSystem;
16     protected $cache;
17
18     /**
19      * @var FileSystemInstance
20      */
21     protected $storageInstance;
22     protected $storageUrl;
23
24     /**
25      * ImageService constructor.
26      * @param $imageTool
27      * @param $fileSystem
28      * @param $cache
29      */
30     public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
31     {
32         $this->imageTool = $imageTool;
33         $this->fileSystem = $fileSystem;
34         $this->cache = $cache;
35     }
36
37     public function saveNew(Image $image, UploadedFile $uploadedFile, $type)
38     {
39         $storage = $this->getStorage();
40         $secureUploads = Setting::get('app-secure-images');
41         $imageName = str_replace(' ', '-', $uploadedFile->getClientOriginalName());
42
43         if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
44
45         $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
46         while ($storage->exists($imagePath . $imageName)) {
47             $imageName = str_random(3) . $imageName;
48         }
49         $fullPath = $imagePath . $imageName;
50
51         $storage->put($fullPath, file_get_contents($uploadedFile->getRealPath()));
52
53         $userId = auth()->user()->id;
54         $image = $image->forceCreate([
55             'name' => $imageName,
56             'path' => $fullPath,
57             'url' => $this->getPublicUrl($fullPath),
58             'type' => $type,
59             'created_by' => $userId,
60             'updated_by' => $userId
61         ]);
62
63         return $image;
64     }
65
66     /**
67      * Get the thumbnail for an image.
68      * If $keepRatio is true only the width will be used.
69      * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
70      *
71      * @param Image $image
72      * @param int   $width
73      * @param int   $height
74      * @param bool  $keepRatio
75      * @return string
76      */
77     public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
78     {
79         $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
80         $thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path);
81
82         if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
83             return $this->getPublicUrl($thumbFilePath);
84         }
85
86         $storage = $this->getStorage();
87
88         if ($storage->exists($thumbFilePath)) {
89             return $this->getPublicUrl($thumbFilePath);
90         }
91
92         // Otherwise create the thumbnail
93         $thumb = $this->imageTool->make($storage->get($image->path));
94         if ($keepRatio) {
95             $thumb->resize($width, null, function ($constraint) {
96                 $constraint->aspectRatio();
97                 $constraint->upsize();
98             });
99         } else {
100             $thumb->fit($width, $height);
101         }
102
103         $thumbData = (string)$thumb->encode();
104         $storage->put($thumbFilePath, $thumbData);
105         $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
106
107         return $this->getPublicUrl($thumbFilePath);
108     }
109
110     /**
111      * Destroys an Image object along with its files and thumbnails.
112      * @param Image $image
113      * @return bool
114      */
115     public function destroyImage(Image $image)
116     {
117         $storage = $this->getStorage();
118
119         $imageFolder = dirname($image->path);
120         $imageFileName = basename($image->path);
121         $allImages = collect($storage->allFiles($imageFolder));
122
123         $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
124             $expectedIndex = strlen($imagePath) - strlen($imageFileName);
125             return strpos($imagePath, $imageFileName) === $expectedIndex;
126         });
127
128         $storage->delete($imagesToDelete->all());
129
130         // Cleanup of empty folders
131         foreach ($storage->directories($imageFolder) as $directory) {
132             if ($this->isFolderEmpty($directory)) $storage->deleteDirectory($directory);
133         }
134         if ($this->isFolderEmpty($imageFolder)) $storage->deleteDirectory($imageFolder);
135
136         $image->delete();
137         return true;
138     }
139
140     /**
141      * Get the storage that will be used for storing images.
142      * @return FileSystemInstance
143      */
144     private function getStorage()
145     {
146         if ($this->storageInstance !== null) return $this->storageInstance;
147
148         $storageType = env('STORAGE_TYPE');
149         $this->storageInstance = $this->fileSystem->disk($storageType);
150
151         return $this->storageInstance;
152     }
153
154     /**
155      * Check whether or not a folder is empty.
156      * @param $path
157      * @return int
158      */
159     private function isFolderEmpty($path)
160     {
161         $files = $this->getStorage()->files($path);
162         $folders = $this->getStorage()->directories($path);
163         return count($files) === 0 && count($folders) === 0;
164     }
165
166     /**
167      * Gets a public facing url for an image by checking relevant environment variables.
168      * @param $filePath
169      * @return string
170      */
171     private function getPublicUrl($filePath)
172     {
173         if ($this->storageUrl === null) {
174             $storageUrl = env('STORAGE_URL');
175
176             // Get the standard public s3 url if s3 is set as storage type
177             if ($storageUrl == false && env('STORAGE_TYPE') === 's3') {
178                 $storageDetails = config('filesystems.disks.s3');
179                 $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
180             }
181
182             $this->storageUrl = $storageUrl;
183         }
184
185         return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath;
186     }
187
188
189 }