开发者日记:2023年11月20日 周一 晴
项目名称:跨平台大文件传输系统(WebUploader+Vue3+JSP+腾讯云COS)
项目背景与核心挑战
近期承接了一个高难度外包项目,客户要求实现20G级文件/文件夹上传下载,需满足以下硬性条件:
- 断点续传:即使重启电脑,进度不能丢失(需持久化存储)
- 文件夹层级保留:上传后需100%还原原始目录结构
- 全浏览器兼容:从IE8到现代浏览器(Edge/Chrome/Firefox/Safari/Opera)
- 技术栈:Vue3(前端)+ JSP(后端)+ MySQL(数据库)+ 腾讯云COS(存储)
现存问题:
- 网上开源方案仅支持单文件上传,无完整文件夹上传实现
- IE8的Flash上传存在安全策略限制
- 腾讯云COS的分片上传API与百度OBS有差异,需重新适配
- 20G文件传输需解决内存溢出和超时问题
技术方案设计
前端架构(Vue3 + WebUploader)
graph TD
A[用户选择文件/文件夹] --> B{浏览器类型}
B -->|Chrome/Firefox| C[使用webkitdirectory API]
B -->|IE8| D[调用ActiveX控件递归读取]
C & D --> E[生成文件树结构]
E --> F[计算文件MD5(spark-md5)]
F --> G[分片上传(WebUploader)]
G --> H[本地存储进度(localStorage)]
后端架构(JSP + Servlet)
关键数据库设计
CREATE TABLE upload_tasks (
task_id VARCHAR(36) PRIMARY KEY, -- UUID
file_md5 VARCHAR(64) NOT NULL,
relative_path VARCHAR(512), -- 保留文件夹层级(如 /docs/2023/)
total_chunks INT,
uploaded_chunks INT,
status ENUM('pending','uploading','completed','failed'),
cos_key VARCHAR(1024), -- COS存储路径
create_time DATETIME DEFAULT NOW()
);
核心代码实现
前端:文件夹上传与断点续传(Vue3)
// src/components/FolderUploader.vue
import { ref, onMounted } from 'vue';
import WebUploader from 'webuploader';
import SparkMD5 from 'spark-md5';
export default {
setup() {
const taskList = ref([]);
const uploader = ref(null);
// 初始化上传器(兼容IE8)
const initUploader = () => {
const isIE8 = !!window.ActiveXObject || "ActiveXObject" in window;
uploader.value = WebUploader.create({
swf: '/static/Uploader.swf', // IE8回退
server: '/api/upload-chunk',
chunked: true,
chunkSize: isIE8 ? 4 * 1024 * 1024 : 10 * 1024 * 1024,
threads: isIE8 ? 1 : 3, // IE8限制并发
formData: {
taskId: localStorage.getItem('currentTaskId') || ''
}
});
// 恢复未完成任务
restoreTasks();
};
// 递归解析文件夹(跨浏览器)
const handleFolderSelect = (e) => {
const files = e.target.files;
if (!files.length) return;
const parseFolder = (entries, parentPath = '') => {
for (let entry of entries) {
if (entry.isFile) {
const relativePath = parentPath ? `${parentPath}/${entry.name}` : entry.name;
addUploadTask(entry, relativePath);
} else if (entry.isDirectory) {
const dirReader = entry.createReader();
dirReader.readEntries((newEntries) => {
parseFolder(newEntries, parentPath ? `${parentPath}/${entry.name}` : entry.name);
});
}
}
};
// Chrome/Firefox使用webkitGetAsEntry
if (files[0].webkitGetAsEntry) {
const entry = files[0].webkitGetAsEntry();
if (entry.isDirectory) {
const dirReader = entry.createReader();
dirReader.readEntries(parseFolder);
} else {
addUploadTask(files[0], files[0].name);
}
}
// IE8使用ActiveX(代码省略)
};
// 添加上传任务
const addUploadTask = (file, relativePath) => {
const task = {
file,
relativePath,
md5: '',
chunkCount: Math.ceil(file.size / uploader.value.options.chunkSize)
};
// 计算文件MD5(用于断点校验)
calculateFileMD5(file, (md5) => {
task.md5 = md5;
taskList.value.push(task);
saveTaskToLocal(task);
});
};
onMounted(() => {
initUploader();
document.getElementById('folderInput').addEventListener('change', handleFolderSelect);
});
return { taskList, uploader };
}
};
后端:JSP分片处理与COS上传
// /api/upload-chunk.jsp
<%@ page import="com.qcloud.cos.COSClient, com.qcloud.cos.model.*" %>
<%@ page import="java.util.UUID, java.io.*" %>
<%
// 1. 获取参数
String taskId = request.getParameter("taskId");
int chunkIndex = Integer.parseInt(request.getParameter("chunkIndex"));
int totalChunks = Integer.parseInt(request.getParameter("totalChunks"));
String fileMd5 = request.getParameter("fileMd5");
String relativePath = request.getParameter("relativePath");
// 2. 保存分片
String tempDir = application.getRealPath("/") + "/temp/" + taskId;
File dir = new File(tempDir);
if (!dir.exists()) dir.mkdirs();
Part filePart = request.getPart("file");
String chunkPath = tempDir + "/chunk_" + chunkIndex;
filePart.write(chunkPath);
// 3. 更新数据库
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/file_transfer", "user", "pass");
PreparedStatement stmt = conn.prepareStatement(
"UPDATE upload_tasks SET uploaded_chunks=? WHERE task_id=?")) {
stmt.setInt(1, chunkIndex + 1);
stmt.setString(2, taskId);
stmt.executeUpdate();
}
// 4. 检查是否全部上传完成
boolean isComplete = false;
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/file_transfer", "user", "pass");
ResultSet rs = conn.createStatement().executeQuery(
"SELECT uploaded_chunks, total_chunks FROM upload_tasks WHERE task_id='" + taskId + "'")) {
if (rs.next()) {
isComplete = rs.getInt("uploaded_chunks") >= rs.getInt("total_chunks");
}
}
// 5. 合并并上传COS
if (isComplete) {
String finalPath = tempDir + "/final_" + fileMd5;
mergeChunks(tempDir, finalPath, totalChunks); // 合并逻辑省略
// 腾讯云COS上传
COSClient cosClient = new COSClient("secretId", "secretKey", "region");
String cosKey = "uploads/" + taskId + "/" + relativePath;
cosClient.putObject(new PutObjectRequest("your-bucket", cosKey, new File(finalPath)));
// 清理临时文件
deleteDirectory(new File(tempDir));
}
out.print("{\"status\":\"success\"}");
%>
关键问题解决
-
IE8文件夹上传:
- 使用``配合ActiveX的
FileSystemObject递归读取 - 需用户手动设置IE安全级别为"低"(客户要求,已文档说明)
- 使用``配合ActiveX的
-
腾讯云COS分片校验:
- 上传前通过
COSObject的doesObjectExist方法检查分片是否已存在
- 上传前通过
-
20G文件内存优化:
- 采用流式处理,避免一次性加载整个文件到内存
- JSP设置
request.setAttribute("maxPostSize", "21474836480")(20GB)
求助与社区支持
目前IE8的ActiveX方案在Windows 10上存在权限问题,已在QQ群(374992201)发布详细错误日志。完整代码已上传至Gitee(待脱敏),急需大神协助解决:
- ActiveX控件在Win10的兼容性问题
- 大文件合并时的内存溢出优化
- 跨浏览器进度条同步显示
明日计划:编写自动重试机制和COS上传失败后的回滚逻辑,确保20G文件传输的稳定性。
(日记结束)
附:技术栈对比表
| 模块 | 原方案 | 当前方案 | 优化点 |
|---|---|---|---|
| 前端框架 | jQuery | Vue3 Composition API | 响应式数据管理 |
| 上传组件 | WebUploader基础版 | 定制版(支持文件夹+断点) | 递归解析文件夹结构 |
| 后端语言 | PHP | JSP | 利用Servlet处理大文件流 |
| 对象存储 | 百度OBS | 腾讯云COS | 适配不同的分片API规范 |
如需完整项目或调试协助,请联系QQ群或留言获取测试账号!
导入项目
导入到Eclipse:点南查看教程
导入到IDEA:点击查看教程
springboot统一配置:点击查看教程
工程

NOSQL
NOSQL示例不需要任何配置,可以直接访问测试

创建数据表
选择对应的数据表脚本,这里以SQL为例


修改数据库连接信息

访问页面进行测试

文件存储路径
up6/upload/年/月/日/guid/filename


效果预览
文件上传

文件刷新续传
支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传

文件夹上传
支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。

768

被折叠的 条评论
为什么被折叠?



