MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Meteor:Hochladen einer Datei vom Client in die Mongo-Sammlung vs. Dateisystem vs. GridFS

Sie können Dateien mit Meteor hochladen, ohne weitere Pakete oder einen Drittanbieter zu verwenden

Option 1:DDP, Datei in einer Mongo-Sammlung speichern

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

Erklärung

Zunächst wird die Datei mithilfe der HTML5-Datei-API aus der Eingabe abgerufen. Ein Reader wird mit dem neuen FileReader erstellt. Die Datei wird als readAsArrayBuffer gelesen. Dieser Arraybuffer gibt, wenn Sie console.log verwenden, {} zurück und DDP kann dies nicht über die Leitung senden, also muss es in Uint8Array konvertiert werden.

Wenn Sie dies in Meteor.call einfügen, führt Meteor automatisch EJSON.stringify(Uint8Array) aus und sendet es mit DDP. Sie können die Daten im Websocket-Traffic der Chrome-Konsole überprüfen. Sie sehen eine Zeichenfolge, die base64 ähnelt

Auf der Serverseite ruft Meteor EJSON.parse() auf und konvertiert es zurück in den Puffer

Vorteile

  1. Einfach, keine Tricks, keine zusätzlichen Pakete
  2. Halten Sie sich an das Data-on-the-Wire-Prinzip

Nachteile

  1. Mehr Bandbreite:Die resultierende base64-Zeichenfolge ist ~ 33 % größer als die Originaldatei
  2. Dateigrößenbeschränkung:Große Dateien können nicht gesendet werden (Limit ~ 16 MB?)
  3. Kein Caching
  4. Noch kein gzip oder Komprimierung
  5. Nehmen viel Speicherplatz in Anspruch, wenn Sie Dateien veröffentlichen

Option 2:XHR, Post vom Client zum Dateisystem

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

Erklärung

Die Datei im Client wird gegrabbt, ein XHR-Objekt erstellt und die Datei per 'POST' an den Server gesendet.

Auf dem Server werden die Daten in ein zugrunde liegendes Dateisystem geleitet. Sie können zusätzlich den Dateinamen bestimmen, eine Bereinigung durchführen oder vor dem Speichern prüfen, ob er bereits existiert etc.

Vorteile

  1. Wenn Sie XHR 2 nutzen, um Arraybuffer zu senden, ist im Vergleich zu Option 1 kein neuer FileReader() erforderlich
  2. Arraybuffer ist weniger sperrig im Vergleich zu base64-Strings
  3. Keine Größenbeschränkung, ich habe eine Datei von ~ 200 MB in localhost ohne Probleme gesendet
  4. Dateisystem ist schneller als mongodb (mehr dazu später im Benchmarking unten)
  5. Zwischenspeicherbar und gzip

Nachteile

  1. XHR 2 ist in älteren Browsern nicht verfügbar, z. unter IE10, aber natürlich können Sie ein traditionelles post
    implementieren. Ich habe nur xhr =new XMLHttpRequest() anstelle von HTTP.call('POST') verwendet, da der aktuelle HTTP.call in Meteor noch nicht in der Lage ist, Arraybuffer zu senden (Zeigen Sie mir, wenn ich falsch liege).
  2. /path/to/dir/ muss außerhalb von meteor liegen, sonst löst das Schreiben einer Datei in /public einen Reload aus

Option 3:XHR, in GridFS speichern

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

Erklärung

Das Clientskript ist dasselbe wie in Option 2.

Laut der letzten Zeile von Meteor 1.0.x mongo_driver.j wird ein globales Objekt namens MongoInternals bereitgestellt. Sie können defaultRemoteCollectionDriver() aufrufen, um das aktuelle Datenbank-DB-Objekt zurückzugeben, das für den GridStore erforderlich ist. In Version A wird der GridStore auch von MongoInternals verfügbar gemacht. Der vom aktuellen Meteor verwendete Mongo ist v1.4.x

Dann können Sie innerhalb einer Route ein neues Schreibobjekt erstellen, indem Sie var file =new GridStore(...) (API) aufrufen. Anschließend öffnen Sie die Datei und erstellen einen Stream.

Ich habe auch eine Version B hinzugefügt. In dieser Version wird der GridStore mit einem neuen Mongodb-Laufwerk über Npm.require('mongodb') aufgerufen, dieser Mongo ist zum Zeitpunkt des Schreibens die neueste Version 2.0.13. Die neue API erfordert nicht, dass Sie die Datei öffnen, Sie können stream(true) direkt aufrufen und mit dem Piping beginnen

Vorteile

  1. Wie in Option 2, gesendet mit Arraybuffer, weniger Overhead im Vergleich zum base64-String in Option 1
  2. Sie müssen sich keine Gedanken über die Bereinigung von Dateinamen machen
  3. Trennung vom Dateisystem, keine Notwendigkeit, in das Temp-Verzeichnis zu schreiben, die Datenbank kann gesichert werden, Rep, Shard usw.
  4. Keine Notwendigkeit, ein anderes Paket zu implementieren
  5. Zwischenspeicherbar und gzippbar
  6. Bewahren Sie im Vergleich zur normalen Mongo-Kollektion viel größere Größen auf
  7. Verwendung von Pipe zur Reduzierung der Speicherüberlastung

Nachteile

  1. Instabiles Mongo GridFS . Ich habe Version A (Mongo 1.x) und B (Mongo 2.x) hinzugefügt. In Version A bekam ich beim Pipen von großen Dateien> 10 MB viele Fehler, einschließlich beschädigter Dateien, unfertiger Pipes. Dieses Problem wurde in Version B mit Mongo 2.x gelöst, hoffentlich wird Meteor bald auf Mongodb 2.x aktualisieren
  2. API-Verwirrung . In Version A müssen Sie die Datei öffnen, bevor Sie streamen können, aber in Version B können Sie streamen, ohne open aufzurufen. Das API-Dokument ist auch nicht sehr klar und der Stream ist nicht zu 100% syntaktisch mit Npm.require('fs') austauschbar. In fs rufen Sie file.on('finish') auf, aber in GridFS rufen Sie file.on('end') auf, wenn das Schreiben beendet/endet.
  3. GridFS bietet keine Schreibatomarität. Wenn also mehrere gleichzeitige Schreibvorgänge in dieselbe Datei erfolgen, kann das Endergebnis sehr unterschiedlich sein
  4. Geschwindigkeit . Mongo GridFS ist viel langsamer als das Dateisystem.

Benchmark Sie können in Option 2 und Option 3 sehen, dass ich var start =Date.now() eingefügt habe und beim Schreiben von end, ich console.logout die Zeit in ms , unten ist das Ergebnis. Dual Core, 4 GB RAM, HDD, basierend auf Ubuntu 14.04.

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

Sie können sehen, dass FS viel schneller ist als GridFS. Für eine Datei von 200 MB dauert es ~ 80 Sekunden mit GridFS, aber nur ~ 1 Sekunde in FS. Ich habe SSD nicht ausprobiert, das Ergebnis kann anders sein. Im wirklichen Leben kann die Bandbreite jedoch bestimmen, wie schnell die Datei vom Client zum Server gestreamt wird, das Erreichen einer Übertragungsgeschwindigkeit von 200 MB/s ist nicht typisch. Andererseits ist eine Übertragungsgeschwindigkeit von ~2 MB/s (GridFS) eher die Norm.

Fazit

Dies ist keineswegs umfassend, aber Sie können entscheiden, welche Option für Ihre Anforderungen am besten geeignet ist.

  • DDP ist das einfachste und hält sich an das grundlegende Meteor-Prinzip, aber die Daten sind umfangreicher, während der Übertragung nicht komprimierbar und nicht zwischenspeicherbar. Aber diese Option kann gut sein, wenn Sie nur kleine Dateien benötigen.
  • XHR mit Dateisystem gekoppelt ist der "traditionelle" Weg. Stabile API, schnell, „streambar“, komprimierbar, zwischenspeicherbar (ETag usw.), muss sich aber in einem separaten Ordner befinden
  • XHR gekoppelt mit GridFS , Sie erhalten den Vorteil eines Rep-Sets, skalierbar, kein berührendes Dateisystemverzeichnis, große Dateien und viele Dateien, wenn das Dateisystem die Anzahl einschränkt, auch komprimierbar im Cache. Die API ist jedoch instabil, Sie erhalten Fehler bei mehreren Schreibvorgängen, es ist s..l..o..w..

Hoffentlich kann Meteor DDP bald gzip, Caching usw. unterstützen und GridFS kann schneller sein ...