使用formidable和(knox或aws-sdk)在Node.js上将文件流上传到S3

20

我正在尝试通过表单直接将文件流上传到Amazon S3存储桶,使用的是aws-sdkknox。表单处理使用的是formidable

我的问题是:如何正确地使用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中间件的配置方式如下:

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: '您的套接字连接在超时期间内未被读取或写入。空闲连接将被关闭。', code: 'RequestTimeout', name: 'RequestTimeout', statusCode: 400, retryable: false }

一个经过调整的handlePart()函数的knox版本也惨遭失败:

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
  )      

我在某个地方也得到了一个带有400状态码的大型res对象。

在两种情况下,区域都配置为eu-west-1

附加说明:

node 0.10.12

来自npm的最新formidable(1.0.14)

来自npm的最新aws-sdk(1.3.1)

来自npm的最新knox(0.8.3)

4个回答

13
使用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);

我在多方使用了这个。 - ifiok

10

据Formidable的创建者所述,直接流式传输到Amazon S3是不可能的:

S3 API要求在创建文件时提供新文件的大小信息。 multipart / form-data文件在完全接收之前无法获得此信息。 这意味着流式传输是不可能的。

确实,form.bytesExpected是指整个表单的大小,而不是单个文件的大小。

因此,数据必须首先在服务器上命中内存或磁盘,然后才能上传到S3。


3
这刚刚节省了我很多时间,谢谢。 - Engineer
9
事实并非如此。将内容传输至S3是可能的!您只需要知道上传的大小。如果您的客户端可以提供该大小信息,则确实可以使用管道上传到S3,而无需进行麻烦的硬盘写入。我正在编写一个CLI和中间服务器,用于上传至S3。因为我控制了客户端和服务器,所以可以在上传之前确定文件大小。我认为可能还有其他类似我的边缘情况不应被忽视。我使用knox通过PUT请求流式传输到S3。 - CharlesTWall3
@CharlesTWall3 这是一个非常有效的评论,我当时没有考虑到这一点 - 我只想到了一个服务器端的解决方案。如果您成功实现某些功能,请随意发布答案,我很乐意投票支持您的解决方案。您可能还想编辑此答案。谢谢! - jbmusso
@arcseldon,https://dev59.com/32s05IYBdhLWcg3wB9af 可以帮助您。您可以先设置一个<input type="hidden" name="file_size">并填入相应的文件大小值。 - jbmusso
2
@gulthor - 感谢您的建议。就我的情况而言,我对通过Node应用程序(无浏览器)从mongodb进行流处理很感兴趣。通过使用“s3-upload-stream” NPM模块找到了解决方案。它使用S3多部分API,因此不需要提前指定整个文件大小。它按块工作并自动传递它们的大小。只需花费几分钟从自述文档中复制示例代码并将其插入到我的应用程序中即可。必须称赞NodeJS模块社区的便利性。还有一些旧的SOF帖子仍在尝试使用非最佳解决方案来完成这项任务。再次感谢您的建议。 - arcseldon
显示剩余2条评论

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 的帮助。


在我的情况下,上传成功了,但是图像文件无法正确打开。 - IroNEDR
你检查了你的扩展吗?在AWS上或从另一个下载位置正确打开了吗?也要检查是否收到了正确数量的字节。 - mattdlockyer

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接