整体逻辑如下
前台引用spark-md5获取文件唯一ID值,即md5值,前台将文件进行分片,通过该值进行后台校验,后台以MD5值生成文件夹,以断点生成子文件,以此实现断点续传。
前台计算MD5,前台计算MD5快慢与文件大小相关,成功后方可请求后台,以防无值出现上传文件错乱。
后台用MD5值进行校验,有则跳过,无则追加,文件分片完全上传后,再请求合并接口将文件进行合并。
代码实现逻辑,前台将文件进行分片执行for循环,循环的每片值为该断点位置,将每一片进行记录请求到后台breakpoint参数,如果出现意外结束,再次上传时请求后台方法CheckPoint进行校验(校验逻辑,取出MD5文件夹下最大的文件名作为断点,存储时按断点传就直接取文件名,如果有内置逻辑,就按逻辑截取),如果存在则进行从该断点处上传,如果不存在从头开始,暂停按钮同理,所有分片数据请求成功后,请求FileMerge进行合并。
相应注释见代码,具体前后台代码如下:
前台:
<template>
<div id="app">
<!-- 上传组件 -->
<el-upload action drag :auto-upload="false" :show-file-list="false" :on-change="handleChange">
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处,或
<em>点击上传</em>
</div>
<div class="el-upload__tip" slot="tip">大小不超过 200M 的视频</div>
</el-upload>
<!-- 进度显示 -->
<div class="progress-box">
<span>上传进度:{{ percent.toFixed() }}%</span>
<el-button type="primary" size="mini" @click="handleClickBtn">{{ upload | btnTextFilter}}</el-button>
</div>
<!-- 展示上传成功的视频 -->
<div v-if="videoUrl">
<video :src="videoUrl" controls />
</div>
</div>
</template>
<script>
import SparkMD5 from "spark-md5";
import axios from "axios";
export default {
name: "App3",
filters: {
btnTextFilter(val) {
return val ? "暂停" : "继续";
}
},
data() {
return {
percent: 0,
videoUrl: "",
upload: true,
percentCount: 0,
CurFileName: "",
baseurllocal: window.localStorage.getItem("rooturl"),
curPoint: 0,
calchunk: 0
};
},
methods: {
ReadFileMd5(fileObj) {
return new Promise((resolve, reject) => {
let hash;
const slicelength = 10;
const rfmchunkSize = Math.ceil(fileObj.size / slicelength);
const fileReader = new FileReader();
const md5 = new SparkMD5();
let index = 0;
const loadFile = () => {
const slice = fileObj.slice(index, index + rfmchunkSize);
fileReader.readAsBinaryString(slice);
};
loadFile();
fileReader.onload = e => {
md5.appendBinary(e.target.result);
if (index < fileObj.size) {
index += rfmchunkSize;
loadFile();
} else {
hash = md5.end();
this.hash = hash;
console.log(hash);
resolve(hash);
}
};
});
},
async handleChange(file) {
if (!file) return;
this.percentCount = 0;
this.percent = 0;
this.videoUrl = "";
// 获取文件并转成 ArrayBuffer 对象
const fileObj = file.raw;
this.hash = "";
this.ReadFileMd5(fileObj).then(response => {
const hash = this.hash;
// 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
const chunkSize = 10485760, //2097152,
chunkList = [], // 保存所有切片的数组
chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1]; // 文件后缀名
// 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
let curChunk = 0; // 切片时的初始位置
for (let i = 0; i < chunkListLength; i++) {
const item = {
breakPoint: i.toString(),
chunk: fileObj.slice(curChunk, curChunk + chunkSize),
fileName: `${hash}_${i}.${suffix}` // 文件名规则按照 hash_1.jpg 命名
};
curChunk += chunkSize;
chunkList.push(item);
}
console.log(chunkList);
this.chunkList = chunkList;
this.CurFileName = file.raw.name;
console.log(this.CurFileName);
console.log("chunkListLength" + "------" + chunkListLength);
this.CheckPoint().then(res => {
if (this.percentCount === 0) {
this.percentCount = 100 / this.chunkList.length;
}
if (this.calchunk > 0) {
this.percent += this.percentCount * this.calchunk; // 改变进度
this.reqChuck(this.calchunk);
} else {
this.reqChuck(0);
}
});
});
},
async reqChuck(curchunk) {
let vm = this;
await vm.reqChuckPoint(curchunk);
},
async reqChuckPoint(index) {
this.curPoint = index;
if (!this.upload) {
return;
} else {
if (index > this.chunkList.length - 1) {
this.complete();
// this.percent = 100;
console.log("循环结束");
return;
} else {
const item = this.chunkList[index];
const formData = new FormData();
formData.append("md5", this.hash);
formData.append("breakPoint", item.breakPoint);
formData.append("chunk", item.chunk);
formData.append("fileName", item.fileName);
let url = this.baseurllocal + "/api/weatherforecast/UploadEfilePoint"