Springboot + Vue实现大文件切片上传
大文件切片上传原理就是将一个大文件分成若干份大小相等的块文件,等所有块上传成功后,再将文件进行合并。
一、Springboot后端
1.实体TChunkInfo.java
import org.springframework.web.multipart.MultipartFile;
import java.io.Serializable;
@Data
public class TChunkInfo implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
// 当前文件块,从1开始
private Integer chunkNumber;
// 每块大小
private Long chunkSize;
// 当前分块大小
private Long currentChunkSize;
// 总大小
private Long totalSize;
// 文件标识
private String identifier;
// 文件名
private String filename;
// 相对路径
private String relativePath;
// 总块数
private Integer totalChunks;
// 文件类型
private String type;
// 块内容
private transient MultipartFile upfile;
}
2.实体TFileInfoVO.java
import java.io.Serializable;
/**
* 文件模型参数
* @author YouZ
*/
@Data
public class TFileInfoVO implements Serializable {
private static final long serialVersionUID = 1L;
//附件编号
private String id;
//附件类型
private String fileType;
//附件名称
private String name;
//附件总大小
private Long size;
//附件地址
private String relativePath;
//附件MD5标识
private String uniqueIdentifier;
//附件所属项目ID
private String refProjectId;
}
3.文件切片上传及切片合并接口
@RestController
@RequestMapping("/uploader")
public class FileController {
private static String FILENAME = "";
@Value("${YouZ.upload.file.path}")
private String decryptFilePath;
@Value("${YouZ.upload.file.temp}")
private String decryptFilePathTemp;
/**
* 上传文件块
* @param chunk
* @return
*/
@PostMapping("/chunk")
public String uploadChunk(TChunkInfo chunk) throws IOException{
String apiRlt = "200";
MultipartFile file = chunk.getUpfile();
String guid = chunk.getIdentifier();
Integer chunkNumber = chunk.getChunkNumber();
System.out.println("guid:" + guid);
File outFile = new File(decryptFilePathTemp+File.separator+guid, chunkNumber + ".part");
if ("".equals(FILENAME)) {
FILENAME = chunk.getFilename();
}
InputStream inputStream = file.getInputStream();
FileUtils.copyInputStreamToFile(inputStream, outFile);
return apiRlt;
}
@PostMapping("/mergeFile")
public String mergeFile(@RequestBody TFileInfoVO fileInfoVO) throws Exception{
String rlt = "FALURE";
String guid = fileInfoVO.getUniqueIdentifier();
System.out.println("merge:"+guid);
File file = new File(decryptFilePathTemp+File.separator+guid);
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null && files.length > 0) {
File partFile = new File(decryptFilePath + File.separator + FILENAME);
for (int i = 1; i <= files.length; i++) {
File s = new File(decryptFilePathTemp+File.separator+guid, i + ".part");
FileOutputStream destTempfos = new FileOutputStream(partFile, true);
FileUtils.copyFile(s,destTempfos );
destTempfos.close();
}
FileUtils.deleteDirectory(file);
FILENAME = "";
}
}
return rlt;
}
}
二、Vue前端
1.注册组件(main.js)
使用vue-uploader;一个基于vue的uploader组件,缺省就是分片上传。
import uploader from 'vue-simple-uploader'
Vue.use(uploader)
2.上传器
<!-- 上传器 -->
<uploader
ref="uploader"
:options="options"
:autoStart=false
:file-status-text="fileStatusText"
@file-added="onFileAdded"
@file-success="onFileSuccess"
@file-error="onFileError"
class="uploader-ui">
<uploader-unsupport></uploader-unsupport>
<uploader-drop>
<div>
<uploader-btn id="global-uploader-btn" :attrs="attrs" ref="uploadBtn">选择文件<i class="el-icon-upload el-icon--right"></i></uploader-btn>
</div>
</uploader-drop>
<uploader-list></uploader-list>
</uploader>
变量
import SparkMD5 from 'spark-md5';
import { getToken } from '@/utils/auth'
import {mergeFile} from "@/api/fastLoader/uploadFile";
data() {
return {
options: {
headers: {
Authorization: 'Bearer ' + getToken()
},
//目标上传 URL,默认POST
target: process.env.VUE_APP_BASE_API+'/uploader/chunk',
//分块大小(单位:字节)
chunkSize: '20480000',
//上传文件时文件内容的参数名,对应chunk里的Multipart对象名,默认对象名为file
fileParameterName: 'upfile',
//失败后最多自动重试上传次数
maxChunkRetries: 3,
//是否开启服务器分片校验,对应GET类型同名的target URL
testChunks: false,
/*
服务器分片校验函数,判断秒传及断点续传,传入的参数是Uploader.Chunk实例以及请求响应信息
reponse码是successStatuses码时,才会进入该方法
reponse码如果返回的是permanentErrors 中的状态码,不会进入该方法,直接进入onFileError函数 ,并显示上传失败
reponse码是其他状态码,不会进入该方法,正常走标准上传
checkChunkUploadedByResponse函数直接return true的话,不再调用上传接口
*/
checkChunkUploadedByResponse: function (chunk, response_msg) {
let objMessage = JSON.parse(response_msg);
if (objMessage.skipUpload) {
return true;
}
return (objMessage.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0;
}
},
attrs: {
accept: ACCEPT_CONFIG.getAll()
},
fileStatusText: {
success: '上传成功',
error: '上传失败',
uploading: '上传中',
paused: '暂停',
waiting: '等待上传'
},
}
}
回调函数
methods: {
// 上传
onFileAdded(file) {
this.computeMD5(file);
},
/*
第一个参数 rootFile 就是成功上传的文件所属的根 Uploader.File 对象,它应该包含或者等于成功上传文件;
第二个参数 file 就是当前成功的 Uploader.File 对象本身;
第三个参数就是 message 就是服务端响应内容,永远都是字符串;
第四个参数 chunk 就是 Uploader.Chunk 实例,它就是该文件的最后一个块实例,如果你想得到请求响应码的话,chunk.xhr.status就是
*/
onFileSuccess(rootFile, file, response, chunk) {
//refProjectId为预留字段,可关联附件所属目标,例如所属档案,所属工程等
file.refProjectId = "123456789";
mergeFile(file).then( response=> {
if(responseData.data.code === 200){
console.log("合并操作未成功,结果码:"+responseData.data.code);
}
}).catch(function (error){
console.log("合并后捕获的未知异常:"+error);
});
},
onFileError(rootFile, file, response, chunk) {
console.log('上传完成后异常信息:'+response);
},
error(msg) {
this.$notify({
title: '错误',
message: msg,
type: 'error',
duration: 2000
})
}
}
计算md5,实现断点续传及秒传
/**
* 计算md5,实现断点续传及秒传
* @param file
*/
computeMD5(file) {
file.pause();
//单个文件的大小限制2G
let fileSizeLimit = 2 * 1024 * 1024 * 1024;
console.log("文件大小:"+file.size);
console.log("限制大小:"+fileSizeLimit);
if(file.size > fileSizeLimit){
this.$message({
showClose: true,
message: '文件大小不能超过2G'
});
file.cancel();
}
let fileReader = new FileReader();
let time = new Date().getTime();
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
let currentChunk = 0;
const chunkSize = 20 * 1024 * 1000;
let chunks = Math.ceil(file.size / chunkSize);
let spark = new SparkMD5.ArrayBuffer();
//由于计算整个文件的Md5太慢,因此采用只计算第1块文件的md5的方式
let chunkNumberMD5 = 1;
loadNext();
fileReader.onload = (e => {
spark.append(e.target.result);
if (currentChunk < chunkNumberMD5) {
loadNext();
} else {
let md5 = spark.end();
console.log(md5);
file.uniqueIdentifier = md5;
file.resume();
console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
}
});
fileReader.onerror = function () {
this.error(`文件${file.name}读取出错,请检查该文件`)
file.cancel();
};
function loadNext() {
let start = currentChunk * chunkSize;
let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
currentChunk++;
console.log("计算第"+currentChunk+"块");
}
},
前后端分离的大文件切片上传就完成了;这里需要注意的是因为合并方法需要的时间久,会超时;需要将超时时间写大一点。
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: process.env.VUE_APP_BASE_API,
// 超时
timeout: 30000
})