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

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