]> BookStack Code Mirror - bookstack/commitdiff
Revamped image system to use driver-agnotstic storage and be more efficent
authorDan Brown <redacted>
Mon, 7 Dec 2015 23:00:34 +0000 (23:00 +0000)
committerDan Brown <redacted>
Mon, 7 Dec 2015 23:00:34 +0000 (23:00 +0000)
12 files changed:
.env.example
app/Http/Controllers/ImageController.php
app/Http/routes.php
app/Repos/BookRepo.php
app/Repos/ImageRepo.php [new file with mode: 0644]
composer.json
composer.lock
config/filesystems.php
database/migrations/2015_12_07_195238_add_image_upload_types.php [new file with mode: 0644]
resources/assets/js/components/image-manager.vue
resources/assets/js/pages/page-form.js
resources/views/settings/index.blade.php

index d3cf77234a042ae65c27efb7a43187224a87e483..6d9189b5d5cb255f5350a53a7837ff5439a8c815 100644 (file)
@@ -14,12 +14,23 @@ CACHE_DRIVER=file
 SESSION_DRIVER=file
 QUEUE_DRIVER=sync
 
+# Storage
+STORAGE_TYPE=local
+# Amazon S3 Config
+STORAGE_S3_KEY=false
+STORAGE_S3_SECRET=false
+STORAGE_S3_REGION=false
+STORAGE_S3_BUCKET=false
+# Storage URL
+# Used to prefix image urls for when using custom domains/cdns
+STORAGE_URL=false
+
 # Social Authentication information. Defaults as off.
 GITHUB_APP_ID=false
 GITHUB_APP_SECRET=false
 GOOGLE_APP_ID=false
 GOOGLE_APP_SECRET=false
-# URL for social login redirects, NO TRAILING SLASH
+# URL used for social login redirects, NO TRAILING SLASH
 APP_URL=http://bookstack.dev
 
 # Mail settings
index fd790157034dc3e3800742abaf8b139c77903ab6..7e7fedfdecb4c3b9c6435bb3ea327d4278e0e8d9 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace BookStack\Http\Controllers;
 
+use BookStack\Repos\ImageRepo;
 use Illuminate\Filesystem\Filesystem as File;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
@@ -14,16 +15,19 @@ class ImageController extends Controller
 {
     protected $image;
     protected $file;
+    protected $imageRepo;
 
     /**
      * ImageController constructor.
-     * @param Image $image
-     * @param File  $file
+     * @param Image     $image
+     * @param File      $file
+     * @param ImageRepo $imageRepo
      */
-    public function __construct(Image $image, File $file)
+    public function __construct(Image $image, File $file, ImageRepo $imageRepo)
     {
         $this->image = $image;
         $this->file = $file;
+        $this->imageRepo = $imageRepo;
         parent::__construct();
     }
 
@@ -33,108 +37,31 @@ class ImageController extends Controller
      * @param int $page
      * @return \Illuminate\Http\JsonResponse
      */
-    public function getAll($page = 0)
+    public function getAllGallery($page = 0)
     {
-        $pageSize = 30;
-        $images = $this->image->orderBy('created_at', 'desc')
-            ->skip($page * $pageSize)->take($pageSize)->get();
-        foreach ($images as $image) {
-            $this->loadSizes($image);
-        }
-        $hasMore = $this->image->orderBy('created_at', 'desc')
-                ->skip(($page + 1) * $pageSize)->take($pageSize)->count() > 0;
-        return response()->json([
-            'images' => $images,
-            'hasMore' => $hasMore
-        ]);
-    }
-
-    /**
-     * Loads the standard thumbnail sizes for an image.
-     * @param Image $image
-     */
-    private function loadSizes(Image $image)
-    {
-        $image->thumbnail = $this->getThumbnail($image, 150, 150);
-        $image->display = $this->getThumbnail($image, 840, 0, true);
+        $imgData = $this->imageRepo->getAllGallery($page);
+        return response()->json($imgData);
     }
 
-    /**
-     * Get the thumbnail for an image.
-     * If $keepRatio is true only the width will be used.
-     * @param      $image
-     * @param int  $width
-     * @param int  $height
-     * @param bool $keepRatio
-     * @return string
-     */
-    public function getThumbnail($image, $width = 220, $height = 220, $keepRatio = false)
-    {
-        $explodedPath = explode('/', $image->url);
-        $dirPrefix = $keepRatio ? 'scaled-' : 'thumbs-';
-        array_splice($explodedPath, 4, 0, [$dirPrefix . $width . '-' . $height]);
-        $thumbPath = implode('/', $explodedPath);
-        $thumbFilePath = public_path() . $thumbPath;
-
-        // Return the thumbnail url path if already exists
-        if (file_exists($thumbFilePath)) {
-            return $thumbPath;
-        }
-
-        // Otherwise create the thumbnail
-        $thumb = ImageTool::make(public_path() . $image->url);
-        if($keepRatio) {
-            $thumb->resize($width, null, function ($constraint) {
-                $constraint->aspectRatio();
-                $constraint->upsize();
-            });
-        } else {
-            $thumb->fit($width, $height);
-        }
-
-        // Create thumbnail folder if it does not exist
-        if (!file_exists(dirname($thumbFilePath))) {
-            mkdir(dirname($thumbFilePath), 0775, true);
-        }
-
-        //Save Thumbnail
-        $thumb->save($thumbFilePath);
-        return $thumbPath;
-    }
 
     /**
      * Handles image uploads for use on pages.
      * @param Request $request
      * @return \Illuminate\Http\JsonResponse
      */
-    public function upload(Request $request)
+    public function uploadGallery(Request $request)
     {
         $this->checkPermission('image-create');
         $this->validate($request, [
             'file' => 'image|mimes:jpeg,gif,png'
         ]);
-        $imageUpload = $request->file('file');
 
-        $name = str_replace(' ', '-', $imageUpload->getClientOriginalName());
-        $storageName = substr(sha1(time()), 0, 10) . '-' . $name;
-        $imagePath = '/uploads/images/' . Date('Y-m-M') . '/';
-        $storagePath = public_path() . $imagePath;
-        $fullPath = $storagePath . $storageName;
-        while (file_exists($fullPath)) {
-            $storageName = substr(sha1(rand()), 0, 3) . $storageName;
-            $fullPath = $storagePath . $storageName;
-        }
-        $imageUpload->move($storagePath, $storageName);
-        // Create and save image object
-        $this->image->name = $name;
-        $this->image->url = $imagePath . $storageName;
-        $this->image->created_by = auth()->user()->id;
-        $this->image->updated_by = auth()->user()->id;
-        $this->image->save();
-        $this->loadSizes($this->image);
-        return response()->json($this->image);
+        $imageUpload = $request->file('file');
+        $image = $this->imageRepo->saveNew($imageUpload, 'gallery');
+        return response()->json($image);
     }
 
+
     /**
      * Update image details
      * @param         $imageId
@@ -147,13 +74,12 @@ class ImageController extends Controller
         $this->validate($request, [
             'name' => 'required|min:2|string'
         ]);
-        $image = $this->image->findOrFail($imageId);
-        $image->fill($request->all());
-        $image->save();
-        $this->loadSizes($image);
-        return response()->json($this->image);
+        $image = $this->imageRepo->getById($imageId);
+        $image = $this->imageRepo->updateImageDetails($image, $request->all());
+        return response()->json($image);
     }
 
+
     /**
      * Deletes an image and all thumbnail/image files
      * @param PageRepo $pageRepo
@@ -164,41 +90,18 @@ class ImageController extends Controller
     public function destroy(PageRepo $pageRepo, Request $request, $id)
     {
         $this->checkPermission('image-delete');
-        $image = $this->image->findOrFail($id);
+        $image = $this->imageRepo->getById($id);
 
         // Check if this image is used on any pages
-        $pageSearch = $pageRepo->searchForImage($image->url);
         $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
-        if ($pageSearch !== false && !$isForced) {
-            return response()->json($pageSearch, 400);
-        }
-
-        // Delete files
-        $folder = public_path() . dirname($image->url);
-        $fileName = basename($image->url);
-
-        // Delete thumbnails
-        foreach (glob($folder . '/*') as $file) {
-            if (is_dir($file)) {
-                $thumbName = $file . '/' . $fileName;
-                if (file_exists($file)) {
-                    unlink($thumbName);
-                }
-                // Remove thumb folder if empty
-                if (count(glob($file . '/*')) === 0) {
-                    rmdir($file);
-                }
+        if (!$isForced) {
+            $pageSearch = $pageRepo->searchForImage($image->url);
+            if ($pageSearch !== false) {
+                return response()->json($pageSearch, 400);
             }
         }
 
-        // Delete file and database entry
-        unlink($folder . '/' . $fileName);
-        $image->delete();
-
-        // Delete parent folder if empty
-        if (count(glob($folder . '/*')) === 0) {
-            rmdir($folder);
-        }
+        $this->imageRepo->destroyImage($image);
         return response()->json('Image Deleted');
     }
 
index e605dbee1ed44e976c6c3bfee48e7435219f7e17..5b1e245a15664acd41a738dafd88c91a711d097a 100644 (file)
@@ -45,8 +45,6 @@ Route::group(['middleware' => 'auth'], function () {
 
     });
 
-    // Uploads
-    Route::post('/upload/image', 'ImageController@upload');
 
     // Users
     Route::get('/users', 'UserController@index');
@@ -58,10 +56,13 @@ Route::group(['middleware' => 'auth'], function () {
     Route::delete('/users/{id}', 'UserController@destroy');
 
     // Image routes
-    Route::get('/images/all', 'ImageController@getAll');
-    Route::put('/images/update/{imageId}', 'ImageController@update');
-    Route::delete('/images/{imageId}', 'ImageController@destroy');
-    Route::get('/images/all/{page}', 'ImageController@getAll');
+    Route::group(['prefix' => 'images'], function() {
+        Route::get('/gallery/all', 'ImageController@getAllGallery');
+        Route::get('/gallery/all/{page}', 'ImageController@getAllGallery');
+        Route::post('/gallery/upload', 'ImageController@uploadGallery');
+        Route::put('/update/{imageId}', 'ImageController@update');
+        Route::delete('/{imageId}', 'ImageController@destroy');
+    });
 
     // Links
     Route::get('/link/{id}', 'PageController@redirectFromLink');
index 110426bbdd3d79bf1c955b5b24897c0349654ba5..469fdb31d053a5beec221d91fd6dc50c20850a4e 100644 (file)
@@ -77,6 +77,12 @@ class BookRepo
         return Views::getUserRecentlyViewed($count, $page, $this->book);
     }
 
+    /**
+     * Gets the most viewed books.
+     * @param int $count
+     * @param int $page
+     * @return mixed
+     */
     public function getPopular($count = 10, $page = 0)
     {
         return Views::getPopular($count, $page, $this->book);
diff --git a/app/Repos/ImageRepo.php b/app/Repos/ImageRepo.php
new file mode 100644 (file)
index 0000000..3dce7e9
--- /dev/null
@@ -0,0 +1,265 @@
+<?php namespace BookStack\Repos;
+
+
+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 Setting;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+class ImageRepo
+{
+
+    protected $image;
+    protected $imageTool;
+    protected $fileSystem;
+    protected $cache;
+
+    /**
+     * @var FileSystemInstance
+     */
+    protected $storageInstance;
+    protected $storageUrl;
+
+
+    /**
+     * ImageRepo constructor.
+     * @param Image      $image
+     * @param ImageTool  $imageTool
+     * @param FileSystem $fileSystem
+     * @param Cache      $cache
+     */
+    public function __construct(Image $image, ImageTool $imageTool, FileSystem $fileSystem, Cache $cache)
+    {
+        $this->image = $image;
+        $this->imageTool = $imageTool;
+        $this->fileSystem = $fileSystem;
+        $this->cache = $cache;
+    }
+
+
+    /**
+     * Get an image with the given id.
+     * @param $id
+     * @return mixed
+     */
+    public function getById($id)
+    {
+        return $this->image->findOrFail($id);
+    }
+
+
+    /**
+     * Get all images for the standard gallery view that's used for
+     * adding images to shared content such as pages.
+     * @param int $page
+     * @param int $pageSize
+     * @return array
+     */
+    public function getAllGallery($page = 0, $pageSize = 24)
+    {
+        $images = $this->image->where('type', '=', 'gallery')
+            ->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
+        $hasMore = count($images) > $pageSize;
+
+        $returnImages = $images->take(24);
+        $returnImages->each(function ($image) {
+            $this->loadThumbs($image);
+        });
+
+        return [
+            'images' => $returnImages,
+            'hasMore' => $hasMore
+        ];
+    }
+
+    /**
+     * Save a new image into storage and return the new image.
+     * @param UploadedFile $uploadFile
+     * @param  string      $type
+     * @return Image
+     */
+    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
+        ]);
+
+        $this->loadThumbs($image);
+        return $image;
+    }
+
+    /**
+     * Update the details of an image via an array of properties.
+     * @param Image $image
+     * @param array $updateDetails
+     * @return Image
+     */
+    public function updateImageDetails(Image $image, $updateDetails)
+    {
+        $image->fill($updateDetails);
+        $image->save();
+        $this->loadThumbs($image);
+        return $image;
+    }
+
+
+    /**
+     * 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;
+    }
+
+    /**
+     * 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.
+     * @param Image $image
+     */
+    private function loadThumbs(Image $image)
+    {
+        $image->thumbs = [
+            'gallery' => $this->getThumbnail($image, 150, 150),
+            'display' => $this->getThumbnail($image, 840, 0, true)
+        ];
+    }
+
+    /**
+     * 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
+     */
+    private 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'] . $filePath;
+            }
+
+            $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;
+    }
+
+
+}
\ No newline at end of file
index 6096cc7e45e4d8c4819c6e28ea34df1b7af7b347..7865be636d416dae02c45ba740015faa671e12cc 100644 (file)
@@ -10,7 +10,8 @@
         "intervention/image": "^2.3",
         "laravel/socialite": "^2.0",
         "barryvdh/laravel-ide-helper": "^2.1",
-        "barryvdh/laravel-debugbar": "^2.0"
+        "barryvdh/laravel-debugbar": "^2.0",
+        "league/flysystem-aws-s3-v3": "^1.0"
     },
     "require-dev": {
         "fzaninotto/faker": "~1.4",
index 595186a17e3a59a137a856ba2440de6a8ac893b8..23d2d50b6851b8cf3c4a43fed98019217e74c97e 100644 (file)
@@ -4,9 +4,87 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "c2067be65a036c540976c649dfd3763a",
-    "content-hash": "6581b2cca64df1f4d3df219552fbfd78",
+    "hash": "19725116631f01881caafa33052eecb9",
+    "content-hash": "f1dbd776f0ae13ec99e4e6d99510cd8e",
     "packages": [
+        {
+            "name": "aws/aws-sdk-php",
+            "version": "3.11.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/aws/aws-sdk-php.git",
+                "reference": "2524c78e0fa1ed049719b8b6b0696f0b6dfb1ca2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2524c78e0fa1ed049719b8b6b0696f0b6dfb1ca2",
+                "reference": "2524c78e0fa1ed049719b8b6b0696f0b6dfb1ca2",
+                "shasum": ""
+            },
+            "require": {
+                "guzzlehttp/guzzle": "~5.3|~6.0.1|~6.1",
+                "guzzlehttp/promises": "~1.0",
+                "guzzlehttp/psr7": "~1.0",
+                "mtdowling/jmespath.php": "~2.2",
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "andrewsville/php-token-reflection": "^1.4",
+                "aws/aws-php-sns-message-validator": "~1.0",
+                "behat/behat": "~3.0",
+                "doctrine/cache": "~1.4",
+                "ext-dom": "*",
+                "ext-json": "*",
+                "ext-openssl": "*",
+                "ext-pcre": "*",
+                "ext-simplexml": "*",
+                "ext-spl": "*",
+                "nette/neon": "^2.3",
+                "phpunit/phpunit": "~4.0"
+            },
+            "suggest": {
+                "doctrine/cache": "To use the DoctrineCacheAdapter",
+                "ext-curl": "To send requests using cURL",
+                "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Aws\\": "src/"
+                },
+                "files": [
+                    "src/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Amazon Web Services",
+                    "homepage": "http://aws.amazon.com"
+                }
+            ],
+            "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
+            "homepage": "http://aws.amazon.com/sdkforphp",
+            "keywords": [
+                "amazon",
+                "aws",
+                "cloud",
+                "dynamodb",
+                "ec2",
+                "glacier",
+                "s3",
+                "sdk"
+            ],
+            "time": "2015-12-04 01:19:53"
+        },
         {
             "name": "barryvdh/laravel-debugbar",
             "version": "v2.0.6",
             ],
             "time": "2015-09-30 22:26:59"
         },
+        {
+            "name": "league/flysystem-aws-s3-v3",
+            "version": "1.0.9",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
+                "reference": "595e24678bf78f8107ebc9355d8376ae0eb712c6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/595e24678bf78f8107ebc9355d8376ae0eb712c6",
+                "reference": "595e24678bf78f8107ebc9355d8376ae0eb712c6",
+                "shasum": ""
+            },
+            "require": {
+                "aws/aws-sdk-php": "^3.0.0",
+                "league/flysystem": "~1.0",
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "henrikbjorn/phpspec-code-coverage": "~1.0.1",
+                "phpspec/phpspec": "^2.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "League\\Flysystem\\AwsS3v3\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Frank de Jonge",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Flysystem adapter for the AWS S3 SDK v3.x",
+            "time": "2015-11-19 08:44:16"
+        },
         {
             "name": "league/oauth1-client",
             "version": "1.6.1",
             ],
             "time": "2015-01-11 23:07:46"
         },
+        {
+            "name": "mtdowling/jmespath.php",
+            "version": "2.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/jmespath/jmespath.php.git",
+                "reference": "a7d99d0c836e69d27b7bfca1d33ca2759fba3289"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a7d99d0c836e69d27b7bfca1d33ca2759fba3289",
+                "reference": "a7d99d0c836e69d27b7bfca1d33ca2759fba3289",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.0"
+            },
+            "bin": [
+                "bin/jp.php"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "JmesPath\\": "src/"
+                },
+                "files": [
+                    "src/JmesPath.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "[email protected]",
+                    "homepage": "https://github.com/mtdowling"
+                }
+            ],
+            "description": "Declaratively specify how to extract elements from a JSON document",
+            "keywords": [
+                "json",
+                "jsonpath"
+            ],
+            "time": "2015-05-27 17:21:31"
+        },
         {
             "name": "nesbot/carbon",
             "version": "1.21.0",
index 3fffcf0a2fdf8e0cff3e3ac82b443a499b6780cf..5fd3df6df848da25246a9c12371b813a2f232913 100644 (file)
@@ -45,7 +45,7 @@ return [
 
         'local' => [
             'driver' => 'local',
-            'root'   => storage_path('app'),
+            'root'   => public_path(),
         ],
 
         'ftp' => [
@@ -64,10 +64,10 @@ return [
 
         's3' => [
             'driver' => 's3',
-            'key'    => 'your-key',
-            'secret' => 'your-secret',
-            'region' => 'your-region',
-            'bucket' => 'your-bucket',
+            'key'    => env('STORAGE_S3_KEY', 'your-key'),
+            'secret' => env('STORAGE_S3_SECRET', 'your-secret'),
+            'region' => env('STORAGE_S3_REGION', 'your-region'),
+            'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
         ],
 
         'rackspace' => [
diff --git a/database/migrations/2015_12_07_195238_add_image_upload_types.php b/database/migrations/2015_12_07_195238_add_image_upload_types.php
new file mode 100644 (file)
index 0000000..eed9376
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+use BookStack\Image;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class AddImageUploadTypes extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('images', function (Blueprint $table) {
+            $table->string('path', 400);
+            $table->string('type')->index();
+        });
+
+        Image::all()->each(function($image) {
+            $image->path = $image->url;
+            $image->type = 'gallery';
+            $image->save();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('images', function (Blueprint $table) {
+            $table->dropColumn('type');
+            $table->dropColumn('path');
+        });
+
+    }
+}
index b1c805f8f5c502affe900a5b575036f4a39b2cd2..bb691760b92ae654feec8caef1f62772bce873ee 100644 (file)
@@ -7,7 +7,7 @@
                         <div v-for="image in images">
                             <img class="anim fadeIn"
                                  :class="{selected: (image==selectedImage)}"
-                                 :src="image.thumbnail" :alt="image.title" :title="image.name"
+                                 :src="image.thumbs.gallery" :alt="image.title" :title="image.name"
                                  @click="imageClick(image)"
                                  :style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}">
                         </div>
@@ -88,7 +88,7 @@
         methods: {
             fetchData: function () {
                 var _this = this;
-                this.$http.get('/images/all/' + _this.page, function (data) {
+                this.$http.get('/images/gallery/all/' + _this.page, function (data) {
                     _this.images = _this.images.concat(data.images);
                     _this.hasMore = data.hasMore;
                     _this.page++;
@@ -98,7 +98,7 @@
             setupDropZone: function () {
                 var _this = this;
                 var dropZone = new Dropzone(_this.$els.dropZone, {
-                    url: '/upload/image',
+                    url: '/images/gallery/upload',
                     init: function () {
                         var dz = this;
                         this.on("sending", function (file, xhr, data) {
                                 dz.removeFile(file);
                             });
                         });
-                        this.on('error', function(file, errorMessage, xhr) {
-                            if(errorMessage.file) {
+                        this.on('error', function (file, errorMessage, xhr) {
+                            if (errorMessage.file) {
                                 $(file.previewElement).find('[data-dz-errormessage]').text(errorMessage.file[0]);
                             }
                             console.log(errorMessage);
index f6ded00631c649e2b6a59cb9970e6fee1c294fa2..2bf7221965965932cc9f2d457c943efb0bbb85ab 100644 (file)
@@ -100,7 +100,7 @@ module.exports = {
             onclick: function() {
                 ImageManager.show(function(image) {
                     var html = '<a href="'+image.url+'" target="_blank">';
-                    html += '<img src="'+image.display+'" alt="'+image.name+'">';
+                    html += '<img src="'+image.thumbs.display+'" alt="'+image.name+'">';
                     html += '</a>';
                     editor.execCommand('mceInsertContent', false, html);
                 });
index eb59e0a5ce9ce653743173d8681b2b2240fa6576..ebf5381983bb81a231ce6a34c3dbdb3735a6357d 100644 (file)
                     <label>Allow public viewing?</label>
                     <toggle-switch name="setting-app-public" value="{{ Setting::get('app-public') }}"></toggle-switch>
                 </div>
+                <div class="form-group">
+                    <label>Enable higher security image uploads?</label>
+                    <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>
+                    <toggle-switch name="setting-app-secure-images" value="{{ Setting::get('app-secure-images') }}"></toggle-switch>
+                </div>
             </div>
             <div class="col-md-6">
                 <div class="form-group" id="logo-control">
@@ -57,7 +62,7 @@
                     </select>
                 </div>
                 <div class="form-group">
-                    <label for="setting-registration-confirmation">Require Email Confirmation?</label>
+                    <label for="setting-registration-confirmation">Require email confirmation?</label>
                     <p class="small">If domain restriction is used then email confirmation will be required and the below value will be ignored.</p>
                     <toggle-switch name="setting-registration-confirmation" value="{{ Setting::get('registration-confirmation') }}"></toggle-switch>
                 </div>