Revamped image system to use driver-agnotstic storage and be more efficent
Showing
12 changed files
with
554 additions
and
141 deletions
| ... | @@ -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); | ... | ... |
app/Repos/ImageRepo.php
0 → 100644
| 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> | ... | ... |
-
Please register or sign in to post a comment