思路
将文件切割成多个分片。
对每个分片进行MD5计算,存储到数据库中,作为唯一标识。
上传时将每个分片上传到服务器,存储到指定的目录中。
完成上传时,根据MD5值将所有分片合并成一个完整的文件。
下载时,将文件分段下载,并进行文件合并操作,最终得到完整的文件。
分片实现思路
计算文件MD5值,作为唯一标识。
根据文件大小和指定的分片大小,计算文件需要被切割成多少个分片。
对每个分片进行MD5计算。
将分片的MD5值存储到数据库中。
断点续传实现思路
在上传文件时,记录已上传的分片,以及已经上传的字节数。
在继续上传时,先检查数据库中已上传的分片,然后从上次中断的地方继续上传。
代码实现
分片上传
@PostMapping("/upload")publicvoidupload(@RequestParam("file") MultipartFile file,
@RequestParam("index")int index,
@RequestParam("md5") String md5)throws Exception {
// 获取文件存储路径
String path = getFilePath(md5, index);
// 将分片写入指定路径
File dest = new File(path);
file.transferTo(dest);
// 计算分片MD5并存储到数据库
String chunkMd5 = DigestUtils.md5Hex(new FileInputStream(dest));
chunkService.saveChunk(md5, chunkMd5, index, dest.length());
}
分片合并
@SneakyThrows@PostMapping("/merge")publicvoidmerge(@RequestBody MergeRequest mergeRequest){
File file = new File(getFilePath(mergeRequest.getMd5(), null));
RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
for (int i = 0; i < mergeRequest.getTotalChunks(); i++) {
File chunkFile = new File(getFilePath(mergeRequest.getMd5(), i));
RandomAccessFile chunkAccessFile = new RandomAccessFile(chunkFile, "r");
byte[] buffer = newbyte[1024];
int len;
while ((len = chunkAccessFile.read(buffer)) != -1) {
accessFile.write(buffer, 0, len);
}
chunkAccessFile.close();
chunkFile.delete();
}
accessFile.close();
}
分段下载
@GetMapping("/download")publicvoiddownload(HttpServletRequest request, HttpServletResponse response)throws Exception {
String md5 = request.getParameter("md5");
File file = new File(getFilePath(md5, null));
if (!file.exists()) {
thrownew RuntimeException("文件不存在");
}
response.setHeader("Content-Disposition", "attachment;filename=" + file.getName());
try (FileInputStream inputStream = new FileInputStream(file);
ServletOutputStream outputStream = response.getOutputStream()) {
byte[] bytes = newbyte[1024];
int len;
while ((len = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
}
}
}
断点续传
@PostMapping("/upload-resume")publicvoiduploadResume(@RequestParam("file") MultipartFile file,
@RequestParam("index") Integer index,
@RequestParam("md5") String md5,
HttpServletRequest request)throws Exception {
// 判断该分片是否已经上传if (chunkService.isAlreadyUpload(md5, index)) {
return;
}
// 获取文件存储路径
String path = getFilePath(md5, index);
// 判断是否需要续传long uploadedBytes = 0;
File dir = new File(getFilePath(md5, null));
File[] files = dir.listFiles();
if (files != null) {
List<File> fileList = Arrays.asList(files);
Collections.sort(fileList, Comparator.comparing(File::getName));
String lastFileName = fileList.get(fileList.size() - 1).getName();
if (lastFileName.startsWith(index + "-", 32)) {
uploadedBytes = Long.parseLong(lastFileName.split("-")[1]);
}
}
// 将分片写入指定路径
File dest = new File(path);
long fileLength = file.getSize();
RandomAccessFile accessFile = new RandomAccessFile(dest, "rw");
accessFile.seek(uploadedBytes);
try (InputStream inputStream = file.getInputStream()) {
byte[] buffer = newbyte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
accessFile.write(buffer, 0, len);
uploadedBytes += len;
// 记录已上传的分片和字节数
chunkService.saveResume(md5, index, uploadedBytes);
}
}
accessFile.close();
}
注意事项
文件分片大小应该根据实际情况做出合理的设置,一般建议设置为2MB ~ 10MB。过小会造成太多分片,增加上传和合并的复杂度;过大会占用太多临时空间,且上传时易出现超时等问题。
对于较大的文件,应该避免在内存中进行操作,可以使用随机访问文件(RandomAccessFile)进行操作。
断点续传时,要对已经上传的分片进行排重,以避免重复上传。同时,要记录已上传的字节数,以便进行断点续传操作。