攻克ZIP文件上传难题:从前端解析到云端分段存储全攻略

304 阅读8分钟

背景:大文件上传项目需求剖析

事情是这么回事,上次说完前端大文件批量下载并做成 ZIP 包深入使用 streamSaver 做前端多文件大文件 ZIP,这次又遇到了一个新需求,需要将 ZIP 文件分段上传到公有云上,但是上传前需要先读取一下 ZIP 文件中的一个 json 文件,这个文件里包含了一些配置信息

调研:探寻最优上传路径

因为使用的是公有云的对象存储,所以文件最好不要经过后端而是由前端直接对接到对象存储的上传接口,但是考虑到文件可能很大,比如 4G 以上,所以需要分段上传,又因为需要读取里面的某些配置文件信息,所以需要先将文件在前端做读取,读取完成后再上传

拆解:细化任务颗粒度,步步为营

  • 读取文件里的配置信息
  • 根据配置信息请求后端接口,返回分段上传的 URL 和签名信息
  • 前端根据分段上传的 URL 和签名信息,分段上传文件
  • 前端根据后端返回的分段上传状态,判断是否上传完成,中途失败的支持重试
  • 全部完成上传后告诉后端合并文件
  • aws 等一些安全策略的配置和文件的生命周期管理

跨源资源共享(CORS):打通前端跨域访问”

以 aws 为例,跨域的配置如下,没这个的话,前端直接使用 aws 的 url 无法访问

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["PUT", "POST", "GET"],
    "AllowedOrigins": ["*"],
    "ExposeHeaders": ["ETag", "x-amz-meta-custom-header"]
  }
]

代码:技术落地,从后端到前端

后端生成分段上传的 url 和签名信息

分段上传的 url 和签名信息,需要先请求后端接口,后端返回给前端,这部分每个语言都有实现,参考资料也比较多,也可以用 AI 生成代码,下面是分段的部分,至于合并的部分也用 ai 自己生成吧

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3"
)

func main() {
    // 创建一个新的会话
    sess, err := session.NewSession(&aws.Config{
        Region: aws.String("your-region")},
    )
    if err != nil {
        log.Fatalf("Failed to create session, %v", err)
    }

    http.HandleFunc("/generate-presigned-url-for-multipart-upload", func(w http.ResponseWriter, r *http.Request) {
        svc := s3.New(sess)

        // 初始化分块上传
        initResp, err := svc.CreateMultipartUpload(&s3.CreateMultipartUploadInput{
            Bucket: aws.String("your-bucket-name"),
            Key:    aws.String("path/to/your/object"), // 可以根据需要动态设置
        })
        if err != nil {
            log.Printf("Failed to initialize multipart upload, %v", err)
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            return
        }

        // 生成预签名URL列表
        var urls []string
        for partNumber := 1; partNumber <= 10; partNumber++ { // 假设最多有10个部分
            req, _ := svc.UploadPartRequest(&s3.UploadPartInput{
                Bucket:     aws.String("your-bucket-name"),
                Key:        aws.String("path/to/your/object"),
                UploadId:   initResp.UploadId,
                PartNumber: aws.Int64(int64(partNumber)),
            })

            urlStr, err := req.Presign(15 * time.Minute) // URL有效期为15分钟
            if err != nil {
                log.Printf("Failed to generate presigned URL, %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
                return
            }
            urls = append(urls, urlStr)
        }

        // 返回初始化响应和预签名URL列表
        response := map[string]interface{}{
            "upload_id": initResp.UploadId,
            "urls":      urls,
        }
        json.NewEncoder(w).Encode(response)
    })

    log.Println("Starting server on :8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatalf("Server failed: %v", err)
    }
}

前端读取 zip 文件

前端读取 ZIP 文件中的某个文件的信息, 这个需要花时间调试和查询资料了

import JSZip from 'jszip';

JSZip.loadAsync(file.raw).then(function (zip) {
  for (let key in zip.files) {
    // 循环遍历文件夹下的文件
    if (!zip.files[key].dir) {
      /**
       * 可以在层级中检测自己想要的文本类型
       * 替换即可:如 docx  jpg png pdf 等
       */
      //  if (/\.(json)$/.test(zip.files[key].name)) {
      if (zip.files[key].name == 'config.json') {
        var base = zip.file(zip.files[key].name).async('string');
        // uint8array base64 string
        // 可以选择想要的类型输出,比如解图用base64,文本string等
        base.then(res => {
          // res 是文件里的内容,下面可以对内容进行操作
          let versionObj = (res && JSON.parse(res)) || {};
        });
      }
    }
  }
});

上面的代码是 ai 生成的,在处理小文件的时候可以用到,但是如何上传的是大文件JSZip.loadAsync会有问题,所以 jszip 这个库不好完成此需求,调研一下,选择了这个库zip.js/zip.js,测试下来 GB 级别的数据都能正常解析

import { BlobReader, ZipReader } from '@zip.js/zip.js';

// 使用ZipReader 流式读取ZIP文件
const readFileFromZip = async (file: File) => {
  const blob = file.slice();
  const zipReader = new ZipReader(new BlobReader(blob));
  const textStream = new TransformStream();
  const textTextPromise = new Response(textStream.readable).text();

  for (const entry of await zipReader.getEntries()) {
    if (entry.filename === 'config.json') {
      await entry.getData(textStream.writable);
      const text = await textTextPromise;
      await zipReader.close();
      return text;
    }
  }
  await zipReader.close();
};

前端分段上传和进度控制

前端根据分段上传的 URL 和签名信息,分段上传文件,参考资料也比较多,也可以用 AI 生成代码

<template>
  <div>
    <input type="file" @change="onFileSelected" />
    <button @click="startUpload">Upload</button>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      file: null,
      chunkSize: 1024 * 1024, // 每个分块大小为1MB
      uploadId: '',
      presignedUrls: [],
      parts: []
    };
  },
  methods: {
    onFileSelected(event) {
      this.file = event.target.files[0];
    },
    async startUpload() {
      try {
        const response = await axios.get('http://localhost:8080/generate-presigned-url-for-multipart-upload');
        this.uploadId = response.data.upload_id;
        this.presignedUrls = response.data.urls;

        const totalChunks = Math.ceil(this.file.size / this.chunkSize);
        for (let i = 0; i < totalChunks; i++) {
          const start = i * this.chunkSize;
          const end = Math.min(start + this.chunkSize, this.file.size);
          const chunk = this.file.slice(start, end);

          const formData = new FormData();
          formData.append('file', chunk);

          const config = {
            headers: {
              'Content-Type': this.file.type
            }
          };

          const res = await axios.put(this.presignedUrls[i], formData, config);
          this.parts.push({ ETag: res.headers['etag'], PartNumber: i + 1 });
        }

        // 完成分块上传
        await this.completeMultipartUpload();
      } catch (error) {
        console.error('Error uploading file:', error);
      }
    },
    async completeMultipartUpload() {
      try {
        const completedParts = this.parts.map(part => ({
          ETag: part.ETag,
          PartNumber: part.PartNumber
        }));

        const completeResp = await axios.post(`http://localhost:8080/complete-multipart-upload`, {
          bucket: 'your-bucket-name',
          key: 'path/to/your/object',
          uploadId: this.uploadId,
          parts: completedParts
        });

        if (completeResp.status === 200) {
          console.log('File uploaded successfully');
        } else {
          console.error('Failed to complete multipart upload:', completeResp.statusText);
        }
      } catch (error) {
        console.error('Error completing multipart upload:', error);
      }
    }
  }
};
</script>

上面的 ai 生成代码只是基础示例,后续还有加上并行上传和进度控制, p-limit作为并发控制的依赖库挺好的,uploadSegment 函数中做片段的上传和进度控制和上传状态控制等细节,下面是部分代码

// 按url分片上传
const uploadPromises = urls.map((url, i) => {
  const start = i * uploadInfo.chunkDefaultSize;
  const end = Math.min(file.size, start + uploadInfo.chunkDefaultSize);
  // start 记录分段的起点,end记录分段的终点,percent记录分段上传的百分比, url为分段上传的url, 这些信息记录下来才能方便重试时使用
  progress.value[i] = { index: i, percent: 0, start: start, end: end, url: url, size: end - start, status: 'waiting' };
  // 返回一个函数,而不是直接调用 uploadSegment
  return () => uploadSegment(url, file.slice(start, end), i);
});

// 控制并发数量
const maxConcurrentUploads = 3;
const limit = pLimit(maxConcurrentUploads);

const results = await Promise.allSettled(
  uploadPromises.map(uploadFn =>
    limit(async () => {
      try {
        const result = await uploadFn();
        return result;
      } catch (error) {
        return false;
      }
    }),
  ),
);

总结:复盘得失,闲聊 AI

这样的案例比较少见,一般不推荐上传时读取 zip 文件中的信息,当然也有些场景,需要预览 zip 中的文件列表什么的,这时候可以用 zip.js 来做,开发中依赖了很多 ai 生成的代码, 主要使用了阿里和字节的 vscode ai 插件,不过代码的可用性和准确读还是比较低,需要自己做些很多调整,尤其是前端的代码,本想试试 Cursor 和 windsurf 来做这个需求的,时间紧任务重,下次再来吧

然后在写这个博文时想试试各家的文章润色和扩写,预期有了提示词: 你是一个专业的程序工程师,有着丰富的开发经验,下面是一个工作记录,现在请你帮我把文章润色和扩写一下,要求内容严谨,用词准确,尽量让读者愿意读下去并读得懂, 然后看了下,还是算了吧,语言的通胀并不是我希望看到的,不过文章结构的整理和框架搭建还是挺好用的,当然我也不希望别人拿我的文字用 ai 洗稿...

来感受一下 AI 起的标题,如果有你喜欢的请在评论区留言吧:

以下几个标题可供选择,各有侧重,凸显了项目关键要素、技术难点或成果亮点:

  1. 《破局大文件上传难题:前端读取 + 分段上传公有云实战》:直接点明核心业务场景是大文件上传,强调“前端读取”与“分段上传”两大关键技术动作,“破局”一词彰显攻克难题的不易,引发读者对实战过程的好奇。
  2. 《攻克 ZIP 文件上传瓶颈:从前端解析到云端分段存储全攻略》:突出文件类型为 ZIP,“攻克瓶颈”吸引眼球,暗示过程艰辛;“全攻略”则涵盖了从前端处理到云端存储各环节,让读者知晓能获取系统性解决方案。
  3. 《公有云大文件分段上传的前端技术攻坚与后端协同作战》:点明公有云存储背景,突出前端在技术实现上需攻坚诸多难题,同时提及后端,凸显前后端协同配合对于项目成功的重要性,契合技术团队分工合作情境。
  4. 《当大文件遇上公有云:解锁 ZIP 分段上传与配置读取新姿势》:语言较为活泼新颖,“遇上”拟人化表述拉近与读者距离,“解锁新姿势”勾起读者探索欲,聚焦大文件在公有云场景的独特上传及配置读取方式。
  5. 《实战解析:前端主导的 ZIP 文件云端分段上传及关键配置提取》:“实战解析”让技术人员倍感亲切,点明前端主导,强调了文件上传及提取配置信息两大实操板块,适合想深入了解实操细节的读者。
  6. 《大文件云端征程:前端巧读 ZIP 配置,分段上传步步为营》:“云端征程”赋予项目宏大叙事感,“巧读”突出前端技术巧思,“步步为营”体现上传流程严谨、有条不紊,契合项目特点。

对了, 这里推荐一下我前几天写的一个 ai 解梦的小程序,解梦, 算是凝结了我对解梦的心得,还添加了绘梦的功能,欢迎尝试!

参考: 他山之石