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
- Simples, sem truques, sem pacotes extras
- Atenha-se ao princípio Data on the Wire
Contras
- Mais largura de banda:a string base64 resultante é aproximadamente 33% maior que o arquivo original
- Limite de tamanho de arquivo:não é possível enviar arquivos grandes (limite ~ 16 MB?)
- Sem armazenamento em cache
- Nenhum gzip ou compactação ainda
- 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
- Aproveitando o XHR 2 para que você possa enviar arraybuffer, nenhum novo FileReader() é necessário em comparação com a opção 1
- Arraybuffer é menos volumoso em comparação com a string base64
- Sem limite de tamanho, enviei um arquivo ~ 200 MB no localhost sem problemas
- O sistema de arquivos é mais rápido que o mongodb (mais sobre isso mais adiante no benchmarking abaixo)
- Cachável e gzip
Contras
- 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
- /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
- O mesmo que na opção 2, enviado usando arraybuffer, menos sobrecarga em comparação com a string base64 na opção 1
- Não há necessidade de se preocupar com a higienização do nome do arquivo
- 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
- Não é necessário implementar nenhum outro pacote
- Cachável e pode ser compactado em gzip
- Armazene tamanhos muito maiores em comparação com a coleção mongo normal
- Usando pipe para reduzir a sobrecarga de memória
Contras
- 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
- 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.
- 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
- 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 ...