Pull 135: Backblaze B2 storage driver #138
117
app/AlbumSources/BackblazeB2Source.php
Normal file
117
app/AlbumSources/BackblazeB2Source.php
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\AlbumSources;
|
||||||
|
|
||||||
|
use App\Album;
|
||||||
|
use App\Photo;
|
||||||
|
use App\Services\BackblazeB2Service;
|
||||||
|
use App\Storage;
|
||||||
|
use Guzzle\Http\EntityBody;
|
||||||
|
|
||||||
|
class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var BackblazeB2Service
|
||||||
|
*/
|
||||||
|
private $backblaze;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->backblaze = new BackblazeB2Service();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
{
|
||||||
|
// TODO: Implement fetchPhotoContent() method.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of this album source.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return 'global.album_sources.backblaze_b2';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
{
|
||||||
|
// TODO: Implement getUrlToPhoto() method.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setConfiguration(Storage $configuration)
|
||||||
|
{
|
||||||
|
parent::setConfiguration($configuration);
|
||||||
|
|
||||||
|
$this->backblaze->setCredentials(decrypt($configuration->access_key), decrypt($configuration->secret_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getClient()
|
||||||
|
{
|
||||||
|
$this->backblaze->authorizeAccount();
|
||||||
|
$this->backblaze->setBucketName($this->configuration->container_name);
|
||||||
|
|
||||||
|
return $this->backblaze;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
namespace App\Helpers;
|
namespace App\Helpers;
|
||||||
|
|
||||||
use App\AlbumSources\AmazonS3Source;
|
use App\AlbumSources\AmazonS3Source;
|
||||||
|
use App\AlbumSources\BackblazeB2Source;
|
||||||
use App\AlbumSources\IAlbumSource;
|
use App\AlbumSources\IAlbumSource;
|
||||||
use App\AlbumSources\LocalFilesystemSource;
|
use App\AlbumSources\LocalFilesystemSource;
|
||||||
use App\AlbumSources\OpenStackSource;
|
use App\AlbumSources\OpenStackSource;
|
||||||
@ -46,6 +47,7 @@ class ConfigHelper
|
|||||||
$classes = [
|
$classes = [
|
||||||
LocalFilesystemSource::class,
|
LocalFilesystemSource::class,
|
||||||
AmazonS3Source::class,
|
AmazonS3Source::class,
|
||||||
|
BackblazeB2Source::class,
|
||||||
OpenStackSource::class,
|
OpenStackSource::class,
|
||||||
RackspaceSource::class
|
RackspaceSource::class
|
||||||
];
|
];
|
||||||
|
177
app/Services/BackblazeB2Service.php
Normal file
177
app/Services/BackblazeB2Service.php
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
class BackblazeB2Service
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The individual URL for the account to use to access the API
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $accountApiUrl;
|
||||||
|
|
||||||
|
private $accountID;
|
||||||
|
|
||||||
|
private $authHeader;
|
||||||
|
|
||||||
|
private $authToken;
|
||||||
|
|
||||||
|
private $bucketId;
|
||||||
|
|
||||||
|
private $config;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->config = config('services.backblaze_b2');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authorizeAccount($force = false)
|
||||||
|
{
|
||||||
|
if (empty($this->authToken) || $force)
|
||||||
|
{
|
||||||
|
$result = $this->sendRequest($this->config['auth_url']);
|
||||||
|
|
||||||
|
if (!isset($result->authorizationToken))
|
||||||
|
{
|
||||||
|
throw new \Exception('Authorisation to Backblaze failed. Is the API key correct?');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->authToken = $result->authorizationToken;
|
||||||
|
$this->accountApiUrl = $result->apiUrl;
|
||||||
|
$this->accountID = $result->accountId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBucketName($bucketName)
|
||||||
|
{
|
||||||
|
$this->bucketId = $this->getBucketIdFromName($bucketName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCredentials($applicationKeyID, $applicationKey)
|
||||||
|
{
|
||||||
|
$this->authHeader = sprintf('%s:%s', $applicationKeyID, $applicationKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uploadFile($pathToFileToUpload, $pathToStorage)
|
||||||
|
{
|
||||||
|
// Get a URL to upload our file to
|
||||||
|
list($uploadUrl, $authorizationToken) = $this->getUploadUrl();
|
||||||
|
|
||||||
|
if (empty($uploadUrl) || empty($authorizationToken))
|
||||||
|
{
|
||||||
|
throw new \Exception('No upload URL/authorization token returned from Backblaze B2.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileSize = filesize($pathToFileToUpload);
|
||||||
|
$handle = fopen($pathToFileToUpload, 'r');
|
||||||
|
$fileContents = fread($handle, $fileSize);
|
||||||
|
fclose($handle);
|
||||||
|
$fileContentsSha1 = sha1_file($pathToFileToUpload);
|
||||||
|
|
||||||
|
$ch = $this->getBasicHttpClient($uploadUrl, 'POST', [
|
||||||
|
sprintf('Authorization: %s', $authorizationToken),
|
||||||
|
'Content-Type: b2/x-auto',
|
||||||
|
sprintf('X-Bz-Content-Sha1: %s', $fileContentsSha1),
|
||||||
|
sprintf('X-Bz-File-Name: %s', urlencode($pathToStorage))
|
||||||
|
]);
|
||||||
|
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $fileContents);
|
||||||
|
|
||||||
|
$result = curl_exec($ch);
|
||||||
|
|
||||||
|
var_dump($result);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getBucketIdFromName($bucketName)
|
||||||
|
{
|
||||||
|
$result = $this->sendRequest(
|
||||||
|
sprintf('%s/b2api/v2/b2_list_buckets', $this->accountApiUrl),
|
||||||
|
'POST',
|
||||||
|
[
|
||||||
|
'accountId' => $this->accountID,
|
||||||
|
'bucketName' => $bucketName
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($result->buckets) && is_array($result->buckets) && count($result->buckets) >= 1)
|
||||||
|
{
|
||||||
|
return $result->buckets[0]->bucketId;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception(sprintf('The bucket \'%s\' was not found or your API key does not have access.', $bucketName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getUploadUrl()
|
||||||
|
{
|
||||||
|
$result = $this->sendRequest(
|
||||||
|
sprintf('%s/b2api/v2/b2_get_upload_url', $this->accountApiUrl),
|
||||||
|
'POST',
|
||||||
|
['bucketId' => $this->bucketId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [$result->uploadUrl, $result->authorizationToken];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getBasicHttpClient($url, $method = 'GET', array $httpHeaders = [])
|
||||||
|
{
|
||||||
|
$httpHeaders = array_merge(
|
||||||
|
[
|
||||||
|
'Accept: application/json'
|
||||||
|
],
|
||||||
|
$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)
|
||||||
|
{
|
||||||
|
$httpHeaders = [];
|
||||||
|
|
||||||
|
if (is_null($this->authToken))
|
||||||
|
{
|
||||||
|
// No auth token yet, use username/password
|
||||||
|
$httpHeaders[] = sprintf('Authorization: Basic %s', base64_encode($this->authHeader));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use the auth token we have
|
||||||
|
$httpHeaders[] = sprintf('Authorization: %s', $this->authToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ch = $this->getBasicHttpClient($url, $method, $httpHeaders);
|
||||||
|
|
||||||
|
if (!is_null($postData))
|
||||||
|
{
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
|
||||||
|
if ($httpCode != 200 && $httpCode != 304)
|
||||||
|
{
|
||||||
|
throw new \Exception(sprintf('Exception from Backblaze B2: %s', $result));
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return json_decode($result);
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@
|
|||||||
"type": "project",
|
"type": "project",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.0.0",
|
"php": ">=7.0.0",
|
||||||
|
"ext-curl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"laravel/framework": "5.5.*",
|
"laravel/framework": "5.5.*",
|
||||||
"rackspace/php-opencloud": "^1.16",
|
"rackspace/php-opencloud": "^1.16",
|
||||||
|
@ -14,6 +14,10 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
'backblaze_b2' => [
|
||||||
|
'auth_url' => 'https://api.backblazeb2.com/b2api/v2/b2_authorize_account'
|
||||||
|
],
|
||||||
|
|
||||||
'gitea' => [
|
'gitea' => [
|
||||||
'api_url' => 'https://apps.andysh.uk/api/v1',
|
'api_url' => 'https://apps.andysh.uk/api/v1',
|
||||||
'cache_time_seconds' => 3600,
|
'cache_time_seconds' => 3600,
|
||||||
|
@ -323,6 +323,7 @@ return [
|
|||||||
'photos' => 'photo|photos',
|
'photos' => 'photo|photos',
|
||||||
'users' => 'user|users',
|
'users' => 'user|users',
|
||||||
],
|
],
|
||||||
|
'storage_backblaze_access_key_id_help' => 'To use your account\'s master key, enter your account ID here.',
|
||||||
'storage_title' => 'Storage Locations',
|
'storage_title' => 'Storage Locations',
|
||||||
'sysinfo_panel' => 'System information',
|
'sysinfo_panel' => 'System information',
|
||||||
'sysinfo_widget' => [
|
'sysinfo_widget' => [
|
||||||
|
@ -98,6 +98,8 @@ return [
|
|||||||
'storage_access_key_label' => 'Access key:',
|
'storage_access_key_label' => 'Access key:',
|
||||||
'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_api_key_label' => 'API key:',
|
'storage_api_key_label' => 'API key:',
|
||||||
|
'storage_application_key_id_label' => 'Application key ID:',
|
||||||
|
'storage_application_key_label' => 'Application key:',
|
||||||
'storage_auth_url_label' => 'Authentication URL:',
|
'storage_auth_url_label' => 'Authentication URL:',
|
||||||
'storage_bucket_name_label' => 'Bucket name:',
|
'storage_bucket_name_label' => 'Bucket name:',
|
||||||
'storage_cdn_url_label' => 'Public CDN URL (if supported and enabled):',
|
'storage_cdn_url_label' => 'Public CDN URL (if supported and enabled):',
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
return [
|
return [
|
||||||
'album_sources' => [
|
'album_sources' => [
|
||||||
'amazon_s3' => 'Amazon S3 (or S3-compatible)',
|
'amazon_s3' => 'Amazon S3 (or S3-compatible)',
|
||||||
|
'backblaze_b2' => 'Backblaze B2 Cloud',
|
||||||
'filesystem' => 'Local filesystem',
|
'filesystem' => 'Local filesystem',
|
||||||
'openstack' => 'OpenStack cloud storage',
|
'openstack' => 'OpenStack cloud storage',
|
||||||
'rackspace' => 'Rackspace cloud storage'
|
'rackspace' => 'Rackspace cloud storage'
|
||||||
|
@ -63,6 +63,10 @@
|
|||||||
@include(Theme::viewName('partials.admin_storages_rackspace_options'))
|
@include(Theme::viewName('partials.admin_storages_rackspace_options'))
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="storage_driver == 'BackblazeB2Source'">
|
||||||
|
@include(Theme::viewName('partials.admin_storages_backblaze_b2_options'))
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-check">
|
<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>
|
<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>
|
<label class="form-check-label" for="is-default">@lang('forms.default_storage_label')</label>
|
||||||
|
@ -61,6 +61,11 @@
|
|||||||
@include(Theme::viewName('partials.admin_storages_rackspace_options'))
|
@include(Theme::viewName('partials.admin_storages_rackspace_options'))
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
<div v-if="storage_driver == 'BackblazeB2Source'">
|
||||||
|
<hr/>
|
||||||
|
@include(Theme::viewName('partials.admin_storages_backblaze_b2_options'))
|
||||||
|
</div>
|
||||||
|
|
||||||
<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-default">@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>
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
<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_id_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) }}">
|
||||||
|
<small class="form-text text-muted">@lang('admin.storage_backblaze_access_key_id_help')</small>
|
||||||
|
|
||||||
|
@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_key_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="container-name">@lang('forms.storage_bucket_name_label')</label>
|
||||||
|
<input type="text" class="form-control{{ $errors->has('container_name') ? ' is-invalid' : '' }}" id="container-name" name="container_name" value="{{ old('container_name', $storage->container_name) }}">
|
||||||
|
|
||||||
|
@if ($errors->has('container_name'))
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
<strong>{{ $errors->first('container_name') }}</strong>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
Loading…
Reference in New Issue
Block a user