无插件实现大文件分片上传,断点续传
1. 简介:
本篇文章基于实际项目的开发,将介绍项目中关于大文件分片上传、文件验证、断点续传、手动重试上传等需求的使用场景及实现;
2. 项目需求
1. 在一个音视频的添加中,既要有音视频的简介(如音视频内容文字介绍、自定义主题名称等一些基本的信息),又要有音视频所需要的多个文件(就像电视剧,一部电视剧有多集一样)。在数据库中具体表现为一对多的关系,即一个视频对应多个文件。下文就以电视剧为例
2. 如果一个电视剧中,既有上百兆的,也有几十兆的视频,但是如果在不稳定的一个网络环境中,c传输大文件时,客户想先把小的视频上传了,之后再来继续传未传完的大文件声誉部分,这就需要断点续传的功能;
3. 电视剧中至少有一集(至少有一个文件),无文件的电视剧基本信息无效;
3. 需求分析
1. 确定电视剧基本信息(自定义名称,内容简介、演员简介、播出时间等)及文件的上传方式
- 基本信息和音视频文件分开上传(因为在原有的数据库表设计中,文件表是关联于基本信息,所以必须要有音视频主键,才能在数据库添加对应的文件信息),取得基本信息主键之后再去上传文件;
2. 文件断点续传中,如何分片;文件接收方式;服务器端如何判断是哪个文件的分片;如何拼接各个分片;上传过程中发生意外情况(如断网,关闭浏览器),如何处理?
- 分片方式: 在客户端进行分片;
- 服务器端接收方式:使用MultipartFile接收文件
- 服务器端确定是哪个文件的分片: 在客户端按照一定规则(UUID或其他方式)生成唯一名称,在服务器端直接找到与该名称相同的文件片段;
- 拼接文件分片: 使用NIO的方式,将分片追加到已有分片的后面;
- 上传中发生意外:
A. 断网: 该情况类似于暂停上传,上传到文件处于暂停状态,网络恢复,即可点击继续上传按钮,继续上传;
B. 关闭浏览器: 在关闭时,给用户提示框,询问是否继续保存,若不保存,则根据视频基本信息表的主键的删除脏数据;
C. 第一个文件在上传时候,被用户取消或者断网,则服务器端未修改基本信息为有效,并且也未标记该文件为有效记录,可以理解为脏数据,但不需要清理这些数据(在查询的时候,不能查出这些无效记录,可以在更新视频基本信息记录的时候,查找这些脏数据,并清理磁盘上及数据表中的记录);
4. 实现
1. 基于以上分析,搭建一个简单的web项目工程,如下图:
2. 前端主要功能实现
// 文件分割上传
// 文件大小和分割起点
// 注释的是本地存储实现
var size = file.size, start = localStorage[fileid] * 1 || 0;
//start = $("filelist_" + fileid).filesize;
console.log("start1:"+start);
if (size == start) {
// 已经传过了
fileArray.shift();
if (delete fileArray[fileid]) console.log(fileArray.join() + "---上传成功");
objStateElement.success(fileid, now);
// 回调
onsuccess.call(fileid, {});
localStorage.clear();
return;
}
var funFileSize = function() {
if (file.flagPause == true) {
onpause.call(fileid);
return;
}
var data = new FormData();
data.append("name", encodeURIComponent(file.name));
data.append("fileid", fileid);
data.append("file", file.slice(start, start + fileSplitSize));
data.append("start", start + "");
var p = "?name="+encodeURIComponent(file.name)+"&fileid"+fileid+"&start"+start;
// XMLHttpRequest 2.0 请求
var xhr = new XMLHttpRequest();
xhr.open("post", eleForm.action, true);
// 上传进度中
xhr.upload.addEventListener("progress", function(e) {
objStateElement.backgroundSize(fileid, (e.loaded + start) / size * 100);
}, false);
// ajax成功后
xhr.onreadystatechange = function(e) {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
try {
var json = JSON.parse(xhr.responseText);
} catch (e) {
objStateElement.error(fileid);
return;
}
//var json = JSON.parse(xhr.responseText);
if (!json || !json.succ) {
objStateElement.error(fileid);
onerror.call(fileid, json);
return;
}
if (start + fileSplitSize >= size) {
// 超出,说明全部分割上传完毕
// 上传队列中清除者一项
fileArray.shift();
if (delete fileArray[fileid]) console.log(fileArray.join() + "---上传成功");
objStateElement.success(fileid, now);
// 回调
onsuccess.call(fileid, json);
localStorage.clear();
} else {
// 尚未完全上传完毕
// 改变下一部分文件的起点位置
start += fileSplitSize;
// 存储上传成功的文件点,以便出现意外的时候,下次可以断点续传
localStorage.setItem(fileid, start + "");
// 上传下一个分割文件
funFileSize();
}
} else {
objStateElement.error(fileid);
}
}
};
前端向后台提交文件
var xhr_filesize = new XMLHttpRequest();
xhr_filesize.open("GET", "/BigFileUpload/ajaxFilesUploadServlet?filename=" + nameArray.join(), true);
xhr_filesize.onreadystatechange = function(e) {
if (xhr_filesize.readyState == 4) {
if (xhr_filesize.status == 200 && xhr_filesize.responseText) {
var json = JSON.parse(xhr_filesize.responseText);
if (json.succ && json.data) {
for (var key in json.data) {
if (json.data[key] > 0 && json.data[key] < fileArray[key].size) {
objStateElement.backgroundSize(key, json.data[key] / fileArray[key].size * 100);
objStateElement.keep(key);
}
$("filelist_" + key).filesize = json.data[key];
}
}
}
}
};
xhr_filesize.send();
}
3.后台接收文件
/***
* @Description: 上传流文件并保存
* @param request
* @param response
* @throws ServletException
* @throws IOException
* @version: v1.1.0
* @author: xiangdong.she
* @date: Nov 9, 2017
*
* Modification History:
* Date Author Version Description
*-------------------------------------------------------------
* Nov 9, 2017 xiangdong.she v1.1.0 修改原因
*/
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
request.setCharacterEncoding("utf-8"); //设置编码
JSONObject json = new JSONObject(); //返回的json串
String filename = ""; //文件名称
String path = request.getRealPath("/upload"); //获取文件需要上传到的路径
try
{
List<FileItem> items = new ServletFileUpload(
new DiskFileItemFactory()).parseRequest(request);
for (FileItem item : items)
{
if (item.isFormField())
{
String fieldname = item.getFieldName();
String fieldvalue = "";
if (fieldname.equals("name"))
{
filename = fieldvalue = URLDecoder.decode(item.getString(),
"UTF-8");
}
else
{
fieldvalue = item.getString();
}
System.out.println("fieldname:" + fieldname
+ "--fieldvalue:" + fieldvalue);
// to do list
}
else
{
String fieldname = item.getFieldName();
InputStream filecontent = item.getInputStream();
System.out.println("fieldname:" + fieldname + "--filename:"
+ filename + "---filecontent:" + filecontent
+ "---path:" + path);
//手动写入硬盘
if (makeDir(path))
{
createFile(path, filename);
}
File file = new File(path + File.separator + filename);
FileOutputStream fos = new FileOutputStream(file, true);
InputStream is = item.getInputStream();
IOUtils.copy(is, fos);
is.close();
fos.close();
System.out.println("获取上传文件的总共的容量:" + item.getSize());
}
}
}
catch (FileUploadException e)
{
e.printStackTrace();
}
catch (Exception e)
{
e.printStackTrace();
}
json.put("succ", true);
response.setContentType("text/plain");
response.getWriter().write(json.toString());
}
5.结果展现
无插件实现大文件分片上传,断点续传
注:本文著作权归作者,由demo大师代发,拒绝转载,转载需要作者授权