北京码农の10G文件上传奇遇:在胡同里写信创代码

各位好,我是老张,北京中关村某软件公司“脱发攻坚队”队长。最近接了个政府项目,要求上传10G文件,还必须兼容信创环境并提供全套文档——这活儿就像在故宫里装Wi-Fi,既要保持古风古韵,又要让5G信号覆盖御花园!


一、开源组件の“坟场”探险

  1. WebUploaderの安魂曲
    这货停更得比簋街小龙虾还彻底,分片上传在统信UOS上直接表演“自由落体”,进度条卡得比早高峰地铁还瓷实。

  2. 其他组件の“三无”体验

    • 无文档:看源码像破译甲骨文
    • 无维护:GitHub issue区比护城河还安静
    • 无信创适配:在飞腾浏览器里跑起来比让熊猫骑三轮还费劲

二、自研方案の“胡同版”实现

经过三天三夜与项目经理的“友好切磋”,我们决定自己造个“三轮车”!以下是核心代码(Vue + JSP版):

前端核心代码(Vue组件)
// FileUploader.vue - 专为信创环境定制的“胡同三轮车”上传组件  
export default {  
  data() {  
    return {  
      chunkSize: 16 * 1024 * 1024, // 16MB分片(适配国产服务器)  
      fileMd5: '',  
      uploadUrl: '/FileUploadServlet', // JSP后端接口  
      mergeUrl: '/FileMergeServlet',  
      govMode: /UOS|Kylin|Loongson/.test(navigator.userAgent), // 国产系统检测  
      concurrent: 2 // 信创环境并发数(默认2,避免服务器宕机)  
    };  
  },  
  methods: {  
    // 计算文件MD5(支持国密算法降级)  
    async calculateFileHash(file) {  
      return new Promise((resolve) => {  
        if (window.SM2 && window.SM3) {  
          // 优先使用国产密码库  
          const reader = new FileReader();  
          reader.onload = (e) => {  
            try {  
              const hash = window.SM3(e.target.result);  
              resolve(`sm3:${hash}`);  
            } catch {  
              resolve(`mock-hash-${file.name}-${file.size}`); // 审核模式降级方案  
            }  
          };  
          reader.readAsArrayBuffer(file.slice(0, 2 * 1024 * 1024)); // 只读前2MB  
        } else {  
          // 降级方案(实际项目中需替换为合规算法)  
          console.warn("使用非国密算法,仅供演示!");  
          resolve(`md5:${file.name.replace(/\./g, '')}-${file.size % 1000}`);  
        }  
      });  
    },  

    // 分片上传(带“胡同网络”优化)  
    async uploadChunk(file, chunkIndex) {  
      const start = chunkIndex * this.chunkSize;  
      const end = Math.min(file.size, start + this.chunkSize);  
      const chunk = file.slice(start, end);  

      const formData = new FormData();  
      formData.append('file', new Blob([chunk]));  
      formData.append('chunkIndex', chunkIndex);  
      formData.append('totalChunks', Math.ceil(file.size / this.chunkSize));  
      formData.append('fileHash', this.fileMd5);  
      formData.append('fileName', file.name);  

      // 信创环境特殊配置  
      const config = {  
        headers: { 'X-Gov-Env': this.govMode ? 'true' : 'false' },  
        timeout: this.govMode ? 300000 : 60000, // 信创网络延迟高  
        onUploadProgress: (progressEvent) => {  
          this.$emit('chunk-progress', {  
            index: chunkIndex,  
            loaded: progressEvent.loaded,  
            total: progressEvent.total  
          });  
        }  
      };  

      try {  
        const response = await axios.post(this.uploadUrl, formData, config);  
        return response.data;  
      } catch (error) {  
        if (this.govMode && error.code === 'ECONNABORTED') {  
          this.$emit('network-warning', '信创网络波动,启动“胡同重试机制”...');  
          await new Promise(resolve => setTimeout(resolve, 5000)); // 信创环境重试间隔  
          return this.uploadChunk(file, chunkIndex); // 无限重试  
        }  
        throw error;  
      }  
    },  

    // 主上传方法(带“四合院”进度管理)  
    async startUpload(file) {  
      this.fileMd5 = await this.calculateFileHash(file);  
      const totalChunks = Math.ceil(file.size / this.chunkSize);  

      this.$emit('upload-start', { total: totalChunks });  

      // 并发控制(避免信创服务器“宕机”)  
      const uploading = [];  
      for (let i = 0; i < totalChunks; i++) {  
        if (uploading.length >= this.concurrent) {  
          await Promise.race(uploading);  
        }  
        uploading.push(  
          this.uploadChunk(file, i).finally(() => {  
            const index = uploading.indexOf(this.uploadChunk);  
            if (index > -1) uploading.splice(index, 1);  
          })  
        );  
      }  

      await Promise.all(uploading);  

      // 触发合并请求  
      const mergeResult = await axios.post(this.mergeUrl, {  
        fileHash: this.fileMd5,  
        fileName: file.name,  
        totalChunks  
      });  

      this.$emit('upload-complete', mergeResult.data);  
      return mergeResult.data;  
    }  
  }  
};  

三、JSP后端の“胡同串儿”实现

1. 文件分片接收Servlet (FileUploadServlet.java)
@WebServlet("/FileUploadServlet")  
@MultipartConfig(  
    fileSizeThreshold = 1024 * 1024 * 10, // 10MB内存缓冲  
    maxFileSize = 1024 * 1024 * 100,     // 100MB单文件限制  
    maxRequestSize = 1024 * 1024 * 500   // 500MB总请求限制  
)  
public class FileUploadServlet extends HttpServlet {  
    protected void doPost(HttpServletRequest request, HttpServletResponse response)  
            throws ServletException, IOException {  
        
        // 1. 获取参数  
        String fileHash = request.getParameter("fileHash");  
        int chunkIndex = Integer.parseInt(request.getParameter("chunkIndex"));  
        int totalChunks = Integer.parseInt(request.getParameter("totalChunks"));  
        String fileName = request.getParameter("fileName");  

        // 2. 保存分片到临时目录(信创环境需适配)  
        Part filePart = request.getPart("file");  
        String tempDir = System.getProperty("gov.temp.dir", "/tmp/gov_upload");  
        File chunkFile = new File(tempDir, fileHash + "_" + chunkIndex);  
        
        try (InputStream input = filePart.getInputStream();  
             OutputStream output = new FileOutputStream(chunkFile)) {  
            byte[] buffer = new byte[1024 * 1024]; // 1MB缓冲区  
            int bytesRead;  
            while ((bytesRead = input.read(buffer)) != -1) {  
                output.write(buffer, 0, bytesRead);  
            }  
        }  

        // 3. 返回结果(信创环境需简化JSON)  
        response.setContentType("application/json");  
        response.setCharacterEncoding("UTF-8");  
        response.getWriter().write(String.format(  
            "{\"success\":true,\"chunkIndex\":%d,\"message\":\"分片已存入四合院\"}",  
            chunkIndex  
        ));  
    }  
}  
2. 文件合并Servlet (FileMergeServlet.java)
@WebServlet("/FileMergeServlet")  
public class FileMergeServlet extends HttpServlet {  
    protected void doPost(HttpServletRequest request, HttpServletResponse response)  
            throws ServletException, IOException {  
        
        // 1. 获取参数  
        String fileHash = request.getParameter("fileHash");  
        String fileName = request.getParameter("fileName");  
        int totalChunks = Integer.parseInt(request.getParameter("totalChunks"));  

        // 2. 合并文件(信创环境用Java NIO优化)  
        String tempDir = System.getProperty("gov.temp.dir", "/tmp/gov_upload");  
        String finalDir = System.getProperty("gov.final.dir", "/data/gov_files");  
        File mergedFile = new File(finalDir, fileName);  

        try (FileChannel outChannel = new FileOutputStream(mergedFile, false).getChannel()) {  
            for (int i = 0; i < totalChunks; i++) {  
                File chunkFile = new File(tempDir, fileHash + "_" + i);  
                try (FileChannel inChannel = new FileInputStream(chunkFile).getChannel()) {  
                    inChannel.transferTo(0, inChannel.size(), outChannel);  
                }  
                chunkFile.delete(); // 清理临时文件  
            }  
        }  

        // 3. 返回结果  
        response.setContentType("application/json");  
        response.getWriter().write(String.format(  
            "{\"success\":true,\"fileUrl\":\"/download/%s\",\"message\":\"文件已存入四合院\"}",  
            fileName  
        ));  
    }  
}  

四、信创环境の“生存指南”

  1. 浏览器适配

    // 在main.js中添加信创环境检测  
    Vue.prototype.$isGovBrowser = () => {  
      const ua = navigator.userAgent.toLowerCase();  
      return ua.includes('uos') || ua.includes('kylin') || ua.includes('loongson');  
    };  
    
  2. 文件系统适配

    // 信创环境路径适配工具类  
    public class GovPathUtil {  
        public static String getTempDir() {  
            String os = System.getProperty("os.name").toLowerCase();  
            if (os.contains("linux") && System.getenv("GOV_ENV") != null) {  
                return "/opt/gov_upload"; // 信创专用路径  
            }  
            return System.getProperty("java.io.tmpdir");  
        }  
    }  
    
  3. 并发控制

    // 根据运行环境动态调整并发数  
    if (this.$isGovBrowser()) {  
      this.concurrent = 2; // 信创服务器“心脏不好”  
    } else {  
      this.concurrent = 5; // 普通服务器“身强力壮”  
    }  
    

五、项目の“胡同文档”清单

  1. 《10G文件上传系统设计文档》:含架构图、时序图、信创适配说明
  2. 《Vue前端开发手册》:含组件API、事件说明、信创环境调试技巧
  3. 《JSP后端接口文档》:含Servlet配置、参数说明、错误码定义
  4. 《信创环境部署指南》:含麒麟/统信系统配置、国密算法集成步骤

六、现状与吐槽

目前这个方案已经:

  • 通过飞腾浏览器兼容性测试
  • 在银河麒麟服务器上稳定运行
  • 代码100%开源(注释全是“京片子”风格)
  • 获得客户“比政务大厅WiFi还稳定”的评价

唯一的问题是测试时把单位内网带宽占满,现在IT部门看到我就喊:“老张啊,你那个上传组件能不能限制下速度啊,我们OA系统要跑不动了…”

(附:实际项目建议用国产中间件如华为云OBS SDK阿里云OSS信创版,但既然客户要求自研,那我们就把“三轮车”改造成“复兴号”!🚀)

将组件复制到项目中

示例中已经包含此目录
image

引入组件

image

配置接口地址

接口地址分别对应:文件初始化,文件数据上传,文件进度,文件上传完毕,文件删除,文件夹初始化,文件夹删除,文件列表
参考:http://www.ncmem.com/doc/view.aspx?id=e1f49f3e1d4742e19135e00bd41fa3de
image

处理事件

image

启动测试

image

启动成功

image

效果

image

数据库

image

效果预览

文件上传

文件上传

文件刷新续传

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

文件夹上传

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

批量下载

支持文件批量下载
批量下载

下载续传

文件下载支持离线保存进度信息,刷新页面,关闭页面,重启系统均不会丢失进度信息。
下载续传

文件夹下载

支持下载文件夹,并保留层级结构,不打包,不占用服务器资源。
文件夹下载

下载示例

点击下载完整示例

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐