JavaScriptでaws-s3互換のオブジェクトストレージを使う
コピペできるような記事がなかなか見つからなかったので備忘録
とはいえv3のドキュメントを読めという話ではある(日本語ユーザーガイドも参考になる)。
共通
npm i @aws-sdk/client-s3
npm i @aws-sdk/s3-request-presigner
import { S3Client } from "@aws-sdk/client-s3";
const client = new S3Client({
region: "auto", // サービスによって設定したりしなかったり
endpoint: "<https://example.com>",
credentials: {
accessKeyId: "<accessKeyId>",
secretAccessKey: "<secretAccessKey>",
},
});
ブラウザから試す場合はCORS設定を事前にしておく。
バケット関連
HEADで導通確認
import { HeadBucketCommand } from "@aws-sdk/client-s3";
const res = await client.send(
new HeadBucketCommand({
Bucket: "<bucketName>",
})
);
返り値
{"$metadata":{"httpStatusCode":200,"attempts":1,"totalRetryDelay":0}}
CORS
取得
import { GetBucketCorsCommand } from "@aws-sdk/client-s3";
const res = await client.send(
new GetBucketCorsCommand({
Bucket: "<bucketName>",
})
);
返り値
{
"$metadata": { "httpStatusCode": 200, "attempts": 1, "totalRetryDelay": 0 },
"CORSRules": [
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["HEAD", "GET", "PUT"],
"AllowedOrigins": ["*"]
}
]
}
この例ではどのドメインでもブラウザからHEAD,GET,PUTが通るようになっている。
更新
import { PutBucketCorsCommand } from "@aws-sdk/client-s3";
const res = await client.send(
new PutBucketCorsCommand({
Bucket: "<bucketName>",
CORSConfiguration: {
CORSRules: [
{
AllowedHeaders: ["*"],
AllowedMethods: ["GET", "PUT"],
AllowedOrigins: ["*"],
},
],
},
})
);
この例ではHEADをできなく変更している。
ACL
取得
import { GetBucketAclCommand } from "@aws-sdk/client-s3";
const res = await client.send(
new GetBucketAclCommand({
Bucket: "<bucketName>",
})
);
返り値
{
"$metadata": { "httpStatusCode": 200, "attempts": 1, "totalRetryDelay": 0 },
"Grants": [
{
"Grantee": {
"DisplayName": "113401905553",
"ID": "87177022dd1cae447e0dae5cee78d1e2a574be4a887182a03be04216cc3f5ae7",
"Type": "CanonicalUser"
},
"Permission": "FULL_CONTROL"
}
],
"Owner": {
"DisplayName": "113401905553",
"ID": "87177022dd1cae447e0dae5cee78d1e2a574be4a887182a03be04216cc3f5ae7"
}
}
privateな状態の例
この1つだけ返ってきているGranteeは自分(Ownerを見るとわかる)。
変更
import { PutBucketAclCommand } from "@aws-sdk/client-s3";
const res = await client.send(
new PutBucketAclCommand({
Bucket: "<bucketName>",
ACL: "public-read",
})
);
例ではACL
で既定ACLと呼ばれる事前定義済みの設定に変更している。
既定ACLはawsのuserguideで確認できるが、s3互換サービスによっては全てに対応しているとは限らない。
返り値
{
"$metadata": { "httpStatusCode": 200, "attempts": 1, "totalRetryDelay": 0 },
"Grants": [
{
"Grantee": {
"DisplayName": "113401905553",
"ID": "87177022dd1cae447e0dae5cee78d1e2a574be4a887182a03be04216cc3f5ae7",
"Type": "CanonicalUser"
},
"Permission": "FULL_CONTROL"
},
{
"Grantee": {
"URI": "http://acs.amazonaws.com/groups/global/AllUsers",
"Type": "Group"
},
"Permission": "READ"
}
],
"Owner": {
"DisplayName": "113401905553",
"ID": "87177022dd1cae447e0dae5cee78d1e2a574be4a887182a03be04216cc3f5ae7"
}
}
public-read
の例
オブジェクト関連
オブジェクト一覧例
/
├ test.txt
└ test/
├ test.png
├ test.jpg
├ fuga.txt
└ hello/
└ hoge.txt
オブジェクト関連の一覧を取得する
ListObjectsCommand
とListObjectsV2Command
があってどちらを使えばいいか最初悩むが、ドキュメントには
We recommend that you use the newer version, ListObjectsV2, when developing applications.
とあるのでListObjectsV2Command
を使う。
基本
import { ListObjectsV2Command } from "@aws-sdk/client-s3";
const res = await client.send(
new ListObjectsV2Command({
Bucket: "<bucketName>",
})
);
返り値
{
"$metadata": { "httpStatusCode": 200, "attempts": 1, "totalRetryDelay": 0 },
"Contents": [
{
"Key": "test.txt",
"LastModified": "2022-10-21T09:24:15.296Z",
"ETag": "\"b1946ac92492d2347c6235b4d2611184\"",
"Size": 6,
"StorageClass": "STANDARD"
},
{
"Key": "test/",
"LastModified": "2022-10-21T08:30:04.985Z",
"ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
"Size": 0,
"StorageClass": "STANDARD"
},
{
"Key": "test/fuga.txt",
"LastModified": "2023-05-02T06:38:33.722Z",
"ETag": "\"a2cfcdee74a77c65ce45669bca03b730\"",
"Size": 8199,
"StorageClass": "STANDARD"
},
{
"Key": "test/hello/",
"LastModified": "2022-10-25T07:31:56.767Z",
"ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
"Size": 0,
"StorageClass": "STANDARD"
},
{
"Key": "test/hello/hoge.txt",
"LastModified": "2023-05-02T14:56:26.569Z",
"ETag": "\"a2cfcdee74a77c65ce45669bca03b730\"",
"Size": 8199,
"StorageClass": "STANDARD"
},
{
"Key": "test/test.png",
"LastModified": "2022-10-21T08:43:10.390Z",
"ETag": "\"e28a58536e3dc82f8171fb4442e65591\"",
"Size": 496580,
"StorageClass": "STANDARD"
},
{
"Key": "test/test.jpg",
"LastModified": "2022-10-21T09:24:43.675Z",
"ETag": "\"b1946ac92492d2347c6235b4d2611184\"",
"Size": 6,
"StorageClass": "STANDARD"
}
],
"ContinuationToken": "",
"IsTruncated": false,
"KeyCount": 7,
"MaxKeys": 1000,
"Name": "<bucketName>",
"NextContinuationToken": "",
"Prefix": ""
}
返り値の最大数が決まっている。
この例では1000。それ以下であれば、返す数をリクエスト時にMaxKeys
で指定できる。
条件を指定
階層ごとに取得したい場合はDelimiterを指定する例
const res = await client.send(
new ListObjectsV2Command({
Bucket: "<bucketName>",
Delimiter: "/",
Prefix: "",
})
);
返り値
{
"$metadata": { "httpStatusCode": 200, "attempts": 1, "totalRetryDelay": 0 },
"CommonPrefixes": [{ "Prefix": "test/" }],
"Contents": [
{
"Key": "test.txt",
"LastModified": "2022-10-21T09:24:15.296Z",
"ETag": "\"b1946ac92492d2347c6235b4d2611184\"",
"Size": 6,
"StorageClass": "STANDARD"
}
],
"ContinuationToken": "",
"Delimiter": "/",
"IsTruncated": false,
"KeyCount": 1,
"MaxKeys": 1000,
"Name": "<bucketName>",
"NextContinuationToken": "",
"Prefix": ""
}
その階層直下のディレクトリはCommonPrefixes
として返る。
ディレクトリ(Prefix)とMaxKeysを指定した例
const res = await client.send(
new ListObjectsV2Command({
Bucket: "<bucketName>",
MaxKeys: 2,
Prefix: "test/",
})
);
返り値
{
"$metadata": { "httpStatusCode": 200, "attempts": 1, "totalRetryDelay": 0 },
"Contents": [
{
"Key": "test/",
"LastModified": "2022-10-21T08:30:04.985Z",
"ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
"Size": 0,
"StorageClass": "STANDARD"
},
{
"Key": "test/fuga.txt",
"LastModified": "2023-05-02T06:38:33.722Z",
"ETag": "\"a2cfcdee74a77c65ce45669bca03b730\"",
"Size": 8199,
"StorageClass": "STANDARD"
}
],
"ContinuationToken": "",
"IsTruncated": true,
"KeyCount": 2,
"MaxKeys": 2,
"Name": "<bucketName>",
"NextContinuationToken": "dGVzdC9BUEkudHh0",
"Prefix": "test/"
}
MaxKeys=2
以上にオブジェクトが存在するためNextContinuationToken
が返る。
続きを取得するにはListObjectsV2Command
にContinuationToken
として与える。
空文字列""
になるまで続ければすべてを取得できる。
const res1 = await client.send(
new ListObjectsV2Command({
Bucket: "<bucketName>",
MaxKeys: 2,
Prefix: "test/",
})
);
const res2 = await client.send(
new ListObjectsV2Command({
Bucket: "<bucketName>",
MaxKeys: 2,
ContinuationToken: res1.NextContinuationToken,
})
);
ACL
バケットのACLと同様
取得
import { GetObjectAclCommand } from "@aws-sdk/client-s3";
const res = await client.send(
new GetObjectAclCommand({
Bucket: "<bucketName>",
Key: "test/test.png",
})
);
返り値
{
"$metadata": { "httpStatusCode": 200, "attempts": 1, "totalRetryDelay": 0 },
"Grants": [
{
"Grantee": {
"DisplayName": "113401905553",
"ID": "87177022dd1cae447e0dae5cee78d1e2a574be4a887182a03be04216cc3f5ae7",
"Type": "CanonicalUser"
},
"Permission": "FULL_CONTROL"
}
],
"Owner": {
"DisplayName": "113401905553",
"ID": "87177022dd1cae447e0dae5cee78d1e2a574be4a887182a03be04216cc3f5ae7"
}
}
変更
import { PutObjectAclCommand } from "@aws-sdk/client-s3";
const res = await client.send(
new PutObjectAclCommand({
Bucket: "<bucketName>",
Key: "test/test.png",
ACL: "public-read",
})
);
オブジェクトのダウンロード
GetObjectCommand
import { GetObjectCommand } from "@aws-sdk/client-s3";
const res = await client.send(
new GetObjectCommand({
Bucket: "<bucketName>",
Key: "test/test.png",
})
);
// ブラウザ上で画像をダウンロードしている例
if (res.Body) {
const blob = new Blob([await res.Body.transformToByteArray()]);
const link = document.createElement("a");
link.setAttribute("target", "_blank");
link.download = "test.png";
link.href = URL.createObjectURL(blob);
link.click();
URL.revokeObjectURL(link.href);
}
createObjectURLを使う以上、この方法ではあまり大きなファイルをダウンロードできない。
署名付きURLを使う
import { GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const url = await getSignedUrl(
client,
new GetObjectCommand({
Bucket: "<bucketName>",
Key: "test/test.png",
}),
{
expiresIn: 60,
}
);
// ブラウザ上で画像をダウンロードしている例
const filepaths = url.split("?")[0].split("/");
const filename = filepaths[filepaths.length - 1];
const link = document.createElement("a");
link.setAttribute("target", "_blank");
link.download = filename;
link.href = url;
link.click();
typescriptの場合、client-s3
とs3-request-presigner
のバージョンがズレているとgetSignedUrl
で引数エラーが出ることがある。
オブジェクトのアップロード
PutObjectCommandを使う
import { PutObjectCommand } from "@aws-sdk/client-s3";
const file = e.target.files.item(0); // eはinputのonChangeメソッドの引数想定
const res = await client.send(
new PutObjectCommand({
Bucket: "<bucketName>",
Key: file.name,
Body: file,
})
);
PutObjectCommandは簡単だが最大5GBまで。
multipartUploadを使う
5GBを超えるファイルはマルチパートアップロードを使う。
注意
実行する前にCORSに以下の設定を加えないとUploadPart
の返り値のEtagがundefinedとなり、completeでエラーになる。
{
"ExposeHeaders": ["ETag"]
}
const create = await client.send(
new CreateMultipartUploadCommand({
Bucket: "<bucketName>",
Key: file.name, // ディレクトリを堀りたければ名前の前につける
})
);
const uploadId = create.UploadId;
const chunkSize = 1024 * 1024 * 32;
const fileSize = file.size;
const maxPart = Math.ceil(fileSize / chunkSize);
const partsInfo = [];
for (let part = 0; part < maxPart; part++) {
const slice = file.slice(part * chunkSize, (part + 1) * chunkSize);
const res = await client.send(
new UploadPartCommand({
Body: slice,
Bucket: "<bucketName>",
Key: file.name,
UploadId: uploadId,
PartNumber: part + 1,
})
);
partsInfo.push({
PartNumber: part + 1,
ETag: res.ETag,
});
}
const complete = await client.send(
new CompleteMultipartUploadCommand({
Bucket: "<bucketName>",
Key: file.name,
UploadId: uploadId,
MultipartUpload: {
Parts: partsInfo,
},
})
);
オブジェクトの削除
const res = await client.send(
new DeleteObjectCommand({
Bucket: "<bucketName>",
Key: "<filePath>", // test/hello/hoge.txt
})
);
Discussion