BLUE-8: The OpenStack driver now works completely with all operations - flip, rotate, refresh thumbnails. It's also possible to move photos between albums across different storage providers.

This commit is contained in:
Andy Heathershaw 2016-10-28 12:59:36 +01:00
parent 005c5eb645
commit 640828e244
16 changed files with 425 additions and 143 deletions

View File

@ -5,6 +5,7 @@ namespace App\AlbumSources;
use App\Album;
use App\Photo;
use App\Storage;
use Guzzle\Http\EntityBody;
use Symfony\Component\HttpFoundation\File\File;
interface IAlbumSource
@ -23,20 +24,20 @@ interface IAlbumSource
*/
function deleteThumbnail(Photo $photo, $thumbnail = null);
/**
* Fetches the contents of a thumbnail for a photo.
* @param Photo $photo Photo to fetch the thumbnail for.
* @param string $thumbnail Thumbnail to fetch (or null to fetch the original.)
* @return EntityBody
*/
function fetchPhotoContent(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.
* @param string $thumbnail Thumbnail to get the image to.
* @return string
*/
function getPathToPhoto(Photo $photo, $thumbnail = null);
/**
* Gets the absolute URL to the given photo file.
* @param Photo $photo Photo to get the URL to.
@ -45,30 +46,14 @@ interface IAlbumSource
*/
function getUrlToPhoto(Photo $photo, $thumbnail = null);
/**
* Saves the original photo to its permanent location.
* @param Photo $photo Photo the original relates to.
* @param $tempFilename Filename containing the original image.
* @return mixed
*/
function saveOriginal(Photo $photo, $tempFilename);
/**
* Saves a generated thumbnail to its permanent location.
* @param Photo $photo Photo the thumbnail relates to.
* @param string $thumbnailInfo Information about the thumbnail.
* @param string $tempFilename Filename containing the thumbnail.
* @param Photo $photo Photo the image relates to.
* @param string $tempFilename Filename containing the image.
* @param string $thumbnail Name of the thumbnail (or null for the original.)
* @return mixed
*/
function saveThumbnail(Photo $photo, $thumbnailInfo, $tempFilename);
/**
* Saves an uploaded file to the container and returns the filename.
* @param File $uploadedFile The photo uploaded
* @param string $overrideFilename Specific file name to use, or null to randomly generate one.
* @return File
*/
function saveUploadedPhoto(File $uploadedFile, $overrideFilename = null);
function saveThumbnail(Photo $photo, $tempFilename, $thumbnail = null);
/**
* @param Album $album

View File

@ -6,6 +6,7 @@ use App\Album;
use App\Helpers\MiscHelper;
use App\Photo;
use App\Services\PhotoService;
use Guzzle\Http\EntityBody;
use Symfony\Component\HttpFoundation\File\File;
/**
@ -24,12 +25,33 @@ class LocalFilesystemSource extends AlbumSourceBase implements IAlbumSource
public function deleteThumbnail(Photo $photo, $thumbnail = null)
{
return @unlink($this->getPathToPhoto($photo, $thumbnail));
return @unlink(
join(DIRECTORY_SEPARATOR, [
$this->getPathToAlbum(),
is_null($thumbnail) ? $this->getOriginalsFolder() : $thumbnail,
$photo->storage_file_name
])
);
}
public function getOriginalsFolder()
/**
* Fetches the contents of a thumbnail for a photo.
* @param Photo $photo Photo to fetch the thumbnail for.
* @param string $thumbnail Thumbnail to fetch (or null to fetch the original.)
* @return EntityBody
*/
public function fetchPhotoContent(Photo $photo, $thumbnail = null)
{
return '_originals';
$fh = fopen(
join(DIRECTORY_SEPARATOR, [
$this->getPathToAlbum(),
is_null($thumbnail) ? $this->getOriginalsFolder() : $thumbnail,
$photo->storage_file_name
]),
'r+'
);
return EntityBody::factory($fh);
}
public function getName()
@ -37,16 +59,6 @@ class LocalFilesystemSource extends AlbumSourceBase implements IAlbumSource
return 'global.album_sources.filesystem';
}
public function getPathToPhoto(Photo $photo, $thumbnail = null)
{
if (is_null($thumbnail))
{
$thumbnail = $this->getOriginalsFolder();
}
return sprintf('%s/%s/%s', $this->getPathToAlbum(), $thumbnail, $photo->storage_file_name);
}
public function getUrlToPhoto(Photo $photo, $thumbnail = null)
{
$photoUrl = route('downloadPhoto', [
@ -62,38 +74,21 @@ class LocalFilesystemSource extends AlbumSourceBase implements IAlbumSource
return $photoUrl;
}
public function saveOriginal(Photo $photo, $tempFilename)
{
$this->saveThumbnail($photo, $tempFilename, $this->getOriginalsFolder());
}
public function saveThumbnail(Photo $photo, $tempFilename, $thumbnail)
public function saveThumbnail(Photo $photo, $tempFilename, $thumbnail = null)
{
$fileInfo = new File($tempFilename);
$fileInfo->move(sprintf('%s/%s', $this->getPathToAlbum(), $thumbnail), $photo->storage_file_name);
$fileInfo->move(
join(DIRECTORY_SEPARATOR, [
$this->getPathToAlbum(),
is_null($thumbnail) ? $this->getOriginalsFolder() : $thumbnail
]),
$photo->storage_file_name
);
}
public function saveUploadedPhoto(File $uploadedFile, $overrideFilename = null)
private function getOriginalsFolder()
{
$tempFilename = sprintf(
'%s/%s/%s',
$this->getPathToAlbum(),
$this->getOriginalsFolder(),
is_null($overrideFilename) ? MiscHelper::randomString(20) : basename($overrideFilename)
);
// Only add an extension if an override filename was not given, assume this is present
if (is_null($overrideFilename))
{
$extension = $uploadedFile->guessExtension();
if (!is_null($extension))
{
$tempFilename .= '.' . $extension;
}
}
$uploadedFile->move(dirname($tempFilename), basename($tempFilename));
return new File($tempFilename);
return '_originals';
}
private function getPathToAlbum()

View File

@ -2,6 +2,10 @@
namespace App\AlbumSources;
use App\Photo;
use Guzzle\Http\EntityBody;
use Guzzle\Http\Exception\ClientErrorResponseException;
use Illuminate\Support\Facades\Log;
use OpenCloud\ObjectStore\Exception\ObjectNotFoundException;
use OpenCloud\OpenStack;
use Symfony\Component\HttpFoundation\File\File;
@ -17,7 +21,8 @@ class OpenStackSource extends AlbumSourceBase implements IAlbumSource
*/
public function deleteAlbumContents()
{
// TODO: Implement deleteAlbumContents() method.
// Because all photos live in a single container, there is nothing "global" to delete for the entire album
// The delete routine will have already removed all photos
}
/**
@ -28,7 +33,30 @@ class OpenStackSource extends AlbumSourceBase implements IAlbumSource
*/
public function deleteThumbnail(Photo $photo, $thumbnail = null)
{
// TODO: Implement deleteThumbnail() method.
$photoPath = $this->getPathToPhoto($photo, $thumbnail);
try
{
$this->getContainer()->deleteObject($photoPath);
}
catch (ClientErrorResponseException $ex)
{
// Don't worry if the file no longer exists
Log::warning('Failed deleting image from OpenStack.', ['error' => $ex->getMessage(), 'path' => $photoPath]);
}
}
/**
* Fetches the contents of a thumbnail for a photo.
* @param Photo $photo Photo to fetch the thumbnail for.
* @param string $thumbnail Thumbnail to fetch (or null to fetch the original.)
* @return EntityBody
*/
public function fetchPhotoContent(Photo $photo, $thumbnail = null)
{
$object = $this->getContainer()->getObject($this->getPathToPhoto($photo, $thumbnail));
return $object->getContent();
}
/**
@ -40,17 +68,6 @@ class OpenStackSource extends AlbumSourceBase implements IAlbumSource
return 'global.album_sources.openstack';
}
/**
* Gets the absolute path to the given photo file.
* @param Photo $photo Photo to get the path to.
* @param string $thumbnail Thumbnail to get the image to.
* @return string
*/
public function getPathToPhoto(Photo $photo, $thumbnail = null)
{
// TODO: Implement getPathToPhoto() method.
}
/**
* Gets the absolute URL to the given photo file.
* @param Photo $photo Photo to get the URL to.
@ -59,48 +76,47 @@ class OpenStackSource extends AlbumSourceBase implements IAlbumSource
*/
public function getUrlToPhoto(Photo $photo, $thumbnail = null)
{
return sprintf(
'%s/%s/%s/%s',
'https://photos-dev-898b0644.cdn.memsites.com',
$this->album->url_alias,
is_null($thumbnail) ? $this->getOriginalsFolder() : $thumbnail,
$photo->storage_file_name
);
}
$cdnUrl = $this->configuration->cdn_url;
if (strlen($cdnUrl) > 0)
{
if (substr($cdnUrl, strlen($cdnUrl) - 1, 1) == '/')
{
$cdnUrl = substr($cdnUrl, 0, strlen($cdnUrl) - 1);
}
public function saveOriginal(Photo $photo, $tempFilename)
{
$this->saveThumbnail($photo, $tempFilename, $this->getOriginalsFolder());
return sprintf('%s/%s', $cdnUrl, $this->getPathToPhoto($photo, $thumbnail));
}
// Not using a CDN - use the standard download controller
$photoUrl = route('downloadPhoto', [
'albumUrlAlias' => $this->album->url_alias,
'photoFilename' => $photo->storage_file_name
]);
if (!is_null($thumbnail))
{
$photoUrl .= sprintf('?t=%s', urlencode($thumbnail));
}
return $photoUrl;
}
/**
* Saves a generated thumbnail to its permanent location.
* @param Photo $photo Photo the thumbnail relates to.
* @param string $tempFilename Filename containing the thumbnail.
* @param string $thumbnail Name of the thumbnail.
* @param Photo $photo Photo the image relates to.
* @param string $tempFilename Filename containing the image.
* @param string $thumbnail Name of the thumbnail (or null for the original.)
* @return mixed
*/
public function saveThumbnail(Photo $photo, $tempFilename, $thumbnail)
public function saveThumbnail(Photo $photo, $tempFilename, $thumbnail = null)
{
$photoPath = $this->getPathToPhoto($photo, $thumbnail);
$container = $this->getContainer();
$container->uploadObject(
sprintf('%s/%s/%s', $this->album->url_alias, $thumbnail, $photo->storage_file_name),
fopen($tempFilename, 'r+')
);
$container->uploadObject($photoPath, fopen($tempFilename, 'r+'));
}
/**
* Saves an uploaded file to the container and returns the filename.
* @param File $uploadedFile The photo uploaded
* @param string $overrideFilename Specific file name to use, or null to randomly generate one.
* @return File
*/
public function saveUploadedPhoto(File $uploadedFile, $overrideFilename = null)
{
// TODO: Implement saveUploadedPhoto() method.
}
private function getClient()
protected function getClient()
{
return new OpenStack($this->configuration->auth_url, [
'username' => $this->configuration->username,
@ -109,17 +125,12 @@ class OpenStackSource extends AlbumSourceBase implements IAlbumSource
]);
}
private function getContainer()
protected function getContainer()
{
return $this->getStorageService()->getContainer($this->configuration->container_name);
}
private function getOriginalsFolder()
{
return '_originals';
}
private function getStorageService()
protected function getStorageService()
{
return $this->getClient()->objectStoreService(
$this->configuration->service_name,
@ -127,4 +138,19 @@ class OpenStackSource extends AlbumSourceBase implements IAlbumSource
'publicURL'
);
}
private function getOriginalsFolder()
{
return '_originals';
}
private function getPathToPhoto(Photo $photo, $thumbnail = null)
{
return sprintf(
'%s/%s/%s',
$this->album->url_alias,
is_null($thumbnail) ? $this->getOriginalsFolder() : $thumbnail,
$photo->storage_file_name
);
}
}

View File

@ -112,7 +112,8 @@ class PhotoController extends Controller
{
$this->authorize('admin-access');
settype($direction, 'boolean');
settype($horizontal, 'boolean');
settype($vertical, 'boolean');
$photo = Photo::where('id', intval($photoId))->first();
if (is_null($photo))
@ -407,9 +408,7 @@ class PhotoController extends Controller
return null;
}
$numberChanged = 0;
if ($request->has('bulk-apply'))
if ($request->has('bulk-action'))
{
$numberChanged = $this->applyBulkActions($request, $album);
}
@ -445,6 +444,7 @@ class PhotoController extends Controller
}
$photoService = new PhotoService($photo);
$doNotSave = false;
switch (strtolower($action))
{
case 'change_album':
@ -466,6 +466,7 @@ class PhotoController extends Controller
case 'delete':
$photoService->delete();
$doNotSave = true;
break;
case 'flip_both':
@ -493,7 +494,10 @@ class PhotoController extends Controller
break;
}
$photo->save();
if (!$doNotSave)
{
$photo->save();
}
$numberChanged++;
}

View File

@ -78,7 +78,8 @@ class StorageController extends Controller
'password',
'service_name',
'service_region',
'container_name'
'container_name',
'cdn_url'
]));
$storage->is_active = true;
$storage->is_default = (strtolower($request->get('is_default')) == 'on');
@ -164,6 +165,11 @@ class StorageController extends Controller
App::abort(404);
}
if (isset($storage->password) && strlen($storage->password) > 0)
{
$storage->password = decrypt($storage->password);
}
return Theme::render('admin.edit_storage', ['storage' => $storage]);
}
@ -184,7 +190,17 @@ class StorageController extends Controller
App::abort(404);
}
$storage->fill($request->only(['name']));
$storage->fill($request->only([
'name',
'auth_url',
'tenant_name',
'username',
'password',
'service_name',
'service_region',
'container_name',
'cdn_url'
]));
$storage->is_active = (strtolower($request->get('is_active')) == 'on');
$storage->is_default = (strtolower($request->get('is_default')) == 'on');
@ -193,6 +209,11 @@ class StorageController extends Controller
$storage->is_default = false;
}
if (isset($storage->password) && !empty($storage->password))
{
$storage->password = encrypt($storage->password);
}
$storage->save();
if ($storage->is_default)

View File

@ -11,6 +11,7 @@ use app\Http\Controllers\Admin\AlbumController;
use App\Http\Controllers\Controller;
use App\Http\Middleware\VerifyCsrfToken;
use App\Photo;
use Guzzle\Http\Mimetypes;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Gate;
use Symfony\Component\HttpFoundation\Request;
@ -53,10 +54,23 @@ class PhotoController extends Controller
$thumbnail = $request->get('t');
if (is_null($thumbnail))
{
Gate::forUser($this->getUser())->authorize('photo.download_original', $photo);
$this->authorizeForUser($this->getUser(), 'photo.download_original', $photo);
}
return response()->file($album->getAlbumSource()->getPathToPhoto($photo, $thumbnail));
$photoStream = $album->getAlbumSource()->fetchPhotoContent($photo, $thumbnail);
$mimeType = Mimetypes::getInstance()->fromFilename($photo->storage_file_name);
return response()->stream(
function() use ($photoStream)
{
echo $photoStream;
},
200,
[
'Content-Length' => $photoStream->getContentLength(),
'Content-Type' => $mimeType
]
);
}
public function show(Request $request, $albumUrlAlias, $photoFilename)

View File

@ -24,6 +24,8 @@ class StoreStorageRequest extends FormRequest
*/
public function rules()
{
$result = [];
switch ($this->method())
{
case 'POST':
@ -46,19 +48,35 @@ class StoreStorageRequest extends FormRequest
$result['service_name'] = 'sometimes|required';
$result['service_region'] = 'sometimes|required';
$result['container_name'] = 'sometimes|required';
$result['cdn_url'] = 'sometimes|url';
break;
}
return $result;
break;
case 'PATCH':
case 'PUT':
$storageId = intval($this->segment(3));
return [
$storage = Storage::find($storageId);
$result = [
'name' => 'required|max:255|unique:storages,name,' . $storageId
];
switch ($storage->source)
{
case 'OpenStackSource':
$result['auth_url'] = 'sometimes|required';
$result['tenant_name'] = 'sometimes|required';
$result['username'] = 'sometimes|required';
$result['password'] = 'sometimes|required';
$result['service_name'] = 'sometimes|required';
$result['service_region'] = 'sometimes|required';
$result['container_name'] = 'sometimes|required';
$result['cdn_url'] = 'sometimes|url';
break;
}
break;
}
return $result;
}
}

View File

@ -117,7 +117,7 @@ class PhotoService
$this->photo->save();
// Save the original
$this->albumSource->saveOriginal($this->photo, $photoFile);
$this->albumSource->saveThumbnail($this->photo, $photoFile);
$this->regenerateThumbnails($originalPhotoResource);
}
@ -128,11 +128,11 @@ class PhotoService
$currentSource = $this->photo->album->getAlbumSource();
$newSource = $newAlbum->getAlbumSource();
// Get the current photo's path
$file = new File($currentSource->getPathToPhoto($this->photo));
// First export the original photo from the storage provider
$photoPath = $this->downloadToTemporaryFolder();
// Save to the new album
$newSource->saveUploadedPhoto($file, $this->photo->storage_file_name);
$newSource->saveThumbnail($this->photo, $photoPath);
// Delete the original
$this->delete();
@ -169,25 +169,39 @@ class PhotoService
public function flip($horizontal, $vertical)
{
// First export the original photo from the storage provider
$photoPath = $this->downloadToTemporaryFolder();
$imageInfo = array();
$photoPath = $this->albumSource->getPathToPhoto($this->photo);
$originalPhotoImage = $this->imageHelper->openImage($photoPath, $imageInfo);
if ($this->imageHelper->flipImage($originalPhotoImage, boolval($horizontal), boolval($vertical)))
{
$this->imageHelper->saveImage($originalPhotoImage, $photoPath, $imageInfo);
// Update and save the original image back to the storage provider
$this->albumSource->saveThumbnail($this->photo, $photoPath);
// Re-create the thumbnails
$this->regenerateThumbnails($originalPhotoImage);
$this->photo->save();
}
// Remove the temp file
@unlink($photoPath);
}
public function regenerateThumbnails($originalPhotoResource = null)
{
$photoPath = null;
if (is_null($originalPhotoResource))
{
// First export the original photo from the storage provider
$photoPath = $this->downloadToTemporaryFolder();
$imageInfo = null;
$originalPhotoResource = $this->imageHelper->openImage($this->albumSource->getPathToPhoto($this->photo), $imageInfo);
$originalPhotoResource = $this->imageHelper->openImage($photoPath, $imageInfo);
}
// Generate and save thumbnails
@ -200,16 +214,28 @@ class PhotoService
$generatedThumbnailPath = $this->imageHelper->generateThumbnail($originalPhotoResource, $this->photo, $thumbnail);
$this->albumSource->saveThumbnail($this->photo, $generatedThumbnailPath, $thumbnail['name']);
}
if (is_null($originalPhotoResource) && !is_null($photoPath))
{
// Remove the temp file
@unlink($photoPath);
}
}
public function rotate($angle)
{
$imageInfo = array();
$photoPath = $this->albumSource->getPathToPhoto($this->photo);
// First export the photo from the storage provider
$photoPath = $this->downloadToTemporaryFolder();
$originalPhotoImage = $this->imageHelper->openImage($photoPath, $imageInfo);
$originalPhotoImage = $this->imageHelper->rotateImage($originalPhotoImage, intval($angle));
$this->imageHelper->saveImage($originalPhotoImage, $photoPath, $imageInfo);
// Update and save the original image back to the storage provider
$this->albumSource->saveThumbnail($this->photo, $photoPath);
if ($angle == 90 || $angle == 270)
{
$width = $this->photo->width;
@ -220,6 +246,27 @@ class PhotoService
$this->regenerateThumbnails($originalPhotoImage);
$this->photo->save();
// Remove the temp file
@unlink($photoPath);
}
private function downloadToTemporaryFolder()
{
$photoPath = tempnam(sys_get_temp_dir(), 'BlueTwilight_');
$photoHandle = fopen($photoPath, 'w');
$stream = $this->albumSource->fetchPhotoContent($this->photo);
$stream->rewind();
while (!$stream->feof())
{
fwrite($photoHandle, $stream->read(4096));
}
fflush($photoHandle);
fclose($photoHandle);
$stream->close();
return $photoPath;
}
private function metadataCameraMake(array $exifData)

View File

@ -27,7 +27,8 @@ class Storage extends Model
'password',
'service_name',
'service_region',
'container_name'
'container_name',
'cdn_url'
];
public function albums()

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddCdnUrlStorageColumn extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('storages', function (Blueprint $table) {
$table->string('cdn_url')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('storages', function (Blueprint $table) {
$table->dropColumn('cdn_url');
});
}
}

View File

@ -93,10 +93,16 @@ function EditPhotosViewModel(album_id, language, urls) {
self.albums = ko.observableArray();
self.bulkModifyMethod = ko.observable();
self.photoIDs = ko.observableArray();
self.isSubmitting = false;
/* Called when the Apply button on the "bulk apply selected actions" form is clicked */
self.bulkModifySelected = function()
{
if (self.isSubmitting)
{
return true;
}
var bulk_form = $('form#bulk-modify-form');
if (self.bulkModifyMethod() == 'change_album')
@ -104,8 +110,11 @@ function EditPhotosViewModel(album_id, language, urls) {
// Prompt for the new album to move to
self.promptForNewAlbum(function(dialog) {
var album_id = $('select', dialog).val();
$('input[name="new-album-id"]', form).val(album_id);
bulk_form.submit();
$('input[name="new-album-id"]', bulk_form).val(album_id);
self.isSubmitting = true;
$('button[name="bulk-apply"]', bulk_form).click();
_bt_showLoadingModal();
});
return false;
@ -125,7 +134,9 @@ function EditPhotosViewModel(album_id, language, urls) {
label: language.action_delete,
className: "btn-danger",
callback: function() {
bulk_form.submit();
self.isSubmitting = true;
$('button[name="bulk-apply"]', bulk_form).click();
_bt_showLoadingModal();
}
}
}
@ -151,6 +162,7 @@ function EditPhotosViewModel(album_id, language, urls) {
{
window.location.reload();
});
_bt_showLoadingModal();
});
return false;

View File

@ -31,6 +31,7 @@ return [
'settings_restrict_originals_download_help' => 'With this option enabled, only the photo\'s owner can download the original high-resolution images.',
'storage_active_label' => 'Location is active. Uncheck to prevent creating new albums in this location.',
'storage_auth_url_label' => 'Authentication URL:',
'storage_cdn_url_label' => 'Public CDN URL (if supported and enabled):',
'storage_container_name_label' => 'Container name:',
'storage_driver_label' => 'Storage driver:',
'storage_location_label' => 'Physical location:',

View File

@ -8,6 +8,7 @@ return [
'copyright' => '© :years :link_startAndy Heathershaw:link_end',
'licensed_to' => 'Licensed to :name (:number)',
'version_number' => 'Version :version',
'please_wait' => 'Please wait...',
'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.',
'powered_by' => 'Powered by :link_startBlue Twilight:link_end - the self-hosted photo gallery software.',
'units' => [

View File

@ -145,6 +145,17 @@
</span>
@endif
</div>
<div class="form-group{{ $errors->has('cdn_url') ? ' has-error' : '' }}">
{!! Form::label('cdn_url', trans('forms.storage_cdn_url_label'), ['class' => 'control-label']) !!}
{!! Form::text('cdn_url', old('cdn_url'), ['class' => 'form-control']) !!}
@if ($errors->has('cdn_url'))
<span class="help-block">
<strong>{{ $errors->first('cdn_url') }}</strong>
</span>
@endif
</div>
</div>
</div>

View File

@ -48,6 +48,112 @@
</label>
</div>
@if ($storage->source == 'OpenStackSource')
<hr/>
<div class="row">
<div class="col-md-6">
<div class="form-group{{ $errors->has('auth_url') ? ' has-error' : '' }}">
{!! Form::label('auth_url', trans('forms.storage_auth_url_label'), ['class' => 'control-label']) !!}
{!! Form::text('auth_url', old('auth_url'), ['class' => 'form-control']) !!}
@if ($errors->has('auth_url'))
<span class="help-block">
<strong>{{ $errors->first('auth_url') }}</strong>
</span>
@endif
</div>
</div>
<div class="col-md-6">
<div class="form-group{{ $errors->has('tenant_name') ? ' has-error' : '' }}">
{!! Form::label('tenant_name', trans('forms.storage_tenant_name_label'), ['class' => 'control-label']) !!}
{!! Form::text('tenant_name', old('tenant_name'), ['class' => 'form-control']) !!}
@if ($errors->has('tenant_name'))
<span class="help-block">
<strong>{{ $errors->first('tenant_name') }}</strong>
</span>
@endif
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group{{ $errors->has('username') ? ' has-error' : '' }}">
{!! Form::label('username', trans('forms.username_label'), ['class' => 'control-label']) !!}
{!! Form::text('username', old('username'), ['class' => 'form-control']) !!}
@if ($errors->has('username'))
<span class="help-block">
<strong>{{ $errors->first('username') }}</strong>
</span>
@endif
</div>
</div>
<div class="col-md-6">
<div class="form-group{{ $errors->has('password') ? ' has-error' : '' }}">
{!! Form::label('password', trans('forms.password_label'), ['class' => 'control-label']) !!}
{!! Form::text('password', old('password'), ['class' => 'form-control']) !!}
@if ($errors->has('password'))
<span class="help-block">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group{{ $errors->has('service_name') ? ' has-error' : '' }}">
{!! Form::label('service_name', trans('forms.storage_service_name_label'), ['class' => 'control-label']) !!}
{!! Form::text('service_name', old('service_name'), ['class' => 'form-control']) !!}
@if ($errors->has('service_name'))
<span class="help-block">
<strong>{{ $errors->first('service_name') }}</strong>
</span>
@endif
</div>
</div>
<div class="col-md-6">
<div class="form-group{{ $errors->has('service_region') ? ' has-error' : '' }}">
{!! Form::label('service_region', trans('forms.storage_service_region_label'), ['class' => 'control-label']) !!}
{!! Form::text('service_region', old('service_region'), ['class' => 'form-control']) !!}
@if ($errors->has('service_region'))
<span class="help-block">
<strong>{{ $errors->first('service_region') }}</strong>
</span>
@endif
</div>
</div>
</div>
<div class="form-group{{ $errors->has('container_name') ? ' has-error' : '' }}">
{!! Form::label('container_name', trans('forms.storage_container_name_label'), ['class' => 'control-label']) !!}
{!! Form::text('container_name', old('container_name'), ['class' => 'form-control']) !!}
@if ($errors->has('container_name'))
<span class="help-block">
<strong>{{ $errors->first('container_name') }}</strong>
</span>
@endif
</div>
<div class="form-group{{ $errors->has('cdn_url') ? ' has-error' : '' }}">
{!! Form::label('cdn_url', trans('forms.storage_cdn_url_label'), ['class' => 'control-label']) !!}
{!! Form::text('cdn_url', old('cdn_url'), ['class' => 'form-control']) !!}
@if ($errors->has('cdn_url'))
<span class="help-block">
<strong>{{ $errors->first('cdn_url') }}</strong>
</span>
@endif
</div>
@endif
<div class="form-actions">
<a href="{{ route('storage.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a>
{!! Form::submit(trans('forms.save_action'), ['class' => 'btn btn-success']) !!}

View File

@ -78,6 +78,14 @@
<script src="themes/base/js/knockout.min.js?v={{ urlencode(config('app.version')) }}"></script>
<script src="themes/base/js/app.js?v={{ urlencode(config('app.version')) }}"></script>
<script type="text/javascript">
function _bt_showLoadingModal() {
bootbox.dialog({
closeButton: false,
message: '<center><img src="{{ asset('ripple.svg') }}"/></center>',
title: '@lang('global.please_wait')'
});
}
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')