Passed
Push — master ( 460f00...452302 )
by Ankit
02:12
created

Client::getKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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