3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

どうも、駆け出し12冠エンジニアのアスカです。Amplify Gen2がGAしてドキュメントのStorage機能に関する部分が充実していたので、それを参考にしながら触ってみます!
Amplify Gen2のドキュメント

想像以上にボリュームがあるので、連載の形をとっています。今回は画像表示、ダウンロードと削除の機能を触っていきたいと思います。連載なので、1つ前の記事の内容はある程度前提に書いていくので、分からない部分はあればご参照ください。
参考:
Amplify Gen2のStorage機能を使ってみた-①最初のアップロードまで-
Amplify Gen2のStorage機能を使ってみた-②アップロードとリスト-

画像表示

Storage Image React UI Componentを使ってみる

ドキュメントでは以下のようなサンプルコードが記載されています。

import { StorageImage } from '@aws-amplify/ui-react-storage';

export const DefaultStorageImageExample = () => {
  return <StorageImage alt="cat" path="your-path/cat.jpg" />;
};

どうやらpathに指定した写真をaltのタイトルで表示するようです。UIのドキュメントを参考にすると他にも多数プロパティがありました。

Name Description Type
imgKey Deprecated, use path instead. The key of an image. More Info string
accessLevel Deprecated, use path instead. Access level for files in Storage. More Info '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 boolean

storage画像の表示のレベルでaccessLevelが定義出来るの楽ですね。簡単な所から試してみましょう。
Amplify Gen2のStorage機能を使ってみた-②アップロードとリスト-
のpublic配下の画像をリストしたコードをベースにパスの羅列から画像の羅列に変更してみます。
まず、image_list.tsxを定義してみます。

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に入れてみます。

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がめちゃめちゃになります。
画像
例えばpropsにGet時のHookがあれば画像サイズ調整も可能なのですが、エラー発生時のHookしかないので、このUIを使う場合はアップロードにサイズの調整をしておくしかない気がします。こういった仕様が分かるだけでも今回触った価値はあったかと思います。

ダウンロード

署名付URLからダウンロードする形で以下のようなサンプルがドキュメントに挙げられています。

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);
<a href={linkToStorageFile.url.toString()} target="_blank" rel="noreferrer">
  {fileName} 
</a>

今回はもう一つのdownloadData APIを利用してみます。ドキュメントのサンプルは以下です。

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に追加してみます。

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

以下の画像のようにダウンロードボタンが作成され、クリックすると画像のダウンロードが出来ました。
画像

削除

次に削除機能を試してみます。公式のドキュメントには以下のサンプルコードが記載されています。pathに指定したファイルを削除する関数removeの使い方サンプルですね。

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);
}

そしたら、削除機能を実装してみましょう。以下のように削除ボタンを作成します。

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;

次に、今まで画像をリストしていたファイルの削除ボタンを加えます。

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;

以下のように削除ボタンが追加されます。
画像

これだけでは、私の場合は削除が出来ませんでした。削除の権限があるか確認してなければ付与しましょう!

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'])
    ],
  })
});

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

まとめ

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

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?