AJAX+JAVA实现带进度条切割上传大文件(可以改造成断点续传)

259 阅读3分钟

    上传文件是常用的功能,以前由于网速、技术、电脑硬件各方面的原因导致通过web方式上传大文件是一件比较困难的事情,随着各方面的技术发展,大文件上传变的容易了。

上传文件如果是小文件,以前都是把后台的设置修改成项目中允许的最大容量,一次性做文件上传即可,但这种方法用于上传大文件肯定是不可取的,不管是网速还是客户端浏览器都不支持这种情况。

大文件上传之所以比较麻烦,主要问题就在于文件容量过大,那么如果我们可以将要上传的大文件在前端分块上传其实就能解决这个问题。

上传大文件的流程: 前端将文件分块  --> 不断将小块文件依次上传给后台 --> 后台接收文件后,合并文件

一、前端分割大文件

       文件表单  : <input type="file" id="file" name="file" />

      上传按钮  :<input type="button" id="breakPointUploadFile" name="breakPointUploadFile" />

      分割大文件    

// 设置分块的大小: 2 M
const chunkSize = 2*1024*1024;
const file = $('#file')[0].files[0];
// 分片总数
const totalChunk = Math.ceil(file.size / chunkSize); 

二、JAVA后端接收分块的小文件后合并文件

      java获取到前端提交的上传文件,后台通过常用的IO的处理类写入文件到服务器的指定位置,这样就完成了文件的上传,但常用的文件处理类都是从头开始写入文件,而分块上传大文件时,如果每次都是从头开始写入,

那就会覆盖掉前面上传的文件,这样肯定是不对的。而如果我们可以做到在指定位置写入文件,其实就能解决小文件合并成大文件的功能,而JAVA 的API中的RandomAccessFile类就可以实现此功能。

  RandomAccessFile是java Io体系中功能最丰富的文件内容访问类。即可以读取文件内容,也可以向文件中写入内容。

  • long getFilePointer(); 返回文件记录指针的当前位置
  • void seek(long pos); 将文件记录指针定位到pos位置

  创建RandomAccessFile对象还需要指定一个mode参数。该参数指定RandomAccessFile的访问模式,有以下4个值:

  • “r” 以只读方式来打开指定文件夹。如果试图对该RandomAccessFile执行写入方法,都将抛出IOException异常。
  • “rw” 以读,写方式打开指定文件。如果该文件尚不存在,则试图创建该文件。
  • “rws” 以读,写方式打开指定文件。相对于”rw” 模式,还要求对文件内容或元数据的每个更新都同步写入到底层设备。
  • “rwd” 以读,写方式打开指定文件。相对于”rw” 模式,还要求对文件内容每个更新都同步写入到底层设备。

三、前端AJAX循环上传分块的文件

const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
$(function(){
	const chunkSize = 2 * 1024*1024;// 每个chunk的大小,2兆 	
	$("#breakPointUploadFile").click(function(){
		const file = $('#file')[0].files[0];
		// 分片总数
		const totalChunk = Math.ceil(file.size / chunkSize); 		
		breakPointUploadFile(0,totalChunk,chunkSize,file);
	});	
});
/**
 * 分片上传
 * i - 第几片
 * totalChunk - 分片总数
 * chunkSize  - 每片大小
 * file 要上传的文件
 */
function breakPointUploadFile(i,totalChunk,chunkSize,file){
	var per = (i*100/totalChunk).toFixed(1)+"%";
	$(".progressBar").css("width",per);
	$(".progressBar").html(per);
	const startLength = i * chunkSize; //当前上传文件块的起始位置
    const endLength = Math.min(file.size, startLength + chunkSize);
	var formData = new FormData();
    formData.append("file", blobSlice.call(file, startLength, endLength));
    formData.append("startLength",startLength);
    formData.append("fileName",file.name);   
    $.ajax({
        url: '/upload/breakPointUploadFileDo.do',
        dataType:'json',
        type:'POST',
        async: false,
        data: formData,
        processData : false, // 使数据不做处理
        contentType : false, // 不要设置Content-Type请求头
        success: function(data){
            console.log(data);
            if (data.succeed) {
            	i++;
            	if(i<totalChunk){
            		console.log("****>" + i);
    	        	setTimeout(function (){breakPointUploadFile(i,totalChunk,chunkSize,file);},200);
            	}else{
            		$(".progressBar").css("width","100%");
            		$(".progressBar").html("100%");
            		alert("文件上传成功");
            	}
            }
        },
        error:function(response){
            console.log(response);
            alert("异常")
        }
    });
}

    上面就是分割文件并通过AJAX循环上传的代码。

    上传分块文件的方法:

         function breakPointUploadFile(i,totalChunk,chunkSize,file)  

    进度条处理:

    var per = (i*100/totalChunk).toFixed(1)+"%";
$(".progressBar").css("width",per);
$(".progressBar").html(per);

    当前上传文件块的起始位置:const startLength = i * chunkSize; //要将此值提交到后台
当前上传文件块的结束位置:const endLength = Math.min(file.size, startLength + chunkSize);

四、JAVA后台合并文件   

    @ResponseBody
	@RequestMapping("breakPointUploadFileDo")
	public AjaxResponse breakPointUploadFileDo(
			@RequestParam final MultipartFile file,
			final long startLength,
			final String fileName) {
		final AjaxResponse ajaxResponse = new SucceedResponse("文件上传成功");
		System.out.println(file + " ** " + startLength);
		if (file.getSize() <= 0) {
			return new ErrorResponse("请选择上传的文件!");
		}
		int len = 0;
		final byte[] arr = new byte[1024];
		final String writeFileName = "D:/test-" + fileName;
		try (RandomAccessFile writeFile = new RandomAccessFile(writeFileName, "rw")) {
			writeFile.seek(startLength);
			final InputStream iStream = file.getInputStream();
			while ((len = iStream.read(arr)) != -1) {
				writeFile.write(arr, 0, len);
			}
		} catch (final Exception e) {
			final String errorMessage = "断点上传文件异常";
			this.logger.error(errorMessage, e);
			throw new NormStarRuntimeException(errorMessage, e);
		}
		return ajaxResponse;
	}

  AjaxResponse.java   

public abstract class AjaxResponse implements Serializable {

	private static final long serialVersionUID = -5858819825109609209L;

	/**
	 * 是否成功
	 */
	private boolean succeed;
	/**
	 * 提示信息编码 如:NS00001
	 */
	private String code;
}

SucceedResponse.java \ ErrorRespons.java  继承AjaxResponse ,分别默认succeed的值为true、false

五、效果

  

六、断点续传(简单版)

      具体的代码就不写了,讲一个简单版本的思路。顾名思义,断点续传也即在上传大文件时中途因为各种原因导致文件只上传了一部分,需要再一次继续上传,并且跳过已经上传的文件部分,直接从未上传的部分开始。

      这里就要解决几个问题。1.如果判断上传的文件,以前已经上传过了  2. 如何知道从那一块开始上传

      1.如何判断文件已经上传过了  

             可以通过文件的MD5值来判断此文件以前是否已经上传过,这里就需要用到spark-md5.min.js 插件 (文末有下载地址,及使用方法),后台需要将文件的md5值和当前文件上传成功的最后一次的文件块数记录下来(必须保证2次文件分块的大小是一致的,及上面的chunkSize参数是一致的)

      2. 如何知道从那一块开始上传

          在每次上传的首次,先去数据库里获取文件的MD5是否存在,如果存在,则获取上一次上传成功时的文件块的位置数,然后直接调到下一个文件块上传即可。

spark-md5.min.js 插件及使用方法:spark-md5.zip-Javascript文档类资源-CSDN下载 本文已参与「新人创作礼」活动,一起开启掘金创作之路。