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.

1import { generateClient } from 'aws-amplify/api';
2import { uploadData, getUrl } from 'aws-amplify/storage';
3
4const client = generateClient();
5
6const createSongDetails = {
7 name: `My first song`
8};
9
10// Create the API record:
11const response = await client.graphql({
12 query: mutations.createSong,
13 variables: { input: createSongDetails }
14});
15
16const song = response.data.createSong;
17
18if (!song) return;
19
20// Upload the Storage file:
21const result = await uploadData({
22 key: `${song.id}-${file.name}`,
23 data: file,
24 options: {
25 contentType: 'image/png' // contentType is optional
26 }
27}).result;
28
29const updateSongDetails = {
30 id: song.id,
31 coverArtKey: result?.key
32};
33
34// Add the file association to the record:
35const updateResponse = await client.graphql({
36 query: mutations.updateSong,
37 variables: { input: updateSongDetails }
38});
39
40const updatedSong = updateResponse.data.updateSong;
41
42// If the record has no associated file, we can return early.
43if (!updatedSong.coverArtKey) return;
44
45// Retrieve the file's signed URL:
46const signedURL = await getUrl({ key: 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.

1import { generateClient } from 'aws-amplify/api';
2import { uploadData, getUrl } from 'aws-amplify/storage';
3
4const client = generateClient();
5// Upload the Storage file:
6const result = await uploadData({
7 key: `${currentSong.id}-${file.name}`,
8 data: file,
9 options: {
10 contentType: 'image/png' // contentType is optional
11 }
12}).result;
13
14const updateSongDetails = {
15 id: currentSong.id,
16 coverArtKey: result?.key
17};
18
19// Add the file association to the record:
20const response = await client.graphql({
21 query: mutations.updateSong,
22 variables: { input: updateSongDetails }
23});
24
25const updatedSong = response.data.updateSong;
26
27// If the record has no associated file, we can return early.
28if (!updatedSong?.coverArtKey) return;
29
30// Retrieve the file's signed URL:
31const signedURL = await getUrl({ key: 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:

1import { generateClient } from 'aws-amplify/api';
2import { getUrl } from 'aws-amplify/storage';
3
4const client = generateClient();
5
6// Query the record to get the file key:
7const response = await client.graphql({
8 query: queries.getSong,
9 variables: { id: currentSong.id }
10});
11const song = response.data.getSong;
12
13// If the record has no associated file, we can return early.
14if (!song?.coverArtKey) return;
15
16// Retrieve the signed URL:
17const signedURL = await getUrl({ key: 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.

1import { generateClient } from 'aws-amplify/api';
2
3const client = generateClient();
4
5// Remove the file association, continue to persist both file and record
6const response = await client.graphql({
7 query: queries.getSong,
8 variables: { id: currentSong.id }
9});
10
11const song = response.data.getSong;
12
13// If the record has no associated file, we can return early.
14if (!song?.coverArtKey) return;
15
16const songDetails = {
17 id: song.id,
18 coverArtKey: null
19};
20
21const updatedSong = await client.graphql({
22 query: mutations.updateSong,
23 variables: { input: songDetails }
24});

Remove the record association and delete the file

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

1import { generateClient } from 'aws-amplify/api';
2import { remove } from 'aws-amplify/storage';
3
4const client = generateClient();
5
6// Remove the record association and delete the file
7const response = await client.graphql({
8 query: queries.getSong,
9 variables: { id: currentSong.id }
10});
11
12const song = response?.data?.getSong;
13
14// If the record has no associated file, we can return early.
15if (!song?.coverArtKey) return;
16
17const songDetails = {
18 id: song.id,
19 coverArtKey: null // Set the file association to `null`
20};
21
22// Remove associated file from record
23const updatedSong = await client.graphql({
24 query: mutations.updateSong,
25 variables: { input: songDetails }
26});
27
28// Delete the file from S3:
29await remove({ key: song.coverArtKey });

Delete both file and record

1import { generateClient } from 'aws-amplify/api';
2import { remove } from 'aws-amplify/storage';
3
4const client = generateClient();
5
6// Delete both file and record
7const response = await client.graphql({
8 query: queries.getSong,
9 variables: { id: currentSong.id }
10});
11
12const song = response.data.getSong;
13
14// If the record has no associated file, we can return early.
15if (!song?.coverArtKey) return;
16
17await remove({ key: song.coverArtKey });
18
19const songDetails = {
20 id: song.id
21};
22
23await client.graphql({
24 query: mutations.deleteSong,
25 variables: { input: songDetails }
26});

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.

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

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.

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

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.

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

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.

1import { generateClient } from 'aws-amplify/api';
2import { getUrl } from 'aws-amplify/storage';
3
4const client = generateClient();
5
6// Query the record to get the file keys:
7const response = await client.graphql({
8 query: queries.getPhotoAlbum,
9 variables: { id: currentPhotoAlbum.id }
10});
11const photoAlbum = response.data.getPhotoAlbum;
12
13// If the record has no associated files, we can return early.
14if (!photoAlbum?.imageKeys) return;
15
16// Retrieve the signed URLs for the associated images:
17const signedUrls = await Promise.all(
18 photoAlbum.imageKeys.map(async (imageKey) => {
19 if (!imageKey) return;
20 return await getUrl({ key: imageKey });
21 })
22);

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

1import { generateClient } from 'aws-amplify/api';
2
3const client = generateClient();
4
5const response = await client.graphql({
6 query: queries.getPhotoAlbum,
7 variables: { id: currentPhotoAlbum.id }
8});
9
10const photoAlbum = response.data.getPhotoAlbum;
11
12// If the record has no associated file, we can return early.
13if (!photoAlbum?.imageKeys) return;
14
15const photoAlbumDetails = {
16 id: photoAlbum.id,
17 imageKeys: null
18};
19
20const updatedPhotoAlbum = await client.graphql({
21 query: mutations.updatePhotoAlbum,
22 variables: { input: photoAlbumDetails }
23});

Remove the record association and delete the files

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

Delete record and all associated files

1import { generateClient } from 'aws-amplify/api';
2import { remove } from 'aws-amplify/storage';
3
4const client = generateClient();
5
6const response = await client.graphql({
7 query: queries.getPhotoAlbum,
8 variables: { id: currentPhotoAlbum.id }
9});
10
11const photoAlbum = response.data.getPhotoAlbum;
12
13if (!photoAlbum) return;
14
15const photoAlbumDetails = {
16 id: photoAlbum.id
17};
18
19await client.graphql({
20 query: mutations.deletePhotoAlbum,
21 variables: { input: photoAlbumDetails }
22});
23
24// If the record has no associated file, we can return early.
25if (!photoAlbum?.imageKeys) return;
26
27await Promise.all(
28 photoAlbum?.imageKeys.map(async (imageKey) => {
29 if (!imageKey) return;
30 await remove({ key: imageKey });
31 })
32);

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