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

File::upload()   B

Complexity

Conditions 6
Paths 13

Size

Total Lines 41
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 23
nc 13
nop 1
dl 0
loc 41
ccs 23
cts 23
cp 1
crap 6
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
namespace TusPhp;
4
5
use Carbon\Carbon;
6
use TusPhp\Cache\Cacheable;
7
use TusPhp\Exception\FileException;
8
use TusPhp\Exception\ConnectionException;
9
use TusPhp\Exception\OutOfRangeException;
10
11
class File
12
{
13
    /** @const Max chunk size */
14
    const CHUNK_SIZE = 8192; // 8 bytes
15
16
    /** @const Checksum algorithm. */
17
    const HASH_ALGORITHM = 'sha256';
18
19
    /** @const Input stream */
20
    const INPUT_STREAM = 'php://input';
21
22
    /** @const Read binary mode */
23
    const READ_BINARY = 'rb';
24
25
    /** @const Append binary mode */
26
    const APPEND_BINARY = 'ab+';
27
28
    /** @var string */
29
    protected $checksum;
30
31
    /** @var string */
32
    protected $name;
33
34
    /** @var Cacheable */
35
    protected $cache;
36
37
    /** @var int */
38
    protected $offset;
39
40
    /** @var string */
41
    protected $location;
42
43
    /** @var string */
44
    protected $filePath;
45
46
    /** @var int */
47
    protected $fileSize;
48
49
    /**
50
     * File constructor.
51
     *
52
     * @param string|null    $name
53
     * @param Cacheable|null $cache
54
     */
55 1
    public function __construct(string $name = null, Cacheable $cache = null)
56
    {
57 1
        $this->name  = $name;
58 1
        $this->cache = $cache;
59 1
    }
60
61
    /**
62
     * Set file meta.
63
     *
64
     * @param int    $offset
65
     * @param int    $fileSize
66
     * @param string $filePath
67
     * @param string $location
68
     *
69
     * @return File
70
     */
71 1
    public function setMeta(int $offset, int $fileSize, string $filePath, string $location = null) : self
72
    {
73 1
        $this->offset   = $offset;
74 1
        $this->fileSize = $fileSize;
75 1
        $this->filePath = $filePath;
76 1
        $this->location = $location;
77
78 1
        return $this;
79
    }
80
81
    /**
82
     * Set name.
83
     *
84
     * @param string $name
85
     *
86
     * @return File
87
     */
88 1
    public function setName(string $name) : self
89
    {
90 1
        $this->name = $name;
91
92 1
        return $this;
93
    }
94
95
    /**
96
     * Get name.
97
     *
98
     * @return string
99
     */
100 1
    public function getName() : string
101
    {
102 1
        return $this->name;
103
    }
104
105
    /**
106
     * Set file size.
107
     *
108
     * @param int $size
109
     *
110
     * @return File
111
     */
112 1
    public function setFileSize(int $size) : self
113
    {
114 1
        $this->fileSize = $size;
115
116 1
        return $this;
117
    }
118
119
    /**
120
     * Get file size.
121
     *
122
     * @return int
123
     */
124 1
    public function getFileSize() : int
125
    {
126 1
        return $this->fileSize;
127
    }
128
129
    /**
130
     * Set checksum.
131
     *
132
     * @param string $checksum
133
     *
134
     * @return File
135
     */
136 1
    public function setChecksum(string $checksum) : self
137
    {
138 1
        $this->checksum = $checksum;
139
140 1
        return $this;
141
    }
142
143
    /**
144
     * Get checksum.
145
     *
146
     * @return string
147
     */
148 1
    public function getChecksum() : string
149
    {
150 1
        return $this->checksum;
151
    }
152
153
    /**
154
     * Set offset.
155
     *
156
     * @param int $offset
157
     *
158
     * @return File
159
     */
160 1
    public function setOffset(int $offset) : self
161
    {
162 1
        $this->offset = $offset;
163
164 1
        return $this;
165
    }
166
167
    /**
168
     * Get offset.
169
     *
170
     * @return int
171
     */
172 1
    public function getOffset() : int
173
    {
174 1
        return $this->offset;
175
    }
176
177
    /**
178
     * Set location.
179
     *
180
     * @param string $location
181
     *
182
     * @return File
183
     */
184 1
    public function setLocation(string $location) : self
185
    {
186 1
        $this->location = $location;
187
188 1
        return $this;
189
    }
190
191
    /**
192
     * Get location.
193
     *
194
     * @return string
195
     */
196 1
    public function getLocation() : string
197
    {
198 1
        return $this->location;
199
    }
200
201
    /**
202
     * Set absolute file location.
203
     *
204
     * @param string $path
205
     *
206
     * @return File
207
     */
208 1
    public function setFilePath(string $path) : self
209
    {
210 1
        $this->filePath = $path;
211
212 1
        return $this;
213
    }
214
215
    /**
216
     * Get absolute location.
217
     *
218
     * @return string
219
     */
220 1
    public function getFilePath() : string
221
    {
222 1
        return $this->filePath;
223
    }
224
225
    /**
226
     * Get input stream.
227
     *
228
     * @return string
229
     */
230 1
    public function getInputStream() : string
231
    {
232 1
        return self::INPUT_STREAM;
233
    }
234
235
    /**
236
     * Get file meta.
237
     *
238
     * @return array
239
     */
240 1
    public function details() : array
241
    {
242 1
        $now = Carbon::now();
243
244
        return [
245 1
            'name' => $this->name,
246 1
            'size' => $this->fileSize,
247 1
            'offset' => $this->offset,
248 1
            'location' => $this->location,
249 1
            'file_path' => $this->filePath,
250 1
            'created_at' => $now->format($this->cache::RFC_7231),
251 1
            'expires_at' => $now->addSeconds($this->cache->getTtl())->format($this->cache::RFC_7231),
252
        ];
253
    }
254
255
    /**
256
     * Upload file to server.
257
     *
258
     * @param int $totalBytes
259
     *
260
     * @throws ConnectionException
261
     *
262
     * @return int
263
     */
264 6
    public function upload(int $totalBytes) : int
265
    {
266 6
        $bytesWritten = $this->offset;
267
268 6
        if ($bytesWritten === $totalBytes) {
269 1
            return $bytesWritten;
270
        }
271
272 5
        $input    = $this->open($this->getInputStream(), self::READ_BINARY);
273 5
        $output   = $this->open($this->getFilePath(), self::APPEND_BINARY);
274 4
        $checksum = $this->getChecksum();
275
276
        try {
277 4
            $this->seek($output, $bytesWritten);
278
279 4
            while ( ! feof($input)) {
280 4
                if (CONNECTION_NORMAL !== connection_status()) {
281 1
                    throw new ConnectionException('Connection aborted by user.');
282
                }
283
284 3
                $data  = $this->read($input, self::CHUNK_SIZE);
285 3
                $bytes = $this->write($output, $data, self::CHUNK_SIZE);
286
287 3
                $bytesWritten += $bytes;
288
289 3
                $this->cache->set($checksum, ['offset' => $bytesWritten]);
290
291 3
                if ($bytesWritten > $totalBytes) {
292 1
                    throw new OutOfRangeException('The uploaded file is corrupt.');
293
                }
294
295 2
                if ($bytesWritten === $totalBytes) {
296 2
                    break;
297
                }
298
            }
299 2
        } finally {
300 4
            $this->close($input);
301 4
            $this->close($output);
302
        }
303
304 2
        return $bytesWritten;
305
    }
306
307
    /**
308
     * Open file in append binary mode.
309
     *
310
     * @param string $filePath
311
     * @param string $mode
312
     *
313
     * @throws FileException
314
     *
315
     * @return resource
316
     */
317 6
    public function open(string $filePath, string $mode)
318
    {
319 6
        if (self::INPUT_STREAM !== $filePath &&
320 6
            self::READ_BINARY === $mode &&
321 6
            ! file_exists($filePath)
322
        ) {
323 1
            throw new FileException('File not found.');
324
        }
325
326 5
        $ptr = @fopen($filePath, $mode);
327
328 5
        if (false === $ptr) {
329 2
            throw new FileException("Unable to open $filePath.");
330
        }
331
332 4
        return $ptr;
333
    }
334
335
    /**
336
     * @param Resource $handle
337
     * @param int      $offset
338
     * @param int      $whence
339
     *
340
     * @throws FileException
341
     *
342
     * @return int
343
     */
344 3
    public function seek($handle, int $offset, int $whence = SEEK_SET) : int
345
    {
346 3
        $position = fseek($handle, $offset, $whence);
347
348 3
        if (-1 === $position) {
349 1
            throw new FileException('Cannot move pointer to desired position.');
350
        }
351
352 2
        return $position;
353
    }
354
355
    /**
356
     * Read data from file.
357
     *
358
     * @param Resource $handle
359
     * @param int      $chunkSize
360
     *
361
     * @throws FileException
362
     *
363
     * @return string
364
     */
365 3
    public function read($handle, int $chunkSize) : string
366
    {
367 3
        $data = fread($handle, $chunkSize);
368
369 3
        if (false === $data) {
370 1
            throw new FileException('Cannot read file.');
371
        }
372
373 2
        return (string) $data;
374
    }
375
376
    /**
377
     * Write data to file.
378
     *
379
     * @param Resource $handle
380
     * @param string   $data
381
     * @param int|null $length
382
     *
383
     * @throws FileException
384
     *
385
     * @return int
386
     */
387 2
    public function write($handle, string $data, $length = null) : int
388
    {
389 2
        $bytesWritten = fwrite($handle, $data, $length);
390
391 2
        if (false === $bytesWritten) {
392 1
            throw new FileException('Cannot write to a file.');
393
        }
394
395 1
        return $bytesWritten;
396
    }
397
398
    /**
399
     * Close file.
400
     *
401
     * @param $handle
402
     *
403
     * @return bool
404
     */
405 4
    public function close($handle) : bool
406
    {
407 4
        return fclose($handle);
408
    }
409
}
410