Completed
Push — master ( 611391...86332e )
by Ankit
05:15
created

Client::upload()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 6
nop 1
dl 0
loc 16
ccs 10
cts 10
cp 1
crap 4
rs 9.2
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 | ClientException $e) {
195 1
            $this->create();
196 2
        } catch (ConnectException $e) {
197 1
            throw new ConnectionException("Couldn't connect to server.");
198 1
        }
199 1
200
        // Now, resume upload with PATCH request
201
        return $this->sendPatchRequest($checksum, $bytes);
202
    }
203 4
204
    /**
205
     * Returns offset if file is partially uploaded.
206
     *
207
     * @return bool|int
208
     */
209
    public function getOffset()
210
    {
211 3
        $checksum = $this->getChecksum();
212
213 3
        try {
214
            $offset = $this->sendHeadRequest($checksum);
215
        } catch (FileException | ClientException $e) {
216 3
            return false;
217 2
        }
218 2
219
        return $offset;
220
    }
221 1
222
    /**
223
     * Create resource with POST request.
224
     *
225
     * @throws FileException
226
     *
227
     * @return string
228
     */
229
    public function create() : string
230
    {
231 3
        $response = $this->getClient()->post($this->apiPath, [
232
            'headers' => [
233 3
                'Upload-Length' => $this->fileSize,
234
                'Upload-Checksum' => $this->getUploadChecksumHeader(),
235 3
                'Upload-Metadata' => 'filename ' . base64_encode($this->fileName),
236 3
            ],
237 3
        ]);
238
239
        $data       = json_decode($response->getBody(), true);
240
        $checksum   = $data['data']['checksum'] ?? null;
241 3
        $statusCode = $response->getStatusCode();
242 3
243 3
        if (HttpResponse::HTTP_CREATED !== $statusCode || ! $checksum) {
244
            throw new FileException('Unable to create resource.');
245 3
        }
246 2
247
        return $checksum;
248
    }
249 1
250
    /**
251
     * Send DELETE request.
252
     *
253
     * @param string $checksum
254
     *
255
     * @throws FileException
256
     *
257
     * @return void
258
     */
259
    public function delete(string $checksum)
260
    {
261 3
        try {
262
            $this->getClient()->delete($this->apiPath . '/' . $checksum, [
263
                'headers' => [
264 3
                    'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
265
                ],
266 3
            ]);
267
        } catch (ClientException $e) {
268
            $statusCode = $e->getResponse()->getStatusCode();
269 2
270 2
            if (HttpResponse::HTTP_NOT_FOUND === $statusCode || HttpResponse::HTTP_GONE === $statusCode) {
271
                throw new FileException('File not found.');
272 2
            }
273 2
        }
274
    }
275
276 1
    /**
277
     * Send HEAD request.
278
     *
279
     * @param string $checksum
280
     *
281
     * @throws FileException
282
     *
283
     * @return int
284
     */
285
    protected function sendHeadRequest(string $checksum) : int
286
    {
287 2
        $response = $this->getClient()->head($this->apiPath . '/' . $checksum);
288
289 2
        $statusCode = $response->getStatusCode();
290
291 2
        if (HttpResponse::HTTP_OK !== $statusCode) {
292
            throw new FileException('File not found.');
293 2
        }
294 1
295
        return (int) current($response->getHeader('upload-offset'));
296
    }
297 1
298
    /**
299
     * Send PATCH request.
300
     *
301
     * @param string $checksum
302
     * @param int    $bytes
303
     *
304
     * @throws Exception
305
     * @throws FileException
306
     * @throws ConnectionException
307
     *
308
     * @return int
309
     */
310
    protected function sendPatchRequest(string $checksum, int $bytes) : int
311
    {
312 5
        $data = $this->getData($checksum, $bytes);
313
314 5
        try {
315
            $response = $this->getClient()->patch($this->apiPath . '/' . $checksum, [
316
                'body' => $data,
317 5
                'headers' => [
318 5
                    'Content-Type' => 'application/offset+octet-stream',
319
                    'Content-Length' => strlen($data),
320 5
                    'Upload-Checksum' => $this->getUploadChecksumHeader(),
321 5
                ],
322 5
            ]);
323
324
            return (int) current($response->getHeader('upload-offset'));
325
        } catch (ClientException $e) {
326 1
            $statusCode = $e->getResponse()->getStatusCode();
327 4
328 3
            if (HttpResponse::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE === $statusCode) {
329
                throw new FileException('The uploaded file is corrupt.');
330 3
            }
331 1
332
            if (HttpResponse::HTTP_CONTINUE === $statusCode) {
333
                throw new ConnectionException('Connection aborted by user.');
334 2
            }
335 1
336
            throw new Exception($e->getResponse()->getBody(), $statusCode);
337
        } catch (ConnectException $e) {
338 1
            throw new ConnectionException("Couldn't connect to server.");
339 1
        }
340 1
    }
341
342
    /**
343
     * Get X bytes of data from file.
344
     *
345
     * @param string $checksum
346
     * @param int    $bytes
347
     *
348
     * @return string
349
     */
350
    protected function getData(string $checksum, int $bytes) : string
351
    {
352 2
        $file = new File;
353
354 2
        $handle   = $file->open($this->getFilePath(), $file::READ_BINARY);
355
        $fileMeta = $this->getCache()->get($checksum);
356 2
357 2
        $file->seek($handle, $fileMeta['offset']);
358
359 2
        $data = $file->read($handle, $bytes);
360
361 2
        $file->close($handle);
362
363 2
        return (string) $data;
364
    }
365 2
366
    /**
367
     * Get upload checksum header.
368
     *
369
     * @return string
370
     */
371
    protected function getUploadChecksumHeader() : string
372
    {
373 1
        return $this->getChecksumAlgorithm() . ' ' . base64_encode($this->getChecksum());
374
    }
375
}
376