sendFile($fileName, $fancyName, $forceDownload, $speedLimit, $contentType) 完整支援:
HTTP_RANGE Header,回傳 206 Partial Content。$speedLimit (bytes/sec) 配合 usleep 節流。ETag、Last-Modified、If-None-Match、If-Modified-Since,支援 304 Not Modified。is_readable、路徑穿越防護 (實務需額外 realpath 限制根目錄)。rawurlencode 符合 RFC 5987。<?php
function sendFile(string $file, string $name = '', bool $force = true, int $speed = 0, string $mime = ''): bool {
if (!is_file($file) || !is_readable($file)) {
http_response_code(404);
return false;
}
$stat = stat($file);
$size = $stat['size'];
$mtime = $stat['mtime'];
// ETag: md5(mtime-inode-size) + crc32
$etag = '"' . md5("$mtime-{$stat['ino']}-$size") . '-' . sprintf('%u', crc32($file)) . '"';
header("ETag: $etag");
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
// 快取驗證
if ((isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag) ||
(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $mtime)) {
http_response_code(304);
return true;
}
$name = $name ?: basename($file);
$mime = $mime ?: mime_content_type($file) ?: 'application/octet-stream';
// Range 處理
$start = 0; $end = $size - 1; $partial = false;
if (isset($_SERVER['HTTP_RANGE'])) {
if (preg_match('/bytes=(\d*)-(\d*)/', $_SERVER['HTTP_RANGE'], $r)) {
$start = $r[1] !== '' ? (int)$r[1] : $size - (int)$r[2];
$end = $r[2] !== '' ? (int)$r[2] : $size - 1;
$start = max(0, min($start, $size - 1));
$end = max($start, min($end, $size - 1));
$partial = true;
}
}
$length = $end - $start + 1;
// Header 輸出
http_response_code($partial ? 206 : 200);
if ($partial) header("Content-Range: bytes $start-$end/$size");
header('Accept-Ranges: bytes');
header("Content-Type: $mime");
header("Content-Length: $length");
if ($force) header('Content-Disposition: attachment; filename="' . rawurlencode($name) . '"');
header('Content-Transfer-Encoding: binary');
header('Cache-Control: public, must-revalidate, max-age=0');
header('Pragma: public');
// 發送內容
$fp = fopen($file, 'rb');
fseek($fp, $start);
$bufferSize = 8192;
$sent = 0;
while ($sent < $length && !feof($fp) && connection_status() === 0) {
$read = min($bufferSize, $length - $sent);
echo fread($fp, $read);
ob_flush(); flush();
$sent += $read;
if ($speed > 0) {
$target = $bufferSize * 1e6 / $speed; // 微秒
$elapsed = (microtime(true) - $loopStart) * 1e6;
$sleep = $target - $elapsed;
if ($sleep > 0) usleep($sleep);
}
}
fclose($fp);
return true;
}
| 副檔名 | MIME Type |
|---|---|
| application/pdf | |
| .jpg/.jpeg | image/jpeg |
| .png | image/png |
| .gif | image/gif |
| .zip | application/zip |
| .doc/.docx | application/msword / application/vnd.openxmlformats-officedocument.wordprocessingml.document |
| .xls/.xlsx | application/vnd.ms-excel / application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
| .mp4 | video/mp4 |
| .mp3 | audio/mpeg |
| .apk | application/vnd.android.package-archive |
| .woff/.woff2 | font/woff / font/woff2 |
完整 300+ 條對照表請參考原文附錄或使用 mime_content_type() (需 fileinfo 擴展) 自動偵測。
$real = realpath($file); if (strpos($real, $allowedRoot) !== 0) die('Forbidden');readfile() 或 fread 分塊輸出 (如上),嚴禁 file_get_contents 整個讀入記憶體。X-Accel-Redirect: /protected/path,效能提升 10 倍以上。?token=hmac_sha256(file_id, expiry, secret),防止熱連結。