Passed
Push — dev ( 72eb07...9b7706 )
by 世昌
02:33
created

FileRangeProcessor::getRangeHeader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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