icy2003 /
php
| 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
Loading history...
|
|||
| 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 |