Поток загрузки файла на S3 на Node.js с использованием грозного и (knox или aws-sdk)

Я пытаюсь передать файл, отправленный через форму непосредственно в ведро Amazon S3, используя aws-sdk или knox. Обработка формы выполняется с помощью formidable.

Мой вопрос: как я могу правильно использовать aws-sdk (или knox), используя новейшие функции каждой из этих библиотек для обработки потоков?

Я знаю, что эта тема уже задана здесь в разных вариантах, например:

Тем не менее, я считаю, что ответы немного устарели и/или вне темы (т.е. поддержка CORS, которую я сейчас не хочу использовать по разным причинам) и/или, что самое важное, не ссылаются на последние функции из aws-sdk (см.: https://github.com/aws/aws-sdk-js/issues/13#issuecomment-16085442) или knox (особенно putStream() или его readableStream.pipe(req) вариант, оба объяснены в документе).

После нескольких часов борьбы я пришел к выводу, что мне нужна помощь (отказ от ответственности: я довольно новичок с потоками).

HTML-форма:

<form action="/uploadPicture" method="post" enctype="multipart/form-data">
  <input name="picture" type="file" accept="image/*">
  <input type="submit">
</form>

Express bodyParser middleware настраивается следующим образом:

app.use(express.bodyParser({defer: true}))

Обработчик запросов POST:

uploadPicture = (req, res, next) ->
  form = new formidable.IncomingForm()
  form.parse(req)

  form.onPart = (part) ->
    if not part.filename
      # Let formidable handle all non-file parts (fields)
      form.handlePart(part)
    else
      handlePart(part, form.bytesExpected)

  handlePart = (part, fileSize) ->
    # aws-sdk version
    params =
      Bucket: "mybucket"
      Key: part.filename
      ContentLength: fileSize
      Body: part # passing stream object as body parameter

    awsS3client.putObject(params, (err, data) ->
      if err
        console.log err
      else
        console.log data
    )

Однако я получаю следующую ошибку:

{[RequestTimeout: ваше сокетное соединение с сервером не было прочитано или записано в течение периода ожидания. Недействительные соединения будут закрыты.]

message: "Ваше сокетное соединение с сервером не было прочитано или записано в течение периода ожидания. Недействующие соединения будут закрыты. ',   код:" RequestTimeout",   name: 'RequestTimeout',   statusCode: 400,   retryable: false}

Версия knox для функции handlePart(), адаптированная таким образом, также ужасно терпит неудачу:

handlePart = (part, fileSize) ->
  headers =
    "Content-Length": fileSize
    "Content-Type": part.mime
  knoxS3client.putStream(part, part.filename, headers, (err, res) ->
    if err
      console.log err
    else
      console.log res
  )      

Я также получаю большой объект res с 400 statusCode где-то.

В обоих случаях регион настроен на eu-west-1.

Дополнительные примечания:

node 0,10.12

последний грозный от npm (1.0.14)

последний aws-sdk из npm (1.3.1)

последний нокс от npm (0.8.3)

Ответ 1

Ну, в соответствии с создателем Formidable, прямой поток на Amazon S3 невозможно:

В S3 API вам необходимо указать размер новых файлов при их создании. Эта информация недоступна для файлов multipart/form-data, пока они не будут полностью получены. Это означает, что потоковая передача невозможна.

Действительно, form.bytesExpected относится к размеру всей формы, а не к размеру одного файла.

Следовательно, данные должны либо попадать в память или диск на сервере, прежде чем загружаться на S3.

Ответ 2

Использование AWS S3 multipartUpload (s3-upload-stream в качестве рабочего модуля) и node -formidable читаемый поток, вы можете передать поток для загрузки, например this:

var formidable = require('formidable');
var http = require('http');
var util = require('util');
var AWS      = require('aws-sdk');
var config = require('./config');
var s3 = new AWS.S3({
    accessKeyId: config.get('S3_ACCESS_KEY'),
    secretAccessKey: config.get('S3_SECRET_KEY'),
    apiVersion: '2006-03-01'
});
var s3Stream = require('s3-upload-stream')(s3);
var bucket = 'bucket-name';
var key = 'abcdefgh';


http.createServer(function(req, res) {

    if (req.url == '/upload' && req.method.toLowerCase() == 'post') {

        var form = new formidable.IncomingForm();
        form.on('progress', function(bytesReceived, bytesExpected) {
            //console.log('onprogress', parseInt( 100 * bytesReceived / bytesExpected ), '%');
        });

        form.on('error', function(err) {
            console.log('err',err);
        });

        // This 'end' is for the client to finish uploading
        // upload.on('uploaded') is when the uploading is
        // done on AWS S3
        form.on('end', function() {
            console.log('ended!!!!', arguments);
        });

        form.on('aborted', function() {
            console.log('aborted', arguments);
        });

        form.onPart = function(part) {
            console.log('part',part);
            // part looks like this
            //    {
            //        readable: true,
            //        headers:
            //        {
            //            'content-disposition': 'form-data; name="upload"; filename="00video38.mp4"',
            //            'content-type': 'video/mp4'
            //        },
            //        name: 'upload',
            //            filename: '00video38.mp4',
            //        mime: 'video/mp4',
            //        transferEncoding: 'binary',
            //        transferBuffer: ''
            //    }

            var start = new Date().getTime();
            var upload = s3Stream.upload({
                "Bucket": bucket,
                "Key": part.filename
            });

            // Optional configuration
            //upload.maxPartSize(20971520); // 20 MB
            upload.concurrentParts(5);

            // Handle errors.
            upload.on('error', function (error) {
                console.log('errr',error);
            });
            upload.on('part', function (details) {
                console.log('part',details);
            });
            upload.on('uploaded', function (details) {
                var end = new Date().getTime();
                console.log('it took',end-start);
                console.log('uploaded',details);
            });

            // Maybe you could add compress like
            // part.pipe(compress).pipe(upload)
            part.pipe(upload);
        };

        form.parse(req, function(err, fields, files) {
            res.writeHead(200, {'content-type': 'text/plain'});
            res.write('received upload:\n\n');
            res.end(util.inspect({fields: fields, files: files}));
        });
        return;
    }

    // show a file upload form
    res.writeHead(200, {'content-type': 'text/html'});
    res.end(
        '<form action="/upload" enctype="multipart/form-data" method="post">'+
        '<input type="text" name="title"><br>'+
        '<input type="file" name="upload" multiple="multiple"><br>'+
        '<input type="submit" value="Upload">'+
        '</form>'
    );
}).listen(8080);

Ответ 3

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

Если это помогает любому, кому я мог напрямую передавать с клиента на s3 без необходимости установки пакетов:

https://gist.github.com/mattlockyer/532291b6194f6d9ca40cb82564db9d2a

Сервер предполагает, что req является объектом потока, в моем случае в xhr (send) использовался объект File, который отправляет двоичные данные в современных браузерах.

const fileUploadStream = (req, res) => {
  //get "body" args from header
  const { id, fn } = JSON.parse(req.get('body'));
  const Key = id + '/' + fn; //upload to s3 folder "id" with filename === fn
  const params = {
    Key,
    Bucket: bucketName, //set somewhere
    Body: req, //req is a stream
  };
  s3.upload(params, (err, data) => {
    if (err) {
      res.send('Error Uploading Data: ' + JSON.stringify(err) + '\n' + JSON.stringify(err.stack));
    } else {
      res.send(Key);
    }
  });
};

Да, это нарушает соглашение, но если вы посмотрите на суть, это намного чище, чем все, что я нашел, полагаясь на другие пакеты.

+1 для прагматизма и благодаря @SalehenRahman за помощь.

Ответ 5

В промежуточном программном обеспечении Express я использую formidable PassThrough вместе с PassThrough для потоковой передачи файла на S3 (в моем случае на Minio, который совместим с S3 через Minio SDK; и я считаю, что он работает и для AWS S3 с тем же Minio SDK)

Вот пример кода.

const formidable = require('formidable')
const { PassThrough } = require('stream')

const form = new formidable.IncomingForm()
const pass = new PassThrough()

const fileMeta = {}
form.onPart = part => {
  if (!part.filename) {
    form.handlePart(part)
    return
  }
  fileMeta.name = part.filename
  fileMeta.type = part.mime
  part.on('data', function (buffer) {
    pass.write(buffer)
  })
  part.on('end', function () {
    pass.end()
  })
}
form.parse(req, err => {
  if (err) {
    req.minio = { error: err }
    next()
  } else {
    handlePostStream(req, next, fileMeta, pass)
  }
})

И handlePostStream выглядит так, как handlePostStream ниже:

const uuidv1 = require('uuid/v1')

const handlePostStream = async (req, next, fileMeta, fileStream) => {
  let filename = uuidv1()

  try {
    const metaData = {
      'content-type': fileMeta.type,
      'file-name': Buffer.from(fileMeta.name).toString('base64')
    }

    const minioClient = /* Get Minio Client*/
    await minioClient.putObject(MINIO_BUCKET, filename, fileStream, metaData)

    req.minio = { post: { filename: '${filename}' } }
  } catch (error) {
    req.minio = { error }
  }
  next()
}

Вы можете найти исходный код на GitHub, а также его модульные тесты.