Client   F
last analyzed

Complexity

Total Complexity 94

Size/Duplication

Total Lines 791
Duplicated Lines 0 %

Test Coverage

Coverage 60%

Importance

Changes 18
Bugs 2 Features 0
Metric Value
wmc 94
eloc 300
c 18
b 2
f 0
dl 0
loc 791
ccs 186
cts 310
cp 0.6
rs 2

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 16 3
A listBuckets() 0 13 2
A updateBucket() 0 19 4
A deleteBucket() 0 12 3
A createBucket() 0 15 2
A sendAuthorizedRequest() 0 9 1
B uploadLargeFile() 0 38 7
A deleteFile() 0 20 5
A getFileUri() 0 17 5
A startLargeFile() 0 6 1
A finishLargeFile() 0 17 1
A authorizeAccount() 0 15 2
B copy() 0 42 8
A getFileIdFromBucketAndFileName() 0 10 3
C listFiles() 0 50 13
A fileExists() 0 5 1
B upload() 0 68 7
B download() 0 27 7
A getUploadPartUrl() 0 4 1
A getBucketNameFromId() 0 7 3
B getFile() 0 35 9
A uploadParts() 0 47 3
A getBucketIdFromName() 0 7 3

How to fix   Complexity   

Complex Class

Complex classes like Client often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Client, and based on these observations, apply Extract Interface, too.

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
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
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