背景

上传大文件时使用二进制易造成超时上传,故采用分片上传的方式,一步一步接收,然后存入文件

技术:百度的webuploader

前端
js版本
  • 官网下载webuploader
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
<html>
<head>
<meta charset="utf-8">
<title>webuploader</title>
<script type="text/javascript" src="js/jquery-1.9.1.min.js"></script>
<script type="text/javascript" src="webuploader-0.1.5/webuploader.js"></script>
<script type="text/javascript" src="js/bootstrap.min.js"></script>
<link rel="stylesheet" type="text/css" href="webuploader-0.1.5/webuploader.css">
<link rel="stylesheet" href="css/bootstrap.min.css">
</head>
<body>
<div class="container" style="text-align: center;">
<div id="fileList" class="uploader-list"></div>
<!--存放文件的容器-->
</div>
<div id="picker" class="upload-container" style="text-align: center;">
<div id="ctlBtn" class="btn btn-default" multiple="multiple">选择大文件</div>
<button id="uploadProgress" class="btn btn-default">开始上传</button>
</div>
</body>
<script>
$list = $('#fileList');
var uploader = WebUploader.create({
auto: true, //选完文件后,是否自动上传
swf: '/webuploader-0.1.5/Uploader.swf',
server: 'http://localhost:8081/webuploader/upload',
pick: '#picker',
multiple: true,//选择多个
duplicate: true,//去重,根据文件名字、文件大小和最后修改时间来生成hash Key
chunked: true, //开启分片上传
threads: 30, //并发数
chunkSize : 50 * 1024 * 1024, //每片100M
fileNumLimit: 1024,
fileSizeLimit: 50*1024 * 1024 * 1024,//50G 验证文件总大小是否超出限制, 超出则不允许加入队列
fileSingleSizeLimit: 10*1024 * 1024 * 1024, //10G 验证单个文件大小是否超出限制, 超出则不允许加入队列
disableGlobalDnd: true, //禁掉全局的拖拽功能。这样不会出现图片拖进页面的时候,把图片打开。
method: 'POST',
//resize: false //不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
});

// 当有文件被添加进队列的时候
uploader.on( 'fileQueued', function( file ) {
$list.append('<div id="' + file.id + '" class="item">' +
'<h4 class="info">' + file.name + '<button type="button" fileId="' + file.id + '" class="btn btn-danger btn-delete">删除<span class="glyphicon glyphicon-trash"></span></button></h4>' +
'<p class="state">等待上传</p>' +
'</div>');


//删除
$(".btn-delete").click(function () {
uploader.removeFile(uploader.getFile($(this).attr("fileId"), true));
$(this).parent().parent().fadeOut();//显示删除
$(this).parent().parent().remove();//DOM上删除
});

//md5计算
uploader.md5File(file)
.progress(function(percentage) {
console.log('Percentage:', percentage);
})
// 完成
.then(function (fileMd5) { // 完成
var end = +new Date();
console.log("before-send-file preupload: file.size="+file.size+" file.md5="+fileMd5);
file.wholeMd5 = fileMd5;//获取到了md5
uploader.options.formData.md5value = file.wholeMd5;//每个文件都附带一个md5,便于实现秒传
//$('#' + file.id).find('p.state').text('文件压缩完毕,可以点击上传了');
//$('#uploadButton').append('<div id="UploadBtn" class="webuploader-pick" style="margin-right:10px;background: #00b7ee; ">开始上传</div>' );
console.info("MD5="+fileMd5);
});


});


// 文件上传过程中创建进度条实时显示。
uploader.on( 'uploadProgress', function( file, percentage ) {
var $li = $( '#'+file.id ),
$percent = $li.find('.progress .progress-bar');

// 避免重复创建
if ( !$percent.length ) {
$percent = $('<div class="progress progress-striped active">' +
'<div class="progress-bar" role="progressbar" style="width: 0%">' +
'</div>' +
'</div>').appendTo( $li ).find('.progress-bar');
}

$li.find('p.state').text('上传中');

$percent.css( 'width', percentage * 100 + '%' );
});

uploader.on( 'uploadSuccess', function( file ) {
$( '#'+file.id ).find('p.state').text('已上传');
});

uploader.on( 'uploadError', function( file ) {
$( '#'+file.id ).find('p.state').text('上传出错');
});

uploader.on( 'uploadComplete', function( file ) {
$( '#'+file.id ).find('.progress').fadeOut();
});

$("#uploadButton").click(function () {
$('#picker').onclick();
});

</script>

</html>
vue版本
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
<template>
<div>
<div id="picker">选择文件</div>
<ul class="file-list">
<li class="list" v-for="file in fileList">
<span>{{file.name}}</span>
<span>进度:{{file.percentage}}</span>
</li>
</ul>
</div>
</template>

<script type="text/ecmascript-6">
// import '../assets/lib/jquery2.0.0/jquery-2.0.0.js';
import WebUploader from 'webuploader';

const request = {

};

export default {
name: 'HelloWorld',
data() {
return {
options: {
},
file: {},
fileList: []
}
},
mounted() {
this.initWebUpload();
},
methods: {
initWebUpload() {
const options = this.options;
const $this = this;

this.uploader = WebUploader.create({
auto: true, //选完文件后,是否自动上传
swf: '/webuploader-0.1.5/Uploader.swf',
server: 'http://localhost:8081/webuploader/upload',
pick: '#picker',
multiple: true,//选择多个
duplicate: true,//去重,根据文件名字、文件大小和最后修改时间来生成hash Key
chunked: true, //开启分片上传
threads: 30, //并发数
chunkSize : 50 * 1024 * 1024, //每片100M
fileNumLimit: 1024,
fileSizeLimit: 50*1024 * 1024 * 1024,//50G 验证文件总大小是否超出限制, 超出则不允许加入队列
fileSingleSizeLimit: 10*1024 * 1024 * 1024, //10G 验证单个文件大小是否超出限制, 超出则不允许加入队列
disableGlobalDnd: true, //禁掉全局的拖拽功能。这样不会出现图片拖进页面的时候,把图片打开。
method: 'POST',
//resize: false //不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
});

// 当有文件被添加进队列的时候,添加到页面预览
this.uploader.on('fileQueued', (file) => {
// this.$emit('fileChange', file);
$this.fileList.push(file);
});

this.uploader.on('uploadStart', (file) => {
// 在这里可以准备好formData的数据
//this.uploader.options.formData.key = this.keyGenerator(file);
});

this.uploader.on('startUpload', (file) => {
// 开始一次上传
// this.fileList = [];
});

// 文件上传过程中创建进度条实时显示。
this.uploader.on('uploadProgress', (file, percentage) => {
// this.$emit('progress', file, percentage);
console.log(file, percentage);
$this.fileList.forEach((item, index) => {
if (item.id === file.id) {
item.percentage = percentage * 100;
$this.fileList.splice(index, 1, item);
return;
}
});
});

this.uploader.on('uploadSuccess', (file, response) => {
// this.$emit('success', file, response);
console.log(file, response);
$this.fileList.forEach((item, index) => {
if (item.id === file.id) {
item.percentage = 100;
$this.fileList.splice(index, 1, item);
return;
}
});
});

this.uploader.on('uploadError', (file, reason) => {
// console.error(reason);
// this.$emit('uploadError', file, reason);
});

this.uploader.on('error', (type) => {
let errorMessage = '';
if (type === 'F_EXCEED_SIZE') {
errorMessage = `文件大小不能超过${this.fileSingleSizeLimit / (1024 * 1000)}M`;
} else if (type === 'Q_EXCEED_NUM_LIMIT') {
errorMessage = '文件上传已达到最大上限数';
} else {
errorMessage = `上传出错!请检查后重新上传!错误代码${type}`;
}

console.error(errorMessage);
// this.$emit('error', errorMessage);
});

this.uploader.on('uploadComplete', (file, response) => {

// this.$emit('complete', file, response);
});

this.uploader.on('uploadFinished', () => {
// 所有文件上传结束
// 重置文件队列
this.uploader.reset();
});
},
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#picker {
display: inline-block;
width: 100px;
padding: 7px 10px;
border: 1px solid #0099CC;
background: #0099CC;
border-radius: 3px;
color: #fff;
font-size: 12px;
}

#picker input {
display: none;
}
</style>
后端

先接收分片的文件,然后通过RandomAccessFile写入文件中

注:原理是RandomAccessFile可以指定位置写入文件流

  • 配置类(跨域)
1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebConfig implements WebMvcConfigurer {

//配置跨域请求
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("*")
.allowedMethods("*");
}
}
  • 配置类(http请求大小限制)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class MyMultipartConfigElement {

@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
//允许上传的文件最大值
factory.setMaxFileSize(DataSize.parse("1024MB")); //KB,MB
// 设置总上传数据总大小
factory.setMaxRequestSize(DataSize.parse("1024MB"));
return factory.createMultipartConfig();
}

}
  • bootStrap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server:
port: 8081
#设置临时文件夹
tomcat:
basedir: /root/prods/temp
servlet:
session:
timeout: 1200
reactive:
session:
timeout: 1200

spring:
servlet:
multipart:
max-file-size: 400MB
max-request-size: 2048MB

注:上述配置也不知道哪里起效的故都配置上

  • 实现层
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
//存放临时文件目录
private String uploadPath = "/root/files/";
private String lastPath = "/root/files/tmp/";

@PostMapping("upload")
@CrossOrigin
public Return upload(MultipartFileParam param){

//分片下标
int schunk = param.getChunk();
//总分片数
int schunks = param.getChunks();
//文件名
String name = param.getName();
//分片文件
MultipartFile fileData = param.getFile();
//文件大小
long fileSize = fileData.getSize();
//每片大小
long chunkSize = schunk+1 < schunks ? fileData.getSize() : (param.getSize()-fileSize) / (schunks-1);

try {
File file=new File(uploadPath);
//判断文件夹是否存在,不存在则创建
boolean flag = file.exists();
if(!flag) {
file.setWritable(true, false);
file.mkdirs();
}

//生成分片文件
if (fileData.getSize() > 0) {
String tempFileName = "";
if (name != null) {
tempFileName = schunk + "_" + name;
File fileTemp = new File(uploadPath + tempFileName);
fileTemp.setWritable(true, false);
fileTemp.mkdirs();
fileData.transferTo(fileTemp);
}
//此处由于出现流正在使用未关闭,其它的线程去读取文件的时候读取不到文件问题,故等待1秒
Thread.sleep(1000);

//分片文件
File src = new File(uploadPath+schunk+"_"+name);
//目标文件
File dest = new File(lastPath+name);
RandomAccessFile raf = null;
RandomAccessFile bos = null;
try {
raf = new RandomAccessFile(src, "r");
bos = new RandomAccessFile(dest,"rw");
//读取文件
bos.seek((long)schunk* chunkSize);
//缓冲区
byte[] flush = new byte[1024 << 2];
//接收长度
int len = 0;
while (-1 != (len = raf.read(flush))) {
if (fileSize - len >= 0) { //查看是否足够
//写出
bos.write(flush, 0, len);
fileSize = fileSize - len; //剩余量
} else { //写出最后一次的剩余量
bos.write(flush, 0, (int) fileSize);
break;
}
}
raf.close();
//删除源文件
src.delete();
} catch (IOException e) {
e.printStackTrace();
throw new ThrowException("上传失败");
} finally {
assert raf != null;
raf.close();
assert bos != null;
bos.close();
}
}

} catch (Exception e) {
e.printStackTrace();
}
return Return.ok();
}