Implemented a new option for S3 sources to allow signed URLs and private buckets to be used. #141

This commit is contained in:
Andy Heathershaw 2020-04-18 21:51:28 +01:00
parent 93c6f5da10
commit f773b10244
11 changed files with 107 additions and 12 deletions

View File

@ -42,6 +42,11 @@ abstract class AlbumSourceBase
return self::$albumSourceCache[$fullClassName]; return self::$albumSourceCache[$fullClassName];
} }
public function getConfiguration()
{
return $this->configuration;
}
public function setAlbum(Album $album) public function setAlbum(Album $album)
{ {
$this->album = $album; $this->album = $album;

View File

@ -130,7 +130,19 @@ class AmazonS3Source extends AlbumSourceBase implements IAlbumSource, IAnalysisQ
*/ */
public function getUrlToPhoto(Photo $photo, $thumbnail = null) public function getUrlToPhoto(Photo $photo, $thumbnail = null)
{ {
return $this->getClient()->getObjectUrl($this->configuration->container_name, $this->getPathToPhoto($photo, $thumbnail)); $client = $this->getClient();
if ($this->configuration->s3_signed_urls)
{
$cmd = $client->getCommand('GetObject', [
'Bucket' => $this->configuration->container_name,
'Key' => $this->getPathToPhoto($photo, $thumbnail)
]);
return $client->createPresignedRequest($cmd, '+5 minutes')->getUri();
}
return $client->getObjectUrl($this->configuration->container_name, $this->getPathToPhoto($photo, $thumbnail));
} }
/** /**
@ -144,7 +156,11 @@ class AmazonS3Source extends AlbumSourceBase implements IAlbumSource, IAnalysisQ
{ {
$photoPath = $this->getPathToPhoto($photo, $thumbnail); $photoPath = $this->getPathToPhoto($photo, $thumbnail);
$this->getClient()->upload($this->configuration->container_name, $photoPath, fopen($tempFilename, 'r+'), 'public-read'); $uploadAcl = $this->configuration->s3_signed_urls
? 'private'
: 'public-read';
$this->getClient()->upload($this->configuration->container_name, $photoPath, fopen($tempFilename, 'r+'), $uploadAcl);
} }
/** /**

View File

@ -6,7 +6,6 @@ use App\Album;
use App\Photo; use App\Photo;
use App\Storage; use App\Storage;
use Guzzle\Http\EntityBody; use Guzzle\Http\EntityBody;
use Symfony\Component\HttpFoundation\File\File;
interface IAlbumSource interface IAlbumSource
{ {
@ -32,6 +31,12 @@ interface IAlbumSource
*/ */
function fetchPhotoContent(Photo $photo, $thumbnail = null); function fetchPhotoContent(Photo $photo, $thumbnail = null);
/**
* Gets the configuration of the source.
* @return mixed
*/
function getConfiguration();
/** /**
* Gets the name of this album source. * Gets the name of this album source.
* @return string * @return string

View File

@ -55,12 +55,14 @@ class StorageController extends Controller
$this->authorizeAccessToAdminPanel('admin:manage-storage'); $this->authorizeAccessToAdminPanel('admin:manage-storage');
$filesystemDefaultLocation = sprintf('%s/storage/app/albums', dirname(dirname(dirname(dirname(__DIR__))))); $filesystemDefaultLocation = sprintf('%s/storage/app/albums', dirname(dirname(dirname(dirname(__DIR__)))));
$storage = new Storage();
$storage->s3_signed_urls = true;
return Theme::render('admin.create_storage', [ return Theme::render('admin.create_storage', [
'album_sources' => UserConfig::albumSources(), 'album_sources' => UserConfig::albumSources(),
'filesystem_default_location' => $filesystemDefaultLocation, 'filesystem_default_location' => $filesystemDefaultLocation,
'info' => $request->session()->get('info'), 'info' => $request->session()->get('info'),
'storage' => new Storage() 'storage' => $storage
]); ]);
} }
@ -94,6 +96,7 @@ class StorageController extends Controller
$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');
$storage->is_internal = false; $storage->is_internal = false;
$storage->s3_signed_urls = (strtolower($request->get('s3_signed_urls')) == 'on');
if ($storage->source != 'LocalFilesystemSource' && isset($storage->location)) if ($storage->source != 'LocalFilesystemSource' && isset($storage->location))
{ {
@ -219,10 +222,12 @@ class StorageController extends Controller
'cdn_url', 'cdn_url',
'access_key', 'access_key',
'secret_key', 'secret_key',
'b2_bucket_type' 'b2_bucket_type',
's3_signed_urls'
])); ]));
$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');
$storage->s3_signed_urls = (strtolower($request->get('s3_signed_urls')) == 'on');
if ($storage->is_default && !$storage->is_active) if ($storage->is_default && !$storage->is_active)
{ {

View File

@ -2,6 +2,7 @@
namespace App; namespace App;
use App\AlbumSources\IAlbumSource;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@ -110,9 +111,14 @@ class Photo extends Model
public function thumbnailUrl($thumbnailName = null, $cacheBust = true) public function thumbnailUrl($thumbnailName = null, $cacheBust = true)
{ {
$url = $this->album->getAlbumSource()->getUrlToPhoto($this, $thumbnailName); /** @var IAlbumSource $source */
$source = $this->album->getAlbumSource();
$sourceConfiguration = $source->getConfiguration();
if ($cacheBust) $url = $source->getUrlToPhoto($this, $thumbnailName);
// Cache busting doesn't work with S3 signed URLs
if ($cacheBust && !$sourceConfiguration->s3_signed_urls)
{ {
// Append the timestamp of the last update to avoid browser caching // Append the timestamp of the last update to avoid browser caching
$theDate = is_null($this->updated_at) ? $this->created_at : $this->updated_at; $theDate = is_null($this->updated_at) ? $this->created_at : $this->updated_at;

View File

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

View File

@ -323,7 +323,10 @@ return [
'photos' => 'photo|photos', 'photos' => 'photo|photos',
'users' => 'user|users', 'users' => 'user|users',
], ],
'storage_auth_url_label_help' => 'Leave blank to authenticate with Amazon S3. For an S3-compatible provider, enter your provider\'s authentication URL here.',
'storage_backblaze_access_key_id_help' => 'To use your account\'s master key, enter your account ID here.', 'storage_backblaze_access_key_id_help' => 'To use your account\'s master key, enter your account ID here.',
'storage_s3_signed_urls_help' => 'When enabled, Blue Twilight will upload your photos with a private ACL and will use signed URLs to display the photos to your visitors.',
'storage_s3_signed_urls_tooltip' => 'This location is set to use private images with signed URLs.',
'storage_title' => 'Storage Locations', 'storage_title' => 'Storage Locations',
'sysinfo_panel' => 'System information', 'sysinfo_panel' => 'System information',
'sysinfo_widget' => [ 'sysinfo_widget' => [

View File

@ -113,6 +113,7 @@ return [
'storage_driver_label' => 'Storage driver:', 'storage_driver_label' => 'Storage driver:',
'storage_endpoint_url_label' => 'Endpoint URL (leave blank if using Amazon):', 'storage_endpoint_url_label' => 'Endpoint URL (leave blank if using Amazon):',
'storage_location_label' => 'Physical location:', 'storage_location_label' => 'Physical location:',
'storage_s3_signed_urls' => 'Upload files privately and use signed URLs',
'storage_secret_key_label' => 'Secret key:', 'storage_secret_key_label' => 'Secret key:',
'storage_service_name_label' => 'Service name:', 'storage_service_name_label' => 'Service name:',
'storage_service_region_label' => 'Region:', 'storage_service_region_label' => 'Region:',

View File

@ -61,13 +61,13 @@
@include(Theme::viewName('partials.admin_storages_rackspace_options')) @include(Theme::viewName('partials.admin_storages_rackspace_options'))
@endif @endif
<div v-if="storage_driver == 'BackblazeB2Source'"> @if ($storage->source == 'BackblazeB2Source')
<hr/> <hr/>
@include(Theme::viewName('partials.admin_storages_backblaze_b2_options')) @include(Theme::viewName('partials.admin_storages_backblaze_b2_options'))
</div> @endif
<div class="text-right"> <div class="text-right">
<a href="{{ route('storage.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a> <a href="{{ route('storage.index') }}" class="btn btn-link">@lang('forms.cancel_action')</a>
<button type="submit" class="btn btn-success"><i class="fa fa-fw fa-check"></i> @lang('forms.save_action')</button> <button type="submit" class="btn btn-success"><i class="fa fa-fw fa-check"></i> @lang('forms.save_action')</button>
</div> </div>
</form> </form>

View File

@ -39,7 +39,12 @@
<span style="color: #888; font-style: italic;"> <span style="color: #888; font-style: italic;">
@if ($storage->source == 'LocalFilesystemSource'){{ $storage->location }}@endif @if ($storage->source == 'LocalFilesystemSource'){{ $storage->location }}@endif
@if ($storage->source == 'OpenStackSource'){{ $storage->container_name }} - {{ $storage->service_name }}, {{ $storage->service_region }}@endif @if ($storage->source == 'OpenStackSource'){{ $storage->container_name }} - {{ $storage->service_name }}, {{ $storage->service_region }}@endif
@if ($storage->source == 'AmazonS3Source'){{ $storage->container_name }} - {{ $storage->service_region }}@endif @if ($storage->source == 'AmazonS3Source')
{{ $storage->container_name }} - {{ $storage->service_region }}
@if ($storage->s3_signed_urls)
<i class="fa fa-key ml-2" data-toggle="tooltip" title="@lang('admin.storage_s3_signed_urls_tooltip')"></i>
@endif
@endif
@if ($storage->source == 'RackspaceSource'){{ $storage->container_name }} - {{ $storage->service_region }}@endif @if ($storage->source == 'RackspaceSource'){{ $storage->container_name }} - {{ $storage->service_region }}@endif
</span> </span>
</td> </td>
@ -80,4 +85,12 @@
</div> </div>
</div> </div>
</div> </div>
@endsection @endsection
@push('scripts')
<script type="text/javascript">
$(function () {
$('[data-toggle="tooltip"]').tooltip()
});
</script>
@endpush

View File

@ -55,10 +55,19 @@
<div class="form-group"> <div class="form-group">
<label class="form-control-label" for="auth-url">@lang('forms.storage_auth_url_label')</label> <label class="form-control-label" for="auth-url">@lang('forms.storage_auth_url_label')</label>
<input type="text" class="form-control{{ $errors->has('auth_url') ? ' is-invalid' : '' }}" id="auth-url" name="auth_url" value="{{ old('auth_url', $storage->auth_url) }}"> <input type="text" class="form-control{{ $errors->has('auth_url') ? ' is-invalid' : '' }}" id="auth-url" name="auth_url" value="{{ old('auth_url', $storage->auth_url) }}">
<small class="form-text text-muted">@lang('admin.storage_auth_url_label_help')</small>
@if ($errors->has('auth_url')) @if ($errors->has('auth_url'))
<div class="invalid-feedback"> <div class="invalid-feedback">
<strong>{{ $errors->first('auth_url') }}</strong> <strong>{{ $errors->first('auth_url') }}</strong>
</div> </div>
@endif @endif
</div>
<div class="form-check form-group">
<input type="checkbox" class="form-check-input" id="s3-signed-urls" name="s3_signed_urls"@if (old('s3_signed_urls', $storage->s3_signed_urls)) checked="checked"@endif>
<label for="s3-signed-urls" class="form-check-label">
<strong>@lang('forms.storage_s3_signed_urls')</strong><br>
@lang('admin.storage_s3_signed_urls_help')
</label>
</div> </div>