PHP项目分片上传如何合并文件

wen PHP项目 19

本文目录导读:

PHP项目分片上传如何合并文件

  1. 核心流程
  2. 后端PHP合并实现
  3. 关键注意事项
  4. 前端配合示例(JavaScript)

在PHP项目中实现分片上传的文件合并,通常遵循以下步骤,这里提供一个完整、安全的实现方案。

核心流程

  1. 前端将大文件切成多个小块依次上传
  2. 后端接收每个分片并保存到临时目录
  3. 所有分片上传完成后,发送合并请求
  4. 后端按顺序合并所有分片为最终文件

后端PHP合并实现

基础合并函数

<?php
/**
 * 合并分片文件
 * 
 * @param string $targetFile 最终目标文件路径
 * @param string $tempDir 分片临时目录
 * @param int $totalChunks 总分片数量
 * @param string $fileIdentifier 文件唯一标识
 * @return bool|string 成功返回true,失败返回错误信息
 */
function mergeChunks($targetFile, $tempDir, $totalChunks, $fileIdentifier) {
    // 验证目标目录可写
    $targetDir = dirname($targetFile);
    if (!is_dir($targetDir) || !is_writable($targetDir)) {
        return '目标目录不可写';
    }
    // 打开目标文件(追加写入模式)
    $dest = fopen($targetFile, 'wb');
    if (!$dest) {
        return '无法创建目标文件';
    }
    // 使用flock防止并发问题
    if (!flock($dest, LOCK_EX)) {
        fclose($dest);
        return '无法锁定目标文件';
    }
    try {
        // 按顺序合并所有分片
        for ($i = 0; $i < $totalChunks; $i++) {
            $chunkFile = $tempDir . '/' . $fileIdentifier . '_part_' . $i;
            if (!file_exists($chunkFile)) {
                throw new Exception("缺失分片: {$i}");
            }
            // 读取并写入分片
            $chunk = fopen($chunkFile, 'rb');
            if (!$chunk) {
                throw new Exception("无法读取分片: {$i}");
            }
            // 使用流式传输,避免大文件内存溢出
            stream_copy_to_stream($chunk, $dest);
            fclose($chunk);
            // 合并后立即删除分片,节省空间
            unlink($chunkFile);
        }
        // 验证文件完整性(可选)
        $expectedSize = 0;
        // 可以在上传时记录每个分片的大小,这里简单示例
        flock($dest, LOCK_UN);
        fclose($dest);
        // 删除临时目录(如果为空)
        @rmdir($tempDir);
        return true;
    } catch (Exception $e) {
        flock($dest, LOCK_UN);
        fclose($dest);
        // 合并失败,清理部分文件
        return $e->getMessage();
    }
}

完整的文件上传API

<?php
// upload.php - 处理分片上传请求
header('Content-Type: application/json');
// 配置
$uploadDir = '/path/to/uploads/';      // 最终文件存储目录
$tempDir = '/path/to/temp/';          // 临时分片目录
$maxFileSize = 2 * 1024 * 1024 * 1024; // 2GB
$chunkSize = 5 * 1024 * 1024;         // 5MB
// 确保目录存在
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
if (!is_dir($tempDir)) mkdir($tempDir, 0755, true);
$action = $_POST['action'] ?? '';
$fileIdentifier = $_POST['identifier'] ?? '';
$fileName = $_POST['filename'] ?? '';
$totalChunks = (int)($_POST['totalChunks'] ?? 0);
$chunkIndex = (int)($_POST['chunkIndex'] ?? 0);
// 生成安全的文件名
$safeFileName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $fileName);
$filePath = $uploadDir . '/' . $safeFileName;
$chunkTempDir = $tempDir . '/' . $fileIdentifier;
switch ($action) {
    case 'upload':
        // 处理分片上传
        if (!isset($_FILES['chunk'])) {
            echo json_encode(['success' => false, 'message' => '未找到分片文件']);
            exit;
        }
        $chunkFile = $_FILES['chunk'];
        // 验证文件大小
        if ($chunkFile['size'] > $chunkSize) {
            echo json_encode(['success' => false, 'message' => '分片大小超限']);
            exit;
        }
        // 创建临时目录
        if (!is_dir($chunkTempDir)) {
            mkdir($chunkTempDir, 0755, true);
        }
        // 保存分片
        $chunkPath = $chunkTempDir . '/part_' . $chunkIndex;
        if (!move_uploaded_file($chunkFile['tmp_name'], $chunkPath)) {
            echo json_encode(['success' => false, 'message' => '分片保存失败']);
            exit;
        }
        echo json_encode(['success' => true, 'message' => '分片上传成功']);
        break;
    case 'merge':
        // 合并所有分片
        $result = mergeChunks($filePath, $chunkTempDir, $totalChunks, $fileIdentifier);
        if ($result === true) {
            echo json_encode(['success' => true, 'message' => '文件合并成功', 'path' => $filePath]);
        } else {
            echo json_encode(['success' => false, 'message' => $result]);
        }
        break;
    case 'check':
        // 检查已上传的分片(用于断点续传)
        $uploadedChunks = [];
        if (is_dir($chunkTempDir)) {
            $files = scandir($chunkTempDir);
            foreach ($files as $file) {
                if (strpos($file, 'part_') === 0) {
                    $index = str_replace('part_', '', $file);
                    $uploadedChunks[] = (int)$index;
                }
            }
        }
        echo json_encode([
            'success' => true, 
            'uploadedChunks' => $uploadedChunks,
            'totalChunks' => $totalChunks
        ]);
        break;
    default:
        echo json_encode(['success' => false, 'message' => '无效操作']);
}

安全性增强版本

<?php
/**
 * 安全版本的文件合并 - 防止路径遍历等攻击
 */
function secureMergeChunks($targetFile, $tempDir, $totalChunks, $fileIdentifier) {
    // 1. 验证标识符,防止路径遍历
    if (preg_match('/[^a-zA-Z0-9_-]/', $fileIdentifier)) {
        return '无效的文件标识符';
    }
    // 2. 安全的临时目录路径
    $baseTemp = realpath('/path/to/temp/');
    $chunkDir = $baseTemp . '/' . basename($fileIdentifier);
    // 验证目录在安全范围内
    if (strpos(realpath($chunkDir), $baseTemp) !== 0) {
        return '路径验证失败';
    }
    // 3. 验证总分片数在合理范围
    if ($totalChunks < 1 || $totalChunks > 10000) {
        return '分片数量无效';
    }
    // 4. 安全的目标文件路径
    $targetDir = realpath('/path/to/uploads/');
    $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($targetFile));
    $finalPath = $targetDir . '/' . $safeName;
    // 确保目标路径在允许的目录内
    if (strpos(realpath(dirname($finalPath)), $targetDir) !== 0) {
        return '目标路径无效';
    }
    // 5. 文件类型白名单验证
    $allowedExtensions = ['jpg', 'png', 'pdf', 'mp4', 'zip'];
    $extension = strtolower(pathinfo($safeName, PATHINFO_EXTENSION));
    if (!in_array($extension, $allowedExtensions)) {
        return '不支持的文件类型';
    }
    // 6. 执行合并
    $dest = fopen($finalPath, 'wb');
    if (!$dest) {
        return '无法创建目标文件';
    }
    try {
        for ($i = 0; $i < $totalChunks; $i++) {
            $chunkFile = $chunkDir . '/part_' . $i;
            if (!file_exists($chunkFile)) {
                throw new Exception("缺失分片: {$i}");
            }
            // 验证分片大小
            if (filesize($chunkFile) > 10 * 1024 * 1024) { // 10MB
                throw new Exception("分片大小异常: {$i}");
            }
            $chunk = fopen($chunkFile, 'rb');
            stream_copy_to_stream($chunk, $dest);
            fclose($chunk);
            // 限制总文件大小
            if (ftell($dest) > 2 * 1024 * 1024 * 1024) { // 2GB
                throw new Exception('文件大小超过限制');
            }
            unlink($chunkFile);
        }
        fclose($dest);
        @rmdir($chunkDir);
        return true;
    } catch (Exception $e) {
        if (isset($dest) && is_resource($dest)) {
            fclose($dest);
            unlink($finalPath);
        }
        return $e->getMessage();
    }
}

关键注意事项

内存管理

  • 使用 stream_copy_to_stream() 而非 file_get_contents()
  • 避免一次性加载整个文件到内存

并发控制

  • 使用 flock() 文件锁防止并发写入冲突
  • 考虑使用数据库记录上传状态

错误恢复

  • 记录每个分片的上传状态
  • 支持断点续传(通过check接口)
  • 合并失败时清理残留文件

性能优化

  • 将分片大小设置为5-10MB
  • 使用SSD存储临时分片
  • 考虑使用消息队列处理大量并发

安全防护

  • 验证文件标识符防止路径遍历
  • 限制文件类型和大小
  • 对文件名进行消毒处理
  • 使用 realpath() 验证路径在允许范围内

前端配合示例(JavaScript)

// 简单的分片上传实现
async function uploadFile(file) {
    const chunkSize = 5 * 1024 * 1024; // 5MB
    const totalChunks = Math.ceil(file.size / chunkSize);
    const identifier = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
    for (let i = 0; i < totalChunks; i++) {
        const start = i * chunkSize;
        const end = Math.min(start + chunkSize, file.size);
        const chunk = file.slice(start, end);
        const formData = new FormData();
        formData.append('chunk', chunk);
        formData.append('identifier', identifier);
        formData.append('chunkIndex', i);
        formData.append('totalChunks', totalChunks);
        formData.append('filename', file.name);
        formData.append('action', 'upload');
        await fetch('/upload.php', {
            method: 'POST',
            body: formData
        });
    }
    // 触发合并
    const mergeData = new FormData();
    mergeData.append('action', 'merge');
    mergeData.append('identifier', identifier);
    mergeData.append('totalChunks', totalChunks);
    mergeData.append('filename', file.name);
    return fetch('/upload.php', {
        method: 'POST',
        body: mergeData
    });
}

这个实现方案考虑了安全性、性能和可靠性,适合大多数PHP项目的文件上传需求。

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