springboot+webuploader+mysql实现大文件分片、可以关掉浏览器的断点上传、秒传(含完整项目)

  • 时间:
  • 来源:互联网

项目在此:https://github.com/NoonLoveSnow/bigfileupload.git

先说下为什么用数据库记录文件分片,因为文件分片可能不会存储在本机上,用数据库可以记录分片存储位置,方便获取。。合并后的文件也可以记录其md5值,作为真实文件,用户文件可以只需要新建表引用它就行了。

大文件传输主要注意的有以下几点:

大文件传输要求

大文件传输往往是比较耗时的,所以要求传输操作可继续的即断点续传,不能因为一些故障导致重新上传整个文件。

断点续传思路:

在浏览器传输时需要将文件分片上传,当遇到意外情况继续上传时,则之前传输完成的分片就不用传输了而传输未上传成功的分片。

如何记录上传成功的分片:

文件MD5与分片编号作为联合主键记录在数据库中。

分片上传前如何判断分片已经传输完成:

数据库中有记录,且文件长度与分片大小(chunkSize)相等

合并后文件校验:

文件大小是否不一致,每个分片应有的大小加起来与合并后的文件相比较

关于WebUploader:

网上有很多教程和例子。。

主要代码

后端:

package com.noonsnow.bigfileupload.controller;

import com.alibaba.fastjson.JSONObject;

import com.noonsnow.bigfileupload.mapper.FileBlockMapper;
import com.noonsnow.bigfileupload.mapper.UploadFileMapper;
import com.noonsnow.bigfileupload.pojo.FileBlock;
import com.noonsnow.bigfileupload.pojo.UploadFile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.Date;
import java.util.List;

//http://localhost:8081/uploadPage
@Controller
public class FileUploadController {
    @Autowired
    FileBlockMapper blockMapper;
    @Autowired
    UploadFileMapper fileMapper;

    final static String filesDir = "C:/files";
    final static String blocksDir = "C:/bolcks";

    @RequestMapping("uploadPage")
    public String page() {
        return "bigFileUpload";
    }


    @PostMapping("checkFileIsExist")//检查文件是否存在
    @ResponseBody
    public Object checkFileExist(String md5value) {
        JSONObject resp = new JSONObject();
        List<UploadFile> uploadFiles = fileMapper.getByMd5(md5value);//逻辑上只有一个文件,由于不是按主键查找返回的是一个List
        if (uploadFiles.size() == 0)
            resp.put("exist", "0");
        else
            resp.put("exist", 1);
        return resp.toString();
    }


    @PostMapping("checkChunkIsExist")//检查分片是否存在且完整
    @ResponseBody
    public Object checkChunkExist(String md5value, int chunk, int chunkSize) {

        JSONObject resp = new JSONObject();

        //查数据库,分片是否存在
        FileBlock fileBlock = blockMapper.getByMd5AndChunk(md5value, chunk);

        if (fileBlock == null) {//数据库不存在分片记录或者分片不完整,则重传
            resp.put("exist", "0");
            return resp.toString();
        }

        File block = new File(blocksDir + "/" + md5value + "/" + chunk); //前面没通过就不用执行后面的了,性能会好点吧。。
        // System.out.println("blocklen:"+block.length()+"  "+"chunkSize:"+chunkSize);
        if (block.length() != chunkSize) {//分片是否完整
            resp.put("exist", "0");
            return resp.toString();
        }

        resp.put("exist", "1");
        return resp.toString();

    }


    @PostMapping("upload")//接收上传的分片
    @ResponseBody
    public String fileUpload(String md5value, String chunks, int chunk, String id, String name,
                             String type, String lastModifiedDate, int chunkSize, MultipartFile file) {

        //-----------------------保存分片--------------------
        File blockDir = new File(blocksDir + "/" + md5value);
        blockDir.mkdirs();
        File block = new File(blockDir, String.valueOf(chunk));
        try {
            //搞了N多天,WebUploader tmd要重复发送验证通过的分片,导致多插入数据,导致合并文件变大,坑 (其实还是之前我没把MD5和chunk弄成联合主键,不然它重复插入不了的,自作孽啊)
            boolean exist = blockMapper.getByMd5AndChunk(md5value, chunk) == null ? false : true;
            if (!exist) {
                FileBlock fileBlock = new FileBlock(md5value, new Date(), block.getPath(), chunk, name, chunkSize);
                blockMapper.addFileBlock(fileBlock);//增加分片记录
            }
            file.transferTo(block);

        } catch (IOException e) {
            block.delete();
            blockMapper.deleteByMd5AndChunk(md5value, chunk);
        }

        JSONObject jresp = new JSONObject();
        jresp.put("code", 0);
        jresp.put("msg", "上传成功");
        return jresp.toString();
    }


    @RequestMapping("merge")//合并分片
    @ResponseBody
    public Object merge(String md5value, String name) {
        JSONObject resp = new JSONObject();
        //----------防止重复请求合并,如果数据库有记录,则不用合并了------------
        List<UploadFile> uploadFiles = fileMapper.getByMd5(md5value);
        if (uploadFiles.size() != 0) {
            resp.put("status", "OK");
            return resp.toString();
        }

        File fileDir = new File(filesDir + "/" + md5value);//文件所在目录
        fileDir.mkdirs();
        File file = new File(fileDir, name);
        List<FileBlock> blocks = blockMapper.findByMd5(md5value);//查询出所有分片
        System.out.println("blocks:" + blocks.size());
        //-----------------合并分片-----------------
        BufferedOutputStream bos = null;
        boolean fail = false;//是否异常
        try {
            bos = new BufferedOutputStream(new FileOutputStream(file, true));
            for (FileBlock block : blocks) {
                try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File(block.getLocation())))) {//为了关闭每个分片的输入流
                    int len;
                    byte[] b = new byte[1024];
                    while ((len = bis.read(b)) != -1)
                        bos.write(b, 0, len);
                } catch (Exception e) {
                    e.printStackTrace();
                    resp.put("status", "FAIL");
                    return resp.toString();
                }
            }

        } catch (IOException e) {
            //--------------异常---------------
            fail = true;
            e.printStackTrace();
            resp.put("status", "FAIL");
        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }

        if (fail) {
            file.delete();//必须删除,合并文件是按追加而不是覆盖的方式写入文件。
            return resp.toString();
        }

        if (checkTotalSize(file, blocks)) {
            //新增文件记录
            fileMapper.addUploadFile(new UploadFile(md5value, new Date(), file.getPath(), name));
            //----------------删除分片及记录----------------
            deleteFile(new File(blocksDir + "/" + md5value));
            blockMapper.deleteByMd5(md5value);
            resp.put("status", "OK");

        } else {
            file.delete();
            resp.put("status", "FAIL");
        }
        return resp.toString();
    }

    //-----------验证合并后的文件,就是验证文件大小---------------
    boolean checkTotalSize(File file, List<FileBlock> blocks) {
        long total = 0;
        for (FileBlock block : blocks) {
            total += (long) block.getChunkSize();
        }
        System.out.println("total:" + total + "  " + "fileLength:" + file.length());
        return total == file.length();
    }

    //删除分片文件
    void deleteFile(File file) {
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            for (File f : files) {
                deleteFile(f);
            }
        }
        file.delete();
    }
}

前端:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>webuploader上传</title>
    <link rel="stylesheet" type="text/css" th:href="@{/webuploader/webuploader.css}">
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
    <script type="text/javascript" th:src="@{/webuploader/webuploader.min.js}"></script>

</head>
<body>
<section class="content">
    <div class="container" style="margin-top: 20px">
        <div class="alert alert-info">可以一次上传多个大文件</div>
    </div>
    <div class="container" style="margin-top: 50px">
        <div id="uploader" class="container">
            <div class="container">
                <div id="fileList" class="uploader-list"></div>
                <!--存放文件的容器-->
            </div>
            <div class="btns container">
                <div id="picker" class="webuploader-container"
                     style="float: left; margin-right: 10px">
                    <div>
                        选择文件
                        <!--<input type="file" name="file"
                                      class="webuploader-element-visible" multiple="multiple"/>-->
                    </div>
                </div>

                <div id="UploadBtn" class="webuploader-pick"
                     style="float: left; margin-right: 10px">开始上传
                </div>
                <div id="StopBtn" class="webuploader-pick"
                     style="float: left; margin-right: 10px" status="suspend">暂停上传
                </div>
            </div>
        </div>
    </div>
</section>
<script>

</script>
<script type="text/javascript">
    $(function () {
        //-----------------------注册--------------------


        WebUploader.Uploader.register({

            'before-send-file': 'beforeSendFile',//整个文件上传前
            'before-send': 'beforeSend',//分片上传前
        }, {
            beforeSend: function (block) {

                var deferred = WebUploader.Deferred();
                // block为分块数据。
                var file = block.file; // file为分块对应的file对象。
                var fileMd5 = file.wholeMd5;
                // 修改data可以控制发送哪些携带数据。

                console.info("fileName= " + file.name + " fileMd5= " + fileMd5 + " fileId= " + file.id);
                console.info("input file= " + flie_count);

                $.ajax({
                    type: "POST"
                    , url: '[[@{/checkChunkIsExist}]]'
                    , data: {
                        md5value: fileMd5
                        , chunk: block.chunk
                        ,chunkSize:block.end-block.start
                    }
                    , cache: false
                    , timeout: 1000
                    , dataType: "json"

                }).then(function (resp, textStatus, jqXHR) {
                    if (resp.exist == "1") {
                        deferred.reject();
                    } else {
                        deferred.resolve();
                    }
                }, function (jqXHR, textStatus, errorThrown) {    //任何形式的验证失败,都触发重新上传
                    deferred.resolve();
                });
                return deferred.promise();
            },


            beforeSendFile: function (file) {

                var that = this;
                var deferred = WebUploader.Deferred();
                //md5计算,没有才计算
                if (file.wholeMd5 == "0") {
                    that.owner.md5File(file)
                        .progress(function (percentage) {
                            console.log('Percentage:', percentage);
                        }).progress(function (percentage) {
                        $('#' + file.id).find('p.state').text('读取文件:' + parseInt(percentage * 100) + "%");
                    })
                    // 完成
                        .then(function (fileMd5) { // 完成
                            var end = +new Date();
                            console.log("before-send-file  preupload: file.size=" + file.size + " file.md5=" + fileMd5);
                            file.wholeMd5 = fileMd5;//获取到了md5
                            //uploader.options.formData.md5value = file.wholeMd5;//每个文件都附带一个md5,便于实现秒传
                            $('#' + file.id).find('p.state').text('MD5计算完毕');
                            console.info("MD5=" + fileMd5);

                            //上传前请求服务端,判断文件是否已经上传过
                            $.ajax({
                                type: "post",
                                url: '[[@{/checkFileIsExist}]]',
                                data: {"md5value": fileMd5},
                                dataType: "json",

                                success: function (resp) {
                                    if (resp.exist == "1") {//如果存在则跳过
                                        deferred.reject();
                                        that.owner.skipFile(file);
                                        // alert("文件已存在,无需上传!");
                                        $('#' + file.id).find('p.state').text('秒传!');
                                        $('#' + file.id).find(".info").find('.btn').hide();//上传完后删除"删除"按钮
                                    }
                                    deferred.resolve();
                                }
                            });
                        });
                } else {
                    deferred.resolve();
                }

                return deferred.promise();
            }
        });

        //---------------------注册结束-------------------

        $list = $('#fileList');
        var flie_count = 0;
        var uploader = WebUploader.create({
            //设置选完文件后是否自动上传
            auto: false,
            //swf文件路径
            swf: '[[@{/webuploader/Uploader.swf}]]',
            // 文件接收服务端。
            server: '[[@{/upload}]]',
            // 选择文件的按钮。可选。
            // 内部根据当前运行是创建,可能是input元素,也可能是flash.
            pick: '#picker',
            chunked: true, //开启分块上传
            chunkSize: 2 * 1024 * 1024,
            chunkRetry: 3,//网络问题上传失败后重试次数
            threads: 3, //上传并发数
            //fileNumLimit :1,
            fileSizeLimit: 2000 * 1024 * 1024,//最大2GB
            fileSingleSizeLimit: 2000 * 1024 * 1024,
            resize: false//不压缩
            //选择文件类型
            //accept: {
            //    title: 'Video',
            //    extensions: 'mp4,avi',
            //    mimeTypes: 'video/*'
            //}
        });
        // 当有文件被添加进队列的时候<input type="text" id="s_WU_FILE_'+flie_count+'" />
        uploader.on('fileQueued', function (file) {
            $list.append('<div id="' + file.id + '" class="item">' +
                '<h4 class="info">' + file.name + '<button type="button" fileId="' + file.id + '" class="btn btn-danger btn-delete"><span class="glyphicon glyphicon-trash"></span></button></h4>' +
                '<p class="state">待上传...</p>' +
                '</div>');
            console.info("id=file_" + flie_count);
            flie_count++;
            file.wholeMd5 = "0";

            //删除要上传的文件
            //每次添加文件都给btn-delete绑定删除方法
            $(".btn-delete").click(function () {
                //console.log($(this).attr("fileId"));//拿到文件id
                uploader.removeFile(uploader.getFile($(this).attr("fileId"), true));
                $(this).parent().parent().fadeOut();//视觉上消失了
                $(this).parent().parent().remove();//DOM上删除了
            });
            //uploader.options.formData.guid = WebUploader.guid();//每个文件都附带一个guid,以在服务端确定哪些文件块本来是一个
            //console.info("guid= "+WebUploader.guid());


        });
        // 文件上传过程中创建进度条实时显示。
        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" role="progressbar" style="width: 0%">' +
                    '</div>' +
                    '</div>').appendTo($li).find('.progress-bar');
            }
            $li.find('p.state').text('上传中');
            $percent.css('width', percentage * 100 + '%');
        });


        //块发送前填充数据
        uploader.on('uploadBeforeSend', function (block, data) {
            // block为分块数据。

            // file为分块对应的file对象。
            var file = block.file;
            var fileMd5 = file.wholeMd5;

            // 修改data可以控制发送哪些携带数据。
            console.info("fileName= " + file.name + " fileMd5= " + fileMd5 + " fileId= " + file.id);
            console.info("input file= " + flie_count);
            // 将存在file对象中的md5数据携带发送过去。
            data.md5value = fileMd5;//md5
            data.fileName_ = $("#s_" + file.id).val();
            data.chunkSize=block.end-block.start;
            console.log("fileName_: " + data.fileName_);


        });


        uploader.on('uploadSuccess', function (file) {
            $('#' + file.id).find('p.state').text('已上传,正在合并分片');
            $('#' + file.id).find(".progress").find(".progress-bar").attr("class", "progress-bar progress-bar-success");
            $('#' + file.id).find(".info").find('.btn').fadeOut('slow');//上传完后删除"删除"按钮
            $('#StopBtn').fadeOut('slow');

            //通知合并分片
            $.ajax({
                type: "post",
                url: '[[@{/merge}]]',
                data: {
                    "md5value": file.wholeMd5, "name": file.name
                },
                dataType: "json",

                success: function (resp) {
                    if (resp.status == "OK") {//合并完成
                        $('#' + file.id).find('p.state').text('合并完成');
                    } else {
                        $('#' + file.id).find('p.state').text('上传失败');
                        $('#' + file.id).find(".info").find('.btn').show();//上传完后删除"删除"按钮
                    }
                }
            });
        });
        uploader.on('uploadError', function (file) {
            $('#' + file.id).find('p.state').text('上传出错');
            //上传出错后进度条变红
            $('#' + file.id).find(".progress").find(".progress-bar").attr("class", "progress-bar progress-bar-danger");
            //添加重试按钮
            //为了防止重复添加重试按钮,做一个判断
            //var retrybutton = $('#' + file.id).find(".btn-retry");
            //$('#' + file.id)
            if ($('#' + file.id).find(".btn-retry").length < 1) {
                var btn = $('<button type="button" fileid="' + file.id + '" class="btn btn-success btn-retry"><span class="glyphicon glyphicon-refresh"></span></button>');
                $('#' + file.id).find(".info").append(btn);//.find(".btn-danger")
            }
            $(".btn-retry").click(function () {
                //console.log($(this).attr("fileId"));//拿到文件id
                uploader.retry(uploader.getFile($(this).attr("fileId")));
            });
        });
        uploader.on('uploadComplete', function (file) {//上传完成后回调
            //$('#' + file.id).find('.progress').fadeOut();//上传完删除进度条
            //$('#' + file.id + 'btn').fadeOut('slow')//上传完后删除"删除"按钮
        });
        uploader.on('uploadFinished', function () {
            //上传完后的回调方法
            //alert("所有文件上传完毕");
            //提交表单
        });
        $("#UploadBtn").click(function () {
            uploader.upload();//上传
        });
        $("#StopBtn").click(function () {
            console.log($('#StopBtn').attr("status"));
            var status = $('#StopBtn').attr("status");
            if (status == "suspend") {
                console.log("当前按钮是暂停,即将变为继续");
                $("#StopBtn").html("继续上传");
                $("#StopBtn").attr("status", "continuous");
                console.log("当前所有文件===" + uploader.getFiles());
                console.log("=============暂停上传==============");
                uploader.stop(true);
                console.log("=============所有当前暂停的文件=============");
                console.log(uploader.getFiles("interrupt"));
            } else {
                console.log("当前按钮是继续,即将变为暂停");
                $("#StopBtn").html("暂停上传");
                $("#StopBtn").attr("status", "suspend");
                console.log("===============所有当前暂停的文件==============");
                console.log(uploader.getFiles("interrupt"));
                uploader.upload(uploader.getFiles("interrupt"));
            }
        });
        uploader.on('uploadAccept', function (file, response) {
            if (response._raw === '{"error":true}') {
                return false;
            }
        });
    });
</script>
</body>
</html>

 

本文链接http://element-ui.cn/news/show-40239.aspx