Passed
Branch tus-concatenation (500f8f)
by Ankit
02:34
created

File::delete()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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