断点续传

原因

想实现断点续传,特别是网络不畅的情况下适用。

使用方法

效果图 :
在这里插入图片描述
使用方法:

<div id="chunkupload"></div>
 
<script>
var options = {
    accept:['enc'],
    maxSize:10*1024*1024*1024,
    chunkSize:5*1024*1024,
    threadNum:10,
    finishCallback:function(responseEntity){
       chunkuploader.hideProgressBar();
       console.log(responseEntity);
    }
 }
      
      
 }
var chunkuploader= $("#chunkupload").chunkUploader(options,);

##例子
var uploadOptions = {
			accept:["enc"],
			name: "filename",
			checkType : "md5",
			threadNum:3,
			chunkSize: 5 * 1024 * 1024,
			maxSize:100*1024*1024*1024,
			finishCallback:function (responseEntity) {
				if (responseEntity.responseCode == 200) {
					$.ajax({
							   url: '${action_uri}' + '/uploadBackup/' + responseEntity.data + '?_deviceId=${_deviceId}',
							   timeout: 100000,
							   async: true,
							   contentType: 'application/x-www-form-urlencoded',
							   data: {},
							   success: function (response) {
								   var responseEntity = JSON.parse(response);
								   if (responseEntity.responseCode == 201) {
									   setTimeout("getUploadStatus('" + responseEntity.data + "')", 2000);
									   return;
								   }else{
									   chunkUploader.hideProgressBar();
									   $('#uploaderror').show();
									   $('#uploadsuccess').hide();
									   $('#uploadErrorinfo').html('${sg:msg("restroe_uploading_failed")}');
									   uploadBackupinfo = "";
								   }
							   },
							   error: function (data) {
								   // 去除遮罩
								   chunkUploader.hideProgressBar();
								   $('#uploaderror').show();
								   $('#uploadsuccess').hide();
								   $('#uploadErrorinfo').html('${sg:msg("restroe_uploading_failed")}');
								   uploadBackupinfo = "";
							   }
						   });
					return;
				} else {
					chunkUploader.hideProgressBar();
					$('#uploadsuccess').hide();
					$('#uploadErrorinfo').html(responseEntity.message);
					$('#uploaderror').show();
 				}
			}
		}
		  chunkUploader =$(".file-upload-div").chunkUploader(uploadOptions);
 	});
	function getUploadStatus(UDID) {
		$.ajax({
				   url: '${action_uri}' + '/getUploadStatus/' + UDID + '?_deviceId=${_deviceId}',
				   timeout: 100000,
				   async: true,
				   contentType: 'application/x-www-form-urlencoded',
				   data: {},
				   success: function (response) {
					   var responseEntity = JSON.parse(response);
					   if (responseEntity.responseCode == 201) {
						   setTimeout("getUploadStatus('" + UDID + "')", 2000);
						   return;
					   }
					   if (responseEntity.responseCode == 200) {
						   chunkUploader.setProgressBar("100%");
						   $('#uploadsuccess').show();
						   $('#uploaderror').hide();
						   uploadBackupinfo = responseEntity.data;
 					   }
					   else {
						   $('#uploadErrorinfo').html(responseEntity.message);
						   $('#uploaderror').show();
						   $('#uploadsuccess').hide();
						   uploadBackupinfo = "";
					   }
					   chunkUploader.hideProgressBar();
				   },
				   error: function (data) {
					   // 去除遮罩
					   $('#uploaderror').show();
					   $('#uploadsuccess').hide();
					   $('#uploadErrorinfo').html('${sg:msg("restroe_uploading_failed")}');
					   chunkUploader.hideProgressBar();
					   uploadBackupinfo = "";
				   }
			   });
	}
	;

</script>

在这里插入图片描述
!](https://i-blog.csdnimg.cn/direct/0e12e02c225c4874a4447cc658cbeb2f.png)
在这里插入图片描述

注意事项

  • 上传前,会检查文件大小和文件的格式是否正常,不正常会直接调用finishCallback
  • checkType 是md5时才能做到断点续传,如果fileSize模式只是分片上传
  • 上传的文件用完建议自动删除,否则会占用硬盘空间(如果使用公共的我这边会检查超过24小时的会自动清除)
  • 分片上传的位置有大小控制,超过100G 会自动删除,所以单次文件上传的大小不能超过100G
  • 该组件只是文件上传的组件,具体的业务建议在finishCallback实现
  • 默认位置/var///program-data/temp/chunkupload/(chunk或
    merge)/[md5或uuid]

参考链接

https://www.cnblogs.com/songsu/p/11798612.html
https://blog.csdn.net/thewindkee/article/details/80189434
https://blog.csdn.net/runner_diego/article/details/51379116

源码

JS

(function ($) {
    var _utils = {
        mergeUpload: function ($this) {
            var path = $this.$fileinput.val();
            var fileName = path.substr(path.lastIndexOf('\\') + 1, path.length);
            var formData = {};
            if ($this.options.checkType == "md5") {
                formData.md5 = $this.paramater.md5;
            } else {
                formData.uuid = $this.paramater.uuid;
                formData.fileSize = $this.paramater.fileSize;
            }
            formData.totalNum = $this.paramater.chunks;
            formData.fileName = fileName;
            $.ajax({
                       url: $this.options.mergeUrl,
                       type: "POST",
                       async: false,
                       data: formData,
                       success: function (response) {
                           console.log("merge start date:"+new Date().getTime());
                           console.log(response);
                           $this.setProgressBar("90%");
                           var responseEntity = JSON.parse(response);
                           if (responseEntity.responseCode == 200) {
                               $this.paramater.mergeThreadId = responseEntity.data;
                               _utils.mergeStatus($this);
                           }else{
                               _utils.finishUpload($this, responseEntity,"mergeUpload create thread is error");
                           }
                       },
                       error: function (data) {
                           console.log("merge fail");
                           _utils.finishUpload($this, undefined, $.i18n.map.customer_page_upload_image_fail);
                       }
                   });
        },
        mergeStatus: function ($this) {
            console.log("get mergeStatus")
            var formData = {};
            formData.mergeThreadId = $this.paramater.mergeThreadId;
            $.ajax({
                url: $this.options.mergeStatus,
                type: "POST",
                async: false,
                data: formData,
                success: function (response) {
                    console.log("get mergeStatus success date:"+new Date().getTime());
                    console.log(response);
                    var responseEntity = JSON.parse(response);
                    if (responseEntity.responseCode == 200) {
                         if(responseEntity.data == 0){
                             console.log("continue get mergeStatus");
                             setTimeout(_utils.mergeStatus, 10000, $this);
                         }else if(responseEntity.data == 1){
                             _utils.finishUpload($this, responseEntity);
                         }else{
                             responseEntity.responseCode == 500;
                             _utils.finishUpload($this, responseEntity,"merge fail ,more info server");
                         }
                    }else{
                        responseEntity.message="mergeUpload create thread is error";
                    }
                    },
                error: function (data) {
                    console.log("mergeStatus fail");
                    _utils.finishUpload($this, undefined, $.i18n.map.customer_page_upload_image_fail);
                }
            });
        },
        chunkUploadEnd: function ($this, chunkUploadResult) {
            if (!$this.paramater.uploading) {
                //none
            } else if ($this.paramater.uploading && !chunkUploadResult) { //不正常结束
                $this.paramater.uploading = false; //提示\
                _utils.finishUpload($this, undefined, $.i18n.map.customer_page_upload_image_fail);
            } else if ($this.paramater.uploading && chunkUploadResult && $this.paramater.successChunk.length
                                                                       == $this.paramater.chunks) {//正常结束
                $this.paramater.uploading = false; //提示
                _utils.mergeUpload($this);
            } else {
                $this.setProgressBar(
                    10 + Math.floor($this.paramater.successChunk.length * 75 / $this.paramater.chunks) + "%");
                //none
            }
        },
        chunkUpload: function ($this) {
            var innnerCurrentChunkNum = $this.options.currentChunkNum;
            if ($this.options.threadNum - $this.paramater.currentThreadNum > 0 && innnerCurrentChunkNum < $this.paramater.chunks) {
                $this.options.currentChunkNum = $this.options.currentChunkNum + 1;
                if ($.inArray(innnerCurrentChunkNum, $this.paramater.successHistoryChunk) > -1) {
                    $this.paramater.successChunk.push(innnerCurrentChunkNum);
                    console.log("history exit success:" + innnerCurrentChunkNum);
                    _utils.chunkUploadEnd($this, true);
                } else {
                    var file = $this.$fileinput.get(0).files[0];
                    var formData = new FormData();
                    if ($this.options.checkType == "md5") {
                        formData.append("md5", $this.paramater.md5);
                    } else {
                        formData.append("uuid", $this.options.uuid);
                    }
                    formData.append("chunkIndex", innnerCurrentChunkNum);
                    formData.append("chunkFile", _utils.chunkContent($this, file, innnerCurrentChunkNum));
                    $.ajax({
                               url: $this.options.chunkUrl,
                               timeout: $this.options.threadTimeout,
                               type: "POST",
                               async: true,
                               data: formData,
                               processData: false,
                               contentType: false,
                               success: function (response) {
                                   var responseEntity = JSON.parse(response);
                                       $this.paramater.currentThreadNum = $this.paramater.currentThreadNum - 1;
                                   if (responseEntity.responseCode == 200) {
                                       $this.paramater.successChunk.push(innnerCurrentChunkNum);
                                      // console.log("chunk success date:"+new Date().getTime()+" chunkNum:"+innnerCurrentChunkNum);
                                        _utils.chunkUploadEnd($this, true);
                                   } else {
                                        $this.paramater.failChunk.push(innnerCurrentChunkNum);
                                        console.log("chunkFile fail date:"+new Date().getTime() +" response:"+response);
                                        _utils.chunkUploadEnd($this, false);
                                   }
                               },
                               error: function (data) {
                                   $this.paramater.currentThreadNum = $this.paramater.currentThreadNum - 1;
                                   $this.paramater.failChunk.push(innnerCurrentChunkNum);
                                   console.log("fail:" + innnerCurrentChunkNum);
                                   _utils.chunkUploadEnd($this, false);
                               }
                           });
                    $this.paramater.currentThreadNum = $this.paramater.currentThreadNum + 1;
                }
                console.log("currentThreadNum:" + $this.paramater.currentThreadNum + " currentChunkNum:"
                            + $this.options.currentChunkNum);
            }
            if ($this.paramater.uploading && $this.options.currentChunkNum < $this.paramater.chunks) {
                //console.log("keepuploading");
                setTimeout(_utils.chunkUpload, 200, $this);
            }else{
                console.log("thread end currentChunkNum:"+$this.options.currentChunkNum +" uploading:"+$this.paramater.uploading);
            }
        }, uploadReady: function ($this) {
            $this.$chooseButton = $this.$element.find(".uploader-choose");
            $this.$uploadButton = $this.$element.find(".uploader-upload");
            $this.$chooseButton.addClass('disable');
            $this.$uploadButton.val($.i18n.map.crawl_hint_uploading);

            var file = $this.$fileinput.get(0).files[0];
            $this.paramater.fileSize = file.size;
            $this.paramater.chunks = Math.ceil($this.paramater.fileSize / $this.options.chunkSize);
            $this.paramater.currentThreadNum = 0;
            $this.paramater.failChunk = [];
            $this.paramater.successChunk = [];
            $this.paramater.successHistoryChunk = [];
            $this.paramater.blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
            $this.paramater.uploading = true;
            $this.paramater.uuid = _utils.randomUuid();
            $this.paramater.mergeThreadId="";
            $this.options.currentChunkNum = 0;
            if ($this.options.progressbar) {
                $this.showProgressBar();
            }
            if ($this.options.checkType == "md5") {
                $this.paramater.md5 = "";
                var spark = new SparkMD5.ArrayBuffer();
                var fileReader = new FileReader();
                var tempCurrentChunk = 0;
                fileReader.onload = function (e) {
                    //console.log('read chunk nr', tempCurrentChunk + 1, 'of', $this.paramater.chunks);
                    spark.append(e.target.result);                   // Append array buffer
                    tempCurrentChunk++;
                    $this.setProgressBar(Math.floor(tempCurrentChunk / $this.paramater.chunks * 9) +1+"%");
                    if (tempCurrentChunk < $this.paramater.chunks) {
                        loadNext();
                    } else {
                        console.log('finished loading');
                        $this.paramater.md5 = spark.end();
                        console.info('computed hash', $this.paramater.md5);  // Compute hash
                        _utils.uploadHistory($this, $this.paramater.md5);
                    }
                };
                fileReader.onerror = function () {
                    console.warn('oops, something went wrong.');
                    _utils.finishUpload($this, undefined, $.i18n.map.customer_page_upload_image_fail);
                };
                function loadNext() {
                    fileReader.readAsArrayBuffer(_utils.chunkContent($this, file, tempCurrentChunk));
                }
                $this.setProgressBar("1%");
                loadNext();
            } else {
                _utils.createThread($this);
            }
        },
        finishUpload: function ($this, responseEntity, errorMessage) {
            console.log("finish upload ")
            var temp;
            if (responseEntity) {
                temp = responseEntity;
                if (responseEntity.responseCode == 200) {
                    if ($this.options.checkType == "md5") {
                        responseEntity.data = $this.paramater.md5;
                    } else {
                        responseEntity.data = $this.paramater.uuid;
                    }
                }
            } else {
                temp = {"responseCode": 500, "message": errorMessage};
            }
            $this.$chooseButton.removeClass('disable');
            $this.$uploadButton.val( $this.options.uploadBtnText );
            if ($this.options.finishCallback) {
                $this.options.finishCallback(temp);
            } else {
             _utils.showInfo($this,temp);
            }
        },
        uploadHistory: function ($this, md5) { //失败不影响上传,只是会重新上传
            var fileExist = false;
            var responseVal;
            $.ajax({
                       url: $this.options.historyUrl,
                       type: "GET",
                       async: false,
                       data: {"md5": md5},
                       success: function (response) {
                           var responseEntity = JSON.parse(response);
                           if (responseEntity.responseCode == 200) {
                               if (responseEntity.data == -1) {
                                   console.log("uploadHistory success fileExist");
                                   $this.setProgressBar("95%");
                                   fileExist = true;
                                   responseVal = responseEntity;
                               } else {
                                   console.log("uploadHistory success");
                                   $this.paramater.successHistoryChunk = responseEntity.data;
                               }
                           } else {
                               console.log("uploadHistory result" + responseEntity.message);
                           }
                       },
                       error: function (data) {
                           console.log("uploadHistory fail");
                       }
                   });
            if (fileExist) {
                _utils.finishUpload($this, responseVal);
            } else {
                _utils.createThread($this);
            }
        },
        createThread: function ($this) {
            $this.setProgressBar("10%");
             console.log("chunks date:"+new Date().getTime()+" chunks:"+$this.paramater.chunks );
            for (var i = 0; i < $this.options.threadNum; i++) {
                setTimeout(_utils.chunkUpload, 100 * i, $this);
            }
        },
        chunkContent: function ($this, file, chunkNum) {
            var start = chunkNum * $this.options.chunkSize,
                end = ((start + $this.options.chunkSize) >= $this.paramater.fileSize) ? $this.paramater.fileSize : start + $this.options.chunkSize;
            return $this.paramater.blobSlice.call(file, start, end)
        }, randomUuid: function () {
            function _random() {
                return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
            }

            return (_random() + _random() + "-" + _random() + "-" + _random() + "-" + _random() + _random()
                    + _random());
        }, showInfo: function ($this,responseEntity) {
            console.error(" chunkUploader not implement finishCallback");
            $this.hideProgressBar();
            if (responseEntity.responseCode == 200) {
                utils.ui.message([Message.ResponseNewMessage(Message.ERRORTYPE.INFO, responseEntity.message)]);
            } else {
                utils.ui.message([Message.ResponseNewMessage(Message.ERRORTYPE.ERROR, responseEntity.message)]);
            }
        }

    };
    var _events = {
        onChange: function () {
            var file = this.$fileinput.get(0).files[0];
            var path = this.$fileinput.val();
            var suffix = path.substr(path.lastIndexOf('.') + 1, path.length);
            var fileName = path.substr(path.lastIndexOf('\\') + 1, path.length);
            var maxSize = this.options.maxSize;
            this.messArray = [];
            this.paramater.fileSize = file.size;
            if (!fileName) {
                this.messArray.push($.i18n.map.restore_file_path_type_upload_select);
            } else if(this.paramater.fileSize==0 ){
                this.messArray.push($.i18n.map.file_size_is_zero);
            }else if (this.options.accept && this.options.accept.length!=0&&$.inArray(suffix, this.options.accept) < 0) {
                this.messArray.push($.i18n.map.file_type_no_match);
            }
            if (file && maxSize) {
                if (/^[0-9]*[1-9][0-9]*$/.test(maxSize)) {//纯数字
                    if (this.paramater.fileSize > parseFloat(maxSize)) {
                        this.messArray.push($.i18n.map.base_ret_status_file_exceeds_max_limit);
                    }
                } else {//带单位
                    //单位转换
                    var numPart = parseFloat(maxSize.substr(0, maxSize.length - 2));
                    var strPart = maxSize.substr(maxSize.length - 2, maxSize.length);
                    switch (strPart) {
                        case 'KB':
                            numPart = numPart * 1024;
                            break;
                        case 'MB':
                            numPart = numPart * 1048576;
                            break;
                        case 'GB':
                            numPart = numPart * 1073741824;
                            break;
                        case 'TB':
                            numPart = numPart * 1099511627776;
                            break;
                        default:
                            break;
                    }
                    if (numPart && file.size > numPart) {
                        this.messArray.push($.i18n.map.base_ret_status_file_exceeds_max_limit);
                    }
                }
            }
            this.$filename.val(fileName);
            if (!this.options.showUploadBtn && this.options.autoUpload) {
                this.upload();
            }
        },
        onBtnClick: function () {
            if (!this.$fileinput.hasClass('disable')) {
                this.$fileinput.trigger('click')
            }
        },
        mouseover: function (e) {
            if (!this.$fileinput.hasClass('disable')) {
                if (this.$fileinput.val() || this.$filename.val()) {
                    this.$clear.show();
                } else {
                    this.$clear.hide();
                }
            }
        },
        mouseout: function () {
            if (!this.$fileinput.hasClass('disable')) {
                this.$clear.hide()
            }
        },
        clear: function () {
            this.$fileinput.val('');
            this.$filename.val('');
            this.messArray = [];
            this.messArray.push($.i18n.map.restore_file_path_type_upload_select);
        }
    };
    var html =
        '<label class="js-hover calendar-selection-l inline-block">' +
        '<div class="inline-block fileup-title"></div>' +
        '<input type="text" readonly="readonly">' +
        '<a href="javascript:;" class="i-tipsError js-clear"></a>' +
        '</label>' +
        '<a href="javascript:;" class="btn btn-gray uploader-choose margin-l-s">' + $.i18n.map.button_select
        + '</a>' +
        '<input type="file" name="" class="" accept="" style="display: none">';


    var progress =
        '<div class="mod-popup-mask upload-progress" style="display:none">'
        + ' <div class="mod-popup-progress">'
        + ' <p>' + $.i18n.map.crawl_hint_uploading + '</p>'
        + ' <div class="mod-progress-bar">'
        + ' <div class="progress-bar" style="width: 0%;"></div> <span class="progress-number">0%</span>'
        + ' </div>'
        + ' </div>'
        + ' </div>';


    var defaultSetting = {
        accept:[],
        maxSize: 1 * 1024 * 1024 * 1024,
        chunkSize: 2 * 1024 * 1024,
        threadNum: 5,
        threadTimeout: 60000,
        inputFileName: "uploadFile",
        placeholder: $.i18n.map.source_dest_common_choose_file_blank,
        showUploadBtn: true,
        uploadBtnText: $.i18n.map.common_upload,
        autoUpload: true,
        historyUrl: root_path + "/util/upload/history",
        chunkUrl: root_path + "/util/upload/chunkUpload",
        mergeUrl: root_path + "/util/upload/mergeFile",
        mergeStatus: root_path + "/util/upload/mergeStatus",
        progressbar: true,//进度条,校验10%、分片上传75%、合成10%、最后的5%回掉关闭
        checkType: "md5", //fileSize
        finishCallback: undefined
    };

    var paramater = {
        blobSlice:undefined,
        //公共参数
        uploading: false,
        currentThreadNum: 0,
        chunks: 0, //总块数
        md5: "",
        fileSize: "",
        uuid: "",
        md5Array: [], //废弃
        currentChunkNum: 0,
        failChunk: [], //后期扩展
        successChunk: [], //用来判断上传成功结束,多线程不能根据最后一块成功判断成功
        successHistoryChunk: [] //用来判断上传成功结束,多线程不能根据最后一块成功判断成功
    }

    var chunkUploader = function ($element, options) {
        if ($element.children().length < 1) {
            $element.addClass('inline-block');
            $element.append(html);
            if($(document).find(".main-wrap").length!=0){
                this.$progressElement = $(document).find(".main-wrap");
             }else{
                this.$progressElement = $( "body" );
            }
            this.$progressElement.append(progress);
            if (options.showUploadBtn) {
                $element.append('<input type="button"  class="btn btn-blue margin-l-s uploader-upload" value="'
                                + options.uploadBtnText + '">');
            }
        }
        this.options = options;
        this.paramater = paramater;
        this.$element = $element;

        this.$fileinput = $element.find("input[type='file']");
        this.$filename = $element.find("input[type='text']");
        this.$hoverarea = $element.find(".js-hover");
        this.$clear = $element.find(".js-clear");
        this.$chooseButton = $element.find(".uploader-choose");
        this.$uploadButton = $element.find(".uploader-upload");

        if (options.accept) {
            this.$fileinput.prop('accept', options.accept.map(function (item) {
                return '.' + item
            }).join(','));
        }
        this.$fileinput.prop('name', options.inputFileName);
        this.$fileinput.prop('id', options.inputFileName);
        this.$filename.prop('placeholder', options.placeholder);
        this.messArray = [];

        //绑定事件
        this.$fileinput.off("change.sux.chunkUploader.change")
            .on("change.sux.chunkUploader.change", $.proxy(_events.onChange, this));
        this.$chooseButton.off("click.sux.chunkUploader.click")
            .on("click.sux.chunkUploader.click", $.proxy(_events.onBtnClick, this));
        this.$hoverarea.off("mouseenter.sux.chunkUploader.mouseenter")
            .on("mouseenter.sux.chunkUploader.mouseenter", $.proxy(_events.mouseover, this));
        this.$hoverarea.off("mouseleave.sux.chunkUploader.mouseleave")
            .on("mouseleave.sux.chunkUploader.mouseleave", $.proxy(_events.mouseout, this));
        this.$clear.off("click.sux.chunkUploader.click")
            .on("click.sux.chunkUploader.click", $.proxy(_events.clear, this));
        this.$uploadButton.off("click.sux.chunkUploader.click")
            .on("click.sux.chunkUploader.click", $.proxy(this.upload, this));

        this.messArray.push($.i18n.map.restore_file_path_type_upload_select);
    };
    chunkUploader.prototype.getFile = function () {
        return this.$fileinput.get(0).files[0];
    };

    chunkUploader.prototype.showProgressBar = function () {
         this.$progressElement.find(".upload-progress").show();
        this.$progressElement.find(".progress-bar").width("0%");
        this.$progressElement.find(".progress-number").html("0%");
    };

    chunkUploader.prototype.hideProgressBar = function () {
        this.$progressElement.find(".upload-progress").hide()
    };

    chunkUploader.prototype.setProgressBar = function (percent) {
        this.$progressElement.find(".progress-bar").width(percent);
        this.$progressElement.find(".progress-number").html(percent);
    };

    chunkUploader.prototype.getFileName = function () {
        return this.$filename.val();
    };
    chunkUploader.prototype.upload = function () {
        //progressbar
        if (this.messArray.length > 0) {
            _utils.finishUpload(this, undefined, this.messArray[0]);
            return;
        }
        _utils.uploadReady(this);
    };

    $.fn.chunkUploader = function (options, finishCallback) {  //need finishCallback
        if (!options) {
            options = {};
        }
        if (!$(this).data('sux.chunkUploader')) {
/*            if (finishCallback) {
                options.finishCallback = finishCallback;
            }*/
            var instance = new chunkUploader($(this), $.extend({}, defaultSetting, options));
            $(this).data('sux.chunkUploader', instance);
            return instance;
        } else {
            return $(this).data('sux.chunkUploader');
        }
    }
}(jQuery));

java

@Controller
@RequestMapping("/util/upload")
public class ChunkUploadController extends BaseController {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    private Long MAX_SPACE = Integer.valueOf(BaseConfig.getItem("chunk_upload_place_max", "100")) * 1024 * 1024 * 1024L;
    private FileAlterationMonitor fileAlterationMonitor = null;

    private ConcurrentHashMap mergeHashMap = new ConcurrentHashMap<>(); //0  正在合并 -1合并失败 1 合并成功

    //根据MD5找到之前的上传记录
    @RequestMapping(value = "/history", method = RequestMethod.GET)
    @ResponseBody
    public ResponseEntity history(HttpServletRequest request, HttpServletResponse response) {
        logger.debug("history access");
        try {
            initialize();
            String md5 = request.getParameter("md5");
            String chunkPath = ChunkFileUtil.getChunkFilePath(md5);
            String mergeFolderPath = ChunkFileUtil.getMergeFolderPath(md5);
            File folder = new File(mergeFolderPath);
            if (folder.exists() && folder.listFiles() != null && folder.listFiles().length == 0) {
                folder.delete();
                logger.info("history  exists but is empty");
            }
            if (folder.exists()) {
                logger.info("history  exists not upload");
                return new ResponseEntity(ResponseStatus.SUCCESS, -1);
            } else {
                List<Integer> history = ChunkFileUtil.getChunkUploadHistory(chunkPath);
                return new ResponseEntity(ResponseStatus.SUCCESS, history);
            }
        } catch (Exception e) {
            logger.error("history  error  e:", e);
            return new ResponseEntity<>(ResponseStatus.INTERNAL_ERROR);
        }

    }


    @RequestMapping(value = "/chunkUpload", method = RequestMethod.POST)
    @ResponseBody
    public ResponseEntity<Map<String, Object>> chunkUpload(HttpServletRequest request, HttpServletResponse response) {
        logger.debug("chunkUpload access");
        boolean result;
        try {
            MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
            String md5 = request.getParameter("md5"); //filesize 模式没有uuid
            String uuid = request.getParameter("uuid");
            int chunkIndex = Integer.valueOf(request.getParameter("chunkIndex"));
            MultipartFile configFile = multipartRequest.getFile("chunkFile");
            String chunkPath = StringUtils.isEmpty(md5) ? ChunkFileUtil.getChunkFilePath(uuid) : ChunkFileUtil.getChunkFilePath(md5);
            InputStream inputStream = configFile.getInputStream();
            result = ChunkFileUtil.chunkUpload(chunkPath, chunkIndex, inputStream);
        } catch (Exception e) {
            result = false;
            logger.error("chunkUpload  error  e:", e);
        }
        return result ? new ResponseEntity<>(ResponseStatus.SUCCESS) : new ResponseEntity<>(ResponseStatus.INTERNAL_ERROR);

    }

    @RequestMapping(value = "/mergeFile", method = RequestMethod.POST)
    @ResponseBody
    public ResponseEntity mergeFile(HttpServletRequest request, HttpServletResponse response) {
        logger.info("mergeFile access");
        String threadId = DateFormatUtils.format(new Date(), "yyyyMMdd'T'HHmmssSSS");
          //MD5
        String md5 = request.getParameter("md5");
        //fileSize
        String uuid = request.getParameter("uuid");
        String fileSizeStr = request.getParameter("fileSize");
        try {
            int totalNum = Integer.valueOf(request.getParameter("totalNum"));
            String fileName = request.getParameter("fileName");
            String chunkFolderPath = StringUtils.isEmpty(md5) ? ChunkFileUtil.getChunkFilePath(uuid) : ChunkFileUtil.getChunkFilePath(md5);
            String mergeFolderPath = StringUtils.isEmpty(md5) ? ChunkFileUtil.getMergeFolderPath(uuid) : ChunkFileUtil.getMergeFolderPath(md5);
            mergeHashMap.put(threadId,0);
            Thread thread = new Thread(() -> {
                boolean result;
                if (StringUtils.isEmpty(md5)) {
                    result = ChunkFileUtil.mergeChunk(Long.valueOf(fileSizeStr), totalNum, fileName, chunkFolderPath, mergeFolderPath);
                } else {
                    result = ChunkFileUtil.mergeChunk(md5, totalNum, fileName, chunkFolderPath, mergeFolderPath);
                }
                logger.info("mergeFiles threadId : {} result:{}", threadId,result);
                mergeHashMap.put(threadId,result?1:-1);
            });
            thread.start();

        } catch (Exception e) {
             logger.error("mergeFiles  error  e:", e);
             return  new ResponseEntity(ResponseStatus.INTERNAL_ERROR);
        }
        logger.info("mergeFiles   threadId:{} ", threadId);
         return new ResponseEntity(ResponseStatus.SUCCESS,threadId);
    }


    @RequestMapping(value = "/mergeStatus", method = RequestMethod.POST)
    @ResponseBody
    public ResponseEntity mergeStatus(HttpServletRequest request, HttpServletResponse response) {
        logger.info("mergeStatus access");
        String threadId = request.getParameter("mergeThreadId");
        logger.info("mergeStatus threadId:{}",threadId);
        return new ResponseEntity(ResponseStatus.SUCCESS,mergeHashMap.containsKey(threadId)?mergeHashMap.get(threadId):-1);
    }



    //用到之后增加定时检查,定时扫描任务防止上传空间过大,只监控文件夹新增
    private void initialize() throws Exception {
        logger.debug("task initialize");
        if (fileAlterationMonitor != null) {
            return;
        }
        FileAlterationObserver fileAlterationObserver = new FileAlterationObserver(ChunkFileUtil.getUploadPath(),
                FileFilterUtils.and(new IOFileFilter() {
                    @Override
                    public boolean accept(File file) {
                        return file.isDirectory();
                    }

                    @Override
                    public boolean accept(File dir, String name) {
                        System.out.println("accept:" + dir.getName() + " name:" + name);
                        return false;
                    }
                }));

        fileAlterationObserver.addListener(new FileAlterationListener() {
            @Override
            public void onStart(FileAlterationObserver observer) {
                //none
            }

            @Override
            public void onDirectoryCreate(File directory) {
                executeDelete();
            }

            @Override
            public void onDirectoryChange(File directory) {
                //none
            }

            @Override
            public void onDirectoryDelete(File directory) {
                //none
            }

            @Override
            public void onFileCreate(File file) {
                //none
            }

            @Override
            public void onFileChange(File file) {
                //none
            }

            @Override
            public void onFileDelete(File file) {
                //none
            }

            @Override
            public void onStop(FileAlterationObserver observer) {
                //none
            }
        });
        fileAlterationMonitor = new FileAlterationMonitor(TimeUnit.SECONDS.toMillis(60), fileAlterationObserver);  //TimeUnit.MINUTES.toMillis(1)
        fileAlterationMonitor.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("chunk upload monitor")
                .setPriority(Thread.MAX_PRIORITY).build());
        fileAlterationMonitor.start();
    }

    @PreDestroy
    private void terminate() throws Exception {
        if (fileAlterationMonitor != null) {
            fileAlterationMonitor.stop();
        }
    }

    private synchronized void executeDelete() {
        //检查文件夹的大小大于200G
        File chunkUploadDir = new File(ChunkFileUtil.getUploadPath());
        if (chunkUploadDir.exists() && chunkUploadDir.isDirectory()) {
            if (MAX_SPACE < FileUtil.sizeOfDirectory(chunkUploadDir)) {
                File chunkDir = new File(ChunkFileUtil.getChunkRootPath());
                File mergeDir = new File(ChunkFileUtil.getMergeRootPath());
                File[] chunkFileArray = chunkDir.listFiles();
                File[] mergeFileArray = mergeDir.listFiles();
                List<File> list = new ArrayList<>();
                if (chunkFileArray != null) {
                    list.addAll(Arrays.asList(chunkFileArray));
                }
                if (mergeFileArray != null) {
                    list.addAll(Arrays.asList(mergeFileArray));
                }
                list.sort((f1, f2) -> f1.lastModified() > f2.lastModified() ? 1 : -1);
                do {
                    if (list.size() > 0) {
                        File temp = list.get(0);
                        list.remove(0);
                        try {
                            FileUtil.deleteFolder(temp.getAbsoluteFile().getPath());
                        } catch (Exception e) {
                            logger.error("executeDelete error e:", e);
                        }
                    }
                } while (MAX_SPACE < FileUtil.sizeOfDirectory(chunkUploadDir) && list.size() > 0);
            }
        }
    }
}
public class ChunkFileUtil extends FileUtils {

    private static final Logger logger = LoggerFactory.getLogger(ChunkFileUtil.class);

    private static String chunk_upload_path;

    static {
        try {
            chunk_upload_path = BaseConfig.getItem("chunk_upload_path", "tets");
        } catch (Exception e) {
            logger.error("get BaseConfig chunk_upload_path error", e);
        }
    }


    public static boolean chunkUpload(String chunkFolderPath, int chunkIndex, InputStream inputStream) {

        boolean flag = true;//标示是否成功
        String messageInfo = "success";
        File thunkFile;
        String chunkPath = chunkFolderPath;
        String chunkFilePath = chunkPath + chunkIndex;
        thunkFile = new File(chunkFilePath);
        try {
            FileUtil.createNotExistPath(chunkPath);
            if (thunkFile.exists()) {
                thunkFile.delete();
                thunkFile = new File(chunkFilePath); //TODO test delete do not create
                logger.info("chunkUpload old exit but md5 not match will delete old file"); //TODO
            }
            try (FileOutputStream fos = new FileOutputStream(thunkFile)) {
                byte buffer[] = new byte[1024];
                int length = 0;
                while ((length = inputStream.read(buffer)) != -1) {
                    fos.write(buffer, 0, length);
                }
            } catch (Exception e) {
                thunkFile.delete();
                messageInfo = "storing local exceptions ";
                logger.error(messageInfo, e);
                flag = false;
            }
            logger.info("chunkUpload  end " + messageInfo); //TODO
        } catch (Exception e) {
            logger.error("chunkUpload  error  e:", e);
            flag = false;
        } finally {
            if (!flag) {
                thunkFile.delete();
            }
        }
        return flag;
    }

    public static List<Integer> getChunkUploadHistory(String chunkFolderPath) {
        File tempFileFolder = ChunkFileUtil.getFile(chunkFolderPath);
        List<Integer> history = new ArrayList<>();
        if (tempFileFolder.exists()) {
            File[] files = tempFileFolder.listFiles();
            if (files != null) {
                for (File f : files) {
                    history.add(Integer.valueOf(f.getName()));
                }
            }
        }
        return history;
    }

    public static boolean mergeChunk(String md5, int totalNum, String fileName, String chunkFolderPath, String mergeFolderPath) {
        boolean flag = mergeChunk(totalNum, fileName, chunkFolderPath, mergeFolderPath, () -> {
            String tempMD5 = MD5Util.md5sumForFile(mergeFolderPath + fileName);
            logger.info("mergeFiles check file md5  {} merge after {}", md5, tempMD5); //TODO
            return tempMD5.equals(md5);
        });
        return flag;

    }


    public static boolean mergeChunk(Long fileSize, int totalNum, String fileName, String chunkFolderPath, String mergeFolderPath) {
        boolean flag = mergeChunk(totalNum, fileName, chunkFolderPath, mergeFolderPath, () -> {
            File targetFile = new File(mergeFolderPath + fileName);
            logger.info("mergeFiles check file size   {} merge after {}", fileSize, targetFile.length()); //TODO
            return targetFile.length() == fileSize;
        });

        if (!flag) { //uuid这种删除失败之后清空分片,分片保留无意义
            try {
                FileUtil.deleteFolder(chunkFolderPath);
            } catch (Exception e) {
                logger.error("mergeChunk  fail  delete chunk Folder e:", e);
            }
        }
        return flag;
    }

    /**
     * 合并分片文件
     */
    public static boolean mergeChunk(int totalNum, String fileName, String chunkFolderPath, String mergeFolderPath, Supplier<Boolean> checkFile) {
        String messageInfo = "success";
        boolean flag = true;
        File chunkFolder = FileUtil.getFile(chunkFolderPath);
        try {
            File[] files = chunkFolder.listFiles();
            if (files != null && files.length >= totalNum) {
                File[] childrens = new File[totalNum];
                //检查名称
                for (int i = 0; i < totalNum; i++) {
                    childrens[i] = new File(chunkFolderPath + i);
                    if (!childrens[i].exists() || !childrens[i].isFile()) {
                        flag = false;
                        messageInfo = (chunkFolderPath + i) + " no exist or not file";
                        logger.info(messageInfo);
                        break;
                    }
                }
                if (flag) {
                    try {
                        FileUtil.deleteFolder(mergeFolderPath);
                        FileUtil.createNotExistPath(mergeFolderPath);
                        File targetFile = new File(mergeFolderPath + fileName);
                        int bufSize = 1024;
                        try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(targetFile))) {
                            byte[] buffer = new byte[bufSize];
                            for (int i = 0; i < childrens.length; i++) {
                                try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(childrens[i]))) {
                                    int readCount;
                                    while ((readCount = inputStream.read(buffer)) > 0) {
                                        outputStream.write(buffer, 0, readCount);
                                    }
                                }
                            }
                        }
                        flag = true;
                    } catch (Exception e) {
                        messageInfo = "mergeFiles merge error,mergeFiles " + mergeFolderPath;
                        logger.error(messageInfo, e);
                        flag = false;
                    }
                } else {
                    //none
                }
            } else {
                flag = false;
                messageInfo = "mergeFiles totalNum  error,totalNum: " + totalNum + " chunkFileNum:" + files.length;
                logger.info(messageInfo);
            }
            if (flag && checkFile != null) {
                flag = checkFile.get();
                messageInfo = "mergeFiles check result:" + flag;
                logger.info(messageInfo);
            }
        } catch (Exception e) {
            messageInfo = "mergeFiles merge error e:" + e.getMessage();
            logger.error(messageInfo, e);
        } finally {
            if (flag) {
                try {
                    FileUtil.deleteFolder(chunkFolderPath);
                } catch (Exception e) {
                    logger.error("mergeChunk  success  delete chunk Folder e:", e);
                }
            }
            if (!flag) {
                try {
                    FileUtil.deleteFolder(mergeFolderPath);
                } catch (Exception e) {
                    logger.error("mergeChunk  fail  delete mergeFolder e:", e);
                }
            }
        }
        logger.info("mergeFiles end {} {}", mergeFolderPath, messageInfo); //TODO
        return flag;
    }



    public static String getUploadPath() {
        return chunk_upload_path;
    }

    public static String getChunkRootPath() {
        return chunk_upload_path + "chunk/";
    }

    public static String getMergeRootPath() {
        return chunk_upload_path + "merge/";
    }

    public static String getChunkFilePath(String key) {
        return getChunkRootPath() + key + "/";
    }

    public static String getMergeFolderPath(String key) {
        return getMergeRootPath() + key + "/";
    }
    /**
     * 根据key 获取上传文件的地址
     *
     * @param key(uuid or md5)
     */
    public static String getUploadFilePath(String key) {
        String mergeFilePath = getMergeFolderPath(key);
        File folder = new File(mergeFilePath);
        if (folder.exists() && folder.listFiles() != null && folder.listFiles().length > 0) {
            return mergeFilePath + folder.listFiles()[0].getName();
        }
        return null;
    }
}


    public static String md5sumForFile(String inputpath) {
        Preconditions.checkNotNull(inputpath, "input path is null!");
        String result = null;
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(Paths.get(inputpath).toString());

            result = DigestUtils.md5Hex(fileInputStream);
        } catch (FileNotFoundException e) {
            logger.error("file not found", e);
        } catch (IOException e) {
            logger.error("io exception", e);
        } finally {
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    logger.error("close file input stream error", e);
                }
            }
        }
        return result;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值