大文件上传

为什么会有大文件上传

前景:

       随着信息技术的飞速发展,各行业的数字化程度越来越高,产生的数据量呈爆发式增长。例如医疗领域的医学影像数据,如 CT、MRI 等图像文件,单个文件大小可能达到数百 MB 甚至数 GB;科研领域的实验数据,像大型强子对撞机产生的海量数据,一次实验的数据量就可能高达数 TB。这些大量的、不断增长的数据需要通过大文件上传的方式存储到更安全、更便于管理的服务器或云端。

      多媒体内容的创作和传播日益广泛,高清视频、高分辨率图像和大型音频文件等越来越常见。比如一部 4K 高清电影的大小可能在几十 GB 以上,专业摄影师拍摄的 RAW 格式照片单张也可能达到几十 MB。为了在网络上分享、存储这些多媒体内容,大文件上传功能就显得尤为重要。

大文件上传有什么作用

数据共享与传输:方便团队成员共享大文件  无需物理设备,即可快速将大文件发送给不同地区的用户

   数据备份与存储:将大文件上传至云端,防止本地设备故障导致的数据丢失 适合需要长期保存的大文件

   内容分发:媒体公司可通过上传大文件发布高清视频、音频等内容   开发者可通过上传大文件发布软件更新或补丁

    数据分析与处理:上传大文件至云平台,便于进行大数据分析 上传大文件用于训练机器学习模型,提升模型性能

   远程访问与管理:支持远程访问和管理大文件,提升工作效率 实现多设备间的文件同步,便于随时随地访问

   用户体验:上传大文件可提供高清视频、图片等,提升用户体验  避免压缩导致的质量损失,保持文件完整性

大文件上传步骤

  1. 文件分块 :按照大小固定分成多个小块 每块都有一个唯一的标识 (哈希值)用于区分不同的块
     const createChunks = (file: File): Chunk[] => {
          let cur: number = 0;
          let chunks: Chunk[] = [];
          while (cur < file.size) {
              const end = Math.min(cur + CHUNK_SIZE, file.size);  // 修复越界问题
             //将文件分成多个块
              const blob: Chunk = file.slice(cur, end);
              chunks.push(blob);
              cur = end;  // 使用结束位置作为新起点
          }
          return chunks;
      };

  2. 计算哈希值:每个文件都有一个唯一的哈希值用于标识不同文件, 哈希可以判断文件是否已上传
     const calculateHash = (chunks: Chunk[]): Promise<string> => {
          return new Promise((resolve) => {
              const targets: Chunk[] = [];
              const spark = new sparkMd5.ArrayBuffer();
              //读取内容并计算哈希值
              const fileReader = new FileReader();
              chunks.forEach((chunk: Chunk, index: number) => {
                  if (index === 0 || index === chunks.length - 1) {
                      targets.push(chunk);
                  } else {
                      targets.push(chunk.slice(0, 2));
                      targets.push(chunk.slice(CHUNK_SIZE / 2, CHUNK_SIZE / 2 + 2));
                      // 修正这里的范围,避免超出边界
                      targets.push(chunk.slice(CHUNK_SIZE - 2, CHUNK_SIZE));
                  }
              });
              fileReader.readAsArrayBuffer(new Blob(targets));
              fileReader.onload = (e: ProgressEvent<FileReader>) => {
                  spark.append(e.target?.result as ArrayBuffer);
                  resolve(spark.end());
              };
          });
      };

  3. 分块上传:将每块单独上传到服务器 每块上传时携带文件的哈希值和索引(时间不一样按照时间排序)服务器根据这些存储起来
     
     //前端上传文件块
    const uploadChunks = async (chunks: Chunk[], existChunks: string[]) => {
          const data = chunks.map((chunk: Chunk, index: number) => {
              return {
                  FileHash: FileHash.current,
                  chunkHash: FileHash.current + "_" + index,
                  chunk
              };
          });
    
    
    
    
    //后端
    router.post("/upload", async (req, res) => {
        const form = new multiparty.Form();
        form.parse(req, async (err, fields, files) => {
            if (err) {
                res.status(401).json({
                    ok: false,
                    msg: "上传失败"
                });
                return;
            }
            const fileHash = fields.FileHash[0];
            const chunkHash = fields.chunkHash[0];
            const chunkPath = path.resolve(UPLOAD_DIR, fileHash);
            if (!fse.existsSync(chunkPath)) {
                await fse.mkdirs(chunkPath);
            }
            const oldPath = files.chunk[0].path;
            await fse.move(oldPath, path.resolve(chunkPath, chunkHash));
            res.status(200).json({
                ok: true,
                msg: "上传成功"
            });
        });
    });
    
    

  4. 存储块:接收文件后 存储在临时目录中
     
    //用于构建上传的表单
    const formDatas = data.filter((item) => !existChunks?.includes(item.chunkHash)).map((item) => {
        const formData = new FormData();
        formData.append('FileHash', item.FileHash);
        formData.append('chunkHash', item.chunkHash);
        formData.append('chunk', item.chunk);
        return formData;
    });
    
    const max: number = 6;
    let i: number = 0;
    const taskPool: Promise<Response>[] = [];
    
    while (i < formDatas.length) {
        const task = fetch("http://localhost:3000/upload", {
            method: 'POST',
            body: formDatas[i]
        }).then(res => {
            if (!res.ok) throw new Error(`上传失败: ${res.status}`);
            return res;
        });
    
        taskPool.push(task);
        i++;
        updateProgress(i, formDatas.length);  // 更新上传进度
    
        if (taskPool.length === max) {
          //控制上传的数量
            await Promise.race(taskPool).finally(() => {
                taskPool.splice(0, taskPool.findIndex(t => t === Promise.race(taskPool)));
            });
        }
    }
    await Promise.all(taskPool);
    // 通知后端合并文件
    mergeRequest();
    };

     
  5. 合并文件:所有的块完成后 前端会通知服务器合并  根据索引顺序将所有块合并成和一个完整的文件

     
    // 通知后端合并文件的函数
    const mergeRequest = async () => {
        return fetch("http://localhost:3000/merge", {
            method: 'POST',
            headers: { "content-type": "application/json" },
            body: JSON.stringify({
              //区分不同的文件
                FileHash: FileHash.current,
                FileName: FileName.current,
                //切片大小
                size: CHUNK_SIZE
            })
        })
           .then((res) => {
                alert("合并成功");
                return res;
            });
    };
    
    
    //后端
    router.post("/merge", async (req, res) => {
        const { FileHash, FileName, size } = req.body;
    
        // 如果已经存在该文件,就没必要合并了  合并后的文件路径
        const filePath = path.resolve(UPLOAD_DIR, FileHash + extraExt(FileName));
        if (fse.existsSync(filePath)) {
            res.status(200).json({
                ok: true,
                msg: "文件已存在"
            });
            return;
        }
        // 如果不存在该文件,才去合并 文件存储目录
        const chunkDir = path.resolve(UPLOAD_DIR, FileHash);
        if (!fse.existsSync(chunkDir)) {
            res.status(401).json({
                ok: false,
                msg: "文件不存在"
            });
            return;
        }
        // 合并操作 并排序
        let chunkPaths = await fse.readdir(chunkDir);
        chunkPaths.sort((a, b) => Number(a.split("_")[1]) - Number(b.split("_")[1]));
        const list = chunkPaths.map((chunkName, index) => {
            return new Promise((resolve) => {
                //读取文件并写入到合并后的文件路径中
                const chunkPath = path.resolve(chunkDir, chunkName);
                const readStream = fse.createReadStream(chunkPath);
                const writeStream = fse.createWriteStream(filePath, {
                    start: index * size,
                    end: (index + 1) * size
                });
    
                readStream.on("end", async () => {
                    await fse.unlink(chunkPath);
                    resolve();
                });
    
                readStream.pipe(writeStream);
            });
        });
    
        await Promise.all(list);
    
        //删除临时文件快目录
        await fse.remove(chunkDir);
    
        res.status(200).json({
            ok: true,
            msg: "上传成功"
        });
    });

     
  6. 断点续传:在上传前发送一个验证 检查文件是否已上传或部分上传 服务器返回已上传的列表 前端只上传未完成的块
     
    // 验证文件是否已存在的函数
      
    //返回验证结果
      const verify = async (): Promise<VerifyResponse> => {
        //发送验证
          return fetch("http://localhost:3000/verify", {
              method: 'POST',
              headers: { "content-type": "application/json" },
              body: JSON.stringify({
                  FileHash: FileHash.current,
                  FileName: FileName.current,
              })
          })
             .then((res) => res.json())
             .then((res) => res as VerifyResponse);
      };
    //后端
    router.post("/verify", async (req, res) => {
        const { FileHash, FileName } = req.body;
        //文件是否存在
        const filePath = path.resolve(UPLOAD_DIR, FileHash + extraExt(FileName));
        //文件快是否存在
    
        const chunkDir = path.resolve(UPLOAD_DIR, FileHash);
        //获取已上传的文件快列表
        let chunkPaths = [];
    
        if (fse.existsSync(chunkDir)) {
            chunkPaths = await fse.readdir(chunkDir);
        }
    
        if (fse.existsSync(filePath)) {
            res.status(200).json({
                ok: true,
                data: {
                    shouldUpload: false
                }
            });
        } else {
            res.status(200).json({
                ok: true,
                data: {
                    shouldUpload: true,
                    existChunks: chunkPaths
                }
            });
        }
    });

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值