Completed
Pull Request — master (#74)
by
unknown
06:21
created

Client::isPartial()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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