PHP怎么实现断点上传

wen PHP项目 1

本文目录导读:

PHP怎么实现断点上传

  1. 基础原理
  2. 简单分片上传实现
  3. 改进版本(带MD5校验)
  4. 使用腾讯云/阿里云OSS断点续传
  5. 最佳实践建议

我来详细解释PHP实现断点上传的几种方案:

基础原理

断点上传的核心是:

  • 前端将文件切片
  • 后端接收并保存每个分片
  • 所有分片上传完成后合并

简单分片上传实现

前端代码(HTML + JavaScript)

<!DOCTYPE html>
<html>
<head>断点上传</title>
</head>
<body>
    <input type="file" id="fileInput">
    <button onclick="uploadFile()">开始上传</button>
    <div id="progress"></div>
    <script>
    const chunkSize = 1 * 1024 * 1024; // 1MB 每片
    async function uploadFile() {
        const file = document.getElementById('fileInput').files[0];
        if (!file) return;
        const totalChunks = Math.ceil(file.size / chunkSize);
        let currentChunk = 0;
        // 检查已上传的分片
        const checkResponse = await fetch('check.php', {
            method: 'POST',
            body: JSON.stringify({
                fileName: file.name,
                totalChunks: totalChunks
            })
        });
        const uploadedChunks = await checkResponse.json();
        for (let i = 0; i < totalChunks; i++) {
            if (uploadedChunks.includes(i)) continue;
            const start = i * chunkSize;
            const end = Math.min(start + chunkSize, file.size);
            const chunk = file.slice(start, end);
            const formData = new FormData();
            formData.append('file', chunk);
            formData.append('fileName', file.name);
            formData.append('chunkIndex', i);
            formData.append('totalChunks', totalChunks);
            try {
                await fetch('upload.php', {
                    method: 'POST',
                    body: formData
                });
                const progress = Math.round(((i + 1) / totalChunks) * 100);
                document.getElementById('progress').textContent = 
                    `上传进度: ${progress}%`;
            } catch (error) {
                console.error(`分片 ${i} 上传失败`);
                break;
            }
        }
        // 通知服务器合并文件
        await fetch('merge.php', {
            method: 'POST',
            body: JSON.stringify({
                fileName: file.name,
                totalChunks: totalChunks
            })
        });
    }
    </script>
</body>
</html>

后端PHP代码

upload.php(接收分片)

<?php
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $fileName = $_POST['fileName'];
    $chunkIndex = (int)$_POST['chunkIndex'];
    $uploadDir = 'uploads/chunks/';
    // 创建上传目录
    if (!file_exists($uploadDir)) {
        mkdir($uploadDir, 0777, true);
    }
    if (isset($_FILES['file'])) {
        $chunkName = $fileName . '_chunk_' . $chunkIndex;
        $chunkPath = $uploadDir . md5($chunkName);
        // 保存分片
        if (move_uploaded_file($_FILES['file']['tmp_name'], $chunkPath)) {
            echo json_encode([
                'success' => true,
                'message' => '分片上传成功',
                'chunkIndex' => $chunkIndex
            ]);
        } else {
            echo json_encode([
                'success' => false,
                'message' => '分片上传失败'
            ]);
        }
    }
}

check.php(检查已上传分片)

<?php
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$fileName = $input['fileName'];
$totalChunks = $input['totalChunks'];
$uploadDir = 'uploads/chunks/';
$uploadedChunks = [];
// 遍历检查已上传的分片
for ($i = 0; $i < $totalChunks; $i++) {
    $chunkName = $fileName . '_chunk_' . $i;
    $chunkPath = $uploadDir . md5($chunkName);
    if (file_exists($chunkPath)) {
        $uploadedChunks[] = $i;
    }
}
echo json_encode($uploadedChunks);

merge.php(合并文件)

<?php
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$fileName = $input['fileName'];
$totalChunks = $input['totalChunks'];
$uploadDir = 'uploads/chunks/';
$finalDir = 'uploads/files/';
if (!file_exists($finalDir)) {
    mkdir($finalDir, 0777, true);
}
$finalPath = $finalDir . $fileName;
// 创建并写入最终文件
$finalFile = fopen($finalPath, 'wb');
for ($i = 0; $i < $totalChunks; $i++) {
    $chunkName = $fileName . '_chunk_' . $i;
    $chunkPath = $uploadDir . md5($chunkName);
    if (file_exists($chunkPath)) {
        $chunkContent = file_get_contents($chunkPath);
        fwrite($finalFile, $chunkContent);
        // 删除已合并的分片
        unlink($chunkPath);
    } else {
        fclose($finalFile);
        echo json_encode([
            'success' => false,
            'message' => "分片 {$i} 缺失,合并失败"
        ]);
        return;
    }
}
fclose($finalFile);
echo json_encode([
    'success' => true,
    'message' => '文件合并成功',
    'filePath' => $finalPath
]);

改进版本(带MD5校验)

<?php
// upload.php - 带MD5校验版本
function getFileMd5($filePath) {
    if (file_exists($filePath)) {
        return md5_file($filePath);
    }
    return null;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $fileName = $_POST['fileName'];
    $chunkIndex = (int)$_POST['chunkIndex'];
    $chunkMd5 = isset($_POST['md5']) ? $_POST['md5'] : '';
    $uploadDir = 'uploads/chunks/';
    $chunkName = $fileName . '_chunk_' . $chunkIndex;
    $chunkPath = $uploadDir . md5($chunkName);
    if (isset($_FILES['file'])) {
        $tmpName = $_FILES['file']['tmp_name'];
        // 计算上传分片的MD5
        $uploadedMd5 = md5_file($tmpName);
        // 校验MD5
        if ($uploadedMd5 === $chunkMd5) {
            if (move_uploaded_file($tmpName, $chunkPath)) {
                echo json_encode([
                    'success' => true,
                    'message' => '分片上传成功,校验通过'
                ]);
            }
        } else {
            echo json_encode([
                'success' => false,
                'message' => 'MD5校验失败,请重新上传'
            ]);
        }
    }
}

使用腾讯云/阿里云OSS断点续传

<?php
// 使用阿里云OSS SDK示例
require_once 'aliyun-oss-php-sdk/autoload.php';
use OSS\OssClient;
use OSS\Core\OssException;
$accessKeyId = "your-access-key-id";
$accessKeySecret = "your-access-key-secret";
$endpoint = "oss-cn-hangzhou.aliyuncs.com";
$bucket = "your-bucket";
try {
    $ossClient = new OssClient($accessKeyId, $accessKeySecret, $endpoint);
    // 分片上传
    $object = "uploads/example.zip";
    $file = __DIR__ . "/example.zip";
    // 初始化分片上传
    $uploadId = $ossClient->initiateMultipartUpload($bucket, $object);
    // 上传分片
    $partSize = 10 * 1024 * 1024; // 10MB
    $uploadFileSize = filesize($file);
    $partCount = ceil($uploadFileSize / $partSize);
    $parts = array();
    for ($i = 1; $i <= $partCount; $i++) {
        $start = ($i - 1) * $partSize;
        $length = min($partSize, $uploadFileSize - $start);
        $content = file_get_contents($file, false, null, $start, $length);
        $result = $ossClient->uploadPart($bucket, $object, $uploadId, array(
            'PartNumber' => $i,
            'Content' => $content
        ));
        $parts[] = array(
            'PartNumber' => $i,
            'ETag' => $result['ETag']
        );
    }
    // 完成分片上传
    $result = $ossClient->completeMultipartUpload($bucket, $object, $uploadId, $parts);
    echo "文件上传成功: " . $result['Location'];
} catch (OssException $e) {
    echo "上传失败: " . $e->getMessage();
}

最佳实践建议

前端优化

  • 使用Web Worker处理分片,避免阻塞UI
  • 添加重试机制
  • 显示实时进度条
  • 支持拖拽上传

后端优化

  • 使用Redis记录已上传分片状态
  • 定期清理过期分片
  • 使用消息队列异步处理合并
  • 限制文件大小和并发数
// 使用Redis记录分片状态
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = "upload:{$fileName}";
$redis->sAdd($key, $chunkIndex);
$redis->expire($key, 3600); // 1小时后过期
// 检查分片是否存在
if ($redis->sIsMember($key, $chunkIndex)) {
    echo "该分片已上传";
}

这套方案可以满足大多数断点上传需求,如果需要处理超大文件,建议使用云存储服务商的SDK,它们提供了更完善的断点续传机制。

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