使用webuploader組件實現大文件分片上傳,斷點續傳

無組件斷點續傳.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組件進行大文件的分片上傳、斷點續傳,以及服務器端分片合并與轉移。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容