Dan Brown

Revamped image system to use driver-agnotstic storage and be more efficent

...@@ -14,12 +14,23 @@ CACHE_DRIVER=file ...@@ -14,12 +14,23 @@ CACHE_DRIVER=file
14 SESSION_DRIVER=file 14 SESSION_DRIVER=file
15 QUEUE_DRIVER=sync 15 QUEUE_DRIVER=sync
16 16
17 +# Storage
18 +STORAGE_TYPE=local
19 +# Amazon S3 Config
20 +STORAGE_S3_KEY=false
21 +STORAGE_S3_SECRET=false
22 +STORAGE_S3_REGION=false
23 +STORAGE_S3_BUCKET=false
24 +# Storage URL
25 +# Used to prefix image urls for when using custom domains/cdns
26 +STORAGE_URL=false
27 +
17 # Social Authentication information. Defaults as off. 28 # Social Authentication information. Defaults as off.
18 GITHUB_APP_ID=false 29 GITHUB_APP_ID=false
19 GITHUB_APP_SECRET=false 30 GITHUB_APP_SECRET=false
20 GOOGLE_APP_ID=false 31 GOOGLE_APP_ID=false
21 GOOGLE_APP_SECRET=false 32 GOOGLE_APP_SECRET=false
22 -# URL for social login redirects, NO TRAILING SLASH 33 +# URL used for social login redirects, NO TRAILING SLASH
23 APP_URL=http://bookstack.dev 34 APP_URL=http://bookstack.dev
24 35
25 # Mail settings 36 # Mail settings
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 namespace BookStack\Http\Controllers; 3 namespace BookStack\Http\Controllers;
4 4
5 +use BookStack\Repos\ImageRepo;
5 use Illuminate\Filesystem\Filesystem as File; 6 use Illuminate\Filesystem\Filesystem as File;
6 use Illuminate\Http\Request; 7 use Illuminate\Http\Request;
7 use Illuminate\Support\Facades\Auth; 8 use Illuminate\Support\Facades\Auth;
...@@ -14,16 +15,19 @@ class ImageController extends Controller ...@@ -14,16 +15,19 @@ class ImageController extends Controller
14 { 15 {
15 protected $image; 16 protected $image;
16 protected $file; 17 protected $file;
18 + protected $imageRepo;
17 19
18 /** 20 /**
19 * ImageController constructor. 21 * ImageController constructor.
20 * @param Image $image 22 * @param Image $image
21 * @param File $file 23 * @param File $file
24 + * @param ImageRepo $imageRepo
22 */ 25 */
23 - public function __construct(Image $image, File $file) 26 + public function __construct(Image $image, File $file, ImageRepo $imageRepo)
24 { 27 {
25 $this->image = $image; 28 $this->image = $image;
26 $this->file = $file; 29 $this->file = $file;
30 + $this->imageRepo = $imageRepo;
27 parent::__construct(); 31 parent::__construct();
28 } 32 }
29 33
...@@ -33,108 +37,31 @@ class ImageController extends Controller ...@@ -33,108 +37,31 @@ class ImageController extends Controller
33 * @param int $page 37 * @param int $page
34 * @return \Illuminate\Http\JsonResponse 38 * @return \Illuminate\Http\JsonResponse
35 */ 39 */
36 - public function getAll($page = 0) 40 + public function getAllGallery($page = 0)
37 { 41 {
38 - $pageSize = 30; 42 + $imgData = $this->imageRepo->getAllGallery($page);
39 - $images = $this->image->orderBy('created_at', 'desc') 43 + return response()->json($imgData);
40 - ->skip($page * $pageSize)->take($pageSize)->get();
41 - foreach ($images as $image) {
42 - $this->loadSizes($image);
43 - }
44 - $hasMore = $this->image->orderBy('created_at', 'desc')
45 - ->skip(($page + 1) * $pageSize)->take($pageSize)->count() > 0;
46 - return response()->json([
47 - 'images' => $images,
48 - 'hasMore' => $hasMore
49 - ]);
50 - }
51 -
52 - /**
53 - * Loads the standard thumbnail sizes for an image.
54 - * @param Image $image
55 - */
56 - private function loadSizes(Image $image)
57 - {
58 - $image->thumbnail = $this->getThumbnail($image, 150, 150);
59 - $image->display = $this->getThumbnail($image, 840, 0, true);
60 - }
61 -
62 - /**
63 - * Get the thumbnail for an image.
64 - * If $keepRatio is true only the width will be used.
65 - * @param $image
66 - * @param int $width
67 - * @param int $height
68 - * @param bool $keepRatio
69 - * @return string
70 - */
71 - public function getThumbnail($image, $width = 220, $height = 220, $keepRatio = false)
72 - {
73 - $explodedPath = explode('/', $image->url);
74 - $dirPrefix = $keepRatio ? 'scaled-' : 'thumbs-';
75 - array_splice($explodedPath, 4, 0, [$dirPrefix . $width . '-' . $height]);
76 - $thumbPath = implode('/', $explodedPath);
77 - $thumbFilePath = public_path() . $thumbPath;
78 -
79 - // Return the thumbnail url path if already exists
80 - if (file_exists($thumbFilePath)) {
81 - return $thumbPath;
82 - }
83 -
84 - // Otherwise create the thumbnail
85 - $thumb = ImageTool::make(public_path() . $image->url);
86 - if($keepRatio) {
87 - $thumb->resize($width, null, function ($constraint) {
88 - $constraint->aspectRatio();
89 - $constraint->upsize();
90 - });
91 - } else {
92 - $thumb->fit($width, $height);
93 - }
94 -
95 - // Create thumbnail folder if it does not exist
96 - if (!file_exists(dirname($thumbFilePath))) {
97 - mkdir(dirname($thumbFilePath), 0775, true);
98 } 44 }
99 45
100 - //Save Thumbnail
101 - $thumb->save($thumbFilePath);
102 - return $thumbPath;
103 - }
104 46
105 /** 47 /**
106 * Handles image uploads for use on pages. 48 * Handles image uploads for use on pages.
107 * @param Request $request 49 * @param Request $request
108 * @return \Illuminate\Http\JsonResponse 50 * @return \Illuminate\Http\JsonResponse
109 */ 51 */
110 - public function upload(Request $request) 52 + public function uploadGallery(Request $request)
111 { 53 {
112 $this->checkPermission('image-create'); 54 $this->checkPermission('image-create');
113 $this->validate($request, [ 55 $this->validate($request, [
114 'file' => 'image|mimes:jpeg,gif,png' 56 'file' => 'image|mimes:jpeg,gif,png'
115 ]); 57 ]);
116 - $imageUpload = $request->file('file');
117 58
118 - $name = str_replace(' ', '-', $imageUpload->getClientOriginalName()); 59 + $imageUpload = $request->file('file');
119 - $storageName = substr(sha1(time()), 0, 10) . '-' . $name; 60 + $image = $this->imageRepo->saveNew($imageUpload, 'gallery');
120 - $imagePath = '/uploads/images/' . Date('Y-m-M') . '/'; 61 + return response()->json($image);
121 - $storagePath = public_path() . $imagePath;
122 - $fullPath = $storagePath . $storageName;
123 - while (file_exists($fullPath)) {
124 - $storageName = substr(sha1(rand()), 0, 3) . $storageName;
125 - $fullPath = $storagePath . $storageName;
126 - }
127 - $imageUpload->move($storagePath, $storageName);
128 - // Create and save image object
129 - $this->image->name = $name;
130 - $this->image->url = $imagePath . $storageName;
131 - $this->image->created_by = auth()->user()->id;
132 - $this->image->updated_by = auth()->user()->id;
133 - $this->image->save();
134 - $this->loadSizes($this->image);
135 - return response()->json($this->image);
136 } 62 }
137 63
64 +
138 /** 65 /**
139 * Update image details 66 * Update image details
140 * @param $imageId 67 * @param $imageId
...@@ -147,13 +74,12 @@ class ImageController extends Controller ...@@ -147,13 +74,12 @@ class ImageController extends Controller
147 $this->validate($request, [ 74 $this->validate($request, [
148 'name' => 'required|min:2|string' 75 'name' => 'required|min:2|string'
149 ]); 76 ]);
150 - $image = $this->image->findOrFail($imageId); 77 + $image = $this->imageRepo->getById($imageId);
151 - $image->fill($request->all()); 78 + $image = $this->imageRepo->updateImageDetails($image, $request->all());
152 - $image->save(); 79 + return response()->json($image);
153 - $this->loadSizes($image);
154 - return response()->json($this->image);
155 } 80 }
156 81
82 +
157 /** 83 /**
158 * Deletes an image and all thumbnail/image files 84 * Deletes an image and all thumbnail/image files
159 * @param PageRepo $pageRepo 85 * @param PageRepo $pageRepo
...@@ -164,41 +90,18 @@ class ImageController extends Controller ...@@ -164,41 +90,18 @@ class ImageController extends Controller
164 public function destroy(PageRepo $pageRepo, Request $request, $id) 90 public function destroy(PageRepo $pageRepo, Request $request, $id)
165 { 91 {
166 $this->checkPermission('image-delete'); 92 $this->checkPermission('image-delete');
167 - $image = $this->image->findOrFail($id); 93 + $image = $this->imageRepo->getById($id);
168 94
169 // Check if this image is used on any pages 95 // Check if this image is used on any pages
170 - $pageSearch = $pageRepo->searchForImage($image->url);
171 $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true); 96 $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
172 - if ($pageSearch !== false && !$isForced) { 97 + if (!$isForced) {
98 + $pageSearch = $pageRepo->searchForImage($image->url);
99 + if ($pageSearch !== false) {
173 return response()->json($pageSearch, 400); 100 return response()->json($pageSearch, 400);
174 } 101 }
175 -
176 - // Delete files
177 - $folder = public_path() . dirname($image->url);
178 - $fileName = basename($image->url);
179 -
180 - // Delete thumbnails
181 - foreach (glob($folder . '/*') as $file) {
182 - if (is_dir($file)) {
183 - $thumbName = $file . '/' . $fileName;
184 - if (file_exists($file)) {
185 - unlink($thumbName);
186 - }
187 - // Remove thumb folder if empty
188 - if (count(glob($file . '/*')) === 0) {
189 - rmdir($file);
190 - }
191 - }
192 } 102 }
193 103
194 - // Delete file and database entry 104 + $this->imageRepo->destroyImage($image);
195 - unlink($folder . '/' . $fileName);
196 - $image->delete();
197 -
198 - // Delete parent folder if empty
199 - if (count(glob($folder . '/*')) === 0) {
200 - rmdir($folder);
201 - }
202 return response()->json('Image Deleted'); 105 return response()->json('Image Deleted');
203 } 106 }
204 107
......
...@@ -45,8 +45,6 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -45,8 +45,6 @@ Route::group(['middleware' => 'auth'], function () {
45 45
46 }); 46 });
47 47
48 - // Uploads
49 - Route::post('/upload/image', 'ImageController@upload');
50 48
51 // Users 49 // Users
52 Route::get('/users', 'UserController@index'); 50 Route::get('/users', 'UserController@index');
...@@ -58,10 +56,13 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -58,10 +56,13 @@ Route::group(['middleware' => 'auth'], function () {
58 Route::delete('/users/{id}', 'UserController@destroy'); 56 Route::delete('/users/{id}', 'UserController@destroy');
59 57
60 // Image routes 58 // Image routes
61 - Route::get('/images/all', 'ImageController@getAll'); 59 + Route::group(['prefix' => 'images'], function() {
62 - Route::put('/images/update/{imageId}', 'ImageController@update'); 60 + Route::get('/gallery/all', 'ImageController@getAllGallery');
63 - Route::delete('/images/{imageId}', 'ImageController@destroy'); 61 + Route::get('/gallery/all/{page}', 'ImageController@getAllGallery');
64 - Route::get('/images/all/{page}', 'ImageController@getAll'); 62 + Route::post('/gallery/upload', 'ImageController@uploadGallery');
63 + Route::put('/update/{imageId}', 'ImageController@update');
64 + Route::delete('/{imageId}', 'ImageController@destroy');
65 + });
65 66
66 // Links 67 // Links
67 Route::get('/link/{id}', 'PageController@redirectFromLink'); 68 Route::get('/link/{id}', 'PageController@redirectFromLink');
......
...@@ -77,6 +77,12 @@ class BookRepo ...@@ -77,6 +77,12 @@ class BookRepo
77 return Views::getUserRecentlyViewed($count, $page, $this->book); 77 return Views::getUserRecentlyViewed($count, $page, $this->book);
78 } 78 }
79 79
80 + /**
81 + * Gets the most viewed books.
82 + * @param int $count
83 + * @param int $page
84 + * @return mixed
85 + */
80 public function getPopular($count = 10, $page = 0) 86 public function getPopular($count = 10, $page = 0)
81 { 87 {
82 return Views::getPopular($count, $page, $this->book); 88 return Views::getPopular($count, $page, $this->book);
......
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 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -10,7 +10,8 @@ ...@@ -10,7 +10,8 @@
10 "intervention/image": "^2.3", 10 "intervention/image": "^2.3",
11 "laravel/socialite": "^2.0", 11 "laravel/socialite": "^2.0",
12 "barryvdh/laravel-ide-helper": "^2.1", 12 "barryvdh/laravel-ide-helper": "^2.1",
13 - "barryvdh/laravel-debugbar": "^2.0" 13 + "barryvdh/laravel-debugbar": "^2.0",
14 + "league/flysystem-aws-s3-v3": "^1.0"
14 }, 15 },
15 "require-dev": { 16 "require-dev": {
16 "fzaninotto/faker": "~1.4", 17 "fzaninotto/faker": "~1.4",
......
...@@ -4,10 +4,88 @@ ...@@ -4,10 +4,88 @@
4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
5 "This file is @generated automatically" 5 "This file is @generated automatically"
6 ], 6 ],
7 - "hash": "c2067be65a036c540976c649dfd3763a", 7 + "hash": "19725116631f01881caafa33052eecb9",
8 - "content-hash": "6581b2cca64df1f4d3df219552fbfd78", 8 + "content-hash": "f1dbd776f0ae13ec99e4e6d99510cd8e",
9 "packages": [ 9 "packages": [
10 { 10 {
11 + "name": "aws/aws-sdk-php",
12 + "version": "3.11.4",
13 + "source": {
14 + "type": "git",
15 + "url": "https://github.com/aws/aws-sdk-php.git",
16 + "reference": "2524c78e0fa1ed049719b8b6b0696f0b6dfb1ca2"
17 + },
18 + "dist": {
19 + "type": "zip",
20 + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2524c78e0fa1ed049719b8b6b0696f0b6dfb1ca2",
21 + "reference": "2524c78e0fa1ed049719b8b6b0696f0b6dfb1ca2",
22 + "shasum": ""
23 + },
24 + "require": {
25 + "guzzlehttp/guzzle": "~5.3|~6.0.1|~6.1",
26 + "guzzlehttp/promises": "~1.0",
27 + "guzzlehttp/psr7": "~1.0",
28 + "mtdowling/jmespath.php": "~2.2",
29 + "php": ">=5.5"
30 + },
31 + "require-dev": {
32 + "andrewsville/php-token-reflection": "^1.4",
33 + "aws/aws-php-sns-message-validator": "~1.0",
34 + "behat/behat": "~3.0",
35 + "doctrine/cache": "~1.4",
36 + "ext-dom": "*",
37 + "ext-json": "*",
38 + "ext-openssl": "*",
39 + "ext-pcre": "*",
40 + "ext-simplexml": "*",
41 + "ext-spl": "*",
42 + "nette/neon": "^2.3",
43 + "phpunit/phpunit": "~4.0"
44 + },
45 + "suggest": {
46 + "doctrine/cache": "To use the DoctrineCacheAdapter",
47 + "ext-curl": "To send requests using cURL",
48 + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages"
49 + },
50 + "type": "library",
51 + "extra": {
52 + "branch-alias": {
53 + "dev-master": "3.0-dev"
54 + }
55 + },
56 + "autoload": {
57 + "psr-4": {
58 + "Aws\\": "src/"
59 + },
60 + "files": [
61 + "src/functions.php"
62 + ]
63 + },
64 + "notification-url": "https://packagist.org/downloads/",
65 + "license": [
66 + "Apache-2.0"
67 + ],
68 + "authors": [
69 + {
70 + "name": "Amazon Web Services",
71 + "homepage": "http://aws.amazon.com"
72 + }
73 + ],
74 + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
75 + "homepage": "http://aws.amazon.com/sdkforphp",
76 + "keywords": [
77 + "amazon",
78 + "aws",
79 + "cloud",
80 + "dynamodb",
81 + "ec2",
82 + "glacier",
83 + "s3",
84 + "sdk"
85 + ],
86 + "time": "2015-12-04 01:19:53"
87 + },
88 + {
11 "name": "barryvdh/laravel-debugbar", 89 "name": "barryvdh/laravel-debugbar",
12 "version": "v2.0.6", 90 "version": "v2.0.6",
13 "source": { 91 "source": {
...@@ -1075,6 +1153,53 @@ ...@@ -1075,6 +1153,53 @@
1075 "time": "2015-09-30 22:26:59" 1153 "time": "2015-09-30 22:26:59"
1076 }, 1154 },
1077 { 1155 {
1156 + "name": "league/flysystem-aws-s3-v3",
1157 + "version": "1.0.9",
1158 + "source": {
1159 + "type": "git",
1160 + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
1161 + "reference": "595e24678bf78f8107ebc9355d8376ae0eb712c6"
1162 + },
1163 + "dist": {
1164 + "type": "zip",
1165 + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/595e24678bf78f8107ebc9355d8376ae0eb712c6",
1166 + "reference": "595e24678bf78f8107ebc9355d8376ae0eb712c6",
1167 + "shasum": ""
1168 + },
1169 + "require": {
1170 + "aws/aws-sdk-php": "^3.0.0",
1171 + "league/flysystem": "~1.0",
1172 + "php": ">=5.5.0"
1173 + },
1174 + "require-dev": {
1175 + "henrikbjorn/phpspec-code-coverage": "~1.0.1",
1176 + "phpspec/phpspec": "^2.0.0"
1177 + },
1178 + "type": "library",
1179 + "extra": {
1180 + "branch-alias": {
1181 + "dev-master": "1.0-dev"
1182 + }
1183 + },
1184 + "autoload": {
1185 + "psr-4": {
1186 + "League\\Flysystem\\AwsS3v3\\": "src/"
1187 + }
1188 + },
1189 + "notification-url": "https://packagist.org/downloads/",
1190 + "license": [
1191 + "MIT"
1192 + ],
1193 + "authors": [
1194 + {
1195 + "name": "Frank de Jonge",
1196 + "email": "info@frenky.net"
1197 + }
1198 + ],
1199 + "description": "Flysystem adapter for the AWS S3 SDK v3.x",
1200 + "time": "2015-11-19 08:44:16"
1201 + },
1202 + {
1078 "name": "league/oauth1-client", 1203 "name": "league/oauth1-client",
1079 "version": "1.6.1", 1204 "version": "1.6.1",
1080 "source": { 1205 "source": {
...@@ -1315,6 +1440,61 @@ ...@@ -1315,6 +1440,61 @@
1315 "time": "2015-01-11 23:07:46" 1440 "time": "2015-01-11 23:07:46"
1316 }, 1441 },
1317 { 1442 {
1443 + "name": "mtdowling/jmespath.php",
1444 + "version": "2.2.0",
1445 + "source": {
1446 + "type": "git",
1447 + "url": "https://github.com/jmespath/jmespath.php.git",
1448 + "reference": "a7d99d0c836e69d27b7bfca1d33ca2759fba3289"
1449 + },
1450 + "dist": {
1451 + "type": "zip",
1452 + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a7d99d0c836e69d27b7bfca1d33ca2759fba3289",
1453 + "reference": "a7d99d0c836e69d27b7bfca1d33ca2759fba3289",
1454 + "shasum": ""
1455 + },
1456 + "require": {
1457 + "php": ">=5.4.0"
1458 + },
1459 + "require-dev": {
1460 + "phpunit/phpunit": "~4.0"
1461 + },
1462 + "bin": [
1463 + "bin/jp.php"
1464 + ],
1465 + "type": "library",
1466 + "extra": {
1467 + "branch-alias": {
1468 + "dev-master": "2.0-dev"
1469 + }
1470 + },
1471 + "autoload": {
1472 + "psr-4": {
1473 + "JmesPath\\": "src/"
1474 + },
1475 + "files": [
1476 + "src/JmesPath.php"
1477 + ]
1478 + },
1479 + "notification-url": "https://packagist.org/downloads/",
1480 + "license": [
1481 + "MIT"
1482 + ],
1483 + "authors": [
1484 + {
1485 + "name": "Michael Dowling",
1486 + "email": "mtdowling@gmail.com",
1487 + "homepage": "https://github.com/mtdowling"
1488 + }
1489 + ],
1490 + "description": "Declaratively specify how to extract elements from a JSON document",
1491 + "keywords": [
1492 + "json",
1493 + "jsonpath"
1494 + ],
1495 + "time": "2015-05-27 17:21:31"
1496 + },
1497 + {
1318 "name": "nesbot/carbon", 1498 "name": "nesbot/carbon",
1319 "version": "1.21.0", 1499 "version": "1.21.0",
1320 "source": { 1500 "source": {
......
...@@ -45,7 +45,7 @@ return [ ...@@ -45,7 +45,7 @@ return [
45 45
46 'local' => [ 46 'local' => [
47 'driver' => 'local', 47 'driver' => 'local',
48 - 'root' => storage_path('app'), 48 + 'root' => public_path(),
49 ], 49 ],
50 50
51 'ftp' => [ 51 'ftp' => [
...@@ -64,10 +64,10 @@ return [ ...@@ -64,10 +64,10 @@ return [
64 64
65 's3' => [ 65 's3' => [
66 'driver' => 's3', 66 'driver' => 's3',
67 - 'key' => 'your-key', 67 + 'key' => env('STORAGE_S3_KEY', 'your-key'),
68 - 'secret' => 'your-secret', 68 + 'secret' => env('STORAGE_S3_SECRET', 'your-secret'),
69 - 'region' => 'your-region', 69 + 'region' => env('STORAGE_S3_REGION', 'your-region'),
70 - 'bucket' => 'your-bucket', 70 + 'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
71 ], 71 ],
72 72
73 'rackspace' => [ 73 'rackspace' => [
......
1 +<?php
2 +
3 +use BookStack\Image;
4 +use Illuminate\Database\Schema\Blueprint;
5 +use Illuminate\Database\Migrations\Migration;
6 +
7 +class AddImageUploadTypes extends Migration
8 +{
9 + /**
10 + * Run the migrations.
11 + *
12 + * @return void
13 + */
14 + public function up()
15 + {
16 + Schema::table('images', function (Blueprint $table) {
17 + $table->string('path', 400);
18 + $table->string('type')->index();
19 + });
20 +
21 + Image::all()->each(function($image) {
22 + $image->path = $image->url;
23 + $image->type = 'gallery';
24 + $image->save();
25 + });
26 + }
27 +
28 + /**
29 + * Reverse the migrations.
30 + *
31 + * @return void
32 + */
33 + public function down()
34 + {
35 + Schema::table('images', function (Blueprint $table) {
36 + $table->dropColumn('type');
37 + $table->dropColumn('path');
38 + });
39 +
40 + }
41 +}
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
7 <div v-for="image in images"> 7 <div v-for="image in images">
8 <img class="anim fadeIn" 8 <img class="anim fadeIn"
9 :class="{selected: (image==selectedImage)}" 9 :class="{selected: (image==selectedImage)}"
10 - :src="image.thumbnail" :alt="image.title" :title="image.name" 10 + :src="image.thumbs.gallery" :alt="image.title" :title="image.name"
11 @click="imageClick(image)" 11 @click="imageClick(image)"
12 :style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"> 12 :style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}">
13 </div> 13 </div>
...@@ -88,7 +88,7 @@ ...@@ -88,7 +88,7 @@
88 methods: { 88 methods: {
89 fetchData: function () { 89 fetchData: function () {
90 var _this = this; 90 var _this = this;
91 - this.$http.get('/images/all/' + _this.page, function (data) { 91 + this.$http.get('/images/gallery/all/' + _this.page, function (data) {
92 _this.images = _this.images.concat(data.images); 92 _this.images = _this.images.concat(data.images);
93 _this.hasMore = data.hasMore; 93 _this.hasMore = data.hasMore;
94 _this.page++; 94 _this.page++;
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
98 setupDropZone: function () { 98 setupDropZone: function () {
99 var _this = this; 99 var _this = this;
100 var dropZone = new Dropzone(_this.$els.dropZone, { 100 var dropZone = new Dropzone(_this.$els.dropZone, {
101 - url: '/upload/image', 101 + url: '/images/gallery/upload',
102 init: function () { 102 init: function () {
103 var dz = this; 103 var dz = this;
104 this.on("sending", function (file, xhr, data) { 104 this.on("sending", function (file, xhr, data) {
...@@ -110,8 +110,8 @@ ...@@ -110,8 +110,8 @@
110 dz.removeFile(file); 110 dz.removeFile(file);
111 }); 111 });
112 }); 112 });
113 - this.on('error', function(file, errorMessage, xhr) { 113 + this.on('error', function (file, errorMessage, xhr) {
114 - if(errorMessage.file) { 114 + if (errorMessage.file) {
115 $(file.previewElement).find('[data-dz-errormessage]').text(errorMessage.file[0]); 115 $(file.previewElement).find('[data-dz-errormessage]').text(errorMessage.file[0]);
116 } 116 }
117 console.log(errorMessage); 117 console.log(errorMessage);
......
...@@ -100,7 +100,7 @@ module.exports = { ...@@ -100,7 +100,7 @@ module.exports = {
100 onclick: function() { 100 onclick: function() {
101 ImageManager.show(function(image) { 101 ImageManager.show(function(image) {
102 var html = '<a href="'+image.url+'" target="_blank">'; 102 var html = '<a href="'+image.url+'" target="_blank">';
103 - html += '<img src="'+image.display+'" alt="'+image.name+'">'; 103 + html += '<img src="'+image.thumbs.display+'" alt="'+image.name+'">';
104 html += '</a>'; 104 html += '</a>';
105 editor.execCommand('mceInsertContent', false, html); 105 editor.execCommand('mceInsertContent', false, html);
106 }); 106 });
......
...@@ -23,6 +23,11 @@ ...@@ -23,6 +23,11 @@
23 <label>Allow public viewing?</label> 23 <label>Allow public viewing?</label>
24 <toggle-switch name="setting-app-public" value="{{ Setting::get('app-public') }}"></toggle-switch> 24 <toggle-switch name="setting-app-public" value="{{ Setting::get('app-public') }}"></toggle-switch>
25 </div> 25 </div>
26 + <div class="form-group">
27 + <label>Enable higher security image uploads?</label>
28 + <p class="small">For performance reasons, all images are public by default, This option adds a random, hard-to-guess characters in front of image names. Ensure directory indexes are not enabled to prevent easy access.</p>
29 + <toggle-switch name="setting-app-secure-images" value="{{ Setting::get('app-secure-images') }}"></toggle-switch>
30 + </div>
26 </div> 31 </div>
27 <div class="col-md-6"> 32 <div class="col-md-6">
28 <div class="form-group" id="logo-control"> 33 <div class="form-group" id="logo-control">
...@@ -57,7 +62,7 @@ ...@@ -57,7 +62,7 @@
57 </select> 62 </select>
58 </div> 63 </div>
59 <div class="form-group"> 64 <div class="form-group">
60 - <label for="setting-registration-confirmation">Require Email Confirmation?</label> 65 + <label for="setting-registration-confirmation">Require email confirmation?</label>
61 <p class="small">If domain restriction is used then email confirmation will be required and the below value will be ignored.</p> 66 <p class="small">If domain restriction is used then email confirmation will be required and the below value will be ignored.</p>
62 <toggle-switch name="setting-registration-confirmation" value="{{ Setting::get('registration-confirmation') }}"></toggle-switch> 67 <toggle-switch name="setting-registration-confirmation" value="{{ Setting::get('registration-confirmation') }}"></toggle-switch>
63 </div> 68 </div>
......