一、前言
现在的项目中很多地方都会用到文件上传,不说百分百,但是绝对会占百分之九十(你可以反驳,但是我不接受),如果我们单纯的采用整个文件上传的方式,在遇到大文件就会出现各种花式问题,为了尽量避免这些坑,我们可以采用分片上传的方式将大文件分成不同的小文件进行上传
二、新建文件上传所需的VO文件
1、新建 CheckMd5FileVO 文件以及 UploadVO 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data;
@Data @ApiModel("文件MD5校验VO") public class CheckMd5FileVO {
@ApiModelProperty(value = "分片状态 0:不分片,1:分片", required = true) private Integer type;
@ApiModelProperty(value = "文件名", required = true) private String fileName;
@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); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.Date;
@Data @ApiModel("文件上传VO") public class UploadVO {
private String id;
@ApiModelProperty(value = "文件名称", required = true) private String name;
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;
@ApiModelProperty(value = "文件md5值", required = true) private String fileMd5; @ApiModelProperty(value = "文件名后缀", required = true) public String getSuffix() { return name.substring(name.lastIndexOf(".") + 1); } }
|
三、新建 FileUtil 工具类
该工具类主要是为了处理分片文件的合并和临时文件的清理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| 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()); } } }
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; }
public static boolean deleteDirectory(String sPath) { if (!sPath.endsWith(File.separator)) { sPath = sPath + File.separator; } File dirFile = new File(sPath); 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
service 处理文件分片检查、文件上传等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
| 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;
@Service @Slf4j public class ChunkUploadService { private static final String DELIMITER = "-";
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); } } }
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); } }
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
新建接口,一个用于文件分片检查,一个用于文件上传
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| 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;
@ApiOperation("文件检查") @PostMapping("check") public Result check(CheckMd5FileVO md5FileVO) { return chunkUploadService.check(md5FileVO); }
@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); } } }
|
六、结语
相关功能的测试结果就不贴了,有兴趣可以自己测试一下,不出意外的话,是没有问题的,当然了,出了问题概不负责,哈哈哈哈哈哈……