0%

文件上传功能

[TOC]

文件上传

yml配置

1
2
3
4
5
6
7
8
9
10
11
spring
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB

file:
# 文件路径
filePath: D:/files/
# 文件访问地址
fileUrl: http://localhost:8080

文件存储实体类

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
@ApiModel(value="文件存储",description="文件存储")
@Data
public class IMFile
{
private static final long serialVersionUID = 1L;

@ApiModelProperty("附件ID")
private String fid;
@ApiModelProperty("上传者用户ID")
private String createby;
@ApiModelProperty("上传时间")
private Integer createTime ;
@ApiModelProperty("文件名称")
private String fileName;
@ApiModelProperty("文件描述")
private String description;
/**附件类型:(比如:JPG,DOC,TXT...) */
@ApiModelProperty("文件类型")
private String fileType;
@ApiModelProperty("文件大小")
private Long fileSize;
@ApiModelProperty("附件存储路径")
private String filePath;
@ApiModelProperty("附件访问路径")
private String fileUrl;
@ApiModelProperty("是否是图片 0:否;1是")
private Integer isImage;
@ApiModelProperty("微缩图存储路径")
private String thumb;
@ApiModelProperty("中等大小图片")
private String middle;
@ApiModelProperty("宽度")
private Integer width;
@ApiModelProperty("高度")
private Integer height;
}

文件上传工具类

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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
public class FileUtil {

protected final Logger m_Logger = LoggerFactory.getLogger(this.getClass());

/**
* TODO 文件类型
*/
public static ArrayList<String> FileType =new ArrayList<String>(Arrays.asList("BMP","JPG","JPEG","PNG","GIF","bmp","jpg","jpeg","png","gif"));

public static ArrayList<String> AttachFile =new ArrayList<String>(Arrays.asList("BMP","JPG","JPEG","PNG","GIF","bmp","jpg","jpeg","png","gif","doc","DOC","docx","DOCX","pdf","PDF","txt","TXT"));

public static ArrayList<String> VedioSuffixList = new ArrayList<>(Arrays.asList("avi","flv","mpg","mpeg","mpe","m1v","m2v","mpv2","mp2v","dat","ts","tp","tpr","pva","pss","mp4","m4v",
"m4p","m4b","3gp","3gpp","3g2","3gp2","ogg","mov","qt","amr","rm","ram","rmvb","rpm"));

public static ArrayList<String> AuioSuffixList = new ArrayList<>(Arrays.asList("mp3","wma","m3u"));

public static void main(String[] args)
{
// bfca0e238dad11a710276ec738731923
System.out.println(getFileMD5(new File("C:\\Users\\wxq\\Desktop\\吴雪卿--延期待办12333.xls")));
}

/**
* 根据后缀名获取文件的类型
* @param suffix
* @return
*/
public static String getFileType(String suffix)
{
String valueToReturn = "";
if(StringUtils.isNotBlank(suffix))
{
//如果是图片
if(isImage(suffix))
{
valueToReturn = MessageTypeEmun.IMAGE.getCode();
}
else if("doc".equals(suffix)||"docx".equals(suffix))
{
valueToReturn = MessageTypeEmun.WORD.getCode();
}
else if("pdf".equals(suffix))
{
valueToReturn = MessageTypeEmun.PDF.getCode();
}
else if("xls".equals(suffix)||"xlsx".equals(suffix))
{
valueToReturn = MessageTypeEmun.EXCEL.getCode();
}
else if("ppt".equals(suffix)||"pptx".equals(suffix))
{
valueToReturn = MessageTypeEmun.PPT.getCode();
}
else if(isVedio(suffix))
{
valueToReturn = MessageTypeEmun.VEDIO.getCode();
}
else
{
//其他的多媒体文件
valueToReturn =MessageTypeEmun.MEDIA.getCode();
}
}
return valueToReturn;
}

/**
* 获取文件的MD5值
* @param file
* @return
*/
public static String getFileMD5(File file) {
BigInteger bi = null;
try {
byte[] buffer = new byte[8192];
int len = 0;
MessageDigest md = MessageDigest.getInstance("MD5");
FileInputStream fis = new FileInputStream(file);
while ((len = fis.read(buffer)) != -1) {
md.update(buffer, 0, len);
}
fis.close();
byte[] b = md.digest();
bi = new BigInteger(1, b);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return bi.toString(16);
}

/**
* 获取文件的大小
* @param file
* @return
*/
public static String GetFileSize(File file){
String size = "";
if(file.exists() && file.isFile()){
long fileS = file.length();
DecimalFormat df = new DecimalFormat("#.00");
if (fileS < 1024) {
size = df.format((double) fileS) + "BT";
} else if (fileS < 1048576) {
size = df.format((double) fileS / 1024) + "KB";
} else if (fileS < 1073741824) {
size = df.format((double) fileS / 1048576) + "MB";
} else {
size = df.format((double) fileS / 1073741824) +"GB";
}
}else if(file.exists() && file.isDirectory()){
size = "";
}else{
size = "0BT";
}
return size;
}

/**
* TODO 获取文件名前缀
*
* @param P_Filename
*/
public static String getFilePrefix(String P_Filename) {
return P_Filename.substring(0, P_Filename.lastIndexOf("."));
}

/**
* 根据附件文件的全称
* @param rootPath
* @param attachmentFullPath
* @return
*/

public static String getAttachementAbsPath(String rootPath, String attachmentFullPath) {
String returnObj = "";
if (rootPath != null && rootPath.length() > 0 && attachmentFullPath != null && attachmentFullPath.length() > 0) {
returnObj = attachmentFullPath.substring(rootPath.length()-1, attachmentFullPath.length());
}
return returnObj;
}

/**
* TODO 获取文件后缀
*
* @param P_Filename
*/
public static String getFileSuffix(String P_Filename) {
String[] strArray = P_Filename.split("\\.");
int suffixIndex = strArray.length - 1;
return strArray[suffixIndex];
}

/**
* 判断是否是图片
* @return
*/
public static boolean isImage(String strSuffix)
{
boolean valueToReturn = false;
if(StringUtils.isNotBlank(strSuffix))
{
if(FileType.contains(strSuffix))
{
valueToReturn = true;
}
}
return valueToReturn;
}

/**
* 判断是否是视频
* @return
*/
public static boolean isVedio(String strSuffix)
{
boolean valueToReturn = false;
if(StringUtils.isNotBlank(strSuffix))
{
if(VedioSuffixList.contains(strSuffix))
{
valueToReturn = true;
}
}
return valueToReturn;
}

/**
* 判断是否是音频
* @return
*/
public static boolean isAudio(String strSuffix)
{
boolean valueToReturn = false;
if(StringUtils.isNotBlank(strSuffix))
{
if(AuioSuffixList.contains(strSuffix))
{
valueToReturn = true;
}
}
return valueToReturn;
}

/**
* 判断是否是图片
* @param file
* @return
*/
public static boolean isImage(File file)
{
boolean valueToReturn = false;
if(file.exists())
{
String strFileName=file.getName();
String strFileSuffix=getFileSuffix(strFileName).toUpperCase();
if(FileType.contains(strFileSuffix))
{
valueToReturn = true;
}
}
return valueToReturn;
}

/**
* 判断文件类型是否是图片
* @param P_Threadfile
* @return
*/
public static Boolean isImage(MultipartFile P_Threadfile){
String strFileName=P_Threadfile.getOriginalFilename();
String strFileSuffix=getFileSuffix(strFileName).toUpperCase();
if(FileType.contains(strFileSuffix))
{
return true;
}
return false;
}

/**
* 判断文件类型是否支持图片,pdf,word,txt
* @param P_Threadfile
* @return
*/
public static Boolean isAttachFile(MultipartFile P_Threadfile){
String strFileName=P_Threadfile.getOriginalFilename();
String strFileSuffix=getFileSuffix(strFileName).toUpperCase();
if(AttachFile.contains(strFileSuffix))
{
return true;
}
return false;
}


/**
* TODO 文件上传方法
* @param P_File
* @return
*/
public static IMFile uploadFile(MultipartFile P_File, String uploadPath, String fileUrl) {
IMFile file = null;
// 上传文件路径
String fileFolder = uploadPath + "upload/";
try {
// 上传并返回新文件名称
String filePath = FileUploadUtils.upload(fileFolder, P_File);
String strFileName=P_File.getOriginalFilename();
String strFileSuffix=getFileSuffix(strFileName).toUpperCase();
file = new IMFile();
file.setFileName(strFileName);
file.setFileType(strFileSuffix);
file.setFileSize(P_File.getSize());
file.setFilePath(filePath);
file.setFileUrl(fileUrl+"/files/attachment/"+filePath);
file.setCreateTime((int) (System.currentTimeMillis() / 1000));
//如果是图片类型,则进行压缩
if(FileType.contains(strFileSuffix)) {
file.setIsImage(1);
//size(width,height) 若图片横比200小,高比300小,不变
//而outputQuality是图片的质量,值也是在0到1,越接近于1质量越好,越接近于0质量越差
String strMiddleFilePath = getFilePrefix(filePath) + "_m.jpg";
Thumbnails.of(fileFolder + filePath).outputFormat("jpg").width(400).outputQuality(0.5f).toFile(fileFolder + strMiddleFilePath);
file.setMiddle(strMiddleFilePath);
String strThumbFilePath = getFilePrefix(filePath) + "_s.jpg";
Thumbnails.of(fileFolder + strMiddleFilePath).outputFormat("jpg").width(200).outputQuality(0.5f).toFile(fileFolder + strThumbFilePath);
file.setThumb(strThumbFilePath);

//设置图片的宽度
InputStream inputStream = new FileInputStream(fileFolder+filePath);
BufferedImage image = ImageIO.read(inputStream);
file.setWidth(image.getWidth());
file.setHeight(image.getHeight());
}
else
{
file.setIsImage(0);
if (strFileSuffix.equals("DOCX") || strFileSuffix.equals("DOC")){
String strDocxFilePath = DateFormatUtils.format(new Date(), "yyyy/MM/dd") + "/" +strFileName;
copy(new java.io.File(fileFolder + filePath),new java.io.File(fileFolder+ strDocxFilePath));
file.setThumb(strDocxFilePath);
}
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("附件上传失败");
}
return file;
}


/**
* 复制文件
*
* @param src
* @param dst
* @throws Exception
*/
public static void copy(java.io.File src, java.io.File dst) throws Exception {
int BUFFER_SIZE = 4096;
InputStream in = null;
OutputStream out = null;
try {
in = new BufferedInputStream(new FileInputStream(src), BUFFER_SIZE);
out = new BufferedOutputStream(new FileOutputStream(dst), BUFFER_SIZE);
byte[] buffer = new byte[BUFFER_SIZE];
int len = 0;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
} catch (Exception e) {
throw e;
} finally {
if (null != in) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
in = null;
}
if (null != out) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
out = null;
}
}
}

}

文件上传接口

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
@Api(value = "文件上传接口", tags = { "文件上传相关的接口" })
@CrossOrigin(origins = "*", allowCredentials = "true", maxAge = 3600)
@Controller
@RequestMapping("/system")
public class FileController
{
protected final Logger m_Logger = LoggerFactory.getLogger(this.getClass());

@Autowired
private IMfileMapper m_IMfileMapper;

@Value("${file.filePath}")
private String filePath;

@Value("${file.fileUrl}")
private String fileUrl;

@Autowired
private IOfuserService ofuserService;

@ApiOperation(value = "单文件上传接口", notes = "注意:swaggerUI只支持单文件上传接口")
@PostMapping(value = "/IMfile/uploadOneFile", headers = "content-type=multipart/form-data")
@ResponseBody
@Transactional
public ApiResultEntity uploadOneFile(
@ApiParam(value = "上传的文件", required = true) @RequestParam(value = "file", required = true) MultipartFile file,
@ApiParam(value = "上传者用户ID", required = true, type = "string")@RequestParam(value = "username", required = true) String username,
@ApiParam(value = "备注", type = "string")@RequestParam(value = "description") String description
)
{
ApiResultEntity valueToReturn = new ApiResultEntity();
LinkedHashMap<String, Object> resultMap = new LinkedHashMap<>();
//校验附件格式
if (!FileUtil.isAttachFile(file))
{
valueToReturn = new ApiResultEntity(500000, "错误格式");
return valueToReturn;
}
if (StringUtils.isEmpty(username))
{
return ApiResultEntity.error("上传者用户名不能为空");
}
Ofuser ofuser = ofuserService.findById(username);
if(ofuser==null)
{
return new ApiResultEntity(500000, "用户名错误");
}
//上传附件
IMFile imFile = FileUtil.uploadFile(file,filePath,fileUrl);
if (imFile != null)
{
imFile.setCreateby(username);
imFile.setDescription(description);
int row = this.m_IMfileMapper.insertIMFile(imFile);
if (row>0)
{
resultMap.put("fileUrl", imFile.getFileUrl());
resultMap.put("fileName", imFile.getFileName());
resultMap.put("fileSize", imFile.getFileSize());
resultMap.put("fileType", imFile.getFileType());
}
else
{
//添加失败
valueToReturn.setCode(500000);
}

}
valueToReturn = new ApiResultEntity(600000, "文件上传成功",resultMap);
return valueToReturn;
}

@ApiOperation(value = "多文件上传接口", notes = "注意:swaggerUI不支持多文件上传接口测试,请用postman工具测试")
@PostMapping(value = "/IMfile/uploadFiles", headers = "content-type=multipart/form-data")
@ResponseBody
@Transactional
public ApiResultEntity uploadFiles(
@ApiParam(value = "上传的文件", required = true) @RequestParam(value = "files", required = true) MultipartFile[] files,
@ApiParam(value = "上传者用户名", required = true, type = "string")@RequestParam(value = "username", required = true) String username,
@ApiParam(value = "备注",type = "string")@RequestParam(value = "description") String description
)
{
ApiResultEntity valueToReturn = new ApiResultEntity();

if (StringUtils.isEmpty(username))
{
return ApiResultEntity.error("上传者用户名不能为空");
}
Ofuser ofuser = ofuserService.findById(username);
if(ofuser==null)
{
return new ApiResultEntity(500000, "用户名错误");
}

//检验附件是否为空
if (files.length>0)
{
int row = 0;
JSONArray attachmentList = new JSONArray();
for (MultipartFile multipartFile : files)
{
//上传附件
IMFile file = FileUtil.uploadFile(multipartFile,filePath,fileUrl);
if (file != null)
{
file.setCreateby(username);
file.setDescription(description);
row += this.m_IMfileMapper.insertIMFile(file);

if (row>0)
{
LinkedHashMap<String, Object> attach = new LinkedHashMap<>();
attach.put("fileUrl", file.getFileUrl());
attach.put("fileName", file.getFileName());
attach.put("fileSize", file.getFileSize());
attach.put("fileType", file.getFileType());
attachmentList.add(attach);
}
else
{
valueToReturn = ApiResultEntity.error("用户头像更新失败");
}

}
}
valueToReturn = ApiResultEntity.success("文件上传成功",attachmentList);
}
else
{
valueToReturn = ApiResultEntity.success("附件为空");
}
return valueToReturn;
}
}

大文件断点续传与极速秒传

对于网站,一个文件小则几十M,大则上G,上传一个大文件受网络影响很大,文件越大,上传失败率越高。所以我们要完善文件上传功能,支持断点续传,只上传剩下的部分,这就是断点续传。

文件上传流程图

深色为服务器端处理

image-20210525170241763

image-20210525170331952

分片传输的试探

一个文件有10M,每个文件定位1M,那这个文件就会被分为10片进行传输

分片上传功能开发

1.文件表相关字段

1
2
3
4
shard_index 已上传分片
shard_size 分片大小|B
shard_total 分片总数
key 文件标识

注意:对于一些不涉及安全性的数据,可以交由前端来计算,这样可以减轻服务端的压力

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
@RestController
@Slf4j
public class FileUploadController {

@Autowired
private FileTbService fileTbService;

@PostMapping(value = "/upload")
public Result upload(@RequestParam(value = "file") MultipartFile file,
FileDTO fileDTO) throws Exception {
File fullDir = new File(FileConstance.FILE_PATH);
if (!fullDir.exists()) {
fullDir.mkdir();
}

//uid 防止文件名重复,又可以作为文件的唯一标识
String fullPath = FileConstance.FILE_PATH + fileDTO.getKey() + "." + fileDTO.getShardIndex();
File dest = new File(fullPath);
file.transferTo(dest);
log.info("文件分片 {} 保存完成",fileDTO.getShardIndex());

//开始保存索引分片信息,不存在就新加,存在就修改索引分片
FileTb fileTb = FileTb.builder()
.fKey(fileDTO.getKey())
.fIndex(Math.toIntExact(fileDTO.getShardIndex()))
.fTotal(Math.toIntExact(fileDTO.getShardTotal()))
.fName(fileDTO.getFileName())
.fSize(fileDTO.getSize())
.build();

if (fileTbService.isNotExist(fileDTO.getKey())) {
fileTbService.saveFile(fileTb);
}else {
fileTbService.UpdateFile(fileTb);
}

if (fileDTO.getShardIndex().equals(fileDTO.getShardTotal())) {
log.info("开始合并");
merge(fileDTO);
return Result.success(FileConstance.ACCESS_PATH + fileDTO.getFileName());
}
return Result.success();
}

分片合并功能开发

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
public void merge(FileDTO fileDTO) throws Exception {
Long shardTotal = fileDTO.getShardTotal();
File newFile = new File(FileConstance.FILE_PATH + fileDTO.getFileName());
if (newFile.exists()) {
newFile.delete();
}
//文件追加写入
FileOutputStream outputStream = new FileOutputStream(newFile, true);
//分片文件
FileInputStream fileInputStream = null;
//每个分片设定为10M
byte[] bytes = new byte[10 * 1024 * 1024];
int len;
try {
for (int i = 0; i < shardTotal; i++) {
// 读取第i个分片
fileInputStream = new FileInputStream(new File(FileConstance.FILE_PATH + fileDTO.getKey() + "." + (i + 1)));
while ((len = fileInputStream.read(bytes)) != -1) {
//一直追加到合并的新文件中
outputStream.write(bytes, 0, len);
}
}
} catch (IOException e) {
log.error("分片合并异常", e);
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
outputStream.close();
log.info("IO流关闭");
System.gc();
} catch (Exception e) {
log.error("IO流关闭", e);
}
}
log.info("合并分片结束");

//告诉java虚拟机去回收垃圾 至于什么时候回收 这个取决于 虚拟机的决定
System.gc();
//等待100毫秒 等待垃圾回收去 回收完垃圾
Thread.sleep(100);
log.info("删除分片开始");
for (int i = 0; i < shardTotal; i++) {
String filePath = FileConstance.FILE_PATH + fileDTO.getKey() + "." + (i + 1);
File file = new File(filePath);
boolean result = file.delete();
log.info("删除{},{}", filePath, result ? "成功" : "失败");
}
log.info("删除分片结束");
}

分片检查与极速秒传

  • 上传分片之前先进行分片检查

开始上传之前,先检查一下当前的文件是否上传过了(数据库是否有记录),如果上传过了,已经上传到第几个分片了。