AWS Amplify + ReactでS3オブジェクトのカスタムメタデータを読み書きする

2022.05.02

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AWS Amplify + React で S3オブジェクトのカスタムデータの読み書きをしてみました。

準備

React Appを作成する

アプリ名をmy-s3-controllerとしました。

$ npx create-react-app my-s3-controller
$ cd my-s3-controller

Amplifyをセットアップする

$ amplify init
? Enter a name for the environment dev
? Choose your default editor: Vim (via Terminal, macOS only)
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path:  src
? Distribution Directory Path: build
? Build Command:  npm run-script build
? Start Command: npm run-script start
Using default provider  awscloudformation
? Select the authentication method you want to use: AWS profile
? Please choose the profile you want to use [select your profile]
$ npm install aws-amplify @aws-amplify/ui-react

Amplify Authモジュールを追加する

Amplify Authモジュールを追加します。 ここではユーザーはEmailでログインする設定としました。

$ amplify auth add
Do you want to use the default authentication and security configuration? Default configuration
How do you want users to be able to sign in? Email
Do you want to configure advanced settings? No, I am done.
$ amplify push

Cognito ユーザープールが作成されます。
AWSコンソールで、作成されたユーザープールのプール IDと(*_app_clientWebのID)をメモし、IDプールのCognitoの接続設定に入力して、Cognito IDプールを作成します。

Cognito IDプール作成

Cognito IDプール作成時にロールも作成することになりますが(アプリ名がmy-s3-controllerなのでデフォルトではロール名はCognito_MyS3ControllerAuth_Roleになります)、このロールにs3:ListBuckets3:GetObjects3:PutObjectを与えておきまます。

既存のS3バケットに接続する

今回は既存のS3バケットを使いたかったので、src/index.jsに下記の修正を行います。

  import React from 'react';
  import ReactDOM from 'react-dom/client';
  import './index.css';
  import App from './App';
  import reportWebVitals from './reportWebVitals';
+ import Amplify from 'aws-amplify';
+ import awsExports from './aws-exports';
+ Amplify.configure(awsExports);
+
+ Amplify.configure({
+     Auth: {
+         identityPoolId: process.env.REACT_APP_COGNITO_IDENTITY_POOL_ID,
+         region: process.env.REACT_APP_COGNITO_REGION
+     },
+     Storage: {
+         AWSS3: {
+             bucket: process.env.REACT_APP_S3_BUCKET,
+             region: process.env.REACT_APP_S3_REGION
+         }
+     }
+ });

  const root = ReactDOM.createRoot(document.getElementById('root'));
  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );

  // If you want to start measuring performance in your app, pass a function
  // to log results (for example: reportWebVitals(console.log))
  // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
  reportWebVitals();

.envファイルを下記のように作ります。

REACT_APP_COGNITO_IDENTITY_POOL_ID=「接続するCognito IDプールのプールID
REACT_APP_COGNITO_REGION=[接続するCognitoIDプールのリージョン]
REACT_APP_S3_BUCKET=[接続するS3バケット名]
REACT_APP_S3_REGION=[接続するS3バケットのリージョン]

ここで、一旦、npm startしてhttp://localhost:3000でReactのデフォルト画面が表示されることを確認しておきます。

$ npm start

Reactのデフォルト画面

接続するS3バケットのCORS設定を行う

http://localhost:3000で稼働するアプリからS3バケットにオブジェクト(ファイル)を作るためにはCORS(Cross-Origin Resource Sharing)の設定が必要になります。

下記のようにS3バケットのCORSを設定します。

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "HEAD",
            "GET",
            "PUT",
            "POST",
            "DELETE"
        ],
        "AllowedOrigins": [
            "http://localhost:3000"
        ],
        "ExposeHeaders": [
            "x-amz-server-side-encryption",
            "x-amz-request-id",
            "x-amz-id-2",
            "ETag",
            "x-amz-meta-foo"
        ],
        "MaxAgeSeconds": 3000
    }
]

AllowedOriginsにCORSを許可するアプリのURL(http://localhost:3000)を指定します。
また、カスタムメタデータfooを読み書きしたいので、ExposeHeadersx-amz-meta-fooを追加しています。この設定を行なっておかないと、当該カスタムメタデータの読み込みができません(書き込みは設定していなくてもできます)。nameというカスタムメタデータを読み込みたい場合はx-amz-meta-nameを追加する必要があります。

認証ユーザを作る

src/App.jsを下記に修正してCognitoの認証を通してアプリを使うように変更します。

  import logo from './logo.svg';
  import './App.css';
+ import React from 'react'
+ import { Authenticator } from '@aws-amplify/ui-react';
+ import '@aws-amplify/ui-react/styles.css';

- function App() {
+ const App = () => {
    return (
-     <div className="App">
-       <header className="App-header">
-         <img src={logo} className="App-logo" alt="logo" />
-         <p>
-           Edit <code>src/App.js</code> and save to reload.
-         </p>
-         <a
-           className="App-link"
-           href="https://reactjs.org"
-           target="_blank"
-           rel="noopener noreferrer"
-         >
-           Learn React
-         </a>
-       </header>
-     </div>
-   );
+     <Authenticator>
+       {({ signOut, user }) => (
+         <div className="App">
+           <header className="App-header">
+             <img src={logo} className="App-logo" alt="logo" />
+             <p>
+               Edit <code>src/App.js</code> and save to reload.
+             </p>
+             <a
+               className="App-link"
+               href="https://reactjs.org"
+               target="_blank"
+               rel="noopener noreferrer"
+             >
+               Learn React
+             </a>
+           </header>
+         </div>
+       )}
+     </Authenticator>
+   )
  }

- export default App;
+ export default App

npm startして、アプリにアクセスするとCognitoの認証画面が表示されます。
アプリを利用するために、Create Accountタブからユーザーを作成します。

Cognitoユーザ作成

書き込み

AmplifyのStorage.putメソッドを使うことでS3にオブジェクトを作成できます。
この際に、カスタムメタデータはmetadataキーで指定します。カスタムメタデータfooの値に日本語を使えるようにencodeURIComponentでエンコードします。

  const uploadFile = async(file, foo) => {
    if (!foo) return
    if (!file) return

    const fileName = Date.now() + '.' + file.type.replace(/(.*)\//g, '')
    const result = await Storage.put(fileName, file, {
      level: 'protected',
      contentType: file.type,
      metadata: { foo: encodeURIComponent(foo) }
    })
    if (result) {
      console.log(result)
    } else {
      console.error(result.error)
    }

uploadFileはS3にアップロードするファイルのfileオブジェクトとfooの値を受け取ってStorage.putを呼び出しています。 また、levelprotectedを指定して他のユーザがアップしたファイルも読み取れる指定をしています(ただし、identityIdも指定しないと、protectedの場合は他のユーザがアップしたファイルは読み取れません)。

Storage - Upload files - JavaScript - AWS Amplify Docs

読み込み

S3にアップロードしたオブジェクトの読み込みにはAmplifyではStorage.getメソッドやStorage.listメソッド提供されています。しかし、これらのメソッドでカスタムメタデータを読み出すことは2022/05/01現在できません。したがって、カスタムメタデータを読み出したい場合は、S3 Client - AWS SDK for JavaScript v3を使う必要があります。

AWS SDKをインストールします。

$ npm install aws-sdk

カスタムメタデータの読み出しは次のように行います。

import React from 'react'
import { Auth } from 'aws-amplify'
import { S3Client, HeadObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3'

const fetchS3Objects = async (bucket) => {
  try {
    const s3client = new S3Client({
      region: process.env.REACT_APP_S3_REGION,
      credentials: await Auth.currentCredentials()
    })
    const output = await s3client.send(
      new ListObjectsV2Command({
        Bucket: bucket
      })
    )
    if (!output.Contents) return []

    const heads = []
    for (let i =0; i < output.Contents.length; i++) {
      const c = output.Contents[i]
      const head = await s3client.send(
        new HeadObjectCommand({
          Bucket: bucket,
          Key: c.Key
        })
      )
      heads.push({ foo: decodeURIComponent(head.Metadata.foo) })
    }
    return heads
  } catch (err) {
    console.error(err)
  }
}

ListObjectsV2Commandメソッドでバケット内にあるオブジェクトの一覧情報を得ます。ここには各オブジェクトのKeyが含まれているので、さらに各Keyに対して、HeadObjectCommandメソッドを呼び出しカスタムメタデータをhead.Metadata.fooで取得しています。

ここで気を付ける必要があるのは、ListObjectsV2Command{ level: 'protected' }で制限をかけたオブジェクトだけではなく、バケット内の全オブジェクトを取得するということです。ですので、出力を{ level: 'protected' }のオブジェクトに限定したい場合は、別途Storage.listメソットなどでprotectedに限定された一覧を取得して、ListObjectsV2Commandで得たカスタムメタデータをマージする必要があります。

まとめ

AWS Amplify + ReactでS3オブジェクトのカスタムメタデータを読み書きする方法に関して記述しました。
カスタムメタデータの書き込みはシンプルなのですが、読み込みはCORSのヘッダの設定やaws-sdkの利用など実現には一工夫が必要でした。
参考になれば幸いです。