Dropbox #106 - files can be uploaded to a Dropbox account using a generated access token, and downloaded using the Blue Twilight download endpoint.
This commit is contained in:
parent
da99b0b05a
commit
582e5fffaa
118
app/AlbumSources/DropboxSource.php
Normal file
118
app/AlbumSources/DropboxSource.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\AlbumSources;
|
||||
|
||||
use App\Photo;
|
||||
use App\Services\DropboxService;
|
||||
use Guzzle\Http\EntityBody;
|
||||
|
||||
class DropboxSource extends AlbumSourceBase implements IAlbumSource
|
||||
{
|
||||
/**
|
||||
* @var DropboxService
|
||||
*/
|
||||
private $dropboxClient;
|
||||
|
||||
/**
|
||||
* Deletes an entire album's media contents.
|
||||
* @return void
|
||||
*/
|
||||
public function deleteAlbumContents()
|
||||
{
|
||||
// TODO: Implement deleteAlbumContents() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a thumbnail file for a photo.
|
||||
* @param Photo $photo Photo to delete the thumbnail from.
|
||||
* @param string $thumbnail Thumbnail to delete (or null to delete the original.)
|
||||
* @return void
|
||||
*/
|
||||
public function deleteThumbnail(Photo $photo, $thumbnail = null)
|
||||
{
|
||||
// TODO: Implement deleteThumbnail() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
|
||||
|
||||
return EntityBody::fromString($this->getClient()->downloadFile($pathOnStorage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of this album source.
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'global.album_sources.dropbox';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the absolute URL to the given photo file.
|
||||
* @param Photo $photo Photo to get the URL to.
|
||||
* @param string $thumbnail Thumbnail to get the image to.
|
||||
* @return string
|
||||
*/
|
||||
public function getUrlToPhoto(Photo $photo, $thumbnail = null)
|
||||
{
|
||||
$photoUrl = route('downloadPhoto', [
|
||||
'albumUrlAlias' => $this->album->url_path,
|
||||
'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 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 = null)
|
||||
{
|
||||
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
|
||||
|
||||
$this->getClient()->uploadFile($tempFilename, $pathOnStorage);
|
||||
}
|
||||
|
||||
private function getClient()
|
||||
{
|
||||
if (is_null($this->dropboxClient))
|
||||
{
|
||||
$this->dropboxClient = new DropboxService();
|
||||
$this->dropboxClient->setAccessToken(decrypt($this->configuration->access_token));
|
||||
}
|
||||
|
||||
return $this->dropboxClient;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ namespace App\Helpers;
|
||||
|
||||
use App\AlbumSources\AmazonS3Source;
|
||||
use App\AlbumSources\BackblazeB2Source;
|
||||
use App\AlbumSources\DropboxSource;
|
||||
use App\AlbumSources\IAlbumSource;
|
||||
use App\AlbumSources\LocalFilesystemSource;
|
||||
use App\AlbumSources\OpenStackSource;
|
||||
@ -48,6 +49,7 @@ class ConfigHelper
|
||||
LocalFilesystemSource::class,
|
||||
AmazonS3Source::class,
|
||||
BackblazeB2Source::class,
|
||||
DropboxSource::class,
|
||||
OpenStackSource::class,
|
||||
RackspaceSource::class
|
||||
];
|
||||
|
@ -23,7 +23,7 @@ class StorageController extends Controller
|
||||
$this->middleware('auth');
|
||||
View::share('is_admin', true);
|
||||
|
||||
$this->encryptedFields = ['password', 'access_key', 'secret_key'];
|
||||
$this->encryptedFields = ['password', 'access_key', 'secret_key', 'access_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,7 +89,8 @@ class StorageController extends Controller
|
||||
'cdn_url',
|
||||
'access_key',
|
||||
'secret_key',
|
||||
'b2_bucket_type'
|
||||
'b2_bucket_type',
|
||||
'access_token'
|
||||
]));
|
||||
$storage->is_active = true;
|
||||
$storage->is_default = (strtolower($request->get('is_default')) == 'on');
|
||||
@ -219,7 +220,8 @@ class StorageController extends Controller
|
||||
'cdn_url',
|
||||
'access_key',
|
||||
'secret_key',
|
||||
'b2_bucket_type'
|
||||
'b2_bucket_type',
|
||||
'access_token'
|
||||
]));
|
||||
$storage->is_active = (strtolower($request->get('is_active')) == 'on');
|
||||
$storage->is_default = (strtolower($request->get('is_default')) == 'on');
|
||||
|
@ -65,6 +65,18 @@ class StoreStorageRequest extends FormRequest
|
||||
$result['service_region'] = 'sometimes|required';
|
||||
$result['container_name'] = 'sometimes|required';
|
||||
break;
|
||||
|
||||
case 'BackblazeB2Source':
|
||||
$result['access_key'] = 'sometimes|required';
|
||||
$result['secret_key'] = 'sometimes|required';
|
||||
$result['container_name'] = 'sometimes|required';
|
||||
break;
|
||||
|
||||
case 'DropboxSource':
|
||||
$result['access_key'] = 'sometimes|required';
|
||||
$result['secret_key'] = 'sometimes|required';
|
||||
$result['access_token'] = 'sometimes|required';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
@ -103,6 +115,18 @@ class StoreStorageRequest extends FormRequest
|
||||
$result['service_region'] = 'sometimes|required';
|
||||
$result['container_name'] = 'sometimes|required';
|
||||
break;
|
||||
|
||||
case 'BackblazeB2Source':
|
||||
$result['access_key'] = 'sometimes|required';
|
||||
$result['secret_key'] = 'sometimes|required';
|
||||
$result['container_name'] = 'sometimes|required';
|
||||
break;
|
||||
|
||||
case 'DropboxSource':
|
||||
$result['access_key'] = 'sometimes|required';
|
||||
$result['secret_key'] = 'sometimes|required';
|
||||
$result['access_token'] = 'sometimes|required';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
157
app/Services/DropboxService.php
Normal file
157
app/Services/DropboxService.php
Normal file
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DropboxService
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $accessToken;
|
||||
|
||||
/**
|
||||
* Configuration related to the Backblaze B2 service.
|
||||
* @var \Illuminate\Config\Repository|mixed
|
||||
*/
|
||||
private $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = config('services.dropbox');
|
||||
}
|
||||
|
||||
public function downloadFile($pathOnStorage)
|
||||
{
|
||||
$dropboxArgs = ['path' => $pathOnStorage];
|
||||
|
||||
return $this->sendRequest(
|
||||
$this->config['download_url'],
|
||||
'POST',
|
||||
null,
|
||||
[
|
||||
'http_headers' => [
|
||||
sprintf('Dropbox-API-Arg: %s', json_encode($dropboxArgs)),
|
||||
'Content-Type: application/octet-stream'
|
||||
],
|
||||
'post_body_is_json' => false,
|
||||
'response_body_is_json' => false
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*/
|
||||
public function setAccessToken(string $accessToken)
|
||||
{
|
||||
$this->accessToken = $accessToken;
|
||||
}
|
||||
|
||||
public function uploadFile($pathToFileToUpload, $pathToStorage)
|
||||
{
|
||||
$dropboxArgs = [
|
||||
'path' => $pathToStorage,
|
||||
'mode' => 'add',
|
||||
'mute' => true
|
||||
];
|
||||
|
||||
$uploadResult = $this->sendRequest(
|
||||
$this->config['upload_url'],
|
||||
'POST',
|
||||
file_get_contents($pathToFileToUpload),
|
||||
[
|
||||
'http_headers' => [
|
||||
sprintf('Dropbox-API-Arg: %s', json_encode($dropboxArgs)),
|
||||
'Content-Type: application/octet-stream'
|
||||
],
|
||||
'post_body_is_json' => false
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function getBasicHttpClient($url, $method = 'GET', array $httpHeaders = [])
|
||||
{
|
||||
$httpHeaders = array_merge(
|
||||
[
|
||||
'Accept: application/json',
|
||||
sprintf('Authorization: Bearer %s', $this->accessToken)
|
||||
],
|
||||
$httpHeaders
|
||||
);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
switch (strtoupper($method))
|
||||
{
|
||||
case 'GET':
|
||||
curl_setopt($ch, CURLOPT_HTTPGET, true);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
break;
|
||||
}
|
||||
|
||||
return $ch;
|
||||
}
|
||||
|
||||
private function sendRequest($url, $method = 'GET', $postData = null, array $postOptions = [])
|
||||
{
|
||||
$postOptions = array_merge(
|
||||
[
|
||||
'http_headers' => [],
|
||||
'post_body_is_json' => true,
|
||||
'response_body_is_json' => true
|
||||
],
|
||||
$postOptions
|
||||
);
|
||||
$httpHeaders = $postOptions['http_headers'];
|
||||
|
||||
$ch = $this->getBasicHttpClient($url, $method, $httpHeaders);
|
||||
|
||||
if (!is_null($postData))
|
||||
{
|
||||
if ($postOptions['post_body_is_json'])
|
||||
{
|
||||
$postData = json_encode($postData);
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
|
||||
}
|
||||
|
||||
Log::info(sprintf('%s: %s', strtoupper($method), $url));
|
||||
Log::debug('HTTP headers:', $httpHeaders);
|
||||
|
||||
// Only log a post body if we have one and it's in JSON format (i.e. not a file upload)
|
||||
if (!is_null($postData) && $postOptions['post_body_is_json'])
|
||||
{
|
||||
Log::debug($postData);
|
||||
}
|
||||
|
||||
$result = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
Log::info(sprintf('Received HTTP code %d', $httpCode));
|
||||
|
||||
// Only log a result if we have one and it's in JSON format (i.e. not a file download)
|
||||
if (!is_null($result) && $result !== false && $postOptions['response_body_is_json'])
|
||||
{
|
||||
Log::debug($result);
|
||||
}
|
||||
|
||||
if ($httpCode != 200 && $httpCode != 304)
|
||||
{
|
||||
throw new \Exception(sprintf('Exception from Dropbox: %s', $result));
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return $postOptions['response_body_is_json']
|
||||
? json_decode($result)
|
||||
: $result;
|
||||
}
|
||||
}
|
@ -31,7 +31,8 @@ class Storage extends Model
|
||||
'cdn_url',
|
||||
'access_key',
|
||||
'secret_key',
|
||||
'b2_bucket_type'
|
||||
'b2_bucket_type',
|
||||
'access_token'
|
||||
];
|
||||
|
||||
public function albums()
|
||||
|
@ -19,6 +19,11 @@ return [
|
||||
'download_token_lifetime' => 300
|
||||
],
|
||||
|
||||
'dropbox' => [
|
||||
'download_url' => 'https://content.dropboxapi.com/2/files/download',
|
||||
'upload_url' => 'https://content.dropboxapi.com/2/files/upload'
|
||||
],
|
||||
|
||||
'gitea' => [
|
||||
'api_url' => 'https://apps.andysh.uk/api/v1',
|
||||
'cache_time_seconds' => 3600,
|
||||
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddStorageAccessTokenColumn extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('storages', function (Blueprint $table) {
|
||||
$table->text('access_token')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('storages', function (Blueprint $table) {
|
||||
$table->dropColumn('access_token');
|
||||
});
|
||||
}
|
||||
}
|
@ -96,10 +96,12 @@ return [
|
||||
'settings_social_user_profiles' => 'Enable public user profiles',
|
||||
'settings_social_user_profiles_help' => 'Display public pages for users showing their albums, cameras used and activity.',
|
||||
'storage_access_key_label' => 'Access key:',
|
||||
'storage_access_token_label' => 'Access token:',
|
||||
'storage_active_label' => 'Location is active. Uncheck to prevent creating new albums in this location.',
|
||||
'storage_api_key_label' => 'API key:',
|
||||
'storage_application_key_id_label' => 'Application key ID:',
|
||||
'storage_application_key_label' => 'Application key:',
|
||||
'storage_application_secret_label' => 'Application secret:',
|
||||
'storage_auth_url_label' => 'Authentication URL:',
|
||||
'storage_b2_bucket_type' => [
|
||||
'autodetect' => 'Auto-detect',
|
||||
|
@ -3,6 +3,7 @@ return [
|
||||
'album_sources' => [
|
||||
'amazon_s3' => 'Amazon S3 (or S3-compatible)',
|
||||
'backblaze_b2' => 'Backblaze B2 Cloud',
|
||||
'dropbox' => 'Dropbox',
|
||||
'filesystem' => 'Local filesystem',
|
||||
'openstack' => 'OpenStack cloud storage',
|
||||
'rackspace' => 'Rackspace cloud storage'
|
||||
|
@ -67,6 +67,10 @@
|
||||
@include(Theme::viewName('partials.admin_storages_backblaze_b2_options'))
|
||||
</div>
|
||||
|
||||
<div v-if="storage_driver == 'DropboxSource'">
|
||||
@include(Theme::viewName('partials.admin_storages_dropbox_options'))
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is-default" name="is_default"@if (old('is_default', $storage->is_default)) checked="checked"@endif>
|
||||
<label class="form-check-label" for="is-default">@lang('forms.default_storage_label')</label>
|
||||
|
@ -61,10 +61,15 @@
|
||||
@include(Theme::viewName('partials.admin_storages_rackspace_options'))
|
||||
@endif
|
||||
|
||||
<div v-if="storage_driver == 'BackblazeB2Source'">
|
||||
@if ($storage->source == 'BackblazeB2Source')
|
||||
<hr/>
|
||||
@include(Theme::viewName('partials.admin_storages_backblaze_b2_options'))
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($storage->source == 'DropboxSource')
|
||||
<hr/>
|
||||
@include(Theme::viewName('partials.admin_storages_dropbox_options'))
|
||||
@endif
|
||||
|
||||
<div class="text-right">
|
||||
<a href="{{ route('storage.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a>
|
||||
|
@ -0,0 +1,41 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="form-control-label" for="access-key">@lang('forms.storage_application_key_label')</label>
|
||||
<input type="text" class="form-control{{ $errors->has('access_key') ? ' is-invalid' : '' }}" id="access-key" name="access_key" value="{{ old('access_key', $storage->access_key) }}">
|
||||
|
||||
@if ($errors->has('access_key'))
|
||||
<div class="invalid-feedback">
|
||||
<strong>{{ $errors->first('access_key') }}</strong>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="form-control-label" for="secret-key">@lang('forms.storage_application_secret_label')</label>
|
||||
<input type="text" class="form-control{{ $errors->has('secret_key') ? ' is-invalid' : '' }}" id="secret-key" name="secret_key" value="{{ old('secret_key', $storage->secret_key) }}">
|
||||
|
||||
@if ($errors->has('secret_key'))
|
||||
<div class="invalid-feedback">
|
||||
<strong>{{ $errors->first('secret_key') }}</strong>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="form-control-label" for="access-token">@lang('forms.storage_access_token_label')</label>
|
||||
<input type="text" class="form-control{{ $errors->has('access_token') ? ' is-invalid' : '' }}" id="access-token" name="access_token" value="{{ old('access_token', $storage->access_token) }}">
|
||||
|
||||
@if ($errors->has('access_key'))
|
||||
<div class="invalid-feedback">
|
||||
<strong>{{ $errors->first('access_token') }}</strong>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in New Issue
Block a user