Passed
Push — master ( 2750f5...b3eb32 )
by Ankit
02:25
created

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