Completed
Push — master ( 620d64...611391 )
by Ankit
02:13
created

Client::getUploadChecksumHeader()   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 string */
36
    protected $checksumAlgorithm = 'sha256';
37
38
    /**
39
     * Client constructor.
40
     *
41
     * @param string           $baseUrl
42
     * @param Cacheable|string $cacheAdapter
43
     */
44 1
    public function __construct(string $baseUrl, $cacheAdapter = 'file')
45
    {
46 1
        $this->client = new GuzzleClient([
47 1
            'base_uri' => $baseUrl,
48
        ]);
49
50 1
        $this->setCache($cacheAdapter);
51 1
    }
52
53
    /**
54
     * Set file properties.
55
     *
56
     * @param string $file File path.
57
     * @param string $name File name.
58
     *
59
     * @return Client
60
     */
61 3
    public function file(string $file, string $name = null) : self
62
    {
63 3
        $this->filePath = $file;
64
65 3
        if ( ! file_exists($file) || ! is_readable($file)) {
66 2
            throw new FileException('Cannot read file: ' . $file);
67
        }
68
69 1
        $this->fileName = $name ?? basename($this->filePath);
70 1
        $this->fileSize = filesize($file);
71
72 1
        return $this;
73
    }
74
75
    /**
76
     * Get file path.
77
     *
78
     * @return string|null
79
     */
80 1
    public function getFilePath()
81
    {
82 1
        return $this->filePath;
83
    }
84
85
    /**
86
     * Get file name.
87
     *
88
     * @return string|null
89
     */
90 1
    public function getFileName()
91
    {
92 1
        return $this->fileName;
93
    }
94
95
    /**
96
     * Get file size.
97
     *
98
     * @return int|null
99
     */
100 1
    public function getFileSize()
101
    {
102 1
        return $this->fileSize;
103
    }
104
105
    /**
106
     * Set API path.
107
     *
108
     * @param string $path
109
     *
110
     * @return Client
111
     */
112 1
    public function setApiPath(string $path) : self
113
    {
114 1
        $this->apiPath = $path;
115
116 1
        return $this;
117
    }
118
119
    /**
120
     * Get API path.
121
     *
122
     * @return string
123
     */
124 1
    public function getApiPath() : string
125
    {
126 1
        return $this->apiPath;
127
    }
128
129
    /**
130
     * Get guzzle client.
131
     *
132
     * @return GuzzleClient
133
     */
134 1
    public function getClient() : GuzzleClient
135
    {
136 1
        return $this->client;
137
    }
138
139
    /**
140
     * Get checksum.
141
     *
142
     * @return string
143
     */
144 1
    public function getChecksum() : string
145
    {
146 1
        if (empty($this->checksum)) {
147 1
            $this->checksum = hash_file($this->getChecksumAlgorithm(), $this->getFilePath());
148
        }
149
150 1
        return $this->checksum;
151
    }
152
153
    /**
154
     * Set checksum algorithm.
155
     *
156
     * @param string $algorithm
157
     *
158
     * @return Client
159
     */
160 1
    public function setChecksumAlgorithm(string $algorithm) : self
161
    {
162 1
        $this->checksumAlgorithm = $algorithm;
163
164 1
        return $this;
165
    }
166
167
    /**
168
     * Get checksum algorithm.
169
     *
170
     * @return string
171
     */
172 1
    public function getChecksumAlgorithm() : string
173
    {
174 1
        return $this->checksumAlgorithm;
175
    }
176
177
    /**
178
     * Upload file.
179
     *
180
     * @param int $bytes Bytes to upload
181
     *
182
     * @throws ConnectionException
183
     *
184
     * @return int
185
     */
186 5
    public function upload(int $bytes = -1) : int
187
    {
188 5
        $bytes    = $bytes < 0 ? $this->getFileSize() : $bytes;
189 5
        $checksum = $this->getChecksum();
190
191
        try {
192
            // Check if this upload exists with HEAD request
193 5
            $this->sendHeadRequest($checksum);
194 3
        } catch (FileException $e) {
195 1
            $this->create();
196 2
        } catch (ClientException $e) {
197 1
            $this->create();
198 1
        } catch (ConnectException $e) {
199 1
            throw new ConnectionException("Couldn't connect to server.");
200
        }
201
202
        // Now, resume upload with PATCH request
203 4
        return $this->sendPatchRequest($checksum, $bytes);
204
    }
205
206
    /**
207
     * Returns offset if file is partially uploaded.
208
     *
209
     * @return bool|int
210
     */
211 3
    public function getOffset()
212
    {
213 3
        $checksum = $this->getChecksum();
214
215
        try {
216 3
            $offset = $this->sendHeadRequest($checksum);
217 2
        } catch (FileException | ClientException $e) {
218 2
            return false;
219
        }
220
221 1
        return $offset;
222
    }
223
224
    /**
225
     * Create resource with POST request.
226
     *
227
     * @throws FileException
228
     *
229
     * @return string
230
     */
231 3
    public function create() : string
232
    {
233 3
        $response = $this->getClient()->post($this->apiPath, [
234
            'headers' => [
235 3
                'Upload-Length' => $this->fileSize,
236 3
                'Upload-Checksum' => $this->getUploadChecksumHeader(),
237 3
                'Upload-Metadata' => 'filename ' . base64_encode($this->fileName),
238
            ],
239
        ]);
240
241 3
        $data       = json_decode($response->getBody(), true);
242 3
        $checksum   = $data['data']['checksum'] ?? null;
243 3
        $statusCode = $response->getStatusCode();
244
245 3
        if (HttpResponse::HTTP_CREATED !== $statusCode || ! $checksum) {
246 2
            throw new FileException('Unable to create resource.');
247
        }
248
249 1
        return $checksum;
250
    }
251
252
    /**
253
     * Send DELETE request.
254
     *
255
     * @param string $checksum
256
     *
257
     * @throws FileException
258
     *
259
     * @return void
260
     */
261 3
    public function delete(string $checksum)
262
    {
263
        try {
264 3
            $this->getClient()->delete($this->apiPath . '/' . $checksum, [
265
                'headers' => [
266 3
                    'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
267
                ],
268
            ]);
269 2
        } catch (ClientException $e) {
270 2
            $statusCode = $e->getResponse()->getStatusCode();
271
272 2
            if (HttpResponse::HTTP_NOT_FOUND === $statusCode || HttpResponse::HTTP_GONE === $statusCode) {
273 2
                throw new FileException('File not found.');
274
            }
275
        }
276 1
    }
277
278
    /**
279
     * Send HEAD request.
280
     *
281
     * @param string $checksum
282
     *
283
     * @throws FileException
284
     *
285
     * @return int
286
     */
287 2
    protected function sendHeadRequest(string $checksum) : int
288
    {
289 2
        $response = $this->getClient()->head($this->apiPath . '/' . $checksum);
290
291 2
        $statusCode = $response->getStatusCode();
292
293 2
        if (HttpResponse::HTTP_OK !== $statusCode) {
294 1
            throw new FileException('File not found.');
295
        }
296
297 1
        return (int) current($response->getHeader('upload-offset'));
298
    }
299
300
    /**
301
     * Send PATCH request.
302
     *
303
     * @param string $checksum
304
     * @param int    $bytes
305
     *
306
     * @throws Exception
307
     * @throws FileException
308
     * @throws ConnectionException
309
     *
310
     * @return int
311
     */
312 5
    protected function sendPatchRequest(string $checksum, int $bytes) : int
313
    {
314 5
        $data = $this->getData($checksum, $bytes);
315
316
        try {
317 5
            $response = $this->getClient()->patch($this->apiPath . '/' . $checksum, [
318 5
                'body' => $data,
319
                'headers' => [
320 5
                    'Content-Type' => 'application/offset+octet-stream',
321 5
                    'Content-Length' => strlen($data),
322 5
                    'Upload-Checksum' => $this->getUploadChecksumHeader(),
323
                ],
324
            ]);
325
326 1
            return (int) current($response->getHeader('upload-offset'));
327 4
        } catch (ClientException $e) {
328 3
            $statusCode = $e->getResponse()->getStatusCode();
329
330 3
            if (HttpResponse::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE === $statusCode) {
331 1
                throw new FileException('The uploaded file is corrupt.');
332
            }
333
334 2
            if (HttpResponse::HTTP_CONTINUE === $statusCode) {
335 1
                throw new ConnectionException('Connection aborted by user.');
336
            }
337
338 1
            throw new Exception($e->getResponse()->getBody(), $statusCode);
339 1
        } catch (ConnectException $e) {
340 1
            throw new ConnectionException("Couldn't connect to server.");
341
        }
342
    }
343
344
    /**
345
     * Get X bytes of data from file.
346
     *
347
     * @param string $checksum
348
     * @param int    $bytes
349
     *
350
     * @return string
351
     */
352 2
    protected function getData(string $checksum, int $bytes) : string
353
    {
354 2
        $file = new File;
355
356 2
        $handle   = $file->open($this->getFilePath(), $file::READ_BINARY);
357 2
        $fileMeta = $this->getCache()->get($checksum);
358
359 2
        $file->seek($handle, $fileMeta['offset']);
360
361 2
        $data = $file->read($handle, $bytes);
362
363 2
        $file->close($handle);
364
365 2
        return (string) $data;
366
    }
367
368
    /**
369
     * Get upload checksum header.
370
     *
371
     * @return string
372
     */
373 1
    protected function getUploadChecksumHeader() : string
374
    {
375 1
        return $this->getChecksumAlgorithm() . ' ' . base64_encode($this->getChecksum());
376
    }
377
}
378