Completed
Pull Request — master (#101)
by Ankit
02:04
created

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