简易实现文件分块上传功能——后端代码的实现

原理解析

面对大文件上传时,如果文件过大,不仅上传耗时长,并且后端的I/O线程将处于长时间的写文件状态,不仅拖累后端的运行,而且对于前端用户的体验也不好,如果中途失败的话,文件上传失败,不仅白白浪费时间,后端也白白浪费了线程的工作效率

这个时候,就可以考虑前后端合作实现文件的分块上传,前端实现文件的切割服务,后端根据文件块进行文件块的存储、最终文件块的merge成为最终的上传文件

其实原理很简单,前端将文件分块,同时对文件的内容进行MD5处理生成一个签名,在发送文件块的时候,附带几个信息

  • 当前文件块的chunkId
  • 总文件chunk数目
  • 原文件名
  • 文件MD5签名

后端代码根据这些信息对文件chunk进行处理

  • 根据文件的MD5签名建立一个临时的文件块存储文件夹
  • 对每一个文件chunkId进行校验,查看是否重复上传,如果重复上传发送给前端,否则将文件块保存
  • 当前文件的chunkId等于总文件的chunkId时,根据文件MD5签名创建的临时目录,将目录下的所有文件块读出,根据chunkId顺序merge起来,再根据最终的源文件名生成最终的上传文件
  • 清除临时文件

代码实现

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
@Slf4j
@Component
public class FileHandlerImpl implements FileHandler {

@Value(value = "${file.parent.tmp.path}") private String parentPath;

@Override
public Mono<ServerResponse> chunkCheck(ServerRequest request) {
int chunkNumber = Integer.valueOf(request.queryParam(FileChunk.FILE_CHUNK_NUMBER_FIELD).orElse("-1"));
String fileName = request.queryParam(FileChunk.FILE_NAME_FIELD).orElse("");
boolean isExist = FileUploadUtils.judgeFileExist(filePathCreate(parentPath, fileName), chunkNumber + ".tmp");
return render(isExist, isExist ? HttpStatus.OK : HttpStatus.CONFLICT);
}

@Override
public Mono<ServerResponse> chunkSave(ServerRequest request) {
return request.multipartData().map(MultiValueMap::toSingleValueMap)
.map(stringPartMap -> {
HashMap<String, String> param = new HashMap<>(stringPartMap.size());
AtomicReference<FilePart> filePart = new AtomicReference<>(null);
stringPartMap.values().stream().flatMap(part -> {
if (part.headers().getContentType() == null) {
FormFieldPart fieldPart = (FormFieldPart) part;
param.put(part.name(), fieldPart.value());
} else {
filePart.set((FilePart) part);
}
return Stream.empty();
}).count();
FileChunk fileChunk = (FileChunk) JsonUtils.toObj(JsonUtils.toJson(param), FileChunk.class);
return FileUploadUtils.saveFile(filePart.get(), filePathCreate(parentPath, fileChunk.getFilename()),
fileChunk.getChunkNumber() + ".tmp");
}).flatMap(resultData -> render(resultData, resultData.getValue()));
}

@Override
public Mono<ServerResponse> merge(ServerRequest request) {
return request.bodyToMono(FileChunk.class)
.map(fileChunk -> FileUploadUtils.fileMerge(parentPath, fileChunk.getIdentifier(),
fileChunk.getFilename(), fileChunk.getChunks()))
.flatMap(s -> render(s, HttpStatus.OK));
}
}
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
public class FileUploadUtils {

/**
* 文件合并请求
* @param parentPath
* @param fileName
* @param chunkNum
* @return
*/
public static String fileMerge(String parentPath, String fileMD5, String fileName, int chunkNum) {
String finalFileName = null;
FileOutputStream targetFile = null;
try {
new File(filePathCreate(parentPath, fileMD5)).mkdirs();
finalFileName = filePathCreate(parentPath, fileMD5, fileName);
log.info("finalFileName : {}", finalFileName);
targetFile = new FileOutputStream(finalFileName);
byte[] buffer = new byte[1024];
for (int i = 1; i <= chunkNum; i ++ ) {
String chunkFile = i + ".tmp";
File tmpFile = new File(filePathCreate(parentPath, fileName, chunkFile));
InputStream inputStream = new FileInputStream(tmpFile);
int len;
while ((len = inputStream.read(buffer)) != -1) {
targetFile.write(buffer, 0, len);
}
inputStream.close();
}
} catch (FileNotFoundException e) {
log.error("File merge FileNotFoundException : {}", e.getMessage());
finalFileName = "Err";
} catch (IOException e) {
log.error("File merge IOException : {}", e.getMessage());
} finally {
if (targetFile != null) {
try {
targetFile.close();
} catch (IOException e) {
log.error("File merge IOException : {}", e.getMessage());
}
}
}
return finalFileName;
}

/**
* 保存文件
* @param uploadFile
* @param parentPath
* @param name
* @return
*/
public static ResultData<HttpStatus> saveFile(FilePart uploadFile, String parentPath, String name) {
if (uploadFile == null || StringUtils.isNotEmpty(name)) {
return ResultData.builder().value(HttpStatus.CONFLICT).errMsg("上传失败").builded();
}
new File(parentPath).mkdirs();
String fileName = parentPath + name;
File file = new File(fileName);
if (file.exists()) {
return ResultData.builder().value(HttpStatus.CONFLICT).errMsg("文件已存在").builded();
}
uploadFile.transferTo(file);
return ResultData.builder().value(HttpStatus.OK).builded();
}

/**
* 判断文件是否存在
* @param parentPath
* @param fileName
* @return
*/
public static boolean judgeFileExist(String parentPath, String fileName) {
return new File(parentPath + fileName).exists();
}

/**
* 创建文件路径
* @param parent
* @param present
* @param other
* @return
*/
public static String filePathCreate(String parent, String present, String... other) {
StringBuilder sb = new StringBuilder();
sb.append(parent).append(present).append("/");
for (String s : other) {
sb.append(s).append("/");
}
return sb.toString();
}
}