Passed
Push — master ( a75c4c...5b3138 )
by
unknown
10:58 queued 08:02
created

Client   F

Complexity

Total Complexity 86

Size/Duplication

Total Lines 726
Duplicated Lines 0 %

Test Coverage

Coverage 56.27%

Importance

Changes 18
Bugs 2 Features 0
Metric Value
wmc 86
eloc 269
c 18
b 2
f 0
dl 0
loc 726
ccs 157
cts 279
cp 0.5627
rs 2

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 16 3
C listFiles() 0 50 13
A fileExists() 0 5 1
A listBuckets() 0 13 2
A updateBucket() 0 19 4
A deleteBucket() 0 12 3
A createBucket() 0 15 2
B upload() 0 65 7
B download() 0 27 7
A deleteFile() 0 20 5
B getFile() 0 35 9
A sendAuthorizedRequest() 0 9 1
B uploadLargeFile() 0 38 7
A getFileUri() 0 17 5
A startLargeFile() 0 6 1
A finishLargeFile() 0 17 1
A authorizeAccount() 0 15 2
A getFileIdFromBucketAndFileName() 0 10 3
A getUploadPartUrl() 0 4 1
A getBucketNameFromId() 0 7 3
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 29
    public function __construct($accountId, $applicationKey, array $options = [])
35
    {
36 29
        $this->accountId = $accountId;
37 29
        $this->applicationKey = $applicationKey;
38
39 29
        $this->authTimeoutSeconds = 12 * 60 * 60; // 12 hour default
40 29
        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 29
        $this->reAuthTime = Carbon::now('UTC')->subSeconds($this->authTimeoutSeconds * 2);
46
47 29
        $this->client = new HttpClient(['exceptions' => false]);
48 29
        if (isset($options['client'])) {
49 29
            $this->client = $options['client'];
50
        }
51 29
    }
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
        );
236
    }
237
238
    /**
239
     * Download a file from a B2 bucket.
240
     *
241
     * @param array $options
242
     *
243
     * @return bool
244
     */
245 7
    public function download(array $options)
246
    {
247 7
        if (!isset($options['FileId']) && !isset($options['BucketName']) && isset($options['BucketId'])) {
248
            $options['BucketName'] = $this->getBucketNameFromId($options['BucketId']);
249
        }
250
251 7
        $this->authorizeAccount();
252
253 7
        $requestUrl = null;
254 7
        $customHeaders = $options['Headers'] ?? [];
255
        $requestOptions = [
256 7
            'headers' => array_merge([
257 7
                'Authorization' => $this->authToken,
258 7
            ], $customHeaders),
259 7
            'sink' => isset($options['SaveAs']) ? $options['SaveAs'] : null,
260
        ];
261
262 7
        if (isset($options['FileId'])) {
263 4
            $requestOptions['query'] = ['fileId' => $options['FileId']];
264 4
            $requestUrl = $this->downloadUrl.'/b2api/v1/b2_download_file_by_id';
265
        } else {
266 3
            $requestUrl = sprintf('%s/file/%s/%s', $this->downloadUrl, $options['BucketName'], $options['FileName']);
267
        }
268
269 7
        $response = $this->client->guzzleRequest('GET', $requestUrl, $requestOptions, false);
270
271 5
        return isset($options['SaveAs']) ? true : $response;
272
    }
273
274
    /**
275
     * Retrieve a collection of File objects representing the files stored inside a bucket.
276
     *
277
     * @param array $options
278
     *
279
     * @throws GuzzleException If the request fails.
280
     * @throws B2Exception     If the B2 server replies with an error.
281
     *
282
     * @return array
283
     */
284 2
    public function listFiles(array $options)
285
    {
286
        // if FileName is set, we only attempt to retrieve information about that single file.
287 2
        $fileName = !empty($options['FileName']) ? $options['FileName'] : null;
288
289 2
        $nextFileName = null;
290 2
        $maxFileCount = 1000;
291
292 2
        $prefix = isset($options['Prefix']) ? $options['Prefix'] : '';
293 2
        $delimiter = isset($options['Delimiter']) ? $options['Delimiter'] : null;
294
295 2
        $files = [];
296
297 2
        if (!isset($options['BucketId']) && isset($options['BucketName'])) {
298
            $options['BucketId'] = $this->getBucketIdFromName($options['BucketName']);
299
        }
300
301 2
        if ($fileName) {
302
            $nextFileName = $fileName;
303
            $maxFileCount = 1;
304
        }
305
306 2
        $this->authorizeAccount();
307
308
        // B2 returns, at most, 1000 files per "page". Loop through the pages and compile an array of File objects.
309 2
        while (true) {
310 2
            $response = $this->sendAuthorizedRequest('POST', 'b2_list_file_names', [
311 2
                'bucketId'      => $options['BucketId'],
312 2
                'startFileName' => $nextFileName,
313 2
                'maxFileCount'  => $maxFileCount,
314 2
                'prefix'        => $prefix,
315 2
                'delimiter'     => $delimiter,
316
            ]);
317
318 2
            foreach ($response['files'] as $file) {
319
                // if we have a file name set, only retrieve information if the file name matches
320 1
                if (!$fileName || ($fileName === $file['fileName'])) {
321 1
                    $files[] = new File($file['fileId'], $file['fileName'], null, $file['size']);
322
                }
323
            }
324
325 2
            if ($fileName || $response['nextFileName'] === null) {
326
                // We've got all the files - break out of loop.
327 2
                break;
328
            }
329
330 1
            $nextFileName = $response['nextFileName'];
331
        }
332
333 2
        return $files;
334
    }
335
336
    /**
337
     * Test whether a file exists in B2 for the given bucket.
338
     *
339
     * @param array $options
340
     *
341
     * @return bool
342
     */
343
    public function fileExists(array $options)
344
    {
345
        $files = $this->listFiles($options);
346
347
        return !empty($files);
348
    }
349
350
    /**
351
     * Returns a single File object representing a file stored on B2.
352
     *
353
     * @param array $options
354
     *
355
     * @throws GuzzleException
356
     * @throws NotFoundException If no file id was provided and BucketName + FileName does not resolve to a file, a NotFoundException is thrown.
357
     * @throws GuzzleException   If the request fails.
358
     * @throws B2Exception       If the B2 server replies with an error.
359
     *
360
     * @return File
361
     */
362 4
    public function getFile(array $options)
363
    {
364 4
        if (!isset($options['FileId']) && isset($options['BucketId']) && isset($options['FileName'])) {
365
            $files = $this->listFiles([
366
                'BucketId' => $options['BucketId'],
367
                'FileName' => $options['FileName'],
368
            ]);
369
370
            if (empty($files)) {
371
                throw new NotFoundException();
372
            }
373
374
            $options['FileId'] = $files[0]->getId();
375 4
        } elseif (!isset($options['FileId']) && isset($options['BucketName']) && isset($options['FileName'])) {
376
            $options['FileId'] = $this->getFileIdFromBucketAndFileName($options['BucketName'], $options['FileName']);
377
378
            if (!$options['FileId']) {
379
                throw new NotFoundException();
380
            }
381
        }
382
383 4
        $response = $this->sendAuthorizedRequest('POST', 'b2_get_file_info', [
384 4
            'fileId' => $options['FileId'],
385
        ]);
386
387 3
        return new File(
388 3
            $response['fileId'],
389 3
            $response['fileName'],
390 3
            $response['contentSha1'],
391 3
            $response['contentLength'],
392 3
            $response['contentType'],
393 3
            $response['fileInfo'],
394 3
            $response['bucketId'],
395 3
            $response['action'],
396 3
            $response['uploadTimestamp']
397
        );
398
    }
399
400
    /**
401
     * Deletes the file identified by ID from Backblaze B2.
402
     *
403
     * @param array $options
404
     *
405
     * @throws GuzzleException
406
     * @throws NotFoundException
407
     * @throws GuzzleException   If the request fails.
408
     * @throws B2Exception       If the B2 server replies with an error.
409
     *
410
     * @return bool
411
     */
412 3
    public function deleteFile(array $options)
413
    {
414 3
        if (!isset($options['FileName'])) {
415 2
            $file = $this->getFile($options);
416
417 2
            $options['FileName'] = $file->getName();
418
        }
419
420 3
        if (!isset($options['FileId']) && isset($options['BucketName']) && isset($options['FileName'])) {
421
            $file = $this->getFile($options);
422
423
            $options['FileId'] = $file->getId();
424
        }
425
426 3
        $this->sendAuthorizedRequest('POST', 'b2_delete_file_version', [
427 3
            'fileName' => $options['FileName'],
428 3
            'fileId'   => $options['FileId'],
429
        ]);
430
431 2
        return true;
432
    }
433
434
    /**
435
     * Fetches authorization and uri for a file, to allow a third-party system to download public and private files.
436
     *
437
     * @param array $options
438
     *
439
     * @throws GuzzleException
440
     * @throws NotFoundException
441
     * @throws GuzzleException   If the request fails.
442
     * @throws B2Exception       If the B2 server replies with an error.
443
     *
444
     * @return array
445
     */
446
    public function getFileUri(array $options)
447
    {
448
        if (!isset($options['FileId']) && !isset($options['BucketName']) && isset($options['BucketId'])) {
449
            $options['BucketName'] = $this->getBucketNameFromId($options['BucketId']);
450
        }
451
452
        $this->authorizeAccount();
453
454
        if (isset($options['FileId'])) {
455
            $requestUri = $this->downloadUrl.'/b2api/v1/b2_download_file_by_id?fileId='.urlencode($options['FileId']);
456
        } else {
457
            $requestUri = sprintf('%s/file/%s/%s', $this->downloadUrl, $options['BucketName'], $options['FileName']);
458
        }
459
460
        return [
461
            'Authorization' => $this->authToken,
462
            'Uri'           => $requestUri,
463
        ];
464
    }
465
466
    /**
467
     * Authorize the B2 account in order to get an auth token and API/download URLs.
468
     */
469 28
    protected function authorizeAccount()
470
    {
471 28
        if (Carbon::now('UTC')->timestamp < $this->reAuthTime->timestamp) {
472 4
            return;
473
        }
474
475 28
        $response = $this->client->guzzleRequest('GET', self::B2_API_BASE_URL.self::B2_API_V1.'/b2_authorize_account', [
476 28
            'auth' => [$this->accountId, $this->applicationKey],
477
        ]);
478
479 28
        $this->authToken = $response['authorizationToken'];
480 28
        $this->apiUrl = $response['apiUrl'].self::B2_API_V1;
481 28
        $this->downloadUrl = $response['downloadUrl'];
482 28
        $this->reAuthTime = Carbon::now('UTC');
483 28
        $this->reAuthTime->addSeconds($this->authTimeoutSeconds);
484 28
    }
485
486
    /**
487
     * Maps the provided bucket name to the appropriate bucket ID.
488
     *
489
     * @param $name
490
     *
491
     * @return mixed
492
     */
493
    protected function getBucketIdFromName($name)
494
    {
495
        $buckets = $this->listBuckets();
496
497
        foreach ($buckets as $bucket) {
498
            if ($bucket->getName() === $name) {
499
                return $bucket->getId();
500
            }
501
        }
502
    }
503
504
    /**
505
     * Maps the provided bucket ID to the appropriate bucket name.
506
     *
507
     * @param $id
508
     *
509
     * @return mixed
510
     */
511
    protected function getBucketNameFromId($id)
512
    {
513
        $buckets = $this->listBuckets();
514
515
        foreach ($buckets as $bucket) {
516
            if ($bucket->getId() === $id) {
517
                return $bucket->getName();
518
            }
519
        }
520
    }
521
522
    /**
523
     * @param $bucketName
524
     * @param $fileName
525
     *
526
     * @return mixed
527
     */
528
    protected function getFileIdFromBucketAndFileName($bucketName, $fileName)
529
    {
530
        $files = $this->listFiles([
531
            'BucketName' => $bucketName,
532
            'FileName'   => $fileName,
533
        ]);
534
535
        foreach ($files as $file) {
536
            if ($file->getName() === $fileName) {
537
                return $file->getId();
538
            }
539
        }
540
    }
541
542
    /**
543
     * Uploads a large file using b2 large file procedure.
544
     *
545
     * @param array $options
546
     *
547
     * @return File
548
     */
549
    public function uploadLargeFile(array $options)
550
    {
551
        if (substr($options['FileName'], 0, 1) === '/') {
552
            $options['FileName'] = ltrim($options['FileName'], '/');
553
        }
554
555
        //if last char of path is not a "/" then add a "/"
556
        if (substr($options['FilePath'], -1) != '/') {
557
            $options['FilePath'] = $options['FilePath'].'/';
558
        }
559
560
        if (!isset($options['BucketId']) && isset($options['BucketName'])) {
561
            $options['BucketId'] = $this->getBucketIdFromName($options['BucketName']);
562
        }
563
564
        if (!isset($options['FileContentType'])) {
565
            $options['FileContentType'] = 'b2/x-auto';
566
        }
567
568
        $this->authorizeAccount();
569
570
        // 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...
571
        $start = $this->startLargeFile($options['FileName'], $options['FileContentType'], $options['BucketId']);
572
573
        // 2) b2_get_upload_part_url for each thread uploading (takes fileId)
574
        $url = $this->getUploadPartUrl($start['fileId']);
575
576
        // 3) b2_upload_part for each part of the file
577
        $parts = $this->uploadParts($options['FilePath'].$options['FileName'], $url['uploadUrl'], $url['authorizationToken'], $options);
578
579
        $sha1s = [];
580
581
        foreach ($parts as $part) {
582
            $sha1s[] = $part['contentSha1'];
583
        }
584
585
        // 4) b2_finish_large_file.
586
        return $this->finishLargeFile($start['fileId'], $sha1s);
587
    }
588
589
    /**
590
     * Starts the large file upload process.
591
     *
592
     * @param $fileName
593
     * @param $contentType
594
     * @param $bucketId
595
     *
596
     * @throws GuzzleException If the request fails.
597
     * @throws B2Exception     If the B2 server replies with an error.
598
     *
599
     * @return mixed
600
     */
601
    protected function startLargeFile($fileName, $contentType, $bucketId)
602
    {
603
        return $this->sendAuthorizedRequest('POST', 'b2_start_large_file', [
604
            'fileName'      => $fileName,
605
            'contentType'   => $contentType,
606
            'bucketId'      => $bucketId,
607
        ]);
608
    }
609
610
    /**
611
     * Gets the url for the next large file part upload.
612
     *
613
     * @param $fileId
614
     *
615
     * @throws GuzzleException If the request fails.
616
     * @throws B2Exception     If the B2 server replies with an error.
617
     *
618
     * @return mixed
619
     */
620
    protected function getUploadPartUrl($fileId)
621
    {
622
        return $this->sendAuthorizedRequest('POST', 'b2_get_upload_part_url', [
623
            'fileId' => $fileId,
624
        ]);
625
    }
626
627
    /**
628
     * Uploads the file as "parts" of 100MB each.
629
     *
630
     * @param $filePath
631
     * @param $uploadUrl
632
     * @param $largeFileAuthToken
633
     * @param $options
634
     *
635
     * @return array
636
     */
637
    protected function uploadParts($filePath, $uploadUrl, $largeFileAuthToken, $options = [])
638
    {
639
        $return = [];
640
641
        $minimum_part_size = 100 * (1000 * 1000);
642
643
        $local_file_size = filesize($filePath);
644
        $total_bytes_sent = 0;
645
        $bytes_sent_for_part = $minimum_part_size;
646
        $sha1_of_parts = [];
647
        $part_no = 1;
648
        $file_handle = fopen($filePath, 'r');
649
650
        while ($total_bytes_sent < $local_file_size) {
651
652
            // Determine the number of bytes to send based on the minimum part size
653
            if (($local_file_size - $total_bytes_sent) < $minimum_part_size) {
654
                $bytes_sent_for_part = ($local_file_size - $total_bytes_sent);
655
            }
656
657
            // Get a sha1 of the part we are going to send
658
            fseek($file_handle, $total_bytes_sent);
659
            $data_part = fread($file_handle, $bytes_sent_for_part);
660
            array_push($sha1_of_parts, sha1($data_part));
661
            fseek($file_handle, $total_bytes_sent);
662
663
            $customHeaders = $options['Headers'] ?? [];
664
            $response = $this->client->guzzleRequest('POST', $uploadUrl, [
665
                'headers' => array_merge([
666
                    'Authorization'                      => $largeFileAuthToken,
667
                    'Content-Length'                     => $bytes_sent_for_part,
668
                    'X-Bz-Part-Number'                   => $part_no,
669
                    'X-Bz-Content-Sha1'                  => $sha1_of_parts[$part_no - 1],
670
                ], $customHeaders),
671
                'body' => $data_part,
672
            ]);
673
674
            $return[] = $response;
675
676
            // Prepare for the next iteration of the loop
677
            $part_no++;
678
            $total_bytes_sent = $bytes_sent_for_part + $total_bytes_sent;
679
        }
680
681
        fclose($file_handle);
682
683
        return $return;
684
    }
685
686
    /**
687
     * Finishes the large file upload procedure.
688
     *
689
     * @param       $fileId
690
     * @param array $sha1s
691
     *
692
     * @throws GuzzleException If the request fails.
693
     * @throws B2Exception     If the B2 server replies with an error.
694
     *
695
     * @return File
696
     */
697
    protected function finishLargeFile($fileId, array $sha1s)
698
    {
699
        $response = $this->sendAuthorizedRequest('POST', 'b2_finish_large_file', [
700
            'fileId'        => $fileId,
701
            'partSha1Array' => $sha1s,
702
        ]);
703
704
        return new File(
705
            $response['fileId'],
706
            $response['fileName'],
707
            $response['contentSha1'],
708
            $response['contentLength'],
709
            $response['contentType'],
710
            $response['fileInfo'],
711
            $response['bucketId'],
712
            $response['action'],
713
            $response['uploadTimestamp']
714
        );
715
    }
716
717
    /**
718
     * Sends a authorized request to b2 API.
719
     *
720
     * @param string $method
721
     * @param string $route
722
     * @param array  $json
723
     *
724
     * @throws GuzzleException If the request fails.
725
     * @throws B2Exception     If the B2 server replies with an error.
726
     *
727
     * @return mixed
728
     */
729 21
    protected function sendAuthorizedRequest($method, $route, $json = [])
730
    {
731 21
        $this->authorizeAccount();
732
733 21
        return $this->client->guzzleRequest($method, $this->apiUrl.$route, [
734
            'headers' => [
735 21
                'Authorization' => $this->authToken,
736
            ],
737 21
            'json' => $json,
738
        ]);
739
    }
740
}
741