無插件實(shí)現(xiàn)大文件分片上傳,斷點(diǎn)續(xù)傳

文件上傳.gif
1. 簡介:

本篇文章基于實(shí)際項(xiàng)目的開發(fā),將介紹項(xiàng)目中關(guān)于大文件分片上傳、文件驗(yàn)證、斷點(diǎn)續(xù)傳、手動(dòng)重試上傳等需求的使用場(chǎng)景及實(shí)現(xiàn);

2. 項(xiàng)目需求
  1. 在一個(gè)音視頻的添加中,既要有音視頻的簡介(如音視頻內(nèi)容文字介紹、自定義主題名稱等一些基本的信息),又要有音視頻所需要的多個(gè)文件(就像電視劇,一部電視劇有多集一樣)。在數(shù)據(jù)庫中具體表現(xiàn)為一對(duì)多的關(guān)系,即一個(gè)視頻對(duì)應(yīng)多個(gè)文件。下文就以電視劇為例
  2. 如果一個(gè)電視劇中,既有上百兆的,也有幾十兆的視頻,但是如果在不穩(wěn)定的一個(gè)網(wǎng)絡(luò)環(huán)境中,c傳輸大文件時(shí),客戶想先把小的視頻上傳了,之后再來繼續(xù)傳未傳完的大文件聲譽(yù)部分,這就需要斷點(diǎn)續(xù)傳的功能;
  3. 電視劇中至少有一集(至少有一個(gè)文件),無文件的電視劇基本信息無效;
3. 需求分析
  1. 確定電視劇基本信息(自定義名稱,內(nèi)容簡介、演員簡介、播出時(shí)間等)及文件的上傳方式

    • 基本信息和音視頻文件分開上傳(因?yàn)樵谠械臄?shù)據(jù)庫表設(shè)計(jì)中,文件表是關(guān)聯(lián)于基本信息,所以必須要有音視頻主鍵,才能在數(shù)據(jù)庫添加對(duì)應(yīng)的文件信息),取得基本信息主鍵之后再去上傳文件;
  2. 文件斷點(diǎn)續(xù)傳中,如何分片;文件接收方式;服務(wù)器端如何判斷是哪個(gè)文件的分片;如何拼接各個(gè)分片;上傳過程中發(fā)生意外情況(如斷網(wǎng),關(guān)閉瀏覽器),如何處理?

    • 分片方式: 在客戶端進(jìn)行分片;
    • 服務(wù)器端接收方式:使用MultipartFile接收文件
    • 服務(wù)器端確定是哪個(gè)文件的分片: 在客戶端按照一定規(guī)則(UUID或其他方式)生成唯一名稱,在服務(wù)器端直接找到與該名稱相同的文件片段;
    • 拼接文件分片: 使用NIO的方式,將分片追加到已有分片的后面;
    • 上傳中發(fā)生意外:
      A. 斷網(wǎng): 該情況類似于暫停上傳,上傳到文件處于暫停狀態(tài),網(wǎng)絡(luò)恢復(fù),即可點(diǎn)擊繼續(xù)上傳按鈕,繼續(xù)上傳;
      B. 關(guān)閉瀏覽器: 在關(guān)閉時(shí),給用戶提示框,詢問是否繼續(xù)保存,若不保存,則根據(jù)視頻基本信息表的主鍵的刪除臟數(shù)據(jù);
      C. 第一個(gè)文件在上傳時(shí)候,被用戶取消或者斷網(wǎng),則服務(wù)器端未修改基本信息為有效,并且也未標(biāo)記該文件為有效記錄,可以理解為臟數(shù)據(jù),但不需要清理這些數(shù)據(jù)(在查詢的時(shí)候,不能查出這些無效記錄,可以在更新視頻基本信息記錄的時(shí)候,查找這些臟數(shù)據(jù),并清理磁盤上及數(shù)據(jù)表中的記錄);
4. 實(shí)現(xiàn)

根據(jù)需求,已經(jīng)確認(rèn)了先上傳基本信息,后上傳文件,基本信息的提交(標(biāo)題、簡介、封面)比較簡單,只需要在前端提交數(shù)據(jù)表所需字段,然后后臺(tái)返回插入的主鍵即可,所以基本信息的提交及返回不過多說明,僅僅通過前端頁面截圖及后端部分代碼進(jìn)行簡要說明,實(shí)現(xiàn)部分主要講解分析文件上傳部分的代碼;
說明:本篇文章主要涉及到兩張表的操作,兩張表的數(shù)據(jù)結(jié)構(gòu)如下:

DataFile表.png


microClass表.png
  • 4.1.1 前端提交基本信息頁面

提交頁面使用模板+原生html+css實(shí)現(xiàn),每個(gè)人的頁面、所需參數(shù)各不相同,所以在前端代碼中,沒有多大的參考價(jià)值,所以直接使用截圖,來表現(xiàn)我需要做的工作、傳的參數(shù)。

1-基本信息.png
  • 4.1.2 前端保存基本信息代碼

可以根據(jù)自己的業(yè)務(wù)使用FormData封裝傳遞的參數(shù)

//點(diǎn)擊保存按鈕后保存數(shù)據(jù)
        function save_microClass() {
                //獲取form的基本信息
                var classTitle = $("#classTitle").val(),
                classDes = $("#classDes").val(),
                coverFile = document.getElementById("coverFileName").files[0],
                //構(gòu)造一個(gè)新表單,F(xiàn)ormData是HTML5新增的,因?yàn)榛拘畔⒅写嬖诜饷鎴D片,所以需要使用表單提交數(shù)據(jù)            
                var form = new FormData();            
                form.append("title", classTitle); 
                form.append("desc", classDes); 
                form.append("coverFile", coverFile);
                //Ajax提交            
                 $.ajax({                
                    url: "micro/save",               
                    type: "POST",                
                    data: form,                
                    async: true,        //異步                
                    processData: false,  //很重要,告訴jquery不要對(duì)form進(jìn)行處理                
                    contentType: false,  //很重要,指定為false才能形成正確的Content-Type                
                    success: function(data){
                        //在添加音視頻基本信息之后,服務(wù)器端返回新增音視頻的主鍵,以便之后上傳文件時(shí),與文件進(jìn)行關(guān)聯(lián)
                        if(data.length>0){
                            $("#microClassId").val(data);
                            submitFile();//提交文件
                        }
                    }
                });
            }
        }
  • 4.1.3 基本信息服務(wù)器端接收方法

服務(wù)器端需要與前端協(xié)同作戰(zhàn),接收參數(shù)需要與前端傳遞過來的參數(shù)對(duì)應(yīng)上,以免報(bào)出400的錯(cuò)誤。

    /**
     * @Description: 新增基本信息時(shí)保存form表單
     * @param title
     *            音視頻名稱
     * @param coverFile
     *            封面對(duì)象
     * @param desc
     *            音視頻描述字段
     */
    @RequestMapping(value = "/save", method = RequestMethod.POST)
    @ResponseBody
    public Integer create(String title,String desc, MultipartFile coverFile) {
        try {
             //保存基本信息邏輯,自己實(shí)現(xiàn),然后返回基本信息插入后的主鍵
             //request,即HttpServletRequest對(duì)象,在項(xiàng)目啟動(dòng)時(shí)候就被注入,如下:
             /**
              * @Autowired
              * private HttpServletRequest request;
             **/
             Integer videoId = videoService.save(title,desc, coverFile, request);
             return videoId;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
  • 4.2.1 前端上傳文件示例
文件上傳.gif
  • 4.2.2 前端實(shí)現(xiàn)

在前端實(shí)現(xiàn)中,采用純JavaScript+html+css來實(shí)現(xiàn)按鈕的刪除、添加、文件的添加、上傳時(shí)暫停、繼續(xù)、以及文件的順序控制;

    <div class="row pb40 pt40 mlr-10 border-top-1">
                        <div class="col-xs-12 col-lg-12">
                            <div  class="col-xs-12 col-lg-12">
                                <span id="mediaLable">添加視頻內(nèi)容</span><small style="color: #ff5704;margin-left:10px;" id="mediaLimit">視頻格式僅支持MP4及AVI,文件大小需小于500M</small>
                            </div>
                            <div id="mediaFiles" class="col-xs-12 col-lg-12 mt20 video-list-wrap">
                            </div>
                            <div class="col-xs-10 col-xs-offset-1 col-lg-10 col-lg-offset-1 mt20 mb10">
                                <a href="javascript:;" id="addMediaButton" onclick="addFileInput()" class="mybtn btn-add-user2">添加視頻</a>
                            </div>
                        </div>
                    </div>

以上代碼,就是文件上傳部分的html代碼,無需關(guān)心css樣式,但需要注意每個(gè)元素的id/name/事件函數(shù);以下就是JavaScript實(shí)現(xiàn)文件上傳邏輯的代碼;

<script type="text/javascript">      
        //刪除一個(gè)上傳文件文本框
        function delFileInput(that) {
            var delFileDivId = $(that).parent().attr("id");
            //如果是修改頁面的刪除則將要?jiǎng)h除的附件ID保存在隱藏框
            if (delFileDivId.indexOf("modify") == 0) {
                var deleteFileIds = $("#deleteFileIds").val();
                if (deleteFileIds.length > 0) {
                    deleteFileIds = deleteFileIds + "," + delFileDivId.split("_").pop();
                    $("#deleteFileIds").val(deleteFileIds);
                } else {
                    $("#deleteFileIds").val(delFileDivId.split("_").pop());
                }
            }
            var uploadFileDivs = $("#mediaFiles").children();
            episodeCount = uploadFileDivs.length-1;
            //刪除后修改集數(shù)
            for(var i=1;i<uploadFileDivs.length;i++){
                if($("#mediaFiles").children()[i].id==$(that).parent().parent().attr("id")){
                    $(that).parent().parent().remove();
                    for(var j=i;j<uploadFileDivs.length;j++){
                        $($("#mediaFiles").children()[j]).find(".account").text("第"+(j+1)+"集");
                    }
                    break;
                }
            }
        }

        //上傳附件文本框計(jì)數(shù)器
        var inputCount = 0;
        var episodeCount = 0;
        //點(diǎn)擊按鈕后新增一個(gè)上傳文件文本框
        function addFileInput() {
            //把新增的每一行都放到一個(gè)div中,刪除時(shí)刪除這個(gè)父div節(jié)點(diǎn)即可
            var rootFileDiv = document.createElement("div");
            rootFileDiv.setAttribute("id", "rootFileDiv" + inputCount);
            if(inputCount==0){
                rootFileDiv.setAttribute("class", "col-xs-12 col-lg-12 video-list");
            }else{
                rootFileDiv.setAttribute("class", "col-xs-12 col-lg-12 full video-list mt20");
            }
            $("#mediaFiles").append(rootFileDiv);
            //刪除圖標(biāo)
            var delDiv = document.createElement("div");
            delDiv.setAttribute("id", "delFileDiv" + inputCount);
            delDiv.setAttribute("class", "delete");
            if(inputCount>0){
                var delDivText = "<span>×</span>";
                delDiv.innerHTML = delDivText;
            }
            //集數(shù)
            var countDiv = document.createElement("div");
            countDiv.setAttribute("id", "account");
            countDiv.setAttribute("class", "account");
            var countDivText = "第"+(episodeCount+1)+"集";
            countDiv.innerHTML = countDivText;
            //文件名稱顯示文本框
            var mediaFileNameDiv = document.createElement("div");
            mediaFileNameDiv.setAttribute("class", "name");
            var mediaFileNameDivInput = "<input type='text' id=mediaFile" + inputCount + "_Name" + " class='form-control' />"; 
            mediaFileNameDiv.innerHTML = mediaFileNameDivInput;
            //文件上傳文本框
            var mediaFileDiv = document.createElement("div");
            mediaFileDiv.setAttribute("class", "filebtn");
            var mediaType = $("#type").val();
            var mediaFileInput;
            if(mediaType==0){
                mediaFileInput = "<a><span>選擇文件 ></span><input type='file' accept='.mp4,.avi' name='myfiles' id=mediaFile"+inputCount+"_ class='' onchange='handleFile(this)'/></a>";
            }else{
                mediaFileInput = "<a><span>選擇文件 ></span><input type='file' accept='.mp3' name='myfiles' id=mediaFile"+inputCount+"_ class='' onchange='handleFile(this)'/></a>";
            }
            mediaFileDiv.innerHTML = mediaFileInput;
            //上傳進(jìn)度條
            var progressbarDiv = document.createElement("div");
            progressbarDiv.setAttribute("id", "progressbar"+inputCount);
            progressbarDiv.setAttribute("class", "progress hide");
            var progressbarDivText = '<div class="progress-bar progress-bar-green" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"></div><div class="percent">0%</div><div class="size">40M/200M</div>';
            progressbarDiv.innerHTML = progressbarDivText;
            //暫停上傳或者繼續(xù)上傳按鈕
            var uploadStatusButtonDiv = document.createElement("div");
            uploadStatusButtonDiv.setAttribute("id", "uploadStatusButton"+inputCount);
            uploadStatusButtonDiv.setAttribute("class", "result");
            uploadStatusButtonDivButton = '<span class="success hide"><i class="fa fa-check-circle"></i>上傳完成</span><span class="stop-continue hide" id="uploadStatusButton" onclick="changeUploadStatus(this)">暫停上傳</span>';
            uploadStatusButtonDiv.innerHTML = uploadStatusButtonDivButton;
            $("#rootFileDiv" + inputCount).append(delDiv);
            $("#rootFileDiv" + inputCount).append(countDiv);
            $("#rootFileDiv" + inputCount).append(mediaFileNameDiv);
            $("#rootFileDiv" + inputCount).append(mediaFileDiv);
            $("#rootFileDiv" + inputCount).append(progressbarDiv);
            $("#rootFileDiv" + inputCount).append(uploadStatusButtonDiv);
            inputCount++;
            episodeCount++;
            return rootFileDiv;
        }
        
        //mediaFileArray存放分片數(shù)據(jù)信息
        var mediaFileArray=new Array();
        //獲取上傳分片數(shù)據(jù)基本信息,獲取完畢后調(diào)用上傳方法提交分片
        function submitFile(){
            var fileInputs = $("input[name='myfiles']");
            for(var i=0;i<fileInputs.length;i++){
                if(fileInputs[i].files[0]!=null){
                    var newName = guid();
                    var name = fileInputs[i].files[0].name, //文件名           
                    size = fileInputs[i].files[0].size, //總大小 
                    type = fileInputs[i].files[0].type, //文件類型
                    shardSize = 10 * 1024 * 1024, // shardSize = 10 * 1024 * 1024, 以10MB為一個(gè)分片            
                    shardCount = Math.ceil(size / shardSize); //總片數(shù)
                    var shardArray = new Array();
                    shardArray[0]=name;
                    shardArray[1]=size;
                    shardArray[2]=type;
                    shardArray[3]=shardSize;
                    shardArray[4]=shardCount;
                    shardArray[5]=0;//當(dāng)前上傳狀態(tài),0-等待上傳,1-正在上傳,2是上傳完成
                    shardArray[6]=0;//已傳輸最后分片編號(hào)
                    shardArray[7]=newName;//服務(wù)器存儲(chǔ)名稱
                    mediaFileArray[i]=shardArray;//將每個(gè)file的分片信息放入全局?jǐn)?shù)組
                }else{
                    //當(dāng)進(jìn)入修改頁面file為空時(shí)
                    var newName = guid();
                    var name = "", //文件名           
                    size = 0, //總大小 
                    type = "", //文件類型
                    shardSize = 10 * 1024 * 1024, // shardSize = 10 * 1024 * 1024, 以10MB為一個(gè)分片            
                    shardCount = 0; //總片數(shù)
                    var shardArray = new Array();
                    shardArray[0]=name;
                    shardArray[1]=size;
                    shardArray[2]=type;
                    shardArray[3]=shardSize;
                    shardArray[4]=shardCount;
                    shardArray[5]=0;//當(dāng)前上傳狀態(tài),0-等待上傳,1-正在上傳,2是上傳完成
                    shardArray[6]=0;//已傳輸最后分片編號(hào)
                    shardArray[7]=newName;//服務(wù)器存儲(chǔ)名稱
                    mediaFileArray[i]=shardArray;//將每個(gè)file的分片信息放入全局?jǐn)?shù)組
                }
            }
            postFile(0,0);//第一個(gè)參數(shù)是第幾個(gè)file元素,最后一個(gè)參數(shù)是從第幾個(gè)分片開始上傳
        }
        
        //附件完成上傳計(jì)數(shù)器
        var endCount=0;
        //控制是否繼續(xù)往服務(wù)端發(fā)送分片數(shù)據(jù),0-繼續(xù)提交,1-停止提交
        var flag=0;
        //文件斷點(diǎn)續(xù)傳
        function postFile(fileNum,shardNum){
             if(flag==0){
                //計(jì)算每一片的起始與結(jié)束位置
                var file = $("input[name='myfiles']")[fileNum].files[0];
                var oldName;
                if($("#mediaFiles").find(".video-list").eq(fileNum).find(".form-control").eq(0).val().length>0){
                    oldName = $("#mediaFiles").find(".video-list").eq(fileNum).find(".form-control").eq(0).val()+"#";
                }else{
                    oldName =  mediaFileArray[fileNum][0]+"#";
                }
                //修改時(shí)需要oldname拼接fileId
                var fileNameId = $("#mediaFiles").find(".video-list").eq(fileNum).find(".form-control").eq(0).attr("id");
                if(fileNameId.indexOf("modify")==0&&fileNameId.length>0&&fileNameId.split("_").length>0){
                    oldName = oldName + fileNameId.split("_")[1];
                }
                var start = shardNum * mediaFileArray[fileNum][3],
                end = Math.min(mediaFileArray[fileNum][1], start + mediaFileArray[fileNum][3]);
                /* //修改文件為空,但所有文件都已傳輸完則跳轉(zhuǎn)回view
                if(file==null&&endCount>=mediaFileArray.length){
                    window.location.href="micro/index?type="+$("#type").val();
                    return;
                } */
                //當(dāng)文件不為空,當(dāng)前文件所有分片都傳輸完時(shí)控制button顯示并跳轉(zhuǎn)回view
                if(shardNum >= mediaFileArray[fileNum][4]&&file!=null){
                    $("#mediaFiles").find(".video-list").eq(fileNum).find(".progress").eq(0).addClass("hide");
                    $("#mediaFiles").find(".video-list").eq(fileNum).find(".stop-continue").eq(0).addClass("hide");
                    $("#mediaFiles").find(".video-list").eq(fileNum).find('.success').eq(0).removeClass("hide");
                    //如果本文件傳完,開始傳輸下一個(gè)文件
                    if(fileNum<mediaFileArray.length-1&&mediaFileArray[fileNum+1][6]==0){
                        postFile(fileNum+1,0);
                    }
                    endCount++;
                    //如果所有的文件都傳輸完則跳轉(zhuǎn)回view
                    if(endCount>=mediaFileArray.length){
                        var location = (window.location+'').split('/');
                        var basePath = location[0]+'//'+location[2]+'/'+location[3];
                        window.location.href=basePath+"/micro/index?type="+$("#type").val();
                    }
                    return;            
                }
                //服務(wù)端URL
                var requestUrl = "micro/uploadSlice";
                //構(gòu)造一個(gè)表單,F(xiàn)ormData是HTML5新增的            
                var form = new FormData();
                if(file!=null){
                    form.append("data", file.slice(start,end));  //slice方法用于切出文件的一部分    
                }        
                //form.append("lastModified", file.lastModified);  //文件最后修改時(shí)間不支持IE
                form.append("name", mediaFileArray[fileNum][7]); //文件存儲(chǔ)名稱
                form.append("fileType", mediaFileArray[fileNum][2]); //文件類型
                form.append("total", mediaFileArray[fileNum][4]);  //總片數(shù)            
                form.append("index", shardNum + 1);  //當(dāng)前是第幾片
                form.append("oldName", oldName);  //用戶自定義文件名
                form.append("seq", fileNum);  //文件所處的順序
                form.append("microClassId", $("#microClassId").val());  //microClassId
                //Ajax提交            
                 $.ajax({                
                    url: requestUrl,                
                    type: "POST",                
                    data: form,                
                    async: true,        //異步                
                    processData: false,  //很重要,告訴jquery不要對(duì)form進(jìn)行處理                
                    contentType: false,  //很重要,指定為false才能形成正確的Content-Type                
                    success: function(data){
                        //顯示暫停按鈕
                        if(start==0&&file!=null){
                            $("#mediaFiles").find(".video-list").eq(fileNum).find(".stop-continue").eq(0).removeClass("hide");
                        }
                        mediaFileArray[fileNum][6]=shardNum+1;
                        if(data!="0"){                        
                            shardNum = data++;
                            console.log("當(dāng)前分片數(shù):",shardNum);
                            var num = Math.ceil(shardNum*mediaFileArray[fileNum][3]*100 / mediaFileArray[fileNum][1]); //百分比進(jìn)度
                            //改變進(jìn)度條進(jìn)度
                            $("#mediaFiles").find(".video-list").eq(fileNum).find(".progress").eq(0).removeClass("hide");
                            $("#mediaFiles").find(".video-list").eq(fileNum).find(".progress-bar").eq(0).attr("style","width: "+num+"%;");
                            $("#mediaFiles").find(".video-list").eq(fileNum).find(".percent").eq(0).text(num+"%");
                            $("#mediaFiles").find(".video-list").eq(fileNum).find(".size").eq(0).text(shardNum*10+"/"+Math.ceil(mediaFileArray[fileNum][1]/(1024 * 1024))+"M");
                            //通過button狀態(tài)來判斷是否繼續(xù)上傳
                            var text = $("#mediaFiles").find(".video-list").eq(fileNum).find(".stop-continue").eq(0).text();
                            if(text == "暫停上傳") postFile(fileNum,shardNum);                    
                        }else{
                            //當(dāng)修改文件為空時(shí)的處理
                            $("#mediaFiles").find(".video-list").eq(fileNum).find('.success').eq(0).removeClass("hide");
                            endCount=endCount+1;
                            if(endCount<mediaFileArray.length){
                                postFile(fileNum+1,0);
                            }else{
                                $("#mediaFiles").find(".video-list").eq(fileNum).find(".progress").eq(0).addClass("hide");
                                $("#mediaFiles").find(".video-list").eq(fileNum).find(".stop-continue").eq(0).addClass("hide");
                                $("#mediaFiles").find(".video-list").eq(fileNum).find('.success').eq(0).removeClass("hide");
                                var location = (window.location+'').split('/');
                                var basePath = location[0]+'//'+location[2]+'/'+location[3];
                                window.location.href=basePath+"/micro/index?type="+$("#type").val();
                            }
                            
                        }                
                    },
                    error:function(xhr,errorText,errorType){
                        $("#wrong_msg").text("網(wǎng)絡(luò)錯(cuò)誤,上傳失敗,請(qǐng)檢查網(wǎng)絡(luò)后繼續(xù)上傳!");
                        $("#tips1").modal("show");
                        mediaFileArray[fileNum][6]=shardNum;
                        $("#mediaFiles").find(".video-list").eq(fileNum).find(".stop-continue").eq(0).text("繼續(xù)上傳");
                    }
                });
            }else{
                return;
            }
        }
        
        //控制上傳狀態(tài)
        function changeUploadStatus(that){
            if($(that).text()=="暫停上傳"){
                var fileNum = $(that).parents(".video-list").eq(0).index();
                flag=1;
                $(that).text("繼續(xù)上傳");
                mediaFileArray[fileNum][5]=0;
                if(fileNum<mediaFileArray.length-1 && mediaFileArray[fileNum+1][6]==0){
                    $(that).parents("#mediaFiles").find(".video-list").eq(fileNum+1).find(".stop-continue").eq(0).text("暫停上傳");
                    flag=0;
                    postFile(fileNum+1,mediaFileArray[fileNum+1][6]);
                }
                return;
            }else{
                var fileNum = $(that).parents(".video-list").eq(0).index();
                var shardNum = mediaFileArray[fileNum][6];
                for(var i=0; i<mediaFileArray.length; i++){
                    if(mediaFileArray[i][5]==1){
                        mediaFileArray[i][5]==0;
                        flag=1;
                        $(that).parents("#mediaFiles").find(".video-list").eq(i).find(".stop-continue").eq(0).text("繼續(xù)上傳");
                    }
                }
                flag=0;
                mediaFileArray[fileNum][5]=1;
                $(that).text("暫停上傳");
                postFile(fileNum,shardNum);
                return;
            }
        }
        
        //用于生成uuid
        function S4() {
            return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
        }
        function guid() {
            return (S4()+S4()+S4()+S4()+S4()+S4()+S4()+S4());
        }
    </script>

以上代碼為前端控制文件上傳所需的代碼,之后介紹后臺(tái)部分的實(shí)現(xiàn)

  • 4.3.3 后臺(tái)實(shí)現(xiàn)

后臺(tái)實(shí)現(xiàn),主要是接收文件的分片,利用前端傳入唯一的新文件名稱,使用NIO的方式,將分片進(jìn)行合并。


Controller實(shí)現(xiàn)

/**
     * @Description: 上傳文件分片
     * 
     * @param data
     *            分片文件
     * @param fileType
     *            文件類型 video/avi audio/mp3
     * @param name
     *            文件名稱(newName),由Client生成,即NewName 如
     *            MICRO_CLASS_1502179979829.mp4
     * @param total
     *            分片總數(shù)
     * @param index
     *            當(dāng)前分片數(shù)
     * @param microClassId
     *            文件關(guān)聯(lián)的MicroClass主鍵,通過先保存基本信息取得并返回(DataFile中的OwerID)
     * @param oldName
     *            由用戶自己輸入的節(jié)目名稱,需要與文件對(duì)應(yīng)起來,如 實(shí)現(xiàn)兩個(gè)100年奮斗目標(biāo)
     * @param seq
     *            文件的順序,如第一集對(duì)應(yīng)1,第二集對(duì)應(yīng)2....
     * 
     * @return index 當(dāng)前合并到文件的分片數(shù)
     * 
     * @throws Exception
     */
    @RequestMapping("/uploadSlice")
    @ResponseBody
    public Integer uploadSlice(MultipartFile data, String fileType, String name, Integer total, Integer index,
            Integer microClassId, String oldName, int seq) throws Exception {
        int countFile = 0;// 記錄一次保存中上傳的文件數(shù)目

        /**
         * oldName:使用輸入框中的字符串與fileId拼接,形如D1#D2 如:
         * 實(shí)現(xiàn)兩個(gè)100年奮斗目標(biāo)#36,其中“實(shí)現(xiàn)兩個(gè)100年奮斗目標(biāo)”在數(shù)據(jù)庫中存為OldName,36表示數(shù)據(jù)庫中fileData主鍵
         * 
         * D1: 可以為空,表示用戶將Client獲取到的文件名稱刪除,并且未輸入任何字符串,可以為空; D2:
         * 可以為空,表示新增的文件,不為空,則表示在修改頁面,傳回的fileID;
         **/
        String[] str=oldName.split("#");
        String oldNameFiled=null;
        String fileDataId=null;
        System.out.println(str);
        switch (str.length) {
        case 0:
            break;
        case 1:
            oldNameFiled = oldName.split("#")[0];
            break;
        case 2:
            oldNameFiled = oldName.split("#")[0];
            fileDataId = oldName.split("#")[1];
            break;
        default:
            break;
        }
        
        if (total == 0) {
            // 表示在修改頁面,用戶只可能修改了oldName,但是未修改文件
            if (fileDataId != null && !fileDataId.equals("")) {
                if (oldNameFiled != null && !oldNameFiled.equals("")) {
                    dataFileService.updateOldNameById(fileDataId, oldNameFiled);//
                }
            }
        } else {
            if (index <= total) {//說明是有分片
                String dirType = fileType.split("/")[0];// 文件類型,用于創(chuàng)建不同的目錄,如(video/audio)
                String fileExt = "." + fileType.split("/")[1];// 文件擴(kuò)展名,如.mp3/.mp4/.avi
                System.out.println(data.getSize() + "----" + name + "-----" + total + "----" + index);
                // 追加分片到已有的分片上,返回保存文件的路徑,如/fileDate/video/2017/08/09
                String savePath = FileUtil.randomWrite(request, data.getBytes(), name, dirType, fileExt);
                if (index == 1 && savePath != null) {// 說明是新的文件的第一個(gè)分片,在數(shù)據(jù)庫中創(chuàng)建相應(yīng)的記錄,并且狀態(tài)為無效,等到全部上傳完畢之后在修改為有效
                    dataFile = new DataFile();
                    dataFile.setOldName(oldNameFiled);
                    dataFile.setFileUrl(savePath);
                    dataFile.setNewName(name + fileExt);
                    dataFile.setOwerId(microClassId);
                    dataFile.setSeq(seq);
                    dataFile.setStatus(1);
                    dataFileService.saveDataFile(dataFile);
                }
                if (index == total) {// 說明已經(jīng)成功上傳一個(gè)文件
                    // 根據(jù)文件名稱和OwerId來更新文件記錄,把記錄的狀態(tài)修改為0(有效)
                    dataFileService.updateByNewNameAndOwerId(name+fileExt, microClassId);
                    countFile++;
                    if (countFile == 1) {// 說明已經(jīng)上傳成功一個(gè)文件,則吧MicroClass的狀態(tài)改為0(有效);
                        microClassService.updateMicroClass(microClassId);// 根據(jù)microClassId來修改status
                    }
                    LOGGER.info("已上傳 " + countFile + " 個(gè)文件");
                }
                return index++;
            } else {
                return 0;
            }
        }
        return 0;
    }

FileUtil中randomWrite方法實(shí)現(xiàn)

    /**
     * @Description: 分片文件追加
     * @param request
     * @param sliceFile  分片文件
     * @param name   文件名稱
     * @param dirType  文件夾類型 如video/audio
     * @param fileExt  文件擴(kuò)展名 如.mp4/.avi  ./mp3
     * @return
     */
    public static String randomWrite(HttpServletRequest request, byte[] sliceFile, String name, String dirType,String fileExt) {
        try {
            /** 以讀寫的方式建立一個(gè)RandomAccessFile對(duì)象 **/
             //獲取相對(duì)路徑/home/gzxiaoi/apache-tomcat-8.0.45/webapps
            String realPath=getRealPath(request); 
            //拼接文件保存路徑 /fileDate/video/2017/08/09  如果沒有該文件夾,則創(chuàng)建
            String savePath=getSavePath(realPath,dirType);          
            String realName = name;
            String saveFile =realPath+ savePath + realName+fileExt;
            RandomAccessFile raf = new RandomAccessFile(saveFile, "rw");
            // 將記錄指針移動(dòng)到文件最后
            raf.seek(raf.length());
            raf.write(sliceFile);
            return savePath;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * @Description: 取得tomcat中的webapps目錄 如: /home/software/apache-tomcat-8.0.45/webapps
     * @param request
     * @return
     */
    public static String getRealPath(HttpServletRequest request) {
        String realPath = request.getSession().getServletContext().getRealPath(File.separator);
        realPath = realPath.substring(0, realPath.length() - 1);
        int aString = realPath.lastIndexOf(File.separator);
        realPath = realPath.substring(0, aString);
        return realPath;
    }

/**
     * @Description: 獲取文件保存的路徑,如果沒有該目錄,則創(chuàng)建
     * @param realPath 相對(duì)路徑 ,如   /home/software/apache-tomcat-8.0.45/webapps
     * @param fileType  文件類型 如: images/video/audio用于拼接文件保存路徑,區(qū)分音視頻
     * @return
     */
    public static String getSavePath(String realPath, String fileType) {
        SimpleDateFormat year = new SimpleDateFormat("yyyy");
        SimpleDateFormat m = new SimpleDateFormat("MM");
        SimpleDateFormat d = new SimpleDateFormat("dd");
        Date date = new Date();
        String sp=File.separator + "fileDate" + File.separator +fileType + File.separator + year.format(date) + File.separator
                + m.format(date) + File.separator + d.format(date) + File.separator;
        String savePath = realPath+ sp;
        File folder = new File(savePath);
        if (!folder.exists()) {
            folder.mkdirs();
        }
        return sp;
    }
5. 總結(jié)

本篇文章主要從實(shí)際項(xiàng)目出發(fā),介紹了文件上傳中所常見的一些情況,以及具體的實(shí)現(xiàn)。在斷點(diǎn)續(xù)傳中,需要注意的關(guān)鍵點(diǎn):

  • 瀏覽器端(前端)需要獲取文件的大小,去計(jì)算總分片數(shù),并且需要校驗(yàn)文件的合法性,在上傳過程中,需要及時(shí)的獲取到當(dāng)前傳輸文件的當(dāng)前分片數(shù),以更新下一次需要傳輸文件的范圍大小;
  • 服務(wù)器端,需要根據(jù)前端傳入的參數(shù),去確定分片所屬的源文件,并追加;(或者根據(jù)一定規(guī)則,創(chuàng)建臨時(shí)目錄,將屬于同一文件的分片,放在同一目錄,在將所有分片全部上傳完畢之后<即當(dāng)前分片數(shù)=總分片數(shù)>,在合并所有的分片);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,870評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,527評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,010評(píng)論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,250評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,656評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,853評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,103評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評(píng)論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,487評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,815評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容