解决方案:
1、服务器建立临时文件夹;
2、针对每次上传,在临时目录下动态建立二级临时文件夹,该文件夹需具有唯一性;
3、对文件进行切片上传,上传每个切片到二级临时文件夹,可选择校验每个切片的md5值,在遇错重新传输时可通过校验每个切片的md5值决定是否跳过已上传的切片,实现断点秒传;
4、所有切片上传成功后对文件切片进行合并,然后校验合并后文件的md5值是否与原文件一致,如果一致则代表文件完整;
5、将合并后文件转移至存储区;
6、删除二级临时文件夹;
3和4中的文件切片上传和md5值获取,通过前端webuploader解决。
实现:
官网 http://fex.baidu.com/webuploader/ 下载插件包
下载后包内的readme.md 内容如下:
目录说明
========================
```bash
├── Uploader.swf # SWF文件,当使用Flash运行时需要引入。
├
├── webuploader.js # 完全版本。
├── webuploader.min.js # min版本
├
├── webuploader.flashonly.js # 只有Flash实现的版本。
├── webuploader.flashonly.min.js # min版本
├
├── webuploader.html5only.js # 只有Html5实现的版本。
├── webuploader.html5only.min.js # min版本
├
├── webuploader.noimage.js # 去除图片处理的版本,包括HTML5和FLASH.
├── webuploader.noimage.min.js # min版本
├
├── webuploader.custom.js # 自定义打包方案,请查看 Gruntfile.js,满足移动端使用。
└── webuploader.custom.min.js # min版本
选用webuploader.js 或者 webuploader.min.js,一开始选用了webuploader.html5only.js ,后来发现这个不支持md5方法,不能用来做md5校验。前端资源:jquery.min.js、webuploader.css、webuploader.min.js
service:
接口:
/**
* /** 切片上传大文件
*
* @param guid 临时唯一id
* @param chunk 切片id
* @param file 切片文件 web
* @param fileId 切片文件id
*/
void uploadBigFile(String guid, Integer chunk, MultipartFile file, String fileId)
throws Exception;
/**
* 合并上传的大文件切片
*
* @param guid 临时唯一id
* @param fileId 切片文件id
* @param fileName 切片文件名
* @param fileCategory 文件分类
* @throws Exception
*/
String mergeBigFile(String guid, String fileId, String fileName, String fileCategory, String md5)
throws Exception;
实现:
/**
* 切片上传大文件 此处参数没有接收每个切片的md5,没实现断点秒传,只在最后合并后校验md5值确定文件完整性
*
* @param guid 临时唯一id
* @param chunk 切片id
* @param file 切片文件
* @param fileId 切片文件id
*/
@Override
public void uploadBigFile(String guid, Integer chunk, MultipartFile file, String fileId)
throws Exception {
// 临时文件夹用来存放所有分片文件
String tempFileDir =
fileUploadProperties.getBigFileTmpPath().concat("/").concat(guid).concat(fileId);//fileUploadProperties.getBigFileTmpPath为从配置文件中获取临时目录,后面再拼接上本次上传的二级临时目录(目录名规则:唯一码+文件id)
File parentFileDir = new File(tempFileDir);
if (!parentFileDir.exists()) {
parentFileDir.mkdirs(); // 创建二级临时文件夹,一级临时文件夹不变,提前建好
}
try {
// 分片处理时,前台会多次调用上传接口,每次都会上传文件的一部分到后台
File tempPartFile = new File(parentFileDir, fileId + "_" + chunk + ".part");
FileUtils.copyInputStreamToFile(file.getInputStream(), tempPartFile);
} catch (Exception e) {
FileUtils.deleteDirectory(parentFileDir);
log.error("分片处理文件错误");
throw e;
}
}
/**
* 合并上传的大文件切片
*
* @param guid 临时唯一id
* @param fileId 切片文件id
* @param fileName 切片文件名
* @param fileCategory 文件分类
* @param md5 原文件md5值
* @throws Exception
*/
@Override
public String mergeBigFile(
String guid, String fileId, String fileName, String fileCategory, String md5)
throws Exception {
String newFileUrl = null;
File parentFileDir =
new File(
fileUploadProperties
.getBigFileTmpPath()
.concat("/")
.concat(guid)
.concat(fileId)); // 文件切片目录
File newFile =
new File(
fileUploadProperties
.getBigFileTmpPath()
.concat("/")
.concat(guid)
.concat(fileId)
.concat("/")
.concat(fileName)); // 合并切片后的最终文件
FileOutputStream destTempfos = null;
InputStream newFileForMd5Stream = null;
InputStream newFileInputStream = null;
try {
int dirlength = parentFileDir.listFiles().length;
for (int i = 0; i <= dirlength - 1; i++) {
File partFile = new File(parentFileDir, fileId + "_" + i + ".part");
destTempfos = new FileOutputStream(newFile, true);
// 遍历"所有分片文件"到"最终文件"中
FileUtils.copyFile(partFile, destTempfos);
destTempfos.close();
}
newFileForMd5Stream = new FileInputStream(newFile);
// 校验原文件与最终文件的md5一致性
String filemd5 = DigestUtils.md5Hex(newFileForMd5Stream);//获取最终文件md5值
newFileForMd5Stream.close();
if (!md5.equals(filemd5)) {
log.error(fileName.concat("与原文件md5不一致"));
throw new Exception(fileName.concat("与原文件md5不一致"));
}
// 通过校验的文件放入最终文件夹
FileType fileType = FileType.codeOf(FileTypeUtil.getType(newFile));//文件类型封装,校验文件分类是否业务允许,可去掉
FileCategory fc = FileCategory.codeOf(fileCategory);//文件分类封装,校验文件分类是否业务允许,可去掉
if (null != fc && null != fileType) {
newFileInputStream = new FileInputStream(newFile);
newFileUrl = this.uploadFile(newFileInputStream, fc, fileType);//转移合并后的临时大文件到最终文件夹
}
} catch (Exception e) {
log.error(fileName.concat("合并大文件切片错误"));
throw e;
} finally {
// 关闭所有流
try {
destTempfos.close();
} catch (Exception ex) {
}
try {
newFileForMd5Stream.close();
} catch (Exception ex) {
}
try {
newFileInputStream.close();
} catch (Exception ex) {
}
try {
FileUtils.deleteDirectory(parentFileDir);
} catch (Exception fe) {
}
}
return newFileUrl;//返回文件下载地址
}
controller:
/**
* 大文件切片上传
*
* @param request
* @param guid
* @param chunk
* @param file
* @param fileCategory
*/
@ApiOperation(value = "大文件切片上传", notes = "前端百度webuploader插件大文件切片上传")
@ApiImplicitParams({
@ApiImplicitParam(name = "guid", value = "临时id"),
@ApiImplicitParam(name = "chunk", value = "分片id"),
@ApiImplicitParam(name = "file", value = "大文件"),
@ApiImplicitParam(name = "fileCategory", value = "文件目录,系统支持的目录:portal、bid、tuan"),
@ApiImplicitParam(name = "id", value = "文件id")
})
@PostMapping("/bigFileUpload")
public void bigFileUpload(
HttpServletRequest request,
@RequestParam("guid") String guid,
Integer chunk,
@RequestParam("file") MultipartFile file,
@RequestParam("fileCategory") String fileCategory,
@RequestParam("id") String fileId) {
try {
log.info(fileId.concat("---大文件切片上传"));
boolean isMultipart = ServletFileUpload.isMultipartContent(request);
FileCategory fc = FileCategory.codeOf(fileCategory);
if (null == chunk) {
chunk = 0;
}
if (isMultipart && null != fc) {
fileUploadService.uploadBigFile(guid, chunk, file, fileId);
}
} catch (Exception e) {
log.error(
"大文件切片上传".concat(fileId).concat(" 第").concat(String.valueOf(chunk)).concat("块失败"), e);
}
}
/**
* 合并上传的大文件切片
*
* @param guid
* @param fileName
* @param fileCategory
* @return
*/
@ApiOperation(value = "合并上传的大文件切片", notes = "合并上传的大文件切片")
@ApiImplicitParams({
@ApiImplicitParam(name = "guid", value = "临时id"),
@ApiImplicitParam(name = "fileId", value = "文件id"),
@ApiImplicitParam(name = "fileName", value = "文件名"),
@ApiImplicitParam(name = "fileCategory", value = "文件目录,系统支持的目录:portal、bid、tuan"),
@ApiImplicitParam(name = "md5", value = "文件md5")
})
@PostMapping("/mergeBigFile")
public ApiResult mergeBigFile(
@RequestParam("guid") String guid,
@RequestParam("fileId") String fileId,
@RequestParam("fileName") String fileName,
@RequestParam("fileCategory") String fileCategory,
@RequestParam("md5") String md5) {
try {
log.info(fileId.concat("----大文件合并切片"));
List<FileVO> fileList = new ArrayList<FileVO>();
FileVO fileVO = new FileVO();
fileVO.setFileName(fileName);
String newFileUrl = fileUploadService.mergeBigFile(guid, fileId, fileName, fileCategory, md5);
if (null == newFileUrl) {
fileVO.setCode(500);
fileVO.setMsg("非法文件,非系统接受的文件或文件类型不支持");
} else {
fileVO.setCode(200);
fileVO.setMsg("成功");
fileVO.setFileUrl(newFileUrl);
}
fileList.add(fileVO);
return new ApiResult()
.setSuccess(true)
.setStatus(HttpStatus.OK)
.setMsg("成功")
.setData(fileList);
} catch (Exception e) {
log.error("失败", e);
return new ApiResult().setSuccess(false).setCode(500).setMsg("服务内部错误");
}
}
页面:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta http-stateequiv="X-UA-Compatible" content="IE=edge"><!--ie兼容 -->
<link th:href="${resURL}+‘/res/webupload/webuploader.css‘" rel="stylesheet" type="text/css"/>
<script type="text/javascript" th:src="${resURL}+‘/res/webupload/jquery.min.js‘"></script>
<script type="text/javascript" th:src="${resURL}+‘/res/webupload/webuploader.min.js‘"></script>
</head>
<body>
<div id="uploader" class="wu-example">
<!--用来存放文件信息-->
<div id="thelist" class="uploader-list"></div>
<div class="btns">
<div id="picker">选择文件</div>
<button id="ctlBtn" class="btn btn-default">开始上传</button>
</div>
</div>
</body>
<script type="text/javascript">
var GUID = WebUploader.Base.guid();//一个GUID
var uploader = WebUploader.create({
// 文件接收服务端。
server: ‘/gpapiFile/bigFileUpload‘,
formData: {
guid: GUID,
fileCategory: "bid" //文件目录,可选
},
// 选择文件的按钮。可选。
// 内部根据当前运行是创建,可能是input元素,也可能是flash.
pick: ‘#picker‘,
chunked: true, // 分片处理
chunkSize: 2 * 1024 * 1024, // 每片2M,
chunkRetry: false,// 如果失败,则不重试
threads: 20,// 上传并发数。允许同时最大上传进程数。
// 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
resize: false
});
$("#ctlBtn").click(function () {
uploader.upload();
});
</script>
<script>
// 当有文件被添加进队列的时候
uploader.on(‘fileQueued‘, function (file) {
$("#thelist").append(‘<div id="‘ + file.id + ‘" class="item">‘ +
‘<h4 class="info">‘ + file.name + ‘</h4>‘ +
‘<p class="state">等待上传...</p>‘ +
‘<p class="remove-this">删除</p>‘ +
‘</div>‘);
//删除上传的文件
$("#thelist").on(‘click‘, ‘.remove-this‘, function () {
if ($(this).parent().attr(‘id‘) == file.id) {
uploader.removeFile(file);
$(this).parent().remove();
}
});
});
// 文件上传过程中创建进度条实时显示。
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" style="height:20px; role="progressbar" style="width: 0%">‘ +
‘</div>‘ +
‘</div>‘).appendTo($li).find(‘.progress-bar‘);
}
$li.find(‘p.state‘).text(‘上传中‘);
$percent.css(‘width‘, percentage * 100 + ‘%‘);
});
// 文件上传成功,给item添加成功class, 用样式标记上传成功。
uploader.on(‘uploadSuccess‘, function (file, response) {
$(‘#‘ + file.id).find(‘p.state‘).text(‘已上传,正在校验完整性......‘);
var md5 = "";
uploader.md5File(file.source).fail(function () {//获取文件md5值传给后台做md5校验
console.log("md5失败");
}).then(function (val) {
md5 = val;
$.post(‘/gpapiFile/mergeBigFile‘, {
guid: GUID,
fileId: file.id,
fileName: file.name,
fileCategory: ‘bid‘,
md5: md5
}, function (data) {
if (data.data[0].code == 500) {
$(‘#‘ + file.id).find(‘p.state‘).text(data.data[0].msg);
} else {
$(‘#‘ + file.id).find(‘p.state‘).text(data.data[0].fileUrl);
}//$("#uploader").children("#" + file.id).children(".state").html("上传成功...");
//$("#uploader .state").html("上传成功...");
});
});
});
// 文件上传失败,显示上传出错。
uploader.on(‘uploadError‘, function (file) {
$(‘#‘ + file.id).find(‘p.state‘).text(‘上传出错‘);
});
// 完成上传完了,成功或者失败,先删除进度条。
uploader.on(‘uploadComplete‘, function (file) {
$(‘#‘ + file.id).find(‘.progress‘).fadeOut();
});
//所有文件上传完毕
uploader.on("uploadFinished", function () {
//提交表单
console.log("dd");
});
</script>
</html>
运行效果: