Completed
Pull Request — master (#98)
by Rajendra
03:09
created

Client::sendPatchRequest()   B

Complexity

Conditions 8
Paths 26

Size

Total Lines 42
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 8.0368

Importance

Changes 0
Metric Value
cc 8
eloc 25
nc 26
nop 2
dl 0
loc 42
ccs 22
cts 24
cp 0.9167
crap 8.0368
rs 8.4444
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
        $this->client = new GuzzleClient(
52 2
            ['base_uri' => $baseUri] + $options
53
        );
54 2
    }
55
56
    /**
57
     * Set file properties.
58
     *
59
     * @param string $file File path.
60
     * @param string $name File name.
61
     *
62
     * @return Client
63
     */
64 3
    public function file(string $file, string $name = null) : self
65
    {
66 3
        $this->filePath = $file;
67
68 3
        if ( ! file_exists($file) || ! is_readable($file)) {
69 2
            throw new FileException('Cannot read file: ' . $file);
70
        }
71
72 1
        $this->fileName = $name ?? basename($this->filePath);
73 1
        $this->fileSize = filesize($file);
74
75 1
        return $this;
76
    }
77
78
    /**
79
     * Get file path.
80
     *
81
     * @return string|null
82
     */
83 1
    public function getFilePath() : ?string
84
    {
85 1
        return $this->filePath;
86
    }
87
88
    /**
89
     * Set file name.
90
     *
91
     * @param string $name
92
     *
93
     * @return Client
94
     */
95 1
    public function setFileName(string $name) : self
96
    {
97 1
        $this->fileName = $name;
98
99 1
        return $this;
100
    }
101
102
    /**
103
     * Get file name.
104
     *
105
     * @return string|null
106
     */
107 2
    public function getFileName() : ?string
108
    {
109 2
        return $this->fileName;
110
    }
111
112
    /**
113
     * Get file size.
114
     *
115
     * @return int
116
     */
117 1
    public function getFileSize() : int
118
    {
119 1
        return $this->fileSize;
120
    }
121
122
    /**
123
     * Get guzzle client.
124
     *
125
     * @return GuzzleClient
126
     */
127 2
    public function getClient() : GuzzleClient
128
    {
129 2
        return $this->client;
130
    }
131
132
    /**
133
     * Get checksum.
134
     *
135
     * @return string
136
     */
137 1
    public function getChecksum() : string
138
    {
139 1
        if (empty($this->checksum)) {
140 1
            $this->checksum = hash_file($this->getChecksumAlgorithm(), $this->getFilePath());
141
        }
142
143 1
        return $this->checksum;
144
    }
145
146
    /**
147
     * Set key.
148
     *
149
     * @param string $key
150
     *
151
     * @return Client
152
     */
153 1
    public function setKey(string $key) : self
154
    {
155 1
        $this->key = $key;
156
157 1
        return $this;
158
    }
159
160
    /**
161
     * Get key.
162
     *
163
     * @return string
164
     */
165 1
    public function getKey() : string
166
    {
167 1
        return $this->key;
168
    }
169
170
    /**
171
     * Set checksum algorithm.
172
     *
173
     * @param string $algorithm
174
     *
175
     * @return Client
176
     */
177 1
    public function setChecksumAlgorithm(string $algorithm) : self
178
    {
179 1
        $this->checksumAlgorithm = $algorithm;
180
181 1
        return $this;
182
    }
183
184
    /**
185
     * Get checksum algorithm.
186
     *
187
     * @return string
188
     */
189 1
    public function getChecksumAlgorithm() : string
190
    {
191 1
        return $this->checksumAlgorithm;
192
    }
193
194
    /**
195
     * Check if this is a partial upload request.
196
     *
197
     * @return bool
198
     */
199 2
    public function isPartial() : bool
200
    {
201 2
        return $this->partial;
202
    }
203
204
    /**
205
     * Get partial offset.
206
     *
207
     * @return int
208
     */
209 1
    public function getPartialOffset() : int
210
    {
211 1
        return $this->partialOffset;
212
    }
213
214
    /**
215
     * Set offset and force this to be a partial upload request.
216
     *
217
     * @param int $offset
218
     *
219
     * @return self
220
     */
221 1
    public function seek(int $offset) : self
222
    {
223 1
        $this->partialOffset = $offset;
224
225 1
        $this->partial();
226
227 1
        return $this;
228
    }
229
230
    /**
231
     * Upload file.
232
     *
233
     * @param int $bytes Bytes to upload
234
     *
235
     * @throws Exception
236
     * @throws ConnectionException
237
     *
238
     * @return int
239
     */
240 5
    public function upload(int $bytes = -1) : int
241
    {
242 5
        $key    = $this->getKey();
243 5
        $bytes  = $bytes < 0 ? $this->getFileSize() : $bytes;
244 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...
245
246
        try {
247
            // Check if this upload exists with HEAD request.
248 5
            $offset = $this->sendHeadRequest($key);
249 3
        } catch (FileException | ClientException $e) {
250 2
            $this->create($key);
251 1
        } catch (ConnectException $e) {
252 1
            throw new ConnectionException("Couldn't connect to server.");
253
        }
254
255
        // Now, resume upload with PATCH request.
256 4
        return $this->sendPatchRequest($bytes, $offset);
257
    }
258
259
    /**
260
     * Returns offset if file is partially uploaded.
261
     *
262
     * @return bool|int
263
     */
264 3
    public function getOffset()
265
    {
266 3
        $key = $this->getKey();
267
268
        try {
269 3
            $offset = $this->sendHeadRequest($key);
270 2
        } catch (FileException | ClientException $e) {
271 2
            return false;
272
        }
273
274 1
        return $offset;
275
    }
276
277
    /**
278
     * Create resource with POST request.
279
     *
280
     * @param string $key
281
     *
282
     * @throws FileException
283
     *
284
     * @return void
285
     */
286 3
    public function create(string $key)
287
    {
288
        $headers = [
289 3
            'Upload-Length' => $this->fileSize,
290 3
            'Upload-Key' => $key,
291 3
            'Upload-Checksum' => $this->getUploadChecksumHeader(),
292 3
            'Upload-Metadata' => 'filename ' . base64_encode($this->fileName),
293
        ];
294
295 3
        if ($this->isPartial()) {
296 1
            $headers += ['Upload-Concat' => 'partial'];
297
        }
298
299 3
        $response = $this->getClient()->post($this->apiPath, [
300 3
            'headers' => $headers,
301
        ]);
302
303 3
        $statusCode = $response->getStatusCode();
304
305 3
        if (HttpResponse::HTTP_CREATED !== $statusCode) {
306 1
            throw new FileException('Unable to create resource.');
307
        }
308 2
    }
309
310
    /**
311
     * Concatenate 2 or more partial uploads.
312
     *
313
     * @param string $key
314
     * @param mixed  $partials
315
     *
316
     * @return string
317
     */
318 3
    public function concat(string $key, ...$partials) : string
319
    {
320 3
        $response = $this->getClient()->post($this->apiPath, [
321
            'headers' => [
322 3
                'Upload-Length' => $this->fileSize,
323 3
                'Upload-Key' => $key,
324 3
                'Upload-Checksum' => $this->getUploadChecksumHeader(),
325 3
                'Upload-Metadata' => 'filename ' . base64_encode($this->fileName),
326 3
                'Upload-Concat' => self::UPLOAD_TYPE_FINAL . ';' . implode(' ', $partials),
327
            ],
328
        ]);
329
330 3
        $data       = json_decode($response->getBody(), true);
331 3
        $checksum   = $data['data']['checksum'] ?? null;
332 3
        $statusCode = $response->getStatusCode();
333
334 3
        if (HttpResponse::HTTP_CREATED !== $statusCode || ! $checksum) {
335 2
            throw new FileException('Unable to create resource.');
336
        }
337
338 1
        return $checksum;
339
    }
340
341
    /**
342
     * Send DELETE request.
343
     *
344
     * @param string $key
345
     *
346
     * @throws FileException
347
     *
348
     * @return void
349
     */
350 3
    public function delete(string $key)
351
    {
352
        try {
353 3
            $this->getClient()->delete($this->apiPath . '/' . $key, [
354
                'headers' => [
355 3
                    'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
356
                ],
357
            ]);
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' => 'application/offset+octet-stream',
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
        }
436
437
        try {
438 6
            $response = $this->getClient()->patch($this->apiPath . '/' . $this->getKey(), [
439 6
                'body' => $data,
440 6
                'headers' => $headers,
441
            ]);
442
443 2
            return (int) current($response->getHeader('upload-offset'));
444 4
        } catch (ClientException $e) {
445 3
            $statusCode = $e->getResponse()->getStatusCode();
446
447 3
            if (HttpResponse::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE === $statusCode) {
448 1
                throw new FileException('The uploaded file is corrupt.');
449
            }
450
451 2
            if (HttpResponse::HTTP_CONTINUE === $statusCode) {
452 1
                throw new ConnectionException('Connection aborted by user.');
453
            }
454
455 1
            if (HttpResponse::HTTP_UNSUPPORTED_MEDIA_TYPE == $statusCode) {
456
                throw new ConnectionException('Unsupported media Types.');
457
            }
458
459 1
            if (HttpResponse::HTTP_BAD_REQUEST === $statusCode) {
460
                throw new ConnectionException('Bad request.');
461
            }
462
463 1
            throw new Exception($e->getResponse()->getBody(), $statusCode);
464 1
        } catch (ConnectException $e) {
465 1
            throw new ConnectionException("Couldn't connect to server.");
466
        }
467
    }
468
469
    /**
470
     * Get X bytes of data from file.
471
     *
472
     * @param int $offset
473
     * @param int $bytes
474
     *
475
     * @return string
476
     */
477 2
    protected function getData(int $offset, int $bytes) : string
478
    {
479 2
        $file   = new File;
480 2
        $handle = $file->open($this->getFilePath(), $file::READ_BINARY);
481
482 2
        $file->seek($handle, $offset);
483
484 2
        $data = $file->read($handle, $bytes);
485
486 2
        $file->close($handle);
487
488 2
        return (string) $data;
489
    }
490
491
    /**
492
     * Get upload checksum header.
493
     *
494
     * @return string
495
     */
496 1
    protected function getUploadChecksumHeader() : string
497
    {
498 1
        return $this->getChecksumAlgorithm() . ' ' . base64_encode($this->getChecksum());
499
    }
500
}
501