springboot+webuploader 实现大文件切片上传,兼容IE8+,chrome等浏览器,附可运行代码

2 篇文章 0 订阅
1 篇文章 0 订阅

一、目标
主要解决在web端浏览器上传大文件问题,避免上传途中中断、卡死、jvm溢出等问题,支持进度条显示、秒传功能、多线程上传,兼容IE8+、chrome等主流浏览器
二、方案
前端借助百度提供的webuploader插件,对大文件计算MD5值并切成一个个小文件,多线程向后台上传;后台用springboot实现,根据MD5和接收的个数判断是否接收完毕,如果接收完毕进行后台合并操作。
三、前端部分
Html代码

<div id="uploader" class="wu-example">
		<div id="picker">选择文件</div>
		<a id="btn" onclick="startUpload()">上传 </a>
		<div id="upload_info" style="width: 100%; height: 100px;"></div>
</div>

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(
			'<div id="' + file.id + '" class="item">'
					+ '<p class="info">' + file.name + '</p>'
					+ '<p class="state">等待上传...</p></div>');
});

//文件上传过程中创建进度条实时显示。
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;
        }
    }

--------------------------------------->代码下载<---------------------------------------

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值