先上GitHub地址:前端代码,后台代码,具体代码下载回来就行,这里只讲核心部分。。。
核心思路: 前端对文件进行分片,并且发送文件的唯一标识(文件名、类型、大小或者其他属性进行md5摘要计算可得)、分片索引(第几个分片)、分片总数、文件名称(方便合并后的文件名称)记住这4个参数;后台判断分片索引等于分片总数就开始合并,通过流输出追加的方式合并文件。
先看前端的分片代码:
总分片数=文件大小/每片的大小,再向上取整。
let shardTotal = Math.ceil(size / shardSize); //总片数
项目已经安装MD5组件
// 生成文件标识,标识多次上传的是不是同一个文件
let key = this.$md5(file.name + file.size + file.type);
文件分片截取核心代码:
let fileShard = file.slice(start, end); //从文件中截取当前的分片数据
类似数据库分页原理,但是要注意切片的终点,当切片不足预定的切片大小,那就取文件的大小作为终点的边界。
点击按钮触发事件函数:
async handUpLoad(req) {
let _this = this;
var file = req.file;
/* console.log('handUpLoad', req)
console.log(file);*/
//let param = new FormData();
//通过append向form对象添加数据
//文件名称和格式,方便后台合并的时候知道要合成什么格式
let fileName = file.name;
let suffix = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length).toLowerCase();
//这里判断文件格式,有其他格式的自行判断
if (suffix != 'mp4') {
this.$message.error('文件格式错了哦。。');
return;
}
// 文件分片
// let shardSize = 10 * 1024 * 1024; //以10MB为一个分片
// let shardSize = 50 * 1024; //以50KB为一个分片
let shardSize = _this.shardSize;
let shardIndex = 1; //分片索引,1表示第1个分片
let size = file.size;
let shardTotal = Math.ceil(size / shardSize); //总片数
// 生成文件标识,标识多次上传的是不是同一个文件
let key = this.$md5(file.name + file.size + file.type);
let param = {
key: key,
shardIndex: shardIndex,
shardSize: shardSize,
shardTotal: shardTotal,
size: size,
fileName: fileName,
suffix: suffix
}
/*param.append("uid", key);
param.append("shardIndex", shardIndex);
param.append("shardSize", shardSize);
param.append("shardTotal", shardTotal);
param.append("size", size);
param.append("fileName", fileName);
param.append("suffix", suffix);
*/
let checkIndexData = await _this.check(key);//得到文件分片索引
let checkIndex = checkIndexData.findex;
//console.log(checkIndexData)
if (checkIndex == -1) {
this.recursionUpload(param, file);
} else if (checkIndex < shardTotal) {
param.shardIndex = param.shardIndex + 1;
this.recursionUpload(param, file);
} else {
this.videoUrl = checkIndexData.fname;//把地址赋值给视频标签
this.$message({
message: '极速秒传成功。。。。。',
type: 'success'
});
}
//console.log('结果:', res)
},
前端在发起分片传输的时候先向后台检查分片信息,根据分片索引情况调整分片索引。
前端采取递归的传输方式:
async recursionUpload(param, file) {
//FormData私有类对象,访问不到,可以通过get判断值是否传进去
let _this = this;
let key = param.key;
let shardIndex = param.shardIndex;
let shardTotal = param.shardTotal;
let shardSize = param.shardSize;
let size = param.size;
let fileName = param.fileName;
let suffix = param.suffix;
let fileShard = _this.getFileShard(shardIndex, shardSize, file);
//param.append("file", fileShard);//文件切分后的分片
//param.file = fileShard;
let totalParam = new FormData();
totalParam.append('file', fileShard);
totalParam.append("key", key);
totalParam.append("shardIndex", shardIndex);
totalParam.append("shardSize", shardSize);
totalParam.append("shardTotal", shardTotal);
totalParam.append("size", size);
totalParam.append("fileName", fileName);
totalParam.append("suffix", suffix);
let config = {
//添加请求头
headers: {"Content-Type": "multipart/form-data"}
};
console.log(param);
var res = await this.$http.post('/upload', totalParam, config)
var resData = res.data;
if (resData.status) {
if (shardIndex < shardTotal) {
this.$notify({
title: '成功',
message: '分片' + shardIndex + '上传完成。。。。。。',
type: 'success'
});
} else {
this.videoUrl = resData.data;//把地址赋值给视频标签
this.$notify({
title: '全部成功',
message: '文件上传完成。。。。。。',
type: 'success'
});
}
if (shardIndex < shardTotal) {
console.log('下一份片开始。。。。。。');
// 上传下一个分片
param.shardIndex = param.shardIndex + 1;
_this.recursionUpload(param, file);
}
}
}
后台处理:
先接受前端的分片索引检查,实质查询数据记录的分片信息
和正常的接收文件操作一样,先记录每次文件分片的上传的4个信息,没有记录则是新增,有记录则是通过唯一标识修改分片索引
当前索引分片数==总分片数则开始合并文件:
合并:
public void merge(FilePojo filePojo) throws Exception {
Long shardTotal = filePojo.getShardTotal();
File newFile = new File(FileConstance.FILE_PATH + filePojo.getFileName());
if (newFile.exists()) {
newFile.delete();
}
FileOutputStream outputStream = new FileOutputStream(newFile, true);//文件追加写入
FileInputStream fileInputStream = null;//分片文件
byte[] byt = new byte[10 * 1024 * 1024];
int len;
try {
for (int i = 0; i < shardTotal; i++) {
// 读取第i个分片
fileInputStream = new FileInputStream(new File(FileConstance.FILE_PATH + filePojo.getKey() + "." + (i + 1))); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1
while ((len = fileInputStream.read(byt)) != -1) {
outputStream.write(byt, 0, len);//一直追加到合并的新文件中
}
}
} catch (IOException e) {
log.error("分片合并异常", e);
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
outputStream.close();
log.info("IO流关闭");
System.gc();
} catch (Exception e) {
log.error("IO流关闭", e);
}
}
log.info("合并分片结束");
}
定义文件输出流:
后面的参数一定要true,设置可追加
FileOutputStream outputStream = new FileOutputStream(newFile, true);//文件追加写入
遍历所有分片,然后通过流的形式,边读边写;
测试效果:
全新分片上传:
本机文件情况:
再次上传-极速秒传:
断点续传:
这里模拟,先完整上传,我们删除合并的文件和其他分片,保留2个分片,然后数据库的索引也是改为2,
再次操作:就会从第二分片开始
文件合并后没有数据丢失:
撒花~~~~~~~~~~~完结O(∩_∩)O哈哈~