大文件上传切片+断点+秒传vue3+express

前端

<script setup lang="ts">
import { ref } from "vue";
import SparkMD5 from "spark-md5";
 
const CHUNK_SIZE = 1024 * 1024; // 1MB
const fileName = ref(""); // 文件名称
const fileHash = ref(""); // 文件hash
 
// 创建文件分片
const createChunks = (file: File) => {
  let start = 0;
  const chunks = [];
  while (start < file.size) {
    chunks.push(file.slice(start, start + CHUNK_SIZE));
    start += CHUNK_SIZE;
  }
  return chunks;
};
 
// 计算文件内容hash值
const calculateHash = (file: File): Promise<string> => {
  return new Promise((resolve) => {
    const fileReader = new FileReader();
    fileReader.readAsArrayBuffer(file);
    fileReader.onload = function (e) {
      const spark = new SparkMD5.ArrayBuffer();
      spark.append((e.target as FileReader).result as ArrayBuffer);
      resolve(spark.end());
    };
  });
};
 
// 控制请求并发
const concurRequest = (
  taskPool: Array<() => Promise<Response>>,
  max: number
): Promise<Array<Response | unknown>> => {
  return new Promise((resolve) => {
    if (taskPool.length === 0) {
      resolve([]);
      return;
    }
 
    const results: Array<Response | unknown> = [];
    let index = 0;
    let count = 0;
 
    const request = async () => {
      if (index === taskPool.length) return;
      const i = index;
      const task = taskPool[index];
      index++;
      try {
        results[i] = await task();
      } catch (err) {
        results[i] = err;
      } finally {
        count++;
        if (count === taskPool.length) {
          resolve(results);
        }
        request();
      }
    };
 
    const times = Math.min(max, taskPool.length);
    for (let i = 0; i < times; i++) {
      request();
    }
  });
};
 
// 合并分片请求
const mergeRequest = () => {
  fetch("http://localhost:3000/merge", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      fileHash: fileHash.value,
      fileName: fileName.value,
    }),
  });
};
 
// 上传文件分片
const uploadChunks = async (
  chunks: Array<Blob>,
  existChunks: Array<string>
) => {
  const formDatas = chunks
    .map((chunk, index) => ({
      fileHash: fileHash.value,
      chunkHash: fileHash.value + "-" + index,
      chunk,
    }))
    .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 taskPool = formDatas.map(
    (formData) => () =>
      fetch("http://localhost:3000/upload", {
        method: "POST",
        body: formData,
      })
  );
 
  // 控制请求并发
  await concurRequest(taskPool, 6);
 
  // 合并分片请求
  mergeRequest();
};
 
// 校验文件、文件分片是否存在
const verify = (fileHash: string, fileName: string) => {
  return fetch("http://localhost:3000/verify", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      fileHash,
      fileName,
    }),
  }).then((res) => res.json());
};
 
// 绑定上传事件
const handleUpload = async (e: Event) => {
  const file = (e.target as HTMLInputElement).files?.[0];
  if (!file) return;
 
  fileName.value = file.name;
 
  // 创建文件分片
  const chunks = createChunks(file);
 
  // 计算文件内容hash值
  fileHash.value = await calculateHash(file);
 
  // 校验文件、文件分片是否存在
  const verifyRes = await verify(fileHash.value, fileName.value);
  const { existFile, existChunks } = verifyRes.data;
  if (existFile) return;
 
  // 上传文件分片
  uploadChunks(chunks, existChunks);
};
</script>
 
<template>
  <h1>大文件上传</h1>
  <input type="file" @change="handleUpload" />
</template>

后端

const express = require('express');
const router = express.Router();
const fse = require("fs-extra");
const path = require("path");
const multipart = require("connect-multiparty");
const multipartMiddleware = multipart();


// 所有上传的文件存放在该目录下
const UPLOADS_DIR = path.resolve("uploads");

// 接收文件分片
router.post("/upload", multipartMiddleware, (req, res) => {
    const { fileHash, chunkHash } = req.body;

    // 如果临时文件夹(用于保存分片)不存在,则创建
    const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
    if (!fse.existsSync(chunkDir)) {
        fse.mkdirSync(chunkDir);
    }

    // 如果临时文件夹里不存在该分片,则将用户上传的分片移到临时文件夹里
    const chunkPath = path.resolve(chunkDir, chunkHash);
    if (!fse.existsSync(chunkPath)) {
        fse.moveSync(req.files.chunk.path, chunkPath);
    }

    res.send({
        success: true,
        msg: "上传成功",
    });
});


// 合并文件分片
router.post("/merge", async (req, res) => {
    const { fileHash, fileName } = req.body;

    // 最终合并的文件路径
    const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
    // 临时文件夹路径
    const chunkDir = path.resolve(UPLOADS_DIR, fileHash);

    // 读取临时文件夹,获取该文件夹下“所有文件(分片)名称”的数组对象
    const chunkPaths = fse.readdirSync(chunkDir);

    // 读取临时文件夹获得的文件(分片)名称数组可能乱序,需要重新排序
    chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);

    // 遍历文件(分片)数组,将分片追加到文件中
    const pool = chunkPaths.map(
        (chunkName) =>
            new Promise((resolve) => {
                const chunkPath = path.resolve(chunkDir, chunkName);
                // 将分片追加到文件中
                fse.appendFileSync(filePath, fse.readFileSync(chunkPath));
                // 删除分片
                fse.unlinkSync(chunkPath);
                resolve();
            })
    );
    await Promise.all(pool);
    // 等待所有分片追加到文件后,删除临时文件夹

    fse.removeSync(chunkDir);

    res.send({
        success: true,
        msg: "合并成功",
    });
});





router.post("/verify", (req, res) => {
    const { fileHash, fileName } = req.body;

    // 判断服务器上是否存在该hash值的文件
    const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
    const existFile = fse.existsSync(filePath);

    // 获取已经上传到服务器的文件分片
    const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
    const existChunks = [];
    if (fse.existsSync(chunkDir)) {
        existChunks.push(...fse.readdirSync(chunkDir));
    }

    res.send({
        success: true,
        msg: "校验文件",
        data: {
            existFile,
            existChunks,
        },
    });
});



module.exports = router;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值