Completed
Pull Request — master (#166)
by
unknown
03:05
created

File::setMetaInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
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 4
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 $metaInfo = [];
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[] $metaInfo
254
     * @return File
255
     */
256
    public function setMetaInfo(array $metaInfo): self
257
    {
258
        $this->metaInfo = $metaInfo;
259
        return $this;
260
    }
261
262
    /**
263
     * Get input stream.
264
     *
265
     * @return string
266
     */
267 1
    public function getInputStream() : string
268
    {
269 1
        return self::INPUT_STREAM;
270
    }
271
272
    /**
273
     * Get file meta.
274
     *
275
     * @return array
276
     */
277 1
    public function details() : array
278
    {
279 1
        $now = Carbon::now();
280
281 1
        return array_merge($this->metaInfo, [
282 1
            'name' => $this->name,
283 1
            'size' => $this->fileSize,
284 1
            'offset' => $this->offset,
285 1
            'checksum' => $this->checksum,
286 1
            'location' => $this->location,
287 1
            'file_path' => $this->filePath,
288 1
            'created_at' => $now->format($this->cache::RFC_7231),
289 1
            'expires_at' => $now->addSeconds($this->cache->getTtl())->format($this->cache::RFC_7231),
290
        ]);
291
    }
292
293
    /**
294
     * Upload file to server.
295
     *
296
     * @param int $totalBytes
297
     *
298
     * @throws ConnectionException
299
     *
300
     * @return int
301
     */
302 6
    public function upload(int $totalBytes) : int
303
    {
304 6
        if ($this->offset === $totalBytes) {
305 1
            return $this->offset;
306
        }
307
308 5
        $input  = $this->open($this->getInputStream(), self::READ_BINARY);
309 5
        $output = $this->open($this->getFilePath(), self::APPEND_BINARY);
310 4
        $key    = $this->getKey();
311
312
        try {
313 4
            $this->seek($output, $this->offset);
314
315 4
            while ( ! feof($input)) {
316 4
                if (CONNECTION_NORMAL !== connection_status()) {
317 1
                    throw new ConnectionException('Connection aborted by user.');
318
                }
319
320 3
                $data  = $this->read($input, self::CHUNK_SIZE);
321 3
                $bytes = $this->write($output, $data, self::CHUNK_SIZE);
322
323 3
                $this->offset += $bytes;
324
325 3
                $this->cache->set($key, ['offset' => $this->offset]);
326
327 3
                if ($this->offset > $totalBytes) {
328 1
                    throw new OutOfRangeException('The uploaded file is corrupt.');
329
                }
330
331 2
                if ($this->offset === $totalBytes) {
332 2
                    break;
333
                }
334
            }
335 2
        } finally {
336 4
            $this->close($input);
337 4
            $this->close($output);
338
        }
339
340 2
        return $this->offset;
341
    }
342
343
    /**
344
     * Open file in given mode.
345
     *
346
     * @param string $filePath
347
     * @param string $mode
348
     *
349
     * @throws FileException
350
     *
351
     * @return resource
352
     */
353 6
    public function open(string $filePath, string $mode)
354
    {
355 6
        $this->exists($filePath, $mode);
356
357 5
        $ptr = @fopen($filePath, $mode);
358
359 5
        if (false === $ptr) {
360 2
            throw new FileException("Unable to open $filePath.");
361
        }
362
363 4
        return $ptr;
364
    }
365
366
    /**
367
     * Check if file to read exists.
368
     *
369
     * @param string $filePath
370
     * @param string $mode
371
     *
372
     * @throws FileException
373
     *
374
     * @return bool
375
     */
376 5
    public function exists(string $filePath, string $mode = self::READ_BINARY) : bool
377
    {
378 5
        if (self::INPUT_STREAM === $filePath) {
379 1
            return true;
380
        }
381
382 5
        if (self::READ_BINARY === $mode && ! file_exists($filePath)) {
383 2
            throw new FileException('File not found.');
384
        }
385
386 3
        return true;
387
    }
388
389
    /**
390
     * Move file pointer to given offset.
391
     *
392
     * @param Resource $handle
393
     * @param int      $offset
394
     * @param int      $whence
395
     *
396
     * @throws FileException
397
     *
398
     * @return int
399
     */
400 3
    public function seek($handle, int $offset, int $whence = SEEK_SET) : int
401
    {
402 3
        $position = fseek($handle, $offset, $whence);
403
404 3
        if (-1 === $position) {
405 1
            throw new FileException('Cannot move pointer to desired position.');
406
        }
407
408 2
        return $position;
409
    }
410
411
    /**
412
     * Read data from file.
413
     *
414
     * @param Resource $handle
415
     * @param int      $chunkSize
416
     *
417
     * @throws FileException
418
     *
419
     * @return string
420
     */
421 3
    public function read($handle, int $chunkSize) : string
422
    {
423 3
        $data = fread($handle, $chunkSize);
424
425 3
        if (false === $data) {
426 1
            throw new FileException('Cannot read file.');
427
        }
428
429 2
        return (string) $data;
430
    }
431
432
    /**
433
     * Write data to file.
434
     *
435
     * @param Resource $handle
436
     * @param string   $data
437
     * @param int|null $length
438
     *
439
     * @throws FileException
440
     *
441
     * @return int
442
     */
443 2
    public function write($handle, string $data, $length = null) : int
444
    {
445 2
        $bytesWritten = is_int($length) ? fwrite($handle, $data, $length) : fwrite($handle, $data);
446
447 2
        if (false === $bytesWritten) {
0 ignored issues
show
introduced by
The condition false === $bytesWritten is always false.
Loading history...
448 1
            throw new FileException('Cannot write to a file.');
449
        }
450
451 1
        return $bytesWritten;
452
    }
453
454
    /**
455
     * Merge 2 or more files.
456
     *
457
     * @param array $files File data with meta info.
458
     *
459
     * @return int
460
     */
461 3
    public function merge(array $files) : int
462
    {
463 3
        $destination = $this->getFilePath();
464 3
        $firstFile   = array_shift($files);
465
466
        // First partial file can directly be copied.
467 3
        $this->copy($firstFile['file_path'], $destination);
468
469 2
        $this->offset   = $firstFile['offset'];
470 2
        $this->fileSize = filesize($firstFile['file_path']);
471
472 2
        $handle = $this->open($destination, self::APPEND_BINARY);
473
474 2
        foreach ($files as $file) {
475 2
            if ( ! file_exists($file['file_path'])) {
476 1
                throw new FileException('File to be merged not found.');
477
            }
478
479 1
            $this->fileSize += $this->write($handle, file_get_contents($file['file_path']));
480
481 1
            $this->offset += $file['offset'];
482
        }
483
484 1
        $this->close($handle);
485
486 1
        return $this->fileSize;
487
    }
488
489
    /**
490
     * Copy file from source to destination.
491
     *
492
     * @param string $source
493
     * @param string $destination
494
     *
495
     * @return bool
496
     */
497 2
    public function copy(string $source, string $destination) : bool
498
    {
499 2
        $status = @copy($source, $destination);
500
501 2
        if (false === $status) {
502 1
            throw new FileException(sprintf('Cannot copy source (%s) to destination (%s).', $source, $destination));
503
        }
504
505 1
        return $status;
506
    }
507
508
    /**
509
     * Delete file and/or folder.
510
     *
511
     * @param array $files
512
     * @param bool  $folder
513
     *
514
     * @return bool
515
     */
516 4
    public function delete(array $files, bool $folder = false) : bool
517
    {
518 4
        $status = $this->deleteFiles($files);
519
520 4
        if ($status && $folder) {
521 2
            return rmdir(dirname(current($files)));
522
        }
523
524 2
        return $status;
525
    }
526
527
    /**
528
     * Delete multiple files.
529
     *
530
     * @param array $files
531
     *
532
     * @return bool
533
     */
534 3
    public function deleteFiles(array $files) : bool
535
    {
536 3
        if (empty($files)) {
537 1
            return false;
538
        }
539
540 2
        $status = true;
541
542 2
        foreach ($files as $file) {
543 2
            if (file_exists($file)) {
544 2
                $status = $status && unlink($file);
545
            }
546
        }
547
548 2
        return $status;
549
    }
550
551
    /**
552
     * Close file.
553
     *
554
     * @param $handle
555
     *
556
     * @return bool
557
     */
558 4
    public function close($handle) : bool
559
    {
560 4
        return fclose($handle);
561
    }
562
}
563