Page updated Jan 16, 2024

Working with files / attachments

The Storage and GraphQL API categories can be used together to associate a file, such as an image or video, with a particular record. For example, you might create a User model with a profile picture, or a Post model with an associated image. With Amplify's GraphQL API and Storage categories, you can reference the file within the model itself to create an association.

To get started, run the following commands in an existing Amplify project:

1# For Authenticator component:
2npm i @aws-amplify/ui-react
3
4# Select default configuration:
5amplify add auth
6
7# Select "Content", "Auth users only", full CRUD access,
8# and default configuration:
9amplify add storage
10
11# Select default configuration
12amplify add api

When prompted, use the following schema, which can also be found under amplify/backend/api/[name of project]/schema.graphql:

1type Song @model @auth(rules: [{ allow: public }]) {
2 id: ID!
3 name: String!
4 coverArtKey: String # Set as optional to allow adding file after initial create
5}

Save the schema and run amplify push to deploy the changes.

Next, at the root of your project, set all access to private by configuring the Storage object globally. This will restrict file access to only the signed-in user.

1Storage.configure({ level: 'private' });

For the complete working example, including required imports, obtaining the file from the user, and React component state management, see the Complete Example below.

Configuring authorization

Your application needs authorization credentials for reading and writing to both Storage and the GraphQL API, except in the case where all data and files are intended to be publicly accessible.

The Storage and API categories govern data access based on their own authorization patterns, meaning that it's necessary to configure appropriate auth roles for each individual category. Although both categories share the same access credentials set up through the Auth category, they work independently from one another. For instance, adding an @auth directive to the API schema does not guard against file access in the Storage category. Likewise, adding authorization rules to the Storage category does not guard against data access in the API category.

When you run amplify add storage, the CLI will configure appropriate IAM policies on the bucket using a Cognito Identity Pool role. You will then have the option of adding CRUD (Create, Update, Read and Delete) based permissions as well, so that Authenticated and Guest users will be granted limited permissions within these levels. Even after adding this configuration via the CLI, all Storage access is still public by default. To guard against accidental public access, the Storage access levels must either be configured on the Storage object globally, or set within individual function calls. This guide uses the former approach, setting all Storage access to private globally.

The ability to independently configure authorization rules for each category allows for more granular control over data access, and adds greater flexibility. For scenarios where authorization patterns must be mixed and matched, configure the access level on individual Storage function calls. For example, you may want to use private CRUD access on an individual Storage function call for files that should only be accessible by the owner (such as personal files), protected read access to allow all logged in users to view common files (such as images in a shared photo album), and public read access to allow all users to view a file (such as a public profile picture).

For more details on how to configure Storage authorization levels, see the Storage documentation. For more on configuring GraphQL API authorization, see the API documentation.

Create a record with an associated file

First create a record via the GraphQL API, then upload the file to Storage, and finally add the association between the record and file. Use the following example with the GraphQL API and Storage categories to create a record and associate the file with the record.

The API record's id is prepended to the Storage file name to ensure uniqueness. If this is excluded, multiple API records could then be associated with the same file key unintentionally.

1const createSongDetails: CreateSongInput = {
2 name: `My first song`
3};
4
5// Create the API record:
6const response = await API.graphql<GraphQLQuery<CreateSongMutation>>({
7 query: mutations.createSong,
8 variables: { input: createSongDetails }
9});
10
11const song = response?.data?.createSong;
12
13if (!song) return;
14
15// Upload the Storage file:
16const result = await Storage.put(`${song.id}-${file.name}`, file, {
17 contentType: 'image/png' // contentType is optional
18});
19
20const updateSongDetails: UpdateSongInput = {
21 id: song.id,
22 coverArtKey: result?.key
23};
24
25// Add the file association to the record:
26const updateResponse = await API.graphql<GraphQLQuery<UpdateSongMutation>>({
27 query: mutations.updateSong,
28 variables: { input: updateSongDetails }
29});
30
31const updatedSong = updateResponse?.data?.updateSong;
32
33// If the record has no associated file, we can return early.
34if (!updatedSong?.coverArtKey) return;
35
36// Retrieve the signed URL:
37const signedURL = await Storage.get(updatedSong.coverArtKey);
1const createSongDetails: CreateSongInput = {
2 name: `My first song`,
3};
4
5// Create the API record:
6const response = await API.graphql({
7 query: mutations.createSong,
8 variables: { input: createSongDetails },
9});
10
11const song = response?.data?.createSong;
12
13if (!song) return;
14
15// Upload the Storage file:
16const result = await Storage.put(`${song.id}-${file.name}`, file, {
17 contentType: "image/png", // contentType is optional
18});
19
20const updateSongDetails = {
21 id: song.id,
22 coverArtKey: result?.key,
23};
24
25// Add the file association to the record:
26const updateResponse = await API.graphql<
27 GraphQLQuery<UpdateSongMutation>
28>({
29 query: mutations.updateSong,
30 variables: { input: updateSongDetails },
31});
32
33const updatedSong = updateResponse?.data?.updateSong;
34
35// If the record has no associated file, we can return early.
36if (!updatedSong?.coverArtKey) return;
37
38// Retrieve the signed URL:
39const signedURL = await Storage.get(updatedSong.coverArtKey);

Add or update a file for an associated record

To associate a file with a record, update the record with the key returned by the Storage upload. The following example uploads the file using Storage, updates the record with the file's key, then retrieves the signed URL to download the image. If an image is already associated with the record, this will update the record with the new image.

1// Upload the Storage file:
2const result = await Storage.put(`${currentSong.id}-${file.name}`, file, {
3 contentType: 'image/png' // contentType is optional
4});
5
6const updateSongDetails: UpdateSongInput = {
7 id: currentSong.id,
8 coverArtKey: result?.key
9};
10
11// Add the file association to the record:
12const response = await API.graphql<GraphQLQuery<UpdateSongMutation>>({
13 query: mutations.updateSong,
14 variables: { input: updateSongDetails }
15});
16
17const updatedSong = response?.data?.updateSong;
18
19// If the record has no associated file, we can return early.
20if (!updatedSong?.coverArtKey) return;
21
22// Retrieve the file's signed URL:
23const signedURL = await Storage.get(updatedSong.coverArtKey);
1// Upload the Storage file:
2const result = await Storage.put(`${currentSong.id}-${file.name}`, file, {
3 contentType: 'image/png' // contentType is optional
4});
5
6const updateSongDetails = {
7 id: currentSong.id,
8 coverArtKey: result?.key
9};
10
11// Add the file association to the record:
12const response = await API.graphql({
13 query: mutations.updateSong,
14 variables: { input: updateSongDetails }
15});
16
17const updatedSong = response?.data?.updateSong;
18
19// If the record has no associated file, we can return early.
20if (!updatedSong?.coverArtKey) return;
21
22// Retrieve the file's signed URL:
23const signedURL = await Storage.get(updatedSong.coverArtKey);

Query a record and retrieve the associated file

To retrieve the file associated with a record, first query the record, then use Storage to get the signed URL. The signed URL can then be used to download the file, display an image, etc:

1// Query the record to get the file key:
2const response = await API.graphql<GraphQLQuery<GetSongQuery>>({
3 query: queries.getSong,
4 variables: { id: currentSong.id }
5});
6const song = response.data?.getSong;
7
8// If the record has no associated file, we can return early.
9if (!song?.coverArtKey) return;
10
11// Retrieve the signed URL:
12const signedURL = await Storage.get(song.coverArtKey);
1// Query the record to get the file key:
2const response = await API.graphql({
3 query: queries.getSong,
4 variables: { id: currentSong.id }
5});
6const song = response.data?.getSong;
7
8// If the record has no associated file, we can return early.
9if (!song?.coverArtKey) return;
10
11// Retrieve the signed URL:
12const signedURL = await Storage.get(song.coverArtKey);

Delete and remove files associated with API records

There are three common deletion workflows when working with Storage files and the GraphQL API:

  1. Remove the file association, continue to persist both file and record.
  2. Remove the record association and delete the file.
  3. Delete both file and record.

Remove the file association, continue to persist both file and record

The following example removes the file association from the record, but does not delete the file from S3, nor the record from the database.

1const response = await API.graphql<GraphQLQuery<GetSongQuery>>({
2 query: queries.getSong,
3 variables: { id: currentSong.id }
4});
5
6const song = response?.data?.getSong;
7
8// If the record has no associated file, we can return early.
9if (!song?.coverArtKey) return;
10
11const songDetails: UpdateSongInput = {
12 id: song.id,
13 coverArtKey: null
14};
15
16const updatedSong = await API.graphql<GraphQLQuery<UpdateSongMutation>>({
17 query: mutations.updateSong,
18 variables: { input: songDetails }
19});
1const response = await API.graphql({
2 query: queries.getSong,
3 variables: { id: currentSong.id }
4});
5
6const song = response?.data?.getSong;
7
8// If the record has no associated file, we can return early.
9if (!song?.coverArtKey) return;
10
11const songDetails = {
12 id: song.id,
13 coverArtKey: null
14};
15
16const updatedSong = await API.graphql({
17 query: mutations.updateSong,
18 variables: { input: songDetails }
19});

Remove the record association and delete the file

The following example removes the file from the record, then deletes the file from S3:

1const response = await API.graphql<GraphQLQuery<GetSongQuery>>({
2 query: queries.getSong,
3 variables: { id: currentSong.id }
4});
5
6const song = response?.data?.getSong;
7
8// If the record has no associated file, we can return early.
9if (!song?.coverArtKey) return;
10
11const songDetails: UpdateSongInput = {
12 id: song.id,
13 coverArtKey: null // Set the file association to `null`
14};
15
16// Remove associated file from record
17const updatedSong = await API.graphql<GraphQLQuery<UpdateSongMutation>>({
18 query: mutations.updateSong,
19 variables: { input: songDetails }
20});
21
22// Delete the file from S3:
23await Storage.remove(song.coverArtKey);
1const response = await API.graphql({
2 query: queries.getSong,
3 variables: { id: currentSong.id },
4});
5
6const song = response?.data?.getSong;
7
8// If the record has no associated file, we can return early.
9if (!song?.coverArtKey) return;
10
11const songDetails: UpdateSongInput = {
12 id: song.id,
13 coverArtKey: null, // Set the file association to `null`
14};
15
16// Remove associated file from record
17const updatedSong = await API.graphql({
18 query: mutations.updateSong,
19 variables: { input: songDetails },
20});
21
22// Delete the file from S3:
23await Storage.remove(song.coverArtKey);

Delete both file and record

1const response = await API.graphql<GraphQLQuery<GetSongQuery>>({
2 query: queries.getSong,
3 variables: { id: currentSong.id }
4});
5
6const song = response?.data?.getSong;
7
8// If the record has no associated file, we can return early.
9if (!song?.coverArtKey) return;
10
11await Storage.remove(song.coverArtKey);
12
13const songDetails: DeleteSongInput = {
14 id: song.id
15};
16
17const deletedSong = await API.graphql<GraphQLQuery<DeleteSongMutation>>({
18 query: mutations.deleteSong,
19 variables: { input: songDetails }
20});
1const response = await API.graphql({
2 query: queries.getSong,
3 variables: { id: currentSong.id }
4});
5
6const song = response?.data?.getSong;
7
8// If the record has no associated file, we can return early.
9if (!song?.coverArtKey) return;
10
11await Storage.remove(song.coverArtKey);
12
13const songDetails = {
14 id: song.id
15};
16
17const deletedSong = await API.graphql({
18 query: mutations.deleteSong,
19 variables: { input: songDetails }
20});

Working with multiple files

You may want to add multiple files to a single record, such as a user profile with multiple images. To do this, you can add a list of file keys to the record. The following example adds a list of file keys to a record:

GraphQL schema to associate a data model with multiple files

When prompted after running amplify add api use the following schema, which can also be found under amplify/backend/api/[name of project]/schema.graphql:

1type PhotoAlbum @model @auth(rules: [{ allow: public }]) {
2 id: ID!
3 name: String!
4 imageKeys: [String] #Set as optional to allow adding file(s) after initial create
5}

CRUD operations when working with multiple files is the same as when working with a single file, with the exception that we are now working with a list of image keys, as opposed to a single image key.

Create a record with multiple associated files

First create a record via the GraphQL API, then upload the files to Storage, and finally add the associations between the record and files.

1const photoAlbumDetails: CreatePhotoAlbumInput = {
2 name: `My first photoAlbum`
3};
4
5// Create the API record:
6const response = await API.graphql<GraphQLQuery<CreatePhotoAlbumMutation>>({
7 query: mutations.createPhotoAlbum,
8 variables: { input: photoAlbumDetails }
9});
10
11const photoAlbum = response?.data?.createPhotoAlbum;
12
13if (!photoAlbum) return;
14
15// Upload all files to Storage:
16const imageKeys = await Promise.all(
17 Array.from(e.target.files).map(async (file) => {
18 const result = await Storage.put(`${photoAlbum.id}-${file.name}`, file, {
19 contentType: 'image/png' // contentType is optional
20 });
21
22 return result?.key;
23 })
24);
25
26const updatePhotoAlbumDetails: UpdatePhotoAlbumInput = {
27 id: photoAlbum.id,
28 imageKeys: imageKeys
29};
30
31// Add the file association to the record:
32const updateResponse = await API.graphql<
33 GraphQLQuery<UpdatePhotoAlbumMutation>
34>({
35 query: mutations.updatePhotoAlbum,
36 variables: { input: updatePhotoAlbumDetails }
37});
38
39const updatedPhotoAlbum = updateResponse?.data?.updatePhotoAlbum;
40
41// If the record has no associated file, we can return early.
42if (!updatedPhotoAlbum?.imageKeys?.length) return;
43
44// Retrieve signed urls for all files:
45const signedUrls = await Promise.all(
46 updatedPhotoAlbum.imageKeys.map(async (key) => await Storage.get(key!))
47);
1const photoAlbumDetails = {
2 name: `My first photoAlbum`
3};
4
5// Create the API record:
6const response = await API.graphql({
7 query: mutations.createPhotoAlbum,
8 variables: { input: photoAlbumDetails }
9});
10
11const photoAlbum = response?.data?.createPhotoAlbum;
12
13if (!photoAlbum) return;
14
15// Upload all files to Storage:
16const imageKeys = await Promise.all(
17 Array.from(e.target.files).map(async (file) => {
18 const result = await Storage.put(`${photoAlbum.id}-${file.name}`, file, {
19 contentType: 'image/png' // contentType is optional
20 });
21
22 return result?.key;
23 })
24);
25
26const updatePhotoAlbumDetails = {
27 id: photoAlbum.id,
28 imageKeys: imageKeys
29};
30
31// Add the file association to the record:
32const updateResponse = await API.graphql({
33 query: mutations.updatePhotoAlbum,
34 variables: { input: updatePhotoAlbumDetails }
35});
36
37const updatedPhotoAlbum = updateResponse?.data?.updatePhotoAlbum;
38
39// If the record has no associated file, we can return early.
40if (!updatedPhotoAlbum?.imageKeys?.length) return;
41
42// Retrieve signed urls for all files:
43const signedUrls = await Promise.all(
44 updatedPhotoAlbum.imageKeys.map(async (key) => await Storage.get(key))
45);

Create a record with a single associated file

When a schema allows for multiple associated images, you can still create a record with a single associated file.

1const photoAlbumDetails: CreatePhotoAlbumInput = {
2 name: `My first photoAlbum`
3};
4
5// Create the API record:
6const response = await API.graphql<GraphQLQuery<CreatePhotoAlbumMutation>>({
7 query: mutations.createPhotoAlbum,
8 variables: { input: photoAlbumDetails }
9});
10
11const photoAlbum = response?.data?.createPhotoAlbum;
12
13if (!photoAlbum) return;
14
15// Upload the Storage file:
16const result = await Storage.put(`${photoAlbum.id}-${file.name}`, file, {
17 contentType: 'image/png' // contentType is optional
18});
19
20const updatePhotoAlbumDetails: UpdatePhotoAlbumInput = {
21 id: photoAlbum.id,
22 imageKeys: [result?.key]
23};
24
25// Add the file association to the record:
26const updateResponse = await API.graphql<
27 GraphQLQuery<UpdatePhotoAlbumMutation>
28>({
29 query: mutations.updatePhotoAlbum,
30 variables: { input: updatePhotoAlbumDetails }
31});
32
33const updatedPhotoAlbum = updateResponse?.data?.updatePhotoAlbum;
34
35// If the record has no associated file, we can return early.
36if (!updatedPhotoAlbum?.imageKeys?.length) return;
37
38// Retrieve the file's signed URL:
39const signedURL = await Storage.get(updatedPhotoAlbum.imageKeys[0]!);
1const photoAlbumDetails = {
2 name: `My first photoAlbum`
3};
4
5// Create the API record:
6const response = await API.graphql({
7 query: mutations.createPhotoAlbum,
8 variables: { input: photoAlbumDetails }
9});
10
11const photoAlbum = response?.data?.createPhotoAlbum;
12
13if (!photoAlbum) return;
14
15// Upload the Storage file:
16const result = await Storage.put(`${photoAlbum.id}-${file.name}`, file, {
17 contentType: 'image/png' // contentType is optional
18});
19
20const updatePhotoAlbumDetails = {
21 id: photoAlbum.id,
22 imageKeys: [result?.key]
23};
24
25// Add the file association to the record:
26const updateResponse = await API.graphql({
27 query: mutations.updatePhotoAlbum,
28 variables: { input: updatePhotoAlbumDetails }
29});
30
31const updatedPhotoAlbum = updateResponse?.data?.updatePhotoAlbum;
32
33// If the record has no associated file, we can return early.
34if (!updatedPhotoAlbum?.imageKeys?.length) return;
35
36// Retrieve the file's signed URL:
37const signedURL = await Storage.get(updatedPhotoAlbum.imageKeys[0]);

Add new files to an associated record

To associate additional files with a record, update the record with the keys returned by the Storage uploads.

1// Upload all files to Storage:
2const newImageKeys = await Promise.all(
3 Array.from(e.target.files).map(async (file) => {
4 const result = await Storage.put(
5 `${currentPhotoAlbum.id}-${file.name}`,
6 file,
7 {
8 contentType: 'image/png' // contentType is optional
9 }
10 );
11
12 return result?.key;
13 })
14);
15
16// Query existing record to retrieve currently associated files:
17const queriedResponse = await API.graphql<GraphQLQuery<GetPhotoAlbumQuery>>({
18 query: queries.getPhotoAlbum,
19 variables: { id: currentPhotoAlbum.id }
20});
21
22const photoAlbum = queriedResponse.data?.getPhotoAlbum;
23
24if (!photoAlbum?.imageKeys) return;
25
26// Merge existing and new file keys:
27const updatedImageKeys = [...newImageKeys, ...photoAlbum.imageKeys];
28
29const photoAlbumDetails: UpdatePhotoAlbumInput = {
30 id: currentPhotoAlbum.id,
31 imageKeys: updatedImageKeys
32};
33
34// Update record with merged file associations:
35const response = await API.graphql<GraphQLQuery<UpdatePhotoAlbumMutation>>({
36 query: mutations.updatePhotoAlbum,
37 variables: { input: photoAlbumDetails }
38});
39
40const updatedPhotoAlbum = response?.data?.updatePhotoAlbum;
41
42// If the record has no associated file, we can return early.
43if (!updatedPhotoAlbum?.imageKeys) return;
44
45// Retrieve signed urls for merged image keys:
46const signedUrls = await Promise.all(
47 updatedPhotoAlbum?.imageKeys.map(async (key) => await Storage.get(key!))
48);
1// Upload all files to Storage:
2const newImageKeys = await Promise.all(
3 Array.from(e.target.files).map(async (file) => {
4 const result = await Storage.put(
5 `${currentPhotoAlbum.id}-${file.name}`,
6 file,
7 {
8 contentType: 'image/png' // contentType is optional
9 }
10 );
11
12 return result?.key;
13 })
14);
15
16// Query existing record to retrieve currently associated files:
17const queriedResponse = await API.graphql({
18 query: queries.getPhotoAlbum,
19 variables: { id: currentPhotoAlbum.id }
20});
21
22const photoAlbum = queriedResponse.data?.getPhotoAlbum;
23
24if (!photoAlbum?.imageKeys) return;
25
26// Merge existing and new file keys:
27const updatedImageKeys = [...newImageKeys, ...photoAlbum.imageKeys];
28
29const photoAlbumDetails = {
30 id: currentPhotoAlbum.id,
31 imageKeys: updatedImageKeys
32};
33
34// Update record with merged file associations:
35const response = await API.graphql({
36 query: mutations.updatePhotoAlbum,
37 variables: { input: photoAlbumDetails }
38});
39
40const updatedPhotoAlbum = response?.data?.updatePhotoAlbum;
41
42// If the record has no associated file, we can return early.
43if (!updatedPhotoAlbum?.imageKeys) return;
44
45// Retrieve signed urls for merged image keys:
46const signedUrls = await Promise.all(
47 updatedPhotoAlbum?.imageKeys.map(async (key) => await Storage.get(key))
48);

Update the file for an associated record

Updating a file for an associated record is the same as updating a file for a single file record, with the exception that you will need to update the list of file keys.

1// Upload new file to Storage:
2const result = await Storage.put(`${currentPhotoAlbum.id}-${file.name}`, file, {
3 contentType: 'image/png' // contentType is optional
4});
5
6const newFileKey = result?.key;
7
8// Query existing record to retrieve currently associated files:
9const queriedResponse = await API.graphql<GraphQLQuery<GetPhotoAlbumQuery>>({
10 query: queries.getPhotoAlbum,
11 variables: { id: currentPhotoAlbum.id }
12});
13
14const photoAlbum = queriedResponse.data?.getPhotoAlbum;
15
16if (!photoAlbum?.imageKeys?.length) return;
17
18// Retrieve last image key:
19const [lastImageKey] = photoAlbum.imageKeys.slice(-1);
20
21// Remove last file association by key
22const updatedImageKeys = [
23 ...photoAlbum.imageKeys.filter((key) => key !== lastImageKey),
24 newFileKey
25];
26
27const photoAlbumDetails: UpdatePhotoAlbumInput = {
28 id: currentPhotoAlbum.id,
29 // @ts-ignore
30 imageKeys: updatedImageKeys
31};
32
33// Update record with updated file associations:
34const response = await API.graphql<GraphQLQuery<UpdatePhotoAlbumMutation>>({
35 query: mutations.updatePhotoAlbum,
36 variables: { input: photoAlbumDetails }
37});
38
39const updatedPhotoAlbum = response?.data?.updatePhotoAlbum;
40
41// If the record has no associated file, we can return early.
42if (!updatedPhotoAlbum?.imageKeys) return;
43
44// Retrieve signed urls for merged image keys:
45const signedUrls = await Promise.all(
46 updatedPhotoAlbum?.imageKeys.map(async (key) => await Storage.get(key!))
47);
1// Upload new file to Storage:
2const result = await Storage.put(`${currentPhotoAlbum.id}-${file.name}`, file, {
3 contentType: 'image/png' // contentType is optional
4});
5
6const newFileKey = result?.key;
7
8// Query existing record to retrieve currently associated files:
9const queriedResponse = await API.graphql({
10 query: queries.getPhotoAlbum,
11 variables: { id: currentPhotoAlbum.id }
12});
13
14const photoAlbum = queriedResponse.data?.getPhotoAlbum;
15
16if (!photoAlbum?.imageKeys?.length) return;
17
18// Retrieve last image key:
19const [lastImageKey] = photoAlbum.imageKeys.slice(-1);
20
21// Remove last file association by key
22const updatedImageKeys = [
23 ...photoAlbum.imageKeys.filter((key) => key !== lastImageKey),
24 newFileKey
25];
26
27const photoAlbumDetails = {
28 id: currentPhotoAlbum.id,
29 // @ts-ignore
30 imageKeys: updatedImageKeys
31};
32
33// Update record with updated file associations:
34const response = await API.graphql({
35 query: mutations.updatePhotoAlbum,
36 variables: { input: photoAlbumDetails }
37});
38
39const updatedPhotoAlbum = response?.data?.updatePhotoAlbum;
40
41// If the record has no associated file, we can return early.
42if (!updatedPhotoAlbum?.imageKeys) return;
43
44// Retrieve signed urls for merged image keys:
45const signedUrls = await Promise.all(
46 updatedPhotoAlbum?.imageKeys.map(async (key) => await Storage.get(key))
47);

Query a record and retrieve the associated files

To retrieve the files associated with a record, first query the record, then use Storage to retrieve all of the signed URLs.

1// Query the record to get the file keys:
2const response = await API.graphql<GraphQLQuery<GetPhotoAlbumQuery>>({
3 query: queries.getPhotoAlbum,
4 variables: { id: currentPhotoAlbum.id }
5});
6const photoAlbum = response.data?.getPhotoAlbum;
7
8// If the record has no associated files, we can return early.
9if (!photoAlbum?.imageKeys) return;
10
11// Retrieve the signed URLs for the associated images:
12const signedUrls = await Promise.all(
13 photoAlbum.imageKeys.map(async (imageKey) => {
14 if (!imageKey) return;
15 return await Storage.get(imageKey);
16 })
17);
1// Query the record to get the file keys:
2const response = await API.graphql({
3 query: queries.getPhotoAlbum,
4 variables: { id: currentPhotoAlbum.id }
5});
6const photoAlbum = response.data?.getPhotoAlbum;
7
8// If the record has no associated files, we can return early.
9if (!photoAlbum?.imageKeys) return;
10
11// Retrieve the signed URLs for the associated images:
12const signedUrls = await Promise.all(
13 photoAlbum.imageKeys.map(async (imageKey) => {
14 if (!imageKey) return;
15 return await Storage.get(imageKey);
16 })
17);

Delete and remove files associated with API records

The workflow for deleting and removing files associated with API records is the same as when working with a single file, except that when performing a delete you will need to iterate over the list of files keys and call Storage.remove() for each file.

Remove the file association, continue to persist both files and record

1const response = await API.graphql<GraphQLQuery<GetPhotoAlbumQuery>>({
2 query: queries.getPhotoAlbum,
3 variables: { id: currentPhotoAlbum.id }
4});
5
6const photoAlbum = response?.data?.getPhotoAlbum;
7
8// If the record has no associated file, we can return early.
9if (!photoAlbum?.imageKeys) return;
10
11const photoAlbumDetails: UpdatePhotoAlbumInput = {
12 id: photoAlbum.id,
13 imageKeys: null
14};
15
16const updatedPhotoAlbum = await API.graphql<
17 GraphQLQuery<UpdatePhotoAlbumMutation>
18>({
19 query: mutations.updatePhotoAlbum,
20 variables: { input: photoAlbumDetails }
21});
1const response = await API.graphql({
2 query: queries.getPhotoAlbum,
3 variables: { id: currentPhotoAlbum.id }
4});
5
6const photoAlbum = response?.data?.getPhotoAlbum;
7
8// If the record has no associated file, we can return early.
9if (!photoAlbum?.imageKeys) return;
10
11const photoAlbumDetails = {
12 id: photoAlbum.id,
13 imageKeys: null
14};
15
16const updatedPhotoAlbum = await API.graphql({
17 query: mutations.updatePhotoAlbum,
18 variables: { input: photoAlbumDetails }
19});

Remove the record association and delete the files

1const response = await API.graphql<GraphQLQuery<GetPhotoAlbumQuery>>({
2 query: queries.getPhotoAlbum,
3 variables: { id: currentPhotoAlbum.id }
4});
5
6const photoAlbum = response?.data?.getPhotoAlbum;
7
8// If the record has no associated files, we can return early.
9if (!photoAlbum?.imageKeys) return;
10
11const photoAlbumDetails: UpdatePhotoAlbumInput = {
12 id: photoAlbum.id,
13 imageKeys: null // Set the file association to `null`
14};
15
16// Remove associated files from record
17const updateResponse = await API.graphql<
18 GraphQLQuery<UpdatePhotoAlbumMutation>
19>({
20 query: mutations.updatePhotoAlbum,
21 variables: { input: photoAlbumDetails }
22});
23
24const updatedPhotoAlbum = updateResponse?.data?.updatePhotoAlbum;
25
26// Delete the files from S3:
27await Promise.all(
28 photoAlbum?.imageKeys.map(async (imageKey) => {
29 if (!imageKey) return;
30 await Storage.remove(imageKey);
31 })
32);
1const response = await API.graphql({
2 query: queries.getPhotoAlbum,
3 variables: { id: currentPhotoAlbum.id }
4});
5
6const photoAlbum = response?.data?.getPhotoAlbum;
7
8// If the record has no associated files, we can return early.
9if (!photoAlbum?.imageKeys) return;
10
11const photoAlbumDetails = {
12 id: photoAlbum.id,
13 imageKeys: null // Set the file association to `null`
14};
15
16// Remove associated files from record
17const updateResponse = await API.graphql({
18 query: mutations.updatePhotoAlbum,
19 variables: { input: photoAlbumDetails }
20});
21
22const updatedPhotoAlbum = updateResponse?.data?.updatePhotoAlbum;
23
24// Delete the files from S3:
25await Promise.all(
26 photoAlbum?.imageKeys.map(async (imageKey) => {
27 if (!imageKey) return;
28 await Storage.remove(imageKey);
29 })
30);

Delete record and all associated files

1const response = await API.graphql<GraphQLQuery<GetPhotoAlbumQuery>>({
2 query: queries.getPhotoAlbum,
3 variables: { id: currentPhotoAlbum.id }
4});
5
6const photoAlbum = response?.data?.getPhotoAlbum;
7
8if (!photoAlbum) return;
9
10const photoAlbumDetails: DeletePhotoAlbumInput = {
11 id: photoAlbum.id
12};
13
14await API.graphql<GraphQLQuery<DeletePhotoAlbumMutation>>({
15 query: mutations.deletePhotoAlbum,
16 variables: { input: photoAlbumDetails }
17});
18
19// If the record has no associated file, we can return early.
20if (!photoAlbum?.imageKeys) return;
21
22await Promise.all(
23 photoAlbum?.imageKeys.map(async (imageKey) => {
24 if (!imageKey) return;
25 await Storage.remove(imageKey);
26 })
27);
1const response = await API.graphql({
2 query: queries.getPhotoAlbum,
3 variables: { id: currentPhotoAlbum.id }
4});
5
6const photoAlbum = response?.data?.getPhotoAlbum;
7
8if (!photoAlbum) return;
9
10const photoAlbumDetails = {
11 id: photoAlbum.id
12};
13
14await API.graphql({
15 query: mutations.deletePhotoAlbum,
16 variables: { input: photoAlbumDetails }
17});
18
19// If the record has no associated file, we can return early.
20if (!photoAlbum?.imageKeys) return;
21
22await Promise.all(
23 photoAlbum?.imageKeys.map(async (imageKey) => {
24 if (!imageKey) return;
25 await Storage.remove(imageKey);
26 })
27);

Data consistency when working with records and files

The recommended access patterns in these docs attempt to remove deleted files, but favor leaving orphans over leaving records that point to non-existent files. This optimizes for read latency by ensuring clients rarely attempt to fetch a non-existent file from Storage. However, any app that deletes files can inherently cause records on-device to point to non-existent files.

One example is when we create an API record, associate the Storage file with that record, and then retrieve the file's signed URL. "Device A" calls the GraphQL API to create API_Record_1, and then associates that record with First_Photo. Before "Device A" is about to retrieve the signed URL, "Device B" might query API_Record_1, delete First_Photo, and update the record accordingly. However, "Device A" is still using the old API_Record_1, which is now out-of-date. Even though the shared global state is correctly in sync at every stage, the individual device ("Device A") has an out-of-date record that points to a non-existent file. Similar issues can conceivably occur for updates. Depending on your app, some of these mismatches can be minimized even more with real-time data / GraphQL subscriptions.

It is important to understand when these mismatches can occur and to add meaningful error handling around these cases. This guide does not include exhaustive error handling, real-time subscriptions, re-querying of outdated records, or attempts to retry failed operations. However, these are all important considerations for a production-level application.

Complete examples

1import { useState } from "react";
2import { API, Storage } from "aws-amplify";
3import { Authenticator } from "@aws-amplify/ui-react";
4import "@aws-amplify/ui-react/styles.css";
5import { GraphQLQuery } from "@aws-amplify/api";
6import * as queries from "./graphql/queries";
7import * as mutations from "./graphql/mutations";
8import {
9 CreateSongInput,
10 CreateSongMutation,
11 DeleteSongInput,
12 DeleteSongMutation,
13 GetSongQuery,
14 UpdateSongInput,
15 UpdateSongMutation,
16} from "./API";
17
18function App() {
19 const [currentSong, setCurrentSong] = useState<any>();
20
21 // Used to display image for current song:
22 const [currentImageUrl, setCurrentImageUrl] = useState<
23 string | null | undefined
24 >("");
25
26 // Private access level configuration on the Storage object:
27 Storage.configure({ level: 'private' });
28
29 async function createSongWithImage(e: React.ChangeEvent<HTMLInputElement>) {
30 if (!e.target.files) return;
31
32 const file = e.target.files[0];
33
34 try {
35 const createSongDetails: CreateSongInput = {
36 name: `My first song`,
37 };
38
39 // Create the API record:
40 const response = await API.graphql<GraphQLQuery<CreateSongMutation>>({
41 query: mutations.createSong,
42 variables: { input: createSongDetails },
43 });
44
45 const song = response?.data?.createSong;
46
47 if (!song) return;
48
49 // Upload the Storage file:
50 const result = await Storage.put(`${song.id}-${file.name}`, file, {
51 contentType: "image/png", // contentType is optional
52 });
53
54 const updateSongDetails: UpdateSongInput = {
55 id: song.id,
56 coverArtKey: result?.key,
57 };
58
59 // Add the file association to the record:
60 const updateResponse = await API.graphql<
61 GraphQLQuery<UpdateSongMutation>
62 >({
63 query: mutations.updateSong,
64 variables: { input: updateSongDetails },
65 });
66
67 const updatedSong = updateResponse?.data?.updateSong;
68
69 setCurrentSong(updatedSong);
70
71 // If the record has no associated file, we can return early.
72 if (!updatedSong?.coverArtKey) return;
73
74 // Retrieve the file's signed URL:
75 const signedURL = await Storage.get(updatedSong.coverArtKey);
76 setCurrentImageUrl(signedURL);
77 } catch (error) {
78 console.error("Error create song / file:", error);
79 }
80 }
81
82 // Upload image, add to song, retrieve signed URL and retrieve the image.
83 // Also updates image if one already exists.
84 async function addNewImageToSong(e: React.ChangeEvent<HTMLInputElement>) {
85 if (!currentSong) return;
86
87 if (!e.target.files) return;
88
89 const file = e.target.files[0];
90
91 try {
92 // Upload the Storage file:
93 const result = await Storage.put(`${currentSong.id}-${file.name}`, file, {
94 contentType: "image/png", // contentType is optional
95 });
96
97 const updateSongDetails: UpdateSongInput = {
98 id: currentSong.id,
99 coverArtKey: result?.key,
100 };
101
102 // Add the file association to the record:
103 const response = await API.graphql<GraphQLQuery<UpdateSongMutation>>({
104 query: mutations.updateSong,
105 variables: { input: updateSongDetails },
106 });
107
108 const updatedSong = response?.data?.updateSong;
109
110 setCurrentSong(updatedSong);
111
112 // If the record has no associated file, we can return early.
113 if (!updatedSong?.coverArtKey) return;
114
115 // Retrieve the file's signed URL:
116 const signedURL = await Storage.get(updatedSong.coverArtKey);
117 setCurrentImageUrl(signedURL);
118 } catch (error) {
119 console.error("Error uploading image / adding image to song: ", error);
120 }
121 }
122
123 async function getImageForCurrentSong() {
124 try {
125 // Query the record to get the file key:
126 const response = await API.graphql<GraphQLQuery<GetSongQuery>>({
127 query: queries.getSong,
128 variables: { id: currentSong.id },
129 });
130 const song = response.data?.getSong;
131
132 // If the record has no associated file, we can return early.
133 if (!song?.coverArtKey) return;
134
135 // Retrieve the signed URL:
136 const signedURL = await Storage.get(song.coverArtKey);
137
138 setCurrentImageUrl(signedURL);
139 } catch (error) {
140 console.error("Error getting song / image:", error);
141 }
142 }
143
144 // Remove the file association, continue to persist both file and record
145 async function removeImageFromSong() {
146 if (!currentSong) return;
147
148 try {
149 const response = await API.graphql<GraphQLQuery<GetSongQuery>>({
150 query: queries.getSong,
151 variables: { id: currentSong.id },
152 });
153
154 const song = response?.data?.getSong;
155
156 // If the record has no associated file, we can return early.
157 if (!song?.coverArtKey) return;
158
159 const songDetails: UpdateSongInput = {
160 id: song.id,
161 coverArtKey: null,
162 };
163
164 const updatedSong = await API.graphql<GraphQLQuery<UpdateSongMutation>>({
165 query: mutations.updateSong,
166 variables: { input: songDetails },
167 });
168
169 // If successful, the response here will be `null`:
170 setCurrentSong(updatedSong?.data?.updateSong);
171 setCurrentImageUrl(updatedSong?.data?.updateSong?.coverArtKey);
172 } catch (error) {
173 console.error("Error removing image from song: ", error);
174 }
175 }
176
177 // Remove the record association and delete the file
178 async function deleteImageForCurrentSong() {
179 if (!currentSong) return;
180
181 try {
182 const response = await API.graphql<GraphQLQuery<GetSongQuery>>({
183 query: queries.getSong,
184 variables: { id: currentSong.id },
185 });
186
187 const song = response?.data?.getSong;
188
189 // If the record has no associated file, we can return early.
190 if (!song?.coverArtKey) return;
191
192 const songDetails: UpdateSongInput = {
193 id: song.id,
194 coverArtKey: null, // Set the file association to `null`
195 };
196
197 // Remove associated file from record
198 const updatedSong = await API.graphql<GraphQLQuery<UpdateSongMutation>>({
199 query: mutations.updateSong,
200 variables: { input: songDetails },
201 });
202
203 // Delete the file from S3:
204 await Storage.remove(song.coverArtKey);
205
206 // If successful, the response here will be `null`:
207 setCurrentSong(updatedSong?.data?.updateSong);
208 setCurrentImageUrl(updatedSong?.data?.updateSong?.coverArtKey);
209 } catch (error) {
210 console.error("Error deleting image: ", error);
211 }
212 }
213
214 // Delete both file and record
215 async function deleteCurrentSongAndImage() {
216 if (!currentSong) return;
217
218 try {
219 const response = await API.graphql<GraphQLQuery<GetSongQuery>>({
220 query: queries.getSong,
221 variables: { id: currentSong.id },
222 });
223
224 const song = response?.data?.getSong;
225
226 // If the record has no associated file, we can return early.
227 if (!song?.coverArtKey) return;
228
229 await Storage.remove(song.coverArtKey);
230
231 const songDetails: DeleteSongInput = {
232 id: song.id,
233 };
234
235 await API.graphql<GraphQLQuery<DeleteSongMutation>>({
236 query: mutations.deleteSong,
237 variables: { input: songDetails },
238 });
239
240 clearLocalState();
241 } catch (error) {
242 console.error("Error deleting song: ", error);
243 }
244 }
245
246 function clearLocalState() {
247 setCurrentSong(null);
248 setCurrentImageUrl("");
249 }
250
251 return (
252 <Authenticator>
253 {({ signOut, user }) => (
254 <main
255 style={{
256 alignItems: "center",
257 display: "flex",
258 flexDirection: "column",
259 }}
260 >
261 <h1>Hello {user?.username}!</h1>
262 <h2>{`Current Song: ${currentSong?.id}`}</h2>
263 <label>
264 Create song with file:
265 <input id="name" type="file" onChange={createSongWithImage} />
266 </label>
267 <label>
268 Add / update song image:
269 <input
270 id="name"
271 type="file"
272 onChange={addNewImageToSong}
273 disabled={!currentSong}
274 />
275 </label>
276 <button
277 onClick={getImageForCurrentSong}
278 disabled={!currentSong || !currentImageUrl}
279 >
280 Get image for current song
281 </button>
282 <button
283 onClick={removeImageFromSong}
284 disabled={!currentSong || !currentImageUrl}
285 >
286 Remove image from current song (does not delete image)
287 </button>
288 <button
289 onClick={deleteImageForCurrentSong}
290 disabled={!currentSong || !currentImageUrl}
291 >
292 Remove image from current song, then delete image
293 </button>
294 <button onClick={deleteCurrentSongAndImage} disabled={!currentSong}>
295 Delete current song (and image, if it exists)
296 </button>
297 <button onClick={signOut}>Sign out</button>
298 {currentImageUrl && (
299 <img src={currentImageUrl} alt="Storage file"></img>
300 )}
301 </main>
302 )}
303 </Authenticator>
304 );
305}
306
307export default App;
1import { useState } from 'react';
2import { API, Storage } from 'aws-amplify';
3import { Authenticator } from '@aws-amplify/ui-react';
4import '@aws-amplify/ui-react/styles.css';
5import * as queries from './graphql/queries';
6import * as mutations from './graphql/mutations';
7
8function App() {
9 const [currentSong, setCurrentSong] = useState();
10
11 // Used to display image for current song:
12 const [currentImageUrl, setCurrentImageUrl] = useState('');
13
14 // Private access level configuration on the Storage object:
15 Storage.configure({ level: 'private' });
16
17 async function createSongWithImage(e) {
18 if (!e.target.files) return;
19
20 const file = e.target.files[0];
21
22 try {
23 const createSongDetails = {
24 name: `My first song`
25 };
26
27 // Create the API record:
28 const response = await API.graphql({
29 query: mutations.createSong,
30 variables: { input: createSongDetails }
31 });
32
33 const song = response?.data?.createSong;
34
35 if (!song) return;
36
37 // Upload the Storage file:
38 const result = await Storage.put(`${song.id}-${file.name}`, file, {
39 contentType: 'image/png' // contentType is optional
40 });
41
42 const updateSongDetails = {
43 id: song.id,
44 coverArtKey: result?.key
45 };
46
47 // Add the file association to the record:
48 const updateResponse = await API.graphql({
49 query: mutations.updateSong,
50 variables: { input: updateSongDetails }
51 });
52
53 const updatedSong = updateResponse?.data?.updateSong;
54
55 setCurrentSong(updatedSong);
56
57 // If the record has no associated file, we can return early.
58 if (!updatedSong?.coverArtKey) return;
59
60 // Retrieve the file's signed URL:
61 const signedURL = await Storage.get(updatedSong.coverArtKey);
62 setCurrentImageUrl(signedURL);
63 } catch (error) {
64 console.error('Error create song / file:', error);
65 }
66 }
67
68 // Upload image, add to song, retrieve signed URL and retrieve the image.
69 // Also updates image if one already exists.
70 async function addNewImageToSong(e) {
71 if (!currentSong) return;
72
73 if (!e.target.files) return;
74
75 const file = e.target.files[0];
76
77 try {
78 // Upload the Storage file:
79 const result = await Storage.put(`${currentSong.id}-${file.name}`, file, {
80 contentType: 'image/png' // contentType is optional
81 });
82
83 const updateSongDetails = {
84 id: currentSong.id,
85 coverArtKey: result?.key
86 };
87
88 // Add the file association to the record:
89 const response = await API.graphql({
90 query: mutations.updateSong,
91 variables: { input: updateSongDetails }
92 });
93
94 const updatedSong = response?.data?.updateSong;
95
96 setCurrentSong(updatedSong);
97
98 // If the record has no associated file, we can return early.
99 if (!updatedSong?.coverArtKey) return;
100
101 // Retrieve the file's signed URL:
102 const signedURL = await Storage.get(updatedSong.coverArtKey);
103 setCurrentImageUrl(signedURL);
104 } catch (error) {
105 console.error('Error uploading image / adding image to song: ', error);
106 }
107 }
108
109 async function getImageForCurrentSong() {
110 try {
111 // Query the record to get the file key:
112 const response = await API.graphql({
113 query: queries.getSong,
114 variables: { id: currentSong.id }
115 });
116 const song = response.data?.getSong;
117
118 // If the record has no associated file, we can return early.
119 if (!song?.coverArtKey) return;
120
121 // Retrieve the signed URL:
122 const signedURL = await Storage.get(song.coverArtKey);
123
124 setCurrentImageUrl(signedURL);
125 } catch (error) {
126 console.error('Error getting song / image:', error);
127 }
128 }
129
130 // Remove the file association, continue to persist both file and record
131 async function removeImageFromSong() {
132 if (!currentSong) return;
133
134 try {
135 const response = await API.graphql({
136 query: queries.getSong,
137 variables: { id: currentSong.id }
138 });
139
140 const song = response?.data?.getSong;
141
142 // If the record has no associated file, we can return early.
143 if (!song?.coverArtKey) return;
144
145 const songDetails = {
146 id: song.id,
147 coverArtKey: null
148 };
149
150 const updatedSong = await API.graphql({
151 query: mutations.updateSong,
152 variables: { input: songDetails }
153 });
154
155 // If successful, the response here will be `null`:
156 setCurrentSong(updatedSong?.data?.updateSong);
157 setCurrentImageUrl(updatedSong?.data?.updateSong?.coverArtKey);
158 } catch (error) {
159 console.error('Error removing image from song: ', error);
160 }
161 }
162
163 // Remove the record association and delete the file
164 async function deleteImageForCurrentSong() {
165 if (!currentSong) return;
166
167 try {
168 const response = await API.graphql({
169 query: queries.getSong,
170 variables: { id: currentSong.id }
171 });
172
173 const song = response?.data?.getSong;
174
175 // If the record has no associated file, we can return early.
176 if (!song?.coverArtKey) return;
177
178 const songDetails = {
179 id: song.id,
180 coverArtKey: null // Set the file association to `null`
181 };
182
183 // Remove associated file from record
184 const updatedSong = await API.graphql({
185 query: mutations.updateSong,
186 variables: { input: songDetails }
187 });
188
189 // Delete the file from S3:
190 await Storage.remove(song.coverArtKey);
191
192 // If successful, the response here will be `null`:
193 setCurrentSong(updatedSong?.data?.updateSong);
194 setCurrentImageUrl(updatedSong?.data?.updateSong?.coverArtKey);
195 } catch (error) {
196 console.error('Error deleting image: ', error);
197 }
198 }
199
200 // Delete both file and record
201 async function deleteCurrentSongAndImage() {
202 if (!currentSong) return;
203
204 try {
205 const response = await API.graphql({
206 query: queries.getSong,
207 variables: { id: currentSong.id }
208 });
209
210 const song = response?.data?.getSong;
211
212 // If the record has no associated file, we can return early.
213 if (!song?.coverArtKey) return;
214
215 await Storage.remove(song.coverArtKey);
216
217 const songDetails = {
218 id: song.id
219 };
220
221 await API.graphql({
222 query: mutations.deleteSong,
223 variables: { input: songDetails }
224 });
225
226 clearLocalState();
227 } catch (error) {
228 console.error('Error deleting song: ', error);
229 }
230 }
231
232 function clearLocalState() {
233 setCurrentSong(null);
234 setCurrentImageUrl('');
235 }
236
237 return (
238 <Authenticator>
239 {({ signOut, user }) => (
240 <main
241 style={{
242 alignItems: 'center',
243 display: 'flex',
244 flexDirection: 'column'
245 }}
246 >
247 <h1>Hello {user?.username}!</h1>
248 <h2>{`Current Song: ${currentSong?.id}`}</h2>
249 <label>
250 Create song with file:
251 <input id="name" type="file" onChange={createSongWithImage} />
252 </label>
253 <label>
254 Add / update song image:
255 <input
256 id="name"
257 type="file"
258 onChange={addNewImageToSong}
259 disabled={!currentSong}
260 />
261 </label>
262 <button
263 onClick={getImageForCurrentSong}
264 disabled={!currentSong || !currentImageUrl}
265 >
266 Get image for current song
267 </button>
268 <button
269 onClick={removeImageFromSong}
270 disabled={!currentSong || !currentImageUrl}
271 >
272 Remove image from current song (does not delete image)
273 </button>
274 <button
275 onClick={deleteImageForCurrentSong}
276 disabled={!currentSong || !currentImageUrl}
277 >
278 Remove image from current song, then delete image
279 </button>
280 <button onClick={deleteCurrentSongAndImage} disabled={!currentSong}>
281 Delete current song (and image, if it exists)
282 </button>
283 <button onClick={signOut}>Sign out</button>
284 {currentImageUrl && (
285 <img src={currentImageUrl} alt="Storage file"></img>
286 )}
287 </main>
288 )}
289 </Authenticator>
290 );
291}
292
293export default App;
1import { useState } from "react";
2import { API, Storage } from "aws-amplify";
3import { Authenticator } from "@aws-amplify/ui-react";
4import "@aws-amplify/ui-react/styles.css";
5import { GraphQLQuery } from "@aws-amplify/api";
6import * as queries from "./graphql/queries";
7import * as mutations from "./graphql/mutations";
8import {
9 CreatePhotoAlbumInput,
10 CreatePhotoAlbumMutation,
11 DeletePhotoAlbumInput,
12 DeletePhotoAlbumMutation,
13 GetPhotoAlbumQuery,
14 ListPhotoAlbumsQuery,
15 UpdatePhotoAlbumInput,
16 UpdatePhotoAlbumMutation,
17} from "./API";
18
19function App() {
20 const [currentPhotoAlbum, setCurrentPhotoAlbum] = useState<any>();
21
22 // Used to display images for current photoAlbum:
23 const [currentImages, setCurrentImages] = useState<
24 (string | null | undefined)[] | null | undefined
25 >([]);
26
27 // Private access level configuration on the Storage object:
28 Storage.configure({ level: 'private' });
29
30 async function createPhotoAlbumWithFirstImage(
31 e: React.ChangeEvent<HTMLInputElement>
32 ) {
33 if (!e.target.files) return;
34
35 const file = e.target.files[0];
36
37 try {
38 const photoAlbumDetails: CreatePhotoAlbumInput = {
39 name: `My first photoAlbum`,
40 };
41
42 // Create the API record:
43 const response = await API.graphql<
44 GraphQLQuery<CreatePhotoAlbumMutation>
45 >({
46 query: mutations.createPhotoAlbum,
47 variables: { input: photoAlbumDetails },
48 });
49
50 const photoAlbum = response?.data?.createPhotoAlbum;
51
52 if (!photoAlbum) return;
53
54 // Upload the Storage file:
55 const result = await Storage.put(`${photoAlbum.id}-${file.name}`, file, {
56 contentType: "image/png", // contentType is optional
57 });
58
59 const updatePhotoAlbumDetails: UpdatePhotoAlbumInput = {
60 id: photoAlbum.id,
61 imageKeys: [result?.key],
62 };
63
64 // Add the file association to the record:
65 const updateResponse = await API.graphql<
66 GraphQLQuery<UpdatePhotoAlbumMutation>
67 >({
68 query: mutations.updatePhotoAlbum,
69 variables: { input: updatePhotoAlbumDetails },
70 });
71
72 const updatedPhotoAlbum = updateResponse?.data?.updatePhotoAlbum;
73
74 setCurrentPhotoAlbum(updatedPhotoAlbum);
75
76 // If the record has no associated file, we can return early.
77 if (!updatedPhotoAlbum?.imageKeys?.length) return;
78
79 // Retrieve the file's signed URL:
80 const signedURL = await Storage.get(updatedPhotoAlbum.imageKeys[0]!);
81 setCurrentImages([signedURL]);
82 } catch (error) {
83 console.error("Error create photoAlbum / file:", error);
84 }
85 }
86
87 async function createPhotoAlbumWithMultipleImages(
88 e: React.ChangeEvent<HTMLInputElement>
89 ) {
90 if (!e.target.files) return;
91
92 try {
93 const photoAlbumDetails: CreatePhotoAlbumInput = {
94 name: `My first photoAlbum`,
95 };
96
97 // Create the API record:
98 const response = await API.graphql<
99 GraphQLQuery<CreatePhotoAlbumMutation>
100 >({
101 query: mutations.createPhotoAlbum,
102 variables: { input: photoAlbumDetails },
103 });
104
105 const photoAlbum = response?.data?.createPhotoAlbum;
106
107 if (!photoAlbum) return;
108
109 // Upload all files to Storage:
110 const imageKeys = await Promise.all(
111 Array.from(e.target.files).map(async (file) => {
112 const result = await Storage.put(
113 `${photoAlbum.id}-${file.name}`,
114 file,
115 {
116 contentType: "image/png", // contentType is optional
117 }
118 );
119
120 return result?.key;
121 })
122 );
123
124 const updatePhotoAlbumDetails: UpdatePhotoAlbumInput = {
125 id: photoAlbum.id,
126 imageKeys: imageKeys,
127 };
128
129 // Add the file association to the record:
130 const updateResponse = await API.graphql<
131 GraphQLQuery<UpdatePhotoAlbumMutation>
132 >({
133 query: mutations.updatePhotoAlbum,
134 variables: { input: updatePhotoAlbumDetails },
135 });
136
137 const updatedPhotoAlbum = updateResponse?.data?.updatePhotoAlbum;
138
139 setCurrentPhotoAlbum(updatedPhotoAlbum);
140
141 // If the record has no associated file, we can return early.
142 if (!updatedPhotoAlbum?.imageKeys?.length) return;
143
144 // Retrieve signed urls for all files:
145 const signedUrls = await Promise.all(
146 updatedPhotoAlbum.imageKeys.map(async (key) => await Storage.get(key!))
147 );
148
149 if (!signedUrls) return;
150 setCurrentImages(signedUrls);
151 } catch (error) {
152 console.error("Error create photoAlbum / file:", error);
153 }
154 }
155
156 async function addNewImagesToPhotoAlbum(
157 e: React.ChangeEvent<HTMLInputElement>
158 ) {
159 if (!currentPhotoAlbum) return;
160
161 if (!e.target.files) return;
162
163 try {
164 // Upload all files to Storage:
165 const newImageKeys = await Promise.all(
166 Array.from(e.target.files).map(async (file) => {
167 const result = await Storage.put(
168 `${currentPhotoAlbum.id}-${file.name}`,
169 file,
170 {
171 contentType: "image/png", // contentType is optional
172 }
173 );
174
175 return result?.key;
176 })
177 );
178
179 // Query existing record to retrieve currently associated files:
180 const queriedResponse = await API.graphql<
181 GraphQLQuery<GetPhotoAlbumQuery>
182 >({
183 query: queries.getPhotoAlbum,
184 variables: { id: currentPhotoAlbum.id },
185 });
186
187 const photoAlbum = queriedResponse.data?.getPhotoAlbum;
188
189 if (!photoAlbum?.imageKeys) return;
190
191 // Merge existing and new file keys:
192 const updatedImageKeys = [...newImageKeys, ...photoAlbum.imageKeys];
193
194 const photoAlbumDetails: UpdatePhotoAlbumInput = {
195 id: currentPhotoAlbum.id,
196 imageKeys: updatedImageKeys,
197 };
198
199 // Update record with merged file associations:
200 const response = await API.graphql<
201 GraphQLQuery<UpdatePhotoAlbumMutation>
202 >({
203 query: mutations.updatePhotoAlbum,
204 variables: { input: photoAlbumDetails },
205 });
206
207 const updatedPhotoAlbum = response?.data?.updatePhotoAlbum;
208 setCurrentPhotoAlbum(updatedPhotoAlbum);
209
210 // If the record has no associated file, we can return early.
211 if (!updatedPhotoAlbum?.imageKeys) return;
212
213 // Retrieve signed urls for merged image keys:
214 const signedUrls = await Promise.all(
215 updatedPhotoAlbum?.imageKeys.map(async (key) => await Storage.get(key!))
216 );
217
218 if (!signedUrls) return;
219
220 setCurrentImages(signedUrls);
221 } catch (error) {
222 console.error(
223 "Error uploading image / adding image to photoAlbum: ",
224 error
225 );
226 }
227 }
228
229 // Replace last image associated with current photoAlbum:
230 async function updateLastImage(e: React.ChangeEvent<HTMLInputElement>) {
231 if (!currentPhotoAlbum) return;
232
233 if (!e.target.files) return;
234
235 const file = e.target.files[0];
236
237 try {
238 // Upload new file to Storage:
239 const result = await Storage.put(
240 `${currentPhotoAlbum.id}-${file.name}`,
241 file,
242 {
243 contentType: "image/png", // contentType is optional
244 }
245 );
246
247 const newFileKey = result?.key;
248
249 // Query existing record to retrieve currently associated files:
250 const queriedResponse = await API.graphql<
251 GraphQLQuery<GetPhotoAlbumQuery>
252 >({
253 query: queries.getPhotoAlbum,
254 variables: { id: currentPhotoAlbum.id },
255 });
256
257 const photoAlbum = queriedResponse.data?.getPhotoAlbum;
258
259 if (!photoAlbum?.imageKeys?.length) return;
260
261 // Retrieve last image key:
262 const [lastImageKey] = photoAlbum.imageKeys.slice(-1);
263
264 // Remove last file association by key
265 const updatedImageKeys = [
266 ...photoAlbum.imageKeys.filter((key) => key !== lastImageKey),
267 newFileKey,
268 ];
269
270 const photoAlbumDetails: UpdatePhotoAlbumInput = {
271 id: currentPhotoAlbum.id,
272 // @ts-ignore
273 imageKeys: updatedImageKeys,
274 };
275
276 // Update record with updated file associations:
277 const response = await API.graphql<
278 GraphQLQuery<UpdatePhotoAlbumMutation>
279 >({
280 query: mutations.updatePhotoAlbum,
281 variables: { input: photoAlbumDetails },
282 });
283
284 const updatedPhotoAlbum = response?.data?.updatePhotoAlbum;
285 setCurrentPhotoAlbum(updatedPhotoAlbum);
286
287 // If the record has no associated file, we can return early.
288 if (!updatedPhotoAlbum?.imageKeys) return;
289
290 // Retrieve signed urls for merged image keys:
291 const signedUrls = await Promise.all(
292 updatedPhotoAlbum?.imageKeys.map(async (key) => await Storage.get(key!))
293 );
294
295 if (!signedUrls) return;
296
297 setCurrentImages(signedUrls);
298 } catch (error) {
299 console.error(
300 "Error uploading image / adding image to photoAlbum: ",
301 error
302 );
303 }
304 }
305
306 async function getImagesForPhotoAlbum() {
307 try {
308 // Query the record to get the file keys:
309 const response = await API.graphql<GraphQLQuery<GetPhotoAlbumQuery>>({
310 query: queries.getPhotoAlbum,
311 variables: { id: currentPhotoAlbum.id },
312 });
313 const photoAlbum = response.data?.getPhotoAlbum;
314
315 // If the record has no associated files, we can return early.
316 if (!photoAlbum?.imageKeys) return;
317
318 // Retrieve the signed URLs for the associated images:
319 const signedUrls = await Promise.all(
320 photoAlbum.imageKeys.map(async (imageKey) => {
321 if (!imageKey) return;
322 return await Storage.get(imageKey);
323 })
324 );
325
326 setCurrentImages(signedUrls);
327 } catch (error) {
328 console.error("Error getting photoAlbum / image:", error);
329 }
330 }
331
332 // Remove the file associations, continue to persist both files and record
333 async function removeImagesFromPhotoAlbum() {
334 if (!currentPhotoAlbum) return;
335
336 try {
337 const response = await API.graphql<GraphQLQuery<GetPhotoAlbumQuery>>({
338 query: queries.getPhotoAlbum,
339 variables: { id: currentPhotoAlbum.id },
340 });
341
342 const photoAlbum = response?.data?.getPhotoAlbum;
343
344 // If the record has no associated file, we can return early.
345 if (!photoAlbum?.imageKeys) return;
346
347 const photoAlbumDetails: UpdatePhotoAlbumInput = {
348 id: photoAlbum.id,
349 imageKeys: null,
350 };
351
352 const updatedPhotoAlbum = await API.graphql<
353 GraphQLQuery<UpdatePhotoAlbumMutation>
354 >({
355 query: mutations.updatePhotoAlbum,
356 variables: { input: photoAlbumDetails },
357 });
358
359 // If successful, the response here will be `null`:
360 setCurrentPhotoAlbum(updatedPhotoAlbum?.data?.updatePhotoAlbum);
361 setCurrentImages(updatedPhotoAlbum?.data?.updatePhotoAlbum?.imageKeys);
362 } catch (error) {
363 console.error("Error removing image from photoAlbum: ", error);
364 }
365 }
366
367 // Remove the record association and delete the file
368 async function deleteImagesForCurrentPhotoAlbum() {
369 if (!currentPhotoAlbum) return;
370
371 try {
372 const response = await API.graphql<GraphQLQuery<GetPhotoAlbumQuery>>({
373 query: queries.getPhotoAlbum,
374 variables: { id: currentPhotoAlbum.id },
375 });
376
377 const photoAlbum = response?.data?.getPhotoAlbum;
378
379 // If the record has no associated files, we can return early.
380 if (!photoAlbum?.imageKeys) return;
381
382 const photoAlbumDetails: UpdatePhotoAlbumInput = {
383 id: photoAlbum.id,
384 imageKeys: null, // Set the file association to `null`
385 };
386
387 // Remove associated files from record
388 const updateResponse = await API.graphql<
389 GraphQLQuery<UpdatePhotoAlbumMutation>
390 >({
391 query: mutations.updatePhotoAlbum,
392 variables: { input: photoAlbumDetails },
393 });
394
395 const updatedPhotoAlbum = updateResponse?.data?.updatePhotoAlbum;
396
397 // Delete the files from S3:
398 await Promise.all(
399 photoAlbum?.imageKeys.map(async (imageKey) => {
400 if (!imageKey) return;
401 await Storage.remove(imageKey);
402 })
403 );
404
405 // If successful, the response here will be `null`:
406 setCurrentPhotoAlbum(updatedPhotoAlbum);
407 setCurrentImages(null);
408 } catch (error) {
409 console.error("Error deleting image: ", error);
410 }
411 }
412
413 // Delete both files and record
414 async function deleteCurrentPhotoAlbumAndImages() {
415 if (!currentPhotoAlbum) return;
416
417 try {
418 const response = await API.graphql<GraphQLQuery<GetPhotoAlbumQuery>>({
419 query: queries.getPhotoAlbum,
420 variables: { id: currentPhotoAlbum.id },
421 });
422
423 const photoAlbum = response?.data?.getPhotoAlbum;
424
425 if (!photoAlbum) return;
426
427 const photoAlbumDetails: DeletePhotoAlbumInput = {
428 id: photoAlbum.id,
429 };
430
431 await API.graphql<GraphQLQuery<DeletePhotoAlbumMutation>>({
432 query: mutations.deletePhotoAlbum,
433 variables: { input: photoAlbumDetails },
434 });
435
436 setCurrentPhotoAlbum(null);
437
438 // If the record has no associated file, we can return early.
439 if (!photoAlbum?.imageKeys) return;
440
441 await Promise.all(
442 photoAlbum?.imageKeys.map(async (imageKey) => {
443 if (!imageKey) return;
444 await Storage.remove(imageKey);
445 })
446 );
447
448 clearLocalState();
449 } catch (error) {
450 console.error("Error deleting photoAlbum: ", error);
451 }
452 }
453
454 function clearLocalState() {
455 setCurrentPhotoAlbum(null);
456 setCurrentImages([]);
457 }
458
459 return (
460 <Authenticator>
461 {({ signOut, user }) => (
462 <main
463 style={{
464 alignItems: "center",
465 display: "flex",
466 flexDirection: "column",
467 }}
468 >
469 <h1>Hello {user?.username}!</h1>
470 <h2>{`Current PhotoAlbum: ${currentPhotoAlbum?.id}`}</h2>
471 <label>
472 Create photoAlbum with one file:
473 <input
474 type="file"
475 accept="image/*"
476 onChange={createPhotoAlbumWithFirstImage}
477 />
478 </label>
479 <label>
480 Create photoAlbum with multiple files:
481 <input
482 type="file"
483 accept="image/*"
484 onChange={createPhotoAlbumWithMultipleImages}
485 multiple
486 />
487 </label>
488 <label>
489 Add multiple images to current photoAlbum:
490 <input
491 type="file"
492 accept="image/*"
493 onChange={addNewImagesToPhotoAlbum}
494 disabled={!currentPhotoAlbum}
495 multiple
496 />
497 </label>
498 <label>
499 Replace last image:
500 <input
501 type="file"
502 accept="image/*"
503 onChange={updateLastImage}
504 disabled={!currentPhotoAlbum || !currentImages}
505 />
506 </label>
507 <button
508 onClick={getImagesForPhotoAlbum}
509 disabled={!currentPhotoAlbum || !currentImages}
510 >
511 Get Images for Current Photo Album
512 </button>
513 <button
514 onClick={removeImagesFromPhotoAlbum}
515 disabled={!currentPhotoAlbum || !currentImages}
516 >
517 Remove images from current PhotoAlbum (does not delete images)
518 </button>
519 <button
520 onClick={deleteImagesForCurrentPhotoAlbum}
521 disabled={!currentPhotoAlbum || !currentImages}
522 >
523 Remove images from current PhotoAlbum, then delete images
524 </button>
525 <button
526 onClick={deleteCurrentPhotoAlbumAndImages}
527 disabled={!currentPhotoAlbum}
528 >
529 Delete current PhotoAlbum (and images, if they exist)
530 </button>
531 <button onClick={signOut}>Sign out</button>
532 {currentImages &&
533 currentImages.map((url, idx) => {
534 if (!url) return undefined;
535 return <img src={url} key={idx} alt="Storage file"></img>;
536 })}
537 </main>
538 )}
539 </Authenticator>
540 );
541}
542
543export default App;
1import { useState } from 'react';
2import { API, Storage } from 'aws-amplify';
3import { Authenticator } from '@aws-amplify/ui-react';
4import '@aws-amplify/ui-react/styles.css';
5import * as queries from './graphql/queries';
6import * as mutations from './graphql/mutations';
7
8function App() {
9 const [currentPhotoAlbum, setCurrentPhotoAlbum] = useState();
10
11 // Used to display images for current photoAlbum:
12 const [currentImages, setCurrentImages] = useState([]);
13
14 // Private access level configuration on the Storage object:
15 Storage.configure({ level: 'private' });
16
17 async function createPhotoAlbumWithFirstImage(e) {
18 if (!e.target.files) return;
19
20 const file = e.target.files[0];
21
22 try {
23 const photoAlbumDetails = {
24 name: `My first photoAlbum`
25 };
26
27 // Create the API record:
28 const response = await API.graphql({
29 query: mutations.createPhotoAlbum,
30 variables: { input: photoAlbumDetails }
31 });
32
33 const photoAlbum = response?.data?.createPhotoAlbum;
34
35 if (!photoAlbum) return;
36
37 // Upload the Storage file:
38 const result = await Storage.put(`${photoAlbum.id}-${file.name}`, file, {
39 contentType: 'image/png' // contentType is optional
40 });
41
42 const updatePhotoAlbumDetails = {
43 id: photoAlbum.id,
44 imageKeys: [result?.key]
45 };
46
47 // Add the file association to the record:
48 const updateResponse = await API.graphql({
49 query: mutations.updatePhotoAlbum,
50 variables: { input: updatePhotoAlbumDetails }
51 });
52
53 const updatedPhotoAlbum = updateResponse?.data?.updatePhotoAlbum;
54
55 setCurrentPhotoAlbum(updatedPhotoAlbum);
56
57 // If the record has no associated file, we can return early.
58 if (!updatedPhotoAlbum?.imageKeys?.length) return;
59
60 // Retrieve the file's signed URL:
61 const signedURL = await Storage.get(updatedPhotoAlbum.imageKeys[0]);
62 setCurrentImages([signedURL]);
63 } catch (error) {
64 console.error('Error create photoAlbum / file:', error);
65 }
66 }
67
68 async function createPhotoAlbumWithMultipleImages(e) {
69 if (!e.target.files) return;
70
71 try {
72 const photoAlbumDetails = {
73 name: `My first photoAlbum`
74 };
75
76 // Create the API record:
77 const response = await API.graphql({
78 query: mutations.createPhotoAlbum,
79 variables: { input: photoAlbumDetails }
80 });
81
82 const photoAlbum = response?.data?.createPhotoAlbum;
83
84 if (!photoAlbum) return;
85
86 // Upload all files to Storage:
87 const imageKeys = await Promise.all(
88 Array.from(e.target.files).map(async (file) => {
89 const result = await Storage.put(
90 `${photoAlbum.id}-${file.name}`,
91 file,
92 {
93 contentType: 'image/png' // contentType is optional
94 }
95 );
96
97 return result?.key;
98 })
99 );
100
101 const updatePhotoAlbumDetails = {
102 id: photoAlbum.id,
103 imageKeys: imageKeys
104 };
105
106 // Add the file association to the record:
107 const updateResponse = await API.graphql({
108 query: mutations.updatePhotoAlbum,
109 variables: { input: updatePhotoAlbumDetails }
110 });
111
112 const updatedPhotoAlbum = updateResponse?.data?.updatePhotoAlbum;
113
114 setCurrentPhotoAlbum(updatedPhotoAlbum);
115
116 // If the record has no associated file, we can return early.
117 if (!updatedPhotoAlbum?.imageKeys?.length) return;
118
119 // Retrieve signed urls for all files:
120 const signedUrls = await Promise.all(
121 updatedPhotoAlbum.imageKeys.map(async (key) => await Storage.get(key))
122 );
123
124 if (!signedUrls) return;
125 setCurrentImages(signedUrls);
126 } catch (error) {
127 console.error('Error create photoAlbum / file:', error);
128 }
129 }
130
131 async function addNewImagesToPhotoAlbum(e) {
132 if (!currentPhotoAlbum) return;
133
134 if (!e.target.files) return;
135
136 try {
137 // Upload all files to Storage:
138 const newImageKeys = await Promise.all(
139 Array.from(e.target.files).map(async (file) => {
140 const result = await Storage.put(
141 `${currentPhotoAlbum.id}-${file.name}`,
142 file,
143 {
144 contentType: 'image/png' // contentType is optional
145 }
146 );
147
148 return result?.key;
149 })
150 );
151
152 // Query existing record to retrieve currently associated files:
153 const queriedResponse = await API.graphql({
154 query: queries.getPhotoAlbum,
155 variables: { id: currentPhotoAlbum.id }
156 });
157
158 const photoAlbum = queriedResponse.data?.getPhotoAlbum;
159
160 if (!photoAlbum?.imageKeys) return;
161
162 // Merge existing and new file keys:
163 const updatedImageKeys = [...newImageKeys, ...photoAlbum.imageKeys];
164
165 const photoAlbumDetails = {
166 id: currentPhotoAlbum.id,
167 imageKeys: updatedImageKeys
168 };
169
170 // Update record with merged file associations:
171 const response = await API.graphql({
172 query: mutations.updatePhotoAlbum,
173 variables: { input: photoAlbumDetails }
174 });
175
176 const updatedPhotoAlbum = response?.data?.updatePhotoAlbum;
177 setCurrentPhotoAlbum(updatedPhotoAlbum);
178
179 // If the record has no associated file, we can return early.
180 if (!updatedPhotoAlbum?.imageKeys) return;
181
182 // Retrieve signed urls for merged image keys:
183 const signedUrls = await Promise.all(
184 updatedPhotoAlbum?.imageKeys.map(async (key) => await Storage.get(key))
185 );
186
187 if (!signedUrls) return;
188
189 setCurrentImages(signedUrls);
190 } catch (error) {
191 console.error(
192 'Error uploading image / adding image to photoAlbum: ',
193 error
194 );
195 }
196 }
197
198 // Replace last image associated with current photoAlbum:
199 async function updateLastImage(e) {
200 if (!currentPhotoAlbum) return;
201
202 if (!e.target.files) return;
203
204 const file = e.target.files[0];
205
206 try {
207 // Upload new file to Storage:
208 const result = await Storage.put(
209 `${currentPhotoAlbum.id}-${file.name}`,
210 file,
211 {
212 contentType: 'image/png' // contentType is optional
213 }
214 );
215
216 const newFileKey = result?.key;
217
218 // Query existing record to retrieve currently associated files:
219 const queriedResponse = await API.graphql({
220 query: queries.getPhotoAlbum,
221 variables: { id: currentPhotoAlbum.id }
222 });
223
224 const photoAlbum = queriedResponse.data?.getPhotoAlbum;
225
226 if (!photoAlbum?.imageKeys?.length) return;
227
228 // Retrieve last image key:
229 const [lastImageKey] = photoAlbum.imageKeys.slice(-1);
230
231 // Remove last file association by key
232 const updatedImageKeys = [
233 ...photoAlbum.imageKeys.filter((key) => key !== lastImageKey),
234 newFileKey
235 ];
236
237 const photoAlbumDetails = {
238 id: currentPhotoAlbum.id,
239 // @ts-ignore
240 imageKeys: updatedImageKeys
241 };
242
243 // Update record with updated file associations:
244 const response = await API.graphql({
245 query: mutations.updatePhotoAlbum,
246 variables: { input: photoAlbumDetails }
247 });
248
249 const updatedPhotoAlbum = response?.data?.updatePhotoAlbum;
250 setCurrentPhotoAlbum(updatedPhotoAlbum);
251
252 // If the record has no associated file, we can return early.
253 if (!updatedPhotoAlbum?.imageKeys) return;
254
255 // Retrieve signed urls for merged image keys:
256 const signedUrls = await Promise.all(
257 updatedPhotoAlbum?.imageKeys.map(async (key) => await Storage.get(key))
258 );
259
260 if (!signedUrls) return;
261
262 setCurrentImages(signedUrls);
263 } catch (error) {
264 console.error(
265 'Error uploading image / adding image to photoAlbum: ',
266 error
267 );
268 }
269 }
270
271 async function getImagesForPhotoAlbum() {
272 try {
273 // Query the record to get the file keys:
274 const response = await API.graphql({
275 query: queries.getPhotoAlbum,
276 variables: { id: currentPhotoAlbum.id }
277 });
278 const photoAlbum = response.data?.getPhotoAlbum;
279
280 // If the record has no associated files, we can return early.
281 if (!photoAlbum?.imageKeys) return;
282
283 // Retrieve the signed URLs for the associated images:
284 const signedUrls = await Promise.all(
285 photoAlbum.imageKeys.map(async (imageKey) => {
286 if (!imageKey) return;
287 return await Storage.get(imageKey);
288 })
289 );
290
291 setCurrentImages(signedUrls);
292 } catch (error) {
293 console.error('Error getting photoAlbum / image:', error);
294 }
295 }
296
297 // Remove the file associations, continue to persist both files and record
298 async function removeImagesFromPhotoAlbum() {
299 if (!currentPhotoAlbum) return;
300
301 try {
302 const response = await API.graphql({
303 query: queries.getPhotoAlbum,
304 variables: { id: currentPhotoAlbum.id }
305 });
306
307 const photoAlbum = response?.data?.getPhotoAlbum;
308
309 // If the record has no associated file, we can return early.
310 if (!photoAlbum?.imageKeys) return;
311
312 const photoAlbumDetails = {
313 id: photoAlbum.id,
314 imageKeys: null
315 };
316
317 const updatedPhotoAlbum = await API.graphql({
318 query: mutations.updatePhotoAlbum,
319 variables: { input: photoAlbumDetails }
320 });
321
322 // If successful, the response here will be `null`:
323 setCurrentPhotoAlbum(updatedPhotoAlbum?.data?.updatePhotoAlbum);
324 setCurrentImages(updatedPhotoAlbum?.data?.updatePhotoAlbum?.imageKeys);
325 } catch (error) {
326 console.error('Error removing image from photoAlbum: ', error);
327 }
328 }
329
330 // Remove the record association and delete the file
331 async function deleteImagesForCurrentPhotoAlbum() {
332 if (!currentPhotoAlbum) return;
333
334 try {
335 const response = await API.graphql({
336 query: queries.getPhotoAlbum,
337 variables: { id: currentPhotoAlbum.id }
338 });
339
340 const photoAlbum = response?.data?.getPhotoAlbum;
341
342 // If the record has no associated files, we can return early.
343 if (!photoAlbum?.imageKeys) return;
344
345 const photoAlbumDetails = {
346 id: photoAlbum.id,
347 imageKeys: null // Set the file association to `null`
348 };
349
350 // Remove associated files from record
351 const updateResponse = await API.graphql({
352 query: mutations.updatePhotoAlbum,
353 variables: { input: photoAlbumDetails }
354 });
355
356 const updatedPhotoAlbum = updateResponse?.data?.updatePhotoAlbum;
357
358 // Delete the files from S3:
359 await Promise.all(
360 photoAlbum?.imageKeys.map(async (imageKey) => {
361 if (!imageKey) return;
362 await Storage.remove(imageKey);
363 })
364 );
365
366 // If successful, the response here will be `null`:
367 setCurrentPhotoAlbum(updatedPhotoAlbum);
368 setCurrentImages(null);
369 } catch (error) {
370 console.error('Error deleting image: ', error);
371 }
372 }
373
374 // Delete both files and record
375 async function deleteCurrentPhotoAlbumAndImages() {
376 if (!currentPhotoAlbum) return;
377
378 try {
379 const response = await API.graphql({
380 query: queries.getPhotoAlbum,
381 variables: { id: currentPhotoAlbum.id }
382 });
383
384 const photoAlbum = response?.data?.getPhotoAlbum;
385
386 if (!photoAlbum) return;
387
388 const photoAlbumDetails = {
389 id: photoAlbum.id
390 };
391
392 await API.graphql({
393 query: mutations.deletePhotoAlbum,
394 variables: { input: photoAlbumDetails }
395 });
396
397 setCurrentPhotoAlbum(null);
398
399 // If the record has no associated file, we can return early.
400 if (!photoAlbum?.imageKeys) return;
401
402 await Promise.all(
403 photoAlbum?.imageKeys.map(async (imageKey) => {
404 if (!imageKey) return;
405 await Storage.remove(imageKey);
406 })
407 );
408
409 clearLocalState();
410 } catch (error) {
411 console.error('Error deleting photoAlbum: ', error);
412 }
413 }
414
415 function clearLocalState() {
416 setCurrentPhotoAlbum(null);
417 setCurrentImages([]);
418 }
419
420 return (
421 <Authenticator>
422 {({ signOut, user }) => (
423 <main
424 style={{
425 alignItems: 'center',
426 display: 'flex',
427 flexDirection: 'column'
428 }}
429 >
430 <h1>Hello {user?.username}!</h1>
431 <h2>{`Current PhotoAlbum: ${currentPhotoAlbum?.id}`}</h2>
432 <label>
433 Create photoAlbum with one file:
434 <input
435 type="file"
436 accept="image/*"
437 onChange={createPhotoAlbumWithFirstImage}
438 />
439 </label>
440 <label>
441 Create photoAlbum with multiple files:
442 <input
443 type="file"
444 accept="image/*"
445 onChange={createPhotoAlbumWithMultipleImages}
446 multiple
447 />
448 </label>
449 <label>
450 Add multiple images to current photoAlbum:
451 <input
452 type="file"
453 accept="image/*"
454 onChange={addNewImagesToPhotoAlbum}
455 disabled={!currentPhotoAlbum}
456 multiple
457 />
458 </label>
459 <label>
460 Replace last image:
461 <input
462 type="file"
463 accept="image/*"
464 onChange={updateLastImage}
465 disabled={!currentPhotoAlbum || !currentImages}
466 />
467 </label>
468 <button
469 onClick={getImagesForPhotoAlbum}
470 disabled={!currentPhotoAlbum || !currentImages}
471 >
472 Get Images for Current Photo Album
473 </button>
474 <button
475 onClick={removeImagesFromPhotoAlbum}
476 disabled={!currentPhotoAlbum || !currentImages}
477 >
478 Remove images from current PhotoAlbum (does not delete images)
479 </button>
480 <button
481 onClick={deleteImagesForCurrentPhotoAlbum}
482 disabled={!currentPhotoAlbum || !currentImages}
483 >
484 Remove images from current PhotoAlbum, then delete images
485 </button>
486 <button
487 onClick={deleteCurrentPhotoAlbumAndImages}
488 disabled={!currentPhotoAlbum}
489 >
490 Delete current PhotoAlbum (and images, if they exist)
491 </button>
492 <button onClick={signOut}>Sign out</button>
493 {currentImages &&
494 currentImages.map((url, idx) => {
495 if (!url) return undefined;
496 return <img src={url} key={idx} alt="Storage file"></img>;
497 })}
498 </main>
499 )}
500 </Authenticator>
501 );
502}
503
504export default App;