27

Iv looked all over AWS docks and stack overflow (even went to page 4 of google!!!) but i cannot for the life of me work out how to stream a file from S3. The docs for V3 are pretty useless and all the examples i find are from V2.

The send commond that V3 uses only returns a promise so how do i get a stream and pipe it instead of waiting for the whole file (it needs to be piped into encryption algo then to a response stream)

this.s3.send(
  new GetObjectCommand({
    Bucket: '...',
    Key: key,
  }),
);

I was able to upload fine by passing the stream as the body, is there something i have to do similar here?

  uploadToAws(key) {
    const pass = new PassThrough();

    return {
      writeStream: pass,
      promise: this.s3.send(
        new PutObjectCommand({
          Bucket: '...',
          Key: key,
          Body: pass,
          ServerSideEncryption: '...',
          ContentLength: 37,
        }),
      ),
    };
  }
Jay Povey
  • 539
  • 2
  • 6
  • 15

8 Answers8

40

Body from the GetObjectCommand is a readable stream (https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/interfaces/getobjectcommandoutput.html#body).

So you can do:

const command = new GetObjectCommand({
    Bucket
    Key,
});
const item = await s3Client.send(command);
item.Body.pipe(createWriteStream(fileName));
acorbel
  • 1,300
  • 12
  • 14
  • 2
    you Sir/Madame are a legend. thank you. Wasn't clear i had to wait for the promise response and retrieve the stream from that. – Jay Povey May 03 '21 at 19:19
  • 6
    item.Body does not have a pipe field – Karatekid430 Nov 18 '22 at 05:30
  • 8
    typescript complaints about `pipe` not existing on `Body` tho it works ‍♂️ – Can Rau Nov 29 '22 at 16:19
  • got the above error about `pipe` not existing on Body, resolved with `data.Body!.transformToWebStream()`. Added as answer below. – Akshay Dec 06 '22 at 09:17
  • another way is `Readable.from(Buffer.from(data.Body as Buffer))` – Akshay Dec 09 '22 at 10:48
  • 6
    To fix the pipe not existing on Body do the following. The transformToWebStream gives an error on nodejs `import { Readable } from 'stream'` `const readStream = item.Body as Readable` – sonic_ninja Dec 23 '22 at 17:58
  • 2
    Either way, transforming `item.Body as Readable` then `pipe()` produces empty files. AWS SDK v3 now uses `SdkStream` and the easiest way is use some of its method to read to Memory. `transformToWebStream` and piping ZIP files produces invalid files. AWS SDK 3.267.0. – ajkret Feb 14 '23 at 19:38
  • Sonic Ninja's code works for me, ajkret. – Lee Goddard Mar 29 '23 at 08:28
  • 1
    How would you do this so the stream writes as it reads? Since there's an await on the send command, it has to download the full file first before writing right? – Austin Borden Jun 11 '23 at 05:57
7

Using transformToString() as per docs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/example_s3_GetObject_section.html.

export const handler = async event => {
    try {
            
        // Retrieve the object from S3
        const data = await s3.getObject({ Bucket: BUCKET_NAME, Key: PATH_AND_FILE_NAME });
    
        // Set the content type of the response
        const contentType = data.ContentType;

        // Convert to base64 string
        const streamToString = await data.Body?.transformToString("base64");
    
        // Return the object data in the response
        return {
            statusCode: 200,
            headers: {
                "Content-Type": contentType
            },
            body: streamToString,
            isBase64Encoded: true
        };
    } catch (error) {           
        return {
            statusCode: 500,
            body: "An error occurred: " + error.message
        };
    }
};
Chris
  • 113
  • 1
  • 3
  • 3
    This was the key information I needed. Docs are quite obscure and examples of usage are scarce. I only needed the string and `transformToString()` worked. Thanks. – Mike Smith Jan 13 '23 at 16:45
  • 4
    This loads the entire file into memory before forwarding to the client. This works for small files, but not for big ones. – Michael Cole Jan 17 '23 at 22:42
  • Agreed. Getting a signed url would a better option for large files. – Chris Jan 18 '23 at 23:04
6

Figured it out,

s3Client.send(command) returns a type GetObjectCommandOutput.
const data: GetObjectCommandOutput = await s3Client.send(command)

data.Body is of type SdkStream<Readable | ReadableStream<any> | Blob | undefined> | undefined

Undefined is for error cases, you check there is no error case like this

if (!data.Body)
//handle error

For success case, you can get ReadableStream like this
const readableStream: ReadableStream = data.Body!.transformToWebStream()

For aws-sdk V2, there was createReadStream(), this is seems to be the way in v3.

To pipe through ReadableStream, use
readableStream.pipeTo() or readableStream.pipeThrough()

Akshay
  • 158
  • 1
  • 7
  • For me, `pipeTo()` or `pipeThrough()` still doesn't exist on the `readableStream`. I'm also using Fastify, but whenever I want to return the `readableStream` as stated in the docs, I get "Attempted to send payload of invalid type 'object'. Expected a string or Buffer.". Is it maybe because the readableStream that is returned is not the same as what Fastify expects? – JC97 Jan 10 '23 at 13:07
5

docs links

For those landing here after googling because aws v3 sdk docs are missing details on getobjectcommandoutput interface, you can find the full getobjectcommandoutput definition at source or at "module" → https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/modules/getobjectoutput.html

laconbass
  • 17,080
  • 8
  • 46
  • 54
1

In case you also run into a similar issue while using typescript, This helped me

(object.Body as any).pipe(res);

I did this because as mentioned by laconbass, aws v3 sdk docs are missing details on getobjectcommandoutput.

1

Try this in TypeScript for streaming to a file path, destinationPath:

import { createWriteStream } from "node:fs";
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { Readable } from "node:stream";

const s3 = new S3Client({});

async function downloadFile(
  bucketName: string,
  objectKey: string,
  destinationPath: string
) {
  const data = await s3.send(
    new GetObjectCommand({ Bucket: bucketName, Key: objectKey })
  );
  return new Promise(async (resolve, reject) => {
    const body = data.Body;
    if (body instanceof Readable) {
      const writeStream = createWriteStream(destinationPath);
      body
        .pipe(writeStream)
        .on("error", (err) => reject(err))
        .on("close", () => resolve(null));
    }
  });
}
Philippe Fanaro
  • 6,148
  • 6
  • 38
  • 76
Ben Stickley
  • 1,551
  • 1
  • 16
  • 22
0

To get a blob instead of writing into a file, you can just do this:

const command = new GetObjectCommand({
   Bucket
   Key,
});
const { Body } = await s3Client.send(command);
const blob = await Body.transformToByteArray();
koxon
  • 818
  • 1
  • 10
  • 12
0

This is what worked for me -

const fileStream = fs.createReadStream('/tmp/myfile.ext');

const command = new PutObjectCommand({ Body: fileStream, Bucket, Key });

const result = await client.send(command);

if(result?.$metadata?.httpStatusCode === 200){
    console.log('Success');
};
Ben
  • 1,989
  • 22
  • 25