Passed
Push — master ( bc17cc...be841b )
by Ankit
01:57
created

Client::setChecksum()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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