JSP组件中如何实现文件夹上传的开源与自定义解决方案?

开发者日记:2023年11月20日 周一 晴
项目名称:跨平台大文件传输系统(WebUploader+Vue3+JSP+腾讯云COS)


项目背景与核心挑战

近期承接了一个高难度外包项目,客户要求实现20G级文件/文件夹上传下载,需满足以下硬性条件:

  1. 断点续传:即使重启电脑,进度不能丢失(需持久化存储)
  2. 文件夹层级保留:上传后需100%还原原始目录结构
  3. 全浏览器兼容:从IE8到现代浏览器(Edge/Chrome/Firefox/Safari/Opera)
  4. 技术栈: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)
接收分片
是否首片
创建数据库任务记录
更新分片进度
存储分片到临时目录
是否全部分片完成
合并文件并上传COS
返回继续上传指令
关键数据库设计
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\"}");
%>

关键问题解决

  1. IE8文件夹上传

    • 使用``配合ActiveX的FileSystemObject递归读取
    • 需用户手动设置IE安全级别为"低"(客户要求,已文档说明)
  2. 腾讯云COS分片校验

    • 上传前通过COSObjectdoesObjectExist方法检查分片是否已存在
  3. 20G文件内存优化

    • 采用流式处理,避免一次性加载整个文件到内存
    • JSP设置request.setAttribute("maxPostSize", "21474836480")(20GB)

求助与社区支持

目前IE8的ActiveX方案在Windows 10上存在权限问题,已在QQ群(374992201)发布详细错误日志。完整代码已上传至Gitee(待脱敏),急需大神协助解决:

  1. ActiveX控件在Win10的兼容性问题
  2. 大文件合并时的内存溢出优化
  3. 跨浏览器进度条同步显示

明日计划:编写自动重试机制和COS上传失败后的回滚逻辑,确保20G文件传输的稳定性。


(日记结束)

附:技术栈对比表

模块原方案当前方案优化点
前端框架jQueryVue3 Composition API响应式数据管理
上传组件WebUploader基础版定制版(支持文件夹+断点)递归解析文件夹结构
后端语言PHPJSP利用Servlet处理大文件流
对象存储百度OBS腾讯云COS适配不同的分片API规范

如需完整项目或调试协助,请联系QQ群或留言获取测试账号!

导入项目

导入到Eclipse:点南查看教程
导入到IDEA:点击查看教程
springboot统一配置:点击查看教程

工程

image

NOSQL

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

创建数据表

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

修改数据库连接信息

image

访问页面进行测试

image

文件存储路径

up6/upload/年/月/日/guid/filename
image
image

效果预览

文件上传

文件上传

文件刷新续传

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

文件夹上传

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

下载示例

点击下载完整示例

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值