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