MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Meteor:caricamento di file dal client nella raccolta Mongo rispetto al file system rispetto a GridFS

Puoi eseguire il caricamento di file con Meteor senza utilizzare altri pacchetti o terze parti

Opzione 1:DDP, salvataggio del file in una raccolta 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})         
    }   
});

Spiegazione

Innanzitutto, il file viene prelevato dall'input utilizzando l'API file HTML5. Un lettore viene creato utilizzando il nuovo FileReader. Il file viene letto come readAsArrayBuffer. Questo arraybuffer, se console.log, restituisce {} e DDP non può inviarlo via cavo, quindi deve essere convertito in Uint8Array.

Quando lo inserisci in Meteor.call, Meteor esegue automaticamente EJSON.stringify(Uint8Array) e lo invia con DDP. Puoi controllare i dati nel traffico websocket della console di Chrome, vedrai una stringa simile a base64

Sul lato server, Meteor chiama EJSON.parse() e lo converte in buffer

Pro

  1. Semplice, nessun modo hacky, nessun pacchetto extra
  2. Attenersi al principio Data on the Wire

Contro

  1. Più larghezza di banda:la stringa base64 risultante è circa il 33% più grande del file originale
  2. Limite dimensione file:impossibile inviare file di grandi dimensioni (limite ~ 16 MB?)
  3. Nessuna memorizzazione nella cache
  4. Niente gzip o compressione ancora
  5. Occupa molta memoria se pubblichi file

Opzione 2:XHR, posta dal client al file system

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

Spiegazione

Il file nel client viene prelevato, viene creato un oggetto XHR e il file viene inviato tramite 'POST' al server.

Sul server, i dati vengono convogliati in un file system sottostante. Puoi inoltre determinare il nome del file, eseguire la sanificazione o verificare se esiste già ecc. prima di salvare.

Pro

  1. Sfruttando XHR 2 in modo da poter inviare arraybuffer, non è necessario un nuovo FileReader() rispetto all'opzione 1
  2. L'arraybuffer è meno ingombrante rispetto alla stringa base64
  3. Nessun limite di dimensione, ho inviato un file di ~ 200 MB in localhost senza problemi
  4. Il file system è più veloce di mongodb (ne parleremo più avanti nel benchmarking di seguito)
  5. Cachable e gzip

Contro

  1. XHR 2 non è disponibile nei browser meno recenti, ad es. sotto IE10, ma ovviamente puoi implementare un post tradizionale
    Ho usato solo xhr =new XMLHttpRequest(), piuttosto che HTTP.call('POST') perché l'attuale HTTP.call in Meteor non è ancora in grado di inviare arraybuffer (indicami se sbaglio).
  2. /path/to/dir/ deve essere esterno a meteor, altrimenti scrivere un file in /public attiva un ricaricamento

Opzione 3:XHR, salva su 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);
});     

Spiegazione

Lo script client è lo stesso dell'opzione 2.

Secondo l'ultima riga di Meteor 1.0.x mongo_driver.js, viene esposto un oggetto globale chiamato MongoInternals, puoi chiamare defaultRemoteCollectionDriver() per restituire l'oggetto db del database corrente che è richiesto per GridStore. Nella versione A, il GridStore è esposto anche da MongoInternals. Il mongo utilizzato dall'attuale meteorite è v1.4.x

Quindi all'interno di una route, puoi creare un nuovo oggetto di scrittura chiamando var file =new GridStore(...) (API). Quindi apri il file e crei uno stream.

Ho incluso anche una versione B. In questa versione, GridStore viene chiamato utilizzando una nuova unità mongodb tramite Npm.require('mongodb'), questo mongo è l'ultima v2.0.13 al momento della stesura di questo articolo. La nuova API non richiede l'apertura del file, puoi chiamare stream(true) direttamente e avviare il piping

Pro

  1. Come nell'opzione 2, inviato utilizzando arraybuffer, meno sovraccarico rispetto alla stringa base64 nell'opzione 1
  2. Non c'è bisogno di preoccuparsi della sanificazione del nome file
  3. Separazione dal file system, non è necessario scrivere nella directory temporanea, è possibile eseguire il backup del db, rep, shard ecc
  4. Non c'è bisogno di implementare nessun altro pacchetto
  5. Cachable e può essere compresso con gzip
  6. Conserva taglie molto più grandi rispetto alla normale collezione mongo
  7. Utilizzo di pipe per ridurre il sovraccarico di memoria

Contro

  1. GridFS Mongo instabile . Ho incluso la versione A (mongo 1.x) e B (mongo 2.x). Nella versione A, durante il pipettaggio di file di grandi dimensioni> 10 MB, ho ricevuto molti errori, inclusi file danneggiato, pipe non terminate. Questo problema è stato risolto nella versione B utilizzando mongo 2.x, si spera che meteor venga aggiornato presto a mongodb 2.x
  2. Confusione API . Nella versione A, è necessario aprire il file prima di poter eseguire lo streaming, ma nella versione B è possibile eseguire lo streaming senza chiamare open. Anche il documento API non è molto chiaro e il flusso non è scambiabile al 100% con la sintassi con Npm.require('fs'). In fs, chiami file.on('finish') ma in GridFS chiami file.on('end') quando scrivi finish/ends.
  3. GridFS non fornisce l'atomicità di scrittura, quindi se ci sono più scritture simultanee sullo stesso file, il risultato finale potrebbe essere molto diverso
  4. Velocità . Mongo GridFS è molto più lento del file system.

Parametro Puoi vedere nell'opzione 2 e nell'opzione 3, ho incluso var start =Date.now() e quando scrivo end, ho console.log fuori l'ora in ms , di seguito il risultato. Dual Core, 4 GB di RAM, HDD, basato su Ubuntu 14.04.

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

Puoi vedere che FS è molto più veloce di GridFS. Per un file di 200 MB, sono necessari circa 80 secondi con GridFS, ma solo ~ 1 secondo con FS. Non ho provato SSD, il risultato potrebbe essere diverso. Tuttavia, nella vita reale, la larghezza di banda può determinare la velocità con cui il file viene trasmesso in streaming dal client al server, raggiungere una velocità di trasferimento di 200 MB/sec non è tipico. D'altra parte, una velocità di trasferimento di ~2 MB/sec (GridFS) è più la norma.

Conclusione

Questo non è affatto completo, ma puoi decidere quale opzione è la migliore per le tue esigenze.

  • DDP è il più semplice e si attiene al principio di base di Meteor, ma i dati sono più ingombranti, non comprimibili durante il trasferimento, non memorizzabili nella cache. Ma questa opzione potrebbe essere utile se hai bisogno solo di piccoli file.
  • XHR accoppiato con file system è il modo 'tradizionale'. API stabile, veloce, 'streamable', comprimibile, memorizzabile nella cache (ETag ecc.), ma deve trovarsi in una cartella separata
  • XHR accoppiato con GridFS , ottieni il vantaggio di rep set, scalabile, senza toccare la directory del file system, file di grandi dimensioni e molti file se il file system limita i numeri, anche comprimibile nella cache. Tuttavia, l'API è instabile, ottieni errori in più scritture, è s..l..o..w..

Si spera che presto meteor DDP possa supportare gzip, caching ecc. e GridFS possa essere più veloce ...