基于PHP实现大文件分片上传与断点续传的技术方案
需求背景与技术挑战
在现代Web应用开发中,文件管理系统常需支持大文件(如视频、备份包等)的稳定上传。传统一次性上传方式容易因网络中断或超时导致失败。为此,必须采用分片上传结合断点续传机制,确保高可靠性与良好用户体验。
前端实现:使用WebUploader进行分片处理
前端选用百度开源的WebUploader组件,因其对多浏览器兼容性良好,尤其支持包含IE8在内的旧版本浏览器,并内置了分片、并发上传、断点续传等功能。
初始化上传器
<script type="text/javascript">
let uploader = WebUploader.create({
swf: '/static/webuploader/Uploader.swf', // Flash降级支持
server: '/upload/chunk',
pick: '#filePicker',
auto: true,
chunked: true,
chunkSize: 5 * 1024 * 1024, // 每片5MB
threads: 3,
duplicate: true,
fileVal: 'chunk_data',
formData: {
file_id: '', // 动态填充
chunk_index: '',
total_chunks: ''
},
accept: {
title: 'Files',
extensions: 'mp4,pdf,zip,rar,mov,avi',
mimeTypes: '.mp4,.pdf,.zip,.rar,.mov,.avi'
}
});
</script>
文件夹上传支持
通过HTML5的webkitdirectory属性启用目录选择功能:
<div id="folderPicker">选择文件夹</div>
<input type="file" webkitdirectory style="opacity: 0; position: absolute;" onchange="handleFolderSelect(this.files)" />
<script>
function handleFolderSelect(files) {
Array.from(files).forEach(file => {
let relativePath = file.webkitRelativePath;
uploader.addFiles(file);
// 可记录路径用于服务端重建结构
});
}
</script>
后端逻辑:PHP接收并合并分片
使用原生PHP构建轻量级接口,处理分片上传、校验、存储及最终合并操作。
生成唯一文件标识
客户端上传前可先请求一个临时ID和上传上下文:
// request_id.php
header('Content-Type: application/json');
$fileInfo = $_POST['file_info']; // 包含文件名、大小、最后修改时间
$uniqueId = md5($fileInfo['name'] . $fileInfo['size'] . $fileInfo['last_modified']);
// 创建临时目录
$tempDir = "temp/{$uniqueId}";
if (!is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
echo json_encode([
'upload_id' => $uniqueId,
'chunk_size' => 5 * 1024 * 1024,
'resume' => scanExistingChunks($tempDir, (int)$fileInfo['total_chunks'])
]);
function scanExistingChunks($dir, $total) {
$uploaded = [];
for ($i = 0; $i < $total; $i++) {
if (file_exists("{$dir}/chunk_{$i}")) {
$uploaded[] = $i;
}
}
return $uploaded;
}
接收单个分片
// chunk.php
$uploadId = $_POST['upload_id'];
$index = (int)$_POST['chunk_index'];
$total = (int)$_POST['total_chunks'];
$data = file_get_contents('php://input');
$tempDir = "temp/{$uploadId}";
if (!is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
$file = "{$tempDir}/chunk_{$index}";
file_put_contents($file, $data);
// 返回成功状态
header('Content-Type: application/json');
echo json_encode(['status' => 'received', 'index' => $index]);
分片合并与清理
// merge.php
$uploadId = $_POST['upload_id'];
$fileName = basename($_POST['file_name']);
$totalChunks = (int)$_POST['total_chunks'];
$tempDir = "temp/{$uploadId}";
$targetDir = "uploads/";
if (!is_dir($targetDir)) mkdir($targetDir, 0755);
$finalPath = $targetDir . generateUniqueFileName($fileName);
$handle = fopen($finalPath, 'wb');
for ($i = 0; $i < $totalChunks; $i++) {
$chunkFile = "{$tempDir}/chunk_{$i}";
if (file_exists($chunkFile)) {
$content = file_get_contents($chunkFile);
fwrite($handle, $content);
unlink($chunkFile); // 合并后立即删除
} else {
fclose($handle);
unlink($finalPath);
http_response_code(400);
echo json_encode(['error' => "Missing chunk {$i}"]);
exit;
}
}
fclose($handle);
rrmdir($tempDir); // 删除空目录
echo json_encode([
'status' => 'success',
'file_url' => "/uploads/" . basename($finalPath)
]);
function rrmdir($dir) {
if (is_dir($dir)) {
$files = scandir($dir);
foreach ($files as $file) {
if ($file !== '.' && $file !== '..') {
is_dir("$dir/$file") ? rrmdir("$dir/$file") : unlink("$dir/$file");
}
}
rmdir($dir);
}
}
function generateUniqueFileName($name) {
$ext = pathinfo($name, PATHINFO_EXTENSION);
$base = pathinfo($name, PATHINFO_FILENAME);
$suffix = '';
$counter = 1;
while (file_exists("uploads/{$base}{$suffix}.{$ext}")) {
$suffix = "_{$counter}";
$counter++;
}
return "{$base}{$suffix}.{$ext}";
}
加密传输设计
为满足安全要求,可在前端使用AES加密,后端解密后再存储。
// 前端加密示例(需引入CryptoJS)
const encryptedData = CryptoJS.AES.encrypt(
data, 'secret-key-123'
).toString();
// 后端解密(使用openssl)
function decryptData($data, $key) {
return openssl_decrypt(base64_decode($data), 'AES-128-ECB', $key, OPENSSL_RAW_DATA);
}
断点续传持久化
利用localStorage保存已上传分片索引:
uploader.on('uploadSuccess', function(file, response) {
let uploaded = JSON.parse(localStorage.getItem('uploaded_chunks') || '{}');
uploaded[uploadId] = uploaded[uploadId] || [];
uploaded[uploadId].push(file.chunkIndex);
localStorage.setItem('uploaded_chunks', JSON.stringify(uploaded));
});
部署建议
- PHP配置调优:调整
post_max_size、upload_max_filesize、max_execution_time - 临时目录权限:确保web用户有写入
temp/目录的权限 - 定期清理任务:设置cron定时清除超过24小时未完成的临时分片
数据库辅助管理(可选)
对于复杂场景,可用MySQL记录上传会话:
CREATE TABLE upload_sessions (
id VARCHAR(32) PRIMARY KEY,
filename VARCHAR(255),
total_size BIGINT,
chunk_count INT,
uploaded_chunks TEXT, -- JSON数组
status ENUM('pending','merged','failed'),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);