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

sky20054122 commented 9 years ago

@XGHeaven 你最好使用js调试一下,看看文件传入MD5计算方法时候的大小,起止位置等属性,如果是图片还要看看是不是压缩了。

sky20054122 commented 9 years ago

@beyond290239 你的代码里面没有使用deferred ,后台合并成则deferred.resolve(); 然后return deferred.promise();

/**
     * method:after-send-file
     * 在所有分片都上传完毕后,且没有错误后request,用来做分片验证,此时如果promise被reject,当前文件上传会触发错误。
     * para:file: File对象
     */
    uploader.register({
        'after-send-file' : 'chunkUploadFinish'
    }, {
        chunkUploadFinish : function(file) {
            insertLog("<br>"+moment().format("YYYY-MM-DD HH:mm:ss")+" after-send-file  chunkUploadFinish:文件 "+file.name + " 分片上传完成;");
            //var me = this; 
            //var owner = this.owner;
            var deferred = $.Deferred();
            var fileMd5 = file.wholeMd5;            
            var chunks = file.chunks;
            if(chunks > 1){//TODO 向server发送文件合并请求,根据结果决定文件上传成功与否

                $('#' + file.id).find('span.state').text("合并文件...");
                $.ajax({
                    cache : false,      
                    async:true,         
                    type : "post",
                    dataType : "json",
                    url : baseUrl + "/fileUpload/fileMerge",
                    data : {
                        fileMd5 : fileMd5,
                        isShared : $("#isShared").val(),
                        fileType : $("#fileType").val(),
                        ext:file.ext  //文件扩展名称
                    },
                    success : function(result) {
                        if (result.result) {
                            insertLog("<br>"+moment().format("YYYY-MM-DD HH:mm:ss")+" after-send-file  chunkUploadFinish:文件 "+file.name + " 合并文件成功");
                            $('#' + file.id).find('span.state').text("合并文件成功");
                            insertLog("<br>"+moment().format("YYYY-MM-DD HH:mm:ss")+" after-send-file  chunkUploadFinish:文件 "+file.name + " server响应:"+result.msg);
                            deferred.resolve();
                        } else {
                            insertLog("<br>"+moment().format("YYYY-MM-DD HH:mm:ss")+" after-send-file  chunkUploadFinish:文件 "+file.name + " server响应:"+result.msg);   
                            $('#' + file.id).find('span.state').text("合并文件失败");
                            deferred.reject();
                        }                           
                    }
                });         

            }else{
                deferred.resolve();
            }           

            return deferred.promise();

        }
    });
beyondonly commented 9 years ago

@sky20054122 谢谢!就是这个问题,已解决

beyondonly commented 9 years ago

@sky20054122 我测试了一下,在Flash下Md5验证更快,但是当我测试一个文件在2G左右的时候,Md5不能验证,而且还有一个很奇怪的现象是我上传一个1.1G的文件,Flash下可以进行Md5验证,但是不能上传,一直卡在上传的地方,请问一下,有没有解决的方案?

beyondonly commented 9 years ago

@sky20054122 不想使用Js验证的原因是,验证太慢

beyondonly commented 9 years ago

@sky20054122 我把runtimeOrder: 'flash',调整为flash

sky20054122 commented 9 years ago

@beyond290239 应该优先使用js验证,不支持html5的浏览器才使用flash上传作为补充; 因为flash是把整个文件读进内存才能计算MD5,大文件上传容易导致浏览器消耗内存过大,系统反应慢;

我使用html5模式计算MD5和快,不管是chrome,firefox和ie都必须关掉F12或者firebug调试,尽量不要在console输出;这样计算MD5就很快,理论上应该比flash快多了。

html5计算速度大概是15M/秒

beyondonly commented 9 years ago

@sky20054122 那这样意味着,在ie7 8 9中大文件上传是不支持的?

sky20054122 commented 9 years ago

低版本IE只能使用flash上传,传大文件虽然可以,但是很耗内存;

MD5的计算可以只计算文件的一部分(例如取文件的首位个一部分计算MD5),前后台一致,可以很快计算出该文件是否已经存在

beyondonly commented 9 years ago

@sky20054122 我设置为runtimeOrder: 'flash'这样的模式上传,并且我的内存是8G,所有的物理资源都够,但是1.1G的文件能做Md5校验,但是不能上传,做完了md5后就卡在哪里,同时,上传2G的文件,连Md5都不能校验,目的就是测试一下ie中我能否设置,以及我要提供的最大上传文件大小

sky20054122 commented 9 years ago

@beyond290239 我刚才测试了一下flash模式下,大文件上传,chrome和firefox都没有问题,在IE11下,计算MD5都不动,目前不清楚原因。

beyondonly commented 9 years ago

@sky20054122 恩,chrome和firefox都没有问题,但是我测试的是800M左右,但是上了1G的就不行,就和你说的跟IE中一样

wwg88888888 commented 9 years ago

怎么才能停止上传呢?我使用stop方法,不能停止md5扫描。

wwg88888888 commented 9 years ago

@sky20054122大神

wwg88888888 commented 9 years ago

@sky20054122大神

sky20054122 commented 9 years ago

@wwg88888888 目前调用两种停止,都没有停止计算MD5 ; 分片上传的暂停和继续会丢掉缓存中三个分片的部分,导致最终上传的分片不足, 如果你需要停止MD5计算,要@2betop 添加此功能才行。我只是使用webuploader ,没有参与开发!

wwg88888888 commented 9 years ago

@2betop md5 能停止扫描码?2G的文件,选错了,这停止都停止不掉

wwg88888888 commented 9 years ago

@2betop md5 能停止扫描码?2G的文件,选错了,这停止都停止不掉

mercurychs commented 9 years ago

你这个妙传文件的时候,按照你的思路,先算md5,然后与服务端对比,但是如果服务端没有的话,因为你的流数据已经读过一次,不能重复再读,你怎么把数据再传到服务器。小文件可以在算md5的时候,这样数据变成byte[]数组是可以得,但是大文件肯定不行,不知道你这个大文件妙传能支持多大。或者还是有别的解决方案。

litingtingting commented 9 years ago

我是利用那个hash值来进行比对,算md5太慢了, 我自己改了三处代码 , 就能实现前端的断点续传的支持。 但现在有个新的问题,就是服务器端给我明明是有数据的, 但在uploadSuccess监听的事件中有时候却取不到服务端的值。为什么呢 ? 感觉像是在上传完成前就执行了uploadSuccess这个事件。

WillZeroman commented 9 years ago

分片上传有两个问题: 1)md5校验时太慢了 2)后台多线程同步问题很复杂,怎么让多个分片按顺序写到同一文件中?

litingtingting commented 9 years ago

@lw394407679 webuploader是利用多线程上传, 而且上传时是没有顺序的, 所以我先将分块的文件放在一个独立的临时目录中, 当上传完成后,再进行合并。 而不是写到同一文件中(除非你将线程数改为1),

Hibear commented 9 years ago

@lw394407679 你没遇到分片上传点击暂停又继续上传,会丢失分片的情况吗?

lvmn commented 9 years ago

flash分片上传按理应该内存不会暴增因此我分为多个小片上传,为何不会释放,是不是flash问题?

luoyehanfei commented 9 years ago

关于断点续传与秒传的总结: 1、首先说说pupload这个插件,用过它的都知道,其实webuploader的功能与它有惊人的相似之处,但是pupload是不带md5验证的,得自己去计算。这几天在研究的过程中发现,pupload在HTML5下利用FileRead读取md5时是没有文件大小限制的,但是在flash下,flash实际上是调用的Moxie.swf接口。

这里着重说说flash的断点续传与秒传(同时也希望开发团队能够参考参考):

当文件大小在100M以下时:读取文件流然后利用spark-md5去计算出MD5是没有问题的。

那么,当文件在100M以上,甚至1G,10G,100G的时候怎么办呢?就算是HTML5模式下去计算这样的文件耗时也非常严重,有时候无法忍受了。

于是我想着能不能读取文件的开始字节+结束字节然后组合成 一个伪造的md5去进行验证呢?

可以想象一下,假设文件有351M,我以5M的区间去读取 0-5M的数据,然后346-351的数据,最终组合成一个MD5,这样在(99.9%)常规情况下,算出来的md5值也是唯一的,基本不会出现重复的。

于是我想:既然可以利用这种方式:那么应该在HTML5与Flash 下都能算出md5了,因为这样实际上我只读取了10M的内容。甚至这个值可以设置成1M一个区间,那么2M的内容算MD5,不会超过20ms。

但是:最后经过实现发现,pupload在html5时这样读取是没有任何问题,可以读取并算出md5值的,但是在flash下,超过100M文件时在读取的时候还是报错了。(这个我查阅资料说是flash fileread instance单个实例只支持100M的文件,所以读取会报错)

昨天我正在纠结这个问题的时候偶然发现国产大作 webuploader这款上传插件,于是信心满满的又开始测试了,webuploader自带md5计算方法

uploader.on('fileQueued', function (file) { uploader.md5File(file, 0, 5242880).progress(function (percentage) { $("#thelist").append("

进度:" + percentage + "
") }).then(function (val) { alert('md5 result:' + val); }); $("#thelist").append('
' + '

' + file.name + '

' + '

等待上传...

' + '
'); });

利用 uploader.md5File(file,start,end)方法,我们轻易可以计算出区间的md5,也可以计算完整的md5。

但是.....................在flash模式下(用ie8),又发现了相同的问题,在文件超过100m时同样计算不出md5值,并且假设文件是50g 直接 uploader.md5File(file) 与 uploader.md5File(file,0,"1mb") 所计算的耗时是一样的。

现在初步推测原因是flash的fileread instance 造成的,并且flash在读取区间数据时应该还是先把整个文件加载到内存,从内存中取得的,所以计算整个文件的md5与取一个区间是没有区别的,但是flash这块不了解,也不敢轻易改造源码。

希望webuploader开发团队能在这块上有个崭新的突破,那么做到真正的全兼容 断点续传、秒传也就不远了。

litingtingting commented 9 years ago

我感觉对文件内容做md5加密 ,不如对文件名,文件大小,文件修改时间等文件信息合并做一个hash值,这种情况发生冲突的概念也是相当小的。我做断点续传的秒传思路与原作者不同, 看了源代码:代码没有对每一块分块上传完成后的事件,若某个分块没有实际保存上,就坑了, 并且每一次分块上传都得去做ajax验证,而且skipfIle这个函数是跳过整个文件, 我做了些改造,能实现在整个文件开始上传前,我去获取已传上传完成的chunk, 然后从缺的分块开始上传,只要是上传过的分块就直接跳过。

luoyehanfei commented 9 years ago

楼上这种做法早在几年前用pupload试过,不适用,做出来毫无意义,打个比方用户A上传了一份SQL SERVER安装包,他电脑的时间,文件名等可能和 B用户电脑上的不相同,当B用户又上传一份SQL SERVER安装包时,你无法确定已经穿过了,做不了秒传,最多只能做针对“同一份文件”的断点续传。

litingtingting commented 9 years ago

不是毫无意义,也有一种情况用户在文件的内容后追加了一些内容, 用md5做加密, 得到的值还是不同的,实际上他们相似度可能99.99%, 所以就取决于 对“同一份文件”的定义了, 我这边的应用场景更多的是针对某一个用户的上传,他上次上传了55%,这次要接着传,我说的思路更多是“断”点“续”传,不是秒传。

zz13761743 commented 9 years ago

分片上传功能已实现,但是暂停上传后再次触发上传方法时,上传进度又从0开始上传了

Davis-Wong commented 8 years ago

uploader.on( 'startUpload', function( file ) { uploader.md5File( file ) .then(function(val) { console.log('md5 result:', val); $md5 = val; }); }); 为什么会报错呢?加入列队后fileQueued能正常执行,但是startUpload状态确保错 求大师讲解

AngieJames commented 8 years ago

@beyond290239 @sky20054122 我遇到个这么问题,断点续传做分片上传的时候,我设置了4个线程一起上传,有的用户上传失败,很小一部分用户存在这个问题,偶发问题,查log是分4个线程后,分片文件总数小于四个,并发时有的上传请求就丢了,没有请求到server,导致上传失败。自己本地自测很多遍没有重现,怎么解? 有人遇到过类似的问题么,还有就是当分片文件总数小于线程数时会有无效的请求。即空参数的请求。

litingtingting commented 8 years ago

我遇到的情况是 有分块丢失的情况, 但不清楚是因为没有发起请求,还是服务端的问题。 我也是自己在本地自测测了很多次没有复现,所以后来我在一个文件上传成功之后 ,去 上传服务器 检测一下,判断是否真正上传成功。

wangmeijian commented 8 years ago

用uploader.stop(file)暂停单个文件上传后,用uploader.upload(file)无法继续上传,是怎么回事?

baiyunchen commented 8 years ago

14M的文件,用谷歌/火狐分片上传,好卡啊~整个上传完成差不多要五六分钟,是我哪里没有写对么?

<script type="text/javascript">
    var GUID = WebUploader.Base.guid();//一个GUID
    var uploader = WebUploader.create({
        swf: '/Scripts/Plugins/webuploader-0.1.5/Uploader.swf',
        server: '@Url.Action("Upload")',
        pick: '#picker',
        resize: false,
        chunked: true,//开始分片上传
        chunkSize: 2048000,//每一片的大小
        formData: {
            guid: GUID //自定义参数,待会儿解释
        }
    });
    var $list = $("#thelist");
    uploader.on('fileQueued', function (file) {
        $list.append('<div id="' + file.id + '" class="item">' +
            '<h4 class="info">' + file.name + '</h4>' +
            '<p class="state">等待上传...</p>' +
        '</div>');
    });
    uploader.on('uploadSuccess', function (file, response) {
        $.post('@Url.Action("Merge")', { guid: GUID, fileName: file.name }, function (data) {
            $list.text('已上传');
        });
    });
    uploader.on('uploadProgress', function (file, percentage) {
        console.log('Percentage:', percentage);
        $("#progress-bar").width(percentage * 100 + "%");
    });
    $("#ctlBtn").click(function () {
        uploader.upload();
    });
</script>
nikorliu commented 8 years ago

@luoyehanfei 确实,现在需要兼容IE7/8/9支持,只有flash的方案了,但是flash计算md5确实太慢了。看NTKO的大附件上传非常的快,不知道是用了什么方案

@2betop @sky20054122 有其它办法解决吗?

annezhong commented 8 years ago

IE7/8/9 计算MD5还是不兼容吗? 或者是我没找对方法?

jaanio commented 8 years ago

怎么在外部添加个文件到上传队列呢?

jaanio commented 8 years ago

@2betop 怎么在外部添加个文件到上传队列呢?

JongShau commented 7 years ago

当我切换到IE9下时,选择文件按钮失效,我在webuploader.js 源码中调用flash的位置alert了一下,发现alert没反应,而在IE10和IE11下则没问题,因此确定flash根本没有加载,对于此问题楼主有没有解决之道

wangjunjx8868 commented 7 years ago

@2betop uploader.register({
'before-send' : 'checkchunk' }, { checkchunk : function(block) {

用了上面的HOOK的before-send上传前分段检测MD5, 下面的方法的进度条显示percentage一直是0 uploader.on('uploadProgress', function (file, percentage) { console.log('Percentage:', percentage); $("#progress-bar").width(percentage * 100 + "%"); });

liuweitao2 commented 6 years ago

为什么我owner.md5File(file)后返回是undefined呢?

diskrooms commented 5 years ago

有一个问题 服务器如何得知这个文件的md5 难道把上传目录的所有文件全部md5 一遍吗 如果有多个上传目录呢

LSL1618 commented 5 years ago

@diskrooms ,肯定是前端计算所有文件MD5值,然后发送到后端,与后端计算的MD5值匹配。要比较必须要有来源和目标。

gkm1987 commented 5 years ago

var fileT = uploader.getFile(parentT) uploader.stop(fileT)

Cannot read property 'file' of undefined at HTMLSpanElement.filePause (uploadPage.html:203) 指向stop方法

LSL1618 commented 5 years ago

@gkm1987 ,先确认var fileT = uploader.getFile(parentT)这一句有没有问题。

Wigithub1201 commented 4 years ago

this.uploader.retry(continueFile);

我这里断网之后再连接调用upload方法或者retry并不续传,为什么?