大文件分块上传

断点续传

断点续传需要为每个分块加md5值,如果用户取消上传,可以知道那些分块已经上传了

切块上传

只要校验整个文件的完整性就好

前端代码示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>大文件上传</title>
    <style>
      body {
        padding: 24px;
      }

      button {
        padding: 8px 16px;
        cursor: pointer;
        border: 1px solid #409eff;
        border-radius: 4px;
        background-color: #409eff;
        color: #fff;
        margin-right: 16px;
      }

      .progress {
        width: 100%;
        height: 24px;
        background-color: #ebeef5;
        border-radius: 12px;
        margin-bottom: 24px;
        overflow: hidden;
      }
      .progress-inner {
        width: 0%;
        height: 100%;
        background-color: #67c23a;
        border-radius: 12px;
        text-align: right;
        transition: width 0.6s ease;
      }
      .progress-text {
        color: #fff;
        margin: 0 5px;
        display: inline-block;
        font-size: 12px;
      }
    </style>
  </head>

  <body>
    <!-- 进度条 -->
    <div class="progress">
      <div class="progress-inner">
        <div class="progress-text"></div>
      </div>
    </div>
    <!-- 上传完显示预览地址 -->
    <p>
      <a id="filelink"></a>
    </p>
    <!-- 操作按钮 -->
    <button id="upload">
      <input type="file" id="file" style="display: none" />
      上传文件
    </button>
    <button id="download">下载文件</button>

    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="https://unpkg.com/spark-md5@3.0.2/spark-md5.js"></script>
    <script>
      // 上传
      document.querySelector("#upload").addEventListener("click", () => {
        document.querySelector("#file").click();
      });
      // 选中文件直接上传
      document.querySelector("#file").addEventListener("change", async (e) => {
        const file = e.target.files[0];
        // 获取文件的所有分块
        const blobList = getFileSlices(file);
        const uploadRequests = [];
        // 所有分块的md5值
        // 后端根据md5值存的块,请求合并根据md5值进行合并
        const md5ChunkList = [];
        let totalUploaded = 0;

        for (let i = 0; i < blobList.length; i++) {
          const blob = blobList[i];

          const md5 = await calculateMD5(blob);
          md5ChunkList.push(md5);

          const formData = new FormData();
          formData.append("file", blob);
          formData.append("md5", md5);

          const request = axios({
            method: "post",
            url: "http://localhost:3000/upload",
            data: formData,
            onUploadProgress: (progressEvent) => {
              totalUploaded += progressEvent.loaded;
              let percentCompleted = Math.floor(
                (totalUploaded / file.size) * 100
              );
              // 为合并留1%进度条
              if (percentCompleted >= 100) {
                percentCompleted = 99;
              }
              updateProgressBar(percentCompleted);
            },
          });
          uploadRequests.push(request);
        }

        try {
          // 等待所有分块上传完成,再请求合并
          await Promise.all(uploadRequests);
          const { data } = await axios({
            method: "post",
            url: "http://localhost:3000/merge",
            data: { md5: md5ChunkList, fileName: file.name },
          });

          updateProgressBar(100);
          const url = `http://localhost:3000${data.data}`;
          console.log(url);

          const a = document.querySelector("#filelink");
          a.href = url;
          a.target = "_blank";
          a.innerText = url;
        } catch (e) {
          console.error(e);
        }
      });

      // 获取文件分块
      // 默认分块大小5m
      function getFileSlices(file, segmentSize = 5 * 1024 * 1024) {
        const fileSlices = [];
        let offset = 0;
        while (offset < file.size) {
          const segment = file.slice(offset, offset + segmentSize);
          fileSlices.push(segment);
          offset += segmentSize;
        }
        return fileSlices;
      }

      // 计算分块的md5值
      function calculateMD5(blob) {
        return new Promise((resolve, reject) => {
          const fileReader = new FileReader();
          let spark = new SparkMD5.ArrayBuffer();

          fileReader.onload = (e) => {
            spark.append(e.target.result);
            resolve(spark.end());
          };

          fileReader.onerror = () => {
            reject("blob读取失败");
          };

          fileReader.readAsArrayBuffer(blob);
        });
      }

      function updateProgressBar(percent) {
        const width = percent + "%";
        document.querySelector(".progress-inner").style.width = width;
        document.querySelector(".progress-text").innerText = width;
      }

      // 下载
      document.querySelector("#download").addEventListener("click", () => {
        const url = document.querySelector("#filelink").innerText;
        const fileName = url.substring(url.lastIndexOf("/") + 1);

        const download = document.createElement("a");
        download.href = `http://localhost:3000/download/${fileName}`;
        download.download = fileName;
        download.target = "_blank";
        download.click();
      });
    </script>
  </body>
</html>

后端代码示例

import express from "express";
import cors from "cors";
import multer from "multer";
import fs from "fs";
import path from "path";
import mime from "mime-types";

const app = express();
const port = 3000;
const upload = multer({ dest: "uploads/" });

// 处理跨域请求
app.use(cors());
// 解析客户端请求中的JSON数据,解析后的对象将被添加到req.body
app.use(express.json());
// 启用对静态文件的服务
app.use("/uploads", express.static("uploads"));

app.get("/", (req, res) => {
  res.send("Hello World!");
});

// 上传
app.post("/upload", upload.single("file"), (req, res) => {
  const file = req.file;
  const md5 = req.body.md5;
  const newPath = path.join(path.dirname(file.path), md5);
  fs.renameSync(file.path, newPath);
  res.send({ code: 200 });
});

// 合并
app.post("/merge", async (req, res) => {
  const { md5, fileName } = req.body;
  await concatFiles(md5, fileName);
  res.send({ code: 200, msg: "合并成功", data: `/uploads/${fileName}` });
});

// 下载文件流
app.get("/download/:fileName", (req, res) => {
  const { fileName } = req.params;
  const file = path.join("./uploads", fileName);
  const type = mime.lookup(file);

  res.setHeader(
    "Content-disposition",
    "attachment; filename=" + encodeURIComponent(path.basename(file))
  );
  res.setHeader("Content-type", type);

  const filestream = fs.createReadStream(file);
  filestream.pipe(res);
});

app.listen(port, () => {
  console.log(`listening on port ${port}`);
});

// 合并切片的文件
async function concatFiles(fileChunks, name) {
  const filePath = path.join("./uploads", name);
  const writeStream = fs.createWriteStream(filePath);
  for (let i = 0; i < fileChunks.length; i++) {
    const chunkPath = path.join("./uploads", fileChunks[i]);
    const file = fs.createReadStream(chunkPath);
    file.pipe(writeStream, { end: false });
    // 等待当前文件读完再进行下一个
    await new Promise((resolve) => file.on("end", resolve));
    fs.unlink(chunkPath, (e) => {
      if (e) {
        console.error("删除文件切片失败", e);
      }
    });
  }
  writeStream.end();
}

参考

面试官:如何实现大文件上传
大文件分块上传

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值