欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

WebUploader+springboot 实现文件断点续传

程序员文章站 2022-05-26 16:14:51
...

在网上参照使用WebUploader上传文件断点续传,使用WebUploader和 springboot实现文件断点续传。

主要思路:文件分段上传后存储,在文件最终全部分片都上传成功后讲分片文件进行合并。

 

前端代码:

<h3 >视频文件上传</h3>
        <div style="margin: 20px 20px 20px 0;">
            <div id="picker" class="form-control-focus" >选择文件</div>
        </div>
        <div id="thelist" class="uploader-list"></div>
        <button  id="btnSync" type="button" class="btn btn-primary">开始上传</button>
        <button  id="btnStop" type="button" class="btn btn-danger">暂停</button>
        <button  id="btnRetry" type="button" class="btn btn-info">继续</button>
        <div class="progress">
            <div id="progress" class="progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
                <span class="sr-only"></span>
            </div>
        </div>

<script>
    _extensions = '3gp,mp4,rmvb,mov,avi,m4v,qlv,wmv';
    _mimeTypes = 'video/*,audio/*,application/*';

    var fileMd5;//文件唯一标识
    var fileId;//文件ID
    var fileName;//文件名称
    var count = 0;//当前正在上传的文件在数组中的下标,一次上传多个文件时使用
    var filesArr = new Array();//文件数组:每当有文件被添加进队列的时候 就push到数组中
    var map = {};//key存储文件id,value存储该文件上传过的进度
    //监听分块上传的三种状态
    WebUploader.Uploader.register({
            "before-send-file": "beforeSendFile",//整个文件上传前
            "before-send": "beforeSend",  //每个分片上传前
            "after-send-file": "afterSendFile",  //分片上传完毕
        },
        {
            //所有分块进行上传之前调用此函数
            beforeSendFile: function (file) {
                //alert('分块上传前调用的函数');
                var deferred = WebUploader.Deferred();
                //1、计算文件的唯一标记fileMd5,用于断点续传  如果.md5File(file)方法里只写一个file参数则计算MD5值会很慢 所以加了后面的参数:5*1024*1024
                (new WebUploader.Uploader()).md5File(file, 0, 5 * 1024 * 1024).progress(function (percentage) {
                    $('#' + file.id).find('p.state').text('正在读取文件信息...');
                })
                    .then(function (val) {
                        $('#' + file.id).find("p.state").text("正在上传...");
                        fileMd5 = val;
                        fileId = file.id;
                        uploader.options.formData.guid = fileMd5;
                        //获取文件信息后进入下一步
                        deferred.resolve();
                    });

                fileName = file.name; //为自定义参数文件名赋值
                return deferred.promise();
            },
            //如果有分块上传,则每个分块上传之前调用此函数 ,检验该分片是否上传过
            beforeSend: function (block) {
                //alert('-检验分块是否上传-');
                var deferred = WebUploader.Deferred();
                $.ajax({
                    type: "POST",
                    url: "/common/webuploader/checkChunk",  //ajax验证每一个分片
                    data: {
                        fileName: fileName,
                        fileMd5: fileMd5,  //文件唯一标记
                        chunk: block.chunk,  //当前分块下标
                        chunkSize: block.end - block.start,//当前分块大小
                        guid: uploader.options.formData.guid
                    },
                    cache: false,
                    async: false,  // 与js同步
                    timeout: 1000, //todo 超时的话,只能认为该分片未上传过
                    dataType: "json",
                    success: function (response) {
                        if (response.existFlag) {
                            $('#' + fileId).find("p.state").text("正在续传...");
                            //分块存在,跳过
                            deferred.reject();
                        } else {
                            //分块不存在或不完整,重新发送该分块内容
                            deferred.resolve();
                        }
                    }
                });

                this.owner.options.formData.fileMd5 = fileMd5;
                deferred.resolve();
                //继续执行分片上传
                return deferred.promise();
            },
            //所有分块上传成功后调用此函数,通知后台合并所用分块
            afterSendFile: function () {
                //alert('-所有分块上传完成后调用该函数-');
                //如果分块上传成功,则通知后台合并分块
                $.ajax({
                    type: "POST",
                    url: "/common/webuploader/uploadSuccess",  //ajax将所有片段合并成整体
                    data: {
                        fileName: fileName,
                        fileMd5: fileMd5,
                        guid: uploader.options.formData.guid
                    },
                    success: function () {
                        count++; //每上传完成一个文件 count+1
                        if (count <= filesArr.length - 1) {
                            uploader.upload(filesArr[count].id);//上传文件列表中的下一个文件
                        }
                        //合并成功之后的操作
                    }
                });
            }
        });
    var uploader = WebUploader.create({

        // swf文件路径
        swf: 'webuploader/Uploader.swf',
        // 文件接收服务端。
        server: '/common/webuploader/upload',
        // 选择文件的按钮。可选。
        // 内部根据当前运行是创建,可能是input元素,也可能是flash.
        pick: {
            id: '#picker',
            multiple: true
        },
        accept: {
            title: 'Videos',
            extensions: _extensions,
            mimeTypes: _mimeTypes
        },
        chunked: true,  //分片处理
        chunkSize: 5 * 1024 * 1024, //每片5M
        threads: 1,//上传并发数。允许同时最大上传进程数。
        // 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
        resize: false
    });

    uploader.on("error", function (type) {
        if (type == "Q_TYPE_DENIED") {
            alert("请上传mp4、rmvb、mov、avi、m4v、wmv格式文件");
        }
    });

    // 当有文件被添加进队列的时候
    uploader.on('fileQueued', function (file) {
        $("#thelist").append(
            '<div id="' + file.id + '" class="item">'
            + '<h4 class="info">' + file.name + '</h4>'
            + '<p class="file-pro"></p>'
            + '<p class="state">等待上传...</p>' + '</div>');
    });
    uploader.on('uploadSuccess', function (file) {
        $('#' + 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();
    });

    uploader.on('beforeFileQueued', function (file) {
        // alert(file.size);

    });

    $("#btnSync").on('click', function () {
        if ($(this).hasClass('disabled')) {
            return false;
        }
        //显示遮罩层操作
        //视频上传
        uploader.upload();

    });

    $("#btnStop").on('click', function () {
        $('#' + fileId).find("p.state").text("已暂停...");
        uploader.stop(true);
    });

    $("#btnRetry").on('click', function () {
        $('#' + fileId).find("p.state").text("继续上传...");
        uploader.upload();
    });

    // 文件上传过程中创建进度条实时显示
    uploader.on('uploadProgress', function (file, percentage) {
        $("td.file-pro").text("");
        var $li = $('#' + file.id).find('.file-pro'),
            $percent = $li.find('.file-progress .progress-bar');

        // 避免重复创建
        if (!$percent.length) {
            $percent = $('<div class="file-progress progress-striped active">' +
                '<div class="progress-bar" role="progressbar" style="width: 0%">' +
                '</div>' +
                '</div>' + '<br/><div class="per">0%</div>').appendTo($li).find('.progress-bar');
        }

        $li.siblings('.file-status').text('上传中');
        $li.find('.per').text((percentage * 100).toFixed(2) + '%');

        $percent.css('width', percentage * 100 + '%');
    });

    // 所有文件上传成功后调用
    uploader.on('uploadFinished', function () {
        //隐藏遮罩层操作
    });

    /*关闭上传框窗口后恢复上传框初始状态*/
    $('#picker').on('click', function () {
        // 移除所有并将上传文件移出上传序列
        for (var i = 0; i < uploader.getFiles().length; i++) {
            // 将文件从上传序列移除
            uploader.removeFile(uploader.getFiles()[i]);
            $("#" + uploader.getFiles()[i].id).remove();
        }
        // 重置uploader
        uploader.reset();
    })
</script>

 

 

后台代码:

import com.bootdo.common.config.BootdoConfig;
import com.bootdo.common.utils.R;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.*;

/**
 * webuploader文件上传
 */
@Controller
@RequestMapping("/common/webuploader")
public class WebUploaderController extends BaseController {

	private static final int BUFFER_SIZE = 100 * 1024;
	private static final Logger logger = LoggerFactory.getLogger(WebUploaderController.class);

	@Autowired
	private BootdoConfig bootdoConfig;

	/**
	 * 检测已上传文件分片信息
	 * @param chunk
	 * @param chunkSize
	 * @param guid
	 * @param fileName
	 * @param fileMd5
	 * @return
	 */
	@ResponseBody
	@PostMapping(value="checkChunk")
	public R checkChunk(@RequestParam String chunk, @RequestParam String chunkSize, @RequestParam String guid,
					   @RequestParam String fileName, @RequestParam String fileMd5) {
		// 获取分块文件临时存储的路径并新建该分块文件
		File checkFile = new File(bootdoConfig.getUploadPath() + guid + "/" + chunk);
		// 检查文件是否存在,且大小是否一致
		if (checkFile.exists() && checkFile.length() == Integer.parseInt(chunkSize)) {
			// 上传过 将结果返回给前段处理
			return R.ok().put("existFlag", 1);
		} else {
			// 没有上传过 返回给前段做处理
			return R.ok().put("existFlag", 0);
		}
	}

	/**
	 * 上传文件
	 * @param file
	 * @param chunk
	 * @param chunks
	 * @param guid
	 * @param name
	 * @param fileMd5
	 * @return
	 */
	@ResponseBody
	@PostMapping(value="upload")
	public R upload(@RequestParam MultipartFile file, @RequestParam Integer chunk,  @RequestParam Integer chunks, @RequestParam String guid,
						 @RequestParam String name, @RequestParam String fileMd5) {
		try {
			// 临时文件的保存路
			//检查文件目录,不存在则创建
			String relativePath = bootdoConfig.getUploadPath() + guid;
			File folder = new File(relativePath);
			if (!folder.exists()) {
				folder.mkdirs();
			}
			// 处理获取到的上传文件的文件名的路径部分,只保留文件名部分
			String fileName = name.substring(name.lastIndexOf("\\") + 1);
			// 以chunks分块文件的下标作为上传文件的名字
			File tmpFile = new File(folder, String.valueOf(chunk));

			//文件已存在删除旧文件(上传了同名的文件)
			if (chunk == 0 && tmpFile.exists()) {
				tmpFile.delete();
				tmpFile = new File(folder, String.valueOf(chunk));
			}

			//生成临时文件
			createFile(file.getInputStream(), tmpFile);
			if (chunk == chunks - 1) {
				logger.info("上传完成");
			}else {
				logger.info("还剩["+(chunks-1-chunk)+"]个块文件");
			}
		} catch (IOException e) {
			logger.error(e.getMessage());
		}

		return R.ok();
	}

	/**
	 * 上传文件成功后合并已上传文件块
	 * @param guid
	 * @param fileName
	 * @param fileMd5
	 */
	@ResponseBody
	@PostMapping(value="uploadSuccess")
	public void uploadSuccess(@RequestParam String guid, @RequestParam String fileName, @RequestParam String fileMd5) {
		logger.info("开始合并。。。文件唯一表示符=" + guid + ";文件名字=" + fileName);
		// 进行文件合并
		File newFile = new File(bootdoConfig.getUploadPath() + fileName);
		FileInputStream temp = null;
		FileOutputStream outputStream = null;
		File dir = new File(bootdoConfig.getUploadPath() + guid);
		File[] childs = dir.listFiles();
		List<File> files = Arrays.asList(childs);
		Collections.sort(files, new Comparator<File>() {
			@Override
			public int compare(File o1, File o2) {
				int o1Num = Integer.parseInt(o1.getName());
				int o2Num = Integer.parseInt(o2.getName());
				return o1Num - o2Num;
			}
		});
		try {
			int len;
			byte[] byt = new byte[5 * 1024 * 1024];
			outputStream = new FileOutputStream(newFile, true);// 文件追加写入
			for (int i = 0; i < files.size(); i++) {
				temp = new FileInputStream(files.get(i));
				while ((len = temp.read(byt)) != -1) {
					outputStream.write(byt, 0, len);
				}
				temp.close();
				temp = null;
			}
		} catch (IOException e) {
			throw new RuntimeException("合并文件失败");
		} finally {
			if (temp != null) {
				try {
					temp.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			try {
				outputStream.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		for (int i = 0; i < files.size(); i++) {
			//删除临时文件
			files.get(i).delete();
		}
		//删除临时文件夹
		dir.delete();
	}

	/**
	 * 生成目标文件
	 * @param in
	 * @param destFile
	 */
	private void createFile(InputStream in, File destFile) {
		OutputStream out = null;
		try {
			out = new BufferedOutputStream(new FileOutputStream(destFile), BUFFER_SIZE);
			in = new BufferedInputStream(in, BUFFER_SIZE);
			int len = 0;
			byte[] buffer = new byte[BUFFER_SIZE];
			while ((len = in.read(buffer)) > 0) {
				out.write(buffer, 0, len);
			}
		} catch (Exception e) {
			logger.error(e.getMessage());
		} finally {
			try {
				if (null != in) {
					in.close();
				}
				if(null != out){
					out.close();
				}
			} catch (IOException e) {
				logger.error(e.getMessage());
			}
		}
	}
}