Upload 大文件分片上传
介绍
大文件分片上传具有以下特点:
- 将大文件分割成多个小文件,减少内存占用
- 采用串行上传方式
- 一次上传一个分片
- 上传完成后继续下一个
- 直到所有分片上传完成
- 支持断点续传
- 上传失败时可以继续上传
- 只需重新上传失败的分片
基本使用
XUpload 需要提供了文件上传、文件校验(用来判断是否已经上传过了)、文件合并的请求接口,属性分别为:uploadUrl、verifyUrl、mergeUrl
0%
设置切片大小
可以通过chunkSize属性来设置切片大小,单位为MB,默认为1MB。chunkSize = 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 -ynpm 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"