Pull 135: Backblaze B2 storage driver #138

Manually merged
aheathershaw merged 8 commits from feature/135-backblaze-driver into master 2019-09-14 15:38:16 +01:00
8 changed files with 203 additions and 19 deletions
Showing only changes of commit 608442d566 - Show all commits

View File

@ -2,7 +2,6 @@
namespace App\AlbumSources;
use App\Album;
use App\Photo;
use App\Services\BackblazeB2Service;
use App\Storage;
@ -10,15 +9,26 @@ use Guzzle\Http\EntityBody;
class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource
{
const BUCKET_TYPE_AUTO = 0;
const BUCKET_TYPE_PRIVATE = 1;
const BUCKET_TYPE_PUBLIC = 2;
/**
* @var BackblazeB2Service
*/
private $backblaze;
public function __construct()
{
$this->backblaze = new BackblazeB2Service();
}
/**
* Type of bucket which determines what type of URLs to generate to images.
* @var integer
*/
private $bucketType;
/**
* Token used to download files from a private bucket.
* @var string
*/
private $downloadToken;
/**
* Deletes an entire album's media contents.
@ -68,7 +78,31 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource
*/
public function getUrlToPhoto(Photo $photo, $thumbnail = null)
{
// TODO: Implement getUrlToPhoto() method.
$client = $this->getClient();
$storagePathToFile = $this->getPathToPhoto($photo, $thumbnail);
/*
* From https://www.backblaze.com/b2/docs/b2_download_file_by_name.html:
* The base URL to use comes from the b2_authorize_account call, and looks something like
* https://f345.backblazeb2.com. The "f" in the URL stands for "file", and the number is the cluster
* number containing your account. To this base, you add "file/", your bucket name, a "/", and then the
* name of the file. The file name may itself include more "/" characters.
*/
$fileDownloadUrl = sprintf('%s/file/%s/%s', $client->getDownloadUrl(), $this->configuration->container_name, $storagePathToFile);
switch ($this->bucketType)
{
case self::BUCKET_TYPE_PRIVATE:
if (is_null($this->downloadToken))
{
$this->downloadToken = $client->getDownloadAuthToken();
}
return sprintf('%s?Authorization=%s', $fileDownloadUrl, $this->downloadToken);
case self::BUCKET_TYPE_PUBLIC:
return $fileDownloadUrl;
}
}
/**
@ -88,14 +122,38 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource
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);
if (is_null($this->backblaze))
{
$this->backblaze = new BackblazeB2Service();
$this->backblaze->setCredentials(decrypt($this->configuration->access_key), decrypt($this->configuration->secret_key));
$this->backblaze->authorizeAccount();
$this->backblaze->setBucketName($this->configuration->container_name);
if (intval($this->configuration->b2_bucket_type) == self::BUCKET_TYPE_AUTO)
{
/* Auto-detect the type of bucket in use on B2 */
switch ($this->backblaze->getBucketType())
{
case 'allPrivate':
$this->configuration->b2_bucket_type = self::BUCKET_TYPE_PRIVATE;
break;
case 'allPublic':
$this->configuration->b2_bucket_type = self::BUCKET_TYPE_PUBLIC;
break;
}
$this->configuration->save();
}
// Set the bucket type
$this->bucketType = $this->configuration->b2_bucket_type;
}
return $this->backblaze;
}

View File

@ -88,7 +88,8 @@ class StorageController extends Controller
'container_name',
'cdn_url',
'access_key',
'secret_key'
'secret_key',
'b2_bucket_type'
]));
$storage->is_active = true;
$storage->is_default = (strtolower($request->get('is_default')) == 'on');
@ -217,7 +218,8 @@ class StorageController extends Controller
'container_name',
'cdn_url',
'access_key',
'secret_key'
'secret_key',
'b2_bucket_type'
]));
$storage->is_active = (strtolower($request->get('is_active')) == 'on');
$storage->is_default = (strtolower($request->get('is_default')) == 'on');

View File

@ -10,14 +10,46 @@ class BackblazeB2Service
*/
private $accountApiUrl;
/**
* ID of the account in Backblaze B2.
* @var string
*/
private $accountID;
/**
* The base URL for public access to the account's files.
* @var string
*/
private $downloadUrl;
/**
* Authorisation header for authenticating to the API.
* @var string
*/
private $authHeader;
/**
* Authorisation token for accessing the API post-authentication.
* @var string
*/
private $authToken;
/**
* ID of the bucket.
* @var string
*/
private $bucketId;
/**
* Type of the bucket.
* @var integer
*/
private $bucketType;
/**
* Configuration related to the Backblaze B2 service.
* @var \Illuminate\Config\Repository|mixed
*/
private $config;
public function __construct()
@ -39,12 +71,41 @@ class BackblazeB2Service
$this->authToken = $result->authorizationToken;
$this->accountApiUrl = $result->apiUrl;
$this->accountID = $result->accountId;
$this->downloadUrl = $result->downloadUrl;
}
}
public function getBucketType()
{
return $this->bucketType;
}
public function getDownloadAuthToken()
{
$result = $this->sendRequest(
sprintf('%s/b2api/v2/b2_get_download_authorization', $this->accountApiUrl),
'POST',
[
'bucketId' => $this->bucketId,
'validDurationInSeconds' => intval($this->config['download_token_lifetime']),
'fileNamePrefix' => ''
]
);
return $result->authorizationToken;
}
public function getDownloadUrl()
{
return $this->downloadUrl;
}
public function setBucketName($bucketName)
{
$this->bucketId = $this->getBucketIdFromName($bucketName);
$bucketDetails = $this->getBucketDetailsFromName($bucketName);
$this->bucketId = $bucketDetails->bucketId;
$this->bucketType = $bucketDetails->bucketType;
}
public function setCredentials($applicationKeyID, $applicationKey)
@ -78,12 +139,17 @@ class BackblazeB2Service
curl_setopt($ch, CURLOPT_POSTFIELDS, $fileContents);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
var_dump($result);
exit();
if ($httpCode != 200 && $httpCode != 304)
{
throw new \Exception(sprintf('Exception from Backblaze B2: %s', $result));
}
curl_close($ch);
}
private function getBucketIdFromName($bucketName)
private function getBucketDetailsFromName($bucketName)
{
$result = $this->sendRequest(
sprintf('%s/b2api/v2/b2_list_buckets', $this->accountApiUrl),
@ -96,7 +162,7 @@ class BackblazeB2Service
if (isset($result->buckets) && is_array($result->buckets) && count($result->buckets) >= 1)
{
return $result->buckets[0]->bucketId;
return $result->buckets[0];
}
throw new \Exception(sprintf('The bucket \'%s\' was not found or your API key does not have access.', $bucketName));

View File

@ -30,7 +30,8 @@ class Storage extends Model
'container_name',
'cdn_url',
'access_key',
'secret_key'
'secret_key',
'b2_bucket_type'
];
public function albums()

View File

@ -15,7 +15,8 @@ return [
*/
'backblaze_b2' => [
'auth_url' => 'https://api.backblazeb2.com/b2api/v2/b2_authorize_account'
'auth_url' => 'https://api.backblazeb2.com/b2api/v2/b2_authorize_account',
'download_token_lifetime' => 300
],
'gitea' => [

View File

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

View File

@ -101,6 +101,12 @@ return [
'storage_application_key_id_label' => 'Application key ID:',
'storage_application_key_label' => 'Application key:',
'storage_auth_url_label' => 'Authentication URL:',
'storage_b2_bucket_type' => [
'autodetect' => 'Auto-detect',
'label' => 'Bucket type:',
'private' => 'Private',
'public' => 'Public'
],
'storage_bucket_name_label' => 'Bucket name:',
'storage_cdn_url_label' => 'Public CDN URL (if supported and enabled):',
'storage_container_name_label' => 'Container name:',

View File

@ -39,4 +39,20 @@
@endif
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label" for="b2-bucket-type">@lang('forms.storage_b2_bucket_type.label')</label>
<select class="form-control{{ $errors->has('b2_bucket_type') ? ' is-invalid' : '' }}" id="b2-bucket-type" name="b2_bucket_type">
<option value="0"{{ old('b2_bucket_type', $storage->b2_bucket_type) === 0 ? ' selected="selected"' : '' }}>@lang('forms.storage_b2_bucket_type.autodetect')</option>
<option value="1"{{ old('b2_bucket_type', $storage->b2_bucket_type) === 1 ? ' selected="selected"' : '' }}>@lang('forms.storage_b2_bucket_type.private')</option>
<option value="2"{{ old('b2_bucket_type', $storage->b2_bucket_type) === 2 ? ' selected="selected"' : '' }}>@lang('forms.storage_b2_bucket_type.public')</option>
</select>
@if ($errors->has('b2_bucket_type'))
<div class="invalid-feedback">
<strong>{{ $errors->first('b2_bucket_type') }}</strong>
</div>
@endif
</div>
</div>
</div>