今回の内容
前回、AWS SDK for JavaScript v3(以下v3)でDynamoDBを扱うLambdaを書くというブログを投稿しました。
今回はその第二弾で、S3を扱うLambdaを書いてみます。
S3はAWSのリソースの中でも扱う機会がトップクラスに多い(と思う)ので、覚えて損はないと思います。
なお今回はPutObjectCommand(ファイル作成)、およびGetObject(ファイル読み込み)を扱います。
参考サイト
- S3 Client - AWS SDK for JavaScript v3
- Stream | Node.js v18.6.0 Documentation
- Node.jsのstreamのしくみまとめ
- Node.js Stream を使いこなす - Qiita
PutObjectCommand(ファイル作成)
S3もDynamoDB同様、v3のお作法通り
- 各リソースのクライアントを定義する
- 各リソースの操作コマンド(=これがv2のAPI関数に相当)を定義する
- 1のクライアントのsendメソッドで、2のコマンドを送信する
- 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を文字列に変換する処理を書くのが面倒な場合、何か専用の共通処理を書いてもいいかもしれませんね。
それでは、今回はこの辺で