/** * 通过HTTP发送文件,达到下载文件的效果 * * // 下载一个已经存在的文件 * $response->sendFile('media/packages/package.zip'); * * // 将返回内容当做是下载处理: * $response->body = $content; * $response->sendFile(true, $filename); * * @param string $filename 文件名、文件路径,如果设置为`true`则返回当前的response内容 * @param string $download 下载文件名,默认为当前要处理的文件名 * @param array $options 其他选项 * @throws BaseException */ public function sendFile($filename, $download = null, array $options = null) { // 强制指定了mime if (!empty($options['mime_type'])) { $mime = $options['mime_type']; } // 返回当前的响应内容,作为本次下载 if (true === $filename) { if (empty($download)) { throw new BaseException('Download name must be provided for streaming files'); } $options['delete'] = false; if (!isset($mime)) { // 根据扩展名,获取指定的mime类型 $mime = Mime::getMimeFromExtension(strtolower(pathinfo($download, PATHINFO_EXTENSION))); } // 将响应内容保存到临时文件 $fileData = (string) $this->message->body; $size = strlen($fileData); $file = tmpfile(); fwrite($file, $fileData); unset($fileData); } else { $filename = realpath($filename); if (empty($download)) { $download = pathinfo($filename, PATHINFO_BASENAME); } $size = filesize($filename); if (!isset($mime)) { $mime = Mime::getMimeFromExtension(pathinfo($download, PATHINFO_EXTENSION)); } $file = fopen($filename, 'rb'); } if (!is_resource($file)) { throw new BaseException('Could not read file to send: :file', [':file' => $download]); } // inline和attachment的区别,主要在于浏览器遇到inline类型的下载时,会尝试直接在浏览器打开,而attachment则不会 $disposition = Arr::get($options, 'inline') ? 'inline' : 'attachment'; $temp = $this->_calculateByteRange($size); $start = array_shift($temp); $end = array_shift($temp); if (!empty($options['resumable'])) { if ($start > 0 || $end < $size - 1) { // Partial Content $this->status = 206; } $this->headers('content-range', 'bytes ' . $start . '-' . $end . '/' . $size); $this->headers('accept-ranges', 'bytes'); } $this->headers('content-disposition', $disposition . '; filename="' . $download . '"'); $this->headers('content-type', $mime); $this->headers('content-length', (string) ($end - $start + 1)); $this->sendHeaders(); // @notice 此方法在cli环境下可能无效 while (ob_get_level()) { ob_end_flush(); } ignore_user_abort(true); $prevTimeLimit = ini_get('max_execution_time'); @set_time_limit(0); // 16K $block = 1024 * 16; fseek($file, $start); while (!feof($file) && ($pos = ftell($file)) <= $end) { if (connection_aborted()) { break; } if ($pos + $block > $end) { $block = $end - $pos + 1; } echo fread($file, $block); flush(); } fclose($file); if (!empty($options['delete'])) { try { unlink($filename); } catch (Exception $e) { Base::getLog()->error($e->getMessage(), ['code' => $e->getCode()]); } } // 停止执行 @set_time_limit($prevTimeLimit); Base::getHttp()->end(); }
/** * 覆盖原workerman流程,实现更多功能 * 当接收到完整的http请求后的处理逻辑 * * 1、如果请求的是以php为后缀的文件,则尝试加载 * 2、如果请求的url没有后缀,则尝试加载对应目录的index.php * 3、如果请求的是非php为后缀的文件,尝试读取原始数据并发送 * 4、如果请求的文件不存在,则返回404 * * @param TcpConnection $connection * @param mixed $data * @return mixed */ public function onMessage($connection, $data) { Base::getLog()->debug(__METHOD__ . ' receive http request', ['uri' => $_SERVER['REQUEST_URI'], 'ip' => $connection->getRemoteIp(), 'port' => $connection->getRemotePort(), 'data' => $data]); // 请求的文件 $urlInfo = parse_url($_SERVER['REQUEST_URI']); if (!$urlInfo) { Base::getHttp()->header('HTTP/1.1 400 Bad Request'); Base::getLog()->warning(__METHOD__ . ' receive bad request', ['uri' => $_SERVER['REQUEST_URI'], 'ip' => $connection->getRemoteIp(), 'port' => $connection->getRemotePort()]); return $connection->close($this->error400); } $path = $urlInfo['path']; $pathInfo = pathinfo($path); $extension = isset($pathInfo['extension']) ? $pathInfo['extension'] : ''; if ($extension === '') { $path = ($len = strlen($path)) && $path[$len - 1] === '/' ? $path . $this->indexFile : $path . '/' . $this->indexFile; $extension = 'php'; } $serverName = Arr::get($_SERVER, 'SERVER_NAME'); $rootDir = isset($this->serverRoot[$serverName]) ? $this->serverRoot[$serverName] : current($this->serverRoot); $file = "{$rootDir}/{$path}"; // 对应的php文件不存在,而且支持rewrite if (!is_file($file) && $this->rewrite) { $file = is_string($this->rewrite) ? $rootDir . '/' . $this->rewrite : $rootDir . '/' . $this->indexFile; $extension = 'php'; $_SERVER['PATH_INFO'] = $_SERVER['REQUEST_URI']; } // 请求的文件存在 if (is_file($file)) { Base::getLog()->debug(__METHOD__ . ' request file existed', ['file' => $file, 'extension' => $extension]); // 判断是否是站点目录里的文件 if (!($requestRealPath = realpath($file)) || !($rootDirRealPath = realpath($rootDir)) || 0 !== strpos($requestRealPath, $rootDirRealPath)) { Base::getHttp()->header('HTTP/1.1 400 Bad Request'); Base::getLog()->warning(__METHOD__ . ' receive bad request', ['uri' => $_SERVER['REQUEST_URI'], 'ip' => $connection->getRemoteIp(), 'port' => $connection->getRemotePort()]); return $connection->close('<h1>400 Bad Request</h1>'); } $file = realpath($file); // 如果请求的是php文件 // PHP文件需要include if ($extension === 'php') { Base::getLog()->debug(__METHOD__ . ' handle request', ['uri' => $_SERVER['REQUEST_URI'], 'ip' => $connection->getRemoteIp(), 'port' => $connection->getRemotePort(), 'file' => $file]); Base::getLog()->debug(__METHOD__ . ' clean components - start'); Base::cleanComponents(); Base::getLog()->debug(__METHOD__ . ' clean components - end'); $cwd = getcwd(); chdir($rootDir); ini_set('display_errors', 'off'); // 缓冲输出 ob_start(); // 载入php文件 try { // $_SERVER变量 $_SERVER['HOME'] = $_SERVER['DOCUMENT_ROOT'] = dirname($file); $_SERVER['SCRIPT_FILENAME'] = $file; Base::getLog()->debug(__METHOD__ . ' dispatch client info', ['ip' => $_SERVER['REMOTE_ADDR'], 'port' => $_SERVER['REMOTE_PORT']]); include $file; } catch (Exception $e) { Base::getLog()->error($e->getMessage(), ['code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine()]); // 如果不是exit if ($e->getMessage() != 'jump_exit') { echo $e; } } Patch::applyShutdownFunction(); $content = ob_get_clean(); ini_set('display_errors', 'on'); $result = $connection->close($content); chdir($cwd); return $result; } else { $contentType = Mime::getMimeFromExtension($extension, self::$defaultMimeType); Base::getLog()->debug(__METHOD__ . ' get static file content type', ['extension' => $extension, 'contentType' => $contentType]); Base::getHttp()->header('Content-Type: ' . $contentType); // 获取文件信息 $info = stat($file); $modifiedTime = $info ? date('D, d M Y H:i:s', Arr::get($info, 'mtime')) . ' GMT' : ''; // 如果有$_SERVER['HTTP_IF_MODIFIED_SINCE'] if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $info) { // 文件没有更改则直接304 if ($modifiedTime === $_SERVER['HTTP_IF_MODIFIED_SINCE']) { Base::getLog()->debug(__METHOD__ . ' no modified, return 304'); // 304 Base::getHttp()->header('HTTP/1.1 304 Not Modified'); // 发送给客户端 return $connection->close(''); } } if ($modifiedTime) { Base::getLog()->debug(__METHOD__ . ' set last modified time', ['time' => $modifiedTime]); Base::getHttp()->header("Last-Modified: {$modifiedTime}"); } // 发送给客户端 return $connection->close(file_get_contents($file)); } } else { Base::getLog()->warning(__METHOD__ . ' requested file not found', ['file' => $file]); // 404 Base::getHttp()->header("HTTP/1.1 404 Not Found"); return $connection->close($this->error404); } }
/** * 检测[Mime::getMimeFromExtension] * * @dataProvider dataGetMimeFromExtension * @param string $ext * @param string $expected */ public function testGetMimeFromExtension($ext, $expected) { $this->assertEquals($expected, Mime::getMimeFromExtension($ext)); }