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

Meteor: uploading file from client to Mongo collection vs file system vs GridFS

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

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

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

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

您可以使用 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 文件 API 从输入中抓取文件。 reader 使用新的 FileReader 创建。该文件被读取为 readAsArrayBuffer。这个arraybuffer,如果你console.log, returns {} 和DDP不能通过线路发送这个,所以它必须转换成Uint8Array。

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

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

优点

  1. 简单,没有 hacky 方法,没有额外的包
  2. 坚持线上数据原则

缺点

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

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

/*** 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,您可以发送数组缓冲区,与选项 1 相比,不需要新的 FileReader()
  2. 与 base64 字符串相比,Arraybuffer 体积更小
  3. 没有大小限制,我在本地主机中发送了一个 ~ 200 MB 的文件没有问题
  4. 文件系统比 mongodb 快(稍后在下面的基准测试中更多)
  5. 可缓存和 gzip

缺点

  1. XHR 2 在旧版浏览器中不可用,例如低于 IE10,但当然你可以实现传统的 post
    我只使用 xhr = new XMLHttpRequest(),而不是 HTTP.call('POST') 因为当前 HTTP.call 在 Meteor 中还不能发送 arraybuffer(如果我错了请指出我)。
  2. /path/to/dir/ 必须在 meteor 之外,否则在 /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()来return当前GridStore 所需的数据库 db 对象。在版本 A 中,GridStore 也由 MongoInternals 公开。当前meteor使用的mongo是v1.4.x

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

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

优点

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

缺点

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

基准 你可以在选项 2 和选项 3 中看到,我包括了 var start = Date.now() 并且在写结束时,我 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/sec 的传输速度并不典型。另一方面,传输速度 ~2 MB/sec (GridFS) 更常见。

结论

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

  • DDP是最简单的,坚持Meteor的核心原理,但数据比较庞大,传输时不可压缩,不可缓存。但如果你只需要小文件,这个选项可能会很好。
  • XHR 加上文件系统 就是'traditional' 方式。稳定 API、快速、'streamable'、可压缩、可缓存(ETag 等),但需要位于单独的文件夹中
  • XHR 与 GridFS 相结合,您将获得代表集、可扩展、不接触文件系统目录、大文件和许多文件(如果文件系统限制数量)的好处,也可缓存可压缩的。但是,API不稳定,多次写入会出错,s..l..o..w..

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

您好,我想补充一下关于查看文件的选项 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);};