美文网首页程序人生让前端飞程序员
使用webuploader组件实现大文件分片上传,断点续传

使用webuploader组件实现大文件分片上传,断点续传

作者: Xiangdong_She | 来源:发表于2017-08-20 13:48 被阅读348次
    无组件断点续传.gif
    1. 组件简介

    webuploader:是一个以HTML5为主, Flash为辅的文件上传组件,采用大文件分片/并发上传的方式,极大地提高了文件上传的效率,同时兼容多种浏览器版本;

    2. 项目背景简介

    本篇文章的背景,是在上一篇文章(《无组件实现大文件分片上传,断点续传》)的项目背景下进行的一次尝试,所以本篇文章还是基于上一篇文章的背景,但是不会介绍视频基本信息(视频标题、简介、播出时间等)的操作,主要介绍文件的上传。因为项目的特殊需求,这种使用插进的方式最终没有被采用,因为一些控件无法做到定制化。
    上一篇文章(《无组件实现大文件分片上传,断点续传》)中介绍的文件上传方式,在前端主要采用纯JavaScript来进行文件切分、验证,后台主要采用了NIO的方式进行分片的追加。而在这篇文章中,将介绍前端采用webuploader,后台采用临时目录+传统I/O方式进行分片合并的方式。

    3. 技术实现
    3.1 组件引入

    webuploader官网下载必要的文件,放入项目中。在页面中进行引入;

    0_组件引入.png
    <!-- webuploader文件上传 -->
    <script src="static/webuploader/webuploader.nolog.min.js"></script>
    <link href="static/webuploader/webuploader.css" rel="stylesheet" type="text/css" />
    
    3.2 前端页面实现

    在前端页面中,可以不用关心css样式,但需要注意标签的id/nama属性,这些将在后面的JavaScript中使用到。

    1-页面实现.png
    <!-- Main content -->
        <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-invisible" 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>
    
    3.3 使用组件实现文件的上传、切分、发送

    在这部分,将使用组件完成文件上传、MD5验证、删除、切片、上传进度条显示、暂停、继续上传及上传成功/失败时候的回调。

    无组件断点续传.gif
    <script type="text/javascript">
            $(function () {
                $list = $('#fileList');
                var flie_count = 0;
                var uploader = WebUploader.create({
                    //设置选完文件后是否自动上传
                    auto: false,
                    //swf文件路径
                    swf: 'static/webuploader/Uploader.swf',
                    // 文件接收服务端。
                    server: 'micro/BigFileUp',
                    // 选择文件的按钮。可选。
                    // 内部根据当前运行是创建,可能是input元素,也可能是flash.
                    pick: '#picker',
                    chunked: true, //开启分块上传
                    chunkSize: 10 * 1024 * 1024,
                    chunkRetry: 3,//网络问题上传失败后重试次数
                    threads: 1, //上传并发数
                    //fileNumLimit :1,
                    fileSizeLimit: 2000 * 1024 * 1024,//最大2GB
                    fileSingleSizeLimit: 2000 * 1024 * 1024,
                    resize: false//不压缩
                    //选择文件类型
                    //accept: {
                    //    title: 'Video',
                    //    extensions: 'mp4,avi',
                    //    mimeTypes: 'video/*'
                    //}
                });
                // 当有文件被添加进队列的时候
                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">正在计算文件MD5...请等待计算完毕后再点击上传!</p><input type="text" id="s_WU_FILE_'+flie_count+'" />' +
                            '</div>');
                    console.info("id=file_"+flie_count);
                    flie_count++;
                    
                    //删除要上传的文件
                    //每次添加文件都给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());
                    
                    //md5计算
                    uploader.md5File(file)
                        .progress(function(percentage) {
                          console.log('Percentage:', percentage);
                        })
                        // 完成
                        .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);
                        });
                    
                    
                });
                // 文件上传过程中创建进度条实时显示。
                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();
                    console.log("fileName_: "+data.fileName_);
                    // 删除其他数据
                    // delete data.key;
                    if(block.chunks>1){ //文件大于chunksize 分片上传
                        data.isChunked = true;
                        console.info("data.isChunked= "+data.isChunked);
                    }else{
                        data.isChunked = false;
                        console.info("data.isChunked="+data.isChunked);
                    }
    
                });
                
                
                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');
                });
                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>
    

    以上为前端代码的实现

    3.4 后台分片接收

    在后台分片接收部分,主要是判断文件是否有分片,如果没有,则直接存放到目的目录;如果存在分片,则创建临时目录,存放分片信息;之后判断当前分片所属的文件的所有分片是否已经传输完毕,如果当前分片数==所属文件总分片数,则开始合并文件并转移完整文件到目的目录,并且删除临时目录;
    如下图,是上传文件时所创建的临时目录及目录中的临时文件;

    2-临时目录.png 3-临时文件.png

    Controller实现

    /**
         * 
         * @Description: 
         *          接受文件分片,合并分片
         * @param guid   
         *          可省略;每个文件有自己唯一的guid,后续测试中发现,每个分片也有自己的guid,所以不能使用guid来确定分片属于哪个文件。
         * @param md5value
         *          文件的MD5值
         * @param chunks
         *          当前所传文件的分片总数
         * @param chunk
         *          当前所传文件的当前分片数
         * @param id
         *          文件ID,如WU_FILE_1,后面数字代表当前传的是第几个文件,后续使用此ID来创建临时目录,将属于该文件ID的所有分片全部放在同一个文件夹中
         * @param name
         *          文件名称,如07-中文分词器和业务域的配置.avi
         * @param type
         *          文件类型,可选,在这里没有用到
         * @param lastModifiedDate 文件修改日期,可选,在这里没有用到
         * @param size  当前所传分片大小,可选,没有用到
         * @param file  当前所传分片
         * @return
         * @author: xiangdong.she
         * @date: Aug 20, 2017 12:37:56 PM 
         */
        @ResponseBody
        @RequestMapping(value = "/BigFileUp")
        public String fileUpload(String guid, String md5value, String chunks, String chunk, String id, String name,
                String type, String lastModifiedDate, int size, MultipartFile file) {
            String fileName;
            JSONObject result=new JSONObject();
            try {
                int index;
                String uploadFolderPath = FileUtil.getRealPath(request);
    
                String mergePath = uploadFolderPath + "\\fileDate\\" + id + "\\";
                String ext = name.substring(name.lastIndexOf("."));
    
                // 判断文件是否分块
                if (chunks != null && chunk != null) {
                    index = Integer.parseInt(chunk);
                    fileName = String.valueOf(index) + ext;
                    // 将文件分块保存到临时文件夹里,便于之后的合并文件
                    FileUtil.saveFile(mergePath, fileName, file, request);
                    // 验证所有分块是否上传成功,成功的话进行合并
                    FileUtil.Uploaded(md5value, guid, chunk, chunks, mergePath, fileName, ext, request);
                } else {
                    SimpleDateFormat year = new SimpleDateFormat("yyyy");
                    SimpleDateFormat m = new SimpleDateFormat("MM");
                    SimpleDateFormat d = new SimpleDateFormat("dd");
                    Date date = new Date();
                    String destPath = uploadFolderPath + "\\fileDate\\" + "video" + "\\" + year.format(date) + "\\"
                            + m.format(date) + "\\" + d.format(date) + "\\";// 文件路径
                    String newName = System.currentTimeMillis() + ext;// 文件新名称
                    // fileName = guid + ext;
                    // 上传文件没有分块的话就直接保存目标目录
                    FileUtil.saveFile(destPath, newName, file, request);
                }
    
            } catch (Exception ex) {
                ex.printStackTrace();
                result.put("code", 0);
                result.put("msg", "上传失败");
                result.put("data", null);
                return result.toString();
            }
            result.put("code", 1);
            result.put("msg", "上传成功");
            return result.toString();
        }
    
    
    3.5文件I/O操作实现

    此部分代码较多,已将FileUtil上传至Batatas项目下的Util目录(喜欢Batatas这个项目的小伙伴,别忘了点个star哟,或者也非常欢迎加入我们),在这部分实现中,主要用到了一下几个方法:

    • saveFile()//保存分片至临时目录,或者保存未拆分文件到目标目录;
    • mergeFile()//合并临时目录中的临时文件,并将合并后的文件转移至目标目录;
    • saveStreamToFile()//使用I/O流合并分片文件
    • getSavePath()//获取文件保存的路径,如果没有该目录,则创建,可用于临时目录或目标存放目录的创建;
    • isAllUploaded()//在fileUtil中,使用一个全局的uploadInfoList去存放,已经上传的分片信息;在合并分片之前,首先回去这个List中检查属于该文件的所有分片信息是否已经存在,如果不存在,则不合并;如果已全部存在,则将这些信息从list中删除,并开始合并分片;
    4. 总结

    本篇文章主要介绍了使用百度Webuploader组件进行大文件的分片上传、断点续传,以及服务器端分片合并与转移。

    相关文章

      网友评论

        本文标题:使用webuploader组件实现大文件分片上传,断点续传

        本文链接:https://www.haomeiwen.com/subject/kujfdxtx.html