Added the ability to create storage locations and set those as the album source when creating a new album

This commit is contained in:
Andy Heathershaw 2016-09-24 09:34:08 +01:00
parent fde988e359
commit 48b43c3dd2
24 changed files with 561 additions and 35 deletions

View File

@ -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'
];
/**

View File

@ -0,0 +1,29 @@
<?php
namespace app\AlbumSources;
use App\Album;
use App\Storage;
abstract class AlbumSourceBase
{
/**
* @var Album
*/
protected $album;
/**
* @var Storage
*/
protected $configuration;
public function setAlbum(Album $album)
{
$this->album = $album;
}
public function setConfiguration(Storage $configuration)
{
$this->configuration = $configuration;
}
}

View File

@ -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.

View File

@ -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)

View File

@ -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 = [];

View File

@ -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;

View File

@ -0,0 +1,133 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Storage;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
class StorageController extends Controller
{
public function __construct()
{
$this->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)
{
//
}
}

View File

@ -4,19 +4,45 @@ 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)
)
{
return $next($request);
}
// Check post limit and see if it may have been exceeded
$postLimit = MiscHelper::convertToBytes(ini_get('post_max_size'));
@ -28,12 +54,27 @@ class CheckMaxPostSizeExceeded
$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)
{

View File

@ -26,6 +26,7 @@ class StoreAlbumRequest extends FormRequest
return [
'description' => '',
'name' => 'required|unique:albums|max:255',
'storage_id' => 'required'
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreStorageRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|unique:storages|max:255',
'source' => 'required|max:255',
];
}
}

View File

@ -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__));

20
app/Storage.php Normal file
View File

@ -0,0 +1,20 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
class Storage extends Model
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'source', 'is_default', 'location'
];
}

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateStoragesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('storages', function (Blueprint $table) {
$table->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');
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddAlbumStorageColumn extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('albums', function (Blueprint $table) {
$table->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');
});
}
}

View File

@ -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

View File

@ -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:',

View File

@ -1,11 +1,13 @@
<?php
return [
'album_source_label' => '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'
];

View File

@ -1,5 +1,8 @@
<?php
return [
'album_sources' => [
'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' => [

View File

@ -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' => [

View File

@ -44,6 +44,11 @@
{!! Form::textarea('description', old('description'), ['class' => 'form-control']) !!}
</div>
<div class="form-group">
{!! 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']) !!}
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="is_private">

View File

@ -0,0 +1,77 @@
@extends('themes.base.layout')
@section('title', trans('admin.create_storage'))
@section('breadcrumb')
<div class="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li><a href="{{ route('home') }}">@lang('navigation.breadcrumb.home')</a></li>
<li><a href="{{ route('admin') }}">@lang('navigation.breadcrumb.admin')</a></li>
<li><a href="{{ route('storage.index') }}">@lang('navigation.breadcrumb.storage')</a></li>
<li class="active">@lang('navigation.breadcrumb.create_storage')</li>
</ol>
</div>
</div>
@endsection
@section('content')
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>@lang('admin.create_storage')</h1>
<p>@lang('admin.create_storage_intro')</p>
<hr/>
@if (count($errors) > 0)
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{!! Form::open(['route' => 'storage.store', 'method' => 'POST']) !!}
<div class="form-group">
{!! Form::label('name', trans('forms.name_label'), ['class' => 'control-label']) !!}
{!! Form::text('name', old('name'), ['class' => 'form-control']) !!}
</div>
<div class="form-group">
{!! Form::label('source', trans('forms.album_source_label'), ['class' => 'control-label']) !!}
{!! Form::select('source', $album_sources, old('source'), ['class' => 'form-control', 'data-bind' => 'value: selectedLocation']) !!}
</div>
<div id="storage-options">
<div id="local-filesystem" data-bind="visible: selectedLocation() == 'LocalFilesystemSource'">
<div class="form-group">
{!! Form::label('location', trans('forms.storage_driver_label'), ['class' => 'control-label']) !!}
{!! Form::text('location', old('location', $filesystem_default_location), ['class' => 'form-control']) !!}
</div>
</div>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="is_default">
<strong>@lang('forms.default_storage_label')</strong>
</label>
</div>
<div class="form-actions">
<a href="{{ route('albums.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a>
{!! Form::submit(trans('forms.create_action'), ['class' => 'btn btn-success']) !!}
</div>
{!! Form::close() !!}
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
var viewModel = new StorageLocationsViewModel();
ko.applyBindings(viewModel);
</script>
@endpush

View File

@ -0,0 +1,58 @@
@extends('themes.base.layout')
@section('title', trans('admin.storage_title'))
@section('breadcrumb')
<div class="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li><a href="{{ route('home') }}">@lang('navigation.breadcrumb.home')</a></li>
<li><a href="{{ route('admin') }}">@lang('navigation.breadcrumb.admin')</a></li>
<li class="active">@lang('navigation.breadcrumb.storage')</li>
</ol>
</div>
</div>
@endsection
@section('content')
<div class="container">
<div class="row">
<div class="col-xs-12">
@if (count($storageLocations) == 0)
<div class="text-center">
<h4 class="text-danger"><b>@lang('admin.no_storages_title')</b></h4>
<p>@lang('admin.no_storages_text')</p>
<p>@lang('admin.no_storages_text2')</p>
<p style="margin-top: 40px;">
<a href="{{ route('storage.create') }}" class="btn btn-lg btn-success">@lang('admin.create_storage')</a>
</p>
</div>
@else
<table class="table table-hover table-striped">
<tbody>
@foreach ($storageLocations as $storage)
<tr>
<td>
<span style="font-size: 1.3em;">{{ $storage->name }}@if ($storage->is_default) <i class="fa fa-fw fa-check text-success"></i>@endif</span><br/>
{{ $storage->location }}
</td>
<td class="text-right">
<a href="{{ route('storage.edit', ['id' => $storage->id]) }}" class="btn btn-default">@lang('forms.edit_action')</a>
<a href="{{ route('storage.delete', ['id' => $storage->id]) }}" class="btn btn-danger">@lang('forms.delete_action')</a>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="text-center">
{{ $storageLocations->links() }}
</div>
<div class="pull-right" style="margin-top: 10px;">
<a href="{{ route('storage.create') }}" class="btn btn-success"><i class="fa fa-fw fa-plus"></i> @lang('admin.create_storage')</a>
</div>
@endif
</div>
</div>
</div>
@endsection

View File

@ -4,7 +4,8 @@
<ul class="nav nav-pills">
<li role="presentation"><a href="{{ route('albums.index') }}"><i class="fa fa-fw fa-picture-o"></i> @lang('navigation.breadcrumb.albums')</a></li>
<li role="presentation"><a href="#"><i class="fa fa-fw fa-user"></i> @lang('navigation.breadcrumb.users')</a></li>
<li role="presentation"><a href="{{ route('admin.settings') }}"><i class="fa fa-fw fa-cog"></i> @lang('admin.settings_link')</a></li>
<li role="presentation"><a href="{{ route('admin.settings') }}"><i class="fa fa-fw fa-cog"></i> @lang('navigation.breadcrumb.settings')</a></li>
<li role="presentation"><a href="{{ route('storage.index') }}"><i class="fa fa-fw fa-folder"></i> @lang('navigation.breadcrumb.storage')</a></li>
</ul>
</div>
</div>

View File

@ -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