一、过程分析
一、要实现断点续传我们需要拿到文件的hash值,因为只有这样我们才能完成一个识别,当我们的文件名发生改变时,他的文件hash也不会改变,文件名重复的文件的hash也不相同。
- 前端我们用
spark-md5这个包去生成文件的hash npm install --save spark-md5(GitHub - satazor/js-spark-md5: Lightning fast normal and incremental md5 for javascript)- 先安装好后备用
二、nest后端
- 前端需要发送一次get请求获取该文件是否存在,如果存在则获取目录的列表生成一个列表信息并返回,不存在则需要创建一个文件目录用于存储切片信息。
- 然后post发送切片,后端存储到刚才创建的文件目录
- 发送完成后发送合并请求,将文件合并。(需要携带一个合并后缀名)
一、先在main的bootstrap下创建一个images目录
//创建images目录
try {
mkdirSync(join(__dirname, 'images'));
} catch (error) {}
二、在service下创建三个api接口。切片名字(前端传入)规范需要一个自增的文件名字,以便断点和合并需要。
getDirectory(hash: string) {
//判断目录是否存在
try {
mkdirSync(join(__dirname, `../images/${hash}`));
return {
message: '不存在',
files: [],
};
} catch (e) {
//获取目录下文件,用于断点续传和判断文件是否已经完成
const files = readdirSync(join(__dirname, `../images/${hash}`));
return {
message: '存在',
files,
};
}
}
//上传切片
uploadFile(hash: string, myfilename: string, file: any) {
const ws = createWriteStream(
join(__dirname, `../images/${myfilename}`, `${hash}`),
);
ws.write(file.buffer);
return true;
}
//合并切片
merge(hash: string, count: number, suffix: string) {
const Filebuffers: Buffer[] = [];
for (let i = 1; i < count; i++) {
const buffer = readFileSync(
join(__dirname, `../images/${hash}`, `${hash}_${i}`),
);
Filebuffers.push(buffer);
}
const buffer = Buffer.concat(Filebuffers);
const ws = createWriteStream(
join(__dirname, `../images`, `${hash}.${suffix}`),
);
ws.write(buffer);
return '合并成功';
}
三、在controller下创建三个接口。nest上传文件文档
//判断目录是否存在
@Get('album')
getDirectory(@Query() query) {
return this.uploadService.getDirectory(query.hash);
}
//上传文件,参考官网文档
@Post('album')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file, @Body() body) {
// console.log(body);
const message = this.uploadService.uploadFile(
body.hash,
body.myfilename,
file,
);
return message;
}
//切片合并
@Post('merge')
merge(@Body() Body) {
// console.log(query);
return this.uploadService.merge(Body.hash, Body.count, Body.suffix);
}
三、前端,前端只需要切片和上传就可以
一、我们用刚才安装的spark-md5包去生成文件的唯一hash
import SparkMD5 from "spark-md5";
/**
* @description: 拿到文件的hash值
* @Author: hfunteam
* @param {*} file 文件,File类型
* @return {
* buffer: 文件的二进制流
* hash: 生成的文件hash值
* suffix:文件后缀名
* filename: 文件名称 hash
* }
*/
const changeBuffer: any = function (file: File) {
return new Promise((resolve) => {
let fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = (ev) => {
let buffer: ArrayBuffer = ev.target!.result as ArrayBuffer,
spark = new SparkMD5.ArrayBuffer(),
suffix;
spark.append(buffer);
// 拿到文件的hash值
let hash = spark.end();
// 匹配文件后缀名
suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)![1];
resolve({
buffer,
hash,
suffix,
filename: `${hash}`,
});
};
});
};
前端完整代码
<template>
<div>上传</div>
<input type="file" ref="input" @change="upload" />
<div style="width: 100%; border: 1px black solid">
<div id="progress" style="background-color: red; width: 0">
{{ (progressindex / mycount) * 100 }}%
</div>
</div>
</template>
<script setup lang="ts">
import SparkMD5 from "spark-md5";
import axios from "axios";
import { getCurrentInstance, ref } from "vue";
const instance = getCurrentInstance();
let progressindex = ref(0);
let mycount = ref(1);
/**
* @description: 拿到文件的hash值
* @Author: hfunteam
* @param {*} file 文件,File类型
* @return {
* buffer: 文件的二进制流
* hash: 生成的文件hash值
* suffix:文件后缀名
* filename: 文件名称 hash
* }
*/
const changeBuffer: any = function (file: File) {
return new Promise((resolve) => {
let fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = (ev) => {
let buffer: ArrayBuffer = ev.target!.result as ArrayBuffer,
spark = new SparkMD5.ArrayBuffer(),
suffix;
spark.append(buffer);
// 拿到文件的hash值
let hash = spark.end();
// 匹配文件后缀名
suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)![1];
resolve({
buffer,
hash,
suffix,
filename: `${hash}`,
});
};
});
};
const upload = async function () {
let ln = 0;
let files: any[] = [];
let file = (instance?.proxy?.$refs.input as any).files[0];
console.log(file);
// 获取文件的HASH
let alreadyUploadChunks: any[] = [], // 当前已经上传的切片
{ hash, suffix, filename } = await changeBuffer(file);
// 获取已经上传的切片信息
let Ifexits: any = await axios.get("/upload/album", {
params: {
hash,
},
});
console.log(Ifexits);
if (Ifexits.data.message == "存在") {
// 从后端拿到已经上传的切片列表
files = Ifexits.data.files;
progressindex.value = files.length;
// ln = files.length
}
// 实现文件切片处理 「固定数量 或者 固定大小」
let max = 1024 * 100, // 切片大小
count = Math.ceil(file.size / max), // 切片总数
index = 0, // 当前上传的切片索引值
chunks = []; // 存放切片的数组
mycount.value = count;
console.log(count, "??", index);
if (count == index) {
console.log("文件已存在");
(document.getElementById("progress") as HTMLElement).style.width = `100%`;
} else {
// if (count > 100) {
// max = file.size / 100;
// count = 100;
// }
// 存放切片,注意此处的切片名称,hash+index+suffix
while (index < count) {
chunks.push({
file: file.slice(index * max, (index + 1) * max),
filename: `${hash}_${index + 1}`,
});
index++;
}
// 把每一个切片都上传到服务器上
chunks.forEach((chunk) => {
// 这里进行断点续传:已经上传的无需在上传
if (!files.includes(chunk.filename)) {
let formdata = new FormData();
formdata.append("file", chunk.file);
formdata.append("hash", chunk.filename);
formdata.append("myfilename", filename);
axios
.post("/upload/album", formdata)
.then((data: any) => {
// 管控进度条
progressindex.value++;
manageProgress(progressindex.value, count, hash, suffix);
})
.catch(() => {
// alert("当前切片上传失败,请您稍后再试");
});
}
});
}
};
const manageProgress = async function (
index: any,
count: any,
hash: any,
suffix: string
) {
// console.log(index, "---", count)
// 管控进度
(document.getElementById("progress") as HTMLElement).style.width = `${
(index / count) * 100
}%`;
// 当所有切片都上传成功,我们合并切片
if (index < count) return;
(document.getElementById("progress") as HTMLElement).style.width = `100%`;
try {
let data: any = await axios.post(
"/upload/merge",
{
hash,
count,
suffix,
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
if (data.code === 0) {
alert(`恭喜您,文件上传成功`);
return;
}
throw data.codeText;
} catch (err) {
// alert("切片合并失败,请您稍后再试");
}
};
// setTimeout(() => {
// axios.get("http://localhost:3000/upload");
// }, 1000);
</script>
<style lang="scss" scoped></style>