分片断点上传及下载

分片上传

配置文件

1
2
3
4
5
#application.yml
spring:
servlet:
multipart:
enabled: false
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
<!--pom.xml-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.14</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

前端文件

image-20211007101843728

前端代码

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webuploader</title>
</head>
<!--引入CSS-->
<link rel="stylesheet" type="text/css" href="webuploader.css">
<script src="jquery-1.11.1.js"></script>
<script src="webuploader.js"></script>
<style>
#upload-container, #upload-list{width: 500px; margin: 0 auto; }
#upload-container{cursor: pointer; border-radius: 15px; background: #EEEFFF; height: 200px;}
#upload-list{height: 800px; border: 1px solid #EEE; border-radius: 5px; margin-top: 10px; padding: 10px 20px;}
#upload-container>span{widows: 100%; text-align: center; color: gray; display: block; padding-top: 15%;}
.upload-item{margin-top: 5px; padding-bottom: 5px; border-bottom: 1px dashed gray;}
.percentage{height: 5px; background: green;}
.btn-delete, .btn-retry{cursor: pointer; color: gray;}
.btn-delete:hover{color: orange;}
.btn-retry:hover{color: green;}
</style>
<!--引入JS-->
<body>
<div id="upload-container">
<span>点击或将文件拖拽至此上传</span>
</div>
<div id="upload-list">
</div>
<button id="picker" style="display: none;">点击上传文件</button>
</body>

<script>
$('#upload-container').click(function(event) {
$("#picker").find('input').click();
});
var uploader = WebUploader.create({
auto: true,// 选完文件后,是否自动上传。
swf: 'Uploader.swf',// swf文件路径
server: 'http://localhost:8080/upload',// 文件接收服务端。
dnd: '#upload-container',
pick: '#picker',// 内部根据当前运行是创建,可能是input元素,也可能是flash. 这里是div的id
multiple: true, // 选择多个
chunked: true,// 开启分片上传。
threads: 20, // 上传并发数。允许同时最大上传进程数。
method: 'POST', // 文件上传方式,POST或者GET。
fileSizeLimit: 1024*1024*1024*10, //验证文件总大小是否超出限制, 超出则不允许加入队列。10g
fileSingleSizeLimit: 1024*1024*1024*5, //验证单个文件大小是否超出限制, 超出则不允许加入队列。5g
fileVal:'upload' // [默认值:'file'] 设置文件上传域的name。
});

uploader.on("beforeFileQueued", function(file) {
console.log(file); // 获取文件的后缀
});

uploader.on('fileQueued', function(file) {
// 选中文件时要做的事情,比如在页面中显示选中的文件并添加到文件列表,获取文件的大小,文件类型等
console.log(file.ext); // 获取文件的后缀
console.log(file.size);// 获取文件的大小
console.log(file.name);
var html = '<div class="upload-item"><span>文件名:'+file.name+'</span><span data-file_id="'+file.id+'" class="btn-delete">删除</span><span data-file_id="'+file.id+'" class="btn-retry">重试</span><div class="percentage '+file.id+'" style="width: 0%;"></div></div>';
$('#upload-list').append(html);
uploader.md5File( file )//大文件秒传

// 及时显示进度
.progress(function(percentage) {
console.log('Percentage:', percentage);
})

// 完成
.then(function(val) {
console.log('md5 result:', val);
});
});

uploader.on('uploadProgress', function(file, percentage) {
console.log(percentage * 100 + '%');
var width = $('.upload-item').width();
$('.'+file.id).width(width*percentage);
});

uploader.on('uploadSuccess', function(file, response) {
console.log(file.id+"传输成功");
});

uploader.on('uploadError', function(file) {
console.log(file);
console.log(file.id+'upload error')
});

$('#upload-list').on('click', '.upload-item .btn-delete', function() {
// 从文件队列中删除某个文件id
file_id = $(this).data('file_id');
// uploader.removeFile(file_id); // 标记文件状态为已取消
uploader.removeFile(file_id, true); // 从queue中删除
console.log(uploader.getFiles());
});

$('#upload-list').on('click', '.btn-retry', function() {
uploader.retry($(this).data('file_id'));
});

uploader.on('uploadComplete', function(file) {
console.log(uploader.getFiles());
});
</script>
</html>

后端代码

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
/**
* 分片上传下载
*
* @author: zh'b
* @date: 2021/10/6 15:03
**/

@Controller
public class UploadAndDownloadController {

/**
* 编码格式
*/
private final static String UTF8 ="utf-8";
/**
* 文件保存下载路径
*/
private final static String UPLOAD_DOWNLOAD_PATH = "D:\\upload_test";

/**
* 服务器端实现分片上传
* @param request
* @param response
* @throws Exception
*/
@ResponseBody
@RequestMapping("/upload")
public void upload(HttpServletRequest request, HttpServletResponse response) throws Exception {
//设置编码格式
response.setCharacterEncoding(UTF8);
//当前分片
Integer schunk = null;
//总分片数
Integer schunks = null;
//名称
String name = null;
//保存路径
BufferedOutputStream os = null;
try {
DiskFileItemFactory factory = new DiskFileItemFactory();
//文件超出该大小写入磁盘
factory.setSizeThreshold(1024);
//设置保存路径
factory.setRepository(new File(UPLOAD_DOWNLOAD_PATH));

//解析request
ServletFileUpload uploaded = new ServletFileUpload(factory);
//单个文件不超过5g
uploaded.setFileSizeMax(5L * 1024L * 1024L * 1024L);
//总文件不超过10g
uploaded.setSizeMax(10L * 1024L * 1024L * 1024L);

//解析
List<FileItem> items = uploaded.parseRequest(request);
//遍历所有表单属性
for (FileItem item : items) {
//判断是不是文件,true:表单属性,false:上传的文件
if(item.isFormField()){
//当前分片
if("chunk".equals(item.getFieldName())){
schunk = Integer.parseInt(item.getString(UTF8));
}
//总分片
if("chunks".equals(item.getFieldName())){
schunks = Integer.parseInt(item.getString(UTF8));
}
//名称
if("name".equals(item.getFieldName())){
name = item.getString(UTF8);
}
}
}
//遍历所有上传文件
for(FileItem item : items){
//判断是不是文件,true:表单属性,false:上传的文件
if(!item.isFormField()){
//临时文件名
String temFileName = name;
//名字不能为空
if(name != null){
//当前分片为空,表示上传的是一个完整的文件
if(schunk != null){
//修改临时文件名,因为当前分片文件只是完整文件的一个分片
temFileName = schunk +"_"+name;
}
//查询磁盘中是否有当前分片文件
File temFile = new File(UPLOAD_DOWNLOAD_PATH,temFileName);
//断点续传,当前分片文件不在磁盘中
if(!temFile.exists()){
//写入当前分片文件到磁盘中
item.write(temFile);
}
}
}
}
//文件合并,合并所有分片文件
//判断是否有分片、判断是否为最后一个分片
if(schunk != null && schunk.intValue() == schunks.intValue()-1){
File tempFile = new File(UPLOAD_DOWNLOAD_PATH,name);
os = new BufferedOutputStream(new FileOutputStream(tempFile));

for(int i=0 ;i<schunks;i++){
//获取分片
File file = new File(UPLOAD_DOWNLOAD_PATH,i+"_"+name);
//判断当前分片是否已经就位
while(!file.exists()){
Thread.sleep(100);
file = new File(UPLOAD_DOWNLOAD_PATH,i+"_"+name);
}
//获取当前分片数据
byte[] bytes = FileUtils.readFileToByteArray(file);
//写入数据
os.write(bytes);
//清空缓冲流中的数据
os.flush();
//删除当前分片
file.delete();
}
//清空缓冲流中的数据
os.flush();
}
response.getWriter().write("上传成功"+name);
}finally {
try {
if (os!=null){
os.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

示例

image-20211007102110148

image-20211007102137588

image-20211007102306094

分片下载

服务器端代码

如果只实现了服务器代码,未实现客户端代码 则与 普通下载无任何差异

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
/**
* 分片上传下载
*
* @author: zh'b
* @date: 2021/10/6 15:03
**/

@Controller
public class UploadAndDownloadController {

/**
* 编码格式
*/
private final static String UTF8 ="utf-8";
/**
* 文件保存下载路径
*/
private final static String UPLOAD_DOWNLOAD_PATH = "D:\\upload_test";


/**
* 实现服务器端支持分片下载
* 下载示例:http://localhost:8080/download?downloadFileName=JWT视频.mp4
* @param request
* @param response
* @param downloadFileName
* @throws Exception
*/
@RequestMapping("/download")
public void downLoadFile(HttpServletRequest request, HttpServletResponse response,String downloadFileName) throws Exception {
//参数为空
if (downloadFileName==null|| "".equals(downloadFileName)) {
return;
}
//读取文件
File file = new File(UPLOAD_DOWNLOAD_PATH+"\\"+downloadFileName);
//设置响应编码格式
response.setCharacterEncoding(UTF8);
//输入流
InputStream is = null;
//输出流
OutputStream os = null;
//实现服务器端支持分片下载
try{
//获取文件总长度,用于分片
long fSize = file.length();
//设置内容类型,下载
response.setContentType("application/x-download");
//下载提示框显示正确的名字
String fileName = URLEncoder.encode(file.getName(),UTF8);
//下载提示框
response.addHeader("Content-Disposition","attachment;filename=" + fileName);
//告诉前端 支持分片下载
response.setHeader("Accept-Range","bytes");
//总长度
response.setHeader("fSize",String.valueOf(fSize));
//文件名称
response.setHeader("fName",fileName);

//进行分片操作
//文件起始,文件已经读取了多少
long pos = 0,last = fSize-1,sum = 0;
//判断前端传过来的request是否支持分片下载
if(null != request.getHeader("Range")){
//支持断点续传
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

//获取该分片起始位置
String numRange = request.getHeader("Range").replaceAll("bytes=","");
String[] strRange = numRange.split("-");
//http Range bytes=100-1000 bytes=100-尾
//等于2,表明有确定的起始位置
if(strRange.length == 2){
pos = Long.parseLong(strRange[0].trim());
last = Long.parseLong(strRange[1].trim());
if(last > fSize-1){
last = fSize-1;
}
//不等于2,表明只有确定的起点,是分片的最后一段
}else{
pos = Long.parseLong(numRange.replaceAll("-","").trim());
}
}
//该分片的大小
long rangeLenght = last - pos +1;
String contentRange = "bytes " + pos + "-" + last + "/" + fSize;
//响应分片的起始及大小
response.setHeader("Content-Range",contentRange);
//响应分片长度
response.setHeader("Content-Lenght",String.valueOf(rangeLenght));

//输入输出数据
os = new BufferedOutputStream(response.getOutputStream());
is = new BufferedInputStream(new FileInputStream(file));
//跳过前面已经读取过的分片
is.skip(pos);
byte[] buffer = new byte[1024];
int lenght = 0;
//当前分片的总数据量还未传输完
while(sum < rangeLenght){
//读取的起始位置,剩余长度小于等于buffer则取自身,否则取buffer的长度
lenght = is.read(buffer,0,((rangeLenght-sum) <= buffer.length ? ((int)(rangeLenght-sum)) : buffer.length));
//计算已经输出的总数据量
sum = sum+ lenght;
//输出数据
os.write(buffer,0,lenght);
}
System.out.println("下载完成");
}finally {
try {
if(is != null){
is.close();
}
if(os != null){
os.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

客户端代码

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
/**
* 客户端分片下载
*
* @author: zh'b
* @date: 2021/10/6 20:34
**/
@RestController
public class DownloadClient {
/**
* 分片大小
* 50MB
*/
private final static long PER_PAGE = 1024L * 1024L * 50L;
/**
* 保存路径
*/
private final static String DOWNPATH = "D:\\upload_test\\mytest";
/**
* 分片下载用的线程池,建议自己手动创建
*/
ExecutorService pool = Executors.newFixedThreadPool(10);

/**
* 客户端支持分片下载
* 下载示例:http://localhost:8080/downloadFile?downloadFileName=JWT视频.mp4
* @param downloadFileName
* @return
* @throws Exception
*/
@RequestMapping("/downloadFile")
public String downloadFile(String downloadFileName) throws Exception {
//下载文件名为空
if(downloadFileName==null||"".equals(downloadFileName)){
return "error";
}
//探测,试探性下载
FileInfo fileInfo = download( 0, 10, -1, downloadFileName,null);
//总分片数量
long pages = fileInfo.fSize / PER_PAGE;
//进行分片
for(long i=0;i<=pages; i++){
//将请求提交到线程池,实现异步
pool.submit(new Download(i*PER_PAGE,(i+1)*PER_PAGE-1,i,downloadFileName,fileInfo.fName));
}

return "success";
}

/**
* 储存分片信息
*/
@AllArgsConstructor
@NoArgsConstructor
private static class FileInfo{
//分片大小
long fSize;
//分片名
String fName;
}


/**
* 用于实现分片的异步下载
*/
@AllArgsConstructor
@NoArgsConstructor
private class Download implements Runnable{
//分片的头
long start;
//分片的尾
long end;
//第几个分片
long page;
//下载的文件名
String downloadFileName;
//文件名
String fName;

@Override
public void run() {
try {
FileInfo info = download( start, end, page, downloadFileName,fName);
} catch (Exception e) {
e.printStackTrace();
}
}
}

/**
* 客户端实现分片下载
* @param start
* @param end
* @param page
* @param downloadFileName
* @param fName
* @return
* @throws Exception
*/
private FileInfo download(long start,long end,long page,String downloadFileName,String fName) throws Exception {
//断点下载
File file = new File(DOWNPATH,page+"-"+fName);
//不是探测文件,并且文件已经下载完成,并且文件完整
if(page!=-1 && file.exists() && file.length()==PER_PAGE){
return null;
}

//实现分片下载
HttpClient client = HttpClients.createDefault();
//原下载路径
HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/download?downloadFileName="+downloadFileName);
//设置分片
httpGet.setHeader("Range","bytes="+start+"-"+end);
//发送请求
HttpResponse response = client.execute(httpGet);

//获取文件流
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();

//获取分片大小
String fSize = response.getFirstHeader("fSize").getValue();
//获取分片名
fName = URLDecoder.decode(response.getFirstHeader("fName").getValue(),"utf-8");

//传输数据
FileOutputStream fis = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int ch =0;
while((ch = is.read(buffer)) != -1){
fis.write(buffer,0,ch);
}

//关闭传输流
try {
is.close();
fis.flush();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}

//判断是否为最后一个分片
if(end - Long.parseLong(fSize) >= 0){
mergeFile(fName,page);
}
return new FileInfo(Long.parseLong(fSize), fName);
}

/**
* 合并下载完成的分片文件
* @param fName
* @param page
* @throws Exception
*/
private void mergeFile(String fName, long page) throws Exception {
//最终文件
File tempFile = new File(DOWNPATH,fName);
//获取相应的输出流
BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(tempFile));

for(int i=0 ;i<=page;i++){
//寻找分片文件
File file = new File(DOWNPATH,i+"-"+fName);
//可能某一分片还未下载完成
while(!file.exists() || (i != page && file.length() < PER_PAGE)){
Thread.sleep(100);
}
byte[] bytes = FileUtils.readFileToByteArray(file);
//输出到总文件后面
os.write(bytes);
os.flush();
//删除临时文件
file.delete();
}

//删除探测文件
File file = new File(DOWNPATH,-1+"-null");
file.delete();
os.flush();
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

提示

如果只实现了服务器代码,未实现客户端代码 则与 普通下载无任何差异,在此就不进行演示

如果要实现客户端的分片下载,需同时实现上面两段代码,因为客户端代码需去请求服务器端代码,来实现分片下载,请注意客户端代码的第110行

在请求时使用客户端代码的请求方式就能实现客户端的分片下载,请看一下示例

示例

image-20211007103307366

image-20211007103422944

image-20211007103441408

image-20211007103729374