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:
parent
005c5eb645
commit
640828e244
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
@ -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++;
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -27,7 +27,8 @@ class Storage extends Model
|
||||
'password',
|
||||
'service_name',
|
||||
'service_region',
|
||||
'container_name'
|
||||
'container_name',
|
||||
'cdn_url'
|
||||
];
|
||||
|
||||
public function albums()
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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:',
|
||||
|
@ -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' => [
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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']) !!}
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user