Skip to content

Upload 大文件分片上传

介绍

大文件分片上传具有以下特点:

  1. 将大文件分割成多个小文件,减少内存占用
  2. 采用串行上传方式
    • 一次上传一个分片
    • 上传完成后继续下一个
    • 直到所有分片上传完成
  3. 支持断点续传
    • 上传失败时可以继续上传
    • 只需重新上传失败的分片

基本使用

XUpload 需要提供了文件上传、文件校验(用来判断是否已经上传过了)、文件合并的请求接口,属性分别为:uploadUrlverifyUrlmergeUrl

0%

设置切片大小

可以通过chunkSize属性来设置切片大小,单位为MB,默认为1MBchunkSize = 1则表示1MB

0%

设置请求头

可以通过headers属性来设置请求头,支持动态设置

0%

设置请求body参数

可以通过formData属性来设置请求参数,支持动态设置

0%

手动上传

可以通过manualUpload属性来设置是否手动上传,默认为false,即自动上传。设置为true则会出现一个上传按钮,点击后会触发upload事件

0%

事件

事件名说明回调参数
success文件上传成功时触发(result: object) 服务器返回的结果
error上传过程中发生错误时触发(error: Error) 错误信息
beforeUpload文件上传之前触发-
afterUpload文件上传完成后触发-
changeFile选择文件后触发({ file: File, fileHash: string }) 选择的文件和文件哈希值
pause暂停上传时触发-
resume继续上传时触发-

后端测试代码

这里提供了一个简单的后端测试代码,使用 express 框架,用来测试大文件分片上传。

  • npm init -y
  • npm i express multer cors nodemon
  • 在package.json中的script中添加
js
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon index.js"
  },
  • 根目录下新建index.js 复制以下代码
js
const express = require("express");
const multer = require("multer");
const cors = require("cors");
const fs = require("fs");
const path = require("path");

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

// 中间件配置
app.use(cors());
app.use(express.json());

// 创建必要的目录
const UPLOAD_DIR = path.resolve(__dirname, "uploads");
const CHUNKS_DIR = path.resolve(UPLOAD_DIR, "chunks");
const FILES_DIR = path.resolve(UPLOAD_DIR, "files");

[UPLOAD_DIR, CHUNKS_DIR, FILES_DIR].forEach((dir) => {
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
});

// 检查文件是否已存在(秒传)
app.post("/upload/verify", async (req, res) => {
  try {
    const { fileHash, fileName } = req.body;
    const ext = fileName.split(".").pop();
    const filePath = path.resolve(FILES_DIR, `${fileHash}.${ext}`);

    if (fs.existsSync(filePath)) {
      res.json({
        code: 0,
        message: "文件已存在",
        exists: true,
        url: `/files/${fileHash}.${ext}`,
      });
    } else {
      // 检查是否有未完成的分片上传
      const chunkDir = path.resolve(CHUNKS_DIR, fileHash);
      let uploadedChunks = [];

      if (fs.existsSync(chunkDir)) {
        uploadedChunks = fs.readdirSync(chunkDir).map((name) => parseInt(name));
      }

      res.json({
        code: 0,
        message: "文件不存在",
        exists: false,
        uploadedChunks,
      });
    }
  } catch (error) {
    res.status(500).json({
      code: 1,
      message: "验证失败",
      error: error.message,
    });
  }
});

// 处理分片上传
app.post("/upload/chunk", upload.single("file"), async (req, res) => {
  try {
    const { fileName, fileHash, chunkIndex } = req.body;
    const chunkDir = path.resolve(CHUNKS_DIR, fileHash);

    // 创建存储分片的目录
    if (!fs.existsSync(chunkDir)) {
      fs.mkdirSync(chunkDir);
    }

    // 移动分片文件
    const chunkPath = path.resolve(chunkDir, chunkIndex);
    fs.renameSync(req.file.path, chunkPath);

    res.json({
      code: 0,
      message: "分片上传成功",
    });
  } catch (error) {
    res.status(500).json({
      code: 1,
      message: "分片上传失败",
      error: error.message,
    });
  }
});

// 合并文件分片
app.post("/upload/merge", async (req, res) => {
  try {
    const { fileName, fileHash, totalChunks } = req.body;
    const chunkDir = path.resolve(CHUNKS_DIR, fileHash);
    const ext = fileName.split(".").pop();
    const filePath = path.resolve(FILES_DIR, `${fileHash}.${ext}`);

    // 创建写入流
    const writeStream = fs.createWriteStream(filePath);

    // 按顺序合并分片
    for (let i = 0; i < totalChunks; i++) {
      const chunkPath = path.resolve(chunkDir, i.toString());
      if (fs.existsSync(chunkPath)) {
        const chunkData = fs.readFileSync(chunkPath);
        writeStream.write(chunkData);
        // 删除分片文件
        fs.unlinkSync(chunkPath);
      }
    }

    writeStream.end();

    // 删除分片目录
    if (fs.existsSync(chunkDir)) {
      fs.rmdirSync(chunkDir);
    }

    res.json({
      code: 0,
      message: "文件合并成功",
      url: `/files/${fileHash}.${ext}`,
    });
  } catch (error) {
    res.status(500).json({
      code: 1,
      message: "文件合并失败",
      error: error.message,
    });
  }
});

// 提供文件访问服务
app.use("/files", express.static(FILES_DIR));

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`);
});
  • npm run dev 启动服务
  • 对应的接口地址
uploadUrl="http://localhost:3000/upload/chunk"
verifyUrl="http://localhost:3000/upload/verify"
mergeUrl="http://localhost:3000/upload/merge"