Completed
Push — master ( ccc96b...3d250b )
by Ankit
02:27
created

File::setUploadMetadata()   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
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 0
cts 3
cp 0
crap 2
rs 10
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 Input stream */
17
    const INPUT_STREAM = 'php://input';
18
19
    /** @const Read binary mode */
20
    const READ_BINARY = 'rb';
21
22
    /** @const Append binary mode */
23
    const APPEND_BINARY = 'ab+';
24
25
    /** @var string */
26
    protected $key;
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
    /** @var string[] */
50
    private $uploadMetadata = [];
51
52
    /**
53
     * File constructor.
54
     *
55
     * @param string|null    $name
56
     * @param Cacheable|null $cache
57
     */
58 1
    public function __construct(string $name = null, Cacheable $cache = null)
59
    {
60 1
        $this->name  = $name;
61 1
        $this->cache = $cache;
62 1
    }
63
64
    /**
65
     * Set file meta.
66
     *
67
     * @param int    $offset
68
     * @param int    $fileSize
69
     * @param string $filePath
70
     * @param string $location
71
     *
72
     * @return File
73
     */
74 1
    public function setMeta(int $offset, int $fileSize, string $filePath, string $location = null) : self
75
    {
76 1
        $this->offset   = $offset;
77 1
        $this->fileSize = $fileSize;
78 1
        $this->filePath = $filePath;
79 1
        $this->location = $location;
80
81 1
        return $this;
82
    }
83
84
    /**
85
     * Set name.
86
     *
87
     * @param string $name
88
     *
89
     * @return File
90
     */
91 1
    public function setName(string $name) : self
92
    {
93 1
        $this->name = $name;
94
95 1
        return $this;
96
    }
97
98
    /**
99
     * Get name.
100
     *
101
     * @return string
102
     */
103 1
    public function getName() : string
104
    {
105 1
        return $this->name;
106
    }
107
108
    /**
109
     * Set file size.
110
     *
111
     * @param int $size
112
     *
113
     * @return File
114
     */
115 1
    public function setFileSize(int $size) : self
116
    {
117 1
        $this->fileSize = $size;
118
119 1
        return $this;
120
    }
121
122
    /**
123
     * Get file size.
124
     *
125
     * @return int
126
     */
127 1
    public function getFileSize() : int
128
    {
129 1
        return $this->fileSize;
130
    }
131
132
    /**
133
     * Set key.
134
     *
135
     * @param string $key
136
     *
137
     * @return File
138
     */
139 1
    public function setKey(string $key) : self
140
    {
141 1
        $this->key = $key;
142
143 1
        return $this;
144
    }
145
146
    /**
147
     * Get key.
148
     *
149
     * @return string
150
     */
151 1
    public function getKey() : string
152
    {
153 1
        return $this->key;
154
    }
155
156
    /**
157
     * Set checksum.
158
     *
159
     * @param string $checksum
160
     *
161
     * @return File
162
     */
163 1
    public function setChecksum(string $checksum) : self
164
    {
165 1
        $this->checksum = $checksum;
166
167 1
        return $this;
168
    }
169
170
    /**
171
     * Get checksum.
172
     *
173
     * @return string
174
     */
175 1
    public function getChecksum() : string
176
    {
177 1
        return $this->checksum;
178
    }
179
180
    /**
181
     * Set offset.
182
     *
183
     * @param int $offset
184
     *
185
     * @return File
186
     */
187 1
    public function setOffset(int $offset) : self
188
    {
189 1
        $this->offset = $offset;
190
191 1
        return $this;
192
    }
193
194
    /**
195
     * Get offset.
196
     *
197
     * @return int
198
     */
199 1
    public function getOffset() : int
200
    {
201 1
        return $this->offset;
202
    }
203
204
    /**
205
     * Set location.
206
     *
207
     * @param string $location
208
     *
209
     * @return File
210
     */
211 1
    public function setLocation(string $location) : self
212
    {
213 1
        $this->location = $location;
214
215 1
        return $this;
216
    }
217
218
    /**
219
     * Get location.
220
     *
221
     * @return string
222
     */
223 1
    public function getLocation() : string
224
    {
225 1
        return $this->location;
226
    }
227
228
    /**
229
     * Set absolute file location.
230
     *
231
     * @param string $path
232
     *
233
     * @return File
234
     */
235 1
    public function setFilePath(string $path) : self
236
    {
237 1
        $this->filePath = $path;
238
239 1
        return $this;
240
    }
241
242
    /**
243
     * Get absolute location.
244
     *
245
     * @return string
246
     */
247 1
    public function getFilePath() : string
248
    {
249 1
        return $this->filePath;
250
    }
251
252
    /**
253
     * @param string[] $metadata
254
     *
255
     * @return File
256
     */
257
    public function setUploadMetadata(array $metadata) : self
258
    {
259
        $this->uploadMetadata = $metadata;
260
261
        return $this;
262
    }
263
264
    /**
265
     * Get input stream.
266
     *
267
     * @return string
268
     */
269 1
    public function getInputStream() : string
270
    {
271 1
        return self::INPUT_STREAM;
272
    }
273
274
    /**
275
     * Get file meta.
276
     *
277
     * @return array
278
     */
279 1
    public function details() : array
280
    {
281 1
        $now = Carbon::now();
282
283
        return [
284 1
            'name' => $this->name,
285 1
            'size' => $this->fileSize,
286 1
            'offset' => $this->offset,
287 1
            'checksum' => $this->checksum,
288 1
            'location' => $this->location,
289 1
            'file_path' => $this->filePath,
290 1
            'metadata' => $this->uploadMetadata,
291 1
            'created_at' => $now->format($this->cache::RFC_7231),
292 1
            'expires_at' => $now->addSeconds($this->cache->getTtl())->format($this->cache::RFC_7231),
293
        ];
294
    }
295
296
    /**
297
     * Upload file to server.
298
     *
299
     * @param int $totalBytes
300
     *
301
     * @throws ConnectionException
302
     *
303
     * @return int
304
     */
305 6
    public function upload(int $totalBytes) : int
306
    {
307 6
        if ($this->offset === $totalBytes) {
308 1
            return $this->offset;
309
        }
310
311 5
        $input  = $this->open($this->getInputStream(), self::READ_BINARY);
312 5
        $output = $this->open($this->getFilePath(), self::APPEND_BINARY);
313 4
        $key    = $this->getKey();
314
315
        try {
316 4
            $this->seek($output, $this->offset);
317
318 4
            while ( ! feof($input)) {
319 4
                if (CONNECTION_NORMAL !== connection_status()) {
320 1
                    throw new ConnectionException('Connection aborted by user.');
321
                }
322
323 3
                $data  = $this->read($input, self::CHUNK_SIZE);
324 3
                $bytes = $this->write($output, $data, self::CHUNK_SIZE);
325
326 3
                $this->offset += $bytes;
327
328 3
                $this->cache->set($key, ['offset' => $this->offset]);
329
330 3
                if ($this->offset > $totalBytes) {
331 1
                    throw new OutOfRangeException('The uploaded file is corrupt.');
332
                }
333
334 2
                if ($this->offset === $totalBytes) {
335 2
                    break;
336
                }
337
            }
338 2
        } finally {
339 4
            $this->close($input);
340 4
            $this->close($output);
341
        }
342
343 2
        return $this->offset;
344
    }
345
346
    /**
347
     * Open file in given mode.
348
     *
349
     * @param string $filePath
350
     * @param string $mode
351
     *
352
     * @throws FileException
353
     *
354
     * @return resource
355
     */
356 6
    public function open(string $filePath, string $mode)
357
    {
358 6
        $this->exists($filePath, $mode);
359
360 5
        $ptr = @fopen($filePath, $mode);
361
362 5
        if (false === $ptr) {
363 2
            throw new FileException("Unable to open $filePath.");
364
        }
365
366 4
        return $ptr;
367
    }
368
369
    /**
370
     * Check if file to read exists.
371
     *
372
     * @param string $filePath
373
     * @param string $mode
374
     *
375
     * @throws FileException
376
     *
377
     * @return bool
378
     */
379 5
    public function exists(string $filePath, string $mode = self::READ_BINARY) : bool
380
    {
381 5
        if (self::INPUT_STREAM === $filePath) {
382 1
            return true;
383
        }
384
385 5
        if (self::READ_BINARY === $mode && ! file_exists($filePath)) {
386 2
            throw new FileException('File not found.');
387
        }
388
389 3
        return true;
390
    }
391
392
    /**
393
     * Move file pointer to given offset.
394
     *
395
     * @param Resource $handle
396
     * @param int      $offset
397
     * @param int      $whence
398
     *
399
     * @throws FileException
400
     *
401
     * @return int
402
     */
403 3
    public function seek($handle, int $offset, int $whence = SEEK_SET) : int
404
    {
405 3
        $position = fseek($handle, $offset, $whence);
406
407 3
        if (-1 === $position) {
408 1
            throw new FileException('Cannot move pointer to desired position.');
409
        }
410
411 2
        return $position;
412
    }
413
414
    /**
415
     * Read data from file.
416
     *
417
     * @param Resource $handle
418
     * @param int      $chunkSize
419
     *
420
     * @throws FileException
421
     *
422
     * @return string
423
     */
424 3
    public function read($handle, int $chunkSize) : string
425
    {
426 3
        $data = fread($handle, $chunkSize);
427
428 3
        if (false === $data) {
429 1
            throw new FileException('Cannot read file.');
430
        }
431
432 2
        return (string) $data;
433
    }
434
435
    /**
436
     * Write data to file.
437
     *
438
     * @param Resource $handle
439
     * @param string   $data
440
     * @param int|null $length
441
     *
442
     * @throws FileException
443
     *
444
     * @return int
445
     */
446 2
    public function write($handle, string $data, $length = null) : int
447
    {
448 2
        $bytesWritten = is_int($length) ? fwrite($handle, $data, $length) : fwrite($handle, $data);
449
450 2
        if (false === $bytesWritten) {
0 ignored issues
show
introduced by
The condition false === $bytesWritten is always false.
Loading history...
451 1
            throw new FileException('Cannot write to a file.');
452
        }
453
454 1
        return $bytesWritten;
455
    }
456
457
    /**
458
     * Merge 2 or more files.
459
     *
460
     * @param array $files File data with meta info.
461
     *
462
     * @return int
463
     */
464 3
    public function merge(array $files) : int
465
    {
466 3
        $destination = $this->getFilePath();
467 3
        $firstFile   = array_shift($files);
468
469
        // First partial file can directly be copied.
470 3
        $this->copy($firstFile['file_path'], $destination);
471
472 2
        $this->offset   = $firstFile['offset'];
473 2
        $this->fileSize = filesize($firstFile['file_path']);
474
475 2
        $handle = $this->open($destination, self::APPEND_BINARY);
476
477 2
        foreach ($files as $file) {
478 2
            if ( ! file_exists($file['file_path'])) {
479 1
                throw new FileException('File to be merged not found.');
480
            }
481
482 1
            $this->fileSize += $this->write($handle, file_get_contents($file['file_path']));
483
484 1
            $this->offset += $file['offset'];
485
        }
486
487 1
        $this->close($handle);
488
489 1
        return $this->fileSize;
490
    }
491
492
    /**
493
     * Copy file from source to destination.
494
     *
495
     * @param string $source
496
     * @param string $destination
497
     *
498
     * @return bool
499
     */
500 2
    public function copy(string $source, string $destination) : bool
501
    {
502 2
        $status = @copy($source, $destination);
503
504 2
        if (false === $status) {
505 1
            throw new FileException(sprintf('Cannot copy source (%s) to destination (%s).', $source, $destination));
506
        }
507
508 1
        return $status;
509
    }
510
511
    /**
512
     * Delete file and/or folder.
513
     *
514
     * @param array $files
515
     * @param bool  $folder
516
     *
517
     * @return bool
518
     */
519 4
    public function delete(array $files, bool $folder = false) : bool
520
    {
521 4
        $status = $this->deleteFiles($files);
522
523 4
        if ($status && $folder) {
524 2
            return rmdir(dirname(current($files)));
525
        }
526
527 2
        return $status;
528
    }
529
530
    /**
531
     * Delete multiple files.
532
     *
533
     * @param array $files
534
     *
535
     * @return bool
536
     */
537 3
    public function deleteFiles(array $files) : bool
538
    {
539 3
        if (empty($files)) {
540 1
            return false;
541
        }
542
543 2
        $status = true;
544
545 2
        foreach ($files as $file) {
546 2
            if (file_exists($file)) {
547 1
                $status = $status && unlink($file);
548
            }
549
        }
550
551 2
        return $status;
552
    }
553
554
    /**
555
     * Close file.
556
     *
557
     * @param $handle
558
     *
559
     * @return bool
560
     */
561 4
    public function close($handle) : bool
562
    {
563 4
        return fclose($handle);
564
    }
565
}
566