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