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