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\Album;
use App\Photo; use App\Photo;
use App\Storage; use App\Storage;
use Guzzle\Http\EntityBody;
use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\File;
interface IAlbumSource interface IAlbumSource
@ -23,20 +24,20 @@ interface IAlbumSource
*/ */
function deleteThumbnail(Photo $photo, $thumbnail = null); 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. * Gets the name of this album source.
* @return string * @return string
*/ */
function getName(); 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. * Gets the absolute URL to the given photo file.
* @param Photo $photo Photo to get the URL to. * @param Photo $photo Photo to get the URL to.
@ -45,30 +46,14 @@ interface IAlbumSource
*/ */
function getUrlToPhoto(Photo $photo, $thumbnail = null); 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. * Saves a generated thumbnail to its permanent location.
* @param Photo $photo Photo the thumbnail relates to. * @param Photo $photo Photo the image relates to.
* @param string $thumbnailInfo Information about the thumbnail. * @param string $tempFilename Filename containing the image.
* @param string $tempFilename Filename containing the thumbnail. * @param string $thumbnail Name of the thumbnail (or null for the original.)
* @return mixed * @return mixed
*/ */
function saveThumbnail(Photo $photo, $thumbnailInfo, $tempFilename); function saveThumbnail(Photo $photo, $tempFilename, $thumbnail = null);
/**
* 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);
/** /**
* @param Album $album * @param Album $album

View File

@ -6,6 +6,7 @@ use App\Album;
use App\Helpers\MiscHelper; use App\Helpers\MiscHelper;
use App\Photo; use App\Photo;
use App\Services\PhotoService; use App\Services\PhotoService;
use Guzzle\Http\EntityBody;
use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\File;
/** /**
@ -24,12 +25,33 @@ class LocalFilesystemSource extends AlbumSourceBase implements IAlbumSource
public function deleteThumbnail(Photo $photo, $thumbnail = null) 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() public function getName()
@ -37,16 +59,6 @@ class LocalFilesystemSource extends AlbumSourceBase implements IAlbumSource
return 'global.album_sources.filesystem'; 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) public function getUrlToPhoto(Photo $photo, $thumbnail = null)
{ {
$photoUrl = route('downloadPhoto', [ $photoUrl = route('downloadPhoto', [
@ -62,38 +74,21 @@ class LocalFilesystemSource extends AlbumSourceBase implements IAlbumSource
return $photoUrl; return $photoUrl;
} }
public function saveOriginal(Photo $photo, $tempFilename) public function saveThumbnail(Photo $photo, $tempFilename, $thumbnail = null)
{
$this->saveThumbnail($photo, $tempFilename, $this->getOriginalsFolder());
}
public function saveThumbnail(Photo $photo, $tempFilename, $thumbnail)
{ {
$fileInfo = new File($tempFilename); $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( return '_originals';
'%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);
} }
private function getPathToAlbum() private function getPathToAlbum()

View File

@ -2,6 +2,10 @@
namespace App\AlbumSources; namespace App\AlbumSources;
use App\Photo; 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 OpenCloud\OpenStack;
use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\File;
@ -17,7 +21,8 @@ class OpenStackSource extends AlbumSourceBase implements IAlbumSource
*/ */
public function deleteAlbumContents() 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) 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'; 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. * Gets the absolute URL to the given photo file.
* @param Photo $photo Photo to get the URL to. * @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) public function getUrlToPhoto(Photo $photo, $thumbnail = null)
{ {
return sprintf( $cdnUrl = $this->configuration->cdn_url;
'%s/%s/%s/%s', if (strlen($cdnUrl) > 0)
'https://photos-dev-898b0644.cdn.memsites.com', {
$this->album->url_alias, if (substr($cdnUrl, strlen($cdnUrl) - 1, 1) == '/')
is_null($thumbnail) ? $this->getOriginalsFolder() : $thumbnail, {
$photo->storage_file_name $cdnUrl = substr($cdnUrl, 0, strlen($cdnUrl) - 1);
); }
}
public function saveOriginal(Photo $photo, $tempFilename) return sprintf('%s/%s', $cdnUrl, $this->getPathToPhoto($photo, $thumbnail));
{ }
$this->saveThumbnail($photo, $tempFilename, $this->getOriginalsFolder());
// 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. * Saves a generated thumbnail to its permanent location.
* @param Photo $photo Photo the thumbnail relates to. * @param Photo $photo Photo the image relates to.
* @param string $tempFilename Filename containing the thumbnail. * @param string $tempFilename Filename containing the image.
* @param string $thumbnail Name of the thumbnail. * @param string $thumbnail Name of the thumbnail (or null for the original.)
* @return mixed * @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 = $this->getContainer();
$container->uploadObject( $container->uploadObject($photoPath, fopen($tempFilename, 'r+'));
sprintf('%s/%s/%s', $this->album->url_alias, $thumbnail, $photo->storage_file_name),
fopen($tempFilename, 'r+')
);
} }
/** protected function getClient()
* 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()
{ {
return new OpenStack($this->configuration->auth_url, [ return new OpenStack($this->configuration->auth_url, [
'username' => $this->configuration->username, '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); return $this->getStorageService()->getContainer($this->configuration->container_name);
} }
private function getOriginalsFolder() protected function getStorageService()
{
return '_originals';
}
private function getStorageService()
{ {
return $this->getClient()->objectStoreService( return $this->getClient()->objectStoreService(
$this->configuration->service_name, $this->configuration->service_name,
@ -127,4 +138,19 @@ class OpenStackSource extends AlbumSourceBase implements IAlbumSource
'publicURL' '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'); $this->authorize('admin-access');
settype($direction, 'boolean'); settype($horizontal, 'boolean');
settype($vertical, 'boolean');
$photo = Photo::where('id', intval($photoId))->first(); $photo = Photo::where('id', intval($photoId))->first();
if (is_null($photo)) if (is_null($photo))
@ -407,9 +408,7 @@ class PhotoController extends Controller
return null; return null;
} }
$numberChanged = 0; if ($request->has('bulk-action'))
if ($request->has('bulk-apply'))
{ {
$numberChanged = $this->applyBulkActions($request, $album); $numberChanged = $this->applyBulkActions($request, $album);
} }
@ -445,6 +444,7 @@ class PhotoController extends Controller
} }
$photoService = new PhotoService($photo); $photoService = new PhotoService($photo);
$doNotSave = false;
switch (strtolower($action)) switch (strtolower($action))
{ {
case 'change_album': case 'change_album':
@ -466,6 +466,7 @@ class PhotoController extends Controller
case 'delete': case 'delete':
$photoService->delete(); $photoService->delete();
$doNotSave = true;
break; break;
case 'flip_both': case 'flip_both':
@ -493,7 +494,10 @@ class PhotoController extends Controller
break; break;
} }
$photo->save(); if (!$doNotSave)
{
$photo->save();
}
$numberChanged++; $numberChanged++;
} }

View File

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

View File

@ -11,6 +11,7 @@ use app\Http\Controllers\Admin\AlbumController;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Middleware\VerifyCsrfToken; use App\Http\Middleware\VerifyCsrfToken;
use App\Photo; use App\Photo;
use Guzzle\Http\Mimetypes;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -53,10 +54,23 @@ class PhotoController extends Controller
$thumbnail = $request->get('t'); $thumbnail = $request->get('t');
if (is_null($thumbnail)) 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) public function show(Request $request, $albumUrlAlias, $photoFilename)

View File

@ -24,6 +24,8 @@ class StoreStorageRequest extends FormRequest
*/ */
public function rules() public function rules()
{ {
$result = [];
switch ($this->method()) switch ($this->method())
{ {
case 'POST': case 'POST':
@ -46,19 +48,35 @@ class StoreStorageRequest extends FormRequest
$result['service_name'] = 'sometimes|required'; $result['service_name'] = 'sometimes|required';
$result['service_region'] = 'sometimes|required'; $result['service_region'] = 'sometimes|required';
$result['container_name'] = 'sometimes|required'; $result['container_name'] = 'sometimes|required';
$result['cdn_url'] = 'sometimes|url';
break; break;
} }
break;
return $result;
case 'PATCH': case 'PATCH':
case 'PUT': case 'PUT':
$storageId = intval($this->segment(3)); $storageId = intval($this->segment(3));
$storage = Storage::find($storageId);
return [ $result = [
'name' => 'required|max:255|unique:storages,name,' . $storageId '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(); $this->photo->save();
// Save the original // Save the original
$this->albumSource->saveOriginal($this->photo, $photoFile); $this->albumSource->saveThumbnail($this->photo, $photoFile);
$this->regenerateThumbnails($originalPhotoResource); $this->regenerateThumbnails($originalPhotoResource);
} }
@ -128,11 +128,11 @@ class PhotoService
$currentSource = $this->photo->album->getAlbumSource(); $currentSource = $this->photo->album->getAlbumSource();
$newSource = $newAlbum->getAlbumSource(); $newSource = $newAlbum->getAlbumSource();
// Get the current photo's path // First export the original photo from the storage provider
$file = new File($currentSource->getPathToPhoto($this->photo)); $photoPath = $this->downloadToTemporaryFolder();
// Save to the new album // Save to the new album
$newSource->saveUploadedPhoto($file, $this->photo->storage_file_name); $newSource->saveThumbnail($this->photo, $photoPath);
// Delete the original // Delete the original
$this->delete(); $this->delete();
@ -169,25 +169,39 @@ class PhotoService
public function flip($horizontal, $vertical) public function flip($horizontal, $vertical)
{ {
// First export the original photo from the storage provider
$photoPath = $this->downloadToTemporaryFolder();
$imageInfo = array(); $imageInfo = array();
$photoPath = $this->albumSource->getPathToPhoto($this->photo);
$originalPhotoImage = $this->imageHelper->openImage($photoPath, $imageInfo); $originalPhotoImage = $this->imageHelper->openImage($photoPath, $imageInfo);
if ($this->imageHelper->flipImage($originalPhotoImage, boolval($horizontal), boolval($vertical))) if ($this->imageHelper->flipImage($originalPhotoImage, boolval($horizontal), boolval($vertical)))
{ {
$this->imageHelper->saveImage($originalPhotoImage, $photoPath, $imageInfo); $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->regenerateThumbnails($originalPhotoImage);
$this->photo->save(); $this->photo->save();
} }
// Remove the temp file
@unlink($photoPath);
} }
public function regenerateThumbnails($originalPhotoResource = null) public function regenerateThumbnails($originalPhotoResource = null)
{ {
$photoPath = null;
if (is_null($originalPhotoResource)) if (is_null($originalPhotoResource))
{ {
// First export the original photo from the storage provider
$photoPath = $this->downloadToTemporaryFolder();
$imageInfo = null; $imageInfo = null;
$originalPhotoResource = $this->imageHelper->openImage($this->albumSource->getPathToPhoto($this->photo), $imageInfo); $originalPhotoResource = $this->imageHelper->openImage($photoPath, $imageInfo);
} }
// Generate and save thumbnails // Generate and save thumbnails
@ -200,16 +214,28 @@ class PhotoService
$generatedThumbnailPath = $this->imageHelper->generateThumbnail($originalPhotoResource, $this->photo, $thumbnail); $generatedThumbnailPath = $this->imageHelper->generateThumbnail($originalPhotoResource, $this->photo, $thumbnail);
$this->albumSource->saveThumbnail($this->photo, $generatedThumbnailPath, $thumbnail['name']); $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) public function rotate($angle)
{ {
$imageInfo = array(); $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->openImage($photoPath, $imageInfo);
$originalPhotoImage = $this->imageHelper->rotateImage($originalPhotoImage, intval($angle)); $originalPhotoImage = $this->imageHelper->rotateImage($originalPhotoImage, intval($angle));
$this->imageHelper->saveImage($originalPhotoImage, $photoPath, $imageInfo); $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) if ($angle == 90 || $angle == 270)
{ {
$width = $this->photo->width; $width = $this->photo->width;
@ -220,6 +246,27 @@ class PhotoService
$this->regenerateThumbnails($originalPhotoImage); $this->regenerateThumbnails($originalPhotoImage);
$this->photo->save(); $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) private function metadataCameraMake(array $exifData)

View File

@ -27,7 +27,8 @@ class Storage extends Model
'password', 'password',
'service_name', 'service_name',
'service_region', 'service_region',
'container_name' 'container_name',
'cdn_url'
]; ];
public function albums() 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.albums = ko.observableArray();
self.bulkModifyMethod = ko.observable(); self.bulkModifyMethod = ko.observable();
self.photoIDs = ko.observableArray(); self.photoIDs = ko.observableArray();
self.isSubmitting = false;
/* Called when the Apply button on the "bulk apply selected actions" form is clicked */ /* Called when the Apply button on the "bulk apply selected actions" form is clicked */
self.bulkModifySelected = function() self.bulkModifySelected = function()
{ {
if (self.isSubmitting)
{
return true;
}
var bulk_form = $('form#bulk-modify-form'); var bulk_form = $('form#bulk-modify-form');
if (self.bulkModifyMethod() == 'change_album') if (self.bulkModifyMethod() == 'change_album')
@ -104,8 +110,11 @@ function EditPhotosViewModel(album_id, language, urls) {
// Prompt for the new album to move to // Prompt for the new album to move to
self.promptForNewAlbum(function(dialog) { self.promptForNewAlbum(function(dialog) {
var album_id = $('select', dialog).val(); var album_id = $('select', dialog).val();
$('input[name="new-album-id"]', form).val(album_id); $('input[name="new-album-id"]', bulk_form).val(album_id);
bulk_form.submit();
self.isSubmitting = true;
$('button[name="bulk-apply"]', bulk_form).click();
_bt_showLoadingModal();
}); });
return false; return false;
@ -125,7 +134,9 @@ function EditPhotosViewModel(album_id, language, urls) {
label: language.action_delete, label: language.action_delete,
className: "btn-danger", className: "btn-danger",
callback: function() { 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(); window.location.reload();
}); });
_bt_showLoadingModal();
}); });
return false; 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.', '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_active_label' => 'Location is active. Uncheck to prevent creating new albums in this location.',
'storage_auth_url_label' => 'Authentication URL:', 'storage_auth_url_label' => 'Authentication URL:',
'storage_cdn_url_label' => 'Public CDN URL (if supported and enabled):',
'storage_container_name_label' => 'Container name:', 'storage_container_name_label' => 'Container name:',
'storage_driver_label' => 'Storage driver:', 'storage_driver_label' => 'Storage driver:',
'storage_location_label' => 'Physical location:', 'storage_location_label' => 'Physical location:',

View File

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

View File

@ -145,6 +145,17 @@
</span> </span>
@endif @endif
</div> </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>
</div> </div>

View File

@ -48,6 +48,112 @@
</label> </label>
</div> </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"> <div class="form-actions">
<a href="{{ route('storage.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a> <a href="{{ route('storage.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a>
{!! Form::submit(trans('forms.save_action'), ['class' => 'btn btn-success']) !!} {!! 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/knockout.min.js?v={{ urlencode(config('app.version')) }}"></script>
<script src="themes/base/js/app.js?v={{ urlencode(config('app.version')) }}"></script> <script src="themes/base/js/app.js?v={{ urlencode(config('app.version')) }}"></script>
<script type="text/javascript"> <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({ $.ajaxSetup({
headers: { headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')