Passed
Pull Request — master (#194)
by Ankit
03:20 queued 01:27
created

Client::setChecksum()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
1
<?php
2
3
namespace TusPhp\Tus;
4
5
use TusPhp\File;
6
use Carbon\Carbon;
7
use TusPhp\Config;
8
use TusPhp\Exception\TusException;
9
use TusPhp\Exception\FileException;
10
use GuzzleHttp\Client as GuzzleClient;
11
use GuzzleHttp\Exception\ClientException;
12
use TusPhp\Exception\ConnectionException;
13
use GuzzleHttp\Exception\ConnectException;
14
use Symfony\Component\HttpFoundation\Response as HttpResponse;
15
16
class Client extends AbstractTus
17
{
18
    /** @var GuzzleClient */
19
    protected $client;
20
21
    /** @var string */
22
    protected $filePath;
23
24
    /** @var int */
25
    protected $fileSize = 0;
26
27
    /** @var string */
28
    protected $fileName;
29
30
    /** @var string */
31
    protected $key;
32
33
    /** @var string */
34
    protected $url;
35
36
    /** @var string */
37
    protected $checksum;
38
39
    /** @var int */
40
    protected $partialOffset = -1;
41
42
    /** @var bool */
43
    protected $partial = false;
44
45
    /** @var string */
46
    protected $checksumAlgorithm = 'sha256';
47
48
    /** @var array */
49
    protected $metadata = [];
50
51
    /**
52
     * Client constructor.
53
     *
54
     * @param string $baseUri
55
     * @param array  $options
56
     *
57
     * @throws \ReflectionException
58
     */
59 3
    public function __construct(string $baseUri, array $options = [])
60
    {
61 3
        $options['headers'] = [
62 3
            'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
63 3
        ] + ($options['headers'] ?? []);
64
65 3
        $this->client = new GuzzleClient(
66 3
            ['base_uri' => $baseUri] + $options
67
        );
68
69 3
        Config::set(__DIR__ . '/../Config/client.php');
70
71 3
        $this->setCache('file');
72 3
    }
73
74
    /**
75
     * Set file properties.
76
     *
77
     * @param string $file File path.
78
     * @param string $name File name.
79
     *
80
     * @return Client
81
     */
82 3
    public function file(string $file, string $name = null) : self
83
    {
84 3
        $this->filePath = $file;
85
86 3
        if ( ! file_exists($file) || ! is_readable($file)) {
87 2
            throw new FileException('Cannot read file: ' . $file);
88
        }
89
90 1
        $this->fileName = $name ?? basename($this->filePath);
91 1
        $this->fileSize = filesize($file);
92
93 1
        $this->addMetadata('filename', $this->fileName);
94
95 1
        return $this;
96
    }
97
98
    /**
99
     * Get file path.
100
     *
101
     * @return string|null
102
     */
103 1
    public function getFilePath() : ?string
104
    {
105 1
        return $this->filePath;
106
    }
107
108
    /**
109
     * Set file name.
110
     *
111
     * @param string $name
112
     *
113
     * @return Client
114
     */
115 1
    public function setFileName(string $name) : self
116
    {
117 1
        $this->addMetadata('filename', $this->fileName = $name);
118
119 1
        return $this;
120
    }
121
122
    /**
123
     * Get file name.
124
     *
125
     * @return string|null
126
     */
127 2
    public function getFileName() : ?string
128
    {
129 2
        return $this->fileName;
130
    }
131
132
    /**
133
     * Get file size.
134
     *
135
     * @return int
136
     */
137 1
    public function getFileSize() : int
138
    {
139 1
        return $this->fileSize;
140
    }
141
142
    /**
143
     * Get guzzle client.
144
     *
145
     * @return GuzzleClient
146
     */
147 2
    public function getClient() : GuzzleClient
148
    {
149 2
        return $this->client;
150
    }
151
152
    /**
153
     * Set checksum.
154
     *
155
     * @param string $checksum
156
     *
157
     * @return Client
158
     */
159 1
    public function setChecksum(string $checksum) : self
160
    {
161 1
        $this->checksum = $checksum;
162
163 1
        return $this;
164
    }
165
166
    /**
167
     * Get checksum.
168
     *
169
     * @return string
170
     */
171 1
    public function getChecksum() : string
172
    {
173 1
        if (empty($this->checksum)) {
174 1
            $this->setChecksum(hash_file($this->getChecksumAlgorithm(), $this->getFilePath()));
175
        }
176
177 1
        return $this->checksum;
178
    }
179
180
    /**
181
     * Add metadata.
182
     *
183
     * @param string $key
184
     * @param string $value
185
     *
186
     * @return Client
187
     */
188 1
    public function addMetadata(string $key, string $value) : self
189
    {
190 1
        $this->metadata[$key] = base64_encode($value);
191
192 1
        return $this;
193
    }
194
195
    /**
196
     * Remove metadata.
197
     *
198
     * @param string $key
199
     *
200
     * @return Client
201
     */
202 1
    public function removeMetadata(string $key) : self
203
    {
204 1
        unset($this->metadata[$key]);
205
206 1
        return $this;
207
    }
208
209
    /**
210
     * Set metadata.
211
     *
212
     * @param array $items
213
     *
214
     * @return Client
215
     */
216 1
    public function setMetadata(array $items) : self
217
    {
218 1
        $items = array_map('base64_encode', $items);
219
220 1
        $this->metadata = $items;
221
222 1
        return $this;
223
    }
224
225
    /**
226
     * Get metadata.
227
     *
228
     * @return array
229
     */
230 1
    public function getMetadata() : array
231
    {
232 1
        return $this->metadata;
233
    }
234
235
    /**
236
     * Get metadata for Upload-Metadata header.
237
     *
238
     * @return string
239
     */
240 1
    protected function getUploadMetadataHeader() : string
241
    {
242 1
        $metadata = [];
243
244 1
        foreach ($this->getMetadata() as $key => $value) {
245 1
            $metadata[] = "{$key} {$value}";
246
        }
247
248 1
        return implode(',', $metadata);
249
    }
250
251
    /**
252
     * Set key.
253
     *
254
     * @param string $key
255
     *
256
     * @return Client
257
     */
258 1
    public function setKey(string $key) : self
259
    {
260 1
        $this->key = $key;
261
262 1
        return $this;
263
    }
264
265
    /**
266
     * Get key.
267
     *
268
     * @return string
269
     */
270 1
    public function getKey() : string
271
    {
272 1
        return $this->key;
273
    }
274
275
    /**
276
     * Get url.
277
     *
278
     * @return string|null
279
     */
280 2
    public function getUrl() : ?string
281
    {
282 2
        $this->url = $this->getCache()->get($this->getKey())['location'] ?? null;
283
284 2
        if ( ! $this->url) {
285 1
            throw new FileException('File not found.');
286
        }
287
288 1
        return $this->url;
289
    }
290
291
    /**
292
     * Set checksum algorithm.
293
     *
294
     * @param string $algorithm
295
     *
296
     * @return Client
297
     */
298 1
    public function setChecksumAlgorithm(string $algorithm) : self
299
    {
300 1
        $this->checksumAlgorithm = $algorithm;
301
302 1
        return $this;
303
    }
304
305
    /**
306
     * Get checksum algorithm.
307
     *
308
     * @return string
309
     */
310 1
    public function getChecksumAlgorithm() : string
311
    {
312 1
        return $this->checksumAlgorithm;
313
    }
314
315
    /**
316
     * Check if current upload is expired.
317
     *
318
     * @return bool
319
     */
320 2
    public function isExpired() : bool
321
    {
322 2
        $expiresAt = $this->getCache()->get($this->getKey())['expires_at'] ?? null;
323
324 2
        return empty($expiresAt) || Carbon::parse($expiresAt)->lt(Carbon::now());
325
    }
326
327
    /**
328
     * Check if this is a partial upload request.
329
     *
330
     * @return bool
331
     */
332 2
    public function isPartial() : bool
333
    {
334 2
        return $this->partial;
335
    }
336
337
    /**
338
     * Get partial offset.
339
     *
340
     * @return int
341
     */
342 1
    public function getPartialOffset() : int
343
    {
344 1
        return $this->partialOffset;
345
    }
346
347
    /**
348
     * Set offset and force this to be a partial upload request.
349
     *
350
     * @param int $offset
351
     *
352
     * @return self
353
     */
354 1
    public function seek(int $offset) : self
355
    {
356 1
        $this->partialOffset = $offset;
357
358 1
        $this->partial();
359
360 1
        return $this;
361
    }
362
363
    /**
364
     * Upload file.
365
     *
366
     * @param int $bytes Bytes to upload
367
     *
368
     * @throws TusException
369
     * @throws ConnectionException
370
     *
371
     * @return int
372
     */
373 6
    public function upload(int $bytes = -1) : int
374
    {
375 6
        $bytes  = $bytes < 0 ? $this->getFileSize() : $bytes;
376 6
        $offset = $this->partialOffset < 0 ? 0 : $this->partialOffset;
0 ignored issues
show
Unused Code introduced by
The assignment to $offset is dead and can be removed.
Loading history...
377
378
        try {
379
            // Check if this upload exists with HEAD request.
380 6
            $offset = $this->sendHeadRequest();
381 3
        } catch (FileException | ClientException $e) {
382
            // Create a new upload.
383 2
            $this->url = $this->create($this->getKey());
384 1
        } catch (ConnectException $e) {
385 1
            throw new ConnectionException("Couldn't connect to server.");
386
        }
387
388
        // Verify that upload is not yet expired.
389 5
        if ($this->isExpired()) {
390 1
            throw new TusException('Upload expired.');
391
        }
392
393
        // Now, resume upload with PATCH request.
394 4
        return $this->sendPatchRequest($bytes, $offset);
395
    }
396
397
    /**
398
     * Returns offset if file is partially uploaded.
399
     *
400
     * @return bool|int
401
     */
402 3
    public function getOffset()
403
    {
404
        try {
405 3
            $offset = $this->sendHeadRequest();
406 2
        } catch (FileException | ClientException $e) {
407 2
            return false;
408
        }
409
410 1
        return $offset;
411
    }
412
413
    /**
414
     * Create resource with POST request.
415
     *
416
     * @param string $key
417
     *
418
     * @throws FileException
419
     *
420
     * @return string
421
     */
422 3
    public function create(string $key) : string
423
    {
424
        $headers = [
425 3
            'Upload-Length' => $this->fileSize,
426 3
            'Upload-Key' => $key,
427 3
            'Upload-Checksum' => $this->getUploadChecksumHeader(),
428 3
            'Upload-Metadata' => $this->getUploadMetadataHeader(),
429
        ];
430
431 3
        if ($this->isPartial()) {
432 1
            $headers += ['Upload-Concat' => 'partial'];
433
        }
434
435 3
        $response = $this->getClient()->post($this->apiPath, [
436 3
            'headers' => $headers,
437
        ]);
438
439 3
        $statusCode = $response->getStatusCode();
440
441 3
        if (HttpResponse::HTTP_CREATED !== $statusCode) {
442 1
            throw new FileException('Unable to create resource.');
443
        }
444
445 2
        $uploadLocation = current($response->getHeader('location'));
446
447 2
        $this->getCache()->set($this->getKey(), [
448 2
            'location' => $uploadLocation,
449 2
            'expires_at' => Carbon::now()->addSeconds($this->getCache()->getTtl())->format($this->getCache()::RFC_7231),
450
        ]);
451
452 2
        return $uploadLocation;
453
    }
454
455
    /**
456
     * Concatenate 2 or more partial uploads.
457
     *
458
     * @param string $key
459
     * @param mixed  $partials
460
     *
461
     * @return string
462
     */
463 3
    public function concat(string $key, ...$partials) : string
464
    {
465 3
        $response = $this->getClient()->post($this->apiPath, [
466
            'headers' => [
467 3
                'Upload-Length' => $this->fileSize,
468 3
                'Upload-Key' => $key,
469 3
                'Upload-Checksum' => $this->getUploadChecksumHeader(),
470 3
                'Upload-Metadata' => $this->getUploadMetadataHeader(),
471 3
                'Upload-Concat' => self::UPLOAD_TYPE_FINAL . ';' . implode(' ', $partials),
472
            ],
473
        ]);
474
475 3
        $data       = json_decode($response->getBody(), true);
476 3
        $checksum   = $data['data']['checksum'] ?? null;
477 3
        $statusCode = $response->getStatusCode();
478
479 3
        if (HttpResponse::HTTP_CREATED !== $statusCode || ! $checksum) {
480 2
            throw new FileException('Unable to create resource.');
481
        }
482
483 1
        return $checksum;
484
    }
485
486
    /**
487
     * Send DELETE request.
488
     *
489
     * @throws FileException
490
     *
491
     * @return void
492
     */
493 3
    public function delete()
494
    {
495
        try {
496 3
            $this->getClient()->delete($this->getUrl());
497 2
        } catch (ClientException $e) {
498 2
            $statusCode = $e->getResponse()->getStatusCode();
499
500 2
            if (HttpResponse::HTTP_NOT_FOUND === $statusCode || HttpResponse::HTTP_GONE === $statusCode) {
501 2
                throw new FileException('File not found.');
502
            }
503
        }
504 1
    }
505
506
    /**
507
     * Set as partial request.
508
     *
509
     * @param bool $state
510
     *
511
     * @return void
512
     */
513 3
    protected function partial(bool $state = true)
514
    {
515 3
        $this->partial = $state;
516
517 3
        if ( ! $this->partial) {
518 1
            return;
519
        }
520
521 2
        $key = $this->getKey();
522
523 2
        if (false !== strpos($key, self::PARTIAL_UPLOAD_NAME_SEPARATOR)) {
524 1
            list($key, /* $partialKey */) = explode(self::PARTIAL_UPLOAD_NAME_SEPARATOR, $key);
525
        }
526
527 2
        $this->key = $key . uniqid(self::PARTIAL_UPLOAD_NAME_SEPARATOR);
528 2
    }
529
530
    /**
531
     * Send HEAD request.
532
     *
533
     * @throws FileException
534
     *
535
     * @return int
536
     */
537 2
    protected function sendHeadRequest() : int
538
    {
539 2
        $response   = $this->getClient()->head($this->getUrl());
540 2
        $statusCode = $response->getStatusCode();
541
542 2
        if (HttpResponse::HTTP_OK !== $statusCode) {
543 1
            throw new FileException('File not found.');
544
        }
545
546 1
        return (int) current($response->getHeader('upload-offset'));
547
    }
548
549
    /**
550
     * Send PATCH request.
551
     *
552
     * @param int $bytes
553
     * @param int $offset
554
     *
555
     * @throws TusException
556
     * @throws FileException
557
     * @throws ConnectionException
558
     *
559
     * @return int
560
     */
561 7
    protected function sendPatchRequest(int $bytes, int $offset) : int
562
    {
563 7
        $data    = $this->getData($offset, $bytes);
564
        $headers = [
565 7
            'Content-Type' => self::HEADER_CONTENT_TYPE,
566 7
            'Content-Length' => strlen($data),
567 7
            'Upload-Checksum' => $this->getUploadChecksumHeader(),
568
        ];
569
570 7
        if ($this->isPartial()) {
571 1
            $headers += ['Upload-Concat' => self::UPLOAD_TYPE_PARTIAL];
572
        } else {
573 6
            $headers += ['Upload-Offset' => $offset];
574
        }
575
576
        try {
577 7
            $response = $this->getClient()->patch($this->getUrl(), [
578 7
                'body' => $data,
579 7
                'headers' => $headers,
580
            ]);
581
582 2
            return (int) current($response->getHeader('upload-offset'));
583 5
        } catch (ClientException $e) {
584 4
            throw $this->handleClientException($e);
585 1
        } catch (ConnectException $e) {
586 1
            throw new ConnectionException("Couldn't connect to server.");
587
        }
588
    }
589
590
    /**
591
     * Handle client exception during patch request.
592
     *
593
     * @param ClientException $e
594
     *
595
     * @return mixed
596
     */
597 4
    protected function handleClientException(ClientException $e)
598
    {
599 4
        $statusCode = $e->getResponse()->getStatusCode();
600
601 4
        if (HttpResponse::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE === $statusCode) {
602 1
            return new FileException('The uploaded file is corrupt.');
603
        }
604
605 3
        if (HttpResponse::HTTP_CONTINUE === $statusCode) {
606 1
            return new ConnectionException('Connection aborted by user.');
607
        }
608
609 2
        if (HttpResponse::HTTP_UNSUPPORTED_MEDIA_TYPE === $statusCode) {
610 1
            return new TusException('Unsupported media types.');
611
        }
612
613 1
        return new TusException($e->getResponse()->getBody(), $statusCode);
614
    }
615
616
    /**
617
     * Get X bytes of data from file.
618
     *
619
     * @param int $offset
620
     * @param int $bytes
621
     *
622
     * @return string
623
     */
624 2
    protected function getData(int $offset, int $bytes) : string
625
    {
626 2
        $file   = new File;
627 2
        $handle = $file->open($this->getFilePath(), $file::READ_BINARY);
628
629 2
        $file->seek($handle, $offset);
630
631 2
        $data = $file->read($handle, $bytes);
632
633 2
        $file->close($handle);
634
635 2
        return (string) $data;
636
    }
637
638
    /**
639
     * Get upload checksum header.
640
     *
641
     * @return string
642
     */
643 1
    protected function getUploadChecksumHeader() : string
644
    {
645 1
        return $this->getChecksumAlgorithm() . ' ' . base64_encode($this->getChecksum());
646
    }
647
}
648