1 | <?php |
||
2 | |||
3 | namespace BackblazeB2; |
||
4 | |||
5 | use BackblazeB2\Exceptions\B2Exception; |
||
6 | use BackblazeB2\Exceptions\NotFoundException; |
||
7 | use BackblazeB2\Exceptions\ValidationException; |
||
8 | use BackblazeB2\Http\Client as HttpClient; |
||
9 | use Carbon\Carbon; |
||
10 | use GuzzleHttp\Exception\GuzzleException; |
||
11 | |||
12 | class Client |
||
13 | { |
||
14 | const B2_API_BASE_URL = 'https://api.backblazeb2.com'; |
||
15 | const B2_API_V1 = '/b2api/v1/'; |
||
16 | protected $accountId; |
||
17 | protected $applicationKey; |
||
18 | protected $authToken; |
||
19 | protected $apiUrl; |
||
20 | protected $downloadUrl; |
||
21 | protected $client; |
||
22 | protected $reAuthTime; |
||
23 | protected $authTimeoutSeconds; |
||
24 | |||
25 | /** |
||
26 | * Accepts the account ID, application key and an optional array of options. |
||
27 | * |
||
28 | * @param $accountId |
||
29 | * @param $applicationKey |
||
30 | * @param array $options |
||
31 | * |
||
32 | * @throws \Exception |
||
33 | */ |
||
34 | 30 | public function __construct($accountId, $applicationKey, array $options = []) |
|
35 | { |
||
36 | 30 | $this->accountId = $accountId; |
|
37 | 30 | $this->applicationKey = $applicationKey; |
|
38 | |||
39 | 30 | $this->authTimeoutSeconds = 12 * 60 * 60; // 12 hour default |
|
40 | 30 | if (isset($options['auth_timeout_seconds'])) { |
|
41 | 1 | $this->authTimeoutSeconds = $options['auth_timeout_seconds']; |
|
42 | } |
||
43 | |||
44 | // set reauthorize time to force an authentication to take place |
||
45 | 30 | $this->reAuthTime = Carbon::now('UTC')->subSeconds($this->authTimeoutSeconds * 2); |
|
46 | |||
47 | 30 | $this->client = new HttpClient(['exceptions' => false]); |
|
48 | 30 | if (isset($options['client'])) { |
|
49 | 30 | $this->client = $options['client']; |
|
50 | } |
||
51 | 30 | } |
|
52 | |||
53 | /** |
||
54 | * Create a bucket with the given name and type. |
||
55 | * |
||
56 | * @param array $options |
||
57 | * |
||
58 | * @throws ValidationException |
||
59 | * @throws GuzzleException If the request fails. |
||
60 | * @throws B2Exception If the B2 server replies with an error. |
||
61 | * |
||
62 | * @return Bucket |
||
63 | */ |
||
64 | 5 | public function createBucket(array $options) |
|
65 | { |
||
66 | 5 | if (!in_array($options['BucketType'], [Bucket::TYPE_PUBLIC, Bucket::TYPE_PRIVATE])) { |
|
67 | 1 | throw new ValidationException( |
|
68 | 1 | sprintf('Bucket type must be %s or %s', Bucket::TYPE_PRIVATE, Bucket::TYPE_PUBLIC) |
|
69 | ); |
||
70 | } |
||
71 | |||
72 | 4 | $response = $this->sendAuthorizedRequest('POST', 'b2_create_bucket', [ |
|
73 | 4 | 'accountId' => $this->accountId, |
|
74 | 4 | 'bucketName' => $options['BucketName'], |
|
75 | 4 | 'bucketType' => $options['BucketType'], |
|
76 | ]); |
||
77 | |||
78 | 3 | return new Bucket($response['bucketId'], $response['bucketName'], $response['bucketType']); |
|
79 | } |
||
80 | |||
81 | /** |
||
82 | * Updates the type attribute of a bucket by the given ID. |
||
83 | * |
||
84 | * @param array $options |
||
85 | * |
||
86 | * @throws ValidationException |
||
87 | * @throws GuzzleException If the request fails. |
||
88 | * @throws B2Exception If the B2 server replies with an error. |
||
89 | * |
||
90 | * @return Bucket |
||
91 | */ |
||
92 | 2 | public function updateBucket(array $options) |
|
93 | { |
||
94 | 2 | if (!in_array($options['BucketType'], [Bucket::TYPE_PUBLIC, Bucket::TYPE_PRIVATE])) { |
|
95 | throw new ValidationException( |
||
96 | sprintf('Bucket type must be %s or %s', Bucket::TYPE_PRIVATE, Bucket::TYPE_PUBLIC) |
||
97 | ); |
||
98 | } |
||
99 | |||
100 | 2 | if (!isset($options['BucketId']) && isset($options['BucketName'])) { |
|
101 | $options['BucketId'] = $this->getBucketIdFromName($options['BucketName']); |
||
102 | } |
||
103 | |||
104 | 2 | $response = $this->sendAuthorizedRequest('POST', 'b2_update_bucket', [ |
|
105 | 2 | 'accountId' => $this->accountId, |
|
106 | 2 | 'bucketId' => $options['BucketId'], |
|
107 | 2 | 'bucketType' => $options['BucketType'], |
|
108 | ]); |
||
109 | |||
110 | 2 | return new Bucket($response['bucketId'], $response['bucketName'], $response['bucketType']); |
|
111 | } |
||
112 | |||
113 | /** |
||
114 | * Returns a list of bucket objects representing the buckets on the account. |
||
115 | * |
||
116 | * @throws GuzzleException If the request fails. |
||
117 | * @throws B2Exception If the B2 server replies with an error. |
||
118 | * |
||
119 | * @return array |
||
120 | */ |
||
121 | 2 | public function listBuckets() |
|
122 | { |
||
123 | 2 | $buckets = []; |
|
124 | |||
125 | 2 | $response = $this->sendAuthorizedRequest('POST', 'b2_list_buckets', [ |
|
126 | 2 | 'accountId' => $this->accountId, |
|
127 | ]); |
||
128 | |||
129 | 2 | foreach ($response['buckets'] as $bucket) { |
|
130 | 1 | $buckets[] = new Bucket($bucket['bucketId'], $bucket['bucketName'], $bucket['bucketType']); |
|
131 | } |
||
132 | |||
133 | 2 | return $buckets; |
|
134 | } |
||
135 | |||
136 | /** |
||
137 | * Deletes the bucket identified by its ID. |
||
138 | * |
||
139 | * @param array $options |
||
140 | * |
||
141 | * @throws GuzzleException If the request fails. |
||
142 | * @throws B2Exception If the B2 server replies with an error. |
||
143 | * |
||
144 | * @return bool |
||
145 | */ |
||
146 | 3 | public function deleteBucket(array $options) |
|
147 | { |
||
148 | 3 | if (!isset($options['BucketId']) && isset($options['BucketName'])) { |
|
149 | $options['BucketId'] = $this->getBucketIdFromName($options['BucketName']); |
||
150 | } |
||
151 | |||
152 | 3 | $this->sendAuthorizedRequest('POST', 'b2_delete_bucket', [ |
|
153 | 3 | 'accountId' => $this->accountId, |
|
154 | 3 | 'bucketId' => $options['BucketId'], |
|
155 | ]); |
||
156 | |||
157 | 1 | return true; |
|
158 | } |
||
159 | |||
160 | /** |
||
161 | * Uploads a file to a bucket and returns a File object. |
||
162 | * |
||
163 | * @param array $options |
||
164 | * |
||
165 | * @throws GuzzleException If the request fails. |
||
166 | * @throws B2Exception If the B2 server replies with an error. |
||
167 | * |
||
168 | * @return File |
||
169 | */ |
||
170 | 3 | public function upload(array $options) |
|
171 | { |
||
172 | // Clean the path if it starts with /. |
||
173 | 3 | if (substr($options['FileName'], 0, 1) === '/') { |
|
174 | $options['FileName'] = ltrim($options['FileName'], '/'); |
||
175 | } |
||
176 | |||
177 | 3 | if (!isset($options['BucketId']) && isset($options['BucketName'])) { |
|
178 | $options['BucketId'] = $this->getBucketIdFromName($options['BucketName']); |
||
179 | } |
||
180 | |||
181 | // Retrieve the URL that we should be uploading to. |
||
182 | |||
183 | 3 | $response = $this->sendAuthorizedRequest('POST', 'b2_get_upload_url', [ |
|
184 | 3 | 'bucketId' => $options['BucketId'], |
|
185 | ]); |
||
186 | |||
187 | 3 | $uploadEndpoint = $response['uploadUrl']; |
|
188 | 3 | $uploadAuthToken = $response['authorizationToken']; |
|
189 | |||
190 | 3 | if (is_resource($options['Body'])) { |
|
191 | // We need to calculate the file's hash incrementally from the stream. |
||
192 | 1 | $context = hash_init('sha1'); |
|
193 | 1 | hash_update_stream($context, $options['Body']); |
|
194 | 1 | $hash = hash_final($context); |
|
195 | |||
196 | // Similarly, we have to use fstat to get the size of the stream. |
||
197 | 1 | $size = fstat($options['Body'])['size']; |
|
198 | |||
199 | // Rewind the stream before passing it to the HTTP client. |
||
200 | 1 | rewind($options['Body']); |
|
201 | } else { |
||
202 | // We've been given a simple string body, it's super simple to calculate the hash and size. |
||
203 | 2 | $hash = sha1($options['Body']); |
|
204 | 2 | $size = strlen($options['Body']); |
|
205 | } |
||
206 | |||
207 | 3 | if (!isset($options['FileLastModified'])) { |
|
208 | 2 | $options['FileLastModified'] = round(microtime(true) * 1000); |
|
209 | } |
||
210 | |||
211 | 3 | if (!isset($options['FileContentType'])) { |
|
212 | 2 | $options['FileContentType'] = 'b2/x-auto'; |
|
213 | } |
||
214 | |||
215 | 3 | $customHeaders = $options['Headers'] ?? []; |
|
216 | 3 | $response = $this->client->guzzleRequest('POST', $uploadEndpoint, [ |
|
217 | 3 | 'headers' => array_merge([ |
|
218 | 3 | 'Authorization' => $uploadAuthToken, |
|
219 | 3 | 'Content-Type' => $options['FileContentType'], |
|
220 | 3 | 'Content-Length' => $size, |
|
221 | 3 | 'X-Bz-File-Name' => $options['FileName'], |
|
222 | 3 | 'X-Bz-Content-Sha1' => $hash, |
|
223 | 3 | 'X-Bz-Info-src_last_modified_millis' => $options['FileLastModified'], |
|
224 | 3 | ], $customHeaders), |
|
225 | 3 | 'body' => $options['Body'], |
|
226 | ]); |
||
227 | |||
228 | 3 | return new File( |
|
229 | 3 | $response['fileId'], |
|
230 | 3 | $response['fileName'], |
|
231 | 3 | $response['contentSha1'], |
|
232 | 3 | $response['contentLength'], |
|
233 | 3 | $response['contentType'], |
|
234 | 3 | $response['fileInfo'], |
|
235 | 3 | $response['bucketId'], |
|
236 | 3 | $response['action'], |
|
237 | 3 | $response['uploadTimestamp'] |
|
238 | ); |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * Download a file from a B2 bucket. |
||
243 | * |
||
244 | * @param array $options |
||
245 | * |
||
246 | * @return bool |
||
247 | */ |
||
248 | 7 | public function download(array $options) |
|
249 | { |
||
250 | 7 | if (!isset($options['FileId']) && !isset($options['BucketName']) && isset($options['BucketId'])) { |
|
251 | $options['BucketName'] = $this->getBucketNameFromId($options['BucketId']); |
||
252 | } |
||
253 | |||
254 | 7 | $this->authorizeAccount(); |
|
255 | |||
256 | 7 | $requestUrl = null; |
|
257 | 7 | $customHeaders = $options['Headers'] ?? []; |
|
258 | $requestOptions = [ |
||
259 | 7 | 'headers' => array_merge([ |
|
260 | 7 | 'Authorization' => $this->authToken, |
|
261 | 7 | ], $customHeaders), |
|
262 | 7 | 'sink' => isset($options['SaveAs']) ? $options['SaveAs'] : null, |
|
263 | ]; |
||
264 | |||
265 | 7 | if (isset($options['FileId'])) { |
|
266 | 4 | $requestOptions['query'] = ['fileId' => $options['FileId']]; |
|
267 | 4 | $requestUrl = $this->downloadUrl.'/b2api/v1/b2_download_file_by_id'; |
|
268 | } else { |
||
269 | 3 | $requestUrl = sprintf('%s/file/%s/%s', $this->downloadUrl, $options['BucketName'], $options['FileName']); |
|
270 | } |
||
271 | |||
272 | 7 | $response = $this->client->guzzleRequest('GET', $requestUrl, $requestOptions, false); |
|
273 | |||
274 | 5 | return isset($options['SaveAs']) ? true : $response; |
|
275 | } |
||
276 | |||
277 | /** |
||
278 | * Copy a file. |
||
279 | * |
||
280 | * $options: |
||
281 | * required BucketName or BucketId the source bucket |
||
282 | * required FileName the file to copy |
||
283 | * required SaveAs the path and file name to save to |
||
284 | * optional DestinationBucketId or DestinationBucketName, the destination bucket |
||
285 | * |
||
286 | * @param array $options |
||
287 | * |
||
288 | * @throws B2Exception |
||
289 | * @throws GuzzleException |
||
290 | * @throws NotFoundException |
||
291 | * |
||
292 | * @return File |
||
293 | */ |
||
294 | 1 | public function copy(array $options) |
|
295 | { |
||
296 | 1 | $options['FileName'] = ltrim($options['FileName'], '/'); |
|
297 | 1 | $options['SaveAs'] = ltrim($options['SaveAs'], '/'); |
|
298 | |||
299 | 1 | if (!isset($options['DestinationBucketId']) && isset($options['DestinationBucketName'])) { |
|
300 | $options['DestinationBucketId'] = $this->getBucketIdFromName($options['DestinationBucketName']); |
||
301 | } |
||
302 | |||
303 | 1 | if (!isset($options['BucketId']) && isset($options['BucketName'])) { |
|
304 | $options['BucketId'] = $this->getBucketIdFromName($options['BucketName']); |
||
305 | } |
||
306 | |||
307 | 1 | $sourceFiles = $this->listFiles([ |
|
308 | 1 | 'BucketId' => $options['BucketId'], |
|
309 | 1 | 'FileName' => $options['FileName'], |
|
310 | ]); |
||
311 | 1 | $sourceFileId = !empty($sourceFiles) ? $sourceFiles[0]->getId() : false; |
|
312 | 1 | if (!$sourceFileId) { |
|
313 | throw new NotFoundException('Source file not found in B2'); |
||
314 | } |
||
315 | |||
316 | $json = [ |
||
317 | 1 | 'sourceFileId' => $sourceFileId, |
|
318 | 1 | 'fileName' => $options['SaveAs'], |
|
319 | ]; |
||
320 | 1 | if (isset($options['DestinationBucketId'])) { |
|
321 | $json['DestinationBucketId'] = $options['DestinationBucketId']; |
||
322 | } |
||
323 | |||
324 | 1 | $response = $this->sendAuthorizedRequest('POST', 'b2_copy_file', $json); |
|
325 | |||
326 | 1 | return new File( |
|
327 | 1 | $response['fileId'], |
|
328 | 1 | $response['fileName'], |
|
329 | 1 | $response['contentSha1'], |
|
330 | 1 | $response['contentLength'], |
|
331 | 1 | $response['contentType'], |
|
332 | 1 | $response['fileInfo'], |
|
333 | 1 | $response['bucketId'], |
|
334 | 1 | $response['action'], |
|
335 | 1 | $response['uploadTimestamp'] |
|
336 | ); |
||
337 | } |
||
338 | |||
339 | /** |
||
340 | * Retrieve a collection of File objects representing the files stored inside a bucket. |
||
341 | * |
||
342 | * @param array $options |
||
343 | * |
||
344 | * @throws GuzzleException If the request fails. |
||
345 | * @throws B2Exception If the B2 server replies with an error. |
||
346 | * |
||
347 | * @return array |
||
348 | */ |
||
349 | 3 | public function listFiles(array $options) |
|
350 | { |
||
351 | // if FileName is set, we only attempt to retrieve information about that single file. |
||
352 | 3 | $fileName = !empty($options['FileName']) ? $options['FileName'] : null; |
|
353 | |||
354 | 3 | $nextFileName = null; |
|
355 | 3 | $maxFileCount = 1000; |
|
356 | |||
357 | 3 | $prefix = isset($options['Prefix']) ? $options['Prefix'] : ''; |
|
358 | 3 | $delimiter = isset($options['Delimiter']) ? $options['Delimiter'] : null; |
|
359 | |||
360 | 3 | $files = []; |
|
361 | |||
362 | 3 | if (!isset($options['BucketId']) && isset($options['BucketName'])) { |
|
363 | $options['BucketId'] = $this->getBucketIdFromName($options['BucketName']); |
||
364 | } |
||
365 | |||
366 | 3 | if ($fileName) { |
|
367 | 1 | $nextFileName = $fileName; |
|
368 | 1 | $maxFileCount = 1; |
|
369 | } |
||
370 | |||
371 | 3 | $this->authorizeAccount(); |
|
372 | |||
373 | // B2 returns, at most, 1000 files per "page". Loop through the pages and compile an array of File objects. |
||
374 | 3 | while (true) { |
|
375 | 3 | $response = $this->sendAuthorizedRequest('POST', 'b2_list_file_names', [ |
|
376 | 3 | 'bucketId' => $options['BucketId'], |
|
377 | 3 | 'startFileName' => $nextFileName, |
|
378 | 3 | 'maxFileCount' => $maxFileCount, |
|
379 | 3 | 'prefix' => $prefix, |
|
380 | 3 | 'delimiter' => $delimiter, |
|
381 | ]); |
||
382 | |||
383 | 3 | foreach ($response['files'] as $file) { |
|
384 | // if we have a file name set, only retrieve information if the file name matches |
||
385 | 2 | if (!$fileName || ($fileName === $file['fileName'])) { |
|
386 | 2 | $files[] = new File($file['fileId'], $file['fileName'], $file['contentSha1'], $file['size'], $file['contentType'], $file['fileInfo'], $file['bucketId'], $file['action'], $file['uploadTimestamp']); |
|
387 | } |
||
388 | } |
||
389 | |||
390 | 3 | if ($fileName || $response['nextFileName'] === null) { |
|
391 | // We've got all the files - break out of loop. |
||
392 | 3 | break; |
|
393 | } |
||
394 | |||
395 | 1 | $nextFileName = $response['nextFileName']; |
|
396 | } |
||
397 | |||
398 | 3 | return $files; |
|
399 | } |
||
400 | |||
401 | /** |
||
402 | * Test whether a file exists in B2 for the given bucket. |
||
403 | * |
||
404 | * @param array $options |
||
405 | * |
||
406 | * @return bool |
||
407 | */ |
||
408 | public function fileExists(array $options) |
||
409 | { |
||
410 | $files = $this->listFiles($options); |
||
411 | |||
412 | return !empty($files); |
||
413 | } |
||
414 | |||
415 | /** |
||
416 | * Returns a single File object representing a file stored on B2. |
||
417 | * |
||
418 | * @param array $options |
||
419 | * |
||
420 | * @throws GuzzleException |
||
421 | * @throws NotFoundException If no file id was provided and BucketName + FileName does not resolve to a file, a NotFoundException is thrown. |
||
422 | * @throws GuzzleException If the request fails. |
||
423 | * @throws B2Exception If the B2 server replies with an error. |
||
424 | * |
||
425 | * @return File |
||
426 | */ |
||
427 | 4 | public function getFile(array $options) |
|
428 | { |
||
429 | 4 | if (!isset($options['FileId']) && isset($options['BucketId']) && isset($options['FileName'])) { |
|
430 | $files = $this->listFiles([ |
||
431 | 'BucketId' => $options['BucketId'], |
||
432 | 'FileName' => $options['FileName'], |
||
433 | ]); |
||
434 | |||
435 | if (empty($files)) { |
||
436 | throw new NotFoundException(); |
||
437 | } |
||
438 | |||
439 | $options['FileId'] = $files[0]->getId(); |
||
440 | 4 | } elseif (!isset($options['FileId']) && isset($options['BucketName']) && isset($options['FileName'])) { |
|
441 | $options['FileId'] = $this->getFileIdFromBucketAndFileName($options['BucketName'], $options['FileName']); |
||
442 | |||
443 | if (!$options['FileId']) { |
||
444 | throw new NotFoundException(); |
||
445 | } |
||
446 | } |
||
447 | |||
448 | 4 | $response = $this->sendAuthorizedRequest('POST', 'b2_get_file_info', [ |
|
449 | 4 | 'fileId' => $options['FileId'], |
|
450 | ]); |
||
451 | |||
452 | 3 | return new File( |
|
453 | 3 | $response['fileId'], |
|
454 | 3 | $response['fileName'], |
|
455 | 3 | $response['contentSha1'], |
|
456 | 3 | $response['contentLength'], |
|
457 | 3 | $response['contentType'], |
|
458 | 3 | $response['fileInfo'], |
|
459 | 3 | $response['bucketId'], |
|
460 | 3 | $response['action'], |
|
461 | 3 | $response['uploadTimestamp'] |
|
462 | ); |
||
463 | } |
||
464 | |||
465 | /** |
||
466 | * Deletes the file identified by ID from Backblaze B2. |
||
467 | * |
||
468 | * @param array $options |
||
469 | * |
||
470 | * @throws GuzzleException |
||
471 | * @throws NotFoundException |
||
472 | * @throws GuzzleException If the request fails. |
||
473 | * @throws B2Exception If the B2 server replies with an error. |
||
474 | * |
||
475 | * @return bool |
||
476 | */ |
||
477 | 3 | public function deleteFile(array $options) |
|
478 | { |
||
479 | 3 | if (!isset($options['FileName'])) { |
|
480 | 2 | $file = $this->getFile($options); |
|
481 | |||
482 | 2 | $options['FileName'] = $file->getName(); |
|
483 | } |
||
484 | |||
485 | 3 | if (!isset($options['FileId']) && isset($options['BucketName']) && isset($options['FileName'])) { |
|
486 | $file = $this->getFile($options); |
||
487 | |||
488 | $options['FileId'] = $file->getId(); |
||
489 | } |
||
490 | |||
491 | 3 | $this->sendAuthorizedRequest('POST', 'b2_delete_file_version', [ |
|
492 | 3 | 'fileName' => $options['FileName'], |
|
493 | 3 | 'fileId' => $options['FileId'], |
|
494 | ]); |
||
495 | |||
496 | 2 | return true; |
|
497 | } |
||
498 | |||
499 | /** |
||
500 | * Fetches authorization and uri for a file, to allow a third-party system to download public and private files. |
||
501 | * |
||
502 | * @param array $options |
||
503 | * |
||
504 | * @throws GuzzleException |
||
505 | * @throws NotFoundException |
||
506 | * @throws GuzzleException If the request fails. |
||
507 | * @throws B2Exception If the B2 server replies with an error. |
||
508 | * |
||
509 | * @return array |
||
510 | */ |
||
511 | public function getFileUri(array $options) |
||
512 | { |
||
513 | if (!isset($options['FileId']) && !isset($options['BucketName']) && isset($options['BucketId'])) { |
||
514 | $options['BucketName'] = $this->getBucketNameFromId($options['BucketId']); |
||
515 | } |
||
516 | |||
517 | $this->authorizeAccount(); |
||
518 | |||
519 | if (isset($options['FileId'])) { |
||
520 | $requestUri = $this->downloadUrl.'/b2api/v1/b2_download_file_by_id?fileId='.urlencode($options['FileId']); |
||
521 | } else { |
||
522 | $requestUri = sprintf('%s/file/%s/%s', $this->downloadUrl, $options['BucketName'], $options['FileName']); |
||
523 | } |
||
524 | |||
525 | return [ |
||
526 | 'Authorization' => $this->authToken, |
||
527 | 'Uri' => $requestUri, |
||
528 | ]; |
||
529 | } |
||
530 | |||
531 | /** |
||
532 | * Authorize the B2 account in order to get an auth token and API/download URLs. |
||
533 | */ |
||
534 | 29 | protected function authorizeAccount() |
|
535 | { |
||
536 | 29 | if (Carbon::now('UTC')->timestamp < $this->reAuthTime->timestamp) { |
|
537 | 5 | return; |
|
538 | } |
||
539 | |||
540 | 29 | $response = $this->client->guzzleRequest('GET', self::B2_API_BASE_URL.self::B2_API_V1.'b2_authorize_account', [ |
|
541 | 29 | 'auth' => [$this->accountId, $this->applicationKey], |
|
542 | ]); |
||
543 | |||
544 | 29 | $this->authToken = $response['authorizationToken']; |
|
545 | 29 | $this->apiUrl = $response['apiUrl'].self::B2_API_V1; |
|
546 | 29 | $this->downloadUrl = $response['downloadUrl']; |
|
547 | 29 | $this->reAuthTime = Carbon::now('UTC'); |
|
548 | 29 | $this->reAuthTime->addSeconds($this->authTimeoutSeconds); |
|
549 | 29 | } |
|
550 | |||
551 | /** |
||
552 | * Maps the provided bucket name to the appropriate bucket ID. |
||
553 | * |
||
554 | * @param $name |
||
555 | * |
||
556 | * @return mixed |
||
557 | */ |
||
558 | protected function getBucketIdFromName($name) |
||
559 | { |
||
560 | $buckets = $this->listBuckets(); |
||
561 | |||
562 | foreach ($buckets as $bucket) { |
||
563 | if ($bucket->getName() === $name) { |
||
564 | return $bucket->getId(); |
||
565 | } |
||
566 | } |
||
567 | } |
||
568 | |||
569 | /** |
||
570 | * Maps the provided bucket ID to the appropriate bucket name. |
||
571 | * |
||
572 | * @param $id |
||
573 | * |
||
574 | * @return mixed |
||
575 | */ |
||
576 | protected function getBucketNameFromId($id) |
||
577 | { |
||
578 | $buckets = $this->listBuckets(); |
||
579 | |||
580 | foreach ($buckets as $bucket) { |
||
581 | if ($bucket->getId() === $id) { |
||
582 | return $bucket->getName(); |
||
583 | } |
||
584 | } |
||
585 | } |
||
586 | |||
587 | /** |
||
588 | * @param $bucketName |
||
589 | * @param $fileName |
||
590 | * |
||
591 | * @return mixed |
||
592 | */ |
||
593 | protected function getFileIdFromBucketAndFileName($bucketName, $fileName) |
||
594 | { |
||
595 | $files = $this->listFiles([ |
||
596 | 'BucketName' => $bucketName, |
||
597 | 'FileName' => $fileName, |
||
598 | ]); |
||
599 | |||
600 | foreach ($files as $file) { |
||
601 | if ($file->getName() === $fileName) { |
||
602 | return $file->getId(); |
||
603 | } |
||
604 | } |
||
605 | } |
||
606 | |||
607 | /** |
||
608 | * Uploads a large file using b2 large file procedure. |
||
609 | * |
||
610 | * @param array $options |
||
611 | * |
||
612 | * @return File |
||
613 | */ |
||
614 | public function uploadLargeFile(array $options) |
||
615 | { |
||
616 | if (substr($options['FileName'], 0, 1) === '/') { |
||
617 | $options['FileName'] = ltrim($options['FileName'], '/'); |
||
618 | } |
||
619 | |||
620 | //if last char of path is not a "/" then add a "/" |
||
621 | if (substr($options['FilePath'], -1) != '/') { |
||
622 | $options['FilePath'] = $options['FilePath'].'/'; |
||
623 | } |
||
624 | |||
625 | if (!isset($options['BucketId']) && isset($options['BucketName'])) { |
||
626 | $options['BucketId'] = $this->getBucketIdFromName($options['BucketName']); |
||
627 | } |
||
628 | |||
629 | if (!isset($options['FileContentType'])) { |
||
630 | $options['FileContentType'] = 'b2/x-auto'; |
||
631 | } |
||
632 | |||
633 | $this->authorizeAccount(); |
||
634 | |||
635 | // 1) b2_start_large_file, (returns fileId) |
||
0 ignored issues
–
show
|
|||
636 | $start = $this->startLargeFile($options['FileName'], $options['FileContentType'], $options['BucketId']); |
||
637 | |||
638 | // 2) b2_get_upload_part_url for each thread uploading (takes fileId) |
||
639 | $url = $this->getUploadPartUrl($start['fileId']); |
||
640 | |||
641 | // 3) b2_upload_part for each part of the file |
||
642 | $parts = $this->uploadParts($options['FilePath'].$options['FileName'], $url['uploadUrl'], $url['authorizationToken'], $options); |
||
643 | |||
644 | $sha1s = []; |
||
645 | |||
646 | foreach ($parts as $part) { |
||
647 | $sha1s[] = $part['contentSha1']; |
||
648 | } |
||
649 | |||
650 | // 4) b2_finish_large_file. |
||
651 | return $this->finishLargeFile($start['fileId'], $sha1s); |
||
652 | } |
||
653 | |||
654 | /** |
||
655 | * Starts the large file upload process. |
||
656 | * |
||
657 | * @param $fileName |
||
658 | * @param $contentType |
||
659 | * @param $bucketId |
||
660 | * |
||
661 | * @throws GuzzleException If the request fails. |
||
662 | * @throws B2Exception If the B2 server replies with an error. |
||
663 | * |
||
664 | * @return mixed |
||
665 | */ |
||
666 | protected function startLargeFile($fileName, $contentType, $bucketId) |
||
667 | { |
||
668 | return $this->sendAuthorizedRequest('POST', 'b2_start_large_file', [ |
||
669 | 'fileName' => $fileName, |
||
670 | 'contentType' => $contentType, |
||
671 | 'bucketId' => $bucketId, |
||
672 | ]); |
||
673 | } |
||
674 | |||
675 | /** |
||
676 | * Gets the url for the next large file part upload. |
||
677 | * |
||
678 | * @param $fileId |
||
679 | * |
||
680 | * @throws GuzzleException If the request fails. |
||
681 | * @throws B2Exception If the B2 server replies with an error. |
||
682 | * |
||
683 | * @return mixed |
||
684 | */ |
||
685 | protected function getUploadPartUrl($fileId) |
||
686 | { |
||
687 | return $this->sendAuthorizedRequest('POST', 'b2_get_upload_part_url', [ |
||
688 | 'fileId' => $fileId, |
||
689 | ]); |
||
690 | } |
||
691 | |||
692 | /** |
||
693 | * Uploads the file as "parts" of 100MB each. |
||
694 | * |
||
695 | * @param $filePath |
||
696 | * @param $uploadUrl |
||
697 | * @param $largeFileAuthToken |
||
698 | * @param $options |
||
699 | * |
||
700 | * @return array |
||
701 | */ |
||
702 | protected function uploadParts($filePath, $uploadUrl, $largeFileAuthToken, $options = []) |
||
703 | { |
||
704 | $return = []; |
||
705 | |||
706 | $minimum_part_size = 100 * (1000 * 1000); |
||
707 | |||
708 | $local_file_size = filesize($filePath); |
||
709 | $total_bytes_sent = 0; |
||
710 | $bytes_sent_for_part = $minimum_part_size; |
||
711 | $sha1_of_parts = []; |
||
712 | $part_no = 1; |
||
713 | $file_handle = fopen($filePath, 'r'); |
||
714 | |||
715 | while ($total_bytes_sent < $local_file_size) { |
||
716 | |||
717 | // Determine the number of bytes to send based on the minimum part size |
||
718 | if (($local_file_size - $total_bytes_sent) < $minimum_part_size) { |
||
719 | $bytes_sent_for_part = ($local_file_size - $total_bytes_sent); |
||
720 | } |
||
721 | |||
722 | // Get a sha1 of the part we are going to send |
||
723 | fseek($file_handle, $total_bytes_sent); |
||
724 | $data_part = fread($file_handle, $bytes_sent_for_part); |
||
725 | array_push($sha1_of_parts, sha1($data_part)); |
||
726 | fseek($file_handle, $total_bytes_sent); |
||
727 | |||
728 | $customHeaders = $options['Headers'] ?? []; |
||
729 | $response = $this->client->guzzleRequest('POST', $uploadUrl, [ |
||
730 | 'headers' => array_merge([ |
||
731 | 'Authorization' => $largeFileAuthToken, |
||
732 | 'Content-Length' => $bytes_sent_for_part, |
||
733 | 'X-Bz-Part-Number' => $part_no, |
||
734 | 'X-Bz-Content-Sha1' => $sha1_of_parts[$part_no - 1], |
||
735 | ], $customHeaders), |
||
736 | 'body' => $data_part, |
||
737 | ]); |
||
738 | |||
739 | $return[] = $response; |
||
740 | |||
741 | // Prepare for the next iteration of the loop |
||
742 | $part_no++; |
||
743 | $total_bytes_sent = $bytes_sent_for_part + $total_bytes_sent; |
||
744 | } |
||
745 | |||
746 | fclose($file_handle); |
||
747 | |||
748 | return $return; |
||
749 | } |
||
750 | |||
751 | /** |
||
752 | * Finishes the large file upload procedure. |
||
753 | * |
||
754 | * @param $fileId |
||
755 | * @param array $sha1s |
||
756 | * |
||
757 | * @throws GuzzleException If the request fails. |
||
758 | * @throws B2Exception If the B2 server replies with an error. |
||
759 | * |
||
760 | * @return File |
||
761 | */ |
||
762 | protected function finishLargeFile($fileId, array $sha1s) |
||
763 | { |
||
764 | $response = $this->sendAuthorizedRequest('POST', 'b2_finish_large_file', [ |
||
765 | 'fileId' => $fileId, |
||
766 | 'partSha1Array' => $sha1s, |
||
767 | ]); |
||
768 | |||
769 | return new File( |
||
770 | $response['fileId'], |
||
771 | $response['fileName'], |
||
772 | $response['contentSha1'], |
||
773 | $response['contentLength'], |
||
774 | $response['contentType'], |
||
775 | $response['fileInfo'], |
||
776 | $response['bucketId'], |
||
777 | $response['action'], |
||
778 | $response['uploadTimestamp'] |
||
779 | ); |
||
780 | } |
||
781 | |||
782 | /** |
||
783 | * Sends a authorized request to b2 API. |
||
784 | * |
||
785 | * @param string $method |
||
786 | * @param string $route |
||
787 | * @param array $json |
||
788 | * |
||
789 | * @throws GuzzleException If the request fails. |
||
790 | * @throws B2Exception If the B2 server replies with an error. |
||
791 | * |
||
792 | * @return mixed |
||
793 | */ |
||
794 | 22 | protected function sendAuthorizedRequest($method, $route, $json = []) |
|
795 | { |
||
796 | 22 | $this->authorizeAccount(); |
|
797 | |||
798 | 22 | return $this->client->guzzleRequest($method, $this->apiUrl.$route, [ |
|
799 | 'headers' => [ |
||
800 | 22 | 'Authorization' => $this->authToken, |
|
801 | ], |
||
802 | 22 | 'json' => $json, |
|
803 | ]); |
||
804 | } |
||
805 | } |
||
806 |
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.