diff --git a/app/Album.php b/app/Album.php index aa99a1f..f123e5c 100644 --- a/app/Album.php +++ b/app/Album.php @@ -19,7 +19,7 @@ class Album extends Model * @var array */ protected $fillable = [ - 'name', 'description', 'url_alias', 'is_private', 'user_id' + 'name', 'description', 'url_alias', 'is_private', 'user_id', 'storage_id' ]; /** diff --git a/app/AlbumSources/AlbumSourceBase.php b/app/AlbumSources/AlbumSourceBase.php new file mode 100644 index 0000000..364bc39 --- /dev/null +++ b/app/AlbumSources/AlbumSourceBase.php @@ -0,0 +1,29 @@ +album = $album; + } + + public function setConfiguration(Storage $configuration) + { + $this->configuration = $configuration; + } +} \ No newline at end of file diff --git a/app/AlbumSources/IAlbumSource.php b/app/AlbumSources/IAlbumSource.php index f61206c..add06ef 100644 --- a/app/AlbumSources/IAlbumSource.php +++ b/app/AlbumSources/IAlbumSource.php @@ -22,6 +22,12 @@ interface IAlbumSource */ function deleteThumbnail(Photo $photo, $thumbnail = null); + /** + * Gets the name of this album source. + * @return string + */ + function getName(); + /** * Gets the absolute path to the given photo file. * @param Photo $photo Photo to get the path to. diff --git a/app/AlbumSources/LocalFilesystemSource.php b/app/AlbumSources/LocalFilesystemSource.php index 616047f..1786056 100644 --- a/app/AlbumSources/LocalFilesystemSource.php +++ b/app/AlbumSources/LocalFilesystemSource.php @@ -12,24 +12,8 @@ use Symfony\Component\HttpFoundation\File\File; * Driver for managing files on the local filesystem. * @package App\AlbumSources */ -class LocalFilesystemSource implements IAlbumSource +class LocalFilesystemSource extends AlbumSourceBase implements IAlbumSource { - /** - * @var Album - */ - private $album; - - /** - * @var string - */ - private $parentFolder; - - public function __construct(Album $album, $parentFolder) - { - $this->album = $album; - $this->parentFolder = $parentFolder; - } - public function deleteAlbumContents() { if (file_exists($this->getPathToAlbum()) && is_dir($this->getPathToAlbum())) @@ -48,6 +32,11 @@ class LocalFilesystemSource implements IAlbumSource return '_originals'; } + public function getName() + { + return 'global.album_sources.filesystem'; + } + public function getPathToPhoto(Photo $photo, $thumbnail = null) { if (is_null($thumbnail)) @@ -95,7 +84,7 @@ class LocalFilesystemSource implements IAlbumSource private function getPathToAlbum() { - return sprintf('%s/%s', $this->parentFolder, $this->album->url_alias); + return sprintf('%s/%s', $this->configuration->location, $this->album->url_alias); } private function recursiveDelete($directory) diff --git a/app/Helpers/ConfigHelper.php b/app/Helpers/ConfigHelper.php index 192064e..bea26fa 100644 --- a/app/Helpers/ConfigHelper.php +++ b/app/Helpers/ConfigHelper.php @@ -2,6 +2,9 @@ namespace App\Helpers; +use App\Album; +use App\AlbumSources\IAlbumSource; +use App\AlbumSources\LocalFilesystemSource; use App\Configuration; class ConfigHelper @@ -16,6 +19,24 @@ class ConfigHelper ]; } + public function albumSources() + { + $results = []; + + $classes = [ + LocalFilesystemSource::class + ]; + foreach ($classes as $class) + { + /** @var IAlbumSource $instance */ + $instance = new $class; + $key = basename(str_replace('\\', '/', $class)); + $results[$key] = trans($instance->getName()); + } + + return $results; + } + public function allowedThemeNames() { $results = []; diff --git a/app/Http/Controllers/Admin/AlbumController.php b/app/Http/Controllers/Admin/AlbumController.php index 1c82cc8..78b98a9 100644 --- a/app/Http/Controllers/Admin/AlbumController.php +++ b/app/Http/Controllers/Admin/AlbumController.php @@ -10,6 +10,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests; use App\Photo; use App\Services\PhotoService; +use App\Storage; use App\Upload; use Illuminate\Http\Request; use Illuminate\Support\Facades\App; @@ -45,7 +46,18 @@ class AlbumController extends Controller { $this->authorize('admin-access'); - return Theme::render('admin.create_album'); + $albumSources = []; + foreach (Storage::all()->sortBy('name') as $storage) + { + $albumSources[$storage->id] = $storage->name; + } + + $defaultSourceId = Storage::where('is_default', true)->limit(1)->first(); + + return Theme::render('admin.create_album', [ + 'album_sources' => $albumSources, + 'default_storage_id' => (!is_null($defaultSourceId) ? $defaultSourceId->id : 0) + ]); } public function delete($id) @@ -166,7 +178,7 @@ class AlbumController extends Controller $this->authorize('admin-access'); $album = new Album(); - $album->fill($request->only(['name', 'description'])); + $album->fill($request->only(['name', 'description', 'storage_id'])); $album->is_private = (strtolower($request->get('is_private')) == 'on'); $album->user_id = Auth::user()->id; diff --git a/app/Http/Controllers/Admin/StorageController.php b/app/Http/Controllers/Admin/StorageController.php new file mode 100644 index 0000000..d0ec7ba --- /dev/null +++ b/app/Http/Controllers/Admin/StorageController.php @@ -0,0 +1,133 @@ +middleware('auth'); + } + + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function index() + { + $this->authorize('admin-access'); + + $storageLocations = Storage::orderBy('name') + ->paginate(UserConfig::get('items_per_page')); + + return Theme::render('admin.list_storage', [ + 'storageLocations' => $storageLocations + ]); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response + */ + public function create() + { + $this->authorize('admin-access'); + + $filesystemDefaultLocation = sprintf('%s/storage/app/albums', dirname(dirname(dirname(dirname(__DIR__))))); + + return Theme::render('admin.create_storage', [ + 'album_sources' => UserConfig::albumSources(), + 'filesystem_default_location' => $filesystemDefaultLocation + ]); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function store(Requests\StoreStorageRequest $request) + { + $this->authorize('admin-access'); + + $storage = new Storage(); + $storage->fill($request->only(['name', 'source', 'location'])); + + $storage->is_default = (strtolower($request->get('is_default')) == 'on'); + $storage->save(); + + if ($storage->is_default) + { + // If this storage is flagged as default, remove all others + foreach (Storage::all() as $otherStorage) + { + if ($otherStorage->id == $storage->id) + { + // Ignore the one just created + continue; + } + + $otherStorage->is_default = false; + $otherStorage->save(); + } + } + + return redirect(route('storage.index')); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + //public function show($id) + //{ + // + //} + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function edit($id) + { + // + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\Response + */ + public function update(Request $request, $id) + { + // + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function destroy($id) + { + // + } +} diff --git a/app/Http/Middleware/CheckMaxPostSizeExceeded.php b/app/Http/Middleware/CheckMaxPostSizeExceeded.php index 8241b6a..8773e78 100644 --- a/app/Http/Middleware/CheckMaxPostSizeExceeded.php +++ b/app/Http/Middleware/CheckMaxPostSizeExceeded.php @@ -4,36 +4,77 @@ namespace App\Http\Middleware; use App\Helpers\MiscHelper; use Closure; +use Illuminate\Foundation\Application; use Illuminate\Http\Request; class CheckMaxPostSizeExceeded { + /** + * The application instance. + * + * @var \Illuminate\Foundation\Application + */ + protected $app; + protected $exclude = [ '/admin/photos/analyse/*', '/admin/photos/regenerate-thumbnails/*' ]; + /** + * Create a new middleware instance. + * + * @param \Illuminate\Foundation\Application $app + * @return void + */ + public function __construct(Application $app) + { + $this->app = $app; + } + public function handle(Request $request, Closure $next) { - if ($request->method() == 'POST' && !$this->shouldExclude($request)) + if ( + $this->isRunningInConsole() || + $this->isReading($request) || + $this->shouldPassThrough($request) + ) { - // Check post limit and see if it may have been exceeded - $postLimit = MiscHelper::convertToBytes(ini_get('post_max_size')); + return $next($request); + } - if ( - (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['CONTENT_LENGTH'] > $postLimit) || - (empty($_POST) && empty($_REQUEST)) - ) - { - $request->session()->flash('error', trans('global.post_max_exceeded')); - return back(); - } + // Check post limit and see if it may have been exceeded + $postLimit = MiscHelper::convertToBytes(ini_get('post_max_size')); + + if ( + (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['CONTENT_LENGTH'] > $postLimit) || + (empty($_POST) && empty($_REQUEST)) + ) + { + $request->session()->flash('error', trans('global.post_max_exceeded')); + return back(); } return $next($request); } - protected function shouldExclude(Request $request) + protected function isRunningInConsole() + { + return $this->app->runningInConsole(); + } + + /** + * Determine if the HTTP request uses a ‘read’ verb. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + protected function isReading(Request $request) + { + return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS']); + } + + protected function shouldPassThrough(Request $request) { foreach ($this->exclude as $exclude) { diff --git a/app/Http/Requests/StoreAlbumRequest.php b/app/Http/Requests/StoreAlbumRequest.php index 3e59eda..617ebdf 100644 --- a/app/Http/Requests/StoreAlbumRequest.php +++ b/app/Http/Requests/StoreAlbumRequest.php @@ -26,6 +26,7 @@ class StoreAlbumRequest extends FormRequest return [ 'description' => '', 'name' => 'required|unique:albums|max:255', + 'storage_id' => 'required' ]; } } diff --git a/app/Http/Requests/StoreStorageRequest.php b/app/Http/Requests/StoreStorageRequest.php new file mode 100644 index 0000000..40c0530 --- /dev/null +++ b/app/Http/Requests/StoreStorageRequest.php @@ -0,0 +1,31 @@ + 'required|unique:storages|max:255', + 'source' => 'required|max:255', + ]; + } +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index a69d6c3..42cc89a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -64,6 +64,11 @@ class AppServiceProvider extends ServiceProvider private function checkIfInstalled() { + if ($this->app->runningInConsole()) + { + return true; + } + if ($_SERVER['REQUEST_URI'] == '/install') { $baseDirectory = dirname(dirname(__DIR__)); diff --git a/app/Storage.php b/app/Storage.php new file mode 100644 index 0000000..75b6da3 --- /dev/null +++ b/app/Storage.php @@ -0,0 +1,20 @@ +increments('id'); + $table->string('name'); + $table->string('source', 100); + $table->boolean('is_default'); + $table->string('location'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('storages'); + } +} diff --git a/database/migrations/2016_09_24_083659_add_album_storage_column.php b/database/migrations/2016_09_24_083659_add_album_storage_column.php new file mode 100644 index 0000000..aeded78 --- /dev/null +++ b/database/migrations/2016_09_24_083659_add_album_storage_column.php @@ -0,0 +1,37 @@ +unsignedInteger('storage_id'); + + $table->foreign('storage_id') + ->references('id')->on('storages') + ->onDelete('no action'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('albums', function (Blueprint $table) { + $table->dropForeign('albums_storage_id_foreign'); + $table->dropColumn('storage_id'); + }); + } +} diff --git a/public/themes/base/js/app.js b/public/themes/base/js/app.js index 8a958ca..c153ecf 100644 --- a/public/themes/base/js/app.js +++ b/public/themes/base/js/app.js @@ -91,6 +91,13 @@ function AnalyseImageViewModel(image_info) }); } +function StorageLocationsViewModel() +{ + var self = this; + + self.selectedLocation = ko.observable(true); +} + /** * This file is used by admin/show_album.blade.php to handle photo uploads. * @param album_id ID of the album the photos are being uploaded to diff --git a/resources/lang/en/admin.php b/resources/lang/en/admin.php index 497c6a5..f7703a0 100644 --- a/resources/lang/en/admin.php +++ b/resources/lang/en/admin.php @@ -14,6 +14,8 @@ return [ 'create_album' => 'Create a photo album', 'create_album_intro' => 'Photo albums contain individual photographs together in the same way as a physical photo album or memory book.', 'create_album_intro2' => 'Complete the form below to create a photo album.', + 'create_storage' => 'Create storage location', + 'create_storage_intro' => 'Complete the form below to create a new storage location to hold your photos. You can then select this storage location when you create an album.', 'delete_album' => 'Delete album :name', 'delete_album_confirm' => 'Are you sure you want to permanently delete this album and all its contents?', 'delete_album_warning' => 'This is a permanent action that cannot be undone!', @@ -26,6 +28,9 @@ return [ ], 'no_albums_text' => 'You have no photo albums yet. Click the button below to create one.', 'no_albums_title' => 'No Photo Albums', + 'no_storages_text' => 'You need a storage location to store your uploaded photographs.', + 'no_storages_text2' => 'This can be on your server\'s local filesystem or a cloud location such as Amazon S3 or Rackspace.', + 'no_storages_title' => 'No storage locations defined', 'open_album' => 'Open album', 'photo_actions' => [ 'delete' => 'Delete', @@ -34,7 +39,6 @@ return [ 'rotate_right' => 'Rotate right' ], 'settings_image_protection' => 'Image Protection', - 'settings_link' => 'Settings', 'settings_recaptcha' => 'reCAPTCHA settings', 'settings_save_action' => 'Update Settings', 'settings_saved_message' => 'The settings were updated successfully.', @@ -46,6 +50,7 @@ return [ 'photos' => 'photo|photos', 'users' => 'user|users', ], + 'storage_title' => 'Storage Locations', 'sysinfo_panel' => 'System information', 'sysinfo_widget' => [ 'app_version' => 'Blue Twilight version:', diff --git a/resources/lang/en/forms.php b/resources/lang/en/forms.php index fc3eec0..e53f765 100644 --- a/resources/lang/en/forms.php +++ b/resources/lang/en/forms.php @@ -1,11 +1,13 @@ 'Storage location:', 'apply_action' => 'Apply', 'bulk_edit_photos_label' => 'Bulk edit selected photos:', 'bulk_edit_photos_placeholder' => 'Select an action', 'cancel_action' => 'Cancel', 'continue_action' => 'Continue', 'create_action' => 'Create', + 'default_storage_label' => 'Use as the default storage location for new albums', 'delete_action' => 'Delete', 'description_label' => 'Description:', 'edit_action' => 'Edit', @@ -23,6 +25,8 @@ return [ 'settings_hotlink_protection_help' => 'With this option enabled, direct linking to images is not allowed. Photos can only be viewed through Blue Twilight.', 'settings_restrict_originals_download' => 'Restrict access to original images', 'settings_restrict_originals_download_help' => 'With this option enabled, only the photo\'s owner can download the original high-resolution images.', + 'storage_driver_label' => 'Storage driver:', + 'storage_location_label' => 'Physical location:', 'upload_action' => 'Upload', 'save_action' => 'Save Changes' ]; \ No newline at end of file diff --git a/resources/lang/en/global.php b/resources/lang/en/global.php index 0f3d33d..2f6f06f 100644 --- a/resources/lang/en/global.php +++ b/resources/lang/en/global.php @@ -1,5 +1,8 @@ [ + 'filesystem' => 'Local filesystem' + ], 'app_name' => 'Blue Twilight', 'post_max_exceeded' => 'Your upload exceeded the maximum size the web server is configured to allow. Please check the value of the "post_max_size" parameter in php.ini.', 'units' => [ diff --git a/resources/lang/en/navigation.php b/resources/lang/en/navigation.php index deb5fb1..9a118d5 100644 --- a/resources/lang/en/navigation.php +++ b/resources/lang/en/navigation.php @@ -5,9 +5,11 @@ return [ 'albums' => 'Albums', 'create_album' => 'Create album', 'delete_album' => 'Delete album', + 'create_storage' => 'Create storage', 'edit_album' => 'Edit album', 'home' => 'Gallery', 'settings' => 'Settings', + 'storage' => 'Storage', 'users' => 'Users' ], 'navbar' => [ diff --git a/resources/views/themes/base/admin/create_album.blade.php b/resources/views/themes/base/admin/create_album.blade.php index 62d9617..b397f18 100644 --- a/resources/views/themes/base/admin/create_album.blade.php +++ b/resources/views/themes/base/admin/create_album.blade.php @@ -44,6 +44,11 @@ {!! Form::textarea('description', old('description'), ['class' => 'form-control']) !!} +
+ {!! Form::label('storage_id', trans('forms.album_source_label'), ['class' => 'control-label']) !!} + {!! Form::select('storage_id', $album_sources, old('storage_id', $default_storage_id), ['class' => 'form-control']) !!} +
+
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 47e55d0..63f6e34 100644 --- a/routes/web.php +++ b/routes/web.php @@ -32,6 +32,10 @@ Route::group(['prefix' => 'admin'], function () { Route::post('photos/store-bulk', 'Admin\PhotoController@storeBulk')->name('photos.storeBulk'); Route::put('photos/update-bulk/{albumId}', 'Admin\PhotoController@updateBulk')->name('photos.updateBulk'); Route::resource('photos', 'Admin\PhotoController'); + + // Storage management + Route::get('storage/{id}/delete', 'Admin\StorageController@delete')->name('storage.delete'); + Route::resource('storage', 'Admin\StorageController'); }); // Gallery