sql >> Databasteknik >  >> NoSQL >> MongoDB

Meteor:ladda upp fil från klient till Mongo-samling vs filsystem vs GridFS

Du kan ladda upp filer med Meteor utan att använda fler paket eller en tredje part

Alternativ 1:DDP, spara fil till en mongosamling

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

Förklaring

Först hämtas filen från ingången med HTML5 File API. En läsare skapas med den nya FileReader. Filen läses som readAsArrayBuffer. Denna arraybuffer, om du console.log, returnerar {} och DDP kan inte skicka detta över tråden, så den måste konverteras till Uint8Array.

När du lägger detta i Meteor.call kör Meteor automatiskt EJSON.stringify(Uint8Array) och skickar det med DDP. Du kan kontrollera data i Chrome-konsolens websocket-trafik, du kommer att se en sträng som liknar base64

På serversidan anropar Meteor EJSON.parse() och konverterar den tillbaka till buffert

Proffs

  1. Enkelt, inget hackigt sätt, inga extra paket
  2. Håll dig till Data on the Wire-principen

Nackdelar

  1. Mer bandbredd:den resulterande base64-strängen är ~33 % större än originalfilen
  2. Filstorleksgräns:kan inte skicka stora filer (gräns ~ 16 MB?)
  3. Ingen cachning
  4. Ingen gzip eller komprimering ännu
  5. Ta upp mycket minne om du publicerar filer

Alternativ 2:XHR, post från klient till filsystem

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

Förklaring

Filen i klienten grips, ett XHR-objekt skapas och filen skickas via 'POST' till servern.

På servern leds data in i ett underliggande filsystem. Du kan dessutom bestämma filnamnet, utföra sanering eller kontrollera om det redan finns etc innan du sparar.

Proffs

  1. Genom att dra nytta av XHR 2 så att du kan skicka arraybuffer, behövs ingen ny FileReader() jämfört med alternativ 1
  2. Arraybuffer är mindre skrymmande jämfört med base64-strängen
  3. Ingen storleksbegränsning, jag skickade en fil ~ 200 MB i localhost utan problem
  4. Filsystemet är snabbare än mongodb (mer om detta senare i benchmarking nedan)
  5. Cachbar och gzip

Nackdelar

  1. XHR 2 är inte tillgängligt i äldre webbläsare, t.ex. under IE10, men naturligtvis kan du implementera ett traditionellt inlägg
    Jag använde bara xhr =new XMLHttpRequest(), snarare än HTTP.call('POST') eftersom den nuvarande HTTP.call i Meteor ännu inte kan skicka arraybuffer (visa mig om jag har fel).
  2. /path/to/dir/ måste vara utanför meteor, annars utlöser skrivning av en fil i /public en omladdning

Alternativ 3:XHR, spara till 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);
});     

Förklaring

Klientskriptet är detsamma som i alternativ 2.

Enligt Meteor 1.0.x mongo_driver.js sista raden är ett globalt objekt som heter MongoInternals exponerat, du kan anropa defaultRemoteCollectionDriver() för att returnera det aktuella databasen db-objekt som krävs för GridStore. I version A exponeras GridStore också av MongoInternals. Mongon som används av nuvarande meteor är v1.4.x

Sedan i en rutt kan du skapa ett nytt skrivobjekt genom att anropa var file =new GridStore(...) (API). Du öppnar sedan filen och skapar en stream.

Jag inkluderade också en version B. I den här versionen kallas GridStore för att använda en ny mongodb-enhet via Npm.require('mongodb'), denna mongo är den senaste v2.0.13 när detta skrivs. Det nya API:et kräver inte att du öppnar filen, du kan anropa stream(true) direkt och börja pipa

Proffs

  1. Samma som i alternativ 2, skickad med arraybuffer, mindre overhead jämfört med base64-sträng i alternativ 1
  2. Du behöver inte oroa dig för sanering av filnamn
  3. Separation från filsystemet, inget behov av att skriva till temp dir, db kan säkerhetskopieras, rep, shard etc.
  4. Inget behov av att implementera något annat paket
  5. Cachbar och kan gzippas
  6. Förvara mycket större storlekar jämfört med vanliga mongokollektioner
  7. Användning av pipe för att minska minnesöverbelastning

Nackdelar

  1. Instabil Mongo GridFS . Jag inkluderade version A (mongo 1.x) och B (mongo 2.x). I version A, när jag skickade stora filer> 10 MB, fick jag massor av fel, inklusive skadad fil, oavslutad pipe. Det här problemet är löst i version B med mongo 2.x, förhoppningsvis kommer meteor att uppgradera till mongodb 2.x snart
  2. API-förvirring . I version A måste du öppna filen innan du kan streama, men i version B kan du streama utan att anropa öppen. API-dokumentet är inte heller särskilt tydligt och strömmen är inte 100% syntax utbytbar med Npm.require('fs'). I fs anropar du file.on('finish') men i GridFS anropar du file.on('end') när skrivningen avslutas/slutar.
  3. GridFS tillhandahåller inte skrivatomicitet, så om det finns flera samtidiga skrivningar till samma fil kan slutresultatet bli väldigt olika
  4. Hastighet . Mongo GridFS är mycket långsammare än filsystemet.

Benchmark Du kan se i alternativ 2 och alternativ 3, jag inkluderade var start =Date.now() och när jag skriver slut loggar jag ut tiden i ms , nedan är resultatet. Dual Core, 4 GB ram, hårddisk, ubuntu 14.04-baserad.

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

Du kan se att FS är mycket snabbare än GridFS. För en fil på 200 MB tar det ~80 sek med GridFS men bara ~ 1 sek i FS. Jag har inte provat SSD, resultatet kan bli annorlunda. Men i verkliga livet kan bandbredden diktera hur snabbt filen strömmas från klient till server, att uppnå 200 MB/sek överföringshastighet är inte typiskt. Å andra sidan är en överföringshastighet ~2 MB/sek (GridFS) mer normen.

Slutsats

Detta är inte på något sätt heltäckande, men du kan bestämma vilket alternativ som är bäst för ditt behov.

  • DDP är den enklaste och håller sig till kärnprincipen Meteor men data är mer skrymmande, inte komprimerbar under överföring, inte cachbar. Men det här alternativet kan vara bra om du bara behöver små filer.
  • XHR i kombination med filsystem är det "traditionella" sättet. Stabilt API, snabbt, "streambart", komprimerbart, cachbart (ETag etc), men måste finnas i en separat mapp
  • XHR kopplat med GridFS , du får fördelen av repset, skalbar, ingen beröring filsystem dir, stora filer och många filer om filsystemet begränsar siffrorna, även cachbart komprimerbart. Men API:et är instabilt, du får fel i flera skrivningar, det är s..l..o..w..

Förhoppningsvis snart kan meteor DDP stödja gzip, cachning etc och GridFS kan vara snabbare ...



  1. Hur ökar man ett fält i mongodb?

  2. Hur man säkrar MongoDB med användarnamn och lösenord

  3. Konfigurera redis för att konsekvent avhysa äldre data först

  4. Mongodb $där frågan alltid är sann med nodejs