MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Meteor:carregando arquivo do cliente para a coleção Mongo vs sistema de arquivos vs GridFS


Você pode fazer o upload de arquivos com o Meteor sem usar mais pacotes ou terceiros

Opção 1:DDP, salvando arquivo em uma coleção 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})         
    }   
});

Explicação

Primeiro, o arquivo é obtido da entrada usando a API de arquivo HTML5. Um leitor é criado usando o novo FileReader. O arquivo é lido como readAsArrayBuffer. Esse arraybuffer, se você console.log, retorna {} e o DDP não pode enviá-lo pela rede, portanto, deve ser convertido em Uint8Array.

Quando você coloca isso no Meteor.call, o Meteor executa automaticamente o EJSON.stringify(Uint8Array) e o envia com DDP. Você pode verificar os dados no tráfego do websocket do console chrome, você verá uma string parecida com base64

No lado do servidor, Meteor chama EJSON.parse() e o converte de volta para buffer

Prós
  1. Simples, sem truques, sem pacotes extras
  2. Atenha-se ao princípio Data on the Wire

Contras
  1. Mais largura de banda:a string base64 resultante é aproximadamente 33% maior que o arquivo original
  2. Limite de tamanho de arquivo:não é possível enviar arquivos grandes (limite ~ 16 MB?)
  3. Sem armazenamento em cache
  4. Nenhum gzip ou compactação ainda
  5. Ocupe muita memória se você publicar arquivos

Opção 2:XHR, postagem do cliente para o sistema de arquivos

/*** 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
});

Explicação

O arquivo no cliente é capturado, um objeto XHR é criado e o arquivo é enviado via 'POST' para o servidor.

No servidor, os dados são canalizados para um sistema de arquivos subjacente. Além disso, você pode determinar o nome do arquivo, realizar a higienização ou verificar se já existe, etc., antes de salvar.

Prós
  1. Aproveitando o XHR 2 para que você possa enviar arraybuffer, nenhum novo FileReader() é necessário em comparação com a opção 1
  2. Arraybuffer é menos volumoso em comparação com a string base64
  3. Sem limite de tamanho, enviei um arquivo ~ 200 MB no localhost sem problemas
  4. O sistema de arquivos é mais rápido que o mongodb (mais sobre isso mais adiante no benchmarking abaixo)
  5. Cachável e gzip

Contras
  1. XHR 2 não está disponível em navegadores mais antigos, por exemplo, abaixo do IE10, mas é claro que você pode implementar um post tradicional
    Eu só usei xhr =new XMLHttpRequest(), ao invés de HTTP.call('POST') porque o HTTP.call atual no Meteor ainda não é capaz de enviar arraybuffer (me aponte se estiver errado).
  2. /path/to/dir/ tem que estar fora do meteoro, caso contrário, escrever um arquivo em /public aciona um recarregamento

Opção 3:XHR, salve no 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);
});     

Explicação

O script do cliente é o mesmo da opção 2.

De acordo com a última linha do Meteor 1.0.x mongo_driver.js, um objeto global chamado MongoInternals é exposto, você pode chamar defaultRemoteCollectionDriver() para retornar o objeto db do banco de dados atual que é necessário para o GridStore. Na versão A, o GridStore também é exposto pelo MongoInternals. O mongo usado pelo meteoro atual é v1.4.x

Em seguida, dentro de uma rota, você pode criar um novo objeto de gravação chamando var file =new GridStore(...) (API). Em seguida, você abre o arquivo e cria um fluxo.

Eu também incluí uma versão B. Nesta versão, o GridStore é chamado usando um novo drive mongodb via Npm.require('mongodb'), este mongo é a última v2.0.13 até o momento. A nova API não exige que você abra o arquivo, você pode chamar stream(true) diretamente e iniciar a tubulação

Prós
  1. O mesmo que na opção 2, enviado usando arraybuffer, menos sobrecarga em comparação com a string base64 na opção 1
  2. Não há necessidade de se preocupar com a higienização do nome do arquivo
  3. Separação do sistema de arquivos, não há necessidade de gravar no diretório temporário, o db pode ser copiado, rep, shard etc
  4. Não é necessário implementar nenhum outro pacote
  5. Cachável e pode ser compactado em gzip
  6. Armazene tamanhos muito maiores em comparação com a coleção mongo normal
  7. Usando pipe para reduzir a sobrecarga de memória

Contras
  1. Mongo GridFS instável . Incluí a versão A (mongo 1.x) e B (mongo 2.x). Na versão A, ao canalizar arquivos grandes> 10 MB, recebi muitos erros, incluindo arquivo corrompido, pipe inacabado. Este problema foi resolvido na versão B usando o mongo 2.x, esperamos que o meteoro atualize para o mongodb 2.x em breve
  2. Confusão da API . Na versão A, você precisa abrir o arquivo antes de poder transmitir, mas na versão B, você pode transmitir sem chamar open. O documento da API também não é muito claro e o fluxo não é 100% de sintaxe intercambiável com Npm.require('fs'). Em fs, você chama file.on('finish'), mas em GridFS você chama file.on('end') ao escrever finish/ends.
  3. GridFS não fornece atomicidade de gravação, portanto, se houver várias gravações simultâneas no mesmo arquivo, o resultado final poderá ser muito diferente
  4. Velocidade . O Mongo GridFS é muito mais lento que o sistema de arquivos.

Referência Você pode ver na opção 2 e na opção 3, eu incluí var start =Date.now() e ao escrever end, eu console.log out a hora em ms , abaixo está o resultado. Dual Core, 4 GB de RAM, HDD, baseado no Ubuntu 14.04.
file size   GridFS  FS
100 KB      50      2
1 MB        400     30
10 MB       3500    100
200 MB      80000   1240

Você pode ver que o FS é muito mais rápido que o GridFS. Para um arquivo de 200 MB, leva ~ 80 segundos usando GridFS, mas apenas ~ 1 segundo em FS. Eu não tentei SSD, o resultado pode ser diferente. No entanto, na vida real, a largura de banda pode ditar a velocidade com que o arquivo é transmitido do cliente para o servidor, não é típico atingir uma velocidade de transferência de 200 MB/s. Por outro lado, uma velocidade de transferência de ~2 MB/seg (GridFS) é mais a norma.

Conclusão

Isso não é abrangente, mas você pode decidir qual opção é melhor para sua necessidade.
  • DDP é o mais simples e segue o princípio principal do Meteor, mas os dados são mais volumosos, não são compressíveis durante a transferência, não podem ser armazenados em cache. Mas essa opção pode ser boa se você precisar apenas de arquivos pequenos.
  • XHR acoplado com sistema de arquivos é a maneira 'tradicional'. API estável, rápida, 'streamable', compressível, armazenável em cache (ETag etc), mas precisa estar em uma pasta separada
  • XHR acoplado com GridFS , você obtém o benefício de rep set, escalável, sem tocar no diretório do sistema de arquivos, arquivos grandes e muitos arquivos se o sistema de arquivos restringir os números, também compactável em cache. No entanto, a API é instável, você recebe erros em várias gravações, é s..l..o..w..

Espero que em breve, o meteor DDP possa suportar gzip, cache etc e o GridFS possa ser mais rápido ...