JAVA大文件分段上传+断点续传
- 大文件分段上传+断点续传
- 1.思路解析
- 2.代码实现
大文件分段上传+断点续传
1.思路解析
思路是为了解决实际业务中大文件上传,中途网络中断和充分利用多请求,加速上传,保存上传记录。
针对上述问题,我们采用将大文件进行拆分,拆分成若干个临时小文件,进行上传。每个临时文件进行记录。有了这个思路,那么在上传若干个临时文件时,就需要进行告知服务端,本次上传的大文件基本信息,衍生出第一步操作,初始化接口。
那么接下来,基本思路大概逻辑如下:
-后端接口
- 初始化接口—告知服务端,并记录,后端提供该文件唯一标识
- 单个拆分后的文件分片上传—若干分片上传,断点续传
- 合并接口—完成上传,进行合并,删除临时文件
-前端
- 文件拆分
- 多个文件上传,采取文件下标
- 完成上传
2.代码实现
①接口 “/initMultipart” 实现:
初始化—告知服务端,并记录,后端提供该文件唯一标识
public Object initMultipart(BurstFile burstFile) {
String digest = SecureUtil.md5(burstFile.getOriginName() + burstFile.getFileSize());
burstFile.setMdSign(digest);
burstFile.setIsSuccess("0");
return fileDomainService.createBurstFile(burstFile);
}
/**
* 初始化接口参数
**/
@Data
public static class BurstFileInitDto {
/**
* 文件名称
*/
@NotBlank(message = "文件名称不能为空")
private String originName;
/**
* 后缀
*/
@NotBlank(message = "文件后缀不能为空")
private String suffix;
/**
* 文件大小
*/
@NotBlank(message = "文件大小不能为空")
private String fileSize;
/**
* 分片数
*/
@NotBlank(message = "分片数不能为空")
private String burstNum;
}
②接口 “/uploadMultipart” 实现
上传文件分片+断点续传
@SneakyThrows
public void uploadMultipart(BurstFile burstFile) {
BurstFile db= fileDomainService.findByPartNumberAndMdSign( burstFile.getPartNumber(),burstFile.getMdSign());
if (db != null){
return;
}
//上传分片文件
String dates= ObjectUtil.formatYYYYMMDD();//文件上传的年月目录
creatDir();
String path = "/mountfiles/files/"+dates.substring(0, 4)+"/"+dates.substring(4, 6) + "/"+burstFile.getMdSign();
File dirFile = new File(url+path);
if (!dirFile.exists()) {
dirFile.mkdirs();
System.out.println("成功创建目录:" + burstFile.getMdSign());
}
String filename = burstFile.getFile().getOriginalFilename();
Long size = burstFile.getFile().getSize();
dirFile = new File(url+path+"/"+filename);
MultipartFile file = burstFile.getFile();
file.transferTo(dirFile);
burstFile.setOriginName(filename);
burstFile.setIsSuccess("1");
burstFile.setPartSize(size);
burstFile.setTempUrl(url+path+"/"+filename);
fileDomainService.createBurstFile(burstFile);
}
/**
* 分片文件接口参数
**/
@Data
public class BurstFileDto {
private MultipartFile file;
@NotBlank(message = "文件md5不能为空")
private String mdSign;
@NotNull(message = "分片索引不能为空")
private Integer partNumber;
}
③接口 “/completeMultipart” 实现
完成分片上传,进行合并
@SneakyThrows
@Transactional(rollbackOn = Exception.class)
public Object completeMultipart(String id) {
if (StringUtils.isEmpty(id)){
ValueUtil.isError("文件主键不得为null");
}
BurstFile db = fileDomainService.burstFileDetail(id);
if (db == null){
ValueUtil.isError("未查询到初始化信息");
}
String mdSign = db.getMdSign();
String burstNum = db.getBurstNum();
List<BurstFile> burstFileList= fileDomainService.findByMdSign(mdSign);
if (StringUtils.isEmpty(burstFileList)){
ValueUtil.isError("分片暂未全部上传完毕");
}
List<BurstFile> buDBs = burstFileList.stream().filter(dbFile -> !dbFile.getId().equals(id)).collect(Collectors.toList());
List<File> collect = buDBs.stream().sorted(Comparator
.comparing(BurstFile::getPartNumber)).map(burstFile->new File(burstFile.getTempUrl())).collect(Collectors.toList());
if ( Integer.parseInt(burstNum)!=(collect.size())){
ValueUtil.isError("分片暂未全部上传完毕");
}
File file = collect.get(0).getParentFile();
String from=file.getPath();
creatDir();
String filename = PrimaryUtil.nextId();
String dates= ObjectUtil.formatYYYYMMDD();//文件上传的年月目录
String path = "/mountfiles/files/"+dates.substring(0, 4)+"/"+dates.substring(4, 6) + "/"+filename +"."+db.getSuffix() ;
FileSpiltAndMerge.merge(collect,url+path);
FileUtil.clean(from);
file.delete();
db.setIsSuccess("1");
db.setTempUrl(path);
buDBs.forEach(burstFile -> delBurstFile(burstFile.getId()));
return fileDomainService.createBurstFile(db);
}
@SneakyThrows
private void creatDir() {
File dirFile = new File(url);
String dates= ObjectUtil.formatYYYYMMDD();//文件上传的年月目录
if (!dirFile.exists()) {
dirFile.mkdirs();
System.out.println("成功创建目录:" + dirFile.getCanonicalFile());
}
dirFile = new File(url +"/mountfiles");
if (!dirFile.exists()) {
dirFile.mkdirs();
System.out.println("成功创建mountfiles目录:" + dirFile.getCanonicalFile());
}
dirFile = new File(dirFile +"/files");
if (!dirFile.exists()) {
dirFile.mkdirs();
System.out.println("成功创建files目录:" + dirFile.getCanonicalFile());
}
dirFile = new File(dirFile + "/"+dates.substring(0, 4) );
System.out.println("拼接年的目录:" + dirFile);
if (!dirFile.exists()) {
dirFile.mkdirs();
System.out.println("成功创建目录:" + dirFile.getCanonicalFile());
}
dirFile = new File(dirFile +"/"+dates.substring(4, 6) );
System.out.println("拼接月的目录:" + dirFile);
if (!dirFile.exists()) {
dirFile.mkdirs();
System.out.println("成功创建目录:" + dirFile.getCanonicalFile());
}
}
④合并文件工具类FileSpiltAndMerge
package ddd.special.infrastructure.util;
/**
* @author NJ
* @create 2022/10/10 15:15
*/
import java.io.*;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class FileSpiltAndMerge {
public static void main(String[] args) throws IOException {
// spilt("E:\\home\\test\\a.jpg", 100, "E:\\home\\test\\a");
// merge("E:\\home\\test\\a", "E:\\home\\test\\b.jpg");
// mergeUploadFile("E:\\home\\test\\a\\0", 0,"E:\\home\\test\\b.jpg");
mergeUploadFile("E:\\home\\test\\a\\1", 102400,"E:\\home\\test\\b.jpg");
// mergeUploadFile("E:\\home\\test\\a\\2", 204800,"E:\\home\\test\\b.jpg");
}
public static void spilt(String from, int size, String to) throws IOException {
File f = new File(from);
FileInputStream in = new FileInputStream(f);
FileOutputStream out = null;
FileChannel inChannel = in.getChannel();
FileChannel outChannel = null;
// 将MB单位转为为字节B
long m = size * 1024 ;
// 计算最终会分成几个文件
int count = (int) (f.length() / m);
for (int i = 0; i <= count; i++) {
// 生成文件的路径
String t = to + "/" + i;
try {
out = new FileOutputStream(new File(t));
outChannel = out.getChannel();
// 从inChannel的m*i处,读取固定长度的数据,写入outChannel
if (i != count)
inChannel.transferTo(m * i, m, outChannel);
else// 最后一个文件,大小不固定,所以需要重新计算长度
inChannel.transferTo(m * i, f.length() - m * count, outChannel);
} catch (IOException e) {
e.printStackTrace();
} finally {
out.close();
outChannel.close();
}
}
in.close();
inChannel.close();
}
public static void merge(List<File> list, String to) throws IOException {
File t = new File(to);
FileInputStream in = null;
FileChannel inChannel = null;
FileOutputStream out = new FileOutputStream(t, true);
FileChannel outChannel = out.getChannel();
// 获取目录下的每一个文件名,再将每个文件一次写入目标文件
// 记录新文件最后一个数据的位置
long start = 0;
for (File file : list) {
in = new FileInputStream(file);
inChannel = in.getChannel();
// 从inChannel中读取file.length()长度的数据,写入outChannel的start处
outChannel.transferFrom(inChannel, start, file.length());
start += file.length();
in.close();
inChannel.close();
}
out.close();
outChannel.close();
}
public static void merge(String from, String to) throws IOException {
File t = new File(to);
FileInputStream in = null;
FileChannel inChannel = null;
FileOutputStream out = new FileOutputStream(t, true);
FileChannel outChannel = out.getChannel();
File f = new File(from);
// 获取目录下的每一个文件名,再将每个文件一次写入目标文件
if (f.isDirectory()) {
List<File> list = getAllFileAndSort(from);
// 记录新文件最后一个数据的位置
long start = 0;
for (File file : list) {
in = new FileInputStream(file);
inChannel = in.getChannel();
// 从inChannel中读取file.length()长度的数据,写入outChannel的start处
outChannel.transferFrom(inChannel, start, file.length());
start += file.length();
in.close();
inChannel.close();
}
}
out.close();
outChannel.close();
}
private static void mergeUploadFile(String from, int start, String to) throws IOException {
File t = new File(to);
FileInputStream in = null;
FileChannel inChannel = null;
FileOutputStream out = new FileOutputStream(t, true);
FileChannel outChannel = out.getChannel();
File file = new File(from);
// 获取目录下的每一个文件名,再将每个文件一次写入目标文件
if (file.exists()) {
in = new FileInputStream(file);
inChannel = in.getChannel();
// 从inChannel中读取file.length()长度的数据,写入outChannel的start处
outChannel.transferFrom(inChannel, start, file.length());
start += file.length();
in.close();
inChannel.close();
}
out.close();
outChannel.close();
}
private static List<File> getAllFileAndSort(String dirPath) {
File dirFile = new File(dirPath);
File[] listFiles = dirFile.listFiles();
List<File> list = Arrays.asList(listFiles);
Collections.sort(list, (o1, o2) -> {
return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());
});
return list;
}
}