前端代码部分
// 点击 Button 触发 input从而选择文件并上传。
<template>
<div>
<button type="button" v-on:click="selectFile()" class="btn btn-white btn-default btn-round">
<i class="ace-icon fa fa-upload"></i>{{text}}
</button>
<input class="hidden" type="file" ref="file" v-on:change="uploadFile()" v-bind:id="inputId+'-input'">
</div>
</template>
/**
* 点击【上传】
*/
selectFile () {
let _this = this;
$("#" + _this.inputId + "-input").trigger("click");
},
/**
* 上传文件
*/
uploadFile() {
let _this = this;
// 1. 获取 input 中被选中的文件
let file = _this.$refs.file.files[0];
// 2. 生成文件标识,标识多次上传的是不是同一个文件
let key = hex_md5(file.name + file.size + file.type);
let key10 = parseInt(key, 16);
let key62 = Tool._10to62(key10);
// 判断文件格式 (非必选,根据实际情况选择是否需要限制文件上传类型)
let suffixs = _this.suffixs;
let fileName = file.name;
let suffix = fileName.substring(fileName.lastIndexOf(".")+1, fileName.length).toLowerCase();
if(!(!suffixs || JSON.stringify(suffixs) === "{}" || suffixs.length === 0)) {
let validateSuffix = false;
for(let s of suffixs) {
if(s.toLocaleLowerCase() === suffix) {
validateSuffix = true;
break;
}
}
if(!validateSuffix) {
Toast.warning("文件格式不正确!只支持上传:" + suffixs.join(","));
$("#" + _this0.inputId + "-input").val("");
return;
}
}
// 3. 文件分片开始
// 3.1 设置与计算分片必选参数
let shardSize = 20 * 1024 *1024; // 20M为一个分片
let shardIndex = 1; // 分片索引,1表示第1个分片
let size = file.size; // 文件的总大小
let shardTotal = Math.ceil(size / shardSize); // 总分片数
// 3.2 拼接将要传递到参数, use 非必选,这里用来标识文件用途。
let param = {
'shardIndex': shardIndex,
'shardSize': shardSize,
'shardTotal': shardTotal,
'use': _this.use,
'name': file.name,
'suffix': suffix,
'size': file.size,
'key': key62
};
// 3.3 传递分片参数,通过递归完成分片上传。
_this.upload(param);
},
/**
* 文件分片函数
*/
getFileShard(shardIndex, shardSize) {
let _this = this;
let file = _this.$refs.file.files[0];
let start = (shardIndex - 1) * shardSize; // 当前分片起始位置
let end = Math.min(file.size, start + shardSize); // 当前分片结束位置
let fileShard = file.slice(start, end); // 从文件中截取当前的分片数据
return fileShard;
},
后端部分
Base64ToMultipartFile直接百度粘贴即可
@PostMapping("/big-upload")
public ResponseDto uploadOfMerge(@RequestBody FileDto fileDto) throws IOException {
log.info("上传文件开始");
String use = fileDto.getUse();
String key = fileDto.getKey();
String suffix = fileDto.getSuffix();
String shardBase64 = fileDto.getShard();
// 1. 将分片转为 MultipartFile
MultipartFile shard = Base64ToMultipartFile.base64ToMultipart(fileDto.getShard());
// 获取分片要保存到的路径
// 根据use字段获取文件用途,从而上传到不同文件夹下(非必选)
FileUseEnum useEnum = FileUseEnum.getByCode(use);
// 若文件夹不存在则创建
String dir = useEnum.name().toLowerCase();
File fullDir = new File(FILE_PATH + dir);
if (!fullDir.exists()) {
fullDir.mkdir();
}
String path = new StringBuffer(dir)
.append(File.separator)
.append(key)
.append(".")
.append(suffix).toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4
String localPath = new StringBuffer(path)
.append(".")
.append(fileDto.getShardIndex()).toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1
String fullPath = FILE_PATH + localPath;
// 2. 通过 transferTo 保存文件到服务器磁盘
File dest = new File(fullPath);
shard.transferTo(dest);
log.info(dest.getAbsolutePath());
// 3. 将文件分片信息保存/更新到数据库
log.info("保存文件记录开始");
fileDto.setPath(path);
fileService.saveBigFile(fileDto);
ResponseDto responseDto = new ResponseDto();
responseDto.setContent(fileDto);
// 4. 合并
// 若分片均已上传,将所有分片合并成一个文件。
if (fileDto.getShardIndex().equals(fileDto.getShardTotal())) {
this.merge(fileDto);
}
// 5. 返回分片上传结果
return responseDto;
}
private void merge(FileDto fileDto) {
log.info("合并分片开始");
String path = fileDto.getPath();
Integer shardTotal = fileDto.getShardTotal();
File newFile = new File(FILE_PATH + path);
byte[] byt = new byte[10 * 1024 * 1024];
FileInputStream inputStream = null; // 分片文件
int len;
// 文件追加写入
try (FileOutputStream outputStream = new FileOutputStream(newFile, true);
) {
for (int i = 0; i < shardTotal; i++) {
// 读取第一个分片
inputStream = new FileInputStream(new File(FILE_PATH + path + "." + (i+1))); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1
while ((len = inputStream.read(byt))!=-1) {
outputStream.write(byt, 0, len);
}
}
} catch (FileNotFoundException e) {
log.info("文件寻找异常", e);
} catch (IOException e) {
log.info("分片合并异常", e);
} finally {
try {
if(inputStream !=null) {
inputStream.close();
}
log.info("IO流关闭");
} catch (IOException e) {
log.error("IO流关闭", e);
}
}
log.error("合并分片结束");
System.gc();
// 删除分片
log.info("删除分片开始");
for (int i = 0; i < shardTotal; i++) {
String filePath = FILE_PATH + path + "." + (i + 1);
File file = new File(filePath);
boolean result = file.delete();
log.info("删除{},{}", filePath, result ? "成功" : "失败");
}
log.info("删除分片结束");
}
关于断点续传以及极速秒传
uploadFile 中的 _this.upload(param); 被 检测已上传分片的函数 _this.check(param); 取代。
upload 上传函数由 check 调用。
check(param) {
let _this = this;
_this.$ajax.get(process.env.VUE_APP_SERVER + "/file/admin/check/" + param.key).then((res)=> {
let resp = res.data;
if(resp.success) {
let obj = resp.content;
if(!obj) {
param.shardIndex = 1;
console.log("没有找到文件记录,从分片1开始上传");
_this.upload(param);
} else if (obj.shardIndex === obj.shardTotal) {
// 已上传分片 = 分片总数,说明已全部上传完,不需要再上传
Toast.success("文件极速秒传成功!");
_this.afterUpload(resp);
$("#" + _this.inputId + "-input").val("");
}else {
param.shardIndex = obj.shardIndex + 1;
console.log("没有找到文件记录,从分片1开始上传");
_this.upload(param);
}
} else {
console.log("文件上传失败");
$("#" + _this.inputId + "-input").val("");
}
});
},
后端
@GetMapping("/check/{key}")
public ResponseDto check(@PathVariable String key) {
log.info("检测上传分片开始:{}}", key);
ResponseDto responseDto = new ResponseDto();
FileDto fileDto = fileService.findByKey(key);
responseDto.setContent(fileDto);
return responseDto;
}