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-react3
4# Select default configuration:5amplify add auth6
7# Select "Content", "Auth users only", full CRUD access,8# and default configuration:9amplify add storage10
11# Select default configuration12amplify 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 create5}
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' });
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.
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 optional18});19
20const updateSongDetails: UpdateSongInput = {21 id: song.id,22 coverArtKey: result?.key23};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);
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 optional4});5
6const updateSongDetails: UpdateSongInput = {7 id: currentSong.id,8 coverArtKey: result?.key9};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);
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);
Delete and remove files associated with API records
There are three common deletion workflows when working with Storage files and the GraphQL API:
- Remove the file association, continue to persist both file and record.
- Remove the record association and delete the file.
- 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: null14};15
16const updatedSong = await API.graphql<GraphQLQuery<UpdateSongMutation>>({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 record17const 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);
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.id15};16
17const deletedSong = await API.graphql<GraphQLQuery<DeleteSongMutation>>({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 create5}
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 optional20 });21
22 return result?.key;23 })24);25
26const updatePhotoAlbumDetails: UpdatePhotoAlbumInput = {27 id: photoAlbum.id,28 imageKeys: imageKeys29};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);
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 optional18});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]!);
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 optional9 }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: updatedImageKeys32};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);
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 optional4});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 key22const updatedImageKeys = [23 ...photoAlbum.imageKeys.filter((key) => key !== lastImageKey),24 newFileKey25];26
27const photoAlbumDetails: UpdatePhotoAlbumInput = {28 id: currentPhotoAlbum.id,29 // @ts-ignore30 imageKeys: updatedImageKeys31};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);
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);
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: null14};15
16const updatedPhotoAlbum = await API.graphql<17 GraphQLQuery<UpdatePhotoAlbumMutation>18>({19 query: mutations.updatePhotoAlbum,20 variables: { input: photoAlbumDetails }21});
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 record17const 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);
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.id12};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);
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 | undefined24 >("");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 optional52 });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 optional95 });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 record145 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 file178 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 record198 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 record215 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 <main255 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 <input270 id="name"271 type="file"272 onChange={addNewImageToSong}273 disabled={!currentSong}274 />275 </label>276 <button277 onClick={getImageForCurrentSong}278 disabled={!currentSong || !currentImageUrl}279 >280 Get image for current song281 </button>282 <button283 onClick={removeImageFromSong}284 disabled={!currentSong || !currentImageUrl}285 >286 Remove image from current song (does not delete image)287 </button>288 <button289 onClick={deleteImageForCurrentSong}290 disabled={!currentSong || !currentImageUrl}291 >292 Remove image from current song, then delete image293 </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;