📚

JavaScriptでaws-s3互換のオブジェクトストレージを使う

2023/05/06に公開

コピペできるような記事がなかなか見つからなかったので備忘録

とはいえ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

オブジェクト関連の一覧を取得する

ListObjectsCommandListObjectsV2Commandがあってどちらを使えばいいか最初悩むが、ドキュメントには
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が返る。
続きを取得するにはListObjectsV2CommandContinuationTokenとして与える。
空文字列""になるまで続ければすべてを取得できる。

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-s3s3-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