Test Failed
Push — master ( 5b3138...8aebde )
by
unknown
136:00 queued 133:20
created

Client::copy()   B

Complexity

Conditions 8
Paths 24

Size

Total Lines 39
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 8.1867

Importance

Changes 0
Metric Value
eloc 26
c 0
b 0
f 0
dl 0
loc 39
ccs 18
cts 21
cp 0.8571
rs 8.4444
cc 8
nc 24
nop 1
crap 8.1867
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
     * 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 2
     *
285
     * @throws B2Exception
286
     * @throws GuzzleException
287 2
     * @throws InvalidArgumentException
288
     *
289 2
     * @return File
290 2
     */
291
    public function copy(array $options)
292 2
    {
293 2
        $options['FileName'] = ltrim($options['FileName'], '/');
294
        $options['SaveAs'] = ltrim($options['SaveAs'], '/');
295 2
296
        if (!isset($options['DestinationBucketId']) && isset($options['DestinationBucketName'])) {
297 2
            $options['DestinationBucketId'] = $this->getBucketIdFromName($options['DestinationBucketName']);
298
        }
299
300
        if (!isset($options['BucketId']) && isset($options['BucketName'])) {
301 2
            $options['BucketId'] = $this->getBucketIdFromName($options['BucketName']);
302
        }
303
304
        $sourceFiles = $this->listFiles($options['BucketId'], $options['FileName']);
0 ignored issues
show
Unused Code introduced by
The call to BackblazeB2\Client::listFiles() has too many arguments starting with $options['FileName']. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

304
        /** @scrutinizer ignore-call */ 
305
        $sourceFiles = $this->listFiles($options['BucketId'], $options['FileName']);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

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