Compare commits

...

11 Commits

Author SHA1 Message Date
Andy Heathershaw b5d9b9c6cf Merge branch 'v2.2' of ssh://apps.andysh.uk:7999/aheathershaw/blue-twilight into v2.2 2019-09-14 15:40:33 +01:00
Andy Heathershaw 6692c530e4 Merge branch 'master' into v2.2 2019-09-14 15:40:14 +01:00
Andy Heathershaw da99b0b05a Merge branch 'feature/135-backblaze-driver' of aheathershaw/blue-twilight into master 2019-09-14 15:38:15 +01:00
Andy Heathershaw 99cafbc9a5 Backblaze #135 - B2 storage source now removes the current file version before uploading a new one 2019-09-14 15:35:05 +01:00
Andy Heathershaw a6825bcef9 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. 2019-09-14 10:04:09 +01:00
Andy Heathershaw 69422ffaa4 Backblaze #135 - implemented a retry and backoff period for 500/503 errors 2019-09-11 14:59:25 +01:00
Andy Heathershaw fb6754b8e9 Backblaze #135 - tried to implement b2_download_file_by_id for private buckets, but this doesn't work correctly, logged with Backblaze 2019-09-10 16:24:26 +01:00
Andy Heathershaw ce03b2596f 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. 2019-09-10 15:11:53 +01:00
Andy Heathershaw 608442d566 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. 2019-09-09 21:52:26 +01:00
Andy Heathershaw 437fe9fe1f Updated composer.lock file 2019-09-09 20:51:06 +01:00
Andy Heathershaw 4b6bdeba15 Backblaze #135 - added the storage UI and initial connectivity to B2 2019-09-09 20:35:32 +01:00
21 changed files with 1217 additions and 195 deletions

View File

@ -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);

View File

@ -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;

View File

@ -0,0 +1,248 @@
<?php
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
{
const BUCKET_TYPE_AUTO = 0;
const BUCKET_TYPE_PRIVATE = 1;
const BUCKET_TYPE_PUBLIC = 2;
/**
* @var BackblazeB2Service
*/
private $backblaze;
/**
* 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.
* @return void
*/
public function deleteAlbumContents()
{
// No need to do anything for the album container - once the files are gone, the virtual folder is also gone
}
/**
* 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)
{
$pathOnStorage = $this->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
);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class BackblazeB2FileIdCache extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'photo_id',
'storage_path',
'b2_file_id'
];
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Exceptions;
use Throwable;
class BackblazeRetryException extends \Exception
{
private $innerException;
/**
* @return mixed
*/
public function getInnerException()
{
return $this->innerException;
}
public function __construct($httpCode, \Exception $innerException)
{
parent::__construct('Backblaze requested to retry the request');
$this->innerException = $innerException;
}
}

View File

@ -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
];

View File

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

View File

@ -0,0 +1,400 @@
<?php
namespace App\Services;
use App\Exceptions\BackblazeRetryException;
use Illuminate\Support\Facades\Log;
class BackblazeB2Service
{
/**
* The individual URL for the account to use to access the API
* @var string
*/
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;
/**
* Current file upload token.
* @var string
*/
private $uploadAuthToken;
/**
* Current upload URL.
* @var string
*/
private $uploadUrl;
public function __construct()
{
$this->config = config('services.backblaze_b2');
}
public function authorizeAccount($force = false)
{
if (empty($this->authToken) || $force)
{
$result = $this->sendRequest($this->config['auth_url']);
if (!isset($result->authorizationToken))
{
throw new \Exception('Authorisation to Backblaze failed. Is the API key correct?');
}
$this->authToken = $result->authorizationToken;
$this->accountApiUrl = $result->apiUrl;
$this->accountID = $result->accountId;
$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;
}
}

View File

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

View File

@ -6,6 +6,7 @@
"type": "project",
"require": {
"php": ">=7.0.0",
"ext-curl": "*",
"ext-json": "*",
"laravel/framework": "5.5.*",
"rackspace/php-opencloud": "^1.16",

380
composer.lock generated
View File

@ -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": []
}

View File

@ -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,

View File

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

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateBackblazeB2FileIdCachesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('backblaze_b2_file_id_caches', function (Blueprint $table) {
$table->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');
}
}

143
public/b2_test.php Normal file
View File

@ -0,0 +1,143 @@
<?php
function b2_authorize_account()
{
$application_key_id = "0023254ec9bda08000000000a"; // Obtained from your B2 account page
$application_key = "K002eARNPUlxdj1XaVJbwEYPMz0c7e8"; // Obtained from your B2 account page
$credentials = base64_encode($application_key_id . ":" . $application_key);
$url = "https://api.backblazeb2.com/b2api/v2/b2_authorize_account";
$session = curl_init($url);
// Add headers
$headers = array();
$headers[] = "Accept: application/json";
$headers[] = "Authorization: Basic " . $credentials;
curl_setopt($session, CURLOPT_HTTPHEADER, $headers); // Add headers
curl_setopt($session, CURLOPT_HTTPGET, true); // HTTP GET
curl_setopt($session, CURLOPT_RETURNTRANSFER, true); // Receive server response
$server_output = curl_exec($session);
curl_close ($session);
echo ($server_output);
return json_decode($server_output);
}
function b2_download_file_by_id($download_url, $auth_token)
{
//$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;
$session = curl_init($uri);
curl_setopt($session, CURLOPT_HTTPGET, true); // HTTP GET
curl_setopt($session, CURLOPT_RETURNTRANSFER, true); // Receive server response
echo '<p>' . $uri . '</p>';
$server_output = curl_exec($session); // Let's do this!
if (curl_getinfo($session, CURLINFO_HTTP_CODE) != 200)
{
echo '<p>' . $server_output . '</p>';
}
else
{
echo '<p>' . (strlen($server_output) . ' bytes received') . '</p>'; // 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 '<p>' . $uri . '</p>';
$server_output = curl_exec($session); // Let's do this!
if (curl_getinfo($session, CURLINFO_HTTP_CODE) != 200)
{
echo '<p>' . $server_output . '</p>';
}
else
{
echo '<p>' . (strlen($server_output) . ' bytes received') . '</p>'; // 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 '<p>' . $uri . '</p>';
$server_output = curl_exec($session); // Let's do this!
if (curl_getinfo($session, CURLINFO_HTTP_CODE) != 200)
{
echo '<p>' . $server_output . '</p>';
}
else
{
echo '<p>' . (strlen($server_output) . ' bytes received') . '</p>'; // 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 '<p>' . $uri . '</p>';
$server_output = curl_exec($session); // Let's do this!
if (curl_getinfo($session, CURLINFO_HTTP_CODE) != 200)
{
echo '<p>' . $server_output . '</p>';
}
else
{
echo '<p>' . (strlen($server_output) . ' bytes received') . '</p>'; // Tell me about the rabbits, George!
}
curl_close ($session); // Clean up
}
?>
<h2>b2_authorize_account</h2>
<?php $authorize_account_result = b2_authorize_account(); ?>
<h2>b2_download_file_by_name</h2>
<?php b2_download_file_by_name($authorize_account_result->downloadUrl, $authorize_account_result->authorizationToken); ?>
<h2>b2_download_file_by_id</h2>
<?php b2_download_file_by_id($authorize_account_result->downloadUrl, $authorize_account_result->authorizationToken); ?>

View File

@ -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' => [

View File

@ -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:',

View File

@ -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'

View File

@ -63,6 +63,10 @@
@include(Theme::viewName('partials.admin_storages_rackspace_options'))
</div>
<div v-if="storage_driver == 'BackblazeB2Source'">
@include(Theme::viewName('partials.admin_storages_backblaze_b2_options'))
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="is-default" name="is_default"@if (old('is_default', $storage->is_default)) checked="checked"@endif>
<label class="form-check-label" for="is-default">@lang('forms.default_storage_label')</label>

View File

@ -61,6 +61,11 @@
@include(Theme::viewName('partials.admin_storages_rackspace_options'))
@endif
<div v-if="storage_driver == 'BackblazeB2Source'">
<hr/>
@include(Theme::viewName('partials.admin_storages_backblaze_b2_options'))
</div>
<div class="text-right">
<a href="{{ route('storage.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a>
<button type="submit" class="btn btn-success"><i class="fa fa-fw fa-check"></i> @lang('forms.save_action')</button>

View File

@ -0,0 +1,58 @@
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label" for="access-key">@lang('forms.storage_application_key_id_label')</label>
<input type="text" class="form-control{{ $errors->has('access_key') ? ' is-invalid' : '' }}" id="access-key" name="access_key" value="{{ old('access_key', $storage->access_key) }}">
<small class="form-text text-muted">@lang('admin.storage_backblaze_access_key_id_help')</small>
@if ($errors->has('access_key'))
<div class="invalid-feedback">
<strong>{{ $errors->first('access_key') }}</strong>
</div>
@endif
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label" for="secret-key">@lang('forms.storage_application_key_label')</label>
<input type="text" class="form-control{{ $errors->has('secret_key') ? ' is-invalid' : '' }}" id="secret-key" name="secret_key" value="{{ old('secret_key', $storage->secret_key) }}">
@if ($errors->has('secret_key'))
<div class="invalid-feedback">
<strong>{{ $errors->first('secret_key') }}</strong>
</div>
@endif
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label" for="container-name">@lang('forms.storage_bucket_name_label')</label>
<input type="text" class="form-control{{ $errors->has('container_name') ? ' is-invalid' : '' }}" id="container-name" name="container_name" value="{{ old('container_name', $storage->container_name) }}">
@if ($errors->has('container_name'))
<div class="invalid-feedback">
<strong>{{ $errors->first('container_name') }}</strong>
</div>
@endif
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label" for="b2-bucket-type">@lang('forms.storage_b2_bucket_type.label')</label>
<select class="form-control{{ $errors->has('b2_bucket_type') ? ' is-invalid' : '' }}" id="b2-bucket-type" name="b2_bucket_type">
<option value="0"{{ old('b2_bucket_type', $storage->b2_bucket_type) === 0 ? ' selected="selected"' : '' }}>@lang('forms.storage_b2_bucket_type.autodetect')</option>
<option value="1"{{ old('b2_bucket_type', $storage->b2_bucket_type) === 1 ? ' selected="selected"' : '' }}>@lang('forms.storage_b2_bucket_type.private')</option>
<option value="2"{{ old('b2_bucket_type', $storage->b2_bucket_type) === 2 ? ' selected="selected"' : '' }}>@lang('forms.storage_b2_bucket_type.public')</option>
</select>
@if ($errors->has('b2_bucket_type'))
<div class="invalid-feedback">
<strong>{{ $errors->first('b2_bucket_type') }}</strong>
</div>
@endif
</div>
</div>
</div>