Passed
Push — dev ( 948542...ee24c9 )
by 世昌
02:55
created

FileRangeProccessor::sendMultipleFileByRange()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 2
b 0
f 0
nc 2
nop 2
dl 0
loc 9
rs 10
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 ($ranges === null) {
0 ignored issues
show
introduced by
The condition $ranges === null is always false.
Loading history...
59
            $response->setHeader('content-type', $this->mime);
60
            $response->sendFile($this->file->getRealPath());
61
        } elseif (is_array($ranges)) {
0 ignored issues
show
introduced by
The condition is_array($ranges) is always true.
Loading history...
62
            $this->sendRanges($ranges, $response);
63
        } elseif ($ranges === false || $request->getMethod() !== 'GET') {
64
            $response->status(400);
65
        }
66
    }
67
68
    protected function sendRanges(array $ranges, Response $response)
69
    {
70
        if (count($ranges) === 1) {
71
            $response->status(206);
72
            $range = $ranges[0];
73
            $response->setHeader('content-type', $this->mime);
74
            $response->setHeader('content-range', $this->getRangeHeader($range));
75
            $this->sendFileByRange($response, $range);
76
        } else {
77
            $response->status(206);
78
            $this->sendMultipleFileByRange($response, $ranges);
79
        }
80
    }
81
82
    /**
83
     * 发送多Range
84
     *
85
     * @param Response $response
86
     * @param array $ranges
87
     * @return void
88
     */
89
    protected function sendMultipleFileByRange(Response $response, array $ranges)
90
    {
91
        $separates = 'multiple_range_' . base64_encode(md5(uniqid(), true));
92
        $response->setHeader('content-type', 'multipart/byteranges; boundary=' . $separates);
93
        foreach ($ranges as $range) {
94
            $response->write('--' . $separates . "\r\n");
95
            $this->sendMultipleRangePart($response, $range);
96
            $this->sendFileByRange($response, $range);
97
            $response->write("\r\n");
98
        }
99
    }
100
101
102
    /**
103
     * 发送范围数据
104
     *
105
     * @param Response $response
106
     * @param array $range
107
     * @return void
108
     */
109
    protected function sendFileByRange(Response $response, array $range)
110
    {
111
        // [start,end] = $end - $start + 1
112
        $response->write(new DataStream($this->file, $range['start'], $range['end'] - $range['start'] + 1));
113
    }
114
115
    /**
116
     * 获取Range描述
117
     *
118
     * @param Request $request
119
     * @return array|bool|null
120
     */
121
    protected function getRanges(Request $request)
122
    {
123
        $ranges = $this->parseRangeHeader($request);
124
        if (is_array($ranges)) {
125
            return $this->parseRanges($ranges);
126
        } elseif ($ranges === false) {
127
            return false;
128
        }
129
        return null;
130
    }
131
132
    /**
133
     * 写Range头
134
     *
135
     * @param Response $response
136
     * @param array $range
137
     * @return void
138
     */
139
    protected function sendMultipleRangePart(Response $response, array $range)
140
    {
141
        $response->write('Content-Type: ' . $this->mime . "\r\n");
142
        $response->write('Content-Range: ' . $this->getRangeHeader($range) . "\r\n\r\n");
143
    }
144
145
    /**
146
     * 生成Range头
147
     *
148
     * @param array $range
149
     * @return string
150
     */
151
    protected function getRangeHeader(array $range): string
152
    {
153
        return sprintf('bytes %d-%d/%d', $range['start'], $range['end'], $this->file->getSize());
154
    }
155
156
    /**
157
     * 获取Range描述
158
     *
159
     * @param Request $request
160
     * @return array|bool|null
161
     */
162
    protected function parseRangeHeader(Request $request)
163
    {
164
        $range = $request->getHeader('range', null);
165
        if (is_string($range)) {
166
            $range = trim($range);
167
            if (strpos($range, 'bytes=') !== 0) {
168
                return false;
169
            }
170
            $rangesFrom = substr($range, strlen('bytes='));
171
            return explode(',', $rangesFrom);
172
        }
173
        return null;
174
    }
175
176
    /**
177
     * 处理范围
178
     *
179
     * @param array $ranges
180
     * @return array|bool
181
     */
182
    protected function parseRanges(array $ranges)
183
    {
184
        $range = [];
185
        foreach ($ranges as $value) {
186
            if (($r = $this->parseRange($value)) !== null) {
187
                $range[] = $r;
188
            } else {
189
                return false;
190
            }
191
        }
192
        return $range;
193
    }
194
195
    /**
196
     * 处理Range
197
     *
198
     * @param string $range
199
     * @return array
200
     */
201
    protected function parseRange(string $range): ?array
202
    {
203
        $range = trim($range);
204
        if (strrpos($range, '-') === strlen($range) - 1) {
205
            return [
206
                'start' => intval(rtrim($range, '-')),
207
                'end' => $this->file->getSize() - 1,
208
            ];
209
        } elseif (strpos($range, '-') !== false) {
210
            list($start, $end) = explode('-', $range, 2);
211
            $length = intval($end - $start);
212
            if ($length <= 0) {
213
                return ['start' => intval($start), 'end' => $this->file->getSize() - 1];
214
            }
215
            return ['start' => intval($start), 'end' => intval($end)];
216
        }
217
        return null;
218
    }
219
}
220