Passed
Pull Request — master (#75)
by Mark
03:40
created

Client::sendAuthorizedRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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