一、目标
主要解决在web端浏览器上传大文件问题,避免上传途中中断、卡死、jvm溢出等问题,支持进度条显示、秒传功能、多线程上传,兼容IE8+、chrome等主流浏览器
二、方案
前端借助百度提供的webuploader插件,对大文件计算MD5值并切成一个个小文件,多线程向后台上传;后台用springboot实现,根据MD5和接收的个数判断是否接收完毕,如果接收完毕进行后台合并操作。
三、前端部分
Html代码
选择文件
上传
Js代码
var fileMd5;
var state;
var uploader;
var chunkSize=5 * 1024 * 1024;
var access_token="0010";
var user_id="1";
$(function() {
//监听分块上传过程中的三个时间点
WebUploader.Uploader.register({
"before-send-file" : "beforeSendFile",
"before-send" : "beforeSend",
"after-send-file" : "afterSendFile",
}, {
//时间点1:所有分块进行上传之前调用此函数
beforeSendFile : function(file) {
var deferred = WebUploader.Deferred();
var owner = this.owner;
//1、计算文件的唯一标记,用于断点续传
(new WebUploader.Uploader()).md5File(file, 0, chunkSize)
.progress(function(percentage) {
$('#upload_info').find("p.state").text("正在读取文件信息...");
}).then(function(val) {
fileMd5 = val;
$('#upload_info').find("p.state").text("成功获取文件信息...");
//2、判断文件是否已存在
$.ajax({
type : "GET",
url : webUploadAddress + "/resource/findResourceFileExistsByMd5",
data : {
access_token:access_token,
user_id:user_id,
//文件唯一标记
md5value : fileMd5
},
dataType : "json",
async:false,
success : function(response) {
if (response.code==2000) {
if (response.data.isExist) {
//文件已上传,跳过
console.log("文件已上传");
deferred.reject();
owner.skipFile(file);
} else {
//获取文件信息后进入下一步
deferred.resolve();
}
}
}
});
});
return deferred.promise();
},
//时间点2:如果有分块上传,则每个分块上传之前调用此函数
beforeSend : function(block) {
var deferred = WebUploader.Deferred();
$.ajax({
type : "GET",
url : webUploadAddress + "/resource/findResourceChunkExists",
data : {
access_token:access_token,
user_id:user_id,
//文件唯一标记
md5value : fileMd5,
//当前分块下标
chunk : block.chunk,
//当前分块大小
chunkSize : block.end - block.start
},
dataType : "json",
async:false,
success : function(response) {
if (response.code==2000) {
if (response.data.isExist) {
//分块存在,跳过
console.log("分块存在,跳过");
deferred.reject();
} else {
//分块不存在或不完整,重新发送该分块内容
console.log("分块不存在或不完整,重新发送该分块内容");
deferred.resolve();
}
}
}
});
this.owner.options.formData.md5value =fileMd5;
this.owner.options.formData.chunk =block.chunk;
this.owner.options.formData.access_token =access_token;
this.owner.options.formData.user_id =user_id;
deferred.resolve();
return deferred.promise();
},
//时间点3:所有分块上传成功后调用此函数
afterSendFile : function() {
//如果分块上传成功,则获取上传结果
$.ajax({
type : "GET",
url : webUploadAddress + "/resource/findUploadResult",
data : {
access_token:access_token,
user_id:user_id,
//文件唯一标记
md5value : fileMd5
},
dataType : "json",
async:false,
success : function(response) {
if (response.code==2000) {
if (response.data) {
console.log("如果分块上传成功,则获取上传结果",response.data);
}
}
}
});
}
});
uploader = WebUploader
.create({
method:'POST',
// swf文件路径
swf :Global.assets
+ '/resource/thirdparty/webuploader-0.1.5/Uploader.swf',
// 文件接收服务端。
server : webUploadAddress + '/resource/upload',
// 选择文件的按钮。可选。
// 内部根据当前运行是创建,可能是input元素,也可能是flash.
pick :'#picker',
resize : false,// 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
auto : false,
prepareNextFile :true,//是否允许在文件传输时提前把下一个文件准备好
chunked : true,//开启分片上传
chunkRetry:3,//如果某个分片由于网络问题出错,允许自动重传多少次?
chunkSize :chunkSize,
duplicate :true,
formData:{
title:'视频上传',
resourceType:'video/mp4',
guid:"SD.mp4"
},
threads:3,
accept : {
//限制上传文件为MP4
// extensions : 'mp4',
// mimeTypes : 'video/mp4',
}
});
//当有文件被添加进队列的时候
uploader.on('beforeFileQueued', function(file) {
//清空队列
uploader.reset();
});
//当有文件被添加进队列的时候
uploader.on('fileQueued', function(file) {
$('#upload_info').empty();
$('#upload_info').html(
''
+ '' + file.name + '
'
+ '等待上传...
');
});
//文件上传过程中创建进度条实时显示。
uploader.on('uploadProgress', function(file, percentage) {
//进度最大99%,不然后台合并时会在100%卡一会,造成用户疑惑
var jd=Math.round(percentage * 100);
if(jd>99){
jd=99;
}
$('#upload_info').find('p.state').text(
'上传中 ' + jd + '%');
});
uploader.on('uploadSuccess', function(file) {
$('#upload_info').find('p.state').text(
'上传中 ' + 100 + '%');
$('#' + file.id).find('p.state').text('上传成功!');
});
uploader.on('uploadError', function(file) {
$('#' + file.id).find('p.state').text('上传出错!');
});
uploader.on('uploadComplete', function(file) {
$('#' + file.id).find('.progress').fadeOut();
resetUpload();
});
uploader.on( 'all', function( type ) {
if ( type === 'startUpload' ) {
state = 'uploading';
} else if ( type === 'stopUpload' ) {
state = 'paused';
} else if ( type === 'uploadFinished' ) {
state = 'done';
}
});
});
function resetUpload() {
uploader.upload();
$('#btn').attr("onclick", "stopUpload()");
$('#btn').text("上传");
}
function startUpload() {
uploader.upload();
$('#btn').attr("onclick", "stopUpload()");
$('#btn').text("取消上传");
}
function stopUpload() {
uploader.stop(true);
$('#btn').attr("onclick", "startUpload()");
$('#btn').text("继续上传");
}
四、后端部分
接收代码
package com.demo.fileupload.controller;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.demo.fileupload.dto.ResponseEx;
import com.demo.fileupload.entity.Resource;
import com.demo.fileupload.entity.ResourceFile;
import com.demo.fileupload.service.ResourceService;
import com.demo.fileupload.vo.FileSaveInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
/**
* 资源上传Controller
*
* @author zjx
* @date 2018年10月29日 上午10:44:22
* @version V1.0
* @history 2018年10月29日 上午10:44:22 create
* @Description
*
*/
@Api(tags="资源", description = "资源相关api文档接口列表")
@RestController
@RequestMapping("/resource")
public class ResourceController extends BaseUploadController {
@Autowired
private ResourceService resourceService;
@ApiOperation(value="资源上传", notes="资源上传接口,swagger不能模拟分片功能,需百度的webuploader或者plupload等支持分片功能的上传组件支持",httpMethod = "POST", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiImplicitParams({
@ApiImplicitParam(name = "access_token", value = "鉴权token", required = true, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "user_id", value = "用户ID", required = true, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "title", value = "资源标题", required = true, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "resourceType", value = "资源类型", required = true, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "brief", value = "资源备注说明", required = false, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "guid", value = "临时文件名", required = false, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "md5value", value = "客户端生成md5值", required = false, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "chunks", value = "总分片数", required = false, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "chunk", value = "当前分片序号", required = false, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "name", value = "上传文件名", required = false, dataType = "String", paramType="query")
})
@PostMapping("/upload")
public ResponseEx upload(
String access_token,
String user_id,
String title,
String resourceType,
String brief,
String guid,
String md5value,
String chunks,
String chunk,
String name,
@RequestParam(value = "file",required = false) MultipartFile file){
Assert.notNull(title,"资源文件标题不能为空");
Assert.notNull(resourceType,"资源文件类型不能为空");
Resource resource = new Resource();
resource.setBrief(brief);
resource.setName(title);
resource.setType(resourceType);
resource.setUserId(user_id);
if(file == null){
Assert.notNull(md5value,"MD5值不能为空");
ResourceFile resourceFile = resourceService.findFirstByMd5Value(md5value);
Assert.notNull(resourceFile,"资源文件不存在!");
resource.setFileId(resourceFile.getId());
resourceService.saveUpload(resource);
}else {
//文件保存
FileSaveInfo info = fileUpload(guid, md5value, chunks, chunk, name, file);
if (info != null) {
info.setType("resource");
resourceService.saveUpload(resource, info);
}
}
return new ResponseEx();
}
@ApiOperation(value="根据md5值判断资源文件是否存在", notes="根据md5值判断资源文件是否存在",httpMethod = "GET", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiImplicitParams({
@ApiImplicitParam(name = "access_token", value = "鉴权token", required = false, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "user_id", value = "用户ID", required = true, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "md5value", value = "文件md5值", required = true, dataType = "String", paramType="query")
})
@GetMapping("/findResourceFileExistsByMd5")
public ResponseEx findResourceFileExists( String access_token,
String user_id,String md5value){
ResourceFile resourceFile = resourceService.findFirstByMd5Value(md5value);
return new ResponseEx().data(Collections.singletonMap("isExist",resourceFile != null));
}
@ApiOperation(value="根据md5值和分片下标判断当前分片是否存在", notes="根据md5值和分片下标判断当前分片是否存在",httpMethod = "GET", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiImplicitParams({
@ApiImplicitParam(name = "access_token", value = "鉴权token", required = false, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "user_id", value = "用户ID", required = true, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "md5value", value = "文件md5值", required = true, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "chunk", value = "分片下标", required = true, dataType = "Integer", paramType="query"),
@ApiImplicitParam(name = "chunkSize", value = "分片大小", required = true, dataType = "Integer", paramType="query")
})
@GetMapping("/findResourceChunkExists")
public ResponseEx findResourceChunkExists( String access_token,
String user_id,String md5value,Integer chunk,Integer chunkSize){
boolean isExist=resourceService.findResourceChunkExists(md5value,chunk,chunkSize);
return new ResponseEx().data(Collections.singletonMap("isExist",isExist));
}
@ApiOperation(value="根据md5值获取上传结果", notes="根据md5值获取上传结果",httpMethod = "GET", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiImplicitParams({
@ApiImplicitParam(name = "access_token", value = "鉴权token", required = false, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "user_id", value = "用户ID", required = true, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "md5value", value = "文件md5值", required = true, dataType = "String", paramType="query")
})
@GetMapping("/findUploadResult")
public ResponseEx findUploadResult( String access_token,
String user_id,String md5value){
FileSaveInfo fileSaveInfo=resourceService.findUploadResult(md5value);
return new ResponseEx().data(fileSaveInfo);
}
}
合并主要代码
/**
* 上传文件
* @param md5 MD5
* @param guid 随机生成的文件名
* @param chunk 文件分块序号
* @param chunks 文件分块数
* @param fileName 文件名
* @param ext 文件后缀名
* @return FileSaveInfo 文件保存信息对象 如果全部分片保存成功则返回对象,还没成功返回空
*/
public static FileSaveInfo uploaded( final String md5,
String guid,
final String chunk,
final String chunks,
final String uploadFolderPath,
final String fileName,
final String name,
final String ext)
throws Exception {
synchronized (uploadInfoList) {
uploadInfoList.add(new UploadInfo(md5, chunks, chunk, uploadFolderPath, fileName, ext,new Date()));
}
boolean allUploaded = isAllUploaded(md5, chunks);
int chunksNumber = Integer.parseInt(chunks);
if (allUploaded) {
//判断是否自动命名
if(FileUtil.getPropertiesValue("file.save.name.auto").equals("true")){
guid = UUID.randomUUID().toString();
}
String lastSavePath = mergeFile(chunksNumber, ext, guid, uploadFolderPath,md5);
File file = new File(lastSavePath);
FileSaveInfo info = new FileSaveInfo();
info.setSaveName(guid+ext);
info.setMd5(md5);
info.setSize(file.length());
info.setPath(lastSavePath);
info.setFix(ext);
info.setRelativePath(relativePath(FileUtil.getSavePath(),lastSavePath));
info.setName(name);
info.setCreateDate(new Date());
//合并成功的临时存储
synchronized (mergeSuccessList) {
mergeSuccessList.add(info);
}
return info;
}else{
return null;
}
}
--------------------------------------->代码下载<---------------------------------------