ワタミチートシート以来、久々のカンペ記事。
LambdaのNode.js 18ランタイムではAWS SDK v3のみプリインストールされているなど、いよいよAWS SDK for JSを移行すべき状況になっている。 しかし私は未だにSDK v3でS3のファイルをダウンロード/アップロードする操作に慣れないので、この記事にまとめる。
事前準備
以降のコードに必要なライブラリは、基本的に以下の一つだけでOK。
npm install @aws-sdk/client-s3
コード
S3頻出のパターンとして、以下5つがあるだろう:
- メモリ上のデータをS3にアップロード
- ファイルシステム上のデータをS3にアップロード
- メモリ上にS3のデータをダウンロード
- ファイルシステム上にS3のデータをダウンロード
- S3上のファイルを他のS3バケットにコピー/移動
それぞれのコードを下記のとおりである。
1. メモリ上からアップロード
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({}); // Bodyのデータをtest.txtとしてアップロード await s3.send( new PutObjectCommand({ Body: 'some data', // Bufferなども指定可 Bucket: process.env.BUCKET_NAME, Key: 'test.txt', }) );
2. ファイルシステム上からアップロード
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { createReadStream } from 'fs'; const s3 = new S3Client({}); // ローカル上のtest.txtをアップロード await s3.send( new PutObjectCommand({ Body: createReadStream('test.txt'), Bucket: process.env.BUCKET_NAME, Key: 'test.txt', }) );
ファイルを一度メモリ上に展開する方法も考えられるが、streamを使うほうが省メモリで済む。
備考: 対象のファイルサイズが大きな場合
対象のファイルサイズが大きな場合は、Multipart機能を使うことでより高速にアップロードできる場合がある。これを便利に使うための機能がSDK v3にはあるので、追加でインストールする。
npm i @aws-sdk/lib-storage
コードは以下:
import { S3Client } from '@aws-sdk/client-s3'; import { Upload } from "@aws-sdk/lib-storage"; import { createReadStream } from 'fs'; const s3 = new S3Client({}); const upload = new Upload({ client: s3, params: { Body: createReadStream('sample.bin'), Bucket: process.env.BUCKET_NAME, Key: 'sample.bin', }, // 性能改善用の細かなパラメータ queueSize: 10, // アップロードの並列数 partSize: 1024 * 1024 * 5, // 分割時のサイズ 全体の分割数(合計サイズ/partSize)が10000を超えないようにする }); await upload.done();
手元で500MB程度のファイルをアップロードしたところ、およそ40%速くなった。色々な条件にも依ると思うため、参考までに。こちらも参照。
3. メモリ上にダウンロード
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({}); // S3上のtest.txtをダウンロード const s3Object = await s3.send( new GetObjectCommand({ Bucket: process.env.BUCKET_NAME, Key: 'test.txt', }) ); // バイト列として取得したいときはこちら // const bytes = await s3Object.Body?.transformToByteArray(); const str = await s3Object.Body?.transformToString();
少し前までこれが少し面倒だったのが最近楽になった件は、ここにも書いた。
TIL: AWS SDK for JavaScript v3 で s3.GetObject する最新の方法 - maybe daily dev notes
4. ファイルシステム上にダウンロード
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; import {createWriteStream } from 'fs'; import { Readable } from 'stream'; const s3 = new S3Client({}); // S3上のtest.txtをダウンロードし./test.txtに保存 const s3Object = await s3.send( new GetObjectCommand({ Bucket: process.env.BUCKET_NAME, Key: 'test.txt', }) ); await new Promise((resolve, reject) => { if (s3Object.Body instanceof Readable) { s3Object.Body.pipe(createWriteStream('test.txt')) .on('error', (err) => reject(err)) .on('close', () => resolve(0)); } });
この場合は依然としてStreamを繰る必要がある。
5. S3からS3にコピー/移動
import { S3Client, CopyObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({}); // test.txtをtest_copy.txtにコピー await s3.send( new CopyObjectCommand({ Bucket: process.env.TARGET_BUCKET_NAME, Key: 'test_copy.txt', CopySource: `${process.env.SOURCE_BUCKET_NAME}/test.txt` }) ); // 移動の際は元ファイルを削除 await s3.send( new DeleteObjectCommand({ Bucket: process.env.SOURCE_BUCKET_NAME, Key: 'test.txt', }) )
コピー元であるCopySourceの指定方法がすこし特殊になる。詳細はこちら。
移動したい場合は、コピー後に元ファイルを削除する。2つの操作をアトミックに実行するS3 APIは今のところ存在しないので、整合性が重要な場合は要注意 (あまりないと思うが。)
なお、CopyObject APIを使えるのはファイルサイズが5GBまで。5GBを超えるファイルは、コピー元ファイルをダウンロードし、コピー先にアップロードする必要がある。
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; import { Upload } from '@aws-sdk/lib-storage'; const s3 = new S3Client({}); // 5GBを超えるファイルをコピーする const s3Object = await s3.send( new GetObjectCommand({ Bucket: process.env.SOURCE_BUCKET_NAME, Key: 'sample.bin', }) ); // 大きなファイルで高効率なMultipartアップロードを利用 const upload = new Upload({ client: s3, params: { Body: s3Object.Body, Bucket: process.env.TARGET_BUCKET_NAME, Key: 'sample_copy.bin', }, }); await upload.done();
注意
いくつかの疑問が生じたので、ついでに調べた:
@aws-sdk/client-s3
にS3
とS3Client
の2つあるが、どっち使えば良い?
このドキュメントに詳しく書かれている。S3
はv2と似た体験を実現するために用意されたもの。これを使うと、s3.putObject
のように xxCommand
クラスを使わずにAPIを呼べる。
一方でTree shaking観点ではこの古い方法はイマイチらしく、フロントエンドなどバンドルサイズの要求がシビアな場面では S3Client
を使う方法が好まれる。個人的には書き方を使い分けるのも面倒なので S3Client
の方で統一するのが良いと思うが、バックエンドでの利用などバンドルサイズが多少大きくても問題ない場合は書きやすい昔の記法もアリだろう。
なおこれはS3に限らず、DynamoDBやEC2など他のすべてのサービス用SDKで共通の話。
new S3({})
の{}
、必要?
必要。型定義上この引数を省略することはできない。理由は不明だが、オプショナルにするとnullチェックが追加で必要になるので、多少気持ちはわかる。このIssueで提案はされたが、特に対応されなかった模様。