Passed
Push — master ( b45126...8af3f1 )
by 世昌
03:18
created

FileRangeProccessor::sendFileRanges()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 12
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 14
rs 9.8666
1
<?php
2
3
namespace suda\application\processor;
4
5
use Exception;
6
use function explode;
7
use function is_array;
8
use function md5;
9
use function rtrim;
10
use SplFileObject;
11
use function strpos;
12
use function substr;
13
use suda\framework\Request;
14
use suda\framework\Response;
15
use suda\application\Application;
16
use suda\framework\response\MimeType;
17
use suda\framework\http\stream\DataStream;
18
use function uniqid;
19
20
/**
21
 * 响应
22
 */
23
class FileRangeProccessor implements RequestProcessor
24
{
25
    /**
26
     * 文件路径
27
     *
28
     * @var SplFileObject
29
     */
30
    protected $file;
31
32
    /**
33
     * MIME
34
     *
35
     * @var string
36
     */
37
    protected $mime;
38
39
    public function __construct($file)
40
    {
41
        $this->file = $file instanceof SplFileObject ? $file : new SplFileObject($file);
42
        $this->mime = MimeType::getMimeType($this->file->getExtension());
43
    }
44
45
    /**
46
     * 处理文件请求
47
     *
48
     * @param Application $application
49
     * @param Request $request
50
     * @param Response $response
51
     * @return void
52
     * @throws Exception
53
     */
54
    public function onRequest(Application $application, Request $request, Response $response)
55
    {
56
        $ranges = $this->getRanges($request);
57
        $response->setHeader('accept-ranges', 'bytes');
58
        if ($request->getMethod() !== 'GET' || $ranges === false) {
59
            $response->status(400);
60
        } else {
61
            $this->sendFileRanges($response, $ranges);
62
        }
63
    }
64
65
    /**
66
     * @param Response $response
67
     * @param array $ranges
68
     * @throws Exception
69
     */
70
    protected function sendFileRanges(Response $response, array $ranges)
71
    {
72
        if (count($ranges) === 0) {
73
            $response->setHeader('content-type', $this->mime);
74
            $response->sendFile($this->file->getRealPath());
75
        } elseif (count($ranges) === 1) {
76
            $response->status(206);
77
            $range = $ranges[0];
78
            $response->setHeader('content-type', $this->mime);
79
            $response->setHeader('content-range', $this->getRangeHeader($range));
80
            $this->sendFileByRange($response, $range);
81
        } else {
82
            $response->status(206);
83
            $this->sendMultipleFileByRange($response, $ranges);
84
        }
85
    }
86
87
    /**
88
     * 发送多Range
89
     *
90
     * @param Response $response
91
     * @param array $ranges
92
     * @return void
93
     */
94
    protected function sendMultipleFileByRange(Response $response, array $ranges)
95
    {
96
        $separates = 'multiple_range_' . base64_encode(md5(uniqid(), true));
97
        $response->setHeader('content-type', 'multipart/byteranges; boundary=' . $separates);
98
        foreach ($ranges as $range) {
99
            $response->write('--' . $separates . "\r\n");
100
            $this->sendMultipleRangePart($response, $range);
101
            $this->sendFileByRange($response, $range);
102
            $response->write("\r\n");
103
        }
104
    }
105
106
107
    /**
108
     * 发送范围数据
109
     *
110
     * @param Response $response
111
     * @param array $range
112
     * @return void
113
     */
114
    protected function sendFileByRange(Response $response, array $range)
115
    {
116
        // [start,end] = $end - $start + 1
117
        $response->write(new DataStream($this->file, $range['start'], $range['end'] - $range['start'] + 1));
118
    }
119
120
    /**
121
     * 获取Range描述
122
     * @param Request $request
123
     * @return array|bool
124
     */
125
    protected function getRanges(Request $request)
126
    {
127
        $ranges = $this->parseRangeHeader($request);
128
        if (count($ranges) > 0) {
129
            return $this->parseRanges($ranges);
130
        }
131
        return [];
132
    }
133
134
    /**
135
     * 写Range头
136
     *
137
     * @param Response $response
138
     * @param array $range
139
     * @return void
140
     */
141
    protected function sendMultipleRangePart(Response $response, array $range)
142
    {
143
        $response->write('Content-Type: ' . $this->mime . "\r\n");
144
        $response->write('Content-Range: ' . $this->getRangeHeader($range) . "\r\n\r\n");
145
    }
146
147
    /**
148
     * 生成Range头
149
     *
150
     * @param array $range
151
     * @return string
152
     */
153
    protected function getRangeHeader(array $range): string
154
    {
155
        return sprintf('bytes %d-%d/%d', $range['start'], $range['end'], $this->file->getSize());
156
    }
157
158
    /**
159
     * 获取Range描述
160
     * @param Request $request
161
     * @return array
162
     */
163
    protected function parseRangeHeader(Request $request)
164
    {
165
        $range = $request->getHeader('range', null);
166
        $range = trim($range);
167
        if (strpos($range, 'bytes=') === 0) {
168
            $rangesFrom = substr($range, strlen('bytes='));
169
            return explode(',', $rangesFrom);
170
        }
171
        return [];
172
    }
173
174
    /**
175
     * 处理范围
176
     *
177
     * @param array $ranges
178
     * @return array|bool
179
     */
180
    protected function parseRanges(array $ranges)
181
    {
182
        $range = [];
183
        foreach ($ranges as $value) {
184
            if (($r = $this->parseRange($value)) !== null) {
185
                $range[] = $r;
186
            } else {
187
                return false;
188
            }
189
        }
190
        return $range;
191
    }
192
193
    /**
194
     * 处理Range
195
     * @param string $range
196
     * @return array|null
197
     */
198
    protected function parseRange(string $range): ?array
199
    {
200
        $range = trim($range);
201
        if (strrpos($range, '-') === strlen($range) - 1) {
202
            return [
203
                'start' => intval(rtrim($range, '-')),
204
                'end' => $this->file->getSize() - 1,
205
            ];
206
        } elseif (strpos($range, '-') !== false) {
207
            list($start, $end) = explode('-', $range, 2);
208
            $length = intval($end - $start);
209
            if ($length <= 0) {
210
                return ['start' => intval($start), 'end' => $this->file->getSize() - 1];
211
            }
212
            return ['start' => intval($start), 'end' => intval($end)];
213
        }
214
        return null;
215
    }
216
}
217