Passed
Pull Request — master (#82)
by
unknown
01:58
created

Client::setChecksumAlgorithm()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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