fex-team / webuploader

It's a new file uploader solution!
http://fex.baidu.com/webuploader/
BSD 3-Clause "New" or "Revised" License
7.71k stars 2.34k forks source link

如何实现秒传与断点续传 #142

Open 2betop opened 10 years ago

2betop commented 10 years ago

因为这是小众需求,所以默认没有做在webuploader里面,而只是提供hook接口,让用户很简单的扩展此功能。

那么,都有哪些重要的hook接口呢?

对于秒传来说,其实就是文件上传前,把内容读取出来,算出md5值,然后通过ajax与服务端验证进行验证, 然后根据结果选择继续上传还是掉过上传。

像这个操作里面有两个都是异步操作,文件内容blob读取和ajax请求。所以这个handler必须是异步的,怎样告诉组件此handler是异步的呢?只需要在hanlder里面返回一个promise对象就可以了,这样webuploader就会等待此过程,监听此promise的完成事件,自动继续。

以下是此思路的简单实现。

Uploader.register({
    'before-send-file': 'preupload'
}, {
    preupload: function( file ) {
        var me = this,
            owner = this.owner,
            server = me.options.server,
            deferred = WebUploader.Deferred();

        owner.md5File( file.source )

            // 如果读取出错了,则通过reject告诉webuploader文件上传出错。
            .fail(function() {
                deferred.reject();
            })

            // md5值计算完成
            .then(function( md5 ) {

                // 与服务安验证
                $.ajax(server, {
                    dataType: 'json',
                    data: {
                        md5: ret
                    },
                    success: function( response ) {

                        // 如果验证已经上传过
                        if ( response.exist ) {
                            owner.skipFile( file );

                            console.log('文件重复,已跳过');
                        }

                        // 介绍此promise, webuploader接着往下走。
                        deferred.resolve();
                    }
                });
            });

        return deferred.promise();
    }
});

关于断点续传

其实就是秒传分片,跟秒传整个文件是一个思路。关于md5验证这块,可以ajax请求验证,也可以在文件秒传验证的时候,把已经成功的分片md5列表拿到,这样分片验证的时候就只需要本地验证就行了,减少请求数。

具体实现和思路请查看这里https://github.com/fex-team/webuploader/issues/139

2betop commented 10 years ago

代码中 md5File 的调用说明,请查看这: http://fex.baidu.com/webuploader/doc/index.html#WebUploader_Uploader_md5File

litterGuy commented 10 years ago

md5Blob 在ie下无法使用 有没有什么好的解决办法?

2betop commented 10 years ago

恩,ie下确实不能用,只能扩展flash在那个里面实现。

litterGuy commented 10 years ago

filereader 已经实现在swf内了 想问下 有没有可以直接在外面用js调用读取文件的方法(不会改flash啊)

2betop commented 10 years ago

fileReader确实可以把文件内容读取出来,但是把内容读取出来再交给JS来md5,效率肯定会很慢的。最好还是flash直接把md5值算好了把结果交给js。

以后如果这块需求比较我可以考虑加上此功能。

ps:文库那边发现可以编译c/c++版本的md5代码给flash调用,md5速度比html5的速度快出很多。但是文件读取速度html5又比flash快,所以对于一个文件读取+md5运算总体时间花费基本上差不多。

anota commented 10 years ago

md5Blob 找不到Deferred方法 是依赖了什么库么

2betop commented 10 years ago

我直接使用的jQuery的Deferred,你没引入吗?

anota commented 10 years ago

好吧 哪应该写错了吧 好像是$.deferred()吧。

2betop commented 10 years ago

对,已更新https://gist.github.com/2betop/10399507

cloudsiu commented 10 years ago

请问在IE如何获取到文件的md5码? md5Blob 在IE下报错! 找不到FileReader 对象

2betop commented 10 years ago

IE里面只能用flash实现,可以自己扩展flash实现md5功能,目前没有此功能。

cloudsiu commented 10 years ago

也就是说IE环境下没法实现断点续传功能? 还是说有其他方法 因为本人不懂Flash! 还有刚才在使用Hook before-file是 返回的file对象访问source时返回的是undefind 无法给分块产生MD5

2betop commented 10 years ago

对,目前 IE 下无法实现断点续传,除非把 flash 版本的 md5 计算模块扩展到 webuploader 中去。

cloudsiu commented 10 years ago

那请问往后的版本会考虑拓展这个功能吗? 被IE无法支持断点续传功能困扰很久了!麻烦大神能指点迷津!

yuanhaibo commented 10 years ago

md5 用这个 https://github.com/satazor/SparkMD5

cloudsiu commented 10 years ago

请问我在使用上述的方法进行MD5码的获取时,浏览器依旧被阻塞住了! 请问有何办法可以解决此问题!

2betop commented 10 years ago

自 0.1.4 后 已添加 md5 模块,包括 flash 和 html5 两个实现。

PerterPon commented 10 years ago

看这个issue的意思,断点续传也是分多个请求实现的么?

2betop commented 10 years ago

恩,基于 chunk 上传的。

PerterPon commented 10 years ago

好吧,目前有办法能在一个请求里面实现么?仅针对chrome而且,不考虑兼容新的话

2betop commented 10 years ago

不 chunk 的话,其实后端很难知道已经接收了多少的。像 php 都是等你的上传文件全部存入临时文件后才执行到脚本里面来。你每次接受文件,要不就是进度为0,要不进度为 100%。

2betop commented 10 years ago

还是我理解错你的意思了,你是不想每次 chunk 上传文件的时候都做 ajax 验证?还是压根就不想 chunk? 如果只是担心 不想 chunk的时候多发一次 ajax 验证请求的话,其实第一次就可以把当前文件所有已成功的分块 sign 取到,分块验证的时候本地验证就行了。

PerterPon commented 10 years ago

后端开个文件流,前端边上传边写到文件里边,然后下次重新开始的时候,先读下这个文件,然后再选择开始上传的文件offset应该就行了吧?

PerterPon commented 10 years ago

还有个问题想请教下,发现XHR.upload的progress事件,只有在send formData对象的时候才会正常触发,如果send一个大字符串的话,貌似就最后上传完成的时候会触发一次,不知道这是什么问题呢。

yanggen commented 10 years ago

为了实现秒传,采用您在这里贴的实现代码,代码放在了webuploader.js里面,位置是在 负责文件上传 (define('widgets/upload',[…… ) 的代码片段的最后,但是运行时提示md5Blob未定义,请问采用什么方式可以调用'widgets/md5'里面的md5Blob方法? 这段秒传的实现代码应该放在什么地方

2betop commented 10 years ago

upload.md5File

2betop commented 10 years ago

这个帖子是之前 webuploader 没有提供 md5File 时写的,现在 webuploader 有 md5File 方法了,改成采用这个方法即可,其他地方不变。

yanggen commented 10 years ago

大神,我改成了md5File,依然报md5File未定义的错误。我对js的闭包不是太熟,在widgets/upload的内部,貌似是没有办法使用md5File,所以报错了。 请问这种扩展代码是不是不应该在webuploader.js里面修改啊,如果在前端页面里写这种扩展代码得话,Uploader.register 方法和 WebUploader 对象又没有定义。不知道有没有讲清楚,我是JS新手,见谅哈

qq20140717175756

image

2betop commented 10 years ago

已经挂到 uploader 的类上,这里你改成 owner.md5File 就行了,owner 指向 uploader 实例。

确保是用的 1.0.4+ 版本啊,之前的版本都没加这个功能。

yanggen commented 10 years ago

感谢,那个未定义的错误没有了,开始上传后,出现了blob.ruid未定义的错误,(webuploader.js除了扩展了上面的代码,未做其他修改),请问这个问题是什么原因

报错信息 “Cannot read property 'ruid' of undefined ”

2betop commented 10 years ago

blob = file.source.getSource() 改成 blob = file.source; 其他问题自己慢慢看。

febear commented 10 years ago

能请问您一下,webuploader能多实例化吗?如何做呢?

lichunxue80 commented 10 years ago

我是用java写的,后台断点之后报org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. Stream ended unexpectedly,为啥

ghmin commented 10 years ago

Uploader.register 您好,这个方法我在0.1.5版本上面怎么都没办法运行,用WebUploader.Uploader.register才有效,但是,hook了的事件譬如 bofore-sned-file 他不会运行。。这是何解呢?官网上面似乎有没有这个解释。

sky20054122 commented 10 years ago
// 扩展md5逻辑
    var uploader = WebUploader.Uploader;

    /**
     * method:before-send-file
     * 在文件发送之前request,此时还没有分片(如果配置了分片的话),可以用来做文件整体md5验证。
     * para:file: File对象
     */
    uploader.register({
        'before-send-file' : 'preupload'
    }, {
        preupload : function(file) {
            $('#uploadBtn').attr("disabled",true);
            var me = this, 
            owner = this.owner, 
            server = me.options.server, 
            deferred = WebUploader.Deferred(), 
            blob = file.source.getSource();

            //var fileMd5 = file.wholeMd5;
            var start =  +new Date();
            var $li = $('#' + file.id), 
            $MD5Percent = $li.find('.progress  .progress-bar-info');
            $li.find('span.state').text('计算MD5');
            insertLog("<br>"+moment().format("YYYY-MM-DD HH:mm:ss")+" before-send-file  preupload:开始计算文件("+file.name+")MD5. ");
            owner.md5File( file )           
            .progress(function(percentage) {   // 及时显示进度             
                var percent = parseInt(percentage * 100 ) + '%';
                $MD5Percent.css('width', percent);
                $MD5Percent.html(percent);
                //console.log('Percentage:', percent);
            })               
            .then(function(fileMd5) {   // 完成              
                var end = +new Date();
               // console.log("before-send-file  preupload: file.size="+file.size+" file.md5="+fileMd5);
                insertLog("<br>"+moment().format("YYYY-MM-DD HH:mm:ss")+" before-send-file  preupload:计算文件("+file.name+")MD5完成. 耗时  " + (end - start) + '毫秒  fileMd5: ' + fileMd5);
                file.wholeMd5 = fileMd5;

                $.ajax({
                    cache : false,                  
                    type : "post",
                    dataType : "json",
                    url : baseUrl + "/fileUpload/existsMd5",
                    data : {
                        fileMd5 : fileMd5,
                        fileName : file.name,
                        isShared : $("#isShared").val()
                    },
                    success : function(result) {

                            $('#uploadBtn').attr("disabled",false);
                            me.options.formData.fileMd5 = fileMd5;
                            me.options.formData.isShared = $("#isShared").val();
                            me.options.formData.fileType = $("#fileType").val();
                            if (result.result) {//文件存在
                                insertLog("<br>"+moment().format("YYYY-MM-DD HH:mm:ss")+" before-send-file  preupload:文件 "+file.name + " 已经存在,跳过上传   fileMd5:"+fileMd5);
                                $MD5Percent.hide();
                                var $li = $('#' + file.id), 
                                $percent = $li.find('.progress  .progress-bar-success');
                                $li.find('span.state').text('文件重复,已跳过');
                                $percent.css('width', 100 + '%');
                                owner.skipFile(file);
                            }else{//文件不存在
                                file.wholeMd5 = fileMd5;
                                file.chunkMd5s = result.chunkMd5s;  //如果后台已经有该文件的分片记录
                            }

                            // me.data.chunksMd5 = "chunksMd5";
                            $('#' + file.id+' .cancleBtn').removeClass("btn-info");
                            $('#' + file.id+' .cancleBtn').attr("disabled",true);
                            deferred.resolve(true);
                            // return deferred.reject();
                    }
                });
            });

            return deferred.promise();
        }
    });
beyondonly commented 10 years ago

@2betop 请问一下,现在的版本是否支持ie7+的断点续传

beyondonly commented 10 years ago

@2betop 0.1.4内置 md5 文件读取功能,支持 html5 + flash 两个运行时,ie7+中是否支持

beyondonly commented 10 years ago

@2betop 请问一下,断点续传的时候跳过分片怎么设置

wwg88888888 commented 9 years ago

如何实现断点续传呢?怎么设置需要上传的分片?

sky20054122 commented 9 years ago

@wwg88888888 查看 #139 #142

wwg88888888 commented 9 years ago

@sky20054122 谢谢

wwg88888888 commented 9 years ago

@sky20054122 owner.md5File 这个能分片验证码?我在 https://github.com/fex-team/webuploader/issues/139 上只看到了整体验证md5

sky20054122 commented 9 years ago

@wwg88888888 可以做分片验证 文档里面有

http://fex.baidu.com/webuploader/doc/index.html#WebUploader_File

md5File
md5File( file[, start[, end]] ) ⇒ promise
计算文件 md5 值,返回一个 promise 对象,可以监听 progress 进度。

uploader.on( 'fileQueued', function( file ) {
    var $li = ...;

    uploader.md5File( file )

        // 及时显示进度
        .progress(function(percentage) {
            console.log('Percentage:', percentage);
        })

        // 完成
        .then(function(val) {
            console.log('md5 result:', val);
        });

});

我的例子 (md5File参数传入分片文件或者传入file文件和起止字节数就是计算分片的MD5)
/**
     * method:before-send
     * 在分片发送之前request,可以用来做分片验证,如果此分片已经上传成功了,可返回一个rejected promise来跳过此分片上传
     * para:block: 分片对象
     */
    uploader.register({  
        'before-send' : 'checkchunk'
    }, {
        checkchunk : function(block) {
            var me = this; 
            var owner = this.owner;
            var deferred = $.Deferred();
            var chunkFile = block.blob;
            var file = block.file;
            var chunk = block.chunk;
            var chunks = block.chunks;
            var start = block.start;
            var end = block.end;
            var total = block.total;

            file.chunks = chunks;           

            insertLog("<br>"+moment().format("YYYY-MM-DD HH:mm:ss")+" before-send  checkchunk:文件 "+file.name + " 分片"+chunk+"/"+chunks+"准备上传 ;");

            if(chunks>1){ //文件大于chunksize 分片上传
                owner.md5File(chunkFile)            
                .progress(function(percentage) {
                    //分片MD5计算可以不知道计算进度
                })  
                .then(function(chunkMd5) {  
                    //owner.options.formData.chunkMd5 = chunkMd5;
                    block.chunkMd5 = chunkMd5;
                    var chunkMd5s = file.chunkMd5s;
                    var exists = false;
                    if (typeof(chunkMd5s) == "undefined") { 
                        exists = false;
                    }  else{
                        exists = chunkMd5 == chunkMd5s[chunk]?true:false;                   
                    }

                    if (exists) {
                        insertLog("<br>"+moment().format("YYYY-MM-DD HH:mm:ss")+" before-send  checkchunk:文件 "+file.name + " 分片"+chunk+"/"+chunks+"已经存在,跳过上传;");                    
                        deferred.reject();
                    } else {                        
                        deferred.resolve();
                    }  

                    //console.log("before-send  checkchunk:"+chunk+"/"+chunks+" chunkMd5="+chunkMd5);
                });

            }else{//未分片文件上传
                block.chunkMd5 = file.wholeMd5;
                owner.options.formData.chunkMd5 = file.wholeMd5;
                deferred.resolve();
            }           

            owner.options.formData.fileMd5 = file.wholeMd5;
            return deferred.promise();
        }
    }); 
XGHeaven commented 9 years ago

为什么我的代码register和你的最开始的是一样的,但是不管我怎么写uploader,request,他就是不触发这个对应的函数,请问这是怎么回事?

sky20054122 commented 9 years ago

@XGHeaven

image

beyondonly commented 9 years ago

@sky20054122 在断点续传的时候,当文件的所有分片都上传完,但是使用这个方法去合并分片的时候,有问题,问题是当所有分片上传完之后,其实文件状态已经变成了uploadFinished,但实际上下面的方式还没完,没返回相应的数据 /* * * 所有分片已上传完成 * / WebUploader.Uploader.register({ 'after-send-file': 'afterSend' }, { afterSend: function( file ) { var me = this, owner = this.owner, server = me.options.server, deferred = WebUploader.Deferred();

                        var condition1=me.options.allowMinBreakPointResumeSize<=file.size;
                        var condition2=file.size>=me.options.chunkSize;
                        if((condition1&&condition2)){

                          // 与服务端验证
                            $.ajax(me.options.merger, {
                                dataType: 'json',
                                data: {
                                    fileMd5: me.options.formData.fileMd5,
                                    fileName:file.name,
                                    fileSize:file.size
                                },
                                success: function( response ) {
                                    if(response!=""){
                                        /**
                                         * 更新进度条、跳过该文件
                                         */
                                        var $tr = $('#' + file.id),percentage=1,
                                        $progressText = $tr.find('.fileSpanProgressText'),
                                        $percent = $tr.find('.fileSpanProgressPercentage');

                                        $progressText.html(Math.round(percentage * 100) + '%');
                                        $percent.css('width', Math.round(percentage * 100) + '%');
                                        percentages[ file.id ] = [file.size,1];
                                        updateTotalProgress();

                                         /**
                                         * 设置成功信息
                                         */
                                        var $tr = $('#' + file.id).children().eq(0);
                                        var ret=$.parseJSON(response);
                                        var exist=$tr.find("input").val();
                                        if(typeof(exist)== "undefined"){
                                            var input = "<input class='" + ret.code + "' type='hidden' value='" +response+ "'/>";
                                            $(input).appendTo($tr);
                                        }
                                    }
                                }
                            });
                        }
                    }
                });
XGHeaven commented 9 years ago

问一下为什么WebUploader计算的MD5跟PHP计算出来的MD5不一样?

sky20054122 commented 9 years ago

我打印的日志,上传顺序是对的 image

sky20054122 commented 9 years ago

@XGHeaven 我后台是java计算的MD5和webuploader是一样的,可以使用第三方工具测试MD5的正确值,看看错误出在哪里。https://md5file.com/calculator (我前面分片上传到后台,java合并文件出问题,导致MD5不一样,后来修正的。你看看你的具体原因是什么)

XGHeaven commented 9 years ago

@sky20054122 我用过第三方计算软件,发现PHP计算的是正确的,而上传的却是错误的