Completed
Push — master ( b339ab...2f90cc )
by Ankit
06:47
created

src/File.php (1 issue)

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