现在很多地方都会用到文件上传,但是遇到大文件就会出现各种花式问题,为了尽量避免这些坑,所以我们可以采用分片上传的方式将大文件分成不同的小文件进行上传
一、新建 CheckMd5FileVO 文件以及 UploadVO 文件
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 文件MD5校验VO
*/
@Data
@ApiModel("文件MD5校验VO")
public class CheckMd5FileVO {
/**
* 0:不分片,1:分片
*/
@ApiModelProperty(value = "分片状态 0:不分片,1:分片", required = true)
private Integer type;
/**
* 文件名
*/
@ApiModelProperty(value = "文件名", required = true)
private String fileName;
/**
* 文件Md5(文件唯一表示)
*/
@ApiModelProperty(value = "文件Md5(文件唯一表示)", required = true)
private String fileMd5;
/**
* 当前分片下标
*/
@ApiModelProperty(value = "当前分片下标", required = true)
private Long chunk;
/**
* 文件大小(如果分片了,则是分片文件大小)
*/
@ApiModelProperty(value = "文件大小 如果分片了,则是分片文件大小", required = true)
private Long fileSize;
private String formData;
@ApiModelProperty(value = "文件后缀名", required = true)
public String getSuffix() {
return fileName.substring(fileName.lastIndexOf(".") + 1);
}
}
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
* 文件上传VO
*/
@Data
@ApiModel("文件上传VO")
public class UploadVO {
/***
* 文件id WU_FILE_0
*
*/
private String id;
/**
* 文件名称 Beyond Compare.rar
*/
@ApiModelProperty(value = "文件名称", required = true)
private String name;
/**
* 类型 application/octet-stream
*/
private String type;
/**
* 文件大小
*/
private Long size;
/**
* 最后修改时间
*/
private Date lastModifiedDate;
/**
* 分片片数
*/
@ApiModelProperty(value = "分片片数", required = true)
private Long chunks;
/**
* 当前分片标识
*/
@ApiModelProperty(value = "当前分片标识", required = true)
private Long chunk;
/**
* 分片设置大小
*/
@ApiModelProperty(value = "分片设置大小", required = true)
private Long chunkSize;
/**
* 表单数据
*/
private String formData;
/**
* 文件Md5(文件的唯一标识)
*/
@ApiModelProperty(value = "文件md5值", required = true)
private String fileMd5;
@ApiModelProperty(value = "文件名后缀", required = true)
public String getSuffix() {
return name.substring(name.lastIndexOf(".") + 1);
}
}
二、新建 FileUtil 文件
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.nio.channels.FileChannel;
import java.util.*;
@Slf4j
public class FileUtil {
public static void randomAccessFile(File in, File out, Long seek) throws IOException {
RandomAccessFile raFile = null;
BufferedInputStream inputStream = null;
try {
// 以读写的方式打开目标文件
raFile = new RandomAccessFile(out, "rw");
raFile.seek(seek);
inputStream = new BufferedInputStream(new FileInputStream(in));
byte[] buf = new byte[1024];
int length = 0;
while ((length = inputStream.read(buf)) != -1) {
raFile.write(buf, 0, length);
}
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (raFile != null) {
raFile.close();
}
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
}
/**
* 删除单个文件
*
* @param sPath 被删除文件的文件名
* @return 单个文件删除成功返回true,否则返回false
*/
public static boolean deleteFile(String sPath) {
boolean flag = false;
File file = new File(sPath);
// 路径为文件且不为空则进行删除
if (file.isFile() && file.exists()) {
file.delete();
flag = true;
}
return flag;
}
/**
* 删除目录(文件夹)以及目录下的文件
*
* @param sPath 被删除目录的文件路径
* @return 目录删除成功返回true,否则返回false
*/
public static boolean deleteDirectory(String sPath) {
// 如果sPath不以文件分隔符结尾,自动添加文件分隔符
if (!sPath.endsWith(File.separator)) {
sPath = sPath + File.separator;
}
File dirFile = new File(sPath);
// 如果dir对应的文件不存在,或者不是一个目录,则退出
if (!dirFile.exists() || !dirFile.isDirectory()) {
return false;
}
boolean flag = true;
// 删除文件夹下的所有文件(包括子目录)
File[] files = dirFile.listFiles();
for (int i = 0; i < files.length; i++) {
// 删除子文件
if (files[i].isFile()) {
flag = deleteFile(files[i].getAbsolutePath());
if (!flag)
break;
} // 删除子目录
else {
flag = deleteDirectory(files[i].getAbsolutePath());
if (!flag)
break;
}
}
if (!flag)
return false;
// 删除当前目录
if (dirFile.delete()) {
return true;
} else {
return false;
}
}
}
三、新建 ChunkUploadService 文件
import com.boran.demonstration.config.CustomConfig;
import com.boran.demonstration.config.Result;
import com.boran.demonstration.enums.ErrorCodeEnum;
import com.boran.demonstration.upload.util.FileUtil;
import com.boran.demonstration.upload.vo.CheckMd5FileVO;
import com.boran.demonstration.upload.vo.UploadVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 代码中对应的以 CustomConfig.xxxx 的自行更换为自己对应的文件路径
* 代码中出现的 ErrorCodeEnum.xxxx 的自行更换为自己对应的错误提示码
*/
@Service
@Slf4j
public class ChunkUploadService {
private static final String DELIMITER = "-";
/**
* 上传之前校验(整个文件、分片)
*
* @param md5FileVO
* @return
*/
public Result check(CheckMd5FileVO md5FileVO) {
if (md5FileVO.getType() == null || md5FileVO.getChunk() == null || md5FileVO.getFileMd5() == null || md5FileVO.getSuffix() == null || md5FileVO.getFileName() == null) {
return Result.error(ErrorCodeEnum.MISSING_REQUIRED_ARGUMENTS);
}
Integer type = md5FileVO.getType();
Long chunk = md5FileVO.getChunk();
String fileName = md5FileVO.getFileMd5() + "." + md5FileVO.getSuffix();
Long fileSize = md5FileVO.getFileSize();
if (type == 0) {// 未分片校验
String destFilePath = CustomConfig.fileSave + File.separator + fileName;
File destFile = new File(destFilePath);
if (destFile.exists() && destFile.length() == fileSize) {
Map<String, Object> map = new HashMap<>();
map.put("url", fileName);
map.put("size", fileSize);
return Result.ok(map);
} else {
return Result.error(ErrorCodeEnum.FILE_NOT_EXISTS);
}
} else {// 分片校验
String fileMd5 = md5FileVO.getFileMd5();
String destFileDir = CustomConfig.fileSave + File.separator + fileMd5;
String destFileName = chunk + DELIMITER + fileName;
String destFilePath = destFileDir + File.separator + destFileName;
File destFile = new File(destFilePath);
if (destFile.exists() && destFile.length() == fileSize) {
return Result.error(ErrorCodeEnum.CHUNK_EXISTS);
} else {
return Result.error(ErrorCodeEnum.CHUNK_NOT_EXISTS);
}
}
}
/**
* 分片上传
*
* @param file
* @param uploadVO
* @return
*/
public Result ChunkUploads(MultipartFile file, UploadVO uploadVO) {
String fileMd5 = uploadVO.getFileMd5();
String fileName = fileMd5 + "." + uploadVO.getSuffix();
Long chunk = uploadVO.getChunk();// 当前片
Long chunks = uploadVO.getChunks();// 总共多少片
// 分片目录创建
String chunkDirPath = CustomConfig.fileDispose + File.separator + fileMd5;
File chunkDir = new File(chunkDirPath);
if (!chunkDir.exists()) {
chunkDir.mkdirs();
}
// 分片文件上传
String chunkFileName = chunk + DELIMITER + fileName;
String chunkFilePath = chunkDir + File.separator + chunkFileName;
File chunkFile = new File(chunkFilePath);
try {
file.transferTo(chunkFile);
} catch (Exception e) {
log.error("分片上传出错", e);
return Result.error(ErrorCodeEnum.CHUNK_UPLOAD_ERROR);
}
// 合并分片
Long chunkSize = uploadVO.getChunkSize();
long seek = chunkSize * chunk;
String destFilePath = CustomConfig.fileSave + File.separator + fileName;
File destFile = new File(destFilePath);
if (chunkFile.length() > 0) {
try {
System.out.println("合并中......");
FileUtil.randomAccessFile(chunkFile, destFile, seek);
} catch (IOException e) {
log.error("分片{}合并失败:{}", chunkFile.getName(), e.getMessage());
return Result.error(ErrorCodeEnum.CHUNK_MERGE_FAIL);
}
}
if (chunk == chunks - 1) {
// 删除分片文件夹
System.out.println("删除分片文件夹......");
FileUtil.deleteDirectory(chunkDirPath);
Map<String, Object> map = new HashMap<>();
map.put("url", fileName);
return Result.ok(map);
} else {
return Result.error(ErrorCodeEnum.UPLOADING);
}
}
/**
* 未分片上传
*
* @param file
* @param uploadVO
* @return
*/
public Result UnChunkUploads(MultipartFile file, UploadVO uploadVO) {
String suffix = uploadVO.getSuffix();
String fileName = uploadVO.getFileMd5() + "." + suffix;
// 文件上传
File destFile = new File(CustomConfig.fileSave + File.separator + fileName);
if (file != null && !file.isEmpty()) {
// 上传目录
File fileDir = new File(CustomConfig.fileSave);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
if (destFile.exists()) {
destFile.delete();
}
try {
file.transferTo(destFile);
Map<String, Object> map = new HashMap<>();
map.put("url", fileName);
return Result.ok(map);
} catch (Exception e) {
log.error("文件上传出错", e);
return Result.error(ErrorCodeEnum.FILE_UPLOAD_ERROR);
}
}
return Result.error(ErrorCodeEnum.UPLOAD_FAIL);
}
}
四、新建 Controller 文件
import com.boran.demonstration.config.BaseController;
import com.boran.demonstration.config.Result;
import com.boran.demonstration.upload.service.ChunkUploadService;
import com.boran.demonstration.upload.vo.CheckMd5FileVO;
import com.boran.demonstration.upload.vo.UploadVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@Api(tags = "文件上传")
@CrossOrigin
@RestController
@RequestMapping("/file")
public class FileController {
@Autowired
private ChunkUploadService chunkUploadService;
/**
* 文件检查
*
* @param md5FileVO
* @return
*/
@ApiOperation("文件检查")
@PostMapping("check")
public Result check(CheckMd5FileVO md5FileVO) {
return chunkUploadService.check(md5FileVO);
}
/**
* 上传文件
*
* @param file
* @param uploadVO
* @return
*/
@ApiOperation("上传文件")
@PostMapping("/save")
public Result save(@RequestParam("file") MultipartFile file, UploadVO uploadVO) {
Long chunk = uploadVO.getChunk();
if (chunk == null) {// 没有分片
return chunkUploadService.UnChunkUploads(file, uploadVO);
} else {// 分片
return chunkUploadService.ChunkUploads(file, uploadVO);
}
}
}
就此大功告成......