Meteor:将文件从客户端上传到Mongo集合vs文件系统与GridFS

时间:2015-01-14 00:50:23

标签: mongodb file-upload meteor gridfs

流星很棒,但缺乏对传统文件上传的原生支持。有几种方法可以处理文件上传:

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

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

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

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

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

2 个答案:

答案 0 :(得分:73)

您可以使用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})         
    }   
});

<强> Explantion

首先,使用HTML5 File API从输入中获取文件。使用新的FileReader创建阅读器。该文件读作readAsArrayBuffer。这个arraybuffer,如果你是console.log,返回{},而DDP不能通过网络发送,所以必须将它转换为Uint8Array。

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

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

<强>赞成

  1. 简单,没有hacky方式,没有额外的包
  2. 坚持有线数据原则
  3. <强>缺点

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

    7. 选项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对象,并通过&#39; POST&#39;发送文件。到服务器。

      在服务器上,数据通过管道传输到基础文件系统。在保存之前,您还可以确定文件名,执行消毒或检查是否已存在等。

      <强>赞成

      1. 利用XHR 2以便发送arraybuffer,与选项1相比,不需要新的FileReader()
      2. 与base64字符串
      3. 相比,Arraybuffer不那么笨重
      4. 没有大小限制,我在localhost中发送了一个200 MB的文件,没有问题
      5. 文件系统比mongodb更快(稍后在下面的基准测试中更多)
      6. Cachable和gzip
      7. <强>缺点

        1. 旧版浏览器无法使用XHR 2,例如IE10以下,但当然你可以实现一个传统的帖子&lt; form&gt;我只使用xhr = new XMLHttpRequest(),而不是HTTP.call(&#39; POST&#39;),因为Meteor中的当前HTTP.call还不能发送arraybuffer(如果我错了,请指出我)。 / LI>
        2. / path / to / dir /必须在meteor外部,否则在/ public中写入文件会触发重新加载

        3. 选项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中,MongoInternals也公开了GridStore。当前流星使用的mongo是v1.4.x

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

          我还包含了一个版本B.在这个版本中,GridStore是通过Npm.require(&#39; mongodb&#39;)使用新的mongodb驱动器调用的,这个mongo是最新的v2.0.13。 。新的API并不要求您打开文件,您可以直接调用stream(true)并开始管道

          <强>赞成

          1. 与使用arraybuffer发送的选项2相同,与选项1中的base64字符串相比开销更少
          2. 无需担心文件名清除
          3. 与文件系统分离,无需写入temp dir,db可以备份,rep,shard等
          4. 无需实施任何其他套餐
          5. Cachable,可以gzipped
          6. 与普通mongo系列相比,存储更大的尺寸
          7. 使用管道减少内存过载
          8. <强>缺点

            1. 不稳定的Mongo GridFS 。我包括版本A(mongo 1.x)和B(mongo 2.x)。在版本A中,当管理大文件时> 10 MB,我收到很多错误,包括损坏的文件,未完成的管道。使用mongo 2.x在版本B中解决了这个问题,希望meteor很快升级到mongodb 2.x
            2. API混淆。在版本A中,您需要先打开文件才能进行流式传输,但在版本B中,您可以在不调用open的情况下进行流式传输。 API文档也不是很清楚,并且流不是100%语法可与Npm.require交换(&#39; fs&#39;)。在fs中,你调用file.on(&#39; finish&#39;)但是在GridFS中你在写完成/结束时调用file.on(&#39; end&#39;)。
            3. GridFS不提供写入原子性,因此如果同一个文件有多个并发写入,则最终结果可能会有很大不同
            4. 速度的。 Mongo GridFS比文件系统慢得多。
            5. <强>基准 您可以在选项2和选项3中看到,我包括var start = Date.now(),当写入结束时,我在 ms 中记录时间,以下是结果。双核,4 GB RAM,HDD,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 是最简单的,坚持核心流星原则,但数据更笨重,在传输过程中不可压缩,不可缓存。但是,如果您只需要小文件,这个选项可能会很好。
              • XHR与文件系统相结合是传统的&#39;办法。稳定的API,快速,可流式传输,可压缩,可缓存(ETag等),但需要位于单独的文件夹中
              • XHR加上GridFS ,你可以获得rep set,可扩展,没有触摸文件系统dir,大文件和许多文件的好处,如果文件系统限制数字,也可以缓存压缩。但是,API不稳定,您在多次写入时会出错,它会出现错误...

              希望很快,流星DDP可以支持gzip,缓存等,GridFS可以更快 ......

答案 1 :(得分:0)

您好,只需添加有关查看文件的Option1。我没有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);};