web-dev-qa-db-fra.com

Meteor: téléchargement de fichiers du client vers la collection Mongo vs système de fichiers vs GridFS

Meteor est génial mais il manque des supports natifs pour le téléchargement de fichiers traditionnel. Il existe plusieurs options pour gérer le téléchargement de fichiers:

Depuis le client , les données peuvent être envoyées à l'aide de:

  • Meteor.call ('saveFile', données) ou collection.insert ({fichier: données}) 
  • Formulaire 'POST' ou HTTP.call ('POST')

Sur le serveur , le fichier peut être enregistré dans:

  • une collection de fichiers mongodb par collection.insert ({file: data})
  • système de fichiers dans/path/to/dir
  • mongodb GridFS

Quels sont les avantages et les inconvénients de ces méthodes et comment les appliquer au mieux? Je suis conscient qu'il existe également d'autres options telles que l'enregistrement sur un site tiers et l'obtention d'une URL.

35
Green

Meteor vous permet de télécharger des fichiers très simplement avec Meteor sans utiliser de paquet supplémentaire ni un tiers.

Option 1: DDP, enregistrer le fichier dans une collection 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})         
    }   
});

Explantion

Tout d'abord, le fichier est saisi à partir de l'entrée à l'aide de l'API de fichier HTML5. Un lecteur est créé en utilisant un nouveau FileReader. Le fichier est lu en tant que readAsArrayBuffer. Ce tampon de matrice, si vous console.log, renvoie {} et que DDP ne peut pas l'envoyer par câble, il doit donc être converti en Uint8Array.

Lorsque vous mettez cela dans Meteor.call, Meteor exécute automatiquement EJSON.stringify (Uint8Array) et l'envoie avec DDP. Vous pouvez vérifier les données dans le trafic websocket de la console chrome, vous verrez une chaîne ressemblant à base64

Meteor appelle EJSON.parse () côté serveur et le reconvertit en tampon 

Avantages

  1. Simple, pas de hacky, pas de paquets supplémentaires
  2. S'en tenir au principe des données sur le fil

Les inconvénients

  1. Plus de bande passante: la chaîne base64 résultante est environ 33% plus grande que le fichier d'origine
  2. Limite de taille de fichier: impossible d'envoyer de gros fichiers (limite ~ 16 Mo?)
  3. Pas de cache 
  4. Pas encore de compression ou de gzip
  5. Prenez beaucoup de mémoire si vous publiez des fichiers

Option 2: XHR, publier du client au système de fichiers 

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

Explication 

Le fichier dans le client est saisi, un objet XHR est créé et le fichier est envoyé via "POST" au serveur. 

Sur le serveur, les données sont acheminées dans un système de fichiers sous-jacent. Vous pouvez en outre déterminer le nom du fichier, effectuer une désinfection ou vérifier s'il existe déjà, etc. avant de l'enregistrer.

Avantages

  1. Tirant parti de XHR 2 pour pouvoir envoyer un tableau de mémoire tampon, aucun nouveau FileReader () n’est nécessaire par rapport à l’option 1.
  2. Arraybuffer est moins encombrant par rapport à la chaîne base64
  3. Aucune limite de taille, j'ai envoyé un fichier ~ 200 Mo dans localhost sans problème
  4. Le système de fichiers est plus rapide que mongodb (plus tard dans l'analyse comparative ci-dessous)
  5. Cachable et gzip

Les inconvénients

  1. XHR 2 n'est pas disponible dans les anciens navigateurs, par exemple. en dessous de IE10, mais bien sûr, vous pouvez implémenter une publication traditionnelle <form> J'ai uniquement utilisé xhr = new XMLHttpRequest (), plutôt que HTTP.call ('POST') car le HTTP.call actuel dans Meteor n'est pas encore en mesure d'envoyer un tampon de tableau (pointe moi si je me trompe).
  2. / path/to/dir/doit être en dehors de météore, sinon l'écriture d'un fichier dans/public déclenche un rechargement

Option 3: XHR, enregistrer dans 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);
});     

Explication 

Le script client est le même que dans l'option 2.

Selon Meteor 1.0.x mongo_driver.js last, un objet global appelé MongoInternals est exposé. Vous pouvez appeler defaultRemoteCollectionDriver () pour renvoyer l’objet de base de données actuel requis pour GridStore. Dans la version A, le GridStore est également exposé par les MongoInternals. Le mongo utilisé par le météore actuel est v1.4.x

Ensuite, à l'intérieur d'une route, vous pouvez créer un nouvel objet d'écriture en appelant var file = new GridStore (...) ( API ). Vous ouvrez ensuite le fichier et créez un flux.

J'ai également inclus une version B. Dans cette version, le GridStore est appelé à l'aide d'un nouveau lecteur mongodb via Npm.require ('mongodb'), ce mongo est la dernière version v2.0.13 en date de cette écriture. Le nouveau API ne vous oblige pas à ouvrir le fichier, vous pouvez appeler stream (true) directement et démarrer la tuyauterie

Avantages

  1. Identique à l'option 2, envoyée à l'aide de arraybuffer, moins de temps système que la chaîne base64 de l'option 1
  2. Pas besoin de s'inquiéter de la désinfection des noms de fichiers
  3. Séparation du système de fichiers, pas besoin d’écrire dans un répertoire temporaire, la base de données peut être sauvegardée, rep, shard, etc.
  4. Pas besoin d'implémenter un autre paquet
  5. Cachable et peut être gzippé
  6. Stockez des tailles beaucoup plus grandes par rapport à la collection de mongos normale
  7. Utilisation du canal pour réduire la surcharge de mémoire 

Les inconvénients

  1. Unstable Mongo GridFS. J'ai inclus la version A (mongo 1.x) et B (mongo 2.x). Dans la version A, lors de la création de gros fichiers> 10 Mo, de nombreuses erreurs ont été commises, y compris un fichier corrompu et un tuyau inachevé. Ce problème est résolu dans la version B avec mongo 2.x, nous espérons que meteor passera bientôt à mongodb 2.x
  2. Confusion d'API. Dans la version A, vous devez ouvrir le fichier avant de pouvoir diffuser, mais dans la version B, vous pouvez diffuser sans appeler open. La documentation de l'API n'est pas très claire non plus et le flux de la syntaxe n'est pas 100% interchangeable avec Npm.require ('fs'). Dans fs, vous appelez file.on ('finish') mais dans GridFS, vous appelez file.on ('end') lors de l'écriture des finitions/fins.
  3. GridFS ne fournit pas d'atomicité d'écriture. Ainsi, s'il existe plusieurs écritures simultanées dans le même fichier, le résultat final peut être très différent.
  4. La vitesse. Mongo GridFS est beaucoup plus lent que le système de fichiers.

Benchmark Vous pouvez voir dans les options 2 et 3 que j’ai inclus var start = Date.now () et lors de l’écriture de fin, je console.log le temps passé dans ms, en-dessous est le résultat. Dual Core, 4 Go de RAM, disque dur, basé sur Ubuntu 14.04.file size GridFS FS 100 KB 50 2 1 MB 400 30 10 MB 3500 100 200 MB 80000 1240

Conclusion.

En aucun cas, ceci est complet, mais vous pouvez décider quelle option est la mieux adaptée à vos besoins.

.

  • _ {XHR couplé avec le système de fichiers} _ est la méthode 'traditionnelle'. API stable, rapide, 'streamable', compressible, cachable (ETag, etc.), mais doit être dans un dossier séparé.
  • XHR couplé avec GridFS, vous bénéficiez des avantages suivants: rep rep, système évolutif, répertoire du système de fichiers ne touchant pas, fichiers volumineux et plusieurs fichiers si le système de fichiers limite le nombre, également compressible. Cependant, l'API est instable, vous obtenez des erreurs dans plusieurs écritures, c'est s..l..o..w ..
  • Heureusement, météore DDP peut prendre en charge gzip, la mise en cache, etc. et GridFS peut être plus rapide...

Hopefully soon, meteor DDP can support gzip, caching etc and GridFS can be faster...

72
Green

Bonjour, juste pour ajouter à Option1 concernant la visualisation du fichier. Je l'ai fait sans ejson.

<template name='tryUpload'>
  <p>Choose file to upload</p>
  <input name="upload" class='fileupload' type='file'>
</template>

Template.tryUpload.events({
'change .fileupload':function(event,template){
console.log('change & view');
var f = event.target.files[0];//assuming upload 1 file only
if(!f) return;
var r = new FileReader();
r.onload=function(event){
  var buffer = new Uint8Array(r.result);//convert to binary
  for (var i = 0, strLen = r.length; i < strLen; i++){
    buffer[i] = r.charCodeAt(i);
  }
  var toString = String.fromCharCode.apply(null, buffer );
  console.log(toString);
  //Meteor.call('saveFiles',buffer);
}
r.readAsArrayBuffer(f);};
0
bobobobooo