From 4b6bdeba15e7761fc043f19bc2dda0deabca9750 Mon Sep 17 00:00:00 2001 From: Andy Heathershaw Date: Mon, 9 Sep 2019 20:35:32 +0100 Subject: [PATCH 1/8] Backblaze #135 - added the storage UI and initial connectivity to B2 --- app/AlbumSources/BackblazeB2Source.php | 117 ++++++++++++ app/Helpers/ConfigHelper.php | 2 + app/Services/BackblazeB2Service.php | 177 ++++++++++++++++++ composer.json | 1 + config/services.php | 4 + resources/lang/en/admin.php | 1 + resources/lang/en/forms.php | 2 + resources/lang/en/global.php | 1 + .../base/admin/create_storage.blade.php | 4 + .../themes/base/admin/edit_storage.blade.php | 5 + ...in_storages_backblaze_b2_options.blade.php | 42 +++++ 11 files changed, 356 insertions(+) create mode 100644 app/AlbumSources/BackblazeB2Source.php create mode 100644 app/Services/BackblazeB2Service.php create mode 100644 resources/views/themes/base/partials/admin_storages_backblaze_b2_options.blade.php 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 From 437fe9fe1fabed8ea0ee5c67bca4e75113c08fdd Mon Sep 17 00:00:00 2001 From: Andy Heathershaw Date: Mon, 9 Sep 2019 20:51:06 +0100 Subject: [PATCH 2/8] Updated composer.lock file --- composer.lock | 380 +++++++++++++++++++++++++------------------------- 1 file changed, 192 insertions(+), 188 deletions(-) diff --git a/composer.lock b/composer.lock index 329849f..8229830 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "359fa33910865037863ae6ae724af956", + "content-hash": "53d67647c5a4d0d450470522c903f745", "packages": [ { "name": "aws/aws-sdk-php", - "version": "3.105.0", + "version": "3.111.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "3a1159eeb14f707780817bebf73dd3eeeeb710cc" + "reference": "a31376012346118b2b88df6d2f0c185af71e3096" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3a1159eeb14f707780817bebf73dd3eeeeb710cc", - "reference": "3a1159eeb14f707780817bebf73dd3eeeeb710cc", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a31376012346118b2b88df6d2f0c185af71e3096", + "reference": "a31376012346118b2b88df6d2f0c185af71e3096", "shasum": "" }, "require": { @@ -87,7 +87,7 @@ "s3", "sdk" ], - "time": "2019-07-08T18:16:57+00:00" + "time": "2019-09-09T18:13:28+00:00" }, { "name": "doctrine/cache", @@ -389,28 +389,30 @@ }, { "name": "doctrine/lexer", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8" + "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/1febd6c3ef84253d7c815bed85fc622ad207a9f8", - "reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e17f069ede36f7534b95adec71910ed1b49c74ea", + "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^7.2" }, "require-dev": { - "phpunit/phpunit": "^4.5" + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11.8", + "phpunit/phpunit": "^8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -423,14 +425,14 @@ "MIT" ], "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, { "name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com" }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com" @@ -445,20 +447,20 @@ "parser", "php" ], - "time": "2019-06-08T11:03:04+00:00" + "time": "2019-07-30T19:33:28+00:00" }, { "name": "egulias/email-validator", - "version": "2.1.9", + "version": "2.1.11", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "128cc721d771ec2c46ce59698f4ca42b73f71b25" + "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/128cc721d771ec2c46ce59698f4ca42b73f71b25", - "reference": "128cc721d771ec2c46ce59698f4ca42b73f71b25", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/92dd169c32f6f55ba570c309d83f5209cefb5e23", + "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23", "shasum": "" }, "require": { @@ -468,7 +470,8 @@ "require-dev": { "dominicsayers/isemail": "dev-master", "phpunit/phpunit": "^4.8.35||^5.7||^6.0", - "satooshi/php-coveralls": "^1.0.1" + "satooshi/php-coveralls": "^1.0.1", + "symfony/phpunit-bridge": "^4.4@dev" }, "suggest": { "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" @@ -476,7 +479,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "2.1.x-dev" } }, "autoload": { @@ -502,7 +505,7 @@ "validation", "validator" ], - "time": "2019-06-23T10:14:27+00:00" + "time": "2019-08-13T17:33:27+00:00" }, { "name": "erusev/parsedown", @@ -835,25 +838,25 @@ }, { "name": "kylekatarnls/update-helper", - "version": "1.1.1", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/kylekatarnls/update-helper.git", - "reference": "b34a46d7f5ec1795b4a15ac9d46b884377262df9" + "reference": "5786fa188e0361b9adf9e8199d7280d1b2db165e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kylekatarnls/update-helper/zipball/b34a46d7f5ec1795b4a15ac9d46b884377262df9", - "reference": "b34a46d7f5ec1795b4a15ac9d46b884377262df9", + "url": "https://api.github.com/repos/kylekatarnls/update-helper/zipball/5786fa188e0361b9adf9e8199d7280d1b2db165e", + "reference": "5786fa188e0361b9adf9e8199d7280d1b2db165e", "shasum": "" }, "require": { - "composer-plugin-api": "^1.1.0", + "composer-plugin-api": "^1.1.0 || ^2.0.0", "php": ">=5.3.0" }, "require-dev": { "codeclimate/php-test-reporter": "dev-master", - "composer/composer": "^2.0.x-dev", + "composer/composer": "2.0.x-dev || ^2.0.0-dev", "phpunit/phpunit": ">=4.8.35 <6.0" }, "type": "composer-plugin", @@ -876,20 +879,20 @@ } ], "description": "Update helper", - "time": "2019-06-05T08:34:23+00:00" + "time": "2019-07-29T11:03:54+00:00" }, { "name": "laravel/framework", - "version": "v5.5.45", + "version": "v5.5.48", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "52c79ecf54b6168a54730ccb6c4c9f3561732a80" + "reference": "e3e8d585dcfab5abe6261b060f4df0d48f9924bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/52c79ecf54b6168a54730ccb6c4c9f3561732a80", - "reference": "52c79ecf54b6168a54730ccb6c4c9f3561732a80", + "url": "https://api.github.com/repos/laravel/framework/zipball/e3e8d585dcfab5abe6261b060f4df0d48f9924bf", + "reference": "e3e8d585dcfab5abe6261b060f4df0d48f9924bf", "shasum": "" }, "require": { @@ -1010,7 +1013,7 @@ "framework", "laravel" ], - "time": "2019-01-28T20:53:19+00:00" + "time": "2019-08-20T15:46:40+00:00" }, { "name": "laravel/socialite", @@ -1077,16 +1080,16 @@ }, { "name": "league/flysystem", - "version": "1.0.53", + "version": "1.0.55", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "08e12b7628f035600634a5e76d95b5eb66cea674" + "reference": "33c91155537c6dc899eacdc54a13ac6303f156e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/08e12b7628f035600634a5e76d95b5eb66cea674", - "reference": "08e12b7628f035600634a5e76d95b5eb66cea674", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/33c91155537c6dc899eacdc54a13ac6303f156e6", + "reference": "33c91155537c6dc899eacdc54a13ac6303f156e6", "shasum": "" }, "require": { @@ -1157,7 +1160,7 @@ "sftp", "storage" ], - "time": "2019-06-18T20:09:29+00:00" + "time": "2019-08-24T11:17:19+00:00" }, { "name": "league/oauth1-client", @@ -1251,16 +1254,16 @@ }, { "name": "monolog/monolog", - "version": "1.24.0", + "version": "1.25.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" + "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", - "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/70e65a5470a42cfec1a7da00d30edb6e617e8dcf", + "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf", "shasum": "" }, "require": { @@ -1325,7 +1328,7 @@ "logging", "psr-3" ], - "time": "2018-11-05T09:00:11+00:00" + "time": "2019-09-06T13:49:17+00:00" }, { "name": "mtdowling/cron-expression", @@ -1534,22 +1537,22 @@ }, { "name": "php-amqplib/php-amqplib", - "version": "v2.9.2", + "version": "v2.10.0", "source": { "type": "git", "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "76faddcd668dabb8d4f7c00e86b8a9decd781a59" + "reference": "04e5366f032906d5f716890427e425e71307d3a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/76faddcd668dabb8d4f7c00e86b8a9decd781a59", - "reference": "76faddcd668dabb8d4f7c00e86b8a9decd781a59", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/04e5366f032906d5f716890427e425e71307d3a8", + "reference": "04e5366f032906d5f716890427e425e71307d3a8", "shasum": "" }, "require": { "ext-bcmath": "*", "ext-sockets": "*", - "php": ">=5.4.0" + "php": ">=5.6" }, "replace": { "videlalvaro/php-amqplib": "self.version" @@ -1557,14 +1560,14 @@ "require-dev": { "ext-curl": "*", "nategood/httpful": "^0.2.20", - "phpdocumentor/phpdocumentor": "^2.9", - "phpunit/phpunit": "^4.8", + "phpdocumentor/phpdocumentor": "dev-master", + "phpunit/phpunit": "^5.7|^6.5|^7.0", "squizlabs/php_codesniffer": "^2.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "2.10-dev" } }, "autoload": { @@ -1583,18 +1586,18 @@ }, { "name": "John Kelly", - "email": "johnmkelly86@gmail.com", - "role": "Maintainer" + "role": "Maintainer", + "email": "johnmkelly86@gmail.com" }, { "name": "Raúl Araya", - "email": "nubeiro@gmail.com", - "role": "Maintainer" + "role": "Maintainer", + "email": "nubeiro@gmail.com" }, { "name": "Luke Bakken", - "email": "luke@bakken.io", - "role": "Maintainer" + "role": "Maintainer", + "email": "luke@bakken.io" } ], "description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.", @@ -1604,7 +1607,7 @@ "queue", "rabbitmq" ], - "time": "2019-04-24T15:36:21+00:00" + "time": "2019-08-08T18:28:18+00:00" }, { "name": "psr/container", @@ -2043,16 +2046,16 @@ }, { "name": "symfony/console", - "version": "v3.4.29", + "version": "v3.4.31", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c4d2f3529755ffc0be9fb823583b28d8744eeb3d" + "reference": "4510f04e70344d70952566e4262a0b11df39cb10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c4d2f3529755ffc0be9fb823583b28d8744eeb3d", - "reference": "c4d2f3529755ffc0be9fb823583b28d8744eeb3d", + "url": "https://api.github.com/repos/symfony/console/zipball/4510f04e70344d70952566e4262a0b11df39cb10", + "reference": "4510f04e70344d70952566e4262a0b11df39cb10", "shasum": "" }, "require": { @@ -2111,7 +2114,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2019-06-05T11:33:52+00:00" + "time": "2019-08-26T07:52:58+00:00" }, { "name": "symfony/css-selector", @@ -2168,16 +2171,16 @@ }, { "name": "symfony/debug", - "version": "v3.4.29", + "version": "v3.4.31", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "1172dc1abe44dfadd162239153818b074e6e53bf" + "reference": "0b600300918780001e2821db77bc28b677794486" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/1172dc1abe44dfadd162239153818b074e6e53bf", - "reference": "1172dc1abe44dfadd162239153818b074e6e53bf", + "url": "https://api.github.com/repos/symfony/debug/zipball/0b600300918780001e2821db77bc28b677794486", + "reference": "0b600300918780001e2821db77bc28b677794486", "shasum": "" }, "require": { @@ -2220,7 +2223,7 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2019-06-18T21:26:03+00:00" + "time": "2019-08-20T13:31:17+00:00" }, { "name": "symfony/event-dispatcher", @@ -2284,16 +2287,16 @@ }, { "name": "symfony/finder", - "version": "v3.4.29", + "version": "v3.4.31", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "5f80266a729e30bbcc37f8bf0e62c3d5a38c8208" + "reference": "1fcad80b440abcd1451767349906b6f9d3961d37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/5f80266a729e30bbcc37f8bf0e62c3d5a38c8208", - "reference": "5f80266a729e30bbcc37f8bf0e62c3d5a38c8208", + "url": "https://api.github.com/repos/symfony/finder/zipball/1fcad80b440abcd1451767349906b6f9d3961d37", + "reference": "1fcad80b440abcd1451767349906b6f9d3961d37", "shasum": "" }, "require": { @@ -2329,20 +2332,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2019-05-30T15:47:52+00:00" + "time": "2019-08-14T09:39:58+00:00" }, { "name": "symfony/http-foundation", - "version": "v3.4.29", + "version": "v3.4.31", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "8cfbf75bb3a72963b12c513a73e9247891df24f8" + "reference": "b3d57a1c325f39f703b249bed7998ce8c64236b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/8cfbf75bb3a72963b12c513a73e9247891df24f8", - "reference": "8cfbf75bb3a72963b12c513a73e9247891df24f8", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b3d57a1c325f39f703b249bed7998ce8c64236b4", + "reference": "b3d57a1c325f39f703b249bed7998ce8c64236b4", "shasum": "" }, "require": { @@ -2383,20 +2386,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2019-06-22T20:10:25+00:00" + "time": "2019-08-26T07:50:50+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.4.29", + "version": "v3.4.31", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "abbb38dbab652ddc40a86d0c3b0e14ca52d58ed2" + "reference": "f6d35bb306b26812df007525f5757a8b0e95857e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/abbb38dbab652ddc40a86d0c3b0e14ca52d58ed2", - "reference": "abbb38dbab652ddc40a86d0c3b0e14ca52d58ed2", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6d35bb306b26812df007525f5757a8b0e95857e", + "reference": "f6d35bb306b26812df007525f5757a8b0e95857e", "shasum": "" }, "require": { @@ -2472,20 +2475,20 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2019-06-26T13:56:39+00:00" + "time": "2019-08-26T16:36:29+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "82ebae02209c21113908c229e9883c419720738a" + "reference": "550ebaac289296ce228a706d0867afc34687e3f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", - "reference": "82ebae02209c21113908c229e9883c419720738a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", + "reference": "550ebaac289296ce228a706d0867afc34687e3f4", "shasum": "" }, "require": { @@ -2497,7 +2500,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2513,13 +2516,13 @@ "MIT" ], "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, { "name": "Gert de Pagter", "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], "description": "Symfony polyfill for ctype functions", @@ -2530,20 +2533,20 @@ "polyfill", "portable" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-iconv", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "f037ea22acfaee983e271dd9c3b8bb4150bd8ad7" + "reference": "685968b11e61a347c18bf25db32effa478be610f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/f037ea22acfaee983e271dd9c3b8bb4150bd8ad7", - "reference": "f037ea22acfaee983e271dd9c3b8bb4150bd8ad7", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/685968b11e61a347c18bf25db32effa478be610f", + "reference": "685968b11e61a347c18bf25db32effa478be610f", "shasum": "" }, "require": { @@ -2555,7 +2558,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2589,20 +2592,20 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af" + "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c766e95bec706cdd89903b1eda8afab7d7a6b7af", - "reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6af626ae6fa37d396dc90a399c0ff08e5cfc45b2", + "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2", "shasum": "" }, "require": { @@ -2616,7 +2619,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2632,13 +2635,13 @@ "MIT" ], "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, { "name": "Laurent Bassin", "email": "laurent@bassin.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", @@ -2651,20 +2654,20 @@ "portable", "shim" ], - "time": "2019-03-04T13:44:35+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fe5e94c604826c35a32fa832f35bd036b6799609" + "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609", - "reference": "fe5e94c604826c35a32fa832f35bd036b6799609", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17", + "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17", "shasum": "" }, "require": { @@ -2676,7 +2679,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2710,20 +2713,20 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "bc4858fb611bda58719124ca079baff854149c89" + "reference": "54b4c428a0054e254223797d2713c31e08610831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/bc4858fb611bda58719124ca079baff854149c89", - "reference": "bc4858fb611bda58719124ca079baff854149c89", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/54b4c428a0054e254223797d2713c31e08610831", + "reference": "54b4c428a0054e254223797d2713c31e08610831", "shasum": "" }, "require": { @@ -2733,7 +2736,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2769,20 +2772,20 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c" + "reference": "04ce3335667451138df4307d6a9b61565560199e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/ab50dcf166d5f577978419edd37aa2bb8eabce0c", - "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e", + "reference": "04ce3335667451138df4307d6a9b61565560199e", "shasum": "" }, "require": { @@ -2791,7 +2794,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2824,20 +2827,20 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/process", - "version": "v3.4.29", + "version": "v3.4.31", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d129c017e8602507688ef2c3007951a16c1a8407" + "reference": "d822cb654000a95b7855362c0d5b127f6a6d8baa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d129c017e8602507688ef2c3007951a16c1a8407", - "reference": "d129c017e8602507688ef2c3007951a16c1a8407", + "url": "https://api.github.com/repos/symfony/process/zipball/d822cb654000a95b7855362c0d5b127f6a6d8baa", + "reference": "d822cb654000a95b7855362c0d5b127f6a6d8baa", "shasum": "" }, "require": { @@ -2873,20 +2876,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2019-05-30T15:47:52+00:00" + "time": "2019-08-26T07:52:58+00:00" }, { "name": "symfony/routing", - "version": "v3.4.29", + "version": "v3.4.31", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8d804d8a65a26dc9de1aaf2ff3a421e581d050e6" + "reference": "8b0faa681c4ee14701e76a7056fef15ac5384163" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8d804d8a65a26dc9de1aaf2ff3a421e581d050e6", - "reference": "8d804d8a65a26dc9de1aaf2ff3a421e581d050e6", + "url": "https://api.github.com/repos/symfony/routing/zipball/8b0faa681c4ee14701e76a7056fef15ac5384163", + "reference": "8b0faa681c4ee14701e76a7056fef15ac5384163", "shasum": "" }, "require": { @@ -2949,26 +2952,26 @@ "uri", "url" ], - "time": "2019-06-26T11:14:13+00:00" + "time": "2019-08-26T07:50:50+00:00" }, { "name": "symfony/translation", - "version": "v4.3.2", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "934ab1d18545149e012aa898cf02e9f23790f7a0" + "reference": "28498169dd334095fa981827992f3a24d50fed0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/934ab1d18545149e012aa898cf02e9f23790f7a0", - "reference": "934ab1d18545149e012aa898cf02e9f23790f7a0", + "url": "https://api.github.com/repos/symfony/translation/zipball/28498169dd334095fa981827992f3a24d50fed0f", + "reference": "28498169dd334095fa981827992f3a24d50fed0f", "shasum": "" }, "require": { "php": "^7.1.3", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^1.1.2" + "symfony/translation-contracts": "^1.1.6" }, "conflict": { "symfony/config": "<3.4", @@ -3025,20 +3028,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2019-06-13T11:03:18+00:00" + "time": "2019-08-26T08:55:16+00:00" }, { "name": "symfony/translation-contracts", - "version": "v1.1.5", + "version": "v1.1.6", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "cb4b18ad7b92a26e83b65dde940fab78339e6f3c" + "reference": "325b17c24f3ee23cbecfa63ba809c6d89b5fa04a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/cb4b18ad7b92a26e83b65dde940fab78339e6f3c", - "reference": "cb4b18ad7b92a26e83b65dde940fab78339e6f3c", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/325b17c24f3ee23cbecfa63ba809c6d89b5fa04a", + "reference": "325b17c24f3ee23cbecfa63ba809c6d89b5fa04a", "shasum": "" }, "require": { @@ -3082,20 +3085,20 @@ "interoperability", "standards" ], - "time": "2019-06-13T11:15:36+00:00" + "time": "2019-08-02T12:15:04+00:00" }, { "name": "symfony/var-dumper", - "version": "v3.4.29", + "version": "v3.4.31", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "7b92618169c44af4bb226f69dbac42b56b1a7745" + "reference": "5408ad7194737ee1bc5ab7a9683fb6925f92c3e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7b92618169c44af4bb226f69dbac42b56b1a7745", - "reference": "7b92618169c44af4bb226f69dbac42b56b1a7745", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/5408ad7194737ee1bc5ab7a9683fb6925f92c3e4", + "reference": "5408ad7194737ee1bc5ab7a9683fb6925f92c3e4", "shasum": "" }, "require": { @@ -3151,7 +3154,7 @@ "debug", "dump" ], - "time": "2019-06-13T16:26:35+00:00" + "time": "2019-08-26T07:50:50+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -3311,16 +3314,16 @@ }, { "name": "filp/whoops", - "version": "2.4.1", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "6fb502c23885701a991b0bba974b1a8eb6673577" + "reference": "cde50e6720a39fdacb240159d3eea6865d51fd96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/6fb502c23885701a991b0bba974b1a8eb6673577", - "reference": "6fb502c23885701a991b0bba974b1a8eb6673577", + "url": "https://api.github.com/repos/filp/whoops/zipball/cde50e6720a39fdacb240159d3eea6865d51fd96", + "reference": "cde50e6720a39fdacb240159d3eea6865d51fd96", "shasum": "" }, "require": { @@ -3368,7 +3371,7 @@ "throwable", "whoops" ], - "time": "2019-07-04T09:00:00+00:00" + "time": "2019-08-07T09:00:00+00:00" }, { "name": "fzaninotto/faker", @@ -3532,16 +3535,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.9.1", + "version": "1.9.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72" + "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72", - "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea", + "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea", "shasum": "" }, "require": { @@ -3576,7 +3579,7 @@ "object", "object graph" ], - "time": "2019-04-07T13:18:21+00:00" + "time": "2019-08-09T12:45:53+00:00" }, { "name": "phar-io/manifest", @@ -4501,16 +4504,16 @@ }, { "name": "sebastian/exporter", - "version": "3.1.0", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" + "reference": "06a9a5947f47b3029d76118eb5c22802e5869687" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/06a9a5947f47b3029d76118eb5c22802e5869687", + "reference": "06a9a5947f47b3029d76118eb5c22802e5869687", "shasum": "" }, "require": { @@ -4537,6 +4540,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -4545,17 +4552,13 @@ "name": "Volker Dusch", "email": "github@wallbash.com" }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, { "name": "Adam Harvey", "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", @@ -4564,7 +4567,7 @@ "export", "exporter" ], - "time": "2017-04-03T13:19:02+00:00" + "time": "2019-08-11T12:43:14+00:00" }, { "name": "sebastian/global-state", @@ -4945,16 +4948,16 @@ }, { "name": "webmozart/assert", - "version": "1.4.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" + "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", + "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4", + "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4", "shasum": "" }, "require": { @@ -4962,8 +4965,7 @@ "symfony/polyfill-ctype": "^1.8" }, "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" + "phpunit/phpunit": "^4.8.36 || ^7.5.13" }, "type": "library", "extra": { @@ -4992,7 +4994,7 @@ "check", "validate" ], - "time": "2018-12-25T11:19:39+00:00" + "time": "2019-08-24T08:43:50+00:00" } ], "aliases": [], @@ -5001,7 +5003,9 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.0.0" + "php": ">=7.0.0", + "ext-curl": "*", + "ext-json": "*" }, "platform-dev": [] } From 608442d56604b949020d81e625e0227f2a150ccf Mon Sep 17 00:00:00 2001 From: Andy Heathershaw Date: Mon, 9 Sep 2019 21:52:26 +0100 Subject: [PATCH 3/8] Backblaze #135 - introduced the config setting to choose whether to generate private or public URLs, or to auto-detect. Photos are now displayed from B2. --- app/AlbumSources/BackblazeB2Source.php | 78 ++++++++++++++++--- .../Controllers/Admin/StorageController.php | 6 +- app/Services/BackblazeB2Service.php | 76 ++++++++++++++++-- app/Storage.php | 3 +- config/services.php | 3 +- ...9_205137_add_backblaze_storage_columns.php | 34 ++++++++ resources/lang/en/forms.php | 6 ++ ...in_storages_backblaze_b2_options.blade.php | 16 ++++ 8 files changed, 203 insertions(+), 19 deletions(-) create mode 100644 database/migrations/2019_09_09_205137_add_backblaze_storage_columns.php diff --git a/app/AlbumSources/BackblazeB2Source.php b/app/AlbumSources/BackblazeB2Source.php index acde3e4..c6c21d7 100644 --- a/app/AlbumSources/BackblazeB2Source.php +++ b/app/AlbumSources/BackblazeB2Source.php @@ -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; } diff --git a/app/Http/Controllers/Admin/StorageController.php b/app/Http/Controllers/Admin/StorageController.php index 7093643..6a1c775 100644 --- a/app/Http/Controllers/Admin/StorageController.php +++ b/app/Http/Controllers/Admin/StorageController.php @@ -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'); diff --git a/app/Services/BackblazeB2Service.php b/app/Services/BackblazeB2Service.php index eb98438..de0671d 100644 --- a/app/Services/BackblazeB2Service.php +++ b/app/Services/BackblazeB2Service.php @@ -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)); diff --git a/app/Storage.php b/app/Storage.php index cd7cd42..660178c 100644 --- a/app/Storage.php +++ b/app/Storage.php @@ -30,7 +30,8 @@ class Storage extends Model 'container_name', 'cdn_url', 'access_key', - 'secret_key' + 'secret_key', + 'b2_bucket_type' ]; public function albums() diff --git a/config/services.php b/config/services.php index c658a92..e7ace08 100644 --- a/config/services.php +++ b/config/services.php @@ -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' => [ diff --git a/database/migrations/2019_09_09_205137_add_backblaze_storage_columns.php b/database/migrations/2019_09_09_205137_add_backblaze_storage_columns.php new file mode 100644 index 0000000..e6d3e74 --- /dev/null +++ b/database/migrations/2019_09_09_205137_add_backblaze_storage_columns.php @@ -0,0 +1,34 @@ +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'); + }); + } +} diff --git a/resources/lang/en/forms.php b/resources/lang/en/forms.php index 14674f1..0e1cc96 100644 --- a/resources/lang/en/forms.php +++ b/resources/lang/en/forms.php @@ -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:', 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 index c903ed7..4450e21 100644 --- 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 @@ -39,4 +39,20 @@ @endif
+
+
+ + + + @if ($errors->has('b2_bucket_type')) +
+ {{ $errors->first('b2_bucket_type') }} +
+ @endif +
+
\ No newline at end of file From ce03b2596ff18d187de565cb649b63a11c485ef8 Mon Sep 17 00:00:00 2001 From: Andy Heathershaw Date: Tue, 10 Sep 2019 15:11:53 +0100 Subject: [PATCH 4/8] Backblaze #135 - album storage driver is now cached to maintain state within the same request, prevents multiple calls to B2. Images can now be deleted and (I think) edited. --- app/Album.php | 6 +- app/AlbumSources/AlbumSourceBase.php | 25 ++++ app/AlbumSources/BackblazeB2Source.php | 50 +++++++- app/BackblazeB2FileIdCache.php | 19 +++ app/Services/BackblazeB2Service.php | 111 ++++++++++++++---- ...eate_backblaze_b2_file_id_caches_table.php | 38 ++++++ 6 files changed, 220 insertions(+), 29 deletions(-) create mode 100644 app/BackblazeB2FileIdCache.php create mode 100644 database/migrations/2019_09_10_085020_create_backblaze_b2_file_id_caches_table.php diff --git a/app/Album.php b/app/Album.php index e375df9..158357c 100644 --- a/app/Album.php +++ b/app/Album.php @@ -2,6 +2,7 @@ namespace App; +use App\AlbumSources\AlbumSourceBase; use App\AlbumSources\IAlbumSource; use App\AlbumSources\LocalFilesystemSource; use App\Helpers\MiscHelper; @@ -158,10 +159,7 @@ class Album extends Model */ public function getAlbumSource() { - $fullClassName = sprintf('App\AlbumSources\%s', $this->storage->source); - - /** @var IAlbumSource $source */ - $source = new $fullClassName; + $source = AlbumSourceBase::make($this->storage->source); $source->setAlbum($this); $source->setConfiguration($this->storage); diff --git a/app/AlbumSources/AlbumSourceBase.php b/app/AlbumSources/AlbumSourceBase.php index 364bc39..374e17f 100644 --- a/app/AlbumSources/AlbumSourceBase.php +++ b/app/AlbumSources/AlbumSourceBase.php @@ -17,6 +17,31 @@ abstract class AlbumSourceBase */ protected $configuration; + /** + * @var mixed + */ + private static $albumSourceCache = []; + + /** + * Makes an album source class for the given source name (relative class name.) + * @param string $sourceName Name of the source. + * @return IAlbumSource + */ + public static function make($sourceName) + { + $fullClassName = sprintf('App\AlbumSources\%s', $sourceName); + + if (!array_key_exists($fullClassName, self::$albumSourceCache)) + { + /** @var IAlbumSource $source */ + $source = app($fullClassName); + + self::$albumSourceCache[$fullClassName] = $source; + } + + return self::$albumSourceCache[$fullClassName]; + } + public function setAlbum(Album $album) { $this->album = $album; diff --git a/app/AlbumSources/BackblazeB2Source.php b/app/AlbumSources/BackblazeB2Source.php index c6c21d7..b7e3986 100644 --- a/app/AlbumSources/BackblazeB2Source.php +++ b/app/AlbumSources/BackblazeB2Source.php @@ -2,10 +2,12 @@ namespace App\AlbumSources; +use App\BackblazeB2FileIdCache; use App\Photo; use App\Services\BackblazeB2Service; use App\Storage; use Guzzle\Http\EntityBody; +use Illuminate\Support\Facades\Log; class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource { @@ -36,7 +38,7 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource */ public function deleteAlbumContents() { - // TODO: Implement deleteAlbumContents() method. + // No need to do anything for the album container - once the files are gone, the virtual folder is also gone } /** @@ -47,7 +49,21 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource */ public function deleteThumbnail(Photo $photo, $thumbnail = null) { - // TODO: Implement deleteThumbnail() method. + $pathOnStorage = $this->getPathToPhoto($photo, $thumbnail); + + // Create or update our cache record + + /** @var BackblazeB2FileIdCache $b2Cache */ + $b2Cache = BackblazeB2FileIdCache::where('storage_path', $pathOnStorage)->first(); + if (is_null($b2Cache)) + { + // TODO: lookup the file on B2 to get the file ID + Log::warning(sprintf('B2 file ID not found in cache: %s', $pathOnStorage)); + return; + } + + $this->getClient()->deleteFile($b2Cache->b2_file_id, $pathOnStorage); + $b2Cache->delete(); } /** @@ -58,7 +74,14 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource */ public function fetchPhotoContent(Photo $photo, $thumbnail = null) { - // TODO: Implement fetchPhotoContent() method. + // Use the same URLs that the public would use to fetch the file + $urlToPhoto = $this->getUrlToPhoto($photo, $thumbnail); + + $ch = curl_init($urlToPhoto); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $fileContent = curl_exec($ch); + + return EntityBody::fromString($fileContent); } /** @@ -93,6 +116,7 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource switch ($this->bucketType) { case self::BUCKET_TYPE_PRIVATE: + // TODO: use the B2 b2_download_file_by_id method so filenames are harder to guess if (is_null($this->downloadToken)) { $this->downloadToken = $client->getDownloadAuthToken(); @@ -116,7 +140,25 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource { $pathOnStorage = $this->getPathToPhoto($photo, $thumbnail); - $this->getClient()->uploadFile($tempFilename, $pathOnStorage); + // Upload the file to B2 + $b2FileID = $this->getClient()->uploadFile($tempFilename, $pathOnStorage); + + // Create or update our cache record + $b2Cache = BackblazeB2FileIdCache::where('storage_path', $pathOnStorage)->first(); + if (is_null($b2Cache)) + { + $b2Cache = new BackblazeB2FileIdCache([ + 'photo_id' => $photo->id, + 'storage_path' => $pathOnStorage, + 'b2_file_id' => $b2FileID + ]); + } + else + { + $b2Cache->b2_file_id = $b2FileID; + } + + $b2Cache->save(); } public function setConfiguration(Storage $configuration) diff --git a/app/BackblazeB2FileIdCache.php b/app/BackblazeB2FileIdCache.php new file mode 100644 index 0000000..b1765cd --- /dev/null +++ b/app/BackblazeB2FileIdCache.php @@ -0,0 +1,19 @@ +sendRequest( + sprintf('%s/b2api/v2/b2_delete_file_version', $this->accountApiUrl), + 'POST', + [ + 'fileId' => $fileID, + 'fileName' => $fileName + ] + ); + } + + public function downloadFile($fileID) + { + $downloadToken = $this->getDownloadAuthToken(); + + return $this->sendRequest( + sprintf('%s/b2api/v2/b2_download_file_by_id?fileId=%s', $this->accountApiUrl, urlencode($fileID), urlencode($downloadToken)), + 'GET', + null, + [ + 'http_headers' => [ + sprintf('Authorization: %s', $downloadToken) + ], + 'response_body_is_json' => false + ] + ); + } + public function getBucketType() { return $this->bucketType; @@ -129,24 +160,24 @@ class BackblazeB2Service fclose($handle); $fileContentsSha1 = sha1_file($pathToFileToUpload); - $ch = $this->getBasicHttpClient($uploadUrl, 'POST', [ + $httpHeaders = [ 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 = $this->sendRequest( + $uploadUrl, + 'POST', + $fileContents, + [ + 'http_headers' => $httpHeaders, + 'post_body_is_json' => false + ] + ); - $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 $result->fileId; } private function getBucketDetailsFromName($bucketName) @@ -206,31 +237,67 @@ class BackblazeB2Service return $ch; } - private function sendRequest($url, $method = 'GET', $postData = null) + private function sendRequest($url, $method = 'GET', $postData = null, array $postOptions = []) { - $httpHeaders = []; + $postOptions = array_merge( + [ + 'authorization_token' => null, + 'http_headers' => [], + 'post_body_is_json' => true, + 'response_body_is_json' => true + ], + $postOptions + ); + $httpHeaders = $postOptions['http_headers']; - if (is_null($this->authToken)) + // Some methods may need to override the authorization token used + if (empty($postOptions['authorization_token'])) { - // No auth token yet, use username/password - $httpHeaders[] = sprintf('Authorization: Basic %s', base64_encode($this->authHeader)); + // No override - work out which auth token to use + 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); + } } else { - // Use the auth token we have - $httpHeaders[] = sprintf('Authorization: %s', $this->authToken); + // Override - use the auth token specified + $httpHeaders[] = sprintf('Authorization: %s', $postOptions['authorization_token']); } $ch = $this->getBasicHttpClient($url, $method, $httpHeaders); if (!is_null($postData)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($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)); + Log::debug($result); + if ($httpCode != 200 && $httpCode != 304) { throw new \Exception(sprintf('Exception from Backblaze B2: %s', $result)); @@ -238,6 +305,8 @@ class BackblazeB2Service curl_close($ch); - return json_decode($result); + return $postOptions['response_body_is_json'] + ? json_decode($result) + : $result; } } \ No newline at end of file diff --git a/database/migrations/2019_09_10_085020_create_backblaze_b2_file_id_caches_table.php b/database/migrations/2019_09_10_085020_create_backblaze_b2_file_id_caches_table.php new file mode 100644 index 0000000..1b7ac4d --- /dev/null +++ b/database/migrations/2019_09_10_085020_create_backblaze_b2_file_id_caches_table.php @@ -0,0 +1,38 @@ +bigIncrements('id'); + $table->unsignedBigInteger('photo_id'); + $table->string('storage_path'); + $table->string('b2_file_id'); + $table->timestamps(); + + $table->foreign('photo_id') + ->references('id')->on('photos') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('backblaze_b2_file_id_caches'); + } +} From fb6754b8e917bc1e33b359ac6eeb4f81a960820e Mon Sep 17 00:00:00 2001 From: Andy Heathershaw Date: Tue, 10 Sep 2019 16:24:26 +0100 Subject: [PATCH 5/8] Backblaze #135 - tried to implement b2_download_file_by_id for private buckets, but this doesn't work correctly, logged with Backblaze --- app/AlbumSources/BackblazeB2Source.php | 35 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/app/AlbumSources/BackblazeB2Source.php b/app/AlbumSources/BackblazeB2Source.php index b7e3986..c9a4c9e 100644 --- a/app/AlbumSources/BackblazeB2Source.php +++ b/app/AlbumSources/BackblazeB2Source.php @@ -102,30 +102,39 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource public function getUrlToPhoto(Photo $photo, $thumbnail = null) { $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); + $pathOnStorage = $this->getPathToPhoto($photo, $thumbnail); switch ($this->bucketType) { case self::BUCKET_TYPE_PRIVATE: - // TODO: use the B2 b2_download_file_by_id method so filenames are harder to guess if (is_null($this->downloadToken)) { $this->downloadToken = $client->getDownloadAuthToken(); } - return sprintf('%s?Authorization=%s', $fileDownloadUrl, $this->downloadToken); + // Once I sort out the issue with b2_download_file_by_id, this line can be removed + return sprintf('%s/file/%s/%s?Authorization=%s', $client->getDownloadUrl(), $this->configuration->container_name, $pathOnStorage, $this->downloadToken); + + /** @var BackblazeB2FileIdCache $b2Cache */ + $b2Cache = BackblazeB2FileIdCache::where('storage_path', $pathOnStorage)->first(); + if (is_null($b2Cache)) + { + // TODO: lookup the file on B2 to get the file ID + Log::warning(sprintf('B2 file ID not found in cache: %s', $pathOnStorage)); + return ''; + } + + return sprintf('%s/b2api/v2/b2_download_file_by_id?fileId=%s&Authorization=%s', $client->getDownloadUrl(), urlencode($b2Cache->b2_file_id), urlencode($this->downloadToken)); case self::BUCKET_TYPE_PUBLIC: - return $fileDownloadUrl; + /* + * 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. + */ + return sprintf('%s/file/%s/%s', $client->getDownloadUrl(), $this->configuration->container_name, $pathOnStorage); } } From 69422ffaa42cda7d2881c5925cab17e224e3f9a3 Mon Sep 17 00:00:00 2001 From: Andy Heathershaw Date: Wed, 11 Sep 2019 14:59:25 +0100 Subject: [PATCH 6/8] Backblaze #135 - implemented a retry and backoff period for 500/503 errors --- app/Exceptions/BackblazeRetryException.php | 25 +++++++++++++++++ app/Services/BackblazeB2Service.php | 32 +++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 app/Exceptions/BackblazeRetryException.php diff --git a/app/Exceptions/BackblazeRetryException.php b/app/Exceptions/BackblazeRetryException.php new file mode 100644 index 0000000..557ebfd --- /dev/null +++ b/app/Exceptions/BackblazeRetryException.php @@ -0,0 +1,25 @@ +innerException; + } + + public function __construct($httpCode, \Exception $innerException) + { + parent::__construct('Backblaze requested to retry the request'); + + $this->innerException = $innerException; + } +} \ No newline at end of file diff --git a/app/Services/BackblazeB2Service.php b/app/Services/BackblazeB2Service.php index 2ffb95c..57149ee 100644 --- a/app/Services/BackblazeB2Service.php +++ b/app/Services/BackblazeB2Service.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Exceptions\BackblazeRetryException; use Illuminate\Support\Facades\Log; class BackblazeB2Service @@ -238,6 +239,27 @@ class BackblazeB2Service } private function sendRequest($url, $method = 'GET', $postData = null, array $postOptions = []) + { + $exponentialBackoff = 1; + $numberOfRetries = 5; // this effectively gives us 31 seconds of retries (1+2+4+8+16) + $numberOfTimesTried = 0; + + while ($numberOfTimesTried < $numberOfRetries) + { + try + { + return $this->sendRequestReal($url, $method, $postData, $postOptions); + } + catch (BackblazeRetryException $ex) + { + // Keep backing off + $exponentialBackoff *= $exponentialBackoff; + $numberOfTimesTried++; + } + } + } + + private function sendRequestReal($url, $method = 'GET', $postData = null, array $postOptions = []) { $postOptions = array_merge( [ @@ -298,7 +320,15 @@ class BackblazeB2Service Log::info(sprintf('Received HTTP code %d', $httpCode)); Log::debug($result); - if ($httpCode != 200 && $httpCode != 304) + // According to the Backblaze B2 Protocol, if we get a 500/503, we should retry the request + if ($httpCode == 500 || $httpCode == 503) + { + throw new BackblazeRetryException( + $httpCode, + new \Exception(sprintf('Exception from Backblaze B2: %s', $result)) + ); + } + else if ($httpCode != 200 && $httpCode != 304) { throw new \Exception(sprintf('Exception from Backblaze B2: %s', $result)); } From a6825bcef90bdaa7f05759047945540af6cacf20 Mon Sep 17 00:00:00 2001 From: Andy Heathershaw Date: Sat, 14 Sep 2019 10:04:09 +0100 Subject: [PATCH 7/8] Backblaze #135 - implemented the re-use of the upload token/URL. Fetching file contents now works by using the b2_download_file_by_id method with an auth header. --- app/AlbumSources/BackblazeB2Source.php | 20 ++-- app/Services/BackblazeB2Service.php | 84 ++++++++++++--- public/b2_test.php | 143 +++++++++++++++++++++++++ 3 files changed, 228 insertions(+), 19 deletions(-) create mode 100644 public/b2_test.php diff --git a/app/AlbumSources/BackblazeB2Source.php b/app/AlbumSources/BackblazeB2Source.php index c9a4c9e..5223d32 100644 --- a/app/AlbumSources/BackblazeB2Source.php +++ b/app/AlbumSources/BackblazeB2Source.php @@ -74,14 +74,22 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource */ public function fetchPhotoContent(Photo $photo, $thumbnail = null) { - // Use the same URLs that the public would use to fetch the file - $urlToPhoto = $this->getUrlToPhoto($photo, $thumbnail); + $pathOnStorage = $this->getPathToPhoto($photo, $thumbnail); - $ch = curl_init($urlToPhoto); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $fileContent = curl_exec($ch); + // First we need the file ID - return EntityBody::fromString($fileContent); + /** @var BackblazeB2FileIdCache $b2Cache */ + $b2Cache = BackblazeB2FileIdCache::where('storage_path', $pathOnStorage)->first(); + if (is_null($b2Cache)) + { + // TODO: lookup the file on B2 to get the file ID + Log::warning(sprintf('B2 file ID not found in cache: %s', $pathOnStorage)); + return EntityBody::fromString(''); + } + + return EntityBody::fromString( + $this->getClient()->downloadFile($b2Cache->b2_file_id) + ); } /** diff --git a/app/Services/BackblazeB2Service.php b/app/Services/BackblazeB2Service.php index 57149ee..ceab613 100644 --- a/app/Services/BackblazeB2Service.php +++ b/app/Services/BackblazeB2Service.php @@ -55,6 +55,18 @@ class BackblazeB2Service */ private $config; + /** + * Current file upload token. + * @var string + */ + private $uploadAuthToken; + + /** + * Current upload URL. + * @var string + */ + private $uploadUrl; + public function __construct() { $this->config = config('services.backblaze_b2'); @@ -92,15 +104,13 @@ class BackblazeB2Service public function downloadFile($fileID) { - $downloadToken = $this->getDownloadAuthToken(); - return $this->sendRequest( - sprintf('%s/b2api/v2/b2_download_file_by_id?fileId=%s', $this->accountApiUrl, urlencode($fileID), urlencode($downloadToken)), + sprintf('%s/b2api/v2/b2_download_file_by_id?fileId=%s', $this->accountApiUrl, urlencode($fileID)), 'GET', null, [ 'http_headers' => [ - sprintf('Authorization: %s', $downloadToken) + sprintf('Authorization: %s', $this->authToken) ], 'response_body_is_json' => false ] @@ -155,6 +165,35 @@ class BackblazeB2Service throw new \Exception('No upload URL/authorization token returned from Backblaze B2.'); } + $exponentialBackoff = 1; + $numberOfRetries = 5; // this effectively gives us 31 seconds of retries (1+2+4+8+16) + $numberOfTimesTried = 0; + + while ($numberOfTimesTried < $numberOfRetries) + { + try + { + return $this->uploadFileReal($pathToFileToUpload, $pathToStorage, $uploadUrl, $authorizationToken); + } + catch (BackblazeRetryException $ex) + { + sleep($exponentialBackoff); + + // Get a new upload token + $this->uploadAuthToken = null; + $this->uploadUrl = null; + + list($uploadUrl, $authorizationToken) = $this->getUploadUrl(); + + // Keep backing off + $exponentialBackoff *= $exponentialBackoff; + $numberOfTimesTried++; + } + } + } + + private function uploadFileReal($pathToFileToUpload, $pathToStorage, $uploadUrl, $authorizationToken) + { $fileSize = filesize($pathToFileToUpload); $handle = fopen($pathToFileToUpload, 'r'); $fileContents = fread($handle, $fileSize); @@ -168,7 +207,7 @@ class BackblazeB2Service sprintf('X-Bz-File-Name: %s', urlencode($pathToStorage)) ]; - $result = $this->sendRequest( + $result = $this->sendRequestReal( $uploadUrl, 'POST', $fileContents, @@ -200,15 +239,21 @@ class BackblazeB2Service throw new \Exception(sprintf('The bucket \'%s\' was not found or your API key does not have access.', $bucketName)); } - private function getUploadUrl() + private function getUploadUrl($alwaysGetNewToken = false) { - $result = $this->sendRequest( - sprintf('%s/b2api/v2/b2_get_upload_url', $this->accountApiUrl), - 'POST', - ['bucketId' => $this->bucketId] - ); + if (is_null($this->uploadAuthToken) || $alwaysGetNewToken) + { + $result = $this->sendRequest( + sprintf('%s/b2api/v2/b2_get_upload_url', $this->accountApiUrl), + 'POST', + ['bucketId' => $this->bucketId] + ); - return [$result->uploadUrl, $result->authorizationToken]; + $this->uploadAuthToken = $result->authorizationToken; + $this->uploadUrl = $result->uploadUrl; + } + + return [$this->uploadUrl, $this->uploadAuthToken]; } private function getBasicHttpClient($url, $method = 'GET', array $httpHeaders = []) @@ -252,7 +297,15 @@ class BackblazeB2Service } catch (BackblazeRetryException $ex) { + // Clear the upload token if requested + if (isset($postOptions['clear_upload_token_on_retry']) && $postOptions['clear_upload_token_on_retry']) + { + $this->uploadAuthToken = null; + $this->uploadUrl = null; + } + // Keep backing off + sleep($exponentialBackoff); $exponentialBackoff *= $exponentialBackoff; $numberOfTimesTried++; } @@ -318,7 +371,12 @@ class BackblazeB2Service $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); Log::info(sprintf('Received HTTP code %d', $httpCode)); - Log::debug($result); + + // 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); + } // According to the Backblaze B2 Protocol, if we get a 500/503, we should retry the request if ($httpCode == 500 || $httpCode == 503) diff --git a/public/b2_test.php b/public/b2_test.php new file mode 100644 index 0000000..a741604 --- /dev/null +++ b/public/b2_test.php @@ -0,0 +1,143 @@ +' . $uri . '

'; + + $server_output = curl_exec($session); // Let's do this! + + if (curl_getinfo($session, CURLINFO_HTTP_CODE) != 200) + { + echo '

' . $server_output . '

'; + } + else + { + echo '

' . (strlen($server_output) . ' bytes received') . '

'; // Tell me about the rabbits, George! + } + + curl_close ($session); // Clean up + + //$download_url = ""; // From b2_authorize_account call + $file_id = "4_z731245f41efc196b6dda0018_f116729ca6de74b38_d20190910_m132847_c002_v0001127_t0021"; // The ID of the file you want to download + $uri = $download_url . "/b2api/v2/b2_download_file_by_id?fileId=" . $file_id . '&Authorization=' . $auth_token; + + $session = curl_init($uri); + + curl_setopt($session, CURLOPT_HTTPGET, true); // HTTP GET + curl_setopt($session, CURLOPT_RETURNTRANSFER, true); // Receive server response + + echo '

' . $uri . '

'; + + $server_output = curl_exec($session); // Let's do this! + + if (curl_getinfo($session, CURLINFO_HTTP_CODE) != 200) + { + echo '

' . $server_output . '

'; + } + else + { + echo '

' . (strlen($server_output) . ' bytes received') . '

'; // Tell me about the rabbits, George! + } + + curl_close ($session); // Clean up +} + +function b2_download_file_by_name($download_url, $auth_token) +{ + //$download_url = ""; // From b2_authorize_account call + $bucket_name = "andysh-bt-test"; // The NAME of the bucket you want to download from + $file_name = "B2-Test-Album/preview/7tgoy55do1vjv180ytlp.jpeg"; // The name of the file you want to download + $uri = $download_url . "/file/" . $bucket_name . "/" . $file_name; + + $session = curl_init($uri); + + curl_setopt($session, CURLOPT_HTTPGET, true); // HTTP GET + curl_setopt($session, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($session, CURLOPT_RETURNTRANSFER, true); // Receive server response + + echo '

' . $uri . '

'; + + $server_output = curl_exec($session); // Let's do this! + + if (curl_getinfo($session, CURLINFO_HTTP_CODE) != 200) + { + echo '

' . $server_output . '

'; + } + else + { + echo '

' . (strlen($server_output) . ' bytes received') . '

'; // Tell me about the rabbits, George! + } + + curl_close ($session); // Clean up + + // You will need to use the account authorization token if your bucket's type is allPrivate. + + //$download_url = ""; // From b2_authorize_account call + $bucket_name = "andysh-bt-test"; // The NAME of the bucket you want to download from + $file_name = "B2-Test-Album/preview/7tgoy55do1vjv180ytlp.jpeg"; // The name of the file you want to download + //$auth_token = ""; // From b2_authorize_account call + $uri = $download_url . "/file/" . $bucket_name . "/" . $file_name . '?Authorization=' . $auth_token; + + $session = curl_init($uri); + + curl_setopt($session, CURLOPT_HTTPGET, true); // HTTP POST + curl_setopt($session, CURLOPT_RETURNTRANSFER, true); // Receive server response + + echo '

' . $uri . '

'; + + $server_output = curl_exec($session); // Let's do this! + + if (curl_getinfo($session, CURLINFO_HTTP_CODE) != 200) + { + echo '

' . $server_output . '

'; + } + else + { + echo '

' . (strlen($server_output) . ' bytes received') . '

'; // Tell me about the rabbits, George! + } + + curl_close ($session); // Clean up +} + +?> +

b2_authorize_account

+ + +

b2_download_file_by_name

+downloadUrl, $authorize_account_result->authorizationToken); ?> + +

b2_download_file_by_id

+downloadUrl, $authorize_account_result->authorizationToken); ?> From 99cafbc9a5afbcb17ba80844aef440204fbdf888 Mon Sep 17 00:00:00 2001 From: Andy Heathershaw Date: Sat, 14 Sep 2019 15:35:05 +0100 Subject: [PATCH 8/8] Backblaze #135 - B2 storage source now removes the current file version before uploading a new one --- app/AlbumSources/BackblazeB2Source.php | 40 +++++++++++++++++--------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/app/AlbumSources/BackblazeB2Source.php b/app/AlbumSources/BackblazeB2Source.php index 5223d32..b206c1a 100644 --- a/app/AlbumSources/BackblazeB2Source.php +++ b/app/AlbumSources/BackblazeB2Source.php @@ -53,12 +53,9 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource // Create or update our cache record - /** @var BackblazeB2FileIdCache $b2Cache */ - $b2Cache = BackblazeB2FileIdCache::where('storage_path', $pathOnStorage)->first(); + $b2Cache = $this->getB2FileFromCache($pathOnStorage); if (is_null($b2Cache)) { - // TODO: lookup the file on B2 to get the file ID - Log::warning(sprintf('B2 file ID not found in cache: %s', $pathOnStorage)); return; } @@ -78,12 +75,9 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource // First we need the file ID - /** @var BackblazeB2FileIdCache $b2Cache */ - $b2Cache = BackblazeB2FileIdCache::where('storage_path', $pathOnStorage)->first(); + $b2Cache = $this->getB2FileFromCache($pathOnStorage); if (is_null($b2Cache)) { - // TODO: lookup the file on B2 to get the file ID - Log::warning(sprintf('B2 file ID not found in cache: %s', $pathOnStorage)); return EntityBody::fromString(''); } @@ -123,12 +117,9 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource // Once I sort out the issue with b2_download_file_by_id, this line can be removed return sprintf('%s/file/%s/%s?Authorization=%s', $client->getDownloadUrl(), $this->configuration->container_name, $pathOnStorage, $this->downloadToken); - /** @var BackblazeB2FileIdCache $b2Cache */ - $b2Cache = BackblazeB2FileIdCache::where('storage_path', $pathOnStorage)->first(); + $b2Cache = $this->getB2FileFromCache($pathOnStorage); if (is_null($b2Cache)) { - // TODO: lookup the file on B2 to get the file ID - Log::warning(sprintf('B2 file ID not found in cache: %s', $pathOnStorage)); return ''; } @@ -157,11 +148,17 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource { $pathOnStorage = $this->getPathToPhoto($photo, $thumbnail); + $b2Cache = $this->getB2FileFromCache($pathOnStorage); + if (!is_null($b2Cache)) + { + // Delete the current file version if we're replacing a file that already exists + $this->getClient()->deleteFile($b2Cache->b2_file_id, $pathOnStorage); + } + // Upload the file to B2 $b2FileID = $this->getClient()->uploadFile($tempFilename, $pathOnStorage); // Create or update our cache record - $b2Cache = BackblazeB2FileIdCache::where('storage_path', $pathOnStorage)->first(); if (is_null($b2Cache)) { $b2Cache = new BackblazeB2FileIdCache([ @@ -183,6 +180,23 @@ class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource parent::setConfiguration($configuration); } + /** + * @param $pathOnStorage + * @return BackblazeB2FileIdCache|null + */ + private function getB2FileFromCache($pathOnStorage) + { + $b2Cache = BackblazeB2FileIdCache::where('storage_path', $pathOnStorage)->first(); + if (is_null($b2Cache)) + { + // TODO: lookup the file on B2 to get the file ID + Log::warning(sprintf('B2 file ID not found in cache: %s', $pathOnStorage)); + return null; + } + + return $b2Cache; + } + private function getClient() { if (is_null($this->backblaze))