springboot 大文件切片上传
1. 前端(vue element ui & 原生)
初始变量声明:
currentFile: {}, // 当前上传的文件
bigFileSliceCount: 20, // 大文件切片后的子文件数量(也可使用其它限定方式,如按照文件大小,每10MB切一片,此处采用的是固定切片的子文件数量的方式倒推切片大小)
接口:切片上传图片&合并切片文件
<el-button @click="partUploadClick()" type="info"> 上传文件</el-button>
<input v-show="false" id="uploadPartfile" class="upload-css" type='file' @click.stop=''
@change='handleFileChange($event, currentFile); currentFile = {}' single />
// 文件上传处理
partUploadClick() {
var clickEvent = document.createEvent('MouseEvent'); // 1.创建一个鼠标事件类型
clickEvent.initMouseEvent('click', false, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); // 2.初始化一个click事件
document.getElementById('uploadPartfile').dispatchEvent(clickEvent); // 3.派发(触发)
},
handleFileChange(event, item) {
let inputDom = event.target // input 本身,从这里获取 files<FileList>
let files = inputDom.files // input 中的文件,是 FileList 对象,一个类似数组的文件组,但不是数组,可遍历
var GUID = this.guid();
var file = files[0], //文件对象
name = file.name, //文件名
size = file.size; //总大小
// console.log(size)
if (size <= 524288000) { // 文件大小小于等于500MB,直接整文件上传
this.handleFiles(event, item)
} else { // 大于500MB 切片上传
var shardSize = size / this.bigFileSliceCount, // 根据切片数量,确定每切片的大小是多少
shardCount = Math.ceil(size / shardSize); //总片数
item.download = true
for (var i = 0; i < shardCount; ++i) {
//计算每一片的起始与结束位置
var start = i * shardSize,
end = Math.min(size, start + shardSize);
var partFile = file.slice(start, end);
this.partUpload(GUID, partFile, name, shardCount, i, item.fullPath, item, size);
}
}
},
partUpload: function (GUID, partFile, name, chunks, chunk, filePath, item, size) {
//构造一个表单,FormData是HTML5新增的
const _this = this
var form = new FormData();
form.append("guid", GUID);
form.append("file", partFile); //slice方法用于切出文件的一部分
form.append("fileName", name);
form.append("chunks", chunks); //总片数
form.append("chunk", chunk); //当前是第几片
form.append("filePath", filePath); //文件保存路径
uploadFileByPart(form).then((res) => {
// console.log(res)
let data = res.data
_this.status++;
if (data.code == 200) {
_this.complete = this.status * (100 / this.bigFileSliceCount)
let finishSize = this.status != this.bigFileSliceCount ? (size / this.bigFileSliceCount) * this.status : size
if (finishSize > 1048576) {
_this.finishSize = parseFloat(finishSize / 1048576).toFixed(1) + 'M';
} else if (finishSize > 1024) {
_this.finishSize = parseInt(finishSize / 1024) + 'K';
} else {
_this.finishSize = finishSize + 'B';
}
// console.log(this.status + " / " + chunks)
}
if (this.status == chunks) {
_this.mergeFile(GUID, name, filePath, item);
}
}).catch((err) => {
console.log("请求出错", err);
});
},
// 切片上传文件
export const uploadFileByPart = (fileFormData) => {
return http({
url: `/xxxxx/files/part`,
method: 'post',
data: fileFormData,
async: true,
processData: false,
contentType: false
})
}
mergeFile: function (GUID, name, filePath, item) {
var formMerge = new FormData();
formMerge.append("guid", GUID);
formMerge.append("fileName", name);
formMerge.append("filePath", filePath);
mergeFileByPart(formMerge).then((res) => {
item.download = false
let data = res.data
if (data.code == 200) {
this.$message({
message: '上传成功',
type: 'success'
});
this.getFalcoPath();
}
}).catch((err) => {
item.download = false
console.log("请求出错", err);
});
},
// 切片合并文件
export const mergeFileByPart = (fileFormData) => {
return http({
url: `/xxxxx/files/merge`,
method: 'post',
processData: false,
contentType: false,
data: fileFormData
})
}
guid: function (prefix) {
var counter = 0;
var guid = (+new Date()).toString(32),
i = 0;
for (; i < this.bigFileSliceCount; i++) {
guid += Math.floor(Math.random() * 65535).toString(32);
}
return (prefix || 'wu_') + guid + (counter++).toString(32);
}
// 整文件上传时的处理
handleFiles(event, item) {
let inputDom = event.target // input 本身,从这里获取 files<FileList>
let files = inputDom.files // input 中的文件,是 FileList 对象,一个类似数组的文件组,但不是数组,可遍历
// console.log(files)
let fileFormData = new window.FormData()
fileFormData.append('file', files[0])
item.download = true
// console.log("上传文件路径" + item.fullPath)
const _this = this
uploadFileByPath(fileFormData, item.fullPath, this.gress).then((res) => {
// console.log(res)
item.download = false
inputDom.value = ''
if (res.data.code == 200) {
_this.$message({
message: '上传成功',
type: 'success'
});
this.getFalcoPath();
} else if (res.data.code == 2001) {
this.$message.error('上传失败');
}
}).catch((err) => {
item.download = false
console.log("请求出错", err);
});
},
// 单个文件完整上传文件
export const uploadFileByPath = (fileFormData, filePath, uploadProgress) => {
return http({
url: `/xxxxx/files/upload`,
method: 'post',
onUploadProgress: function (progressEvent) {
uploadProgress(progressEvent)
},
data: fileFormData,
params: { 'filePath': filePath }
})
}
// 上传与下载文件的回调
gress(progress) {
const self = this
this.complete = ((progress.loaded / progress.total) * 100).toFixed(0)
if (progress.loaded > 1048576) {
self.finishSize = parseFloat(progress.loaded / 1048576).toFixed(1) + 'M';
} else if (progress.loaded > 1024) {
self.finishSize = parseInt(progress.loaded / 1024) + 'K';
} else {
self.finishSize = progress.loaded + 'B';
}
// console.log("已下载:" + self.finishSize + ' 比例 ' + this.complete)
},
2. 后台
接口:
@ResponseBody
@PostMapping("/xxxxx/files/upload")
public ApiResult upload(@RequestParam(value = "file", required = false) MultipartFile multipartFile, @RequestParam(required = false) String filePath) {
File file=new File(filePath + File.separator + multipartFile.getOriginalFilename());
try {
FileUtil.copy(multipartFile.getBytes(), file);
} catch (IOException e) {
return ApiResult.fail(2001,"上传失败");
}
return ApiResult.ok("上传成功");
}
@PostMapping("/xxxxx/files/part")
@ResponseBody
public ApiResult bigFile(HttpServletRequest request, HttpServletResponse response, String guid, Integer chunk, MultipartFile file, Integer chunks, String filePath, String fileName) {
try {
boolean isMultipart = ServletFileUpload.isMultipartContent(request);
if (isMultipart) {
if (chunk == null) chunk = 0;
// 临时目录用来存放所有分片文件
String tempFileDir = applicationProperties.getNasPath() + File.separator + "临时文件夹" + File.separator + guid;
File parentFileDir = new File(tempFileDir);
if (!parentFileDir.exists()) {
parentFileDir.mkdirs();
}
// 分片处理时,前台会多次调用上传接口,每次都会上传文件的一部分到后台
log.info(SecurityUtils.getCurrentLogin() + " 上传:" + filePath + File.separator + fileName + "; 切片文件名:" + guid + "_" + chunk + ".part");
File tempPartFile = new File(parentFileDir, guid + "_" + chunk + ".part");
FileUtils.copyInputStreamToFile(file.getInputStream(), tempPartFile);
}
} catch (Exception e) {
log.error(SecurityUtils.getCurrentLogin() + " 上传:" + filePath + File.separator + fileName + "; 切片文件名:" + guid + "_" + chunk + ".part" + " error: " + e.getMessage());
e.printStackTrace();
return ApiResult.fail(2009,e.getMessage());
}
return ApiResult.ok(200,"上次成功");
}
@RequestMapping("/xxxxx/files/merge")
@ResponseBody
public ApiResult mergeFile(String guid, String fileName, String filePath) {
try {
log.info(SecurityUtils.getCurrentLogin() + " 合并切片文件:" + filePath + File.separator + fileName + " start");
String sname = fileName.substring(fileName.lastIndexOf("."));
//时间格式化格式
Date currentTime = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS");
//获取当前时间并作为时间戳
String timeStamp = simpleDateFormat.format(currentTime);
//拼接新的文件名
String newName = timeStamp + sname;
simpleDateFormat = new SimpleDateFormat("yyyyMM");
String path = applicationProperties.getNasPath() + File.separator + "临时文件夹" + File.separator ;
String tmp = simpleDateFormat.format(currentTime);
File parentFileDir = new File(path + guid);
if (parentFileDir.isDirectory()) {
File destTempFile = new File(filePath, fileName);
if (!destTempFile.exists()) {
//先得到文件的上级目录,并创建上级目录,在创建文件
destTempFile.getParentFile().mkdir();
try {
destTempFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
for (int i = 0; i < parentFileDir.listFiles().length; i++) {
File partFile = new File(parentFileDir, guid + "_" + i + ".part");
FileOutputStream destTempfos = new FileOutputStream(destTempFile, true);
//遍历"所有分片文件"到"最终文件"中
FileUtils.copyFile(partFile, destTempfos);
destTempfos.close();
}
// 删除临时目录中的分片文件
FileUtils.deleteDirectory(parentFileDir);
log.info(SecurityUtils.getCurrentLogin() + " 合并切片文件:" + filePath + File.separator + fileName + " end ");
return ApiResult.ok(200,"合并成功");
}else{
log.info(SecurityUtils.getCurrentLogin() + " 合并切片文件:" + filePath + File.separator + fileName + " end error: 没找到目录");
return ApiResult.fail(2007,"没找到目录");
}
} catch (Exception e) {
log.info(SecurityUtils.getCurrentLogin() + " 合并切片文件:" + filePath + File.separator + fileName + " end error: " + e.getMessage());
e.printStackTrace();
return ApiResult.fail(2008,e.getMessage());
}
}
工具类:
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import java.io.*;
import java.nio.file.*;
@Slf4j
public class FileUtil {
public static void copy(byte[] in, File out) throws IOException {
Assert.notNull(in, "No input byte array specified");
Assert.notNull(out, "No output File specified");
copy(new ByteArrayInputStream(in), Files.newOutputStream(out.toPath()));
}
public static int copy(InputStream in, OutputStream out) throws IOException {
Assert.notNull(in, "No InputStream specified");
Assert.notNull(out, "No OutputStream specified");
try {
return StreamUtils.copy(in, out);
} finally {
try {
in.close();
} catch (IOException ex) {
}
try {
out.close();
} catch (IOException ex) {
}
}
}
}
相关引用Class:
import org.apache.commons.io.FileUtils;