本文主要介绍了文件上传——文件秒传;其实是文件分片上传的一个延续,就是通过计算文件分片的md5值,然后去后端请求校验哪些分片是后端服务器已经存在的,并对这些分片文件做好标记(如后端文件索引,分片数量,不需要上传的分片标识集合等)返回给前端,那么前端在上传分片文件的时候就不需要上传后端服务器已经存在的分片文件了,这样就减少了分片上传的时候就不用上传所有分片文件了,减少上传时间等问题,同时也减少分片上传的请求次数。
大文件分片上传博文地址:大文件分片上传
大文件断点续传博文地址:大文件断点续传
有什么问题可以在留言哦!并在文章末尾附上demo源码下载!
一、创建前后端项目
创建前后端项目,采用springboot项目结构,前端采用的vue框架搭建(前端完全是边用边百度,这里就不附前面项目结构,就一个页面)
二、文件(md5)秒传核心代码
1、后端核心代码
根据需要上传的所有分片md5信息进行检测后端服务器已有的分片信息代码
/**
* 根据传入需要上传的文件片段的md5值来对比服务器中的文件的md5值,将已有对应的md5值的文件过滤出来,
* 通知前端或者自行出来这些文件,即为不需要上传的文件分片,并将已有的文件分片地址索引返回给前端进行出来
* @param upLoadFileListMd5 原本需要上传文件的索引分片信息
* @return
*/
@Override
public Result<List<DiskFileIndexVo>> checkDiskFile(List<DiskFileIndexVo> upLoadFileListMd5) {
// upLoadFileListMd5.forEach(System.out::println);
List<DiskFileIndexVo> notUploadFile;
try {
//后端服务器已经存在的分片md5值集合
// List<DiskFileIndexVo> diskFileMd5IndexList = getDiskFileMd5Index();
List<DiskFileIndexVo> diskFileMd5IndexList = diskFileIndexVos;
notUploadFile = upLoadFileListMd5.stream().filter(uf -> diskFileMd5IndexList.stream().anyMatch(
df -> {
if(df.getFileMd5().equals(uf.getFileMd5())){
uf.setFileIndex(df.getFileName());//不需要上传文件的地址索引
return true;
}
return false;
})).collect(Collectors.toList());
// notUploadFile = notUploadFile.stream().collect(Collectors.collectingAndThen(
// Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(DiskFileIndexVo::getFileMd5))), ArrayList::new));
log.info("过滤出不需要上传的文件分片:{}",notUploadFile);
} catch (Exception e) {
log.error("上传文件检测异常!",e);
return Result.error("上传文件检测异常!");
}
return Result.success(notUploadFile);
}
对已经过滤出需要上传的分片文件进行接受和合并分片代码(合并分片采用多线程来完成的)
/**
* 多文件(分片)秒传
* 通过对比已有的文件分片md5值和需要上传文件分片的MD5值,
* 在文件分片合并的时候,对已有的文件进行地址索引,对没有的文件进行临时文件写入
* 最后合并的时候根据不同的文件分片进行文件读取写入
* @param filePart 上传没有的分片文件
* @param fileInfo 当前分片文件相关信息
* @param fileOther 已存在文件分片相关信息
* @return
*/
@Override
public Result<String> multipleFilePartFlashUpload(MultipartFile filePart, String fileInfo, String fileOther) {
DiskFileIndexVo upFileInfo = JSONObject.parseObject(fileInfo, DiskFileIndexVo.class);
List<DiskFileIndexVo> notUpFileInfoList = JSON.parseArray(fileOther, DiskFileIndexVo.class);
//实际情况下,这些路径都应该是服务器上面存储文件的路径
String filePath = System.getProperty("user.dir") + "\\file\\";//文件存放路径
//正常情况下,这个临时文件也应该放入(服务器)非临时文件夹中,这样方便下次其他文件上传查找是否曾经上传过类似的
//当前demo是单独存放在临时文件夹中,文件合并完成之后直接删除的
String tempPath = filePath + "temp\\" + upFileInfo.getFileUid();//临时文件存放路径
File dir = new File(tempPath);
if (!dir.exists()) dir.mkdirs();
//生成一个临时文件名
String tempFileNamePath = tempPath + "\\" + upFileInfo.getFileName() + "_" + upFileInfo.getPartIndex() + ".part";
try {
filePart.transferTo(new File(tempFileNamePath));
File tempDir = new File(tempPath);
File[] tempFiles = tempDir.listFiles();
notUpFileInfoList = notUpFileInfoList.stream().filter(e ->
upFileInfo.getFileUid().equals(e.getFileUid())).collect(Collectors.toList());
//如果临时文件夹中分片数量和实际分片数量一致的时候,就需要进行分片合并
one: if ((upFileInfo.getPartNum()-notUpFileInfoList.size()) == tempFiles.length){
//需要校验一下,表示已有异步程序正在合并了;如果是分布式这个校验可以加入redis的分布式锁来完成
if (isMergePart.get(upFileInfo.getFileUid()) != null) {
break one;
}
isMergePart.put(upFileInfo.getFileUid(),tempFiles.length);
System.out.println(upFileInfo.getFileName() + ":所有分片上传完成,预计总分片:" + upFileInfo.getPartNum()
+ "; 实际总分片:" + tempFiles.length + "; 已存在分片数:" + notUpFileInfoList.size());
//使用多线程来完成对每个文件的合并
Future<Integer> submit = partMergeTask.submit(
new PartMergeFlashTaskExecutor(filePath, upFileInfo,notUpFileInfoList));
// System.out.println("上传文件名:" + upFileInfo.getFileName() + "; 总大小:" + submit.get());
isMergePart.remove(upFileInfo.getFileUid());
}
}catch (Exception e){
log.error("{}:多文件(分片)秒传失败!",upFileInfo.getFileName(),e);
return Result.error("","多文件(分片)秒传失败!");
}
//通过返回成功的分片值,来验证分片是否有丢失
return Result.success(upFileInfo.getPartIndex().toString(),upFileInfo.getFileUid());
}
各个文件分片合并多线程执行器代码
/**
* @ClassName: ScheduledTask
* @Author: jdh
* @CreateTime: 2022-07-05
* @Description: 多文件(分片)秒传,已存在分片和上传分片文件合并写入执行器
*/
@Slf4j
public class PartMergeFlashTaskExecutor implements Callable<Integer> {
private String filePath;
private String tempPath;
private DiskFileIndexVo upFileInfo;
private List<DiskFileIndexVo> diskFileIndexVos;
public PartMergeFlashTaskExecutor() {
super();
}
public PartMergeFlashTaskExecutor(String filePath, DiskFileIndexVo upFileInfo, List<DiskFileIndexVo> diskFileIndexVos){
this.filePath = filePath;
this.tempPath = filePath;
this.upFileInfo = upFileInfo;
this.diskFileIndexVos = diskFileIndexVos;
}
@Override
public Integer call() throws Exception {
System.out.println("当前正在合并的文件是:" + upFileInfo.getFileName());
// diskFileIndexVos.forEach(System.out::println);
InputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
Integer partSizeTotal = 0;
try {
fileOutputStream = new FileOutputStream(filePath + upFileInfo.getFileName());
for (int i = 0; i < upFileInfo.getPartNum(); i++) {
int index = i;
//获取是上传的临时文件还是服务器已有的临时文件
if (diskFileIndexVos.size() != 0 && diskFileIndexVos.stream().anyMatch(d -> d.getPartIndex() == index)) {
DiskFileIndexVo diskFileIndexVo = diskFileIndexVos.stream().filter(d -> d.getPartIndex() == index).findFirst().get();
tempPath = filePath + "part\\" + diskFileIndexVo.getFileIndex();
}else {
tempPath = filePath + "temp\\" + upFileInfo.getFileUid() + "\\" + upFileInfo.getFileName() + "_" + i + ".part";
}
fileInputStream = new FileInputStream(tempPath);
byte[] buf = new byte[1024 * 8];//8MB
int length;
while ((length = fileInputStream.read(buf)) != -1) {//读取fis文件输入字节流里面的数据
fileOutputStream.write(buf, 0, length);//通过fos文件输出字节流写出去
partSizeTotal += length;
}
fileInputStream.close();
}
tempPath = filePath + "temp\\" + upFileInfo.getFileUid() + "\\";
//todo 这里可以校验一下文件是否合并完成,并将本次所上传的分片且服务器路径没有的分片文件保存一下;然后在删除临时分片文件
// 删除临时文件夹里面的分片文件 如果使用流操作且没有关闭输入流,可能导致删除失败
for (int i = 0; i < upFileInfo.getPartNum(); i++) {
int index = i;
if (diskFileIndexVos.stream().noneMatch(d -> d.getPartIndex() == index)) {
boolean delete = new File(tempPath + upFileInfo.getFileName() + "_" + i + ".part").delete();
File file = new File(tempPath + upFileInfo.getFileName() + "_" + i + ".part");
// System.out.println(i + "; 是否删除:" + delete + " ; 是否还存在:" + file.exists());
}
}
//在删除对应的临时文件夹
File tempDir = new File(tempPath);
if (tempDir.listFiles().length == 0) {
boolean delete = tempDir.delete();
// System.out.println("文件夹: " + tempPath + ";是否删除: " + delete);
}
}catch (Exception e){
log.error("{}:文件分片合并失败!",upFileInfo.getFileName(),e);
throw new Exception(e);
}finally {
try {
if (fileOutputStream != null) {
fileOutputStream.flush();
fileOutputStream.close();
}
} catch (Exception e){
log.error("{} 文件分片合并完成后,关闭输入输出流错误!",upFileInfo.getFileName(),e);
e.printStackTrace();
}
}
return partSizeTotal;
}
}
2、前端核心代码
代码讲解:需要在分片之前将需要分片上传的文件的文件名,文件uuid,分片总数等相关信息计算出来,然后在循环里面对文件片段上传的时候在将当前分片值一起传给后端。
文件uuid:主要是后端在合并分片的时候才知道哪些分片是一起的;
文件名:后端合并分片的之后对文件进行命名保存;
分片MD5值:分片文件的md5值;
分片总数:后端计算是否所有分片都上传完成了;
当前分片:后端保存分片的时候命名索引,方便合并的时候按照分片索引进行合并;
注意:前端循环计算md5值demo采用阻塞模式计算的,不然不好归纳所有分片文件的md5值
计算文件md5值的代码
下面一种是直接返回md5值,另外一种是单纯的先读取文件流
三、源码和结尾
gitee后端链接:java_fileUpload_demo: 关于文件上传的一些基本介绍(如文件上传、分片上传、md5值秒传、断点续传等)
gitee前端链接:vue2_demo: 各实现功能的前端页面支撑,采用vue2
其实思路很简单也很明确,先将一个大文件分成n个小文件并计算所有小文件的md5值,然后再给后端检测这些分片的md5值的文件是否存在,即对这些分片进行过滤出来,并将过滤结果返回给前端处理出不需要上传的分片和需要上传的文件分片。
再次说明下,为了方便大家直接能够使用demo源码,本文所有都采用纯java模式,没有加入任何中间件和其他数据存储服务器来对数据处理;但是实际情况很多操作都不可能依靠纯java代码模式,毕竟这样操作起来麻烦,而且文件数据或者其他相关数据标识信息容易丢失;都会使用第三方技术来做相关的功能。