diff --git a/app/AlbumSources/BackblazeB2Source.php b/app/AlbumSources/BackblazeB2Source.php new file mode 100644 index 0000000..acde3e4 --- /dev/null +++ b/app/AlbumSources/BackblazeB2Source.php @@ -0,0 +1,117 @@ +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 + ); + } +} \ No newline at end of file diff --git a/app/Helpers/ConfigHelper.php b/app/Helpers/ConfigHelper.php index 2ae648c..c432d46 100644 --- a/app/Helpers/ConfigHelper.php +++ b/app/Helpers/ConfigHelper.php @@ -3,6 +3,7 @@ namespace App\Helpers; use App\AlbumSources\AmazonS3Source; +use App\AlbumSources\BackblazeB2Source; use App\AlbumSources\IAlbumSource; use App\AlbumSources\LocalFilesystemSource; use App\AlbumSources\OpenStackSource; @@ -46,6 +47,7 @@ class ConfigHelper $classes = [ LocalFilesystemSource::class, AmazonS3Source::class, + BackblazeB2Source::class, OpenStackSource::class, RackspaceSource::class ]; diff --git a/app/Services/BackblazeB2Service.php b/app/Services/BackblazeB2Service.php new file mode 100644 index 0000000..eb98438 --- /dev/null +++ b/app/Services/BackblazeB2Service.php @@ -0,0 +1,177 @@ +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); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index f0988ea..5cdad6d 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "type": "project", "require": { "php": ">=7.0.0", + "ext-curl": "*", "ext-json": "*", "laravel/framework": "5.5.*", "rackspace/php-opencloud": "^1.16", diff --git a/config/services.php b/config/services.php index eab4683..c658a92 100644 --- a/config/services.php +++ b/config/services.php @@ -14,6 +14,10 @@ return [ | */ + 'backblaze_b2' => [ + 'auth_url' => 'https://api.backblazeb2.com/b2api/v2/b2_authorize_account' + ], + 'gitea' => [ 'api_url' => 'https://apps.andysh.uk/api/v1', 'cache_time_seconds' => 3600, diff --git a/resources/lang/en/admin.php b/resources/lang/en/admin.php index 6940c04..b844d06 100644 --- a/resources/lang/en/admin.php +++ b/resources/lang/en/admin.php @@ -323,6 +323,7 @@ return [ 'photos' => 'photo|photos', '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', 'sysinfo_panel' => 'System information', 'sysinfo_widget' => [ diff --git a/resources/lang/en/forms.php b/resources/lang/en/forms.php index ceda533..14674f1 100644 --- a/resources/lang/en/forms.php +++ b/resources/lang/en/forms.php @@ -98,6 +98,8 @@ return [ 'storage_access_key_label' => 'Access key:', '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_auth_url_label' => 'Authentication URL:', 'storage_bucket_name_label' => 'Bucket name:', 'storage_cdn_url_label' => 'Public CDN URL (if supported and enabled):', diff --git a/resources/lang/en/global.php b/resources/lang/en/global.php index 0f83c7b..c5da0bd 100644 --- a/resources/lang/en/global.php +++ b/resources/lang/en/global.php @@ -2,6 +2,7 @@ return [ 'album_sources' => [ 'amazon_s3' => 'Amazon S3 (or S3-compatible)', + 'backblaze_b2' => 'Backblaze B2 Cloud', 'filesystem' => 'Local filesystem', 'openstack' => 'OpenStack cloud storage', 'rackspace' => 'Rackspace cloud storage' diff --git a/resources/views/themes/base/admin/create_storage.blade.php b/resources/views/themes/base/admin/create_storage.blade.php index f14e2cb..679e144 100644 --- a/resources/views/themes/base/admin/create_storage.blade.php +++ b/resources/views/themes/base/admin/create_storage.blade.php @@ -63,6 +63,10 @@ @include(Theme::viewName('partials.admin_storages_rackspace_options')) +
+ @include(Theme::viewName('partials.admin_storages_backblaze_b2_options')) +
+
is_default)) checked="checked"@endif> diff --git a/resources/views/themes/base/admin/edit_storage.blade.php b/resources/views/themes/base/admin/edit_storage.blade.php index e4302dc..c7d4705 100644 --- a/resources/views/themes/base/admin/edit_storage.blade.php +++ b/resources/views/themes/base/admin/edit_storage.blade.php @@ -61,6 +61,11 @@ @include(Theme::viewName('partials.admin_storages_rackspace_options')) @endif +
+
+ @include(Theme::viewName('partials.admin_storages_backblaze_b2_options')) +
+
@lang('forms.cancel_action') diff --git a/resources/views/themes/base/partials/admin_storages_backblaze_b2_options.blade.php b/resources/views/themes/base/partials/admin_storages_backblaze_b2_options.blade.php new file mode 100644 index 0000000..c903ed7 --- /dev/null +++ b/resources/views/themes/base/partials/admin_storages_backblaze_b2_options.blade.php @@ -0,0 +1,42 @@ +
+
+
+ + + @lang('admin.storage_backblaze_access_key_id_help') + + @if ($errors->has('access_key')) +
+ {{ $errors->first('access_key') }} +
+ @endif +
+
+
+
+ + + + @if ($errors->has('secret_key')) +
+ {{ $errors->first('secret_key') }} +
+ @endif +
+
+
+ +
+
+
+ + + + @if ($errors->has('container_name')) +
+ {{ $errors->first('container_name') }} +
+ @endif +
+
+
\ No newline at end of file