2019-09-09 20:35:32 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
2019-09-11 14:59:25 +01:00
|
|
|
use App\Exceptions\BackblazeRetryException;
|
2019-09-10 15:11:53 +01:00
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
|
2019-09-09 20:35:32 +01:00
|
|
|
class BackblazeB2Service
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* The individual URL for the account to use to access the API
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private $accountApiUrl;
|
|
|
|
|
2019-09-09 21:52:26 +01:00
|
|
|
/**
|
|
|
|
* ID of the account in Backblaze B2.
|
|
|
|
* @var string
|
|
|
|
*/
|
2019-09-09 20:35:32 +01:00
|
|
|
private $accountID;
|
|
|
|
|
2019-09-09 21:52:26 +01:00
|
|
|
/**
|
|
|
|
* The base URL for public access to the account's files.
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private $downloadUrl;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Authorisation header for authenticating to the API.
|
|
|
|
* @var string
|
|
|
|
*/
|
2019-09-09 20:35:32 +01:00
|
|
|
private $authHeader;
|
|
|
|
|
2019-09-09 21:52:26 +01:00
|
|
|
/**
|
|
|
|
* Authorisation token for accessing the API post-authentication.
|
|
|
|
* @var string
|
|
|
|
*/
|
2019-09-09 20:35:32 +01:00
|
|
|
private $authToken;
|
|
|
|
|
2019-09-09 21:52:26 +01:00
|
|
|
/**
|
|
|
|
* ID of the bucket.
|
|
|
|
* @var string
|
|
|
|
*/
|
2019-09-09 20:35:32 +01:00
|
|
|
private $bucketId;
|
|
|
|
|
2019-09-09 21:52:26 +01:00
|
|
|
/**
|
|
|
|
* Type of the bucket.
|
|
|
|
* @var integer
|
|
|
|
*/
|
|
|
|
private $bucketType;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Configuration related to the Backblaze B2 service.
|
|
|
|
* @var \Illuminate\Config\Repository|mixed
|
|
|
|
*/
|
2019-09-09 20:35:32 +01:00
|
|
|
private $config;
|
|
|
|
|
2019-09-14 10:04:09 +01:00
|
|
|
/**
|
|
|
|
* Current file upload token.
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private $uploadAuthToken;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Current upload URL.
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private $uploadUrl;
|
|
|
|
|
2019-09-09 20:35:32 +01:00
|
|
|
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;
|
2019-09-09 21:52:26 +01:00
|
|
|
$this->downloadUrl = $result->downloadUrl;
|
2019-09-09 20:35:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-10 15:11:53 +01:00
|
|
|
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(
|
2019-09-14 10:04:09 +01:00
|
|
|
sprintf('%s/b2api/v2/b2_download_file_by_id?fileId=%s', $this->accountApiUrl, urlencode($fileID)),
|
2019-09-10 15:11:53 +01:00
|
|
|
'GET',
|
|
|
|
null,
|
|
|
|
[
|
|
|
|
'http_headers' => [
|
2019-09-14 10:04:09 +01:00
|
|
|
sprintf('Authorization: %s', $this->authToken)
|
2019-09-10 15:11:53 +01:00
|
|
|
],
|
|
|
|
'response_body_is_json' => false
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-09-09 21:52:26 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-09-09 20:35:32 +01:00
|
|
|
public function setBucketName($bucketName)
|
|
|
|
{
|
2019-09-09 21:52:26 +01:00
|
|
|
$bucketDetails = $this->getBucketDetailsFromName($bucketName);
|
|
|
|
|
|
|
|
$this->bucketId = $bucketDetails->bucketId;
|
|
|
|
$this->bucketType = $bucketDetails->bucketType;
|
2019-09-09 20:35:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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.');
|
|
|
|
}
|
|
|
|
|
2019-09-14 10:04:09 +01:00
|
|
|
$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)
|
|
|
|
{
|
2019-09-09 20:35:32 +01:00
|
|
|
$fileSize = filesize($pathToFileToUpload);
|
|
|
|
$handle = fopen($pathToFileToUpload, 'r');
|
|
|
|
$fileContents = fread($handle, $fileSize);
|
|
|
|
fclose($handle);
|
|
|
|
$fileContentsSha1 = sha1_file($pathToFileToUpload);
|
|
|
|
|
2019-09-10 15:11:53 +01:00
|
|
|
$httpHeaders = [
|
2019-09-09 20:35:32 +01:00
|
|
|
sprintf('Authorization: %s', $authorizationToken),
|
|
|
|
'Content-Type: b2/x-auto',
|
|
|
|
sprintf('X-Bz-Content-Sha1: %s', $fileContentsSha1),
|
|
|
|
sprintf('X-Bz-File-Name: %s', urlencode($pathToStorage))
|
2019-09-10 15:11:53 +01:00
|
|
|
];
|
2019-09-09 20:35:32 +01:00
|
|
|
|
2019-09-14 10:04:09 +01:00
|
|
|
$result = $this->sendRequestReal(
|
2019-09-10 15:11:53 +01:00
|
|
|
$uploadUrl,
|
|
|
|
'POST',
|
|
|
|
$fileContents,
|
|
|
|
[
|
|
|
|
'http_headers' => $httpHeaders,
|
|
|
|
'post_body_is_json' => false
|
|
|
|
]
|
|
|
|
);
|
2019-09-09 20:35:32 +01:00
|
|
|
|
2019-09-10 15:11:53 +01:00
|
|
|
return $result->fileId;
|
2019-09-09 20:35:32 +01:00
|
|
|
}
|
|
|
|
|
2019-09-09 21:52:26 +01:00
|
|
|
private function getBucketDetailsFromName($bucketName)
|
2019-09-09 20:35:32 +01:00
|
|
|
{
|
|
|
|
$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)
|
|
|
|
{
|
2019-09-09 21:52:26 +01:00
|
|
|
return $result->buckets[0];
|
2019-09-09 20:35:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
throw new \Exception(sprintf('The bucket \'%s\' was not found or your API key does not have access.', $bucketName));
|
|
|
|
}
|
|
|
|
|
2019-09-14 10:04:09 +01:00
|
|
|
private function getUploadUrl($alwaysGetNewToken = false)
|
2019-09-09 20:35:32 +01:00
|
|
|
{
|
2019-09-14 10:04:09 +01:00
|
|
|
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;
|
|
|
|
}
|
2019-09-09 20:35:32 +01:00
|
|
|
|
2019-09-14 10:04:09 +01:00
|
|
|
return [$this->uploadUrl, $this->uploadAuthToken];
|
2019-09-09 20:35:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-09-10 15:11:53 +01:00
|
|
|
private function sendRequest($url, $method = 'GET', $postData = null, array $postOptions = [])
|
2019-09-11 14:59:25 +01:00
|
|
|
{
|
|
|
|
$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)
|
|
|
|
{
|
2019-09-14 10:04:09 +01:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2019-09-11 14:59:25 +01:00
|
|
|
// Keep backing off
|
2019-09-14 10:04:09 +01:00
|
|
|
sleep($exponentialBackoff);
|
2019-09-11 14:59:25 +01:00
|
|
|
$exponentialBackoff *= $exponentialBackoff;
|
|
|
|
$numberOfTimesTried++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function sendRequestReal($url, $method = 'GET', $postData = null, array $postOptions = [])
|
2019-09-09 20:35:32 +01:00
|
|
|
{
|
2019-09-10 15:11:53 +01:00
|
|
|
$postOptions = array_merge(
|
|
|
|
[
|
|
|
|
'authorization_token' => null,
|
|
|
|
'http_headers' => [],
|
|
|
|
'post_body_is_json' => true,
|
|
|
|
'response_body_is_json' => true
|
|
|
|
],
|
|
|
|
$postOptions
|
|
|
|
);
|
|
|
|
$httpHeaders = $postOptions['http_headers'];
|
2019-09-09 20:35:32 +01:00
|
|
|
|
2019-09-10 15:11:53 +01:00
|
|
|
// Some methods may need to override the authorization token used
|
|
|
|
if (empty($postOptions['authorization_token']))
|
2019-09-09 20:35:32 +01:00
|
|
|
{
|
2019-09-10 15:11:53 +01:00
|
|
|
// 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);
|
|
|
|
}
|
2019-09-09 20:35:32 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2019-09-10 15:11:53 +01:00
|
|
|
// Override - use the auth token specified
|
|
|
|
$httpHeaders[] = sprintf('Authorization: %s', $postOptions['authorization_token']);
|
2019-09-09 20:35:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$ch = $this->getBasicHttpClient($url, $method, $httpHeaders);
|
|
|
|
|
|
|
|
if (!is_null($postData))
|
|
|
|
{
|
2019-09-10 15:11:53 +01:00
|
|
|
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);
|
2019-09-09 20:35:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$result = curl_exec($ch);
|
|
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
|
2019-09-10 15:11:53 +01:00
|
|
|
Log::info(sprintf('Received HTTP code %d', $httpCode));
|
2019-09-14 10:04:09 +01:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
2019-09-10 15:11:53 +01:00
|
|
|
|
2019-09-11 14:59:25 +01:00
|
|
|
// 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)
|
2019-09-09 20:35:32 +01:00
|
|
|
{
|
|
|
|
throw new \Exception(sprintf('Exception from Backblaze B2: %s', $result));
|
|
|
|
}
|
|
|
|
|
|
|
|
curl_close($ch);
|
|
|
|
|
2019-09-10 15:11:53 +01:00
|
|
|
return $postOptions['response_body_is_json']
|
|
|
|
? json_decode($result)
|
|
|
|
: $result;
|
2019-09-09 20:35:32 +01:00
|
|
|
}
|
|
|
|
}
|