Статьи

Потоковые файлы из MongoDB GridFS

Не так давно я написал в Твиттере то, что я почувствовал, как маленький триумф в моем последнем проекте — потоковую передачу файлов из MongoDB GridFS для загрузки (вместо того, чтобы извлекать весь файл в память и затем подавать его). Я обещал вести блог об этом, но, к сожалению, мое конкретное использование было немного связано с доменом в моем проекте, поэтому я не мог просто показать его как есть. Поэтому я собрал пример приложения node.js + GridFS и поделился им с GitHub, и буду использовать этот пост, чтобы объяснить, как я это сделал. :)

Модуль GridFS

Прежде всего, специальный реквизит идет к tjholowaychuk, который ответил в канале # node.js irc, когда я спросил, повезло ли кому-нибудь использовать GridFS из mongoose . Большая часть моего результирующего кода получена из сущности, которой он поделился со мной. Во всяком случае, к коду. Я опишу, как я использую gridfs, и после установки основы проиллюстрирую, как просто передавать файлы из GridFS.

Я создал модуль gridfs, который в основном обращается к GridStore через mongoose (который я использую во всем приложении), который также может использовать соединение db, созданное при подключении mongoose к серверу mongodb.

1
2
3
4
5
6
mongoose = require "mongoose"
request  = require "request"
 
GridStore = mongoose.mongo.GridStore
Grid      = mongoose.mongo.Grid
ObjectID = mongoose.mongo.BSONPure.ObjectID

Мы не можем получить файлы из mongodb, если не можем что-то в них поместить, поэтому давайте создадим операцию putFile.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
exports.putFile = (path, name, options..., fn) ->
  db = mongoose.connection.db
  options = parse(options)
  options.metadata.filename = name
  new GridStore(db, name, "w", options).open (err, file) ->
    return fn(err)  if err
    file.writeFile path, fn
 
 
 
parse = (options) ->
  opts = {}
  if options.length > 0
    opts = options[0]
  if !opts.metadata
    opts.metadata = {}
  opts

Это действительно просто делегирует операции putFile, которая существует в GridStore как часть модуля mongodb. У меня также есть небольшая логика для анализа параметров, предоставляя значения по умолчанию, если они не были предоставлены. Следует отметить одну интересную особенность: я храню имя файла в метаданных, потому что в то время я столкнулся с забавной проблемой, когда файлы, полученные из gridFS, имели в качестве имени файла идентификатор (хотя взгляд на монго показывает, что имя файла на самом деле база данных).

Теперь операция get. Первоначальная реализация этого просто передавала содержимое в виде буфера предоставленному обратному вызову, вызывая store.readBuffer (), но теперь это изменяется, чтобы передать полученный объект хранилища обратному вызову. Значение в том, что вызывающая сторона может использовать объект хранилища для доступа к метаданным, contentType и другим деталям. Пользователь также может определить, как он хочет прочитать файл (либо в память, либо с помощью ReadableStream).

01
02
03
04
05
06
07
08
09
10
11
12
exports.get = (id, fn) ->
  db = mongoose.connection.db
  id = new ObjectID(id)
  store = new GridStore(db, id, "r",
    root: "fs"
  )
  store.open (err, store) ->
    return fn(err)  if err
    # band-aid
    if "#{store.filename}" == "#{store.fileId}" and store.metadata and store.metadata.filename
      store.filename = store.metadata.filename
    fn null, store

У этого кода есть небольшая ошибка в том, что он проверяет, совпадают ли имя файла и fileId. Если это так, то он проверяет, установлено ли metadata.filename, и устанавливает store.filename в найденное там значение. Я поставил эту проблему на обсуждение позже. :)

Модель

В моем конкретном случае я хотел прикрепить файлы к модели. В этом примере давайте представим, что у нас есть Приложение для чего-либо (работа, заявка на кредит и т. Д.), К которому мы можем прикрепить любое количество файлов. Подумайте о налоговых поступлениях, заполненном заявлении, других отсканированных документах.

1
2
3
4
5
6
7
8
ApplicationSchema = new mongoose.Schema(
  name: String
  files: [ mongoose.Schema.Mixed ]
)
ApplicationSchema.methods.addFile = (file, options, fn) ->
  gridfs.putFile file.path, file.filename, options, (err, result) =>
    @files.push result
    @save fn

Здесь я определяю файлы как массив типов объектов Mixed (то есть они могут быть чем угодно) и метод addFile, который в основном принимает объект, который по крайней мере содержит атрибут пути и имени файла. Он использует это для сохранения файла в gridfs и сохраняет полученный объект файла gridstore в массиве файлов (он содержит такие вещи, как id, uploadDate, contentType, name, size и т. Д.).

Обработка запросов

Это все подключается к обработчику запросов для обработки отправки формы в / new. Все это влечет за собой создание экземпляра модели приложения, добавление загруженного файла из запроса (в данном случае мы назвали поле файла «file», следовательно, req.files.file ) и сохранение его.

1
2
3
4
5
6
7
app.post "/new", (req, res) ->
  application = new Application()
  application.name = req.body.name
  opts =
    content_type: req.files.file.type
  application.addFile req.files.file, opts, (err, result) ->
    res.redirect "/"

Теперь сумма всей этой работы позволяет нам пожинать плоды, упрощая загрузку запрошенного файла из gridFS.

1
2
3
4
5
app.get "/file/:id", (req, res) ->
  gridfs.get req.params.id, (err, file) ->
    res.header "Content-Type", file.type
    res.header "Content-Disposition", "attachment; filename=#{file.filename}"
    file.stream(true).pipe(res)

Здесь мы просто ищем файл по идентификатору и используем полученный объект файла для установки полей Content-Type и Content-Disposition и, наконец, используем ReadableStream :: pipe для записи файла в объект ответа (который является экземпляром WritableStream ). Это волшебство, которое передает данные из MongoDB на клиентскую сторону.

идеи

Это просто скромное начало. Другие идеи включают в себя полностью инкапсулирование gridfs внутри модели. Продвигаясь дальше, мы можем даже превратить модель gridfs в плагин mongoose, чтобы полностью использовать gridfs в черном ящике.

Не стесняйтесь проверить проект и дайте мне знать, если у вас есть идеи, чтобы пойти еще дальше. Вилка прочь! :)

Ссылка: потоковая передача файлов из MongoDB GridFS от нашего партнера по JCG Джеймса Карра в блоге Rants and Musings of Agile Developer