分片上传与断点续传解决方案

上传文件,基本上是每个网站应用都会具有的一个功能。对于一个网络存储应用,对于上传功能要求更是迫切。 html

现在市面上成熟上传插件,如WebUploader,"体积太大",不适合于移动端上传;再加上做为一位程序员的"操守",固然仍是更喜欢本身造轮子。前端

因而花了一天半时间,MoUploader应运而生。为何叫MoUploader呢?Mo表示Mobile(其实更是由于个人绰号moyu)html5

关于实现原理

首先须要明确,上传这东西不单单是只须要前端就能完成的很好的,须要前端后端统一数据格式,从而实现断点续传。(因此,该文适合于全栈工程师,至少是想成为)git

还有,为何须要分片,不分片能实现断点续传吗?分片是为了充分利用网络带宽,加快上传速度;不分片也是可以实现断点续传的。详细参考 HTML5文件上传组件深度剖析.
分片上传与断点续传之间没有很直接的关系.程序员

好了,进入正题。github

实现断点续传的前提是须要服务器记录某文件的上传进度,那么根据什么判断是否是同一个文件呢?能够利用文件内容求md5码,若是文件过大,求取md5码也是一个很长的过程,因此对于大文件,只能针对某一段数据进行计算,加上服务器对cookie用户信息的判断,获得相对惟一的key。数据库

在前端页面,须要将文件按照必定大小进行分片,一次请求只发送这一小片数据,因此咱们能够同时发起多个请求。但一次同时请求的链接数不宜过多,服务器负载太重。对于文件分片操做,H5具备十分强大的File API,直接利用File对象的slice方法便可获得Blob对象。json

至于同时传输数据的链接数控制逻辑,就须要花点脑子思考了。前端把数据顺利得传给服务器了,服务器只须要按照数据中给的开始字节位置,与读取到的文件片断数据,写入文件便可后端

更多信息就看源码吧!MoUploaderpromise

功能实现

文件结构

file-upload/
├── bower_components/ # bower包
├── db.js   # 数据操做接口
├── demo.html
├── md5.json # 数据
├── mouploader.js # 源码
├── README.md 
└── server.js # demo.html服务, 创建在3000端口

1 directories, 8 files.

(打印文件目录树使用的是本身写的print-dir)

怎么使用

  1. 引入script,amd/cmd/...,

  2. 使用MoUploader

    input.onchange = function (e) {
        var self = this;
        var moUploader = MoUploader({ 
            files: this.files,
            uploadUrl: '/upload',
            request: false,
            onBeforeUpload: function (index) {
                if(index>=0) {
                    self.files[index].progress = appendUploading(self.files[index], index)
                }
            },
            onOverAllProgress: function (index, loaded, total) {
                console.log(loaded / total)
                //setProgress(loaded / total, self.files[index].progress)
            },
            onLoad: function (index, chunkIndex, chunksNum) {
                console.log('onLoad', this, arguments)
            },
            onAbort: function (index, chunkIndex, chunksNum) {
                console.log('onAbort', this, arguments)
            },
            onError: function (index, chunkIndex, chunksNum) {
                console.log('onError', this, arguments)
            },
            onContinue: function (file, md5, index) {
                return new Promise(function(reslove, reject) {
                    var xhr = new XMLHttpRequest()
                    xhr.open('GET', '/getFile?md5='+md5, true);
                    xhr.send(null);
                    xhr.addEventListener('readystatechange', function () {
                        if(xhr.readyState === 4 && xhr.status === 200) {
                            var json = JSON.parse(xhr.responseText);
                            log(json)
                            reslove(json.pos)
                        }
                    })
                })
            }
        })
        
        // pause or continue upload
        // if index < 0, will run for all files
        // moUploader.pause(index);
        // moUploader.continue(index);    
    }
  3. 配置选项

    var default_ops = {
        // chunk Size: byte
        chunkSize: (1<<20) * 5,
        // Number: request Number.
        // Array: files requests.
        // Boolean: open or close Slice, if false, chunkSize don't work.
        request: 3,
        files: [],
        uploadUrl: '/',
        // function: get uploaded pos.
        // arguments: file, md5, index.
        // need return a promise object which will return uploaded pos.
        onContinue: null,
        // if false, md5 will be setted by filename.
        md5: true,
        // md5Size: slice file 0 - md5Size for calculate md5
        md5Size: (1<<20) * 50,
        // called when before upload.
        // arguments: file index or -1 (will begin upload)
        onBeforeUpload: null,
        // function: uploading progress listener.
        // *only listen one request.*
        // arguments: index, chunkIndex, chunksNum, loaded, total.
        onProgress: null,
        // function: overall uploading progress listener.
        // arguments: index, loaded, total
        onOverAllProgress: null,
        // function: called when one request is ended.
        // arguments: index, chunkIndex, chunksNum
        onLoad: null,
        // function: called when one request is aborted.
        // arguments: index, chunkIndex, chunksNum
        onAbort: null,
        // function: called when one request happens error.
        // arguments: index, chunkIndex, chunksNum
        onError: null
    }
  4. 服务器数据处理 (Node.js)
    数据分段写入文件

    function writeBuffer(bf, path, pos) {
        var fd = fs.openSync(path, 'a+');
        fs.writeSync(fd, bf, 0, bf.length, Number(pos) || 0)
        console.log(`write buffer, pos: ${pos}, path: ${path}, length: ${bf.length}`)
    }
    
    function store(param, chunks) {
        param.chunks = param.chunks || 1
        param.chunk = param.chunk || 0
        var p = path.join('./upload', param.name)
        var bf = Buffer.concat(chunks);
    
        var json = db.get(param.md5);
        if(json) {
            json.pos = parseInt(json.pos!=null?json.pos : 0)
            json.size = parseInt(json.size!=null?json.size : 0)
        }
        if(!json || (json.pos+json.size) <= param.pos) {
            // 新的数据pos比数据库中大,更新数据
            param.size = bf.length
            db.set(param.md5, param)
            db.save();
            writeBuffer(bf, p, param.pos || 0)
        }
    }
    
    var multiparty = require('multiparty')
    var form = new multiparty.Form({
        autoFields: true,
        autoFiles: false,
    });
    
    form.on('part', (part) => {
        form.on('aborted', () => {
            //意外退出或者暂停都会保存数据
            console.log('aborted');
            store(param, chunks)
        })
    
        var chunks = []
        part.on('data', (data) => {
            if(part.filename) {
                chunks.push(data)
            }
        }).on('end', () => {
            console.log('end')
            store(param, chunks)
        })
    
    });
    form.on('field', (name, value) => {
        param[name] = value;
    });
相关文章
相关标签/搜索