PHP下载文件如何断点续传

wen PHP项目 3

本文目录导读:

PHP下载文件如何断点续传

  1. 基础断点续传实现
  2. 支持多范围和特殊情况的增强版
  3. 使用示例
  4. 前端配合实现
  5. 重要注意事项

在PHP中实现断点续传下载,主要依赖于HTTP协议的Range头,以下是完整实现方案:

基础断点续传实现

<?php
function downloadFile($filePath, $fileName = '') {
    // 验证文件
    if (!file_exists($filePath)) {
        http_response_code(404);
        die('文件不存在');
    }
    // 获取文件信息
    $fileSize = filesize($filePath);
    $fileName = $fileName ?: basename($filePath);
    // 设置基础头信息
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . $fileName . '"');
    header('Accept-Ranges: bytes');
    header('Content-Length: ' . $fileSize);
    // 解析Range头
    $start = 0;
    $end = $fileSize - 1;
    $isPartialRequest = false;
    if (isset($_SERVER['HTTP_RANGE'])) {
        // 只处理单个范围的请求
        if (preg_match('/bytes=(\d*)-(\d*)/', $_SERVER['HTTP_RANGE'], $matches)) {
            $isPartialRequest = true;
            if (!empty($matches[1])) {
                $start = intval($matches[1]);
            }
            if (!empty($matches[2])) {
                $end = intval($matches[2]);
            }
            // 验证范围有效性
            if ($start >= $fileSize || $end >= $fileSize) {
                header('HTTP/1.1 416 Requested Range Not Satisfiable');
                header('Content-Range: bytes */' . $fileSize);
                exit;
            }
            if ($start > $end) {
                header('HTTP/1.1 400 Bad Request');
                exit;
            }
        }
    }
    // 设置响应状态码
    if ($isPartialRequest) {
        http_response_code(206);
        header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
        header('Content-Length: ' . ($end - $start + 1));
    }
    // 输出文件内容
    $fp = fopen($filePath, 'rb');
    if ($fp === false) {
        http_response_code(500);
        die('无法打开文件');
    }
    // 移动到指定位置
    fseek($fp, $start);
    // 设置缓冲大小
    $buffer = 1024 * 1024; // 1MB
    while (!feof($fp) && ($pos = ftell($fp)) <= $end) {
        if ($pos + $buffer > $end) {
            $buffer = $end - $pos + 1;
        }
        echo fread($fp, $buffer);
        flush();
        // 防止内存溢出
        if (connection_aborted()) {
            break;
        }
    }
    fclose($fp);
    exit;
}

支持多范围和特殊情况的增强版

<?php
class ResumeDownload {
    private $filePath;
    private $fileName;
    private $mimeType;
    public function __construct($filePath, $fileName = '', $mimeType = '') {
        $this->filePath = $filePath;
        $this->fileName = $fileName ?: basename($filePath);
        $this->mimeType = $mimeType ?: $this->getMimeType($filePath);
    }
    public function download() {
        // 检查文件
        if (!file_exists($this->filePath)) {
            $this->sendError(404, 'File not found');
            return;
        }
        $fileSize = filesize($this->filePath);
        // 处理Range请求
        $ranges = $this->parseRanges();
        header('Content-Type: ' . $this->mimeType);
        header('Content-Disposition: attachment; filename="' . $this->fileName . '"');
        header('Accept-Ranges: bytes');
        header('Cache-Control: public, must-revalidate, max-age=0');
        header('Pragma: public');
        if (empty($ranges)) {
            // 完整下载
            header('Content-Length: ' . $fileSize);
            $this->sendFileContents(0, $fileSize - 1);
        } else {
            // 多范围请求(简化处理,实际可支持多个范围)
            $range = $ranges[0];
            // 处理无效范围
            if ($range[0] >= $fileSize || $range[1] >= $fileSize) {
                header('HTTP/1.1 416 Requested Range Not Satisfiable');
                header('Content-Length: 0');
                header('Content-Range: bytes */' . $fileSize);
                return;
            }
            // 发送206响应
            header('HTTP/1.1 206 Partial Content');
            header('Content-Range: bytes ' . $range[0] . '-' . $range[1] . '/' . $fileSize);
            header('Content-Length: ' . ($range[1] - $range[0] + 1));
            $this->sendFileContents($range[0], $range[1]);
        }
    }
    private function parseRanges() {
        if (!isset($_SERVER['HTTP_RANGE'])) {
            return [];
        }
        $rangeHeader = $_SERVER['HTTP_RANGE'];
        // 检查是否为bytes范围
        if (strpos($rangeHeader, 'bytes') !== 0) {
            return [];
        }
        // 解析范围
        $ranges = [];
        $parts = explode(',', substr($rangeHeader, 6));
        foreach ($parts as $part) {
            if (preg_match('/(\d*)-(\d*)/', trim($part), $matches)) {
                $start = !empty($matches[1]) ? intval($matches[1]) : null;
                $end = !empty($matches[2]) ? intval($matches[2]) : null;
                if ($start === null) {
                    // 最后N个字节: -N
                    $fileSize = filesize($this->filePath);
                    $start = max(0, $fileSize - $end);
                    $end = $fileSize - 1;
                } elseif ($end === null) {
                    // 从某位置到末尾: N-
                    $end = filesize($this->filePath) - 1;
                }
                $ranges[] = [$start, $end];
            }
        }
        return $ranges;
    }
    private function sendFileContents($start, $end) {
        $fp = fopen($this->filePath, 'rb');
        if (!$fp) {
            $this->sendError(500, 'Unable to open file');
            return;
        }
        fseek($fp, $start);
        $bytesToSend = $end - $start + 1;
        $buffer = 8192; // 8KB
        while ($bytesToSend > 0 && !feof($fp)) {
            $read = min($buffer, $bytesToSend);
            $data = fread($fp, $read);
            if ($data === false) {
                break;
            }
            echo $data;
            flush();
            $bytesToSend -= strlen($data);
            // 检查连接
            if (connection_aborted()) {
                break;
            }
        }
        fclose($fp);
    }
    private function sendError($code, $message) {
        http_response_code($code);
        echo $message;
    }
    private function getMimeType($filePath) {
        $mimeTypes = [
            'mp4' => 'video/mp4',
            'avi' => 'video/x-msvideo',
            'mp3' => 'audio/mpeg',
            'pdf' => 'application/pdf',
            'txt' => 'text/plain',
            // 添加更多类型...
        ];
        $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
        return isset($mimeTypes[$ext]) ? $mimeTypes[$ext] : 'application/octet-stream';
    }
}

使用示例

<?php
// 简单用法
$filePath = '/path/to/file.zip';
downloadFile($filePath);
// 增强版用法
$download = new ResumeDownload('/path/to/video.mp4', 'myvideo.mp4');
$download->download();
// 直接在HTML中调用
<a href="download.php?file=bigfile.zip">下载文件</a>

前端配合实现

// 前端断点续传下载
async function downloadWithResume(url) {
    try {
        const response = await fetch(url, {
            headers: {
                'Range': 'bytes=0-1023' // 先下载第一部分
            }
        });
        if (response.status === 206) {
            const contentLength = response.headers.get('Content-Length');
            const contentRange = response.headers.get('Content-Range');
            console.log('Partial:', contentRange);
            // 获取文件总大小
            const totalSize = parseInt(contentRange.split('/')[1]);
            // 继续下载剩余部分
            let start = 1024;
            while (start < totalSize) {
                const nextResponse = await fetch(url, {
                    headers: {
                        'Range': `bytes=${start}-${start + 1023}`
                    }
                });
                await nextResponse.blob();
                start += 1024;
            }
        }
    } catch (error) {
        console.error('Download error:', error);
    }
}

重要注意事项

  1. 性能优化

    • 使用合适的内存缓冲大小(建议1-8MB)
    • 使用flush()及时输出
  2. 安全性

    • 验证文件路径,防止目录遍历攻击
    • 检查用户权限
  3. 异常处理

    • 处理文件不存在、权限不足等情况
    • 处理连接中断
  4. 兼容性

    • 支持IE、Chrome、Firefox等主流浏览器
    • 支持curl、wget等下载工具

这个实现可以完美支持迅雷、IDM等下载工具的断点续传功能。

抱歉,评论功能暂时关闭!