1 | <?php |
||
2 | /** |
||
3 | * Class LocalFile |
||
4 | * |
||
5 | * @link https://www.icy2003.com/ |
||
6 | * @author icy2003 <[email protected]> |
||
7 | * @copyright Copyright (c) 2019, icy2003 |
||
8 | */ |
||
9 | namespace icy2003\php\icomponents\file; |
||
10 | |||
11 | use Exception; |
||
12 | use icy2003\php\C; |
||
13 | use icy2003\php\I; |
||
14 | use icy2003\php\icomponents\file\FileInterface; |
||
15 | use icy2003\php\ihelpers\Arrays; |
||
16 | use icy2003\php\ihelpers\Console; |
||
17 | use icy2003\php\ihelpers\Header; |
||
18 | use icy2003\php\ihelpers\Http; |
||
19 | use icy2003\php\ihelpers\Request; |
||
20 | use icy2003\php\ihelpers\Strings; |
||
21 | |||
22 | /** |
||
23 | * 本地文件 |
||
24 | * |
||
25 | * - 支持本地文件操作 |
||
26 | * - 支持网络文件部分属性:文件是否存在、文件大小 |
||
27 | */ |
||
28 | class LocalFile extends Base implements FileInterface |
||
29 | { |
||
30 | |||
31 | /** |
||
32 | * 配置 |
||
33 | * |
||
34 | * @var array |
||
35 | */ |
||
36 | protected $_c = [ |
||
37 | 'loader' => 'curl', |
||
38 | 'locale' => 'zh_CN.UTF-8', |
||
39 | 'buffer' => 4096, |
||
40 | 'mode' => 'rb', |
||
41 | 'rtrim' => true, |
||
42 | ]; |
||
43 | |||
44 | /** |
||
45 | * 文件属性 |
||
46 | * |
||
47 | * - 文件名为键,属性为值 |
||
48 | * |
||
49 | * @var array |
||
50 | */ |
||
51 | protected $_attributes = []; |
||
52 | |||
53 | /** |
||
54 | * 初始化 |
||
55 | * |
||
56 | * @param array $options 配置 |
||
57 | * - locale:地区,默认 zh_CN.UTF-8 |
||
58 | * - buffer:以字节方式读写时每段的字节长度,默认为 4096,即 4kb |
||
59 | * - mode:指定了所要求到该流的访问类型,默认 rb @link https://www.php.net/manual/zh/function.fopen.php |
||
60 | * - rtrim:在遍历行时是否去除行尾空白,默认 true,即去除 |
||
61 | * - loader:读取远程资源时用的方法,默认为 curl(当其他方法无法读取时也会设置为 curl),支持值:curl、fopen、fsockopen |
||
62 | * - curl:使用 curl 获取远程文件信息 |
||
63 | * - fopen:需要手动开启 allow_url_fopen 才能使用,不建议开启 |
||
64 | * - fsockopen:使用 fsockopen 发送头获取信息 |
||
65 | * @return void |
||
66 | 35 | */ |
|
67 | public function __construct($options = []) |
||
68 | 35 | { |
|
69 | 35 | $this->_c = Arrays::merge($this->_c, $options); |
|
70 | 35 | setlocale(LC_ALL, (string) I::get($this->_c, 'locale', 'zh_CN.UTF-8')); |
|
71 | 35 | clearstatcache(); |
|
72 | } |
||
73 | |||
74 | /** |
||
75 | * 获取 Hash 值 |
||
76 | * |
||
77 | * @param string $fileName |
||
78 | * |
||
79 | * @return string |
||
80 | 17 | */ |
|
81 | private function __hash($fileName) |
||
82 | 17 | { |
|
83 | return md5($fileName); |
||
84 | } |
||
85 | |||
86 | /** |
||
87 | * 以别名返回路径 |
||
88 | * |
||
89 | * @param string $file |
||
90 | * |
||
91 | * @return string |
||
92 | 34 | */ |
|
93 | private function __file($file) |
||
94 | 34 | { |
|
95 | return (string) I::getAlias($file); |
||
96 | } |
||
97 | |||
98 | /** |
||
99 | * 加载一个文件,本地(支持别名)或网络文件 |
||
100 | * |
||
101 | * @param string $fileName |
||
102 | * |
||
103 | * @return static |
||
104 | 17 | */ |
|
105 | protected function _load($fileName) |
||
106 | 17 | { |
|
107 | 17 | $fileName = $this->getRealpath($fileName); |
|
108 | 17 | $hashName = $this->__hash($fileName); |
|
109 | 17 | $this->_attributes[$hashName] = I::get($this->_attributes, $hashName, [ |
|
110 | 'file' => $fileName, |
||
111 | 'isCached' => false, |
||
112 | 'isLocal' => true, |
||
113 | // 以下属性需要重新设置 |
||
114 | 17 | 'isExists' => false, |
|
115 | 'fileSize' => 0, |
||
116 | 'spl' => null, |
||
117 | 'splInfo' => null, |
||
118 | 17 | ]); |
|
119 | null === $this->_attributes[$hashName]['splInfo'] && $this->_attributes[$hashName]['splInfo'] = new \SplFileInfo($fileName); |
||
120 | 17 | try { |
|
121 | 17 | $splInfo = $this->_attributes[$hashName]['splInfo']; |
|
122 | 17 | if (true !== $splInfo->isDir()) { |
|
123 | null === $this->_attributes[$hashName]['spl'] && $this->_attributes[$hashName]['spl'] = new \SplFileObject($fileName, $this->_c['mode']); |
||
124 | 7 | } |
|
125 | } catch (Exception $e) { |
||
126 | 7 | // 报错了也得继续跑,如果跑完一次 spl 和 splInfo 属性还是 null,在调用它们的时候自然会报错 |
|
127 | $this->_c['error'] = $e->getMessage(); |
||
128 | 7 | // 尝试用 curl 获取 |
|
129 | $this->_c['loader'] = 'curl'; |
||
130 | } |
||
131 | 17 | // 如果已经被缓存了,直接返回 |
|
132 | 4 | if (true === $this->_attributes[$hashName]['isCached']) { |
|
133 | return $this; |
||
134 | } |
||
135 | 17 | // 加上缓存标记 |
|
136 | 17 | $this->_attributes[$hashName]['isCached'] = true; |
|
137 | 2 | if (preg_match('/^https?:\/\//', $fileName)) { |
|
138 | $this->_attributes[$hashName]['isLocal'] = false; |
||
139 | 2 | // 加载网络文件 |
|
140 | 2 | if ('curl' === $this->_c['loader'] && extension_loaded('curl')) { |
|
141 | 2 | $curl = curl_init($fileName); |
|
142 | 2 | if (is_resource($curl)) { |
|
143 | 2 | curl_setopt($curl, CURLOPT_NOBODY, true); |
|
144 | 2 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); |
|
145 | curl_setopt($curl, CURLOPT_HEADER, true); |
||
146 | // 公用名(Common Name)一般来讲就是填写你将要申请SSL证书的域名 (domain)或子域名(sub domain) |
||
147 | // - 设置为 1 是检查服务器SSL证书中是否存在一个公用名(common name) |
||
148 | // - 设置成 2,会检查公用名是否存在,并且是否与提供的主机名匹配 |
||
149 | 2 | // - 0 为不检查名称。 在生产环境中,这个值应该是 2(默认值) |
|
150 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); |
||
151 | 2 | // 禁止 cURL 验证对等证书(peer's certificate)。要验证的交换证书可以在 CURLOPT_CAINFO 选项中设置 |
|
152 | 2 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); |
|
153 | 2 | $result = curl_exec($curl); |
|
154 | 2 | if ($result && $info = curl_getinfo($curl)) { |
|
155 | 2 | if (200 == $info['http_code']) { |
|
156 | 2 | $this->_attributes[$hashName]['isExists'] = true; |
|
157 | $this->_attributes[$hashName]['fileSize'] = (int) $info['download_content_length']; |
||
158 | } |
||
159 | 2 | } |
|
160 | curl_close($curl); |
||
161 | 2 | } |
|
162 | return $this; |
||
163 | 1 | } |
|
164 | 1 | if ('fsockopen' === $this->_c['loader']) { |
|
165 | 1 | $url = parse_url($fileName); |
|
166 | 1 | $host = $url['host']; |
|
167 | 1 | $path = (string) I::get($url, 'path', '/'); |
|
168 | 1 | $port = (int) I::get($url, 'port', 80); |
|
169 | 1 | $fp = fsockopen($host, $port); |
|
170 | 1 | if (is_resource($fp)) { |
|
171 | 1 | fputs($fp, "GET {$path} HTTP/1.1\r\n"); |
|
172 | 1 | fputs($fp, "Host: {$host}:{$port}\r\n"); |
|
173 | 1 | fputs($fp, "Connection: Close\r\n\r\n"); |
|
174 | 1 | while (!feof($fp)) { |
|
175 | 1 | $line = fgets($fp); |
|
176 | 1 | preg_match('/HTTP.*(\s\d{3}\s)/', $line, $arr) && $this->_attributes[$hashName]['isExists'] = true; |
|
177 | preg_match('/Content-Length:(.*)/si', $line, $arr) && $this->_attributes[$hashName]['fileSize'] = (int) trim($arr[1]); |
||
178 | 1 | } |
|
179 | fclose($fp); |
||
180 | 1 | } |
|
181 | return $this; |
||
182 | 1 | } |
|
183 | 1 | if ('fopen' === $this->_c['loader'] && (bool) ini_get('allow_url_fopen')) { |
|
184 | 1 | $headArray = (array) get_headers($fileName, 1); |
|
185 | 1 | if (preg_match('/200/', $headArray[0])) { |
|
186 | 1 | $this->_attributes[$hashName]['isExists'] = true; |
|
187 | $this->_attributes[$hashName]['fileSize'] = (int) $headArray['Content-Length']; |
||
188 | 1 | } |
|
189 | return $this; |
||
190 | } |
||
191 | 17 | } else { |
|
192 | 17 | $this->_attributes[$hashName]['isLocal'] = true; |
|
193 | 17 | $this->_attributes[$hashName]['isExists'] = file_exists($fileName); |
|
194 | 15 | if ($this->_attributes[$hashName]['isExists']) { |
|
195 | $this->_attributes[$hashName]['fileSize'] = filesize($fileName); |
||
196 | 17 | } |
|
197 | $this->chmod($fileName, 0777, FileConstants::RECURSIVE_DISABLED); |
||
198 | 17 | } |
|
199 | return $this; |
||
200 | } |
||
201 | |||
202 | /** |
||
203 | * 获取文件的属性 |
||
204 | * |
||
205 | * @param string $fileName |
||
206 | * @param string $name |
||
207 | * |
||
208 | * @return mixed |
||
209 | 17 | */ |
|
210 | public function attribute($fileName, $name) |
||
211 | 17 | { |
|
212 | 17 | $this->_load($fileName); |
|
213 | return I::get($this->_attributes, $this->__hash($this->getRealpath($fileName)) . '.' . $name); |
||
214 | } |
||
215 | |||
216 | /** |
||
217 | * 获取文件对象 |
||
218 | * |
||
219 | * @param string $fileName |
||
220 | * @param string $mode 读写的模式,默认 rb |
||
221 | * |
||
222 | * @return \SplFileObject|null |
||
223 | * @throws Exception |
||
224 | 6 | */ |
|
225 | public function spl($fileName, $mode = 'rb') |
||
226 | 6 | { |
|
227 | 6 | $this->_c['mode'] = $mode; |
|
228 | 6 | $spl = $this->attribute($fileName, 'spl'); |
|
229 | 4 | C::assertTrue($spl instanceof \SplFileObject, '文件打开失败:' . $fileName); |
|
230 | return $spl; |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||
231 | } |
||
232 | |||
233 | /** |
||
234 | * 获取文件信息对象 |
||
235 | * |
||
236 | * @param string $fileName |
||
237 | * |
||
238 | * @return \SplFileInfo |
||
239 | 1 | */ |
|
240 | public function splInfo($fileName) |
||
241 | 1 | { |
|
242 | 1 | $splInfo = $this->attribute($fileName, 'splInfo'); |
|
243 | return $splInfo; |
||
0 ignored issues
–
show
|
|||
244 | } |
||
245 | |||
246 | /** |
||
247 | * 遍历行的生成器 |
||
248 | * |
||
249 | * - 自动关闭后再次调用需要重新读取文件,不建议自动关闭 |
||
250 | * |
||
251 | * @param string $fileName |
||
252 | * @param boolean $autoClose 是否自动关闭文件,默认 false |
||
253 | * |
||
254 | * @return \Generator |
||
255 | 2 | */ |
|
256 | public function linesGenerator($fileName, $autoClose = false) |
||
257 | { |
||
258 | 2 | try { |
|
259 | 2 | $spl = $this->spl($fileName, 'r'); |
|
260 | 2 | while (false === $spl->eof() && ($line = $spl->fgets())) { |
|
261 | 2 | true === $this->_c['rtrim'] && $line = rtrim($line); |
|
262 | yield $line; |
||
263 | 2 | } |
|
264 | 2 | } finally { |
|
265 | true === $autoClose && $this->close($fileName); |
||
266 | 2 | } |
|
267 | } |
||
268 | |||
269 | /** |
||
270 | * 返回文本的某行 |
||
271 | * |
||
272 | * - 每取一行,文件指针会回到初始位置,如果需要大量的行,请直接使用 linesGenerator |
||
273 | * - 自动关闭后再次调用需要重新读取文件,不建议自动关闭 |
||
274 | * |
||
275 | * @param string $fileName |
||
276 | * @param integer $lineNumber 行号 |
||
277 | * @param boolean $autoClose 是否自动关闭文件,默认 false |
||
278 | * |
||
279 | * @return string|null |
||
280 | 1 | */ |
|
281 | public function line($fileName, $lineNumber = 0, $autoClose = false) |
||
282 | 1 | { |
|
283 | 1 | $spl = $this->spl($fileName, 'r'); |
|
284 | 1 | $lineNumber = (int) $lineNumber; |
|
285 | 1 | foreach ($this->linesGenerator($fileName, $autoClose) as $k => $line) { |
|
286 | 1 | if ($k === $lineNumber) { |
|
287 | 1 | $spl->rewind(); |
|
288 | return $line; |
||
289 | } |
||
290 | 1 | } |
|
291 | return null; |
||
292 | } |
||
293 | |||
294 | /** |
||
295 | * 遍历字节的生成器 |
||
296 | * |
||
297 | * - 自动关闭后再次调用需要重新读取文件,不建议自动关闭 |
||
298 | * |
||
299 | * @param string $fileName |
||
300 | * @param boolean $autoClose 是否自动关闭文件,默认 false |
||
301 | * @param integer|null $buffer 每次读取的字节数,默认 null,值等于初始化时的 buffer 选项 |
||
302 | * |
||
303 | * @return \Generator |
||
304 | 1 | */ |
|
305 | public function dataGenerator($fileName, $autoClose = false, $buffer = null) |
||
306 | 1 | { |
|
307 | 1 | $bufferSize = 0; |
|
308 | null === $buffer && $buffer = $this->_c['buffer']; |
||
309 | 1 | try { |
|
310 | 1 | $spl = $this->spl($fileName, 'rb'); |
|
311 | 1 | $size = $this->getFilesize($fileName); |
|
312 | 1 | while (!$spl->eof() && $size > $bufferSize) { |
|
313 | 1 | $bufferSize += $buffer; |
|
314 | yield $spl->fread($bufferSize); |
||
315 | 1 | } |
|
316 | 1 | } finally { |
|
317 | true === $autoClose && $this->close($fileName); |
||
318 | 1 | } |
|
319 | } |
||
320 | |||
321 | /** |
||
322 | * @ignore |
||
323 | 1 | */ |
|
324 | public function getATime($fileName) |
||
325 | 1 | { |
|
326 | return fileatime($this->__file($fileName)); |
||
327 | } |
||
328 | |||
329 | /** |
||
330 | * @ignore |
||
331 | 2 | */ |
|
332 | public function getBasename($file, $suffix = null) |
||
333 | 2 | { |
|
334 | return parent::getBasename($this->__file($file), $suffix); |
||
335 | } |
||
336 | |||
337 | /** |
||
338 | * @ignore |
||
339 | 1 | */ |
|
340 | public function getCTime($fileName) |
||
341 | 1 | { |
|
342 | return filectime($this->__file($fileName)); |
||
343 | } |
||
344 | |||
345 | /** |
||
346 | * @ignore |
||
347 | 1 | */ |
|
348 | public function getExtension($fileName) |
||
349 | 1 | { |
|
350 | return pathinfo($this->__file($fileName), PATHINFO_EXTENSION); |
||
0 ignored issues
–
show
|
|||
351 | } |
||
352 | |||
353 | /** |
||
354 | * @ignore |
||
355 | 1 | */ |
|
356 | public function getFilename($fileName) |
||
357 | 1 | { |
|
358 | return pathinfo($this->__file($fileName), PATHINFO_FILENAME); |
||
0 ignored issues
–
show
|
|||
359 | } |
||
360 | |||
361 | /** |
||
362 | * @ignore |
||
363 | 1 | */ |
|
364 | public function getMtime($fileName) |
||
365 | 1 | { |
|
366 | return filemtime($this->__file($fileName)); |
||
367 | } |
||
368 | |||
369 | /** |
||
370 | * @ignore |
||
371 | 5 | */ |
|
372 | public function getDirname($path) |
||
373 | 5 | { |
|
374 | return parent::getDirname($this->__file($path)); |
||
375 | } |
||
376 | |||
377 | /** |
||
378 | * @ignore |
||
379 | 1 | */ |
|
380 | public function getPerms($path) |
||
381 | 1 | { |
|
382 | return fileperms($this->__file($path)); |
||
383 | } |
||
384 | |||
385 | /** |
||
386 | * @ignore |
||
387 | 3 | */ |
|
388 | public function getFilesize($fileName) |
||
389 | 3 | { |
|
390 | return (int) $this->attribute($fileName, 'fileSize'); |
||
391 | } |
||
392 | |||
393 | /** |
||
394 | * @ignore |
||
395 | 1 | */ |
|
396 | public function getType($path) |
||
397 | 1 | { |
|
398 | return filetype($this->__file($path)); |
||
399 | } |
||
400 | |||
401 | /** |
||
402 | * @ignore |
||
403 | 19 | */ |
|
404 | public function isDir($dir) |
||
405 | 19 | { |
|
406 | return is_dir($this->__file($dir)); |
||
407 | } |
||
408 | |||
409 | /** |
||
410 | * @ignore |
||
411 | 1 | */ |
|
412 | public function isDot($dir) |
||
413 | 1 | { |
|
414 | return in_array($this->getBasename($dir), ['.', '..']); |
||
415 | } |
||
416 | |||
417 | /** |
||
418 | * @ignore |
||
419 | 10 | */ |
|
420 | public function isFile($file) |
||
421 | 10 | { |
|
422 | 10 | $isLocal = $this->attribute($file, 'isLocal'); |
|
423 | 10 | if (true === $isLocal) { |
|
424 | return is_file($this->__file($file)); |
||
425 | 1 | } else { |
|
426 | return (bool) $this->attribute($file, 'isExists'); |
||
427 | } |
||
428 | } |
||
429 | |||
430 | /** |
||
431 | * @ignore |
||
432 | 1 | */ |
|
433 | public function isLink($link) |
||
434 | 1 | { |
|
435 | return is_link($this->__file($link)); |
||
436 | } |
||
437 | |||
438 | /** |
||
439 | * @ignore |
||
440 | 1 | */ |
|
441 | public function isReadable($path) |
||
442 | 1 | { |
|
443 | return is_readable($this->__file($path)); |
||
444 | } |
||
445 | |||
446 | /** |
||
447 | * @ignore |
||
448 | 1 | */ |
|
449 | public function isWritable($path) |
||
450 | 1 | { |
|
451 | return is_writable($this->__file($path)); |
||
452 | } |
||
453 | |||
454 | /** |
||
455 | * @ignore |
||
456 | */ |
||
457 | public function getCommandResult($command) |
||
458 | { |
||
459 | return Console::exec($command); |
||
460 | } |
||
461 | |||
462 | /** |
||
463 | * @ignore |
||
464 | 18 | */ |
|
465 | public function getRealpath($path) |
||
466 | 18 | { |
|
467 | 18 | $path = $this->__file($path); |
|
468 | 18 | $realPath = realpath($path); |
|
469 | 18 | false === $realPath && $realPath = parent::getRealpath($path); |
|
470 | return Strings::replace($realPath, ["\\" => '/']); |
||
471 | } |
||
472 | |||
473 | /** |
||
474 | * @ignore |
||
475 | 3 | */ |
|
476 | public function getLists($dir = null, $flags = FileConstants::COMPLETE_PATH | FileConstants::RECURSIVE_DISABLED) |
||
477 | 3 | { |
|
478 | 3 | null === $dir && $dir = $this->getRealpath('./'); |
|
479 | 3 | $dir = rtrim($this->__file($dir), '/') . '/'; |
|
480 | 3 | $iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS); |
|
481 | 3 | if (I::hasFlag($flags, FileConstants::RECURSIVE)) { |
|
482 | $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST); |
||
483 | 3 | } |
|
484 | $files = []; |
||
485 | /** |
||
486 | * @var \RecursiveDirectoryIterator $file |
||
487 | 3 | */ |
|
488 | 3 | foreach ($iterator as $file) { |
|
489 | 3 | if (I::hasFlag($flags, FileConstants::COMPLETE_PATH)) { |
|
490 | $files[] = $file->getPathname(); |
||
491 | 2 | } else { |
|
492 | $files[] = $file->getFilename(); |
||
493 | } |
||
494 | 3 | } |
|
495 | return $files; |
||
496 | } |
||
497 | |||
498 | /** |
||
499 | * @ignore |
||
500 | 2 | */ |
|
501 | public function getFileContent($file) |
||
502 | 2 | { |
|
503 | 2 | if ($this->isFile($file)) { |
|
504 | return file_get_contents($this->__file($file)); |
||
505 | } |
||
506 | return false; |
||
507 | } |
||
508 | |||
509 | /** |
||
510 | * @ignore |
||
511 | 1 | */ |
|
512 | public function putFileContent($file, $string, $mode = 0777) |
||
513 | 1 | { |
|
514 | 1 | $this->createDir($this->getDirname($file), $mode); |
|
515 | 1 | $isCreated = false !== file_put_contents($this->__file($file), $string); |
|
516 | 1 | $this->chmod($file, $mode, FileConstants::RECURSIVE_DISABLED); |
|
517 | return $isCreated; |
||
518 | } |
||
519 | |||
520 | /** |
||
521 | * @ignore |
||
522 | 5 | */ |
|
523 | public function deleteFile($file) |
||
524 | 5 | { |
|
525 | 5 | if ($this->isFile($file)) { |
|
526 | 5 | $this->close($file); |
|
527 | return unlink($this->__file($file)); |
||
528 | 1 | } |
|
529 | return true; |
||
530 | } |
||
531 | |||
532 | /** |
||
533 | * @ignore |
||
534 | 1 | */ |
|
535 | public function uploadFile($fileMap, $overwrite = true) |
||
536 | 1 | { |
|
537 | return false; |
||
538 | } |
||
539 | |||
540 | /** |
||
541 | * 从远程下载文件到本地 |
||
542 | * |
||
543 | * @ignore |
||
544 | */ |
||
545 | public function downloadFile($fileMap, $overwrite = true) |
||
546 | 1 | { |
|
547 | set_time_limit(0); |
||
548 | 1 | list($fromFile, $toFile) = $this->fileMap($fileMap); |
|
549 | 1 | $this->createDir($this->getDirname($toFile)); |
|
550 | 1 | if ($this->isFile($toFile) && false === $overwrite) { |
|
551 | return true; |
||
552 | } |
||
553 | $content = Http::get($fromFile); |
||
554 | return $this->putFileContent($toFile, $content); |
||
555 | } |
||
556 | |||
557 | /** |
||
558 | * download() 配置名:ip |
||
559 | */ |
||
560 | const C_DOWNLOAD_IP = 'ip'; |
||
561 | /** |
||
562 | * download() 配置名:speed |
||
563 | */ |
||
564 | const C_DOWNLOAD_SPEED = 'speed'; |
||
565 | /** |
||
566 | * download() 配置名:xSendFile |
||
567 | */ |
||
568 | const C_DOWNLOAD_X_SEND_FILE = 'xSendFile'; |
||
569 | /** |
||
570 | * download() 配置名:xSendFileRoot |
||
571 | */ |
||
572 | const C_DOWNLOAD_X_SEND_FILE_ROOT = 'xSendFileRoot'; |
||
573 | |||
574 | /** |
||
575 | * 服务端给客户端提供下载请求 |
||
576 | * |
||
577 | * @param string|array $fileName self::fileMap() |
||
578 | * @param null|array $config 配置项 |
||
579 | * - ip:限特定 IP 访问,数组或逗号字符串,默认为 *,即对所有 IP 不限制 |
||
580 | * - speed:限速,默认不限速(读取速度为1024 * [buffer]),单位 kb/s |
||
581 | * - xSendFile:是否使用 X-Sendfile 进行下载,默认 false,即不使用。X-Sendfile 缓解了 PHP 的压力,但同时 PHP 将失去对资源的控制权,因为 PHP 并不知道资源发完了没 |
||
582 | * - xSendFileRoot:文件根路径,默认为 /protected/。此时 Nginx 可作如下配置,更多 @link https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/ |
||
583 | * ```nginx.conf |
||
584 | * location /protected/ { |
||
585 | * internal; # 表示这个路径只能在 Nginx 内部访问,不能用浏览器直接访问防止未授权的下载 |
||
586 | * alias /usr/share/nginx/html/protected/; # 别名 |
||
587 | * # root /usr/share/nginx/html; # 根目录 |
||
588 | * } |
||
589 | * ``` |
||
590 | * @param callback $callback 下载完成后的回调,参数列表:文件属性数组 |
||
591 | * |
||
592 | * @return void |
||
593 | * @info 此函数之后不得有任何输出 |
||
594 | * @throws Exception |
||
595 | */ |
||
596 | public function download($fileName, $config = null, $callback = null) |
||
597 | { |
||
598 | Header::xPoweredBy(); |
||
599 | set_time_limit(0); |
||
600 | list($originName, $downloadName) = $this->fileMap($fileName); |
||
601 | $originName = $this->__file($originName); |
||
602 | try { |
||
603 | $ip = (string) I::get($config, self::C_DOWNLOAD_IP, '*'); |
||
604 | if ('*' !== $ip) { |
||
605 | C::assertTrue(Arrays::in((new Request())->getUserIP(), Strings::toArray($ip)), 'http/1.1 403.6 此 IP 禁止访问'); |
||
606 | } |
||
607 | if ($this->isFile($originName)) { |
||
608 | $fileSize = $this->getFilesize($originName); |
||
609 | header('Content-type:application/octet-stream'); |
||
610 | header('Accept-Ranges:bytes'); |
||
611 | header('Content-Length:' . $fileSize); |
||
612 | header('Content-Disposition: attachment; filename=' . $downloadName); |
||
613 | $speed = (int) I::get($config, self::C_DOWNLOAD_SPEED, 0); |
||
614 | $xSendFile = I::get($config, self::C_DOWNLOAD_X_SEND_FILE, false); |
||
615 | $xSendFileRoot = (string) I::get($config, self::C_DOWNLOAD_X_SEND_FILE_ROOT, '/protected/'); |
||
616 | if (true === $xSendFile) { |
||
617 | $path = rtrim($xSendFileRoot, '/') . '/' . $this->getBasename($originName); |
||
618 | header('X-Accel-Redirect: ' . $path); // Nginx、Cherokee 实现了该头 |
||
619 | header('X-Sendfile: ' . $path); // Apache、Lighttpd v1.5、Cherokee 实现了该头 |
||
620 | header('X-LIGHTTPD-send-file: ' . $path); // Lighttpd v1.4 实现了该头 |
||
621 | if ($speed) { |
||
622 | header('X-Accel-Limit-Rate: ' . $speed); // 单位 kb/s |
||
623 | } |
||
624 | } else { |
||
625 | flush(); |
||
626 | foreach ($this->dataGenerator($originName, true, ($speed ? $speed : $this->_c['buffer'] * 1024)) as $data) { |
||
627 | echo $data; |
||
628 | flush(); |
||
629 | $speed > 0 && sleep(1); |
||
630 | } |
||
631 | } |
||
632 | } |
||
633 | } catch (Exception $e) { |
||
634 | header($e->getMessage()); |
||
635 | } finally { |
||
636 | I::call($callback, [$this->_attributes]); |
||
637 | // 必须要终止掉,防止发送其他数据导致错误 |
||
638 | die; |
||
0 ignored issues
–
show
|
|||
639 | } |
||
640 | } |
||
641 | |||
642 | /** |
||
643 | * @ignore |
||
644 | */ |
||
645 | public function chown($file, $user, $flags = FileConstants::RECURSIVE_DISABLED) |
||
646 | { |
||
647 | $file = $this->__file($file); |
||
648 | if ($this->isDir($file) && I::hasFlag($flags, FileConstants::RECURSIVE)) { |
||
649 | $files = $this->getLists($file, FileConstants::COMPLETE_PATH | FileConstants::RECURSIVE); |
||
650 | foreach ($files as $subFile) { |
||
651 | /** @scrutinizer ignore-unhandled */@chown($subFile, $user); |
||
652 | } |
||
653 | } elseif ($this->isFile($file)) { |
||
654 | return /** @scrutinizer ignore-unhandled */@chown($file, $user); |
||
655 | } else { |
||
656 | return false; |
||
657 | } |
||
658 | } |
||
659 | |||
660 | /** |
||
661 | * @ignore |
||
662 | */ |
||
663 | public function chgrp($file, $group, $flags = FileConstants::RECURSIVE_DISABLED) |
||
664 | { |
||
665 | $file = $this->__file($file); |
||
666 | if ($this->isDir($file) && I::hasFlag($flags, FileConstants::RECURSIVE)) { |
||
667 | $files = $this->getLists($file, FileConstants::COMPLETE_PATH | FileConstants::RECURSIVE); |
||
668 | foreach ($files as $subFile) { |
||
669 | /** @scrutinizer ignore-unhandled */@chgrp($subFile, $group); |
||
670 | } |
||
671 | } elseif ($this->isFile($file)) { |
||
672 | return /** @scrutinizer ignore-unhandled */@chgrp($file, $group); |
||
673 | } else { |
||
674 | return false; |
||
675 | } |
||
676 | } |
||
677 | |||
678 | /** |
||
679 | * @ignore |
||
680 | */ |
||
681 | public function chmod($file, $mode = 0777, $flags = FileConstants::RECURSIVE_DISABLED) |
||
682 | { |
||
683 | $file = $this->__file($file); |
||
684 | if ($this->isDir($file) && I::hasFlag($flags, FileConstants::RECURSIVE)) { |
||
685 | $files = $this->getLists($file, FileConstants::COMPLETE_PATH | FileConstants::RECURSIVE); |
||
686 | foreach ($files as $subFile) { |
||
687 | /** @scrutinizer ignore-unhandled */@chmod($subFile, $mode); |
||
688 | } |
||
689 | } else { |
||
690 | return /** @scrutinizer ignore-unhandled */@chmod($file, $mode); |
||
691 | } |
||
692 | } |
||
693 | 18 | ||
694 | /** |
||
695 | 18 | * @ignore |
|
696 | 18 | */ |
|
697 | 1 | public function symlink($from, $to) |
|
698 | 1 | { |
|
699 | 1 | $from = $this->__file($from); |
|
700 | $to = $this->__file($to); |
||
701 | return @symlink($from, $to); |
||
702 | 18 | } |
|
703 | |||
704 | 1 | /** |
|
705 | * @ignore |
||
706 | */ |
||
707 | public function close($fileName = null) |
||
708 | { |
||
709 | if (is_string($fileName)) { |
||
710 | $fileName = [$this->__hash($this->getRealpath($fileName))]; |
||
711 | } elseif (is_array($fileName)) { |
||
712 | foreach ($fileName as $k => $name) { |
||
713 | $fileName[$k] = $this->__hash($this->getRealpath($name)); |
||
714 | } |
||
715 | } |
||
716 | foreach ($this->_attributes as $hashName => /** @scrutinizer ignore-unused */$attribute) { |
||
717 | if (null === $fileName || is_array($fileName) && in_array($hashName, $fileName)) { |
||
718 | unset($this->_attributes[$hashName]); |
||
719 | 35 | } |
|
720 | } |
||
721 | 35 | return true; |
|
722 | 8 | } |
|
723 | 35 | ||
724 | /** |
||
725 | * @ignore |
||
726 | */ |
||
727 | protected function _copy($fromFile, $toFile) |
||
728 | 35 | { |
|
729 | 17 | $fromFile = $this->__file($fromFile); |
|
730 | 17 | $toFile = $this->__file($toFile); |
|
731 | return copy($fromFile, $toFile); |
||
732 | } |
||
733 | 35 | ||
734 | /** |
||
735 | * @ignore |
||
736 | */ |
||
737 | protected function _move($fromFile, $toFile) |
||
738 | { |
||
739 | 3 | $fromFile = $this->__file($fromFile); |
|
740 | $toFile = $this->__file($toFile); |
||
741 | 3 | return rename($fromFile, $toFile); |
|
742 | 3 | } |
|
743 | 3 | ||
744 | /** |
||
745 | * @ignore |
||
746 | */ |
||
747 | protected function _mkdir($dir, $mode = 0777) |
||
748 | { |
||
749 | 1 | $dir = $this->__file($dir); |
|
750 | return mkdir($dir, $mode); |
||
751 | 1 | } |
|
752 | 1 | ||
753 | 1 | /** |
|
754 | * @ignore |
||
755 | */ |
||
756 | protected function _rmdir($dir) |
||
757 | { |
||
758 | $dir = $this->__file($dir); |
||
759 | 1 | return rmdir($dir); |
|
760 | } |
||
761 | 1 | ||
762 | } |
||
763 |