Completed
Pull Request — master (#11)
by Ankit
02:36
created

Client::getChecksumAlgorithm()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
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\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 $checksum;
34
35
    /** @var int */
36
    protected $offset = -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
    public function setFileName(string $name) : self
99
    {
100
        $this->fileName = $name;
101
102
        return $this;
103
    }
104
105
    /**
106
     * Get file name.
107
     *
108
     * @return string|null
109
     */
110 1
    public function getFileName()
111
    {
112 1
        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
     * Set API path.
127
     *
128
     * @param string $path
129
     *
130
     * @return Client
131
     */
132 1
    public function setApiPath(string $path) : self
133
    {
134 1
        $this->apiPath = $path;
135
136 1
        return $this;
137
    }
138
139
    /**
140
     * Get API path.
141
     *
142
     * @return string
143
     */
144 1
    public function getApiPath() : string
145
    {
146 1
        return $this->apiPath;
147
    }
148
149
    /**
150
     * Get guzzle client.
151
     *
152
     * @return GuzzleClient
153
     */
154 1
    public function getClient() : GuzzleClient
155
    {
156 1
        return $this->client;
157
    }
158
159
    /**
160
     * Get checksum.
161
     *
162
     * @return string
163
     */
164 1
    public function getChecksum() : string
165
    {
166 1
        if (empty($this->checksum)) {
167 1
            $this->checksum = hash_file($this->getChecksumAlgorithm(), $this->getFilePath());
168
        }
169
170 1
        return $this->checksum;
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
    public function isPartial() : bool
203
    {
204
        return $this->partial;
205
    }
206
207
    /**
208
     * Set offset and force this to be a partial upload request.
209
     *
210
     * @param int $offset
211
     *
212
     * @return $this
213
     */
214
    public function seek(int $offset)
215
    {
216
        $this->offset = $offset;
217
218
        $this->partial();
219
220
        return $this;
221
    }
222
223
    /**
224
     * Upload file.
225
     *
226
     * @param int $bytes Bytes to upload
227
     *
228
     * @throws ConnectionException
229
     *
230
     * @return int
231
     */
232 5
    public function upload(int $bytes = -1) : int
233
    {
234 5
        $bytes    = $bytes < 0 ? $this->getFileSize() : $bytes;
235 5
        $checksum = $this->getChecksum();
236
237
        try {
238
            // Check if this upload exists with HEAD request
239 5
            $this->sendHeadRequest($checksum);
240 3
        } catch (FileException | ClientException $e) {
241 2
            $this->create($checksum);
242 1
        } catch (ConnectException $e) {
243 1
            throw new ConnectionException("Couldn't connect to server.");
244
        }
245
246
        // Now, resume upload with PATCH request
247 4
        return $this->sendPatchRequest($checksum, $bytes);
248
    }
249
250
    /**
251
     * Returns offset if file is partially uploaded.
252
     *
253
     * @return bool|int
254
     */
255 3
    public function getOffset()
256
    {
257 3
        $checksum = $this->getChecksum();
258
259
        try {
260 3
            $offset = $this->sendHeadRequest($checksum);
261 2
        } catch (FileException | ClientException $e) {
262 2
            return false;
263
        }
264
265 1
        return $offset;
266
    }
267
268
    /**
269
     * Create resource with POST request.
270
     *
271
     * @param string $checksum
272
     *
273
     * @throws FileException
274
     *
275
     * @return string
276
     */
277 3
    public function create(string $checksum) : string
278
    {
279
        $headers = [
280 3
            'Upload-Length' => $this->fileSize,
281 3
            'Upload-Checksum' => $this->getUploadChecksumHeader($checksum),
282 3
            'Upload-Metadata' => 'filename ' . base64_encode($this->fileName),
283
        ];
284
285 3
        if ($this->isPartial()) {
286
            $headers += ['Upload-Concat' => 'partial'];
287
        }
288
289 3
        $response = $this->getClient()->post($this->apiPath, [
290 3
            'headers' => $headers,
291
        ]);
292
293 3
        $data       = json_decode($response->getBody(), true);
294 3
        $checksum   = $data['data']['checksum'] ?? null;
295 3
        $statusCode = $response->getStatusCode();
296
297 3
        if (HttpResponse::HTTP_CREATED !== $statusCode || ! $checksum) {
298 2
            throw new FileException('Unable to create resource.');
299
        }
300
301 1
        return $checksum;
302
    }
303
304
    /**
305
     * Concatenate 2 or more partial uploads.
306
     *
307
     * @param string $checksum
308
     * @param array  $partials
309
     *
310
     * @return string
311
     */
312
    public function concat(string $checksum, ...$partials) : string
313
    {
314
        $response = $this->getClient()->post($this->apiPath, [
315
            'headers' => [
316
                'Upload-Length' => $this->fileSize,
317
                'Upload-Checksum' => $this->getUploadChecksumHeader($checksum),
318
                'Upload-Metadata' => 'filename ' . base64_encode($this->fileName),
319
                'Upload-Concat' => 'final;' . implode(' ', $partials),
320
            ],
321
        ]);
322
323
        $data       = json_decode($response->getBody(), true);
324
        $checksum   = $data['data']['checksum'] ?? null;
325
        $statusCode = $response->getStatusCode();
326
327
        if (HttpResponse::HTTP_CREATED !== $statusCode || ! $checksum) {
328
            throw new FileException('Unable to create resource.');
329
        }
330
331
        return $checksum;
332
    }
333
334
    /**
335
     * Send DELETE request.
336
     *
337
     * @param string $checksum
338
     *
339
     * @throws FileException
340
     *
341
     * @return void
342
     */
343 3
    public function delete(string $checksum)
344
    {
345
        try {
346 3
            $this->getClient()->delete($this->apiPath . '/' . $checksum, [
347
                'headers' => [
348 3
                    'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
349
                ],
350
            ]);
351 2
        } catch (ClientException $e) {
352 2
            $statusCode = $e->getResponse()->getStatusCode();
353
354 2
            if (HttpResponse::HTTP_NOT_FOUND === $statusCode || HttpResponse::HTTP_GONE === $statusCode) {
355 2
                throw new FileException('File not found.');
356
            }
357
        }
358 1
    }
359
360
    /**
361
     * Set as partial request.
362
     *
363
     * @param bool $state
364
     */
365
    protected function partial(bool $state = true)
366
    {
367
        $this->partial = $state;
368
369
        if ( ! $this->partial) {
370
            return;
371
        }
372
373
        $checksum = $this->getChecksum();
374
375
        if (false !== strpos($checksum, self::PARTIAL_UPLOAD_NAME_SEPARATOR)) {
376
            list($checksum) = explode(self::PARTIAL_UPLOAD_NAME_SEPARATOR, $checksum);
377
        }
378
379
        $this->checksum = $checksum . uniqid(self::PARTIAL_UPLOAD_NAME_SEPARATOR);
380
    }
381
382
    /**
383
     * Send HEAD request.
384
     *
385
     * @param string $checksum
386
     *
387
     * @throws FileException
388
     *
389
     * @return int
390
     */
391 2
    protected function sendHeadRequest(string $checksum) : int
392
    {
393 2
        $response = $this->getClient()->head($this->apiPath . '/' . $checksum);
394
395 2
        $statusCode = $response->getStatusCode();
396
397 2
        if (HttpResponse::HTTP_OK !== $statusCode) {
398 1
            throw new FileException('File not found.');
399
        }
400
401 1
        return (int) current($response->getHeader('upload-offset'));
402
    }
403
404
    /**
405
     * Send PATCH request.
406
     *
407
     * @param string $checksum
408
     * @param int    $bytes
409
     *
410
     * @throws Exception
411
     * @throws FileException
412
     * @throws ConnectionException
413
     *
414
     * @return int
415
     */
416 5
    protected function sendPatchRequest(string $checksum, int $bytes) : int
417
    {
418 5
        $data    = $this->getData($checksum, $bytes);
419
        $headers = [
420 5
            'Content-Type' => 'application/offset+octet-stream',
421 5
            'Content-Length' => strlen($data),
422 5
            'Upload-Checksum' => $this->getUploadChecksumHeader($checksum),
423
        ];
424
425 5
        if ($this->isPartial()) {
426
            $headers += ['Upload-Concat' => 'partial'];
427
        }
428
429
        try {
430 5
            $response = $this->getClient()->patch($this->apiPath . '/' . $checksum, [
431 5
                'body' => $data,
432 5
                'headers' => $headers,
433
            ]);
434
435 1
            return (int) current($response->getHeader('upload-offset'));
436 4
        } catch (ClientException $e) {
437 3
            $statusCode = $e->getResponse()->getStatusCode();
438
439 3
            if (HttpResponse::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE === $statusCode) {
440 1
                throw new FileException('The uploaded file is corrupt.');
441
            }
442
443 2
            if (HttpResponse::HTTP_CONTINUE === $statusCode) {
444 1
                throw new ConnectionException('Connection aborted by user.');
445
            }
446
447 1
            throw new Exception($e->getResponse()->getBody(), $statusCode);
448 1
        } catch (ConnectException $e) {
449 1
            throw new ConnectionException("Couldn't connect to server.");
450
        }
451
    }
452
453
    /**
454
     * Get X bytes of data from file.
455
     *
456
     * @param string $checksum
457
     * @param int    $bytes
458
     *
459
     * @return string
460
     */
461 2
    protected function getData(string $checksum, int $bytes) : string
462
    {
463 2
        $file   = new File;
464 2
        $handle = $file->open($this->getFilePath(), $file::READ_BINARY);
465 2
        $offset = $this->offset;
466
467 2
        if ($offset < 0) {
468 2
            $fileMeta = $this->getCache()->get($checksum);
469 2
            $offset   = $fileMeta['offset'];
470
        }
471
472 2
        $file->seek($handle, $offset);
473
474 2
        $data = $file->read($handle, $bytes);
475
476 2
        $file->close($handle);
477
478 2
        return (string) $data;
479
    }
480
481
    /**
482
     * Get upload checksum header.
483
     *
484
     * @param string|null $checksum
485
     *
486
     * @return string
487
     */
488 1
    protected function getUploadChecksumHeader(string $checksum = null) : string
489
    {
490 1
        if (empty($checksum)) {
491 1
            $checksum = $this->getChecksum();
492
        }
493
494 1
        return $this->getChecksumAlgorithm() . ' ' . base64_encode($checksum);
495
    }
496
}
497