【AWS Amplify × Vue.js 簡単サーバーレスアプリ構築チュートリアル】⑤ StorageでS3に画像保存実装編

# はじめに
この記事は、全部で6記事にわたるたチュートリアルの5つ目です。

[① 概要説明編](https://qiita.com/minato-naka/items/7b445bcf0794189e65a0)
[② Amplify利用準備・初期設定編](https://qiita.com/minato-naka/items/8e5c0999867d170a5f66)
[③ Authでユーザー登録、ログイン機能実装編](https://qiita.com/minato-naka/items/4e4dcfea2bf5231f6c86)
[④ API（Graphql）でCRUD実装編](https://qiita.com/minato-naka/items/03da3987d5a81dfe865f)
[⑤ StorageでS3に画像保存実装編](https://qiita.com/minato-naka/items/abffc5e1e949a6fea494)
↑↑↑今ここ↑↑↑
[⑥ Hostingでアプリ公開＆自動デプロイ実装編](https://qiita.com/minato-naka/items/3123950aa852ee3250fe)

前回の記事では、AmplifyのAPI機能を使って
基本のCRUD機能を一通り実装しました。

今回はAmplifyのStorage機能を使って
S3に画像を登録する機能を実装していきます。


今回も、
Amplify利用準備・初期設定編の[Amplify機能の利用手順](https://qiita.com/minato-naka/items/8e5c0999867d170a5f66#amplify%E6%A9%9F%E8%83%BD%E3%81%AE%E5%88%A9%E7%94%A8%E6%89%8B%E9%A0%86)で説明した通り、
①AmplifyCLIのaddでバックエンド設定追加
②AmplifyCLIのpushでバックエンドリソース作成
③フロントエンドにバックエンド連携コード実装
の3ステップでStorage機能を追加していきます。


# AmplifyCLIのaddでバックエンド設定追加

ターミナルで下記コマンドを実行します。
`amplify add storage`

すると質問がいろいろ表示されるので
下記のように回答しましょう。


`? Please select from one of the below mentioned services:` → ` Content (Images, audio, video, etc.)`
`? Please provide a friendly name for your resource that will be used to label this category in the project: ` → `amplifyvuealbum`
`? Please provide bucket name:` → ` amplify-vue-album`
`? Who should have access:` → ` Auth users only`
`? What kind of access do you want for Authenticated users?` → ` create/update, read, delete`
`? Do you want to add a Lambda Trigger for your S3 Bucket?` → ` No`

下記メッセージが表示され、バックエンド設定ファイルが更新されます。

```
Successfully added resource amplifyvuealbum locally
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/f0c1b56b290066e5502a5177fac5e8fc94f96545)

さらに、前回のAPI編でテーブルを作成しましたが
Photoテーブルのs3keyカラムは追加していませんでした。
今回このカラムを利用するので、
schema.graphqlを編集してs3keyカラムを追加しましょう。

```diff:amplify/backend/api/amplifyvuealbum/schema.graphql
 type Photo
   @model
   @auth(rules: [{ allow: owner }])
   @key(name: "byAlbum", fields: ["albumID"]) {
   id: ID!
   name: String!
+  s3key: String!
   albumID: ID!
   album: Album @connection(fields: ["albumID"])
 }
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/bc564fa483c4ac000ebdca913a7a52f78b0a437f)


# AmplifyCLIのpushでバックエンドリソース作成

これでバックエンド設定は作成完了なので
Storageの追加と、テーブル定義の変更をpushして
実際にAWS上に反映させましょう。


下記コマンドを実行します。
`amplify push`

確認が表示されるのでyesにします。

```
| Category | Resource name           | Operation | Provider plugin   |
| -------- | ----------------------- | --------- | ----------------- |
| Storage  | amplifyvuealbum         | Create    | awscloudformation |
| Api      | amplifyvuealbum         | Update    | awscloudformation |
| Auth     | amplifyvuealbum921b97fd | No Change | awscloudformation |
? Are you sure you want to continue? Yes
```

さらに、今回はテーブル定義を変更したため、
それに伴ってquery、mutationも自動で修正するかどうかを聞かれます。
yesと答えて自動更新しましょう。

```
? Do you want to update code for your updated GraphQL API Yes
? Do you want to generate GraphQL statements (queries, mutations and subscription) based on your schema types?
This will overwrite your current graphql queries, mutations and subscriptions Yes
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/9592cdbe9cf09eb62a5cf921c43e375a4f68c4ea)


しばらく待って処理が完了すると、
実際にS3バケットが作成されているはずなので、
AWS上で実際に確認してみましょう。


# フロントエンドにバックエンド連携コード実装

バックエンドにS3の準備ができたので、
実際に画像をS3に登録、取得するためのコードを実装していきます。


写真登録ページへの画像登録フォームの追加と
アルバム詳細ページへの写真一覧表示、削除機能の追加をします。


・写真登録

```diff:src/views/photo/Create.vue
  <template>
    <div>
      <h1>写真登録</h1>
      <form @submit.prevent="submitCreate">
        <label>Name</label><br />
        <input v-model="form.name" placeholder="Enter photo name" /><br />
-       <p>ここに画像登録フォーム</p>
+       <label>Photo</label><br />
+       <input type="file" @change="onFileChange" /><br />
        <input type="submit" value="Submit" />
      </form>
    </div>
  </template>
  <script>
  import { API } from "aws-amplify";
  import { createPhoto } from "../../graphql/mutations";
+ import { Storage } from "aws-amplify";
  export default {
    name: "PhotoCreate",
    props: {
      albumId: String,
    },
    data() {
      return {
        form: {
          albumID: this.albumId,
          name: "",
+         s3key: "",
        },
+       image: null,
      };
    },
    methods: {
+     onFileChange(e) {
+       this.image = e.target.files[0];
+     },
      async submitCreate() {
+       const s3key = new Date().getTime().toString(16) + this.image.name;
+       await Storage.put(s3key, this.image, {
+         level: "private",
+       })
+         .then((result) => {
+           this.form.s3key = result.key;
+         })
+         .catch((error) => {
+           console.log(error);
+         });
        await API.graphql({
          query: createPhoto,
          variables: { input: this.form },
        })
          .then((result) => {
            console.log(result);
            this.$router.push({
              name: "AlbumShow",
              params: { albumId: this.albumId },
            });
          })
          .catch((error) => {
            console.log(error);
          });
      },
    },
  };
  </script>
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/86b2ae72ea16e6968ed321fbf4b5e353884fb813)


・アルバム詳細

```diff:src/views/album/Show.vue
  <template>
    <div>
      <h1>アルバム詳細</h1>
      <h2>{{ album.name }}</h2>
      <router-link
        custom
        v-slot="{ navigate }"
        :to="{ name: 'PhotoCreate', params: { albumId: albumId } }"
      >
        <button @click="navigate">Add Photo</button>
      </router-link>
      <table border="1">
        <tr v-for="(photo, index) in album.photos.items" :key="photo.id">
          <td>{{ photo.name }}</td>
          <td>
-           ここに画像表示
+           <amplify-s3-image :img-key="photo.s3key" level="private" />
          </td>
          <td>
            <button @click="deletePhoto(index, photo)">Delete Photo</button>
          </td>
        </tr>
      </table>
    </div>
  </template>
  <script>
  import { API } from "aws-amplify";
  import { getAlbum } from "../../graphql/queries";
  import { deletePhoto } from "../../graphql/mutations";
+ import { Storage } from "aws-amplify";
  export default {
    name: "AlbumShow",
    props: {
      albumId: String,
    },
    async created() {
      this.getAlbum();
    },
    data() {
      return {
        album: {
          name: null,
          photos: []
        },
      };
    },
    methods: {
      async getAlbum() {
        await API.graphql({
          query: getAlbum,
          variables: { id: this.albumId },
        })
          .then((result) => {
            console.log(result);
            this.album = result.data.getAlbum;
          })
          .catch((error) => {
            console.log(error);
          });
      },
      async deletePhoto(index, photo) {
        if (!confirm("Delete Photo?")) return;
+       await Storage.remove(photo.s3key, {
+         level: "private",
+       })
+         .then((result) => {
+           console.log(result);
+         })
+         .catch((error) => {
+           console.log(error);
+         });
        await API.graphql({
          query: deletePhoto,
          variables: { input: { id: photo.id } },
        })
          .then((result) => {
            console.log(result);
            this.album.photos.items.splice(index, 1);
          })
          .catch((error) => {
            console.log(error);
          });
      },
    },
  };
  </script>


+ <style scoped>
+ amplify-s3-image {
+   --height: 100px;
+ }
+ </style>

```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/f571177161a4d9494186d2f2df0b2d18571e9fd3)


これで、AmplifyのStorage機能を使ってS3画像を登録、表示、削除する
コードは完成です。

簡単にポイントを解説します。


・写真登録ページ
まずAmplifyのStorage機能を使うため、Storageモジュールをインポートしています。
`import { Storage } from "aws-amplify";`

そして、フォーム送信の `submitCreate()` メソッドで、
下記のコードによりS3への画像登録を行っています。

```
Storage.put(s3key, this.image, {
        level: "private",
      })
```

Storageモジュールのputメソッドに、s3keyとファイルデータ、オプションを渡すだけです。
s3keyは任意の文字列で問題ないです。
（今回はランダム文字列 + 実際の画像ファイル名をs3keyとしてセットしています）
this.imageには今回追加した画像フォームでアップロードしたファイルデータが入っています。
オプションには、privateをセットすることで、画像をアップしたユーザ自身しかその画像を参照更新できない設定にしています。

これで、Storage.putの次にあるAPIによるPhotoデータ登録時に、
先ほどカラム追加したs3keyもDBに登録されるようになっています。


・アルバム詳細
アルバム詳細の方では、S3画像を表示する機能と削除する機能の2つを追加しています。

S3画像の表示はこの1行だけで実装できます。
`<amplify-s3-image :img-key="photo.s3key" level="private" />`

この `amplify-s3-image` というタグは、
今回利用しているAmplifyのUIライブラリが用意してくれているコンポーネントです。
https://docs.amplify.aws/ui/storage/s3-image/q/framework/vue

`:img-key` に表示したい画像のS3キーをセットするだけでS3の画像を表示できちゃいます。
（先ほどs3keyはPhotoデータに登録されるようになっているので、ここで取得できる）
※そのままだとアップロードした画像のサイズそのまま表示されてレイアウトが崩れるので、一番下にサイズを統一するCSSを書いています

画像削除機能の方では、もともと作っていた `deletePhoto()` のメソッドに
下記コードを追加するだけです。

```
Storage.remove(photo.s3key, {
        level: "private",
      })
```

Storageのremoveメソッドにs3keyとオプションを渡すだけです。
これで、S3上から画像を削除することができます。


実際に画像の登録、表示、削除の機能をいろいろ動かしてみましょう。
※動作確認時にはPhotoを1件も登録していない（または新品の）アルバムで確認してください
（既存のPhotoデータはs3keyカラムがnullの状態なので、エラーが発生してしまいます）

画像登録をすると、
s3バケットに「/private」というディレクトリが作られ、
その配下にユーザーごとのディレクトリが作られ、
その下に画像データが登録されます。
AWSのS3のページに行って実際に確認してみるといいです。

また、画像を削除したらS3上からもその画像が削除されることを確認してみましょう。

これで、AmplifyのStorage機能を利用した
S3への画像登録、表示、削除機能の実装が完了です。

# おわりに

これで、Auth、API、Storageの機能が導入されたので、
アプリの機能としてすべて実装完了です。

最後に、次の記事でAmplifyのHosting機能を利用して
アプリ公開する方法、GitHubと連携して自動デプロイする方法を解説します。
（次に進む前に、LGTMしてもらえるとうれしいです）
[【AWS Amplify × Vue.js 簡単サーバーレスアプリ構築チュートリアル】⑥ Hostingでアプリ公開＆自動デプロイ実装編](https://qiita.com/minato-naka/items/3123950aa852ee3250fe)