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
плюсы
- простой, не хакерский способ, никаких дополнительных пакетов
- придерживаться данных по принципу провода
минусы
- больше пропускной способности: результирующая строка 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" на сервер.
на сервере данные передаются в базовую файловую систему. Вы можете дополнительно определить имя файла, выполнить очистку или проверить, существует ли оно уже и т. д. перед сохранением.
плюсы
- воспользовавшись XHR 2, Вы можете отправить arraybuffer, новый FileReader () не требуется по сравнению с вариантом 1
- Arraybuffer менее громоздкий по сравнению с base64 string
- без ограничения размера, я отправил файл ~ 200 МБ в localhost без проблем
- файловая система работает быстрее, чем mongodb (подробнее об этом ниже в бенчмаркинге)
- Cachable и gzip
минусы
- XHR 2 недоступен в старых браузерах, например, ниже IE10, но конечно, вы можете реализовать традиционный post
- / 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) напрямую и начать трубопровод
плюсы
- то же, что и в Варианте 2, отправлено с помощью arraybuffer, меньше накладных расходов по сравнению со строкой base64 в опции 1
- не нужно беспокоиться о дезинфекции имени файла
- отделение от файловой системы, нет необходимости писать в temp dir, БД можно создать резервную копию, rep, shard и т. д.
- нет необходимости реализовывать какой-либо другой пакет
- Cachable и может быть gzipped
- магазин гораздо больших размеров по сравнению с обычной коллекцией mongo
- используя трубу для уменьшения перегрузки памяти
минусы
- Нестабильная Mongo GridFS. Я включил версию A (mongo 1.x) и B (mongo 2.икс.) В версии A, когда конвейерные большие файлы > 10 MB, я получил много ошибок, включая поврежденный файл, незавершенную трубу. Эта проблема решена в версии B с помощью mongo 2.x, надеюсь, meteor обновится до mongodb 2.х только
- API путаница. В версии, вам нужно открыть файл, прежде чем вы можете stream, но в версии B вы можете передавать без вызова open. Документ API также не очень понятен, и поток не является синтаксисом 100%, обмениваемым с Npm.require('fs'). В fs вы вызываете файл.on ('finish'), но в GridFS вы вызываете файл.on ('end') при завершении/завершении записи.
- GridFS не обеспечивает атомарность записи, поэтому, если есть несколько одновременных записей в один и тот же файл, конечный результат может быть очень разным
- скорость. Монго 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);};