整体思路:
获取上传的文件,将文件按size分割成1M的二进制数据流数组,然后逐条创建为单个文件进行上传。等待全部上传后,调用合并接口,在后端进行对文件的合并处理。
由于是逐条单个文件上传的,所以如果过程中发生网络错误,会在浏览器缓存中记录上传的文件的在数组中的索引,下次继续上传时将从索引处开始上传。
后端提供2个接口:1是接受上传数据的接口。2是处理文件的合并接口。
前端部分:
<template>
<Upload
:show-upload-list="false"
:on-success="upVideoPicture"
:before-upload="handeleBeforeUpVideo"
:action="''"
:data="contLaunchUrl"
name="file"
type="drag"
style="display: inline-block; width: 150px" >
<div style="width: 150px; height: 105px; line-height: 105px">
<Icon type="ios-camera" size="20"></Icon>
</div>
<Spin fix v-if="uploadVideoLoading">
<Icon type="ios-loading" size=18 class="demo-spin-icon-load"></Icon>
<div>Loading</div>
</Spin>
</Upload>
</template>
<script>
export default {
data(){
return {
contLaunchUrl:{},
uploadVideoLoading:false,
}
},
methods:{
upVideoPicture(data){
console.log("ok",data)
},
handeleBeforeUpVideo(file){
this.uploadVideoLoading = true;
this.fileChunk(file)
return false;//阻止默认行为
},
// 文件切片
fileChunk(file){
let that = this
// 文件名称设置为时间戳+四位随机数
let name = new Date().getTime() + '' + (Math.floor(Math.random()*(9999-1000))+1000);
let fileName = name // 获取文件名
let fileType = `${file.name.split('.')[1]}` // 文件类型后缀
let chunkSize = 1 * 1024 * 1024 // 示例1M
let chunkList = [] // 创建一个数组用来存储每一片文件流数据
if (file.size < chunkSize) { // 如果文件大小小于1M就只有一片,不用切
chunkList.push(file.slice(0)) // 文件流从第一个字节直接截取到最后就可以了
} else { // 如果文件大小大于1M 就需要切片了
var start = 0, end = 0 // 创建两个变量 开始位置 结束位置
while (true) { // 循环
end += chunkSize // 结束为止 = 结束位置 + 每片大小
let blob = file.slice(start, end) // 文件流从开始位置截取到结束位置
start += chunkSize // 截取完,开始位置后移
if (!blob.size) { // 如果截取不到了就退出 检查截取的情况
break;
}
chunkList.push(blob) // 把截取的每一片数据保存到数组
}
}
//一共分了多少片
that.transformFileType(chunkList,fileName,fileType); // 文件类型转换
},
// 文件类型转换
async transformFileType(list,name,type){ // list,name,type是上传时需要的参数,可根据情况而定
let that = this
let uploadFileNum = 0; // 已经上传的文件数量
let num = localStorage.getItem("uploadFileNum")?localStorage.getItem("uploadFileNum"):0 // 已经上传文件数量
// 如果已经全部上传 直接合并
if(num == list.length) return that.mergeFileFun(name,list.length,type)
//刚开始num为0
for (var i = num; i < list.length; i++) {
// 这么写是因为文件转换是异步任务
let transToFile = async (blob, fileName, fileType) => {
return new window.File([blob], fileName, {type: fileType})
}
// 转换完成后可以将file对象传给接口 fileFormData 是需要传递的参数
let fileFormData = new FormData();
const transToFileRes = await transToFile(list[i], "video", type)
fileFormData.append('file',transToFileRes) //文件内容
fileFormData.append('fileName',name) //文件名称
fileFormData.append('fileNum',Number(i)+1) //文件次数
fileFormData.append('filePath','video') //文件夹地址
await that.uploadFileMethods(fileFormData,i,name,list,type) // 上传文件
}
},
// 上传文件
async uploadFileMethods(fileFormData,uploadFileNum,name,list,type) {
let that = this
//上传
this.$API.target.base.uploadFile(fileFormData).then(res => {
if(res.code != 200){
that.uploadVideoLoading = false;
that.$Message.error(res.message);
}
uploadFileNum = Number(uploadFileNum) + 1
localStorage.setItem("uploadFileNum", uploadFileNum); // 已经上传文件数量
// 如果已经上传数量uploadFileNum与切片长度一致,就开始合并文件
if(uploadFileNum == list.length){
that.mergeFileFun(name,list.length,type);
}
})
},
// 合并文件
mergeFileFun(name,fileNo,type){
let that = this
let fileFormData = new FormData(); // 合并文件接口参数
fileFormData.append('fileName',name) // 合并的该套文件的文件名
fileFormData.append('fileTotalNum',fileNo) // 该套文件总数量
fileFormData.append('fileSuffix',type) // 文件类型后缀
fileFormData.append('filePath','video') // 该套文件存放一级路径
this.$API.target.base.mergeFile(fileFormData).then(res => {
if(res.code != 200){
that.uploadVideoLoading = false;
that.$Message.error(res.message);
}
that.uploadVideoLoading = false;
that.$Message.info("文件上传至:"+res.data); // 后端返回的文件路径
localStorage.removeItem('uploadFileNum') // 合并成功后清除已经上传文件数量标记
})
},
}
}
</script>
<style scoped>
</style>
请求接口封装:
//上传接口
export function uploadFile(data){
return productInstance({
url: "/protal/uploadfile",
method: "post",
data:data,
}).then(res => res.data)
}
//合并接口
export function mergeFile(data){
return productInstance({
url: "/protal/mergefile",
method: "post",
data:data,
}).then(res => res.data)
}
后端部分:
//大文件上传接口
@PostMapping("/uploadfile")
public Response<Boolean> uploadfile(@RequestParam(value = "file") MultipartFile file,
@RequestParam(value = "fileName") String fileName,
@RequestParam(value = "fileNum") int fileNum,
@RequestParam(value = "filePath") String filePath){
try{
//只接受文件的过程
File f = new File("D:\\Temp\\"+filePath+"\\"+ fileName );
if(!f.exists()){ f.mkdirs(); }
File newfile = new File("D:\\Temp\\"+filePath+"\\"+fileName+"\\"+ fileNum);
file.transferTo(newfile);//如果没有就创建
return new Response<>(ErrorCode.group0.getCode(),ErrorCode.group0.getMessage(),true);
} catch (Exception ex){
log.error("JqAccountTableController-------------login-------------ex:" + ex);
return new Response<>(ErrorCode.group2.getCode(),ErrorCode.group2.getMessage());
}
}
//大文件合并接口
@PostMapping("/mergefile")
public Response<String> mergefile(@RequestParam(value = "fileName") String fileName,
@RequestParam(value = "fileTotalNum") String fileTotalNum,
@RequestParam(value = "fileSuffix") String fileSuffix,
@RequestParam(value = "filePath") String filePath){
try{
String from = "D:\\Temp\\"+filePath+"\\"+fileName;//数据源
String to = "D:\\Temp\\"+filePath+"\\"+ fileName + "." + fileSuffix;//最终生成的文件
//新文件名
File t = new File(to);
FileOutputStream out = new FileOutputStream(t, true);
FileChannel outChannel = out.getChannel();
File fromFile = new File(from);
// 获取目录下的每一个文件名,再将每个文件一次写入目标文件
if (fromFile.exists()) {
//从目录下获取全部的文件列表
File dirFile = new File(from);
File[] listFiles = dirFile.listFiles();
List<File> list = Arrays.asList(listFiles);
Collections.sort(list, (o1, o2) -> { return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName()); });
FileInputStream in = null;
FileChannel inChannel = null;
long start = 0;// 记录新文件最后一个数据的位置
for (File file : list) {
in = new FileInputStream(file);
inChannel = in.getChannel();
//将分片内容吸入到新文件中
outChannel.transferFrom(inChannel, start, file.length());
start += file.length();
in.close();//吸口关闭
inChannel.close();//吸口关闭
}
}
//文件关闭
out.close();
outChannel.close();
//删除文件夹以及数据
if (fromFile.exists()) {
//获取目录下子文件
File[] files = fromFile.listFiles();
//遍历该目录下的文件对象
for (File f : files) {
//文件删除
f.delete();
}
//文件夹删除
fromFile.delete();
}
//返回
return new Response<>(ErrorCode.group0.getCode(),ErrorCode.group0.getMessage(),to);
} catch (Exception ex){
log.error("JqAccountTableController-------------login-------------ex:" + ex);
return new Response<>(ErrorCode.group2.getCode(),ErrorCode.group2.getMessage());
}
}