本文目录导读:

在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项目的文件上传需求。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播