我们在工作中可能会遇到大文件的上传,如果一次传输,很有可能会遇到 网络、超时各种各样的问题。这里将分片上传和断点续传两种方式介绍一下方案。
一、分片上传
分片上传的原理:前端首先请求接口获取这次上传的uuid,上传的时间等信息返回给前端页面。前端将所要上传文件进行分片,在上传的时候包含 服务器返回给前端的 该次上传的uuid,该篇文件的md5,上传的第几片的的标识,和文件名称等标识。等到上传的片数等于总片数的时候对所有分片进行合并,删除临时分片。
这里由springboot+freemarker 来演示这个原理
(服务端用到工具类和前端用到的 js由于断点续传 同样也用到,在介绍完 断点续传 之后统一贴出来)
1、首先添加配置文件 application.properties(由于断点续传 需要依赖mysql故也将mysql引入)
server.port=8080
spring.freemarker.expose-spring-macro-helpers=true
# 是否优先从文件系统加载template,以支持热加载,默认为true
spring.freemarker.prefer-file-system-access=true
# 设定模板的后缀.
spring.freemarker.suffix=.ftl
# 设定模板的加载路径,多个以逗号分隔,默认:
spring.freemarker.template-loader-path=classpath:/templates/
spring.mvc.static-path-pattern=/static/**
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/pingyougou
spring.datasource.username=root
spring.datasource.password=root
#配置.xml文件路径
mybatis.config-locations=classpath:mybatis/mybatis-config.xml
mybatis.mapper-locations=classpath:conf/mapper/*Mapper.xml
#配置模型路径
mybatis.type-aliases-package=com.yin.freemakeradd.pojo
2、引入controller类
localhost:8080/upload2/index 第断点上传入口
@Controller
@RequestMapping(value = "/test")
public class IndexController {
@GetMapping(value = "/upload/index")
public String index(Model model){
return "breakpointBurst";
}
@GetMapping(value = "/upload2/index")
public String index2(Model model){
return "burst";
}
}
分片上传的具体上传逻辑
@RestController
@RequestMapping(value = "/burst/test")
public class BurstController {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final String temp_dir = "C:\\Users\\Administrator\\Desktop\\upload";
private static final String final_dir = "C:\\Users\\Administrator\\Desktop\\test";
/**
* 上传文件
*
* @param request
* @return
* @throws IllegalStateException
* @throws IOException
*/
@RequestMapping(value = "/upload")
public Map<String, Object> upload(
HttpServletRequest request, @RequestParam(value = "data",required = false) MultipartFile multipartFile) throws IllegalStateException, IOException, Exception {
String uuid = request.getParameter("uuid");
String fileName = request.getParameter("name");
//总大小
String size = request.getParameter("size");
//总片数
int total = Integer.valueOf(request.getParameter("total"));
//当前是第几片
int index = Integer.valueOf(request.getParameter("index"));
//整个文件的md5
String fileMd5 = request.getParameter("filemd5");
//文件第一个分片上传的日期(如:20200118)
String date = request.getParameter("date");
//分片的md5
String md5 = request.getParameter("md5");
//生成上传文件的路径信息,按天生成
String savePath = "\\upload" + File.separator + date;
String saveDirectory = temp_dir + savePath +File.separator+uuid;
//验证路径是否存在,不存在则创建目录
File path = new File(saveDirectory);
if (!path.exists()) {
path.mkdirs();
}
//文件分片位置
//File file = new File(saveDirectory, uuid + "_" + index);
multipartFile.transferTo(new File(saveDirectory, uuid + "_" + index));
if (path.isDirectory()) {
File[] fileArray = path.listFiles();
if (fileArray != null) {
if (fileArray.length == total) {
//分块全部上传完毕,合并
String suffix = NameUtil.getExtensionName(fileName);
String dir = final_dir + savePath;
File newFile = new File(dir, uuid + "." + suffix);
File copyDir = new File(dir);
if(!copyDir.mkdir()){
copyDir.mkdirs();
}
FileOutputStream outputStream = new FileOutputStream(newFile, true);//文件追加写入
byte[] byt = new byte[10 * 1024 * 1024];
int len;
// FileInputStream temp = null;//分片文件
for (int i = 0; i < total; i++) {
int j = i + 1;
FileInputStream temp = new FileInputStream(new File(saveDirectory, uuid + "_" + j));
while ((len = temp.read(byt)) != -1) {
System.out.println("-----" + len);
outputStream.write(byt, 0, len);
}
//关闭流
temp.close();
}
outputStream.close();
//删除临时文件
FileUtil.deleteFolder( temp_dir+ savePath +File.separator+uuid);
}
}
}
HashMap map = new HashMap<>();
map.put("fileId", uuid);
return map;
}
/**
* 上传文件前获取id和日期(如果是分片上传这一步可以交给前端处理)
*
* @param request
* @return
* @throws IOException
*/
@RequestMapping(value = "/isUpload")
public Map<String, Object> getUpload(HttpServletRequest request) throws Exception {
HashMap<String,Object> map = new HashMap<>();
String uuid = UUID.randomUUID().toString();
map.put("fileId", uuid);
map.put("date", formatter.format(LocalDateTime.now()));
return map;
}
}
分片前端展示:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>HTML5大文件分片上传示例</title>
<script src="http://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<script type="text/javascript" src="/static/md5.js"></script>
<script>
var i = -1;
var succeed = 0;
var databgein; //开始时间
var dataend; //结束时间
var action = false; //false检验分片是否上传过(默认); true上传文件
var page = {
init: function () {
$("#upload").click(function () {
$("#upload").attr('disabled', true)
databgein = new Date();
var file = $("#file")[0].files[0]; //文件对象
isUpload(file);
});
}
};
$(function () {
page.init();
});
function isUpload(file) {
//构造一个表单,FormData是HTML5新增的
var form = new FormData();
var r = new FileReader();
r.readAsBinaryString(file);
$(r).load(function (e) {
var bolb = e.target.result;
var md5 = hex_md5(bolb);
form.append("md5", md5);
//Ajax提交
$.ajax({
url: "http://localhost:8080//burst/test/isUpload",
type: "POST",
data: form,
//异步
async: true,
//很重要,告诉jquery不要对form进行处理
processData: false,
//很重要,指定为false才能形成正确的Content-Type
contentType: false,
success: function (dataObj) {
// var dataObj = eval("("+data+")");
var uuid = dataObj.fileId;
var date = dataObj.date;
//没有上传过文件
upload(file, uuid, md5, date);
}, error: function (XMLHttpRequest, textStatus, errorThrown) {
alert("服务器出错!");
}
});
})
}
/*
* file 文件对象
* uuid 后端生成的uuid
* filemd5 整个文件的md5
* date 文件第一个分片上传的日期(如:20170122)
*/
function upload(file, uuid, filemd5, date) {
name = file.name; //文件名
size = file.size; //总大小
var shardSize = 512 * 1024, //以0.5m为一个分片
shardCount = Math.ceil(size / shardSize); //总片数
if (i > shardCount) {
i = -1;
i = shardCount;
} else {
i += 1; //只有在检测分片时,i才去加1; 上传文件时无需加1
}
//计算每一片的起始与结束位置
var start = i * shardSize,
end = Math.min(size, start + shardSize);
//构造一个表单,FormData是HTML5新增的
var form = new FormData();
form.append("action", "upload"); //直接上传分片
form.append("data", file.slice(start, end)); //slice方法用于切出文件的一部分