Meteor: загрузка файла из клиента в коллекцию Mongo vs file system vs GridFS

Meteor велик, но ему не хватает родной поддержки для традиционной загрузки файлов. Существует несколько вариантов обработки загрузки файлов:

от клиента, данные могут быть отправлены через:

  • Метеор.вызов ('saveFile', data)или сбор.insert ({file:data})
  • форма "POST" или HTTP.call ('POST')

на сервере, то файл можно сохранить в:

  • коллекция файлов mongodb коллекция.insert ({file:data})
  • файловая система в /path/to / dir
  • в MongoDB GridFS

каковы плюсы и минусы этих методов и как лучше их реализовать? Я знаю, что есть и другие варианты, такие как сохранение на сторонний сайт и получение url-адреса.

2 ответов


вы можете добиться загрузки файлов довольно просто с Meteor без использования каких-либо пакетов или третьей стороны

Вариант 1: DDP, сохранение файла в коллекцию 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

во-первых, файл захватывается из входных данных с помощью HTML5 File API. Читатель создается с помощью нового FileReader. Файл читается как readAsArrayBuffer. Это arraybuffer, если вы утешаете.log, returns {} и DDP не могут отправить это по проводу, поэтому он имеет для преобразования в Uint8Array.

когда вы положили это в Метеор.вызов, Meteor автоматически запускает EJSON.stringify (Uint8Array) и отправляет его с DDP. Вы можете проверить данные в Chrome console websocket traffic, вы увидите строку, напоминающую base64

на стороне сервера, Метеор вызов EJSON.parse () и преобразует его обратно в buffer

плюсы

  1. простой, не хакерский способ, никаких дополнительных пакетов
  2. придерживаться данных по принципу провода

минусы

  1. больше пропускной способности: результирующая строка base64 ~ 33% больше, чем исходный файл
  2. ограничение размера файла: не удается отправить большие файлы (ограничение ~ 16 МБ?)
  3. нет кэширования
  4. нет gzip или сжатия еще
  5. занимают много памяти, если вы публикуете файлы

Вариант 2: XHR, сообщение от клиента к файловой системе

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

объяснение

файл в клиенте захватывается, создается объект XHR и файл отправляется через "POST" на сервер.

на сервере данные передаются в базовую файловую систему. Вы можете дополнительно определить имя файла, выполнить очистку или проверить, существует ли оно уже и т. д. перед сохранением.

плюсы

  1. воспользовавшись XHR 2, Вы можете отправить arraybuffer, новый FileReader () не требуется по сравнению с вариантом 1
  2. Arraybuffer менее громоздкий по сравнению с base64 string
  3. без ограничения размера, я отправил файл ~ 200 МБ в localhost без проблем
  4. файловая система работает быстрее, чем mongodb (подробнее об этом ниже в бенчмаркинге)
  5. Cachable и gzip

минусы

  1. XHR 2 недоступен в старых браузерах, например, ниже IE10, но конечно, вы можете реализовать традиционный post
    я использовал только xhr = new XMLHttpRequest (), а не HTTP.вызов ('POST'), потому что текущий HTTP.вызов в Meteor еще не может отправить arraybuffer (укажите мне, если я ошибаюсь).
  2. / path/to/ dir /должен быть вне meteor, иначе запись файла в / public запускает перезагрузку

Вариант 3: XHR, сохранить в 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);
});     

объяснение

клиентский скрипт то же, что и в Варианте 2.

согласно Метеору 1.0.x mongo_driver.js в последней строке отображается глобальный объект MongoInternals, вы можете вызвать defaultRemoteCollectionDriver (), чтобы вернуть текущий объект базы данных db, который требуется для GridStore. В версии A GridStore также предоставляется MongoInternals. Монго используется текущая Метеор В1.4.x

затем внутри маршрута вы можете создать новый объект записи, вызвав var file = new GridStore(...) (API). Затем откройте файл и создайте поток.

Я также включил версию B. В этой версии GridStore вызывается с помощью нового диска mongodb через Npm.require ('mongodb'), этот монго является последним v2.0.13 на момент написания этой книги. Новый API не требует, чтобы вы открыли файл, вы можете вызвать stream(true) напрямую и начать трубопровод

плюсы

  1. то же, что и в Варианте 2, отправлено с помощью arraybuffer, меньше накладных расходов по сравнению со строкой base64 в опции 1
  2. не нужно беспокоиться о дезинфекции имени файла
  3. отделение от файловой системы, нет необходимости писать в temp dir, БД можно создать резервную копию, rep, shard и т. д.
  4. нет необходимости реализовывать какой-либо другой пакет
  5. Cachable и может быть gzipped
  6. магазин гораздо больших размеров по сравнению с обычной коллекцией mongo
  7. используя трубу для уменьшения перегрузки памяти

минусы

  1. Нестабильная Mongo GridFS. Я включил версию A (mongo 1.x) и B (mongo 2.икс.) В версии A, когда конвейерные большие файлы > 10 MB, я получил много ошибок, включая поврежденный файл, незавершенную трубу. Эта проблема решена в версии B с помощью mongo 2.x, надеюсь, meteor обновится до mongodb 2.х только
  2. API путаница. В версии, вам нужно открыть файл, прежде чем вы можете stream, но в версии B вы можете передавать без вызова open. Документ API также не очень понятен, и поток не является синтаксисом 100%, обмениваемым с Npm.require('fs'). В fs вы вызываете файл.on ('finish'), но в GridFS вы вызываете файл.on ('end') при завершении/завершении записи.
  3. GridFS не обеспечивает атомарность записи, поэтому, если есть несколько одновременных записей в один и тот же файл, конечный результат может быть очень разным
  4. скорость. Монго GridFS много медленнее, чем файловая система.

Benchmark Вы можете видеть в опции 2 и опции 3, я включил var start = Date.теперь () и когда запись заканчивается, я утешаю.выйдите из системы время в ms, ниже результат. Двухъядерный, 4 ГБ оперативной памяти, HDD, ubuntu 14.04.

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

вы можете видеть, что FS намного быстрее, чем GridFS. Для файла 200 МБ требуется ~ 80 сек с использованием GridFS, но только ~ 1 сек в FS. Я не пробовал SSD, результат может быть другим. Однако в реальной жизни пропускная способность может диктовать, как быстро файл передается от клиента к серверу, достижение скорости передачи 200 МБ/сек не является типичным. С другой стороны, скорость передачи ~2 МБ/сек (GridFS) является более нормой.

вывод

ни в коем случае это не является всеобъемлющим, но вы можете решить, какой вариант лучше всего подходит для ваших нужд.

  • DDP является самым простым и придерживается принципа ядра Метеор, но данные более громоздкие, несжимаемые во время передачи, недоступные. Но эта опция может быть хорошей, если вам нужны только небольшие файлы.
  • XHR в сочетании с файловой системой - это "традиционный" способ. Стабильный API, быстрый, "потоковый", сжимаемый, доступный (ETag и т. д.), Но должен быть в отдельной папке
  • XHR в сочетании с GridFS, вы получаете преимущество набора rep, масштабируемого, не касаясь файловой системы dir, больших файлов и многих файлов, если файл система ограничивает номера, также cachable compressible. Однако API нестабилен, вы получаете ошибки в нескольких записях, это s..л..о..Вт..

надеюсь, скоро, meteor DDP может поддерживать gzip, кэширование и т. д. И GridFS могут быть быстрее...


Привет, чтобы добавить в Option1 относительно просмотра файла. Я сделал это без 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);};