Java当中实现分片上传
文章目录
![](https://i-blog.csdnimg.cn/blog_migrate/18c9f88e5cef16ea9afca2bbd50bff0c.png)
一:背景
Web端实现大文件上传下载的需求,要求将文件上传到对象存储当中,大文件上传有以下痛点:
- 文件上传超时:原因是前端请求框架限制最大请求时长,后端设置了接口访问的超时时间,或者是 nginx(或其它代理/网关) 限制了最大请求时长。
- 文件大小超限:原因在于后端对单个请求大小做了限制,一般 nginx 和 server 都会做这个限制。
- 上传时间过久
- 由于各种网络原因上传失败,且失败之后需要从头开始。
二:解决方案
1、整体方案
1.前端根据代码中设置好的分片大小将上传的文件切成若干个小文件,分多次请求依次上传,后端再将文件碎片拼接为一个完整的文件,然后再去进行上传
2.如果需要某个碎片上传失败,也不会影响其它文件碎片,只需要重新上传失败的部分就可以了,则需要设计一个表去维护上传的切片相关的一些信息
2、main方法代码实例
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ShardingFileDTO {
//文件名称(包含文件后缀)
private String fileName;
//文件总大小 MB
private String size;
//文件总分片数
private int shardTotal;
//分片文件索引下标
private int shardIndex;
//文件后缀,视频后缀为mp4,图片则为jpg等
private String suffix;
//唯一标识
private String onlyCode;
}
package com.xxy.demotest.controller.ShardingFile;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSON;
import com.xxy.demotest.controller.ShardingFile.model.ShardingFileDTO;
import com.xxy.demotest.haikang.aliyun.ALiYun;
import com.xxy.demotest.result.baseresult.BaseResponse;
import com.xxy.demotest.utils.WorkUtil.FileUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @beLongProjecet: demo-test
* @beLongPackage: com.xxy.demotest.controller.ShardingFile
* @author: xxx
* @createTime: 2023/09/01 15:16
* @description: 分片文件上传
* @version: v1.0
*/
@RestController
@RequestMapping("sharding")
@RequiredArgsConstructor
@Slf4j
public class ShardingFileController {
public static final String shardPath="D:\\test\\sharding\\";
public static final String savePath="D:\\test\\save\\";
private static void excuteFile(ShardingFileDTO dto, MultipartFile multipartFile) throws IOException {
log.info("文件分片上传请求开始,请求参数: {}", JSON.toJSONString(dto));
//获取本地文件夹地址
String fileFolderPath = savePath + dto.getOnlyCode();
log.info("本地文件夹地址,fileFolder的值为:{}", fileFolderPath);
//如果目标文件夹不存在,则直接创建一个
FileUtil.createFolder(fileFolderPath);
//本地文件全路径
String fileFullPath =fileFolderPath + File.separator+ dto.getFileName()+"_"+ dto.getShardIndex()+"."+ dto.getSuffix();
log.info("本地文件全路径,fileFullPath的值为:{}", fileFullPath);
//将分片文件保存到指定路径
multipartFile.transferTo(new File(fileFullPath));
//更新到文件上传表中
//判断当前分片索引是否等于分片总数,如果等于分片总数则执行文件合并
if (dto.getShardIndex()==dto.getShardTotal()) {
//文件合并
log.info("文件分片合并开始");
File dirFile = new File(fileFolderPath);
if (!dirFile.exists()) {
throw new RuntimeException("文件不存在");
}
//分片上传的文件已经位于同一个文件夹下,方便寻找和遍历(当文件数大于十的时候记得排序用冒泡排序确保顺序是正确的)
List<String> filePaths = FileUtil.listFiles(fileFolderPath);
if (CollectionUtils.isNotEmpty(filePaths)) {
//将此里面文件按照索引进行排序
log.info("filePaths的值为:{}", filePaths);
// 使用自定义的Comparator来对文件路径进行排序
Collections.sort(filePaths, new FilePathComparator());
//进行合并,顺序按照索引进行合并
String mergedFilePath =fileFolderPath+File.separator+dto.getFileName();
log.info("生成新的文件的路径,mergedFilePath的值为:{}", mergedFilePath);
mergeFiles(filePaths, mergedFilePath);
//合并完成将新文件上传到对象存储中
String upload = ALiYun.upload(FileUtil.fileToMultipartFile(new File(mergedFilePath)));
log.info("文件最终访问地址,upload的值为:{}", upload);
//可以异步
//删除所有临时切片文件
deleteFolderAndSubfolders(fileFolderPath);
//删除所有切片
deleteFolderAndSubfolders(shardPath+dto.getOnlyCode());
}
}
}
public static void main(String[] args) {
String fastUUID = IdUtil.fastSimpleUUID();
//String sourceFilePath = "C:\\Users\\wonder\\Desktop\\ai测试图片\\切片文件上传\\metacosmic_conference.zip"; // 源文件路径
String sourceFilePath = "C:\\Users\\wonder\\Desktop\\ai测试图片\\人像.png"; // 源文件路径
String outputDirectory = shardPath + fastUUID;
FileUtil.createFolder(outputDirectory);
//封装dto参数
ShardingFileDTO shardingFileDTO = new ShardingFileDTO();
shardingFileDTO.setFileName(new File(sourceFilePath).getName());
shardingFileDTO.setSize("10MB");
shardingFileDTO.setSuffix(getFileExtension(sourceFilePath));
shardingFileDTO.setOnlyCode(fastUUID);
long sliceSize = 5 * 1024 * 1024; // 切片大小,这里设置为5MB
try {
File sourceFile = new File(sourceFilePath);
String fileName = sourceFile.getName();
int lastDotIndex = fileName.lastIndexOf('.');
String suffix = fileName.substring(lastDotIndex + 1);
long fileSize = sourceFile.length(); // 获取文件大小
int sliceNumber = (int) Math.ceil((double) fileSize / sliceSize); // 计算切片数量
log.info("共切割成 " + sliceNumber + " 个文件切片");
shardingFileDTO.setShardTotal(sliceNumber);
FileInputStream fis = new FileInputStream(sourceFile);
byte[] buffer = new byte[(int) sliceSize];
int bytesRead;
List<String> sliceFilePaths = new ArrayList<>();
for (int i = 0; i < sliceNumber; i++) {
int num = i + 1;
shardingFileDTO.setShardIndex(num);
String sliceFileName = "slice_" + num;
String sliceFilePath = outputDirectory + File.separator + sliceFileName+"."+suffix;
// 创建切片文件并写入数据
FileOutputStream fos = new FileOutputStream(sliceFilePath);
bytesRead = fis.read(buffer, 0, (int) sliceSize);
fos.write(buffer, 0, bytesRead);
fos.close();
sliceFilePaths.add(sliceFilePath);
File file = new File(sliceFilePath);
excuteFile(shardingFileDTO,FileUtil.fileToMultipartFile(file));
}
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 合并成新的文件
* @param filePaths
* @param mergedFilePath
*/
public static void mergeFiles(List<String> filePaths, String mergedFilePath) {
try (FileOutputStream fos = new FileOutputStream(mergedFilePath);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
for (String filePath : filePaths) {
try (FileInputStream fis = new FileInputStream(filePath);
BufferedInputStream bis = new BufferedInputStream(fis)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
}
}
log.info("文件合并完成");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获取文件扩展名
* @param fileName
* @return
*/
public static String getFileExtension(String fileName) {
int lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex > 0) {
return fileName.substring(lastDotIndex + 1);
}
return ""; // 如果文件名中没有点,返回空字符串
}
/**
* 删除文件夹下所有的文件
* @param folderPath
*/
public static void deleteFilesInFolder(String folderPath) {
File folder = new File(folderPath);
// 检查文件夹是否存在
if (!folder.exists() || !folder.isDirectory()) {
System.out.println("指定的路径不是一个有效的文件夹.");
return;
}
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
// 删除文件
if (file.delete()) {
System.out.println("已删除文件: " + file.getName());
} else {
System.out.println("无法删除文件: " + file.getName());
}
}
}
}
}
/**
* 删除文件夹
* @param folderPath
*/
public static void deleteFolder(String folderPath) {
File folder = new File(folderPath);
// 删除文件夹
if (folder.exists() && folder.isDirectory()) {
if (folder.delete()) {
System.out.println("已删除文件夹: " + folderPath);
} else {
System.out.println("无法删除文件夹: " + folderPath);
}
}
}
/**
* 删除文件夹中所有文件和子文件夹
* @param folderPath
*/
public static void deleteFolderAndSubfolders(String folderPath) {
File folder = new File(folderPath);
// 检查文件夹是否存在
if (!folder.exists()) {
System.out.println("文件夹不存在.");
return;
}
if (folder.isDirectory()) {
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
// 递归删除子文件夹及其内容
deleteFolderAndSubfolders(file.getAbsolutePath());
} else {
// 删除文件
if (file.delete()) {
System.out.println("已删除文件: " + file.getName());
} else {
System.out.println("无法删除文件: " + file.getName());
}
}
}
}
}
// 删除文件夹本身
if (folder.delete()) {
System.out.println("已删除文件夹: " + folderPath);
} else {
System.out.println("无法删除文件夹: " + folderPath);
}
}
}
class FilePathComparator implements Comparator<String> {
private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+");
@Override
public int compare(String filePath1, String filePath2) {
int number1 = extractNumber(filePath1);
int number2 = extractNumber(filePath2);
return Integer.compare(number1, number2);
}
private int extractNumber(String filePath) {
Matcher matcher = NUMBER_PATTERN.matcher(filePath);
if (matcher.find()) {
return Integer.parseInt(matcher.group());
}
return 0; // 如果找不到数字,则返回0或其他适当的默认值
}
}
3、说明
后端如果一个个调用请求有点麻烦,所以用了一个main方法做下说明,执行的流程为:
程序切片–>保存切片(前端上传)–>上传最后一个切片的时候执行文件合并(后端根据条件索引合并)–>合并完成–>执行上传对象存储–>删除切片文件–>接口响应链接
注意:
正常的接口请求当中需要对excuteFile稍微做下改造,省略掉切片的环节即可;
4、FileUtil中的方法
/**
* 创建文件夹
*
* @param path
*/
public static void createFolder(String path) {
File folder = new File(path);
if (!folder.exists()) {
folder.mkdirs();
}
}
/**
* 获取当前文件夹下面的文件列表
*
* @param folderPath
* @return
*/
public static List<String> listFiles(String folderPath) {
List<String> objects = new ArrayList<>();
File folder = new File(folderPath);
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
objects.add(file.getAbsolutePath());
} else if (file.isDirectory()) {
listFiles(file.getAbsolutePath());
}
}
}
return objects;
}
/**
* File转换为MultipartFile
* @param file
* @return
*/
public static MultipartFile fileToMultipartFile(File file) {
FileItem item = new DiskFileItemFactory().createItem("file"
, MediaType.MULTIPART_FORM_DATA_VALUE
, true
, file.getName());
try (InputStream input = new FileInputStream(file);
OutputStream os = item.getOutputStream()) {
// 流转移
IOUtils.copy(input, os);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid file: " + e, e);
}
return new CommonsMultipartFile(item);
}
三:具体到业务中的实现
1.校验文件MD5是否上传过
2.切片文件上传,前端5MB一切片
3.切片文件上传完之后进行合并文件,创建异步合并任务
4.合并文件之后前端定时进行轮循获取上传结果;20mb以内文件每隔3s;100mb以内每隔5s;大于100mb每隔10s
1、dto方法实例
package com.wondertek.oms.controller.business;
import com.wondertek.center.constants.BusinessCenterApi;
import com.wondertek.center.model.dto.business.CheckSliceFileMd5DTO;
import com.wondertek.center.model.dto.business.MergeSliceFileDTO;
import com.wondertek.center.model.dto.business.UploadSliceFileDTO;
import com.wondertek.center.model.vo.business.CheckSliceFileMd5VO;
import com.wondertek.center.model.vo.business.UploadFileVO;
import com.wondertek.oms.service.business.FileOmsService;
import com.wondertek.web.exception.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* @beLongProjecet: Intelligent_video
* @beLongPackage: com.wondertek.center.controller
* @author: xxy
* @createTime: 2022/06/22 15:37
* @description: 文件上传控制层
* @version: v1.0
*/
@RestController
@Api(value = "Web端切片大文件上传", tags = "Web端切片大文件上传")
public class SliceFileOmsController {
@Autowired
private FileOmsService fileOmsService;
@ApiOperation(value = "1.校验文件MD5是否上传过", notes = "1.校验文件MD5是否上传过")
@PostMapping(value = BusinessCenterApi.WEB_CHECK_FILE_MD5)
public Result<CheckSliceFileMd5VO> checkFileMd5(@RequestBody @Validated CheckSliceFileMd5DTO dto) {
CheckSliceFileMd5VO resultVo = this.fileOmsService.checkFileMd5(dto);
return new Result<>(resultVo);
}
@ApiOperation(value = "2.切片文件上传,前端5MB一切片", notes = "2.切片文件上传,前端5MB一切片")
@PostMapping(value = BusinessCenterApi.WEB_UPLOAD_SLICE_FILE)
public Result uploadSliceFile(@RequestParam("file") MultipartFile multipartFile, @Validated UploadSliceFileDTO dto) {
Boolean resultVo= this.fileOmsService.uploadSliceFile(multipartFile,dto);
return new Result(resultVo);
}
@ApiOperation(value = "3.切片文件上传完之后进行合并文件,创建异步合并任务", notes = "3.切片文件上传完之后进行合并文件,创建异步合并任务")
@PostMapping(value = BusinessCenterApi.WEB_MERGE_SLICE_FILE)
public Result mergeSliceFile(@RequestBody @Validated MergeSliceFileDTO dto) {
Boolean resultVo = this.fileOmsService.mergeSliceFile(dto);
return new Result(resultVo);
}
@ApiOperation(value = "4.合并文件之后前端定时进行轮循获取上传结果;20mb以内文件每隔3s;100mb以内每隔5s;大于100mb每隔10s", notes = "4.合并文件之后前端定时进行轮循获取上传结果;20mb以内文件每隔3s;100mb以内每隔5s;大于100mb每隔10s")
@PostMapping(value = BusinessCenterApi.WEB_GET_MERGE_FILE)
public Result getMergeFile(@RequestBody @Validated MergeSliceFileDTO dto) {
UploadFileVO resultVo = this.fileOmsService.getMergeFile(dto);
return new Result(resultVo);
}
}
package com.wondertek.center.model.dto.business;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* @beLongProjecet: metacosmic_conference
* @beLongPackage: com.wondertek.center.model.dto.business
* @author: xxy
* @createTime: 2022/11/17 11:14
* @description:
* @version: v1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CheckSliceFileMd5DTO {
@ApiModelProperty(value = "文件唯一标识MD5值")
@NotBlank(message = "文件唯一标识MD5值 不能为空")
private String fileKey;
@ApiModelProperty(value = "文件名称加后缀,格式为:test.png")
@NotBlank(message = "文件名称 不能为空")
private String fileName;
@ApiModelProperty(value = "文件后缀,不加.")
@NotBlank(message = "文件后缀 不能为空")
private String fileSuffix;
@ApiModelProperty(value = "文件字节大小")
@NotNull(message = "文件字节大小 不能为空")
private Long fileSize;
}
package com.wondertek.center.model.dto.business;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* @beLongProjecet: metacosmic_conference
* @beLongPackage: com.wondertek.center.model.dto.business
* @author: xxy
* @createTime: 2022/11/17 11:14
* @description:
* @version: v1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UploadSliceFileDTO {
@ApiModelProperty(value = "文件总分片数")
@NotNull(message = "文件总分片数 不能为空")
@Min(value = 0,message = "最小为0")
private Long sliceTotal;
@ApiModelProperty(value = "分片文件索引下标,定义从1开始")
@NotNull(message = "分片文件索引下标 不能为空")
@Min(value = 0,message = "最小为0")
private Long sliceIndex;
@ApiModelProperty(value = "未分片原文件唯一标识MD5值")
@NotBlank(message = "文件唯一标识MD5值 不能为空")
private String fileKey;
@ApiModelProperty(value = "未分片原文件名称,格式为:test.png")
@NotBlank(message = "文件名称 不能为空")
private String fileName;
@ApiModelProperty(value = "未分片原文件名称不加后缀,格式为:test")
@NotBlank(message = "文件名称无后缀 不能为空")
private String fileNameNoSuffix;
@ApiModelProperty(value = "未分片原文件后缀")
@NotBlank(message = "文件后缀 不能为空")
private String fileSuffix;
@ApiModelProperty(value = "未分片原文件字节大小")
@NotNull(message = "文件字节大小 不能为空")
private Long fileSize;
}
package com.wondertek.center.model.dto.business;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* @beLongProjecet: metacosmic_conference
* @beLongPackage: com.wondertek.center.model.dto.business
* @author: xxy
* @createTime: 2022/11/17 11:14
* @description:
* @version: v1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MergeSliceFileDTO {
@ApiModelProperty(value = "未分片原文件唯一标识MD5值")
@NotBlank(message = "文件唯一标识MD5值 不能为空")
private String fileKey;
@ApiModelProperty(value = "未分片原文件名称,格式为:test.png")
@NotBlank(message = "文件名称 不能为空")
private String fileName;
@ApiModelProperty(value = "未分片原文件名称不加后缀,格式为:test")
@NotBlank(message = "文件名称无后缀 不能为空")
private String fileNameNoSuffix;
@ApiModelProperty(value = "未分片原文件后缀")
@NotBlank(message = "文件后缀 不能为空")
private String fileSuffix;
@ApiModelProperty(value = "未分片原文件字节大小")
@NotNull(message = "文件字节大小 不能为空")
private Long fileSize;
}
2、serviceImpl实现方法
2.1检查当前文件是否在系统中存在,如果存在直接返回url
@Override
public CheckSliceFileMd5VO checkFileMd5(CheckSliceFileMd5DTO dto) {
CheckSliceFileMd5VO resultVo = new CheckSliceFileMd5VO();
//检查上传文件类型
Long resourceTypeId = this.checkFileType(dto.getFileSuffix(), dto.getFileSize());
//检查fileKey是否支持秒传
Result<UploadFileRecordVO> uploadFileRecordVOResult = webUploadFileRecordFeignService.selectOneByFileKey(dto.getFileKey());
uploadFileRecordVOResult.assertSuccess();
UploadFileRecordVO data = uploadFileRecordVOResult.getData();
Integer sliceFileType = ObjectUtil.isEmpty(data.getId()) ? SliceFileTypeEnum.NO_UPLOAD.getCode() : SliceFileTypeEnum.FAST_UPLOAD.getCode();
if (ObjectUtil.isNotEmpty(data.getId())) {
UploadFileVO uploadFileVO = new UploadFileVO();
String url = "";
String previewFileUrl = "";
log.debug("当前的环境为:{}", ProfileUtil.getActiveProfile());
if (StringUtils.isNotBlank(ProfileUtil.getActiveProfile()) && "prod".equals(ProfileUtil.getActiveProfile())) {
url = data.getFileUrl();
previewFileUrl = EosUtil.addOuterChainUrl(url);
} else {
url = data.getFileUrl();
previewFileUrl = url;
}
uploadFileVO.setFileUrl(url);
uploadFileVO.setPreviewFileUrl(previewFileUrl);
uploadFileVO.setFileName(dto.getFileName());
uploadFileVO.setFileSize(FileSizeUtil.getByteFileSize(dto.getFileSize(), 3) + "MB");
uploadFileVO.setResourceTypeId(resourceTypeId);
uploadFileVO.setMd5Data(dto.getFileKey());
resultVo.setUploadFileVO(uploadFileVO);
}
resultVo.setSliceFileType(sliceFileType);
return resultVo;
}
2.2上传切片文件
@Override
public Boolean uploadSliceFile(MultipartFile multipartFile, UploadSliceFileDTO dto) {
String fileFolderPath = getFileFolderPath(dto.getFileKey());
log.info("文件分片上传请求开始,请求参数: {}", JSON.toJSONString(dto));
//如果目标文件夹不存在,则直接创建一个
FileUtil.createFolder(fileFolderPath);
//本地文件全路径
String sliceFilePath = fileFolderPath + File.separator + dto.getSliceIndex() + "." + dto.getFileSuffix();
log.info("本地切片文件全路径,sliceFilePath的值为:{}", sliceFilePath);
try {
//将分片文件保存到指定路径
multipartFile.transferTo(new File(sliceFilePath));
} catch (IOException e) {
log.error("保存分片文件异常", e);
e.printStackTrace();
return false;
}
//边上传边随机合并文件
String mergedFilePath = getMergeFilePath(dto.getFileName(), fileFolderPath);
// 切片大小,这里设置为5MB
long sliceSize = 5 * 1024 * 1024;
//合并方式一:随机合并randomAccessFile
//使用了自动资源管理 (try-with-resources) 的方式来打开流,这是一个很好的做法,
//因为它会在代码块结束时自动关闭这些资源,而不需要显式调用 close() 方法,在以下代码中,try 块内的资源在离开 try 块后会自动关闭
try (RandomAccessFile randomAccessFile = new RandomAccessFile(mergedFilePath, "rw")) {
// 写入该分片数据
long startPosition;
if (dto.getSliceIndex() - 1 == dto.getSliceTotal() - 1) {
//根据下标计算剩余的大小
startPosition = dto.getFileSize() - new File(sliceFilePath).length();
} else {
//默认从下标0开始
long l = dto.getSliceIndex() - 1;
startPosition = l * sliceSize;
}
log.info("偏移量startPosition的值为:{}", startPosition);
randomAccessFile.seek(startPosition);
try (FileInputStream inputStream = new FileInputStream(sliceFilePath)) {
byte[] buf = new byte[1024];
int len;
while (-1 != (len = inputStream.read(buf))) {
randomAccessFile.write(buf, 0, len);
}
}
} catch (IOException e) {
log.error("随机合并文件异常", e);
e.printStackTrace();
return false;
}
return true;
}
@Value("${saveFilePath.windows}")
private String windowsPath;
@Value("${saveFilePath.linux}")
private String linuxPath;
/**
* 获取存储文件夹路径
*
* @param fileKey 文件唯一标识md5
* @return
*/
private String getFileFolderPath(String fileKey) {
String fileFolderPath = null;
//获取本地文件夹地址
String savePath = "";
if (SystemUtil.SystemNameEnum.TYPE1.getDesc().equals(SystemUtil.getSystemName())) {
savePath = windowsPath;
} else {
savePath = linuxPath + "/";
}
fileFolderPath = savePath + fileKey;
log.info("本地文件夹地址,fileFolder的值为:{}", fileFolderPath);
return fileFolderPath;
}
/**
* @param fileName 原始文件名称加后缀
* @param fileFolderPath
* @return
*/
private static String getMergeFilePath(String fileName, String fileFolderPath) {
String mergedFilePath = fileFolderPath + File.separator + fileName;
return mergedFilePath;
}
2.3合并切片文件
//3.合并切片文件
@Override
public Boolean mergeSliceFile(MergeSliceFileDTO dto) {
log.info("开始合并文件,入参为:{}", JSON.toJSONString(dto));
//封装请求参数
String fileFolderPath = getFileFolderPath(dto.getFileKey());
String mergeFilePath = getMergeFilePath(dto.getFileName(), fileFolderPath);
File mergeFile = new File(mergeFilePath);
//合并的时候去判断此fileKey是否存在
Result<UploadFileRecordVO> uploadFileRecordVOResult = webUploadFileRecordFeignService.selectOneByFileKey(dto.getFileKey());
uploadFileRecordVOResult.assertSuccess();
UploadFileRecordVO data = uploadFileRecordVOResult.getData();
if (Objects.isNull(data.getId())) {
//文件不存在
if (ObjectUtil.isEmpty(mergeFile)) {
//删除本地临时文件
FileUtil.deleteFolderAndSubfolders(fileFolderPath);
throw new BizException(ErrorCodeEnum.NOT_FILE_NOT_EXIST);
}
//文件合并失败,请重新上传
log.info("入参当中计算的dto.getFileSize()的值为:{}", dto.getFileSize());
log.info("合并文件后计算的mergeFile.length()的值为:{}", mergeFile.length());
if (dto.getFileSize() != mergeFile.length()) {
//删除本地临时文件
FileUtil.deleteFolderAndSubfolders(fileFolderPath);
throw new BizException(ErrorCodeEnum.MERGE_FILE_ERROR);
}
}
CompletableFuture.runAsync(() -> {
if (Objects.isNull(data.getId())) {
log.info("创建异步执行任务成功!开始执行合并,文件标识为:{}", dto.getFileKey());
//将文件保存到文件系统,返回文件链接
UploadFileVO uploadFileVO = this.uploadFile(FileUtil.fileToMultipartFile(mergeFile));
log.info("文件上传响应,uploadFileVO的值为:{}", JSON.toJSONString(uploadFileVO));
//将此次记录入库
AddUploadFileRecordDTO addUploadFileRecordDTO = new AddUploadFileRecordDTO();
addUploadFileRecordDTO.setFileKey(dto.getFileKey());
addUploadFileRecordDTO.setFileUrl(uploadFileVO.getFileUrl());
addUploadFileRecordDTO.setFileName(dto.getFileName());
addUploadFileRecordDTO.setFileSuffix(dto.getFileSuffix());
addUploadFileRecordDTO.setFileSize(dto.getFileSize());
log.info("保存相应记录到系统中,addUploadFileRecordDTO的值为:{}", JSON.toJSONString(addUploadFileRecordDTO));
Result<Boolean> insertResult = webUploadFileRecordFeignService.insert(addUploadFileRecordDTO);
insertResult.assertSuccess();
log.info("保存相应记录到系统中,insertResult的值为:{}", JSON.toJSONString(insertResult));
//删除本地临时文件
FileUtil.deleteFolderAndSubfolders(fileFolderPath);
log.info("删除临时文件完成,fileKey为:{}", dto.getFileKey());
log.info("创建异步执行任务结束!文件标识为:{}", dto.getFileKey());
}
}, poolExecutor);
return true;
}
2.4前端定时轮循结果
@Override
public UploadFileVO getMergeFile(MergeSliceFileDTO dto) {
//合并的时候去判断此fileKey是否存在
Result<UploadFileRecordVO> uploadFileRecordVOResult = webUploadFileRecordFeignService.selectOneByFileKey(dto.getFileKey());
uploadFileRecordVOResult.assertSuccess();
UploadFileRecordVO data = uploadFileRecordVOResult.getData();
if (Objects.nonNull(data.getId())) {
UploadFileVO uploadFileVO = new UploadFileVO();
String url = "";
String previewFileUrl = "";
log.debug("当前的环境为:{}", ProfileUtil.getActiveProfile());
if (StringUtils.isNotBlank(ProfileUtil.getActiveProfile()) && "prod".equals(ProfileUtil.getActiveProfile())) {
url = data.getFileUrl();
previewFileUrl = EosUtil.addOuterChainUrl(url);
} else {
url = data.getFileUrl();
previewFileUrl = url;
}
uploadFileVO.setFileUrl(url);
uploadFileVO.setPreviewFileUrl(previewFileUrl);
uploadFileVO.setFileName(dto.getFileName());
uploadFileVO.setFileSize(FileSizeUtil.getByteFileSize(dto.getFileSize(), 3) + "MB");
uploadFileVO.setResourceTypeId(this.checkFileType(dto.getFileSuffix(), dto.getFileSize()));
uploadFileVO.setMd5Data(dto.getFileKey());
log.info("fileKey在此系统中已存在,直接返回结果,相应信息为:{}", JSON.toJSONString(uploadFileVO));
return uploadFileVO;
}
return null;
}
2.4 SQL建表语句
CREATE TABLE `upload_file_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`file_key` varchar(255) NOT NULL COMMENT '文件唯一标识MD5值',
`file_url` text NOT NULL COMMENT '文件上传到对象存储的地址',
`file_name` varchar(255) NOT NULL COMMENT '文件名称',
`file_suffix` varchar(20) NOT NULL COMMENT '文件后缀',
`file_size` bigint(20) NOT NULL COMMENT '文件字节大小',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0:未删除 1:已删除',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=93 DEFAULT CHARSET=utf8mb4 COMMENT='文件上传表';
参考: