【问题标题】:Meteor: uploading file from client to Mongo collection vs file system vs GridFSMeteor:将文件从客户端上传到 Mongo 集合 vs 文件系统 vs GridFS
【发布时间】:2015-03-12 02:48:47
【问题描述】:

Meteor 很棒,但它缺乏对传统文件上传的原生支持。有几种处理文件上传的选项:

从客户端,可以使用以下方式发送数据:

  • Meteor.call('saveFile',data) 或 collection.insert({file:data})
  • 'POST' 表单或 HTTP.call('POST')

在服务器中,文件可以保存到:

  • collection.insert({file:data}) 的 mongodb 文件集合
  • /path/to/dir 中的文件系统
  • mongodb GridFS

这些方法的优缺点是什么?如何最好地实施它们?我知道还有其他选项,例如保存到第三方网站并获取网址。

【问题讨论】:

    标签: mongodb file-upload meteor gridfs


    【解决方案1】:

    您可以使用 Meteor 实现文件上传,而无需使用任何其他软件包或第三方

    选项 1:DDP,将文件保存到 mongo 集合

    /*** client.js ***/
    
    // asign a change event into input tag
    'change input' : function(event,template){ 
        var file = event.target.files[0]; //assuming 1 file only
        if (!file) return;
    
        var reader = new FileReader(); //create a reader according to HTML5 File API
    
        reader.onload = function(event){          
          var buffer = new Uint8Array(reader.result) // convert to binary
          Meteor.call('saveFile', buffer);
        }
    
        reader.readAsArrayBuffer(file); //read the file as arraybuffer
    }
    
    /*** server.js ***/ 
    
    Files = new Mongo.Collection('files');
    
    Meteor.methods({
        'saveFile': function(buffer){
            Files.insert({data:buffer})         
        }   
    });
    

    说明

    首先,使用 HTML5 File API 从输入中获取文件。阅读器是使用新的 FileReader 创建的。该文件被读取为 readAsArrayBuffer。如果您使用 console.log,此数组缓冲区会返回 {},而 DDP 无法通过网络发送它,因此必须将其转换为 Uint8Array。

    当你把它放在 Meteor.call 中时,Meteor 会自动运行 EJSON.stringify(Uint8Array) 并使用 DDP 发送它。你可以查看chrome控制台websocket流量中的数据,你会看到一个类似base64的字符串

    在服务器端,Meteor 调用 EJSON.parse() 并将其转换回缓冲区

    优点

    1. 简单,没有hacky方式,没有额外的包
    2. 坚持数据传输原则

    缺点

    1. 更多带宽:生成的 base64 字符串比原始文件大约 33%
    2. 文件大小限制:无法发送大文件(限制 ~ 16 MB?)
    3. 没有缓存
    4. 还没有 gzip 或压缩
    5. 发布文件会占用大量内存

    选项 2:XHR,从客户端发布到文件系统

    /*** client.js ***/
    
    // asign a change event into input tag
    'change input' : function(event,template){ 
        var file = event.target.files[0]; 
        if (!file) return;      
    
        var xhr = new XMLHttpRequest(); 
        xhr.open('POST', '/uploadSomeWhere', true);
        xhr.onload = function(event){...}
    
        xhr.send(file); 
    }
    
    /*** server.js ***/ 
    
    var fs = Npm.require('fs');
    
    //using interal webapp or iron:router
    WebApp.connectHandlers.use('/uploadSomeWhere',function(req,res){
        //var start = Date.now()        
        var file = fs.createWriteStream('/path/to/dir/filename'); 
    
        file.on('error',function(error){...});
        file.on('finish',function(){
            res.writeHead(...) 
            res.end(); //end the respone 
            //console.log('Finish uploading, time taken: ' + Date.now() - start);
        });
    
        req.pipe(file); //pipe the request to the file
    });
    

    说明

    抓取客户端中的文件,创建一个 XHR 对象,并通过“POST”将文件发送到服务器。

    在服务器上,数据通过管道传输到底层文件系统。您还可以在保存之前确定文件名、执行清理或检查它是否已经存在等。

    优点

    1. 利用 XHR 2,您可以发送 arraybuffer,与选项 1 相比,不需要新的 FileReader()
    2. 与 base64 字符串相比,Arraybuffer 的体积更小
    3. 没有大小限制,我在 localhost 中发送了一个大约 200 MB 的文件,没有问题
    4. 文件系统比 mongodb 更快(稍后在下面的基准测试中会详细介绍)
    5. 可缓存和 gzip

    缺点

    1. XHR 2 在旧版浏览器中不可用,例如IE10以下,不过当然可以实现传统的post
      我只用了xhr = new XMLHttpRequest(),而不是HTTP.call('POST'),因为目前Meteor中的HTTP.call还不能发送arraybuffer (如果我错了,请指出我)。
    2. /path/to/dir/ 必须在流星之外,否则在 /public 中写入文件会触发重新加载

    选项 3:XHR,保存到 GridFS

    /*** client.js ***/
    
    //same as option 2
    
    
    /*** version A: server.js ***/  
    
    var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
    var GridStore = MongoInternals.NpmModule.GridStore;
    
    WebApp.connectHandlers.use('/uploadSomeWhere',function(req,res){
        //var start = Date.now()        
        var file = new GridStore(db,'filename','w');
    
        file.open(function(error,gs){
            file.stream(true); //true will close the file automatically once piping finishes
    
            file.on('error',function(e){...});
            file.on('end',function(){
                res.end(); //send end respone
                //console.log('Finish uploading, time taken: ' + Date.now() - start);
            });
    
            req.pipe(file);
        });     
    });
    
    /*** version B: server.js ***/  
    
    var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
    var GridStore = Npm.require('mongodb').GridStore; //also need to add Npm.depends({mongodb:'2.0.13'}) in package.js
    
    WebApp.connectHandlers.use('/uploadSomeWhere',function(req,res){
        //var start = Date.now()        
        var file = new GridStore(db,'filename','w').stream(true); //start the stream 
    
        file.on('error',function(e){...});
        file.on('end',function(){
            res.end(); //send end respone
            //console.log('Finish uploading, time taken: ' + Date.now() - start);
        });
        req.pipe(file);
    });     
    

    说明

    客户端脚本与选项2中的相同。

    根据 Meteor 1.0.x mongo_driver.js 最后一行,暴露了一个名为 MongoInternals 的全局对象,您可以调用 defaultRemoteCollectionDriver() 来返回 GridStore 所需的当前数据库 db 对象。在版本 A 中,GridStore 也由 MongoInternals 公开。当前流星使用的mongo是v1.4.x

    然后在路由中,您可以通过调用 var file = new GridStore(...) (API) 创建一个新的写入对象。然后打开文件并创建一个流。

    我还包括了一个版本 B。在这个版本中,GridStore 是通过 Npm.require('mongodb') 使用新的 mongodb 驱动器调用的,这个 mongo 是在撰写本文时最新的 v2.0.13。新的API不需要打开文件,直接调用stream(true)即可开始管道

    优点

    1. 与选项 2 相同,使用 arraybuffer 发送,与选项 1 中的 base64 字符串相比开销更少
    2. 无需担心文件名清理
    3. 与文件系统分离,无需写入临时目录,db可以备份,rep,shard等
    4. 无需实现任何其他包
    5. 可缓存且可压缩
    6. 与普通 mongo 集合相比,存储更大的尺寸
    7. 使用管道减少内存过载

    缺点

    1. 不稳定的 Mongo GridFS。我包括版本 A (mongo 1.x) 和 B (mongo 2.x)。在版本 A 中,当管道大于 10 MB 的大文件时,我遇到了很多错误,包括文件损坏、管道未完成。这个问题在 B 版中使用 mongo 2.x 解决了,希望流星能尽快升级到 mongodb 2.x
    2. API 混乱。在版本 A 中,您需要先打开文件才能进行流式传输,但在版本 B 中,您无需调用 open 即可进行流式传输。 API 文档也不是很清楚,并且流不是 100% 可与 Npm.require('fs') 交换的语法。在 fs 中,您调用 file.on('finish'),但在 GridFS 中,您在写入完成/结束时调用 file.on('end')。
    3. GridFS 不提供写入原子性,因此如果有多个并发写入同一个文件,最终结果可能会有很大差异
    4. 速度。 Mongo GridFS 比文件系统慢得多。

    基准测试 您可以在选项 2 和选项 3 中看到,我包含了 var start = Date.now() 并且在写入 end 时,我 console.log 以 ms 为单位退出时间,结果如下。双核,4 GB 内存,硬盘,基于 ubuntu 14.04。

    file size   GridFS  FS
    100 KB      50      2
    1 MB        400     30
    10 MB       3500    100
    200 MB      80000   1240
    

    您可以看到 FS 比 GridFS 快得多。对于 200 MB 的文件,使用 GridFS 大约需要 80 秒,但使用 FS 只需大约 1 秒。我没有尝试过SSD,结果可能会有所不同。然而,在现实生活中,带宽可能决定文件从客户端流式传输到服务器的速度,达到 200 MB/秒的传输速度并不典型。另一方面,传输速度约为 2 MB/秒 (GridFS) 更为常见。

    结论

    这绝不是全面的,但您可以决定哪个选项最适合您的需要。

    • DDP 是最简单的,并坚持核心 Meteor 原则,但数据更庞大,在传输过程中不可压缩,不可缓存。但如果您只需要小文件,此选项可能会很好。
    • XHR 与文件系统相结合 是“传统”方式。稳定的 API、快速、“可流式传输”、可压缩、可缓存(ETag 等),但需要位于单独的文件夹中
    • XHR 与 GridFS 相结合,您可以获得代表集、可扩展、不接触文件系统目录、大文件和许多文件(如果文件系统限制数量)的好处,还可以缓存压缩。但是,API 不稳定,多次写入时会出错,这是 s..l..o..w..

    希望很快,meteor DDP 可以支持 gzip、缓存等,而 GridFS 可以更快...

    【讨论】:

    • 喜欢这些方法!!我只有一个关于 Iron:router 的问题——当我尝试this.request.pipe(file); 时,我得到了TypeError: Cannot call method 'pipe' of undefined 你知道为什么会发生这种情况吗?
    • 我没有用 Iron:router 试过这个,抱歉。在 WebApp.connectHandlers 之前有一个 WebApp.rawConnectHandlers,可以试试这个吗?
    • 没关系 - 当您在回答中提到它时,我很兴奋。 WebApp 是一个包还是......它是用来做什么的? (我正在慢慢了解流星附带的广泛 API):)
    • 啊,是的! Connect API 是关键!它提供了流媒体机制。在 Iron:router 中,它被称为(?)onBeforeAction: function(req, res, next)。我应该将此添加为答案,将其添加到您的答案中还是将其保留在 cmets 中?
    【解决方案2】:

    您好,只是添加到关于查看文件的选项1。我在没有 ejson 的情况下做到了。

    <template name='tryUpload'>
      <p>Choose file to upload</p>
      <input name="upload" class='fileupload' type='file'>
    </template>
    
    Template.tryUpload.events({
    'change .fileupload':function(event,template){
    console.log('change & view');
    var f = event.target.files[0];//assuming upload 1 file only
    if(!f) return;
    var r = new FileReader();
    r.onload=function(event){
      var buffer = new Uint8Array(r.result);//convert to binary
      for (var i = 0, strLen = r.length; i < strLen; i++){
        buffer[i] = r.charCodeAt(i);
      }
      var toString = String.fromCharCode.apply(null, buffer );
      console.log(toString);
      //Meteor.call('saveFiles',buffer);
    }
    r.readAsArrayBuffer(f);};
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-06-29
      • 2020-12-07
      • 2016-02-02
      • 1970-01-01
      • 2017-09-26
      • 1970-01-01
      • 1970-01-01
      • 2017-08-21
      相关资源
      最近更新 更多