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 new file mode 100644 index 0000000..b206c1a --- /dev/null +++ b/app/AlbumSources/BackblazeB2Source.php @@ -0,0 +1,248 @@ +getPathToPhoto($photo, $thumbnail); + + // Create or update our cache record + + $b2Cache = $this->getB2FileFromCache($pathOnStorage); + if (is_null($b2Cache)) + { + return; + } + + $this->getClient()->deleteFile($b2Cache->b2_file_id, $pathOnStorage); + $b2Cache->delete(); + } + + /** + * Fetches the contents of a thumbnail for a photo. + * @param Photo $photo Photo to fetch the thumbnail for. + * @param string $thumbnail Thumbnail to fetch (or null to fetch the original.) + * @return EntityBody + */ + public function fetchPhotoContent(Photo $photo, $thumbnail = null) + { + $pathOnStorage = $this->getPathToPhoto($photo, $thumbnail); + + // First we need the file ID + + $b2Cache = $this->getB2FileFromCache($pathOnStorage); + if (is_null($b2Cache)) + { + return EntityBody::fromString(''); + } + + return EntityBody::fromString( + $this->getClient()->downloadFile($b2Cache->b2_file_id) + ); + } + + /** + * 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) + { + $client = $this->getClient(); + $pathOnStorage = $this->getPathToPhoto($photo, $thumbnail); + + switch ($this->bucketType) + { + case self::BUCKET_TYPE_PRIVATE: + if (is_null($this->downloadToken)) + { + $this->downloadToken = $client->getDownloadAuthToken(); + } + + // 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); + + $b2Cache = $this->getB2FileFromCache($pathOnStorage); + if (is_null($b2Cache)) + { + 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: + /* + * 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); + } + } + + /** + * 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); + + $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 + 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) + { + 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)) + { + $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; + } + + 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/BackblazeB2FileIdCache.php b/app/BackblazeB2FileIdCache.php new file mode 100644 index 0000000..b1765cd --- /dev/null +++ b/app/BackblazeB2FileIdCache.php @@ -0,0 +1,19 @@ +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/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/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 new file mode 100644 index 0000000..ceab613 --- /dev/null +++ b/app/Services/BackblazeB2Service.php @@ -0,0 +1,400 @@ +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; + $this->downloadUrl = $result->downloadUrl; + } + } + + public function deleteFile($fileID, $fileName) + { + $this->sendRequest( + sprintf('%s/b2api/v2/b2_delete_file_version', $this->accountApiUrl), + 'POST', + [ + 'fileId' => $fileID, + 'fileName' => $fileName + ] + ); + } + + public function downloadFile($fileID) + { + return $this->sendRequest( + sprintf('%s/b2api/v2/b2_download_file_by_id?fileId=%s', $this->accountApiUrl, urlencode($fileID)), + 'GET', + null, + [ + 'http_headers' => [ + sprintf('Authorization: %s', $this->authToken) + ], + 'response_body_is_json' => false + ] + ); + } + + 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) + { + $bucketDetails = $this->getBucketDetailsFromName($bucketName); + + $this->bucketId = $bucketDetails->bucketId; + $this->bucketType = $bucketDetails->bucketType; + } + + 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.'); + } + + $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); + fclose($handle); + $fileContentsSha1 = sha1_file($pathToFileToUpload); + + $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)) + ]; + + $result = $this->sendRequestReal( + $uploadUrl, + 'POST', + $fileContents, + [ + 'http_headers' => $httpHeaders, + 'post_body_is_json' => false + ] + ); + + return $result->fileId; + } + + private function getBucketDetailsFromName($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]; + } + + throw new \Exception(sprintf('The bucket \'%s\' was not found or your API key does not have access.', $bucketName)); + } + + private function getUploadUrl($alwaysGetNewToken = false) + { + if (is_null($this->uploadAuthToken) || $alwaysGetNewToken) + { + $result = $this->sendRequest( + sprintf('%s/b2api/v2/b2_get_upload_url', $this->accountApiUrl), + 'POST', + ['bucketId' => $this->bucketId] + ); + + $this->uploadAuthToken = $result->authorizationToken; + $this->uploadUrl = $result->uploadUrl; + } + + return [$this->uploadUrl, $this->uploadAuthToken]; + } + + 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, 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) + { + // 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++; + } + } + } + + private function sendRequestReal($url, $method = 'GET', $postData = null, array $postOptions = []) + { + $postOptions = array_merge( + [ + 'authorization_token' => null, + 'http_headers' => [], + 'post_body_is_json' => true, + 'response_body_is_json' => true + ], + $postOptions + ); + $httpHeaders = $postOptions['http_headers']; + + // Some methods may need to override the authorization token used + if (empty($postOptions['authorization_token'])) + { + // 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 + { + // Override - use the auth token specified + $httpHeaders[] = sprintf('Authorization: %s', $postOptions['authorization_token']); + } + + $ch = $this->getBasicHttpClient($url, $method, $httpHeaders); + + if (!is_null($postData)) + { + if ($postOptions['post_body_is_json']) + { + $postData = json_encode($postData); + } + + curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); + } + + Log::info(sprintf('%s: %s', strtoupper($method), $url)); + Log::debug('HTTP headers:', $httpHeaders); + + // Only log a post body if we have one and it's in JSON format (i.e. not a file upload) + if (!is_null($postData) && $postOptions['post_body_is_json']) + { + Log::debug($postData); + } + + $result = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + Log::info(sprintf('Received HTTP code %d', $httpCode)); + + // Only log a result if we have one and it's in JSON format (i.e. not a file download) + if (!is_null($result) && $result !== false && $postOptions['response_body_is_json']) + { + Log::debug($result); + } + + // 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)); + } + + curl_close($ch); + + return $postOptions['response_body_is_json'] + ? json_decode($result) + : $result; + } +} \ No newline at end of file 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/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/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": [] } diff --git a/config/services.php b/config/services.php index eab4683..e7ace08 100644 --- a/config/services.php +++ b/config/services.php @@ -14,6 +14,11 @@ return [ | */ + 'backblaze_b2' => [ + 'auth_url' => 'https://api.backblazeb2.com/b2api/v2/b2_authorize_account', + 'download_token_lifetime' => 300 + ], + 'gitea' => [ 'api_url' => 'https://apps.andysh.uk/api/v1', 'cache_time_seconds' => 3600, 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/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'); + } +} 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); ?> diff --git a/readme.md b/readme.md index de4f604..f959c4d 100644 --- a/readme.md +++ b/readme.md @@ -6,28 +6,24 @@ It takes advantage of modern frameworks (Laravel, Bootstrap 4, VueJS) as well as You can see Blue Twilight in action on my own photo gallery - the reason I wrote Blue Twilight - at: [photos.andysh.uk](https://photos.andysh.uk) -## Version 2 Branch (2.0, 2.1, etc.) +## Blue Twilight Cloud -Version 2 is the first version I have released as open-source. The previous version (1.1.2) was only ever used on my own gallery. - -This is a major update that includes 2 key new features: fine-grained security controls, and nested albums. It also updates the default template to Bootstrap v4 and VueJS (replacing KnockoutJS.) - -With the launch of version 2.0.0, this has now been officially released - see the [Releases](https://apps.andysh.uk/aheathershaw/blue-twilight/releases) page for the latest version. +If you want your own dedicated, private instance of Blue Twilight without the hassle of managing servers, hosting and updates - check out [Blue Twilight Cloud](https://showmy.photos). ## Demo System -See Blue Twilight in action using the demo system. Full details are [available here](https://andysh.uk/software/blue-twilight-php-photo-gallery/demo/). +See Blue Twilight in action using the demo system. Full details are [available here](https://showmy.photos/demo/). -The link to the demo system is: http://demo.showmy.photos. Login with: +The link to the demo system is: https://demo.showmy.photos. Login with: * Username: **demo@demo.com** * Password: **demo123** ## Useful Links -* [Blue Twilight website](https://andysh.uk/software/blue-twilight-php-photo-gallery/) -* [User Manual](https://andysh.uk/software/blue-twilight-php-photo-gallery/manual/) -* [Installation Guide](https://andysh.uk/software/blue-twilight-php-photo-gallery/manual/installation/) +* [Blue Twilight website](https://showmy.photos/) +* [User Manual](https://showmy.photos/user-guide/) +* [Installation Guide](https://showmy.photos/user-guide/installation/) * [Issues/Tasks](https://apps.andysh.uk/aheathershaw/blue-twilight/issues) * [Roadmap](https://apps.andysh.uk/aheathershaw/blue-twilight/milestones) 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..0e1cc96 100644 --- a/resources/lang/en/forms.php +++ b/resources/lang/en/forms.php @@ -98,7 +98,15 @@ 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_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/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..4450e21 --- /dev/null +++ b/resources/views/themes/base/partials/admin_storages_backblaze_b2_options.blade.php @@ -0,0 +1,58 @@ +
+
+
+ + + @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 +
+
+
+
+ + + + @if ($errors->has('b2_bucket_type')) +
+ {{ $errors->first('b2_bucket_type') }} +
+ @endif +
+
+
\ No newline at end of file