完整项目在此:https://github.com/NoonLoveSnow/bigfileupload.git
先说下为什么用数据库记录文件分片,因为文件分片可能不会存储在本机上,用数据库可以记录分片存储位置,方便获取。。合并后的文件也可以记录其md5值,作为真实文件,用户文件可以只需要新建表引用它就行了。
大文件传输主要注意的有以下几点:
大文件传输要求:
大文件传输往往是比较耗时的,所以要求传输操作可继续的即断点续传,不能因为一些故障导致重新上传整个文件。
断点续传思路:
在浏览器传输时需要将文件分片上传,当遇到意外情况继续上传时,则之前传输完成的分片就不用传输了而传输未上传成功的分片。
如何记录上传成功的分片:
文件MD5与分片编号作为联合主键记录在数据库中。
分片上传前如何判断分片已经传输完成:
数据库中有记录,且文件长度与分片大小(chunkSize)相等
合并后文件校验:
文件大小是否不一致,每个分片应有的大小加起来与合并后的文件相比较
关于WebUploader:
网上有很多教程和例子。。
主要代码
后端:
package com.noonsnow.bigfileupload.controller;
import com.alibaba.fastjson.JSONObject;
import com.noonsnow.bigfileupload.mapper.FileBlockMapper;
import com.noonsnow.bigfileupload.mapper.UploadFileMapper;
import com.noonsnow.bigfileupload.pojo.FileBlock;
import com.noonsnow.bigfileupload.pojo.UploadFile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.Date;
import java.util.List;
//http://localhost:8081/uploadPage
@Controller
public class FileUploadController {
@Autowired
FileBlockMapper blockMapper;
@Autowired
UploadFileMapper fileMapper;
final static String filesDir = "C:/files";
final static String blocksDir = "C:/bolcks";
@RequestMapping("uploadPage")
public String page() {
return "bigFileUpload";
}
@PostMapping("checkFileIsExist")//检查文件是否存在
@ResponseBody
public Object checkFileExist(String md5value) {
JSONObject resp = new JSONObject();
List<UploadFile> uploadFiles = fileMapper.getByMd5(md5value);//逻辑上只有一个文件,由于不是按主键查找返回的是一个List
if (uploadFiles.size() == 0)
resp.put("exist", "0");
else
resp.put("exist", 1);
return resp.toString();
}
@PostMapping("checkChunkIsExist")//检查分片是否存在且完整
@ResponseBody
public Object checkChunkExist(String md5value, int chunk, int chunkSize) {
JSONObject resp = new JSONObject();
//查数据库,分片是否存在
FileBlock fileBlock = blockMapper.getByMd5AndChunk(md5value, chunk);
if (fileBlock == null) {//数据库不存在分片记录或者分片不完整,则重传
resp.put("exist", "0");
return resp.toString();
}
File block = new File(blocksDir + "/" + md5value + "/" + chunk); //前面没通过就不用执行后面的了,性能会好点吧。。
// System.out.println("blocklen:"+block.length()+" "+"chunkSize:"+chunkSize);
if (block.length() != chunkSize) {//分片是否完整
resp.put("exist", "0");
return resp.toString();
}
resp.put("exist", "1");
return resp.toString();
}
@PostMapping("upload")//接收上传的分片
@ResponseBody
public String fileUpload(String md5value, String chunks, int chunk, String id, String name,
String type, String lastModifiedDate, int chunkSize, MultipartFile file) {
//-----------------------保存分片--------------------
File blockDir = new File(blocksDir + "/" + md5value);
blockDir.mkdirs();
File block = new File(blockDir, String.valueOf(chunk));
try {
//搞了N多天,WebUploader tmd要重复发送验证通过的分片,导致多插入数据,导致合并文件变大,坑 (其实还是之前我没把MD5和chunk弄成联合主键,不然它重复插入不了的,自作孽啊)
boolean exist = blockMapper.getByMd5AndChunk(md5value, chunk) == null ? false : true;
if (!exist) {
FileBlock fileBlock = new FileBlock(md5value, new Date(), block.getPath(), chunk, name, chunkSize);
blockMapper.addFileBlock(fileBlock);//增加分片记录
}
file.transferTo(block);
} catch (IOException e) {
block.delete();
blockMapper.deleteByMd5AndChunk(md5value, chunk);
}
JSONObject jresp = new JSONObject();
jresp.put("code", 0);
jresp.put("msg", "上传成功");
return jresp.toString();
}
@RequestMapping("merge")//合并分片
@ResponseBody
public Object merge(String md5value, String name) {
JSONObject resp = new JSONObject();
//----------防止重复请求合并,如果数据库有记录,则不用合并了------------
List<UploadFile> uploadFiles = fileMapper.getByMd5(md5value);
if (uploadFiles.size() != 0) {
resp.put("status", "OK");
return resp.toString();
}
File fileDir = new File(filesDir + "/" + md5value);//文件所在目录
fileDir.mkdirs();
File file = new File(fileDir, name);
List<FileBlock> blocks = blockMapper.findByMd5(md5value);//查询出所有分片
System.out.println("blocks:" + blocks.size());
//-----------------合并分片-----------------
BufferedOutputStream bos = null;
boolean fail = false;//是否异常
try {
bos = new BufferedOutputStream(new FileOutputStream(file, true));
for (FileBlock block : blocks) {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File(block.getLocation())))) {//为了关闭每个分片的输入流
int len;
byte[] b = new byte[1024];
while ((len = bis.read(b)) != -1)
bos.write(b, 0, len);
} catch (Exception e) {
e.printStackTrace();
resp.put("status", "FAIL");
return resp.toString();
}
}
} catch (IOException e) {
//--------------异常---------------
fail = true;
e.printStackTrace();
resp.put("status", "FAIL");
} finally {
if (bos != null) {
try {
bos.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
if (fail) {
file.delete();//必须删除,合并文件是按追加而不是覆盖的方式写入文件。
return resp.toString();
}
if (checkTotalSize(file, blocks)) {
//新增文件记录
fileMapper.addUploadFile(new UploadFile(md5value, new Date(), file.getPath(), name));
//----------------删除分片及记录----------------
deleteFile(new File(blocksDir + "/" + md5value));
blockMapper.deleteByMd5(md5value);
resp.put("status", "OK");
} else {
file.delete();
resp.put("status", "FAIL");
}
return resp.toString();
}
//-----------验证合并后的文件,就是验证文件大小---------------
boolean checkTotalSize(File file, List<FileBlock> blocks) {
long total = 0;
for (FileBlock block : blocks) {
total += (long) block.getChunkSize();
}
System.out.println("total:" + total + " " + "fileLength:" + file.length());
return total == file.length();
}
//删除分片文件
void deleteFile(File file) {
if (file.isDirectory()) {
File[] files = file.listFiles();
for (File f : files) {
deleteFile(f);
}
}
file.delete();
}
}
前端:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>webuploader上传</title>
<link rel="stylesheet" type="text/css" th:href="@{/webuploader/webuploader.css}">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<script type="text/javascript" th:src="@{/webuploader/webuploader.min.js}"></script>
</head>
<body>
<section class="content">
<div class="container" style="margin-top: 20px">
<div class="alert alert-info">可以一次上传多个大文件</div>
</div>
<div class="container" style="margin-top: 50px">
<div id="uploader" class="container">
<div class="container">
<div id="fileList" class="uploader-list"></div>
<!--存放文件的容器-->
</div>
<div class="btns container">
<div id="picker" class="webuploader-container"
style="float: left; margin-right: 10px">
<div>
选择文件
<!--<input type="file" name="file"
class="webuploader-element-visible" multiple="multiple"/>-->
</div>
</div>
<div id="UploadBtn" class="webuploader-pick"
style="float: left; margin-right: 10px">开始上传
</div>
<div id="StopBtn" class="webuploader-pick"
style="float: left; margin-right: 10px" status="suspend">暂停上传
</div>
</div>
</div>
</div>
</section>
<script>
</script>
<script type="text/javascript">
$(function () {
//-----------------------注册--------------------
WebUploader.Uploader.register({
'before-send-file': 'beforeSendFile',//整个文件上传前
'before-send': 'beforeSend',//分片上传前
}, {
beforeSend: function (block) {
var deferred = WebUploader.Deferred();
// block为分块数据。
var file = block.file; // file为分块对应的file对象。
var fileMd5 = file.wholeMd5;
// 修改data可以控制发送哪些携带数据。
console.info("fileName= " + file.name + " fileMd5= " + fileMd5 + " fileId= " + file.id);
console.info("input file= " + flie_count);
$.ajax({
type: "POST"
, url: '[[@{/checkChunkIsExist}]]'
, data: {
md5value: fileMd5
, chunk: block.chunk
,chunkSize:block.end-block.start
}
, cache: false
, timeout: 1000
, dataType: "json"
}).then(function (resp, textStatus, jqXHR) {
if (resp.exist == "1") {
deferred.reject();
} else {
deferred.resolve();
}
}, function (jqXHR, textStatus, errorThrown) { //任何形式的验证失败,都触发重新上传
deferred.resolve();
});
return deferred.promise();
},
beforeSendFile: function (file) {
var that = this;
var deferred = WebUploader.Deferred();
//md5计算,没有才计算
if (file.wholeMd5 == "0") {
that.owner.md5File(file)
.progress(function (percentage) {
console.log('Percentage:', percentage);
}).progress(function (percentage) {
$('#' + file.id).find('p.state').text('读取文件:' + parseInt(percentage * 100) + "%");
})
// 完成
.then(function (fileMd5) { // 完成
var end = +new Date();
console.log("before-send-file preupload: file.size=" + file.size + " file.md5=" + fileMd5);
file.wholeMd5 = fileMd5;//获取到了md5
//uploader.options.formData.md5value = file.wholeMd5;//每个文件都附带一个md5,便于实现秒传
$('#' + file.id).find('p.state').text('MD5计算完毕');
console.info("MD5=" + fileMd5);
//上传前请求服务端,判断文件是否已经上传过
$.ajax({
type: "post",
url: '[[@{/checkFileIsExist}]]',
data: {"md5value": fileMd5},
dataType: "json",
success: function (resp) {
if (resp.exist == "1") {//如果存在则跳过
deferred.reject();
that.owner.skipFile(file);
// alert("文件已存在,无需上传!");
$('#' + file.id).find('p.state').text('秒传!');
$('#' + file.id).find(".info").find('.btn').hide();//上传完后删除"删除"按钮
}
deferred.resolve();
}
});
});
} else {
deferred.resolve();
}
return deferred.promise();
}
});
//---------------------注册结束-------------------
$list = $('#fileList');
var flie_count = 0;
var uploader = WebUploader.create({
//设置选完文件后是否自动上传
auto: false,
//swf文件路径
swf: '[[@{/webuploader/Uploader.swf}]]',
// 文件接收服务端。
server: '[[@{/upload}]]',
// 选择文件的按钮。可选。
// 内部根据当前运行是创建,可能是input元素,也可能是flash.
pick: '#picker',
chunked: true, //开启分块上传
chunkSize: 2 * 1024 * 1024,
chunkRetry: 3,//网络问题上传失败后重试次数
threads: 3, //上传并发数
//fileNumLimit :1,
fileSizeLimit: 2000 * 1024 * 1024,//最大2GB
fileSingleSizeLimit: 2000 * 1024 * 1024,
resize: false//不压缩
//选择文件类型
//accept: {
// title: 'Video',
// extensions: 'mp4,avi',
// mimeTypes: 'video/*'
//}
});
// 当有文件被添加进队列的时候<input type="text" id="s_WU_FILE_'+flie_count+'" />
uploader.on('fileQueued', function (file) {
$list.append('<div id="' + file.id + '" class="item">' +
'<h4 class="info">' + file.name + '<button type="button" fileId="' + file.id + '" class="btn btn-danger btn-delete"><span class="glyphicon glyphicon-trash"></span></button></h4>' +
'<p class="state">待上传...</p>' +
'</div>');
console.info("id=file_" + flie_count);
flie_count++;
file.wholeMd5 = "0";
//删除要上传的文件
//每次添加文件都给btn-delete绑定删除方法
$(".btn-delete").click(function () {
//console.log($(this).attr("fileId"));//拿到文件id
uploader.removeFile(uploader.getFile($(this).attr("fileId"), true));
$(this).parent().parent().fadeOut();//视觉上消失了
$(this).parent().parent().remove();//DOM上删除了
});
//uploader.options.formData.guid = WebUploader.guid();//每个文件都附带一个guid,以在服务端确定哪些文件块本来是一个
//console.info("guid= "+WebUploader.guid());
});
// 文件上传过程中创建进度条实时显示。
uploader.on('uploadProgress', function (file, percentage) {
var $li = $('#' + file.id),
$percent = $li.find('.progress .progress-bar');
// 避免重复创建
if (!$percent.length) {
$percent = $('<div class="progress progress-striped active">' +
'<div class="progress-bar" role="progressbar" style="width: 0%">' +
'</div>' +
'</div>').appendTo($li).find('.progress-bar');
}
$li.find('p.state').text('上传中');
$percent.css('width', percentage * 100 + '%');
});
//块发送前填充数据
uploader.on('uploadBeforeSend', function (block, data) {
// block为分块数据。
// file为分块对应的file对象。
var file = block.file;
var fileMd5 = file.wholeMd5;
// 修改data可以控制发送哪些携带数据。
console.info("fileName= " + file.name + " fileMd5= " + fileMd5 + " fileId= " + file.id);
console.info("input file= " + flie_count);
// 将存在file对象中的md5数据携带发送过去。
data.md5value = fileMd5;//md5
data.fileName_ = $("#s_" + file.id).val();
data.chunkSize=block.end-block.start;
console.log("fileName_: " + data.fileName_);
});
uploader.on('uploadSuccess', function (file) {
$('#' + file.id).find('p.state').text('已上传,正在合并分片');
$('#' + file.id).find(".progress").find(".progress-bar").attr("class", "progress-bar progress-bar-success");
$('#' + file.id).find(".info").find('.btn').fadeOut('slow');//上传完后删除"删除"按钮
$('#StopBtn').fadeOut('slow');
//通知合并分片
$.ajax({
type: "post",
url: '[[@{/merge}]]',
data: {
"md5value": file.wholeMd5, "name": file.name
},
dataType: "json",
success: function (resp) {
if (resp.status == "OK") {//合并完成
$('#' + file.id).find('p.state').text('合并完成');
} else {
$('#' + file.id).find('p.state').text('上传失败');
$('#' + file.id).find(".info").find('.btn').show();//上传完后删除"删除"按钮
}
}
});
});
uploader.on('uploadError', function (file) {
$('#' + file.id).find('p.state').text('上传出错');
//上传出错后进度条变红
$('#' + file.id).find(".progress").find(".progress-bar").attr("class", "progress-bar progress-bar-danger");
//添加重试按钮
//为了防止重复添加重试按钮,做一个判断
//var retrybutton = $('#' + file.id).find(".btn-retry");
//$('#' + file.id)
if ($('#' + file.id).find(".btn-retry").length < 1) {
var btn = $('<button type="button" fileid="' + file.id + '" class="btn btn-success btn-retry"><span class="glyphicon glyphicon-refresh"></span></button>');
$('#' + file.id).find(".info").append(btn);//.find(".btn-danger")
}
$(".btn-retry").click(function () {
//console.log($(this).attr("fileId"));//拿到文件id
uploader.retry(uploader.getFile($(this).attr("fileId")));
});
});
uploader.on('uploadComplete', function (file) {//上传完成后回调
//$('#' + file.id).find('.progress').fadeOut();//上传完删除进度条
//$('#' + file.id + 'btn').fadeOut('slow')//上传完后删除"删除"按钮
});
uploader.on('uploadFinished', function () {
//上传完后的回调方法
//alert("所有文件上传完毕");
//提交表单
});
$("#UploadBtn").click(function () {
uploader.upload();//上传
});
$("#StopBtn").click(function () {
console.log($('#StopBtn').attr("status"));
var status = $('#StopBtn').attr("status");
if (status == "suspend") {
console.log("当前按钮是暂停,即将变为继续");
$("#StopBtn").html("继续上传");
$("#StopBtn").attr("status", "continuous");
console.log("当前所有文件===" + uploader.getFiles());
console.log("=============暂停上传==============");
uploader.stop(true);
console.log("=============所有当前暂停的文件=============");
console.log(uploader.getFiles("interrupt"));
} else {
console.log("当前按钮是继续,即将变为暂停");
$("#StopBtn").html("暂停上传");
$("#StopBtn").attr("status", "suspend");
console.log("===============所有当前暂停的文件==============");
console.log(uploader.getFiles("interrupt"));
uploader.upload(uploader.getFiles("interrupt"));
}
});
uploader.on('uploadAccept', function (file, response) {
if (response._raw === '{"error":true}') {
return false;
}
});
});
</script>
</body>
</html>
来源:CSDN
作者:qq_41911762
链接:https://blog.csdn.net/qq_41911762/article/details/104005961