将流通过管道传输到s3.upload()


89

我目前正在使用名为s3-upload-stream的node.js插件非常大的文件流式传输到Amazon S3。它使用了多部分的API,并且在大多数情况下效果很好。

但是,此模块显示了它的年龄,我已经不得不对其进行修改(作者也已弃用它)。今天,我遇到了另一个与亚马逊有关的问题,我真的很想接受作者的建议,并开始使用官方的aws-sdk完成上传。

但。

官方SDK似乎不支持管道 s3.upload()。s3.upload的本质是,您必须将可读流作为参数传递给S3构造函数。

我大约有120多个用于执行各种文件处理的用户代码模块,它们与输出的最终目标无关。引擎将它们传递给可管道传输的可写输出流,并通过管道传递给它。我不能给他们一个AWS.S3对象并要求他们调用upload()它,而无需在所有模块中添加代码。我之所以使用s3-upload-stream它是因为它支持管道。

有什么方法可以使aws-sdks3.upload()可以通过流传输到其中?

Answers:


130

upload()用node.jsstream.PassThrough()流包装S3函数。

这是一个例子:

inputStream
  .pipe(uploadFromStream(s3));

function uploadFromStream(s3) {
  var pass = new stream.PassThrough();

  var params = {Bucket: BUCKET, Key: KEY, Body: pass};
  s3.upload(params, function(err, data) {
    console.log(err, data);
  });

  return pass;
}

2
太好了,这解决了我非常丑陋的问题=-)您能解释一下stream.PassThrough()实际做什么吗?
mraxus 16'Oct

6
执行此操作时,PassThrough流是否关闭?我有段时间在s3.upload中请求关闭以达到我的PassThrough流。
four43

7
上传文件的大小为0个字节。如果我将相同的数据从源流传输到文件系统,则一切正常。任何的想法?
Radar155 '17

3
直通流将接收写入的字节并将其输出。这使您可以返回可写流,当您对其进行写入时,aws-sdk将读取该可写流。我还将从s3.upload()返回响应对象,因为否则您将无法确保上传完成。
reconbot

1
s3管道内的参数从何stream而来?
21点

94

答案有点晚了,可能会对其他人有所帮助。您既可以返回可写流,也可以返回Promise,这样您就可以在上传完成后获取响应数据。

const AWS = require('aws-sdk');
const stream = require('stream');

const uploadStream = ({ Bucket, Key }) => {
  const s3 = new AWS.S3();
  const pass = new stream.PassThrough();
  return {
    writeStream: pass,
    promise: s3.upload({ Bucket, Key, Body: pass }).promise(),
  };
}

您可以使用以下功能:

const { writeStream, promise } = uploadStream({Bucket: 'yourbucket', Key: 'yourfile.mp4'});
const readStream = fs.createReadStream('/path/to/yourfile.mp4');

const pipeline = readStream.pipe(writeStream);

现在您可以检查promise:

promise.then(() => {
  console.log('upload completed successfully');
}).catch((err) => {
  console.log('upload failed.', err.message);
});

或者作为stream.pipe()return stream.Writable的目的地(上面的writeStream变量),允许使用管道链,我们还可以使用其事件:

 pipeline.on('close', () => {
   console.log('upload successful');
 });
 pipeline.on('error', (err) => {
   console.log('upload failed', err.message)
 });

它看起来很棒,但在我的身边,我得到这个错误stackoverflow.com/questions/62330721/...
阿科Voltaico

刚刚回答了您的问题。希望能帮助到你。
Ahmet Cetin

48

在接受的答案中,该功能在上传完成之前结束,因此,它是不正确的。下面的代码从可读流中正确地传递管道。

上传参考

async function uploadReadableStream(stream) {
  const params = {Bucket: bucket, Key: key, Body: stream};
  return s3.upload(params).promise();
}

async function upload() {
  const readable = getSomeReadableStream();
  const results = await uploadReadableStream(readable);
  console.log('upload complete', results);
}

您还可以更进一步,并使用以下命令输出进度信息ManagedUpload

const manager = s3.upload(params);
manager.on('httpUploadProgress', (progress) => {
  console.log('progress', progress) // { loaded: 4915, total: 192915, part: 1, key: 'foo.jpg' }
});

ManagedUpload参考

可用事件列表


1
现在,aws-sdk提供了内置于2.3.0+中的promise,因此您不必再取消它们了。s3.upload(params).promise()。then(data => data).catch(error =>错误);
DBrown

1
@DBrown感谢您的指导!因此,我已经更新了答案。
tsuz

1
@tsuz,尝试实施您的解决方案会给我一个错误:TypeError: dest.on is not a function,知道为什么吗?
FireBrand

什么dest.on啊 你能举个例子吗?@FireBrand
tsuz

9
这表示已接受的答案不完整,但不适用于@Womp更新后的文章中所述的s3.upload管道。如果更新此答案以获取其他内容的管道输出,将非常有帮助!
MattW

6

没有答案对我有用,因为我想:

  • 管入 s3.upload()
  • 将结果传送s3.upload()到另一个流

接受的答案不适合后者。其他的依赖promise api,在使用流管道时,这很麻烦。

这是我对已接受答案的修改。

const s3 = new S3();

function writeToS3({Key, Bucket}) {
  const Body = new stream.PassThrough();

  s3.upload({
    Body,
    Key,
    Bucket: process.env.adpBucket
  })
   .on('httpUploadProgress', progress => {
       console.log('progress', progress);
   })
   .send((err, data) => {
     if (err) {
       Body.destroy(err);
     } else {
       console.log(`File uploaded and available at ${data.Location}`);
       Body.destroy();
     }
  });

  return Body;
}

const pipeline = myReadableStream.pipe(writeToS3({Key, Bucket});

pipeline.on('close', () => {
  // upload finished, do something else
})
pipeline.on('error', () => {
  // upload wasn't successful. Handle it
})


它看起来很棒,但在我的身边,我得到这个错误stackoverflow.com/questions/62330721/...
阿科Voltaico

5

类型脚本解决方案:
此示例使用:

import * as AWS from "aws-sdk";
import * as fsExtra from "fs-extra";
import * as zlib from "zlib";
import * as stream from "stream";

和异步功能:

public async saveFile(filePath: string, s3Bucket: AWS.S3, key: string, bucketName: string): Promise<boolean> { 

         const uploadStream = (S3: AWS.S3, Bucket: string, Key: string) => {
            const passT = new stream.PassThrough();
            return {
              writeStream: passT,
              promise: S3.upload({ Bucket, Key, Body: passT }).promise(),
            };
          };
        const { writeStream, promise } = uploadStream(s3Bucket, bucketName, key);
        fsExtra.createReadStream(filePath).pipe(writeStream);     //  NOTE: Addition You can compress to zip by  .pipe(zlib.createGzip()).pipe(writeStream)
        let output = true;
        await promise.catch((reason)=> { output = false; console.log(reason);});
        return output;
}

在以下位置调用此方法:

let result = await saveFileToS3(testFilePath, someS3Bucket, someKey, someBucketName);

4

上面最被接受的答案中要注意的一点是:如果使用的是类似管道的方法,则需要在函数中返回pass,

fs.createReadStream(<filePath>).pipe(anyUploadFunction())

function anyUploadFunction () { 
 let pass = new stream.PassThrough();
 return pass // <- Returning this pass is important for the stream to understand where it needs to write to.
}

否则,它将静静地移至下一个而不会引发错误,或者将引发错误,TypeError: dest.on is not a function具体取决于您编写函数的方式


3

如果它可以帮助我成功地从客户端流式传输到s3的任何人:

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

服务器端代码假定req是一个流对象,在我的情况下,它是从客户端发送的,头中设置了文件信息。

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);
    }
  });
};

是的,这违反了惯例,但是如果您查看要点,它比我使用multer,busboy等发现的任何东西都干净得多...

+1为实用主义,感谢@SalehenRahman的帮助。


multer,busboy处理多部分/表单数据上传。当客户端从XMLHttpRequest发送缓冲区作为主体时,req作为流起作用。
安德烈Werlang

澄清一下,上传是从后端执行的,不是客户端吗?
numX

是的,它正在后端“传输”流,但是它来自前端
mattdlockyer

3

对于那些抱怨当他们使用s3 api上传功能并且零字节文件最终出现在s3上的人(@ Radar155和@gabo)-我也遇到了这个问题。

创建第二个PassThrough流,并将所有数据从第一个流到第二个,并将引用传递到第二个到s3。您可以通过几种不同的方式来执行此操作-可能是一种肮脏的方式,即侦听第一个流上的“数据”事件,然后将相同的数据写入第二个流中-类似于“结束”事件,只需调用第二个流上的end函数。我不知道这是否是aws api中的错误,节点的版本或其他问题-但这对我来说可以解决此问题。

这是它的外观:

var PassThroughStream = require('stream').PassThrough;
var srcStream = new PassThroughStream();

var rstream = fs.createReadStream('Learning/stocktest.json');
var sameStream = rstream.pipe(srcStream);
// interesting note: (srcStream == sameStream) at this point
var destStream = new PassThroughStream();
// call your s3.upload function here - passing in the destStream as the Body parameter
srcStream.on('data', function (chunk) {
    destStream.write(chunk);
});

srcStream.on('end', function () {
    dataStream.end();
});

这实际上也对我有用。每当使用分段上传时,S3上传功能只是默默地“死”,但是在使用您的解决方案时,它运行良好(!)。谢谢!:)
jhdrn

您能否提供一些有关为什么需要第二个流的信息?
noob7

1

在遵循其他答案之后,并使用最新的适用于Node.js的AWS开发工具包,由于s3 upload()函数使用await语法和S3的promise接受流,因此存在一个更简洁的解决方案:

var model = await s3Client.upload({
    Bucket : bucket,
    Key : key,
    ContentType : yourContentType,
    Body : fs.createReadStream(path-to-file)
}).promise();

0

我正在使用KnexJS,使用其流API时遇到了问题。我终于将其修复,希望以下内容对您有所帮助。

const knexStream = knex.select('*').from('my_table').stream();
const passThroughStream = new stream.PassThrough();

knexStream.on('data', (chunk) => passThroughStream.write(JSON.stringify(chunk) + '\n'));
knexStream.on('end', () => passThroughStream.end());

const uploadResult = await s3
  .upload({
    Bucket: 'my-bucket',
    Key: 'stream-test.txt',
    Body: passThroughStream
  })
  .promise();

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.