Подтвердить что ты не робот

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

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

От клиента данные можно отправить с помощью:

  • Meteor.call('saveFile', data) или collection.insert({file: data})
  • 'POST' или HTTP.call('POST')

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

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

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

4b9b3361

Ответ 1

Вы можете легко загружать файлы с помощью 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

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

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

На стороне сервера Meteor вызывает EJSON.parse() и преобразует его обратно в буфер

Pros

  • Простой, без взлома, без дополнительных пакетов
  • Придерживайтесь данных о принципе проводки.

против

  • Больше полосы пропускания: итоговая base64-строка на ~ 33% больше, чем исходный файл
  • Ограничение размера файла: невозможно отправить большие файлы (ограничение ~ 16 МБ?)
  • Нет кеширования
  • Отсутствует gzip или сжатие
  • Возьмите много памяти, если вы публикуете файлы

Вариант 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" на сервер.

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

Pros

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

против

  • XHR 2 недоступен в старых браузерах, например. ниже IE10, но, конечно, вы можете реализовать традиционную запись <form> Я использовал xhr = new XMLHttpRequest(), а не HTTP.call('POST'), потому что текущий HTTP.call в Meteor еще не может отправить arraybuffer (укажите мне, если я ошибаюсь).
  • /path/to/dir/должен быть вне метеор, иначе запись файла в /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);
});     

Объяснение

Клиент script такой же, как в опции 2.

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

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

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

Pros

  • То же, что и в варианте 2, отправленном с использованием arraybuffer, меньше накладных расходов по сравнению с base64 string в опции 1
  • Не нужно беспокоиться о том, что имя файла называется sanitisation
  • Разделение из файловой системы, нет необходимости писать в temp dir, db может быть скопирован, rep, shard и т.д.
  • Не нужно внедрять какой-либо другой пакет.
  • Cachable и может быть gzipped
  • Хранить гораздо большие размеры по сравнению с обычной коллекцией mongo.
  • Использование трубки для уменьшения перегрузки памяти

против

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

Benchmark Вы можете видеть в опции 2 и 3, я включил var start = Date.now(), а при записи конца я console.log выведет время в ms, ниже приведен результат. Dual Core, 4 ГБ, жесткий диск, 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 является самым простым и придерживается основного принципа Meteor, но данные более громоздки, не сжимаются во время передачи, а не кэшируются. Но этот вариант может быть хорошим, если вам нужны только небольшие файлы.
  • XHR в сочетании с файловой системой является "традиционным" способом. Стабильный API, быстрый, "потоковый", сжимаемый, кэшируемый (ETag и т.д.), Но должен находиться в отдельной папке
  • XHR в сочетании с GridFS, вы получаете преимущество набора rep, масштабируемого, не касающегося файловой системы dir, больших файлов и многих файлов, если файловая система ограничивает числа, также сжимаемые с возможностью сжатия. Однако API нестабилен, вы получаете ошибки в нескольких записях, это s..l..o..w..

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