echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【AWS】AWS SDK for JavaScript v3でLambdaを書く(S3編)

今回の内容

前回、AWS SDK for JavaScript v3(以下v3)でDynamoDBを扱うLambdaを書くというブログを投稿しました。
今回はその第二弾で、S3を扱うLambdaを書いてみます。

S3はAWSのリソースの中でも扱う機会がトップクラスに多い(と思う)ので、覚えて損はないと思います。

なお今回はPutObjectCommand(ファイル作成)、およびGetObject(ファイル読み込み)を扱います。

参考サイト

PutObjectCommand(ファイル作成)

S3もDynamoDB同様、v3のお作法通り

  1. 各リソースのクライアントを定義する
  2. 各リソースの操作コマンド(=これがv2のAPI関数に相当)を定義する
  3. 1のクライアントのsendメソッドで、2のコマンドを送信する
  4. 3の結果を取得する。(これがAPIの戻り値に相当)

という流れでソースを書けばOKです。

というわけで、まずはファイル作成のソースを書いてみます。

まずは事前に@aws-sdk/client-s3をインストールしておきます。
また前回同様、Lambdaの型定義用に@types/aws-lambdaもインストールしておきます。

import { Context, APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda';
import { S3Client, PutObjectCommand, PutObjectCommandInput } from '@aws-sdk/client-s3';

const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
  
  // 書き込むテキスト
  const contents = '隣の客はよく客食う客だ';

  // S3クライアントを定義
  const client = new S3Client({});
  
  // 作成するファイル情報
  const putParams: PutObjectCommandInput = {
    Bucket: 'your-bucket-name',
    Key: 'sample.txt',
    Body: contents,
  }
  
  // コマンドを定義&送信
  // 今回は戻り値の取得は不要。(成功or失敗が分かればOK)
  const putCommand = new PutObjectCommand(putParams);
  await client.send(putCommand);
  
  const response: APIGatewayProxyResult = {
    statusCode: 200,
    body: ’成功’,
  };
  
  return response;
};
  
module.exports = { handler };

特に問題ないですね。
ソースもv3のお作法通りだし、PutObjectCommandの引数(PutObjectCommandInput)の内容も、v2のputObjectと同じでOKです。

実際にこのソースと試すと、(設定に問題がなければ)指定したバケットに「sample.txt」というファイルが作成され、その中身は「隣の客はよく客食う客だ」となっているはずです。

では、次はファイルの読み込みです。

GetObjectCommand(ファイル読み込み)

といっても、これも基本はv3のお作法通りでOKです。
またGetObjectCommandの引数(GetObjectCommandInput)の内容も、これまたv2のgetObjectと同じでOKです。

// ネタバレ:このコードは正しく動きません
import { Context, APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda';
import { S3Client, GetObjectCommand, GetObjectCommandInput } from '@aws-sdk/client-s3';

const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
  
  // クライアント定義
  const client = new S3Client({});
  
  // 取得するファイル情報
  const getParams: GetObjectCommandInput = {
    Bucket: 'your-bucket-name',
    Key: 'sample.txt',
  };
  
  // コマンド定義&送信
  const getCommand = new GetObjectCommand(getParams);
  const res = await client.send(getCommand);

  // res.Bodyに読み込んだファイル内容が格納される
  const response: APIGatewayProxyResult = {
    statusCode: 200,
    body: JSON.stringify({
      contents: res.Body
    }),
  };

  return response;
};  
  
module.exports = { handler };

...そう思っていた時期が、俺にもありました。(バギ並感)

Bodyの型が違う

コメントに書いた通り、上記ソースはエラーになってしまい、正しく動きません。
最後のresponseを設定する部分のcontents: res.Body の部分がエラーになります。

実はv3では、Bodyの型がNode.js Stream APIのReadable(=Stream.Readable)型になってます。(Bufferではない)
そのため、Bodyを直で参照しても、ファイルの内容は取得できません。

ファイルの内容を取得するためには、Streamから文字列を取得する処理が必要になります。

というわけで、それを反映したのが下記ソースになります。

なおNode.js Stream APIの型定義を扱うために、@types/readable-streamをインストールしておくと便利です。

import { Context, APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda';
import { Readable } from 'readable-stream';
import { S3Client, GetObjectCommand, GetObjectCommandInput } from '@aws-sdk/client-s3';

const handler = async (event: APIGatewayEvent, context: Context): Promise<APIGatewayProxyResult> => {
  
  const client = new S3Client({});
  
  const getParams: GetObjectCommandInput = {
    Bucket: 'suzukima-s3-local-test-bucket2',
    Key: 'sample.txt',
  };
  
  const getCommand = new GetObjectCommand(getParams);
  const res = await client.send(getCommand);  
  
  // Stream.Readable型に変換 
  const body = res.Body as Readable;
  
  // Stream.Readableを文字列に変換する
  let fileContents = '';
  for await (const chunk of body) {
    fileContents += chunk;
  }
  
  //  こっちのやり方でもOK(公式サイトはこっちのやり方)
  // const streamToString = (stream:Readable) =>
  //   new Promise((resolve, reject) => {
  //     const chunks:Uint8Array[] = [];
  //     stream.on("data", (chunk) => chunks.push(chunk));
  //     stream.on("error", reject);
  //     stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
  // });
  // 
  // fileContents = await streamToString(body);  
  
  const response: APIGatewayProxyResult = {
    statusCode: 200,
    body: JSON.stringify({
      contents: fileContents
    }),
  };
  
  return response;
};
  
module.exports = { handler };

上記を実施すると、下記レスポンスを取得できると思います。

{
  "contents": "隣の客はよく柿食う客だ"
}

なおローカルなどで実行していて「Stream.Readableの内容をファイルに保存したい」という場合、下記のようにします。

import fs from 'fs'  
  
// (const body = res.Body as Readable までは同じなので省略)
  
// Stream.WriteStreamを作成する。
const ws = fs.createWriteStream('sample2.txt'); 
  
// Stream.WriteStreamにStream.ReadStreamの内容を少しづつ書き込む。
body.pipe(ws);

ちなみにNode.js Stream APIについては、「参考サイト」に記載したサイトが詳しく説明してくださっています。

ちなみに

ファイル内容が大量ならともかく、ちょっとした文字列ですらStream.Readableを文字列に変換するのはやっぱりアレということで(?)、下記issueが立ってました。
S3.GetObject no longer returns the result as a string · Issue #1877 · aws/aws-sdk-js-v3 · GitHub

また上記issueのリプによると、get-streamというnpmモジュールを使えば、変換処理を1行で書けるそうです。
https://github.com/aws/aws-sdk-js-v3/issues/1877#issuecomment-799697205

import getStream from 'get-stream';
  
fileContents = await getStream(body);  

まとめ

以上、AWS SDK for JavaScript v3でS3を扱う方法でした。

GetObjectCommandのBodyの扱いだけが変わっているので、そこさえ気を付ければ、あとはv2と同じ方法でOKです。

また毎回Stream.Readableを文字列に変換する処理を書くのが面倒な場合、何か専用の共通処理を書いてもいいかもしれませんね。

それでは、今回はこの辺で