Amplify Gen2のStorage機能を使ってみた-③ダウンロードと削除-

どうも、駆け出し12冠エンジニアのアスカです。Amplify Gen2がGAしてドキュメントのStorage機能に関する部分が充実していたので、それを参考にしながら触ってみます！
[Amplify Gen2のドキュメント](https://docs.amplify.aws/react/build-a-backend/storage/download-files/)

想像以上にボリュームがあるので、連載の形をとっています。今回は画像表示、ダウンロードと削除の機能を触っていきたいと思います。連載なので、1つ前の記事の内容はある程度前提に書いていくので、分からない部分はあればご参照ください。
参考:
[Amplify Gen2のStorage機能を使ってみた-①最初のアップロードまで-](https://qiita.com/asuka0708japan/items/dea93b7d8acf4a7800f6)
[Amplify Gen2のStorage機能を使ってみた-②アップロードとリスト-](https://qiita.com/asuka0708japan/items/819e80c4087e9ce678a7)
# 画像表示
## Storage Image React UI Componentを使ってみる
ドキュメントでは以下のようなサンプルコードが記載されています。
```typescript:
import { StorageImage } from '@aws-amplify/ui-react-storage';

export const DefaultStorageImageExample = () => {
  return <StorageImage alt="cat" path="your-path/cat.jpg" />;
};
```
どうやらpathに指定した写真をaltのタイトルで表示するようです。[UIのドキュメント](https://ui.docs.amplify.aws/react/connected-components/storage/storageimage)を参考にすると他にも多数プロパティがありました。
| Name                   | Description                                                                                                                                       | Type                                                        |
|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------|
| `imgKey`               | Deprecated, use `path` instead. The key of an image. [More Info](https://docs.amplify.aws/gen1/react/build-a-backend/storage/download/)            | `string`                                                    |
| `accessLevel`          | Deprecated, use `path` instead. Access level for files in Storage. [More Info](https://docs.amplify.aws/gen1/react/build-a-backend/storage/configure-access/) | `'guest'` \| `'protected'` \| `'private'`                  |
| `identityId?`          | Deprecated, use `path` instead. The unique Amazon Cognito Identity ID of the image owner.                                                         | `string`                                                    |
| `fallbackSrc?`         | A fallback image source to be loaded when the component fails to load the image from Storage                                                      | `string`                                                    |
| `onGetUrlError?`       | Triggered when an error happens calling Storage.get                                                                                               | `(error: Error) => void`                                    |
| `onStorageGetError?`   | Deprecated, use `onGetUrlError` instead. Triggered when an error happens calling Storage.get                                                      | `(error: Error) => void`                                    |
| `validateObjectExistence?` | Whether to check for the existence of a file. Defaults to true. [More Info](https://docs.amplify.aws/react/build-a-backend/storage/download-files/#more-geturl-options) | `boolean`                                                   |

storage画像の表示のレベルでaccessLevelが定義出来るの楽ですね。簡単な所から試してみましょう。
[Amplify Gen2のStorage機能を使ってみた-②アップロードとリスト-](https://qiita.com/asuka0708japan/items/819e80c4087e9ce678a7)
のpublic配下の画像をリストしたコードをベースにパスの羅列から画像の羅列に変更してみます。
まず、image_list.tsxを定義してみます。
```typescript:src/components/Lists/image_list.tsx
import React, { useState, useEffect } from 'react';
import { list } from 'aws-amplify/storage';
import { StorageImage } from '@aws-amplify/ui-react-storage';

const ImageList = () => {
  const [files, setFiles] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchFiles = async () => {
      try {
        const result = await list({
          path: 'public/'
        });
        console.log(result);
        setFiles(result.items); 
      } catch (err) {
        console.error(err);
        setError(err.message);
      }
    };

    fetchFiles();
  }, []);

  return (
    <div>
      <h1>File List</h1>
      {error && <p>Error fetching files: {error}</p>}
      <ul>
        {files.map((file, index) => (
          <li key={index}>
            <StorageImage alt={file.lastModified} path={file.path} />
          </li>
        ))}
      </ul>
    </div>
  );
};

export default ImageList;
```
上記のコードではリストで取得したファイルを表示しています。
これをapp.tsxに入れてみます。
```typescript:src/App.tsx
import { Authenticator } from '@aws-amplify/ui-react'
import '@aws-amplify/ui-react/styles.css'
import { uploadData } from 'aws-amplify/storage';
import {DefaultStorageManagerExample } from '../src/components/Elements/Buttons/uploadbutton';
import ImageList from '../src/components/Elements/Lists/image_list'


function App() {
  const handleUpload = () => {
    if (file) { // file が undefined でないことを確認
      uploadData({
        path: `picture-submissions/${file.name}`,
        data: file,
      })
      alert(`${file.name}がアップロードされました。`)
  } else{
    alert(`ファイルが選択されていません。`)
  }; 
};

  return (
    <Authenticator>
      {({ signOut, user }) => (
    <main>
      <h1> Welcome to {user?.signInDetails?.loginId}</h1>
      <button onClick={signOut}>Sign out</button>
   
      <DefaultStorageManagerExample/>
      <ImageList/>
    </main>       
    )}
    </Authenticator>
  );
}

export default App;

```
image_list.tsxで定義したImageListをApp.tsxでインポートして以下のように画像を表示出来ました。
しかし、画像のサイズがそのまま表示されるのでUIがめちゃめちゃになります。
![画像](https://d39oe90ddnxn3x.cloudfront.net/02_amplifygen2_storage/03_amplifygen2_storage_download_delete/02_03_amplifygen2_storage_download_delete01.png)
例えばpropsにGet時のHookがあれば画像サイズ調整も可能なのですが、エラー発生時のHookしかないので、このUIを使う場合はアップロードにサイズの調整をしておくしかない気がします。こういった仕様が分かるだけでも今回触った価値はあったかと思います。
# ダウンロード
署名付URLからダウンロードする形で以下のようなサンプルがドキュメントに挙げられています。
```typescript:
import { getUrl } from 'aws-amplify/storage';

const linkToStorageFile = await getUrl({
  path: "album/2024/1.jpg",
  // Alternatively, path: ({identityId}) => `album/{identityId}/1.jpg`
  options: {
    validateObjectExistence?: false,  // defaults to false
    expiresIn?: 20 // validity of the URL, in seconds. defaults to 900 (15 minutes) and maxes at 3600 (1 hour)
    useAccelerateEndpoint: true; // Whether to use accelerate endpoint.
  },
});
console.log('signed URL: ', linkToStorageFile.url);
console.log('URL expires at: ', linkToStorageFile.expiresAt);
```

```typescript:
<a href={linkToStorageFile.url.toString()} target="_blank" rel="noreferrer">
  {fileName} 
</a>
```
今回はもう一つのdownloadData APIを利用してみます。ドキュメントのサンプルは以下です。
```typescript
import { downloadData } from 'aws-amplify/storage';

// Downloads file content to memory
const { body, eTag } = await downloadData({
  path: "/album/2024/1.jpg",
  options: {
    // optional progress callback
    onProgress: (event) => {
      console.log(event.transferredBytes);
    }
    // optional bytes range parameter to download a part of the file, the 2nd MB of the file in this example
    bytesRange: {
      start: 1024,
      end: 2048
    }
  }
}).result;
```

さて、上記サンプルを参考にしつつ、downloadbutton.tsxでダウンロードボタンを作成してimage_list.tsxに追加してみます。
```diff_typescript:src/components/Lists/image_download_button.tsx
// ImageDownloadLink.js
import React from 'react';
import { downloadData } from 'aws-amplify/storage';

const ImageDownloadButton = ({ fileName }) => {
  const handleDownload = async () => {
    try {
      // Assuming downloadData works as described, fetching file content directly.
      const download = await downloadData({
        path: fileName,
        options: {
          onProgress: (event) => {
            console.log(`Downloaded ${event.transferredBytes} bytes`);
          },
          bytesRange: {
            start: 0, // Assuming you want to download the whole file, adjust as necessary
            end: undefined // Not setting an end to download the entire file
          }
        }
      });

      // Assuming download.result.body holds the binary data of the file
      const blob = new Blob([download.result.body], { type: 'application/octet-stream' });
      const url = window.URL.createObjectURL(blob);

      // Creating an anchor tag to facilitate the download
      const link = document.createElement('a');
      link.href = url;
      link.setAttribute('download', fileName.split('/').pop()); // Extracts file name for the download attribute
      document.body.appendChild(link);
      link.click(); // Programmatically clicks the link to trigger the download
      link.parentNode.removeChild(link); // Clean up by removing the link from the DOM
    } catch (error) {
      console.error('Error downloading the file:', error);
      alert('Failed to download the file.');
    }
  };

  return (
    <button onClick={handleDownload}>Download</button>
  );
};

export default ImageDownloadButton;
```

```diff_typescript:src/components/Lists/image_list.tsx
import React, { useState, useEffect } from 'react';
import { list } from 'aws-amplify/storage';
import { StorageImage } from '@aws-amplify/ui-react-storage';
+ import ImageDownloadButton from '../Buttons/image_download_button';

const ImageList = () => {
  const [files, setFiles] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchFiles = async () => {
      try {
        const result = await list({
          path: 'public/'
        });
        console.log(result);
        setFiles(result.items); 
      } catch (err) {
        console.error(err);
        setError(err.message);
      }
    };

    fetchFiles();
  }, []);

  return (
    <div>
      <h1>File List</h1>
      {error && <p>Error fetching files: {error}</p>}
      <ul>
        {files.map((file, index) => (
          <li key={index}>
            <StorageImage alt={file.lastModified} path={file.path} />
+            <ImageDownloadButton fileName={file.path}/>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default ImageList;
```
以下の画像のようにダウンロードボタンが作成され、クリックすると画像のダウンロードが出来ました。
![画像](https://d39oe90ddnxn3x.cloudfront.net/02_amplifygen2_storage/03_amplifygen2_storage_download_delete/02_03_amplifygen2_storage_download_delete03.png)

# 削除
次に削除機能を試してみます。公式のドキュメントには以下のサンプルコードが記載されています。pathに指定したファイルを削除する関数removeの使い方サンプルですね。
```typescript:
import { remove } from 'aws-amplify/storage';

try {
  await remove({ 
    path: 'album/2024/1.jpg',
    // Alternatively, path: ({identityId}) => `album/{identityId}/1.jpg`
  });
} catch (error) {
  console.log('Error ', error);
}
```

そしたら、削除機能を実装してみましょう。以下のように削除ボタンを作成します。
```typescript:src/components/Buttons/deletebutton.tsx
import React from 'react';
import { remove } from 'aws-amplify/storage';

const DeleteImageButton = ({ fileName}) => {
  const handleDelete = async () => {
    try {
      await remove({
        path: fileName
      });
      console.log(`Deleted ${fileName}`);
    } catch (error) {
      console.log('Error deleting the file:', error);
    }
  };

  return (
    <button onClick={handleDelete}>Delete</button>
  );
};

export default DeleteImageButton;
```

次に、今まで画像をリストしていたファイルの削除ボタンを加えます。
```diff_typescript: src/components/Lists/delete_image_list.tsx
import React, { useState, useEffect } from 'react';
import { list } from 'aws-amplify/storage';
import { StorageImage } from '@aws-amplify/ui-react-storage';
+ import DeleteImageButton from '../Buttons/deletebutton';
import ImageDownloadButton from '../Buttons/image_download_button';

const ImageList = () => {
  const [files, setFiles] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchFiles = async () => {
      try {
        const result = await list({
          path: 'public/'
        });
        console.log(result);
        setFiles(result.items); 
      } catch (err) {
        console.error(err);
        setError(err.message);
      }
    };

    fetchFiles();
  }, []);

  return (
    <div>
      <h1>File List</h1>
      {error && <p>Error fetching files: {error}</p>}
      <ul>
        {files.map((file, index) => (
          <li key={index}>
            <StorageImage alt={file.lastModified} path={file.path} />
+           <DeleteImageButton fileName={file.path}/>
            <ImageDownloadButton fileName={file.path}/>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default ImageList;
```
以下のように削除ボタンが追加されます。
![画像](https://d39oe90ddnxn3x.cloudfront.net/02_amplifygen2_storage/03_amplifygen2_storage_download_delete/02_03_amplifygen2_storage_download_delete04.png)

これだけでは、私の場合は削除が出来ませんでした。削除の権限があるか確認してなければ付与しましょう！
```diff_typescript: amplify/storage/resource.ts
import { defineStorage } from '@aws-amplify/backend';

export const storage = defineStorage({
  name: 'amplifygen2Test',
  access: (allow) => ({
    'profile-pictures/{entity_id}/*': [
      allow.guest.to(['read']),
      allow.entity('identity').to(['read', 'write', 'delete'])
    ],
    'picture-submissions/*': [
      allow.authenticated.to(['read','write']),
      allow.guest.to(['read', 'write'])
    ],
    'public/*': [
-      allow.authenticated.to(['read','write']),
+      allow.authenticated.to(['read','write','delete']),
      allow.guest.to(['read'])
    ],
  })
});
```

以上でダウンロードと削除が出来るようになりました。アップロードやリスト、認証設定と合わせて設計してから始めないとめちゃめちゃになっちゃいそうです笑


# まとめ
今回はダウンロードと削除を実装してみました。表示と含めて簡単に実装出来たと思います。
しかし、パス毎にどのユーザがどんな権限を持っているか設計してから実装に入らないと作りながらめちゃめちゃになりそうだなと感じました...
もしも実際にプロダクトに使うみたいな際は気をつけようと思います。