Nest 实现断点续传
目前来说 只要页面不刷新.断点续传能实现.页面要是刷新了.chunks 里面全没了
注意
断点续传 = 单图上传 + 大文件分片上传
前端
html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>大文件上传</title>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
<script src="./upload.js"></script>
</head>
<body>
<input id="fileInput" type="file" />
<button onclick="pause()">暂停</button>
<button onclick="jixu()">继续</button>
<script>
const fileInput = document.querySelector("#fileInput");
// 封装上传方法
fileInput.addEventListener("change", () => {
uploadfile();
});
</script>
</body>
</html>upload.js
js
// 把文件切割
const publicqiege = async () => {
const chunkSize = 1024 * 20 * 1024; //10M
const file = fileInput.files[0] || sessionStorage.getItem("fileSize"); // 获取文件
// 分片上传
let chunks = [];
// 开始位置
let startPos = 0;
// 循环分片
while (startPos < file.size) {
chunks.push(file.slice(startPos, startPos + chunkSize));
startPos += chunkSize;
}
sessionStorage.setItem("fileSize", JSON.stringify(file.size));
sessionStorage.setItem("filename", file.name);
return chunks;
};
// 暂停方法
const pause = async () => {
axios.cancle();
};
// 上传方法
const uploadfile = async () => {
const randomStr = Math.random().toString().slice(2, 8);
const chunks = await publicqiege();
let names = randomStr + "_" + sessionStorage.getItem("filename");
sessionStorage.setItem("upload_name", names);
// 循环任务
const tasks = [];
chunks.map((chunk, index) => {
const data = new FormData();
data.set(
"name",
randomStr + "_" + sessionStorage.getItem("filename") + "-" + index
);
data.append("files", chunk);
tasks.push(axios.post("http://localhost:5000/user/upload", data));
});
await Promise.all(tasks);
// axios.get(
// "http://localhost:5000/user/merge?name=" + randomStr + "_" + file.name
// );
};
/* 继续 */
const jixu = async () => {
// 少的分片汇集
let upload_arr = [];
// 少的分片序号
let upload_index = [];
// 获取到名字
let name = sessionStorage.getItem("upload_name");
// 获取到初始分割
const chunks = await publicqiege();
// 后端查询到是否缺少分片
const { data } = await axios.get(
"http://localhost:5000/user/checkchunks?name=" +
name +
"&" +
"chunkTotal=" +
chunks.length
);
console.log(data);
if (data.code == 200) {
if (data.data.uploadStatus == "uploading") {
data.data.chunkSignsArr.forEach((item, index) => {
if (item == 0) {
upload_arr.push(chunks[index]);
upload_index.push(index);
}
});
}
uploadFile(upload_arr, upload_index);
}
/* 单独上传 */
function uploadFile(upload_arr, upload_index) {
console.log(upload_arr);
console.log(upload_index);
const files = upload_arr;
let count = 0;
// 添加其他字段,例如文件名
// 遍历所有选中的文件,添加到FormData中
let formData_arr = [];
for (let i = 0; i < files.length; i++) {
const formData = new FormData();
formData.append(
"modulename",
sessionStorage.getItem("upload_name") + "$" + upload_index[i]
);
formData.append("file", files[i]);
formData_arr.push(formData);
}
formData_arr.forEach((formData) => {
// 使用fetch API发送数据到服务器
fetch("http://localhost:5000/user/uploadless", {
// 替换为你的服务器端点URL
method: "POST",
body: formData,
})
.then((response) => response.json()) // 假设服务器返回JSON格式的响应
.then((data) => {
if (data.code == 200) {
console.log("上传成功");
count++;
if (count == files.length - 1) {
axios.get(
"http://localhost:5000/user/merge?name=" +
sessionStorage.getItem("upload_name")
);
}
} else {
console.log(data);
console.log("上传失败");
}
})
.catch((error) => {
console.error("Error:", error);
});
});
}
};Nest
安装依赖
bash
npm install --save @nestjs/platform-express multer
npm install -D @types/multer
pnpm add @types/multer -D先写单图上传
- utils/storage.js
ts
import * as multer from "multer";
import * as path from "path";
import * as fs from "fs";
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const name = req.body.modulename.split("$");
const dir = path.join(__dirname, "../../", `uploads/chunks_${name[0]}`);
cb(null, dir);
},
filename: (req, file, cb) => {
const name = req.body.modulename.split("$");
let resultname = name[0] + "-" + name[1];
return cb(null, `${resultname}`);
},
});
/**
* @param { delPath:String } (需要删除文件的地址)
* @param { direct:Boolean } (是否需要处理地址)
*/
const deleteFile = (delPath, direct) => {
delPath = direct ? delPath : path.join(__dirname, delPath);
try {
/**
* @des 判断文件或文件夹是否存在
*/
if (fs.existsSync(delPath)) {
console.log(delPath);
fs.unlinkSync(delPath);
} else {
console.log("inexistence path:", delPath);
}
} catch (error) {
console.log("del error", error);
}
};
export { storage, deleteFile };控制器
upload 是主接口.分片使用.当他突然断了
checkchunks 是告诉你缺少哪个分片
uploadless 是单独上传分片用的
merge 合并接口
ts
import {
Controller,
Post,
Body,
UploadedFiles,
UseInterceptors,
Get,
Query,
Res,
} from "@nestjs/common";
import { UserService } from "./user.service";
import * as fs from "fs";
import { FilesInterceptor } from "@nestjs/platform-express";
import type { Response } from "express";
import { storage } from "../../utils/storage";
@Controller("user")
export class UserController {
constructor(private readonly userService: UserService) {}
// 上传接口
@Post("upload")
@UseInterceptors(
FilesInterceptor("files", 20, {
dest: "uploads",
})
)
uploadFiles(
@UploadedFiles() files: Array<Express.Multer.File>,
@Body() body
) {
console.log("body", body);
console.log("files", files);
// 将分片移动到单独的目录
const fileName = body.name.match(/(.+)\-\d+$/)[1];
const chunkDir = "uploads/chunks_" + fileName;
if (!fs.existsSync(chunkDir)) {
fs.mkdirSync(chunkDir);
}
fs.cpSync(files[0].path, chunkDir + "/" + body.name);
fs.rmSync(files[0].path);
return {
data: "分片上传成功",
};
}
// 检查接口
/* 检查已上传文件或者切片 */
@Get("checkchunks")
checkChunks(
@Query("name") name: string,
@Query("chunkTotal") chunkTotal: string
) {
// 获取切片文件夹、或文件
const vo: any = {
uploadStatus: "empty",
chunkSignsArr: [], // 例子:[1,0,1,0] 表示,第 2、4 块切片没有上传
};
const chunkDir = "uploads/chunks_" + name;
let directory;
if (fs.existsSync(chunkDir)) {
directory = fs.readdirSync(chunkDir);
const chunkSignsArr = new Array<number>(Number(chunkTotal)).fill(0); // 一上来全填充0
// 有文件夹,说明切片未完全上传,正序返回切片排序 (断点续传)
if (directory?.length > 0) {
directory.map((file) => {
const idx = Number(file.split("-").at(-1)); // 取出最后的序号,分割号
chunkSignsArr[idx] = 1;
});
if (chunkSignsArr.includes(0)) {
// 包含0 证明有缺陷
vo.uploadStatus = "uploading";
} else {
vo.uploadStatus = "success"; // 不包含证明都完事了
}
}
vo.chunkSignsArr = chunkSignsArr;
}
return {
data: vo,
};
}
// 合并接口
// 合并接口post
@Get("merge")
merge(@Query("name") name: string, @Res() res: Response) {
console.log(name);
const chunkDir = "uploads/chunks_" + name;
const files = fs.readdirSync(chunkDir);
files.sort((f1: string, f2: string) => {
const idx1 = Number(f1.split("-").at(-1));
const idx2 = Number(f2.split("-").at(-1));
return idx1 < idx2 ? -1 : idx1 > idx2 ? 1 : 0;
});
console.log(files);
// 定义开始位置
let startPos = 0;
files.forEach((file, index) => {
const filePath = chunkDir + "/" + file;
const outputFilePath = "uploads/" + name.split("_").at(1);
const readStream = fs.createReadStream(filePath);
const writeStream = fs.createWriteStream(outputFilePath, {
start: startPos,
});
readStream.pipe(writeStream).on("finish", () => {
if (index === files.length - 1) {
fs.rm(chunkDir, { recursive: true }, () => {
console.log("合并完成333");
res.send({ data: "合并完成", code: 200 });
});
}
});
startPos += fs.statSync(filePath).size;
});
}
// 单独上传接口
// 上传
@Post("uploadless")
@UseInterceptors(
// 独立自己的
FilesInterceptor("file", 10, {
storage: storage,
})
)
async upload(@Body() body: any, files: Array<Express.Multer.File>) {
return {
data: {
message: "上传成功",
},
};
}
}