浅谈文件断点续传和WebUploader的基本结合
0、写在前面的话
上篇博客已经是在8月了,期间到底发生了什么,只有我自己知道,反正就是心情特别糟糕,生活状态工作状态学习状态都十分不好,还有心思进取吗,No!现在状态好起来了,生活又充满了希望 :D
前两周在写视频管理相关的功能,说是要在原来的项目上进行拓展。结果今天领导给我说客户那边还没定,只做技术上研究就行了,不用写具体功能代码(我都写了好吗?)于是突然时间有腾出来,今天整理一下把内容写一些。
要努力努力,为了更好的人为了更好的生活。
1、断点续传的两种方式
1.1 RandomAccessFile
客户端给一个已经上传的位置标记,然后服务器端就可以在指定的位置进行处理。这个断点位置的读取,就要用到RandomAccessFile类,该类不同于InputStream和OutputStream,它既可以对文件进行读也可以进行写,两个重要方法:
- long getFilePoint():返回文件记录指针的当前位置,不指定指针的位置默认是0
- void seek(long pos):设置文件指针偏移,即将文件记录指针定位到pos位置
至于position位置如何去处理,就看各自的想法了。1)你可以将位置存在浏览器(比如localStorage),下次传输的时候前端从残缺位置切割文件 blob.slice() 只传输剩余的部分,后端直接接收接着写入服务器即可;2)也可以前端把文件完整传输,同时带上position参数,由后端通过 RandomAccessFile 在指定位置开始读取内容即可。
至于客户端和服务端之间文件的一致性,多使用md5进行校验。
1.2 分片处理
H5中新增了File API,可以通过使用 slice() 方法生成只有某段文件内容。这个方法就为断点续传提供了新的方式,就是分片处理,假设一个文件是100M大小,那么每次传输我只需要传送10M,按序发送10次请求即可。某个分片传送失败,那么从这个分片再继续发送即可,后端则对分片文件进行合并成完整文件。
其实方式和1.1提到的是类似的,不过每次传输的数据单位量更大一些,完整文件交给后端进行合并。
2、WebUploader的分片断点续传
WebUploader的选项中支持直接开启分片上传:
var uploader = WebUploader.Uploader({
swf: 'path_of_swf/Uploader.swf',
// 开起分片上传
chunked: true,
// 分片大小,默认5M
chunkSize: 5242880,
// 分片出错后(如网络原因等造成出错)的重传次数
chunkRetry: 2,
// 上传并发数
threads: 1
});
x
1
var uploader = WebUploader.Uploader({
2
swf: 'path_of_swf/Uploader.swf',
3
4
// 开起分片上传
5
chunked: true,
6
// 分片大小,默认5M
7
chunkSize: 5242880,
8
// 分片出错后(如网络原因等造成出错)的重传次数
9
chunkRetry: 2,
10
// 上传并发数
11
threads: 1
12
});
开启分片上传后,插件会自动分片上传文件,接下来只需要在配置文件跳过和后端处理即可。官方回应在分片发送前会有监听的事件 uploadBeforeSend,在这个方法的callback里面如果返回的是一个promise,且此promise被reject了,那么此分片就跳过了。(实际上该方式在自测和咨询网友时发现,并没有什么用,即便按照官方说明,分片也没有跳过,仍然往后端进行了请求发送,同时也附带有文件)
webUploader.on('uploadBeforeSend', function(block, data){
data.fileMd5 = block.file.wholeMd5;
var deferred = WebUploader.Deferred();
var chunk = data.chunk;
var existChunks = block.file.existChunks;
//后端返回了已存在分片的数组,这里判断要发送的分片是否已存在
if(existChunks && existChunks.indexOf(chunk) != -1) {
//console.log("分片存在,已跳过:" + chunk);
deferred.reject();
} else {
deferred.resolve();
}
return deferred.promise();
});
1
webUploader.on('uploadBeforeSend', function(block, data){
2
data.fileMd5 = block.file.wholeMd5;
3
var deferred = WebUploader.Deferred();
4
var chunk = data.chunk;
5
var existChunks = block.file.existChunks;
6
//后端返回了已存在分片的数组,这里判断要发送的分片是否已存在
7
if(existChunks && existChunks.indexOf(chunk) != -1) {
8
//console.log("分片存在,已跳过:" + chunk);
9
deferred.reject();
10
} else {
11
deferred.resolve();
12
}
13
return deferred.promise();
14
});
分片是否存在的判断,也有不同的方式,一种你可以每次计算分片的md5值发送给后端,如果服务器已存在则跳过,否则就发送;另一种就是只向服务器查询一次获取已经存在的分片,然后在浏览器端进行比对,但如此需要考虑分片是否并发传输,进行相应处理。
我采用的方式是:先对文件进行md5计算,在服务器端创建和md5值同名的文件夹,每次上传的分片存放在对应文件夹,文件名即分片的序号,比如某文件夹中可能存在文件 0, 1, 2, 3... 前端发送分片前请求后端数据,后端将已经存在的分片名数组返回前端,前端进行跳过处理,同时后端在接收分片也要做是否存在的判断,已存在的话就不再进行读写操作,直到最后分片到达,则进行分片的按序合并即可。
public boolean uploadChunk() throws ChunkUploadException {
HttpServletRequest request = ServletActionContext.getRequest();
//封装源文件信息
FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
//获取同时上传的文件其他属性
Map<String, String> params = getVideoParams(request);
if (params.get("fileMd5") == null || "".equals(params.get("fileMd5"))) {
throw new ChunkUploadException("文件md5值未传递");
}
//存放
File temp = new File(getTempPath(params.get("fileMd5")) + "/" + srcFileInfo.getCurChunk());
if (!temp.exists()) {
try {
VideoUtil.copy(srcFileInfo.getFile(), temp);
} catch (IOException e) {
throw new ChunkUploadException("分片上传失败: chunkNum" + params.get("chunk"));
}
}
//如果是最后分片
return !srcFileInfo.isChunked() || srcFileInfo.getCurChunk() == srcFileInfo.getChunkSize() - 1;
}
x
1
public boolean uploadChunk() throws ChunkUploadException {
2
HttpServletRequest request = ServletActionContext.getRequest();
3
//封装源文件信息
4
FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
5
//获取同时上传的文件其他属性
6
Map<String, String> params = getVideoParams(request);
7
8
if (params.get("fileMd5") == null || "".equals(params.get("fileMd5"))) {
9
throw new ChunkUploadException("文件md5值未传递");
10
}
11
//存放
12
File temp = new File(getTempPath(params.get("fileMd5")) + "/" + srcFileInfo.getCurChunk());
13
if (!temp.exists()) {
14
try {
15
VideoUtil.copy(srcFileInfo.getFile(), temp);
16
} catch (IOException e) {
17
throw new ChunkUploadException("分片上传失败: chunkNum" + params.get("chunk"));
18
}
19
}
20
//如果是最后分片
21
return !srcFileInfo.isChunked() || srcFileInfo.getCurChunk() == srcFileInfo.getChunkSize() - 1;
22
}
public String upload() {
boolean isLastChunk = false;
try {
isLastChunk = uploadChunk();
} catch (ChunkUploadException e) {
e.printStackTrace();
AjaxSupport.sendFailText(null, e.getMessage());
return AJAX_RESULT;
}
//不是最后的分片,直接返回成功响应
if (!isLastChunk) {
AjaxSupport.sendSuccessText("chunk uploaded", "success");
return AJAX_RESULT;
}
//最后切片
else {
HttpServletRequest request = ServletActionContext.getRequest();
//封装源文件信息
FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
//获取同时上传的文件其他属性
Map<String, String> params = getVideoParams(request);
//获取合并文件的文件名
String filename = UUID.randomUUID().toString() + "." + srcFileInfo.getFileType();
//合并文件
File tempDir = new File(getTempPath(params.get("fileMd5")));
File[] tempfileArr = tempDir.listFiles();
File storeFile = new File(getStorePath() + "/" + filename);
try {
VideoUtil.merge(tempfileArr, storeFile);
}
...
x
1
public String upload() {
2
boolean isLastChunk = false;
3
try {
4
isLastChunk = uploadChunk();
5
} catch (ChunkUploadException e) {
6
e.printStackTrace();
7
AjaxSupport.sendFailText(null, e.getMessage());
8
return AJAX_RESULT;
9
}
10
11
//不是最后的分片,直接返回成功响应
12
if (!isLastChunk) {
13
AjaxSupport.sendSuccessText("chunk uploaded", "success");
14
return AJAX_RESULT;
15
}
16
//最后切片
17
else {
18
HttpServletRequest request = ServletActionContext.getRequest();
19
//封装源文件信息
20
FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
21
//获取同时上传的文件其他属性
22
Map<String, String> params = getVideoParams(request);
23
//获取合并文件的文件名
24
String filename = UUID.randomUUID().toString() + "." + srcFileInfo.getFileType();
25
26
//合并文件
27
File tempDir = new File(getTempPath(params.get("fileMd5")));
28
File[] tempfileArr = tempDir.listFiles();
29
File storeFile = new File(getStorePath() + "/" + filename);
30
try {
31
VideoUtil.merge(tempfileArr, storeFile);
32
}
33
...
最后,实际上这种方式断点续传仍然存在很多细节没有考虑,比如多线程,同个浏览器两个tab发送同一文件时如何处理?