Completed
Pull Request — master (#21)
by Ankit
03:01
created

Client::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 7
ccs 4
cts 4
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 $filePath;
22
23
    /** @var int */
24
    protected $fileSize;
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(string $baseUrl, $cacheAdapter = 'file')
51
    {
52 1
        $this->client = new GuzzleClient([
53 1
            'base_uri' => $baseUrl,
54
        ]);
55
56 1
        $this->setCache($cacheAdapter);
57 1
    }
58
59
    /**
60
     * Set file properties.
61
     *
62
     * @param string $file File path.
63
     * @param string $name File name.
64
     *
65
     * @return Client
66
     */
67 3
    public function file(string $file, string $name = null) : self
68
    {
69 3
        $this->filePath = $file;
70
71 3
        if ( ! file_exists($file) || ! is_readable($file)) {
72 2
            throw new FileException('Cannot read file: ' . $file);
73
        }
74
75 1
        $this->fileName = $name ?? basename($this->filePath);
76 1
        $this->fileSize = filesize($file);
77
78 1
        return $this;
79
    }
80
81
    /**
82
     * Get file path.
83
     *
84
     * @return string|null
85
     */
86 1
    public function getFilePath()
87
    {
88 1
        return $this->filePath;
89
    }
90
91
    /**
92
     * Set file name.
93
     *
94
     * @param string $name
95
     *
96
     * @return Client
97
     */
98 1
    public function setFileName(string $name) : self
99
    {
100 1
        $this->fileName = $name;
101
102 1
        return $this;
103
    }
104
105
    /**
106
     * Get file name.
107
     *
108
     * @return string|null
109
     */
110 2
    public function getFileName()
111
    {
112 2
        return $this->fileName;
113
    }
114
115
    /**
116
     * Get file size.
117
     *
118
     * @return int|null
119
     */
120 1
    public function getFileSize()
121
    {
122 1
        return $this->fileSize;
123
    }
124
125
    /**
126
     * Get guzzle client.
127
     *
128
     * @return GuzzleClient
129
     */
130 1
    public function getClient() : GuzzleClient
131
    {
132 1
        return $this->client;
133
    }
134
135
    /**
136
     * Get checksum.
137
     *
138
     * @return string
139
     */
140 1
    public function getChecksum() : string
141
    {
142 1
        if (empty($this->checksum)) {
143 1
            $this->checksum = hash_file($this->getChecksumAlgorithm(), $this->getFilePath());
144
        }
145
146 1
        return $this->checksum;
147
    }
148
149
    /**
150
     * Set key.
151
     *
152
     * @param string $key
153
     *
154
     * @return Client
155
     */
156 1
    public function setKey(string $key) : self
157
    {
158 1
        $this->key = $key;
159
160 1
        return $this;
161
    }
162
163
    /**
164
     * Get key.
165
     *
166
     * @return string
167
     */
168 1
    public function getKey() : string
169
    {
170 1
        return $this->key;
171
    }
172
173
    /**
174
     * Set checksum algorithm.
175
     *
176
     * @param string $algorithm
177
     *
178
     * @return Client
179
     */
180 1
    public function setChecksumAlgorithm(string $algorithm) : self
181
    {
182 1
        $this->checksumAlgorithm = $algorithm;
183
184 1
        return $this;
185
    }
186
187
    /**
188
     * Get checksum algorithm.
189
     *
190
     * @return string
191
     */
192 1
    public function getChecksumAlgorithm() : string
193
    {
194 1
        return $this->checksumAlgorithm;
195
    }
196
197
    /**
198
     * Check if this is a partial upload request.
199
     *
200
     * @return bool
201
     */
202 2
    public function isPartial() : bool
203
    {
204 2
        return $this->partial;
205
    }
206
207
    /**
208
     * Get partial offset.
209
     * @return int
210
     */
211 1
    public function getPartialOffset() : int
212
    {
213 1
        return $this->partialOffset;
214
    }
215
216
    /**
217
     * Set offset and force this to be a partial upload request.
218
     *
219
     * @param int $offset
220
     *
221
     * @return self
222
     */
223 1
    public function seek(int $offset) : self
224
    {
225 1
        $this->partialOffset = $offset;
226
227 1
        $this->partial();
228
229 1
        return $this;
230
    }
231
232
    /**
233
     * Upload file.
234
     *
235
     * @param int $bytes Bytes to upload
236
     *
237
     * @throws ConnectionException
238
     *
239
     * @return int
240
     */
241 5
    public function upload(int $bytes = -1) : int
242
    {
243 5
        $bytes = $bytes < 0 ? $this->getFileSize() : $bytes;
244 5
        $key   = $this->getKey();
245
246
        try {
247
            // Check if this upload exists with HEAD request
248 5
            $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($key, $bytes);
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 string
285
     */
286 4
    public function create(string $key) : string
287
    {
288
        $headers = [
289 4
            'Upload-Length' => $this->fileSize,
290 4
            'Upload-Key' => $key,
291 4
            'Upload-Checksum' => $this->getUploadChecksumHeader(),
292 4
            'Upload-Metadata' => 'filename ' . base64_encode($this->fileName),
293
        ];
294
295 4
        if ($this->isPartial()) {
296 1
            $headers += ['Upload-Concat' => 'partial'];
297
        }
298
299 4
        $response = $this->getClient()->post($this->apiPath, [
300 4
            'headers' => $headers,
301
        ]);
302
303 4
        $data       = json_decode($response->getBody(), true);
304 4
        $checksum   = $data['data']['checksum'] ?? null;
305 4
        $statusCode = $response->getStatusCode();
306
307 4
        if (HttpResponse::HTTP_CREATED !== $statusCode || ! $checksum) {
308 2
            throw new FileException('Unable to create resource.');
309
        }
310
311 2
        return $checksum;
312
    }
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
                'headers' => [
359 3
                    'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
360
                ],
361
            ]);
362 2
        } catch (ClientException $e) {
363 2
            $statusCode = $e->getResponse()->getStatusCode();
364
365 2
            if (HttpResponse::HTTP_NOT_FOUND === $statusCode || HttpResponse::HTTP_GONE === $statusCode) {
366 2
                throw new FileException('File not found.');
367
            }
368
        }
369 1
    }
370
371
    /**
372
     * Set as partial request.
373
     *
374
     * @param bool $state
375
     */
376 3
    protected function partial(bool $state = true)
377
    {
378 3
        $this->partial = $state;
379
380 3
        if ( ! $this->partial) {
381 1
            return;
382
        }
383
384 2
        $key = $this->getKey();
385
386 2
        if (false !== strpos($key, self::PARTIAL_UPLOAD_NAME_SEPARATOR)) {
387 1
            list($key) = explode(self::PARTIAL_UPLOAD_NAME_SEPARATOR, $key);
388
        }
389
390 2
        $this->key = $key . uniqid(self::PARTIAL_UPLOAD_NAME_SEPARATOR);
391 2
    }
392
393
    /**
394
     * Send HEAD request.
395
     *
396
     * @param string $key
397
     *
398
     * @throws FileException
399
     *
400
     * @return int
401
     */
402 2
    protected function sendHeadRequest(string $key) : int
403
    {
404 2
        $response = $this->getClient()->head($this->apiPath . '/' . $key);
405
406 2
        $statusCode = $response->getStatusCode();
407
408 2
        if (HttpResponse::HTTP_OK !== $statusCode) {
409 1
            throw new FileException('File not found.');
410
        }
411
412 1
        return (int) current($response->getHeader('upload-offset'));
413
    }
414
415
    /**
416
     * Send PATCH request.
417
     *
418
     * @param string $key
419
     * @param int    $bytes
420
     *
421
     * @throws Exception
422
     * @throws FileException
423
     * @throws ConnectionException
424
     *
425
     * @return int
426
     */
427 6
    protected function sendPatchRequest(string $key, int $bytes) : int
428
    {
429 6
        $data    = $this->getData($key, $bytes);
430
        $headers = [
431 6
            'Content-Type' => 'application/offset+octet-stream',
432 6
            'Content-Length' => strlen($data),
433 6
            'Upload-Checksum' => $this->getUploadChecksumHeader(),
434
        ];
435
436 6
        if ($this->isPartial()) {
437 1
            $headers += ['Upload-Concat' => self::UPLOAD_TYPE_PARTIAL];
438
        }
439
440
        try {
441 6
            $response = $this->getClient()->patch($this->apiPath . '/' . $key, [
442 6
                'body' => $data,
443 6
                'headers' => $headers,
444
            ]);
445
446 2
            return (int) current($response->getHeader('upload-offset'));
447 4
        } catch (ClientException $e) {
448 3
            $statusCode = $e->getResponse()->getStatusCode();
449
450 3
            if (HttpResponse::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE === $statusCode) {
451 1
                throw new FileException('The uploaded file is corrupt.');
452
            }
453
454 2
            if (HttpResponse::HTTP_CONTINUE === $statusCode) {
455 1
                throw new ConnectionException('Connection aborted by user.');
456
            }
457
458 1
            throw new Exception($e->getResponse()->getBody(), $statusCode);
459 1
        } catch (ConnectException $e) {
460 1
            throw new ConnectionException("Couldn't connect to server.");
461
        }
462
    }
463
464
    /**
465
     * Get X bytes of data from file.
466
     *
467
     * @param string $key
468
     * @param int    $bytes
469
     *
470
     * @return string
471
     */
472 2
    protected function getData(string $key, int $bytes) : string
473
    {
474 2
        $file   = new File;
475 2
        $handle = $file->open($this->getFilePath(), $file::READ_BINARY);
476 2
        $offset = $this->partialOffset;
477
478 2
        if ($offset < 0) {
479 2
            $fileMeta = $this->getCache()->get($key);
480 2
            $offset   = $fileMeta['offset'];
481
        }
482
483 2
        $file->seek($handle, $offset);
484
485 2
        $data = $file->read($handle, $bytes);
486
487 2
        $file->close($handle);
488
489 2
        return (string) $data;
490
    }
491
492
    /**
493
     * Get upload checksum header.
494
     *
495
     * @return string
496
     */
497 1
    protected function getUploadChecksumHeader() : string
498
    {
499 1
        return $this->getChecksumAlgorithm() . ' ' . base64_encode($this->getChecksum());
500
    }
501
}
502