Passed
Push — master ( acba12...b2f89b )
by 世昌
04:15
created

FileRangeProcessor::onRequest()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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