概要
大文件上传、切片上传、断点续传
整体架构流程
后端:技术express、express-fileupload、fs、path等
导入所需的模块:express、express-fileupload、fs、path等。
创建Express应用程序实例。
设置CORS(跨域资源共享)允许跨域访问。
设置静态资源访问,将public文件夹作为静态资源目录。
使用express.json()中间件解析请求体中的JSON数据。
使用express-fileupload中间件处理文件上传。
定义文件上传的路由,包括上传切片文件和合并切片文件的接口。
在上传切片文件的接口中,根据切片文件的名称创建临时存储目录,并将切片文件保存到对应目录中。如果文件已存在,则返回文件已存在的提示。
在合并切片文件的接口中,读取所有切片文件,按照切片文件名中的序号排序,将切片文件内容追加到合并文件中,最后删除切片文件所在的目录。
在查询已上传切片文件的接口中,根据传入的hash值读取对应目录下的所有切片文件,按照序号排序后返回给客户端。
前端:基于Vue和Element UI的大文件断点续传功能。主要的组件是el-upload和el-progress。
在data中定义了一些需要用到的数据,包括上传进度percentage、文件列表fileList、已上传的文件列表already等。
handleRemove、handlePreview、handleExceed、beforeRemove等方法是对上传组件进行操作的回调函数。
handleUpload方法是处理文件上传的逻辑。首先将文件加入fileList,并检查是否已经存在相同hash值的文件,如果有则从localStorage中读取之前的上传进度。然后根据文件大小和指定的chunkSize将文件切片成多个chunk,分别上传每个chunk。在上传过程中通过onUploadProgress回调更新当前的上传进度。
handlePause方法是暂停上传的操作,将isPaused设置为true,同时设置uploading为false以停止上传。
handleResume方法是恢复上传的操作,将isPaused设置为false,如果之前有上传过的文件且上传进度不是100%,则从上次中断的地方继续上传;否则从fileList中最后一个文件开始上传。
clearFiles方法用于清空文件列表和上传进度,并清空localStorage中的数据。
内联代码片
- 后台
let path = "";
//创建存储大文件的临时目录
let [, HASH] = /^([^_]+)_(\d+)/.exec(filename);
path = `${uploadDir}/${HASH}`;
!fs.existsSync(path) ? fs.mkdirSync(path) : null;
path = `${uploadDir}/${HASH}/${filename}`;
let isExists = await exists(path);
if (isExists) {
res.send({
code: 0,
msg: "文件已存在",
});
return;
}
fs.writeFile(path, file.data, async function (err) {
console.log('-------写文件------------');
if (err) {
res.send({ code: 0, msg: "failure" });
} else {
res.send({ code: 1, msg: "success" });
}
});
} catch (error) {
res.send({ code: 0, msg: "failure" });
}
//读取所有切片文件
const { hash } = req.query;
let path = `${uploadDir}/${hash}`;
let suffix;
const chunksName = await fs.readdirSync(`${uploadDir}/${hash}`);
//进行排序
chunksName.sort((a, b) => {
let reg = /_(\d+)/;
return reg.exec(a)[1] - reg.exec(b)[1];
});
chunksName.forEach((item) => {
!suffix ? (suffix = /\.([0-9a-zA-Z]+)$/.exec(item)[1]) : null;
fs.appendFileSync(
`${uploadDir}/${hash}.${suffix}`,
fs.readFileSync(`${path}/${item}`)
);
fs.unlinkSync(`${path}/${item}`);
});
//删除分片所在的文件夹
fs.rmdirSync(path);
res.send({
code: 200,
msg: "上传完毕",
});
const { hash } = req.query;
let path = `${uploadDir}/${hash}`;
let fileList = [];
console.log('path', path);
try {
let chunksName = await fs.readdirSync(path);
console.log('chunksName before', chunksName);
//进行排序
chunksName.sort((a, b) => {
let reg = /_(\d+)/;
return reg.exec(a)[1] - reg.exec(b)[1];
});
console.log('chunksName afeter', chunksName);
res.send({
code: 1,
msg: "",
fileList: chunksName,
});
} catch (error) {
res.send({
code: 0,
msg: "",
fileList,
});
}
- 前端代码
data() {
return {
percentage: 0, // 上传进度
fileList: [], // 文件列表
already: [], // 已上传文件列表
isPaused: false, // 是否暂停上传
uploading: false, // 是否正在上传
currentFile: null, // 当前上传的文件
currentProgress: 0 // 当前上传的进度
}
},
this.uploading = true; // 开始上传
const { name, size } = file;
// 读取文件内容
const content = await read(file);
// 使用SparkMD5库计算文件内容的哈希值
let spark = new SparkMD5.ArrayBuffer();
spark.append(content);
const hash = spark.end();
// 检查本地存储是否有相同哈希值的文件已经上传过
if (localStorage.getItem(`${hash}`)) {
this.percentage = ~~localStorage.getItem(`${hash}`);
}
// 检查是否已经上传过一些片段
const result = await axios.get("http://127.0.0.1:3000/api/upload_already", { params: { hash } });
this.already = result.data.fileList;
const chunkSize = 1 * 1024 * 1024;
const chunkList = [];
const chunkListLength = Math.ceil(size / chunkSize);
const suffix = /\.([0-9A-z]+)$/.exec(name)[1];
let index = 0;
// 将文件切片成多个chunk
while (index < chunkListLength) {
chunkList.push({
chunk: file.slice(index * chunkSize, (index + 1) * chunkSize),
fileName: `${hash}_${index + 1}.${suffix}`
});
index++;
}
let count = 0;
for (let i = 0; i < chunkListLength; i++) {
if (this.isPaused) {
break;
}
let item = chunkList[i];
// 如果该chunk已经在已上传的部分文件列表中,则跳过该chunk继续上传下一个
if (this.already.length > 0 && this.already.includes(item.fileName)) {
count++;
this.percentage = Math.floor((count / chunkListLength) * 100);
continue;
}
const formData = new FormData();
formData.append("file", item.chunk);
formData.append("filename", item.fileName);
// 发送POST请求上传chunk,并监听上传进度
await axios.post("http://127.0.0.1:3000/api/upload_chunk", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress: (progressEvent) => {
this.currentFile = file;
this.currentProgress = Math.floor(((i * chunkSize + progressEvent.loaded) / size) * 100);
this.percentage = this.currentProgress;
},
});
count++;
this.percentage = Math.floor((count / chunkListLength) * 100);
localStorage.setItem(`${hash}`, this.percentage.toString());
if (count === chunkListLength) {
// 完成所有chunk的上传后,发送请求进行文件合并
await axios.get("http://127.0.0.1:3000/api/upload_merge", { params: { hash } });
MessageBox({
type: "success",
message: "上传成功!",
});
this.percentage = 0
this.uploading = false;
return;
}
}
}
this.isPaused = true;
// 停止上传
this.uploading = false;
项目地址:https://gitee.com/wang_fan123/bigFile-upload
技术细节
演示视频
小结
代码比较简单,存在些许bug,欢迎指出共同进步