大文件分片上传
参考博客:https://blog.csdn.net/haohao123nana/article/details/54692669
分片上传的意义在于上传内容较大的文件时,如果出现网络错误,普通上传只能重新开始上传,但是分片上传可以从中断的那个分片继续上传,给力!!!
demo效果图:
思路图解:
前提摘要:
☛☛ 文件的状态保存在mysql数据库中,当上传第一个分片时,将文件记录插入数据库,标记状态为0(未传完),文件上传合并之后,修改状态为1(上传完成)
表结构如下:
☛☛ 当发现文件之前上传过,首先要获取之前上传的所有文件分片,比如之前上传到第5个分片时出错,文件夹下有五个分片,这时候需要删除第五个分片,然后从第五个分片重新开始传,因为不能确定之前传的第五个分片到底完不完整。
OK:直接上代码吧
前端html:
<body>
<input type="file" id="file" />
<button id="upload">上传</button>
<br>
<br>
<p>
文件上传的状态:<span id="state">未上传</span>
</p>
<p>
文件总分片数量:<span id="count">?</span>
</p>
<p>
文件上传总耗时:<span id="useTime">0</span>S
</p>
</body>
JS代码:
<script type="text/javascript">
var i;
var databgein; //开始时间
$("#upload").click(function() {
$("#state").html("上传中...")
$("#useTime").html(0);
$("#count").html("?");
// 设置初始值
i = -1;
// 初始化开始时间
databgein = new Date().getTime();
//文件对象
var file = $("#file")[0].files[0];
// 判断文件是否存在
if (file == null)
alert("请选择上传文件");
// 验证文件是否上传过
isUpload(file);
});
function isUpload(file) {
// 构造一个form表单
var form = new FormData();
// 拼接参数:文件名称
form.append("fileName", file.name);
// 判断是否上传
$.ajax({
url : "http://localhost:8081/demo/file/checkFile",
type : "POST",
data : form,
async : true, //异步
processData : false, //很重要,告诉jquery不要对form进行处理
contentType : false, //很重要,指定为false才能形成正确的Content-Type
success : function(data) {
var update = data.update;
var filemd5 = data.filemd5;
// 文件未上传过
if (data.flag == 1)
upload(file, filemd5, update, false);
// 文件上传过,但是没有传完
if (data.flag == 2)
upload(file, filemd5, update, true);
// 秒传
if (data.flag == 3) {
console.log("文件秒传");
$("#state").html("上传完毕")
}
},
error : function() {
alert("服务器出错!");
}
})
}
function upload(file, filemd5, update, action) {
// 获取文件名
name = file.name;
// 获取文件总大小
size = file.size;
// 以5M为一个分片
var shardSize = 5 * 1024 * 1024;
// 计算总分片数量
var shardCount = Math.ceil(size / shardSize);
$("#count").html(shardCount + "");
// 下一个分片
i += 1;
// 计算每一片的起始与结束位置
var start = i * shardSize;
var end = Math.min(size, start + shardSize);
// 构建表单
var form = new FormData();
// 文件上传状态
form.append("action", action);
// 切割文件分片
form.append("data", file.slice(start, end));
// 整个文件的MD5
form.append("filemd5", filemd5);
// 上传日期
form.append("update", update);
// 文件名称
form.append("name", name);
// 总分片数量
form.append("total", shardCount); //总片数
// 当前上传的是第几个分片
form.append("index", i + 1); //当前是第几片
$("#index").html(i);
$.ajax({
url : "http://localhost:8081/demo/file/upload",
type : "POST",
data : form,
async : false, //异步
processData : false, //很重要,告诉jquery不要对form进行处理
contentType : false, //很重要,指定为false才能形成正确的Content-Type
success : function(data) {
// 如果文件之前上传过,直接从index片继续上传
if (action) {
i = data.index;
action = false;
}
//服务器返回分片是否上传成功
if (data.index == shardCount) {
var time = new Date().getTime() - databgein;
$("#useTime").html(time / 1000);
$("#state").html("上传完毕");
return;
}
upload(file, filemd5, update, action);
},
error : function(XMLHttpRequest, textStatus, errorThrown) {
alert("服务器出错!");
}
})
}
</script>
Controller代码:
/**
* 检验文件是否上传过
*
* @param request
* @return
*/
@PostMapping("/checkFile")
@ResponseBody
public ResponseEntity<Map<String, Object>> checkFile(
HttpServletRequest request) {
try {
// 获取文件名称
String fileName = request.getParameter("fileName");
// 使用spring自带的md5加密工具对文件名加密
String filemd5 = DigestUtils.md5DigestAsHex(fileName.getBytes());
// 验证文件是否上传过
Record fileRes = uploadService.getFileByMd5(filemd5);
// 创建返回数据容器
Map<String, Object> map = new HashMap<String, Object>();
// 没有上传过文件
if (fileRes == null) {
map.put("flag", Constant.NO_UPLOAD);
map.put("update", CommonUtils.format(new Date()));
map.put("filemd5", filemd5);
return new ResponseEntity<Map<String, Object>>(map,
HttpStatus.OK);
}
// 文件已上传,但是还没上传完
if (fileRes.getStatus() == 0) {
map.put("flag", Constant.NO_END_UPLOAD);
map.put("update", CommonUtils.format(fileRes.getUpDate()));
map.put("filemd5", filemd5);
return new ResponseEntity<Map<String, Object>>(map,
HttpStatus.OK);
}
// 文件已上传
map.put("flag", Constant.END_UPLOAD);
return new ResponseEntity<Map<String, Object>>(map, HttpStatus.OK);
} catch (Exception e) {
log.info("验证文件是否上传失败" + e);
return new ResponseEntity<Map<String, Object>>(
HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@PostMapping("/upload")
@ResponseBody
public ResponseEntity<Map<String, Object>> uplaod(
HttpServletRequest request,
@RequestParam(value = "data", required = false) MultipartFile multipartFile) {
// 文件名
String fileName = request.getParameter("name");
// 总片数
int total = Integer.valueOf(request.getParameter("total"));
// 完整文件的md5
String filemd5 = request.getParameter("filemd5");
// 第一个分片上传的日期(如:20170122)
String update = request.getParameter("update");
// 当前是第几片
int index = Integer.valueOf(request.getParameter("index"));
// 判断文件是否上传过
boolean action = Boolean.valueOf(request.getParameter("action"))
.booleanValue();
// 创建返回数据容器
Map<String, Object> map = new HashMap<String, Object>();
try {
// 获取tomcat下的webapp目录
String webapp = request.getSession().getServletContext()
.getRealPath("/");
// 按日期生成文件保存目录
String filePath = webapp + "files" + File.separator + update;
// 分片上传保存路径,以文件名称加密的md5字符串当目录
// String shardDir = CommonUtils.getFileNameNoEx(fileName);
String shardPath = filePath + File.separator + filemd5;
// 验证路径是否存在,不存在则创建目录
File path = new File(shardPath);
if (!path.exists()) {
path.mkdirs();
}
// 当action = true时,文件之前上传过
if (action) {
// 获取之前上传的分片文件的数量
int len = path.listFiles().length;
// 获取最后一个文件分片,并删除(上次传输过程中可能出现错误)
File file = new File(shardPath, filemd5 + "_" + len);
if (file.exists()) {
file.delete();
}
// 返回并指定从第几片开始上传
map.put("index", len - 2);
return new ResponseEntity<Map<String, Object>>(map,
HttpStatus.OK);
}
// 上传分片
multipartFile
.transferTo(new File(shardPath, filemd5 + "_" + index));
// 上传第一个分片,并记录文件到数据库
if (index == Constant.ONE) {
// 验证是否有记录
Record record = uploadService.getFileByMd5(filemd5);
if (record == null) {
// 获取文件后缀
String suffix = CommonUtils.getExtensionName(fileName);
// 拼接url
String url = filePath + File.separator + filemd5 + "."
+ suffix;
// 保存到数据库
Record r = new Record();
r.setMd5(filemd5);
r.setStatus(Constant.ZERO);
r.setUrl(url);
r.setUpDate(new Date());
uploadService.addFile(r);
}
}
// 获取文件夹下的文件数量
File[] fileArray = path.listFiles();
// 比较文件数量和分片数量是否相等,相等意味着上传完成,开始合并
if (fileArray != null && fileArray.length == total) {
// 获取文件后缀
String suffix = CommonUtils.getExtensionName(fileName);
File newFile = new File(filePath, filemd5 + "." + suffix);
// 文件追加写入
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;
temp = new FileInputStream(new File(shardPath, filemd5
+ "_" + j));
while ((len = temp.read(byt)) != -1) {
outputStream.write(byt, 0, len);
}
}
// 关闭流
temp.close();
outputStream.close();
// 更新文件上传的状态为完成
uploadService.updStatus(filemd5, Constant.ONE);
// 删除文件夹
CommonUtils.deleteDir(shardPath);
}
// 返回
map.put("index", index);
return new ResponseEntity<Map<String, Object>>(map, HttpStatus.OK);
} catch (Exception e) {
log.info("大文件上传失败:" + e);
return new ResponseEntity<Map<String, Object>>(
HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Pojo类:
import java.util.Date;
import lombok.Data;
@Data
public class Record {
// 主键
private Integer id;
// 文件存储路径
private String url;
// 上传日期
private Date upDate;
// 文件md5值
private String md5;
// 文件上传状态
private Integer status;
}
Utils类:
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
public class CommonUtils {
/**
* 获取文件的拓展名
*
* @param filename
* @return
*/
public static String getExtensionName(String filename) {
if ((filename != null) && (filename.length() > 0)) {
int dot = filename.indexOf('.');
if ((dot > -1) && (dot < (filename.length() - 1))) {
return filename.substring(dot + 1);
}
}
return filename.toLowerCase();
}
/**
* 获取 YYYYMMDD格式的日期
*
* @param date
* @return
*/
public static String format(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
return sdf.format(date);
}
/**
* 删除文件或文件夹
*
* @param dirPath
*/
public static void deleteDir(String path) {
File file = new File(path);
if (null != file) {
if (!file.exists()) {
return;
}
int i;
// file 是文件
if (file.isFile()) {
boolean result = file.delete();
// 限制循环次数,避免死循环
for (i = 0; !result && i++ < 10; result = file.delete()) {
// 垃圾回收
System.gc();
}
return;
}
// file 是目录
File[] files = file.listFiles();
if (null != files) {
for (i = 0; i < files.length; ++i) {
deleteDir(files[i].getAbsolutePath());
}
}
file.delete();
}
}
}
public class Constant {
public static int NO_UPLOAD = 1; // 没有上传
public static int NO_END_UPLOAD = 2; // 上传,但是没结束
public static int END_UPLOAD = 3; // 上传结束
public static int ZERO = 0;
public static int ONE = 1;
}