本文目录导读:

在PHP项目中实现分片上传的文件合并,通常遵循以下步骤,这里提供一个完整、安全的实现方案。
核心流程
- 前端将大文件切成多个小块依次上传
- 后端接收每个分片并保存到临时目录
- 所有分片上传完成后,发送合并请求
- 后端按顺序合并所有分片为最终文件
后端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项目的文件上传需求。