Passed
Push — master ( 42aecd...859d10 )
by Chris
01:43
created

FileResource::sendDataChunk()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 2
nop 2
dl 0
loc 15
ccs 0
cts 11
cp 0
crap 12
rs 9.4285
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace DaveRandom\Resume;
4
5
final class FileResource implements Resource
6
{
7
    public const DEFAULT_CHUNK_SIZE = 8192;
8
9
    /**
10
     * Full canonical path of file on local file system
11
     *
12
     * @var string
13
     */
14
    private $localPath;
15
16
    /**
17
     * MIME type of file contents
18
     *
19
     * @var string
20
     */
21
    private $mimeType;
22
23
    /**
24
     * Size of local file, in bytes
25
     *
26
     * @var int
27
     */
28
    private $fileSize;
29
30
    /**
31
     * Stream handle for reading the file
32
     *
33
     * @var resource|null
34
     */
35
    private $handle = null;
36
37
    /**
38
     * Chunk size for local file system reads when sending a partial file
39
     *
40
     * @var int
41
     */
42
    private $chunkSize = self::DEFAULT_CHUNK_SIZE;
43
44
    /**
45
     * Open the local file handle if it's not open yet, and set the pointer to the supplied position
46
     *
47
     * @param int $position
48
     */
49
    private function openFile(int $position): void
50
    {
51
        if ($this->handle === null && !$this->handle = \fopen($this->localPath, 'r')) {
52
            throw new SendFileFailureException("Failed to open '{$this->localPath}' for reading");
53
        }
54
55
        if (\fseek($this->handle, $position, \SEEK_SET) !== 0) {
56
            throw new SendFileFailureException('fseek() operation failed');
57
        }
58
    }
59
60
    /**
61
     * Send a chunk of data to the client
62
     *
63
     * @param OutputWriter $outputWriter
64
     * @param int $length
65
     * @return int
66
     */
67
    private function sendDataChunk(OutputWriter $outputWriter, int $length): int
68
    {
69
        $read = $length > $this->chunkSize
70
            ? $this->chunkSize
71
            : $length;
72
73
        $data = \fread($this->handle, $read);
74
75
        if ($data === false) {
76
            throw new SendFileFailureException('fread() operation failed');
77
        }
78
79
        $outputWriter->sendData($data);
80
81
        return \strlen($data);
82
    }
83
84
    /**
85
     * @param string $path Path of file on local file system
86
     * @param string $mimeType MIME type of file contents
87
     * @param int $chunkSize Chunk size for local file system reads when sending a partial file
88
     */
89
    public function __construct(string $path, string $mimeType = null, int $chunkSize = self::DEFAULT_CHUNK_SIZE)
90
    {
91
        $this->chunkSize = $chunkSize;
92
        $this->mimeType = $mimeType ?? 'application/octet-stream';
93
94
        // Make sure the file exists and is a file, otherwise we are wasting our time
95
        $this->localPath = \realpath($path);
96
97
        if ($this->localPath === false || !\is_file($this->localPath)) {
98
            throw new NonExistentFileException("Local path '{$path}' does not exist or is not a file");
99
        }
100
101
        // This shouldn't ever fail but just in case
102
        if (false === $this->fileSize = \filesize($this->localPath)) {
103
            throw new UnreadableFileException("Failed to retrieve size of file '{$this->localPath}'");
104
        }
105
    }
106
107
    /**
108
     * Explicitly close the file handle if it's open
109
     */
110
    public function __destruct()
111
    {
112
        if ($this->handle !== null) {
113
            \fclose($this->handle);
114
        }
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function sendData(OutputWriter $outputWriter, Range $range = null, string $unit = null): void
121
    {
122
        if (\strtolower($unit ?? 'bytes') !== 'bytes') {
123
            throw new UnsatisfiableRangeException('Unit not handled by this resource: ' . $unit);
124
        }
125
126
        $start = 0;
127
        $length = $this->fileSize;
128
129
        if ($range !== null) {
130
            $start = $range->getStart();
131
            $length = $range->getLength();
132
        }
133
134
        $this->openFile($start);
135
136
        while ($length > 0) {
137
            $length -= $this->sendDataChunk($outputWriter, $length);
138
        }
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function getLength(): int
145
    {
146
        return $this->fileSize;
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152
    public function getMimeType(): string
153
    {
154
        return $this->mimeType;
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160
    public function getAdditionalHeaders(): array
161
    {
162
        return [
163
            'Content-Disposition' => 'attachment; filename="' . \basename($this->localPath) . '"'
164
        ];
165
    }
166
167
    /**
168
     * Get the chunk size for local file system reads when sending a partial file
169
     *
170
     * @return int
171
     */
172
    public function getChunkSize(): int
173
    {
174
        return $this->chunkSize;
175
    }
176
177
    /**
178
     * Set the chunk size for local file system reads when sending a partial file
179
     *
180
     * @param int $chunkSize
181
     */
182
    public function setChunkSize(int $chunkSize): void
183
    {
184
        $this->chunkSize = $chunkSize;
185
    }
186
}
187