Amplify has re-imagined the way frontend developers build fullstack applications. Develop and deploy without the hassle.

Page updated Apr 29, 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:

# For Authenticator component:
npm i @aws-amplify/ui-react
# Select default configuration:
amplify add auth
# Select "Content", "Auth users only", full CRUD access,
# and default configuration:
amplify add storage
# Select default configuration
amplify add api

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

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

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.

Storage.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.

import { generateClient } from 'aws-amplify/api';
import { uploadData, getUrl } from 'aws-amplify/storage';
const client = generateClient();
const createSongDetails = {
name: `My first song`
};
// Create the API record:
const response = await client.graphql({
query: mutations.createSong,
variables: { input: createSongDetails }
});
const song = response.data.createSong;
if (!song) return;
// Upload the Storage file:
const result = await uploadData({
key: `${song.id}-${file.name}`,
data: file,
options: {
contentType: 'image/png' // contentType is optional
}
}).result;
const updateSongDetails = {
id: song.id,
coverArtKey: result?.key
};
// Add the file association to the record:
const updateResponse = await client.graphql({
query: mutations.updateSong,
variables: { input: updateSongDetails }
});
const updatedSong = updateResponse.data.updateSong;
// If the record has no associated file, we can return early.
if (!updatedSong.coverArtKey) return;
// Retrieve the file's signed URL:
const 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.

import { generateClient } from 'aws-amplify/api';
import { uploadData, getUrl } from 'aws-amplify/storage';
const client = generateClient();
// Upload the Storage file:
const result = await uploadData({
key: `${currentSong.id}-${file.name}`,
data: file,
options: {
contentType: 'image/png' // contentType is optional
}
}).result;
const updateSongDetails = {
id: currentSong.id,
coverArtKey: result?.key
};
// Add the file association to the record:
const response = await client.graphql({
query: mutations.updateSong,
variables: { input: updateSongDetails }
});
const updatedSong = response.data.updateSong;
// If the record has no associated file, we can return early.
if (!updatedSong?.coverArtKey) return;
// Retrieve the file's signed URL:
const 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:

import { generateClient } from 'aws-amplify/api';
import { getUrl } from 'aws-amplify/storage';
const client = generateClient();
// Query the record to get the file key:
const response = await client.graphql({
query: queries.getSong,
variables: { id: currentSong.id }
});
const song = response.data.getSong;
// If the record has no associated file, we can return early.
if (!song?.coverArtKey) return;
// Retrieve the signed URL:
const 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.

import { generateClient } from 'aws-amplify/api';
const client = generateClient();
// Remove the file association, continue to persist both file and record
const response = await client.graphql({
query: queries.getSong,
variables: { id: currentSong.id }
});
const song = response.data.getSong;
// If the record has no associated file, we can return early.
if (!song?.coverArtKey) return;
const songDetails = {
id: song.id,
coverArtKey: null
};
const updatedSong = await client.graphql({
query: mutations.updateSong,
variables: { input: songDetails }
});

Remove the record association and delete the file

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

import { generateClient } from 'aws-amplify/api';
import { remove } from 'aws-amplify/storage';
const client = generateClient();
// Remove the record association and delete the file
const response = await client.graphql({
query: queries.getSong,
variables: { id: currentSong.id }
});
const song = response?.data?.getSong;
// If the record has no associated file, we can return early.
if (!song?.coverArtKey) return;
const songDetails = {
id: song.id,
coverArtKey: null // Set the file association to `null`
};
// Remove associated file from record
const updatedSong = await client.graphql({
query: mutations.updateSong,
variables: { input: songDetails }
});
// Delete the file from S3:
await remove({ key: song.coverArtKey });

Delete both file and record

import { generateClient } from 'aws-amplify/api';
import { remove } from 'aws-amplify/storage';
const client = generateClient();
// Delete both file and record
const response = await client.graphql({
query: queries.getSong,
variables: { id: currentSong.id }
});
const song = response.data.getSong;
// If the record has no associated file, we can return early.
if (!song?.coverArtKey) return;
await remove({ key: song.coverArtKey });
const songDetails = {
id: song.id
};
await client.graphql({
query: mutations.deleteSong,
variables: { input: songDetails }
});

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:

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

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.

import { generateClient } from 'aws-amplify/api';
import { uploadData, getUrl } from 'aws-amplify/storage';
const client = generateClient();
const photoAlbumDetails = {
name: `My first photoAlbum`
};
// Create the API record:
const response = await client.graphql({
query: mutations.createPhotoAlbum,
variables: { input: photoAlbumDetails }
});
const photoAlbum = response.data.createPhotoAlbum;
if (!photoAlbum) return;
// Upload all files to Storage:
const imageKeys = await Promise.all(
Array.from(e.target.files).map(async (file) => {
const result = await uploadData({
key: `${photoAlbum.id}-${file.name}`,
data: file,
options: {
contentType: 'image/png' // contentType is optional
}
}).result;
return result.key;
})
);
const updatePhotoAlbumDetails = {
id: photoAlbum.id,
imageKeys: imageKeys
};
// Add the file association to the record:
const updateResponse = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: updatePhotoAlbumDetails }
});
const updatedPhotoAlbum = updateResponse.data.updatePhotoAlbum;
// If the record has no associated file, we can return early.
if (!updatedPhotoAlbum.imageKeys?.length) return;
// Retrieve signed urls for all files:
const signedUrls = await Promise.all(
updatedPhotoAlbum.imageKeys.map(async (key) => await getUrl({ key }))
);

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.

import { generateClient } from 'aws-amplify/api';
import { uploadData, getUrl } from 'aws-amplify/storage';
const client = generateClient();
// Upload all files to Storage:
const newImageKeys = await Promise.all(
Array.from(e.target.files).map(async (file) => {
const result = await uploadData({
key: `${currentPhotoAlbum.id}-${file.name}`,
data: file,
options: {
contentType: 'image/png' // contentType is optional
}
}).result;
return result.key;
})
);
// Query existing record to retrieve currently associated files:
const queriedResponse = await client.graphql({
query: queries.getPhotoAlbum,
variables: { id: currentPhotoAlbum.id }
});
const photoAlbum = queriedResponse.data.getPhotoAlbum;
if (!photoAlbum?.imageKeys) return;
// Merge existing and new file keys:
const updatedImageKeys = [...newImageKeys, ...photoAlbum.imageKeys];
const photoAlbumDetails = {
id: currentPhotoAlbum.id,
imageKeys: updatedImageKeys
};
// Update record with merged file associations:
const response = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: photoAlbumDetails }
});
const updatedPhotoAlbum = response.data.updatePhotoAlbum;
// If the record has no associated file, we can return early.
if (!updatedPhotoAlbum?.imageKeys) return;
// Retrieve signed urls for merged image keys:
const signedUrls = await Promise.all(
updatedPhotoAlbum?.imageKeys.map(async (key) => await getUrl({ key }))
);

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.

import { generateClient } from 'aws-amplify/api';
import { uploadData, getUrl } from 'aws-amplify/storage';
const client = generateClient();
// Upload new file to Storage:
const result = await uploadData({
key: `${currentPhotoAlbum.id}-${file.name}`,
data: file,
options: {
contentType: 'image/png' // contentType is optional
}
}).result;
const newFileKey = result.key;
// Query existing record to retrieve currently associated files:
const queriedResponse = await client.graphql({
query: queries.getPhotoAlbum,
variables: { id: currentPhotoAlbum.id }
});
const photoAlbum = queriedResponse.data.getPhotoAlbum;
if (!photoAlbum?.imageKeys?.length) return;
// Retrieve last image key:
const [lastImageKey] = photoAlbum.imageKeys.slice(-1);
// Remove last file association by key
const updatedImageKeys = [
...photoAlbum.imageKeys.filter((key) => key !== lastImageKey),
newFileKey
];
const photoAlbumDetails = {
id: currentPhotoAlbum.id,
imageKeys: updatedImageKeys
};
// Update record with updated file associations:
const response = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: photoAlbumDetails }
});
const updatedPhotoAlbum = response.data.updatePhotoAlbum;
// If the record has no associated file, we can return early.
if (!updatedPhotoAlbum?.imageKeys) return;
// Retrieve signed urls for merged image keys:
const signedUrls = await Promise.all(
updatedPhotoAlbum?.imageKeys.map(async (key) => await getUrl({ key }))
);

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.

import { generateClient } from 'aws-amplify/api';
import { getUrl } from 'aws-amplify/storage';
const client = generateClient();
// Query the record to get the file keys:
const response = await client.graphql({
query: queries.getPhotoAlbum,
variables: { id: currentPhotoAlbum.id }
});
const photoAlbum = response.data.getPhotoAlbum;
// If the record has no associated files, we can return early.
if (!photoAlbum?.imageKeys) return;
// Retrieve the signed URLs for the associated images:
const signedUrls = await Promise.all(
photoAlbum.imageKeys.map(async (imageKey) => {
if (!imageKey) return;
return await getUrl({ key: imageKey });
})
);

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

import { generateClient } from 'aws-amplify/api';
const client = generateClient();
const response = await client.graphql({
query: queries.getPhotoAlbum,
variables: { id: currentPhotoAlbum.id }
});
const photoAlbum = response.data.getPhotoAlbum;
// If the record has no associated file, we can return early.
if (!photoAlbum?.imageKeys) return;
const photoAlbumDetails = {
id: photoAlbum.id,
imageKeys: null
};
const updatedPhotoAlbum = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: photoAlbumDetails }
});

Remove the record association and delete the files

import { generateClient } from 'aws-amplify/api';
import { remove } from 'aws-amplify/storage';
const client = generateClient();
const response = await client.graphql({
query: queries.getPhotoAlbum,
variables: { id: currentPhotoAlbum.id }
});
const photoAlbum = response.data.getPhotoAlbum;
// If the record has no associated files, we can return early.
if (!photoAlbum?.imageKeys) return;
const photoAlbumDetails = {
id: photoAlbum.id,
imageKeys: null // Set the file association to `null`
};
// Remove associated files from record
const updateResponse = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: photoAlbumDetails }
});
const updatedPhotoAlbum = updateResponse.data.updatePhotoAlbum;
// Delete the files from S3:
await Promise.all(
photoAlbum?.imageKeys.map(async (imageKey) => {
if (!imageKey) return;
await remove({ key: imageKey });
})
);

Delete record and all associated files

import { generateClient } from 'aws-amplify/api';
import { remove } from 'aws-amplify/storage';
const client = generateClient();
const response = await client.graphql({
query: queries.getPhotoAlbum,
variables: { id: currentPhotoAlbum.id }
});
const photoAlbum = response.data.getPhotoAlbum;
if (!photoAlbum) return;
const photoAlbumDetails = {
id: photoAlbum.id
};
await client.graphql({
query: mutations.deletePhotoAlbum,
variables: { input: photoAlbumDetails }
});
// If the record has no associated file, we can return early.
if (!photoAlbum?.imageKeys) return;
await Promise.all(
photoAlbum?.imageKeys.map(async (imageKey) => {
if (!imageKey) return;
await remove({ key: imageKey });
})
);

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

import { useState } from "react";
import { Amplify } from 'aws-amplify'
import { generateClient } from 'aws-amplify/api'
import { getUrl, uploadData, remove } from 'aws-amplify/storage'
import { Authenticator } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";
import config from './amplifyconfiguration.json'
import * as queries from "./graphql/queries";
import * as mutations from "./graphql/mutations";
import { Song } from "./API";
Amplify.configure(config, {
Storage: {
S3: {
// configures default access level
defaultAccessLevel: 'private'
}
}
})
const client = generateClient()
function App() {
const [currentSong, setCurrentSong] = useState<Song | null>(null);
// Used to display image for current song:
const [currentImageUrl, setCurrentImageUrl] = useState<string | null | undefined>("");
async function createSongWithImage(e: React.ChangeEvent<HTMLInputElement>) {
if (!e.target.files) return;
const file = e.target.files[0];
try {
const createSongDetails = {
name: `My first song`,
};
// Create the API record:
const response = await client.graphql({
query: mutations.createSong,
variables: { input: createSongDetails },
});
const song = response.data.createSong;
if (!song) return;
// Upload the Storage file:
const result = await uploadData({
key: `${song.id}-${file.name}`,
data: file,
options: {
contentType: "image/png", // contentType is optional
}
}).result;
const updateSongDetails = {
id: song.id,
coverArtKey: result?.key,
};
// Add the file association to the record:
const updateResponse = await client.graphql({
query: mutations.updateSong,
variables: { input: updateSongDetails },
});
const updatedSong = updateResponse.data.updateSong;
setCurrentSong(updatedSong);
// If the record has no associated file, we can return early.
if (!updatedSong.coverArtKey) return;
// Retrieve the file's signed URL:
const signedURL = await getUrl({ key: updatedSong.coverArtKey });
setCurrentImageUrl(signedURL.url.toString());
} catch (error) {
console.error("Error create song / file:", error);
}
}
// Upload image, add to song, retrieve signed URL and retrieve the image.
// Also updates image if one already exists.
async function addNewImageToSong(e: React.ChangeEvent<HTMLInputElement>) {
if (!currentSong) return;
if (!e.target.files) return;
const file = e.target.files[0];
try {
// Upload the Storage file:
const result = await uploadData({
key: `${currentSong.id}-${file.name}`,
data: file, options: {
contentType: "image/png", // contentType is optional
}
}).result;
const updateSongDetails = {
id: currentSong.id,
coverArtKey: result?.key,
};
// Add the file association to the record:
const response = await client.graphql({
query: mutations.updateSong,
variables: { input: updateSongDetails },
});
const updatedSong = response.data.updateSong;
setCurrentSong(updatedSong);
// If the record has no associated file, we can return early.
if (!updatedSong?.coverArtKey) return;
// Retrieve the file's signed URL:
const signedURL = await getUrl({ key: updatedSong.coverArtKey });
setCurrentImageUrl(signedURL.url.toString());
} catch (error) {
console.error("Error uploading image / adding image to song: ", error);
}
}
async function getImageForCurrentSong() {
if (!currentSong) return;
try {
// Query the record to get the file key:
const response = await client.graphql({
query: queries.getSong,
variables: { id: currentSong.id },
});
const song = response.data.getSong;
// If the record has no associated file, we can return early.
if (!song?.coverArtKey) return;
// Retrieve the signed URL:
const signedURL = await getUrl({ key: song.coverArtKey });
setCurrentImageUrl(signedURL.url.toString());
} catch (error) {
console.error("Error getting song / image:", error);
}
}
// Remove the file association, continue to persist both file and record
async function removeImageFromSong() {
if (!currentSong) return;
try {
const response = await client.graphql({
query: queries.getSong,
variables: { id: currentSong.id },
});
const song = response.data.getSong;
// If the record has no associated file, we can return early.
if (!song?.coverArtKey) return;
const songDetails = {
id: song.id,
coverArtKey: null,
};
const updatedSong = await client.graphql({
query: mutations.updateSong,
variables: { input: songDetails },
});
// If successful, the response here will be `null`:
setCurrentSong(updatedSong.data.updateSong);
setCurrentImageUrl(updatedSong.data.updateSong.coverArtKey);
} catch (error) {
console.error("Error removing image from song: ", error);
}
}
// Remove the record association and delete the file
async function deleteImageForCurrentSong() {
if (!currentSong) return;
try {
const response = await client.graphql({
query: queries.getSong,
variables: { id: currentSong.id },
});
const song = response?.data?.getSong;
// If the record has no associated file, we can return early.
if (!song?.coverArtKey) return;
const songDetails = {
id: song.id,
coverArtKey: null, // Set the file association to `null`
};
// Remove associated file from record
const updatedSong = await client.graphql({
query: mutations.updateSong,
variables: { input: songDetails },
});
// Delete the file from S3:
await remove({ key: song.coverArtKey });
// If successful, the response here will be `null`:
setCurrentSong(updatedSong.data.updateSong);
setCurrentImageUrl(updatedSong.data.updateSong.coverArtKey);
} catch (error) {
console.error("Error deleting image: ", error);
}
}
// Delete both file and record
async function deleteCurrentSongAndImage() {
if (!currentSong) return;
try {
const response = await client.graphql({
query: queries.getSong,
variables: { id: currentSong.id },
});
const song = response.data.getSong;
// If the record has no associated file, we can return early.
if (!song?.coverArtKey) return;
await remove({ key: song.coverArtKey });
const songDetails = {
id: song.id,
};
await client.graphql({
query: mutations.deleteSong,
variables: { input: songDetails },
});
clearLocalState();
} catch (error) {
console.error("Error deleting song: ", error);
}
}
function clearLocalState() {
setCurrentSong(null);
setCurrentImageUrl("");
}
return (
<Authenticator>
{({ signOut, user }) => (
<main
style={{
alignItems: "center",
display: "flex",
flexDirection: "column",
}}
>
<h1>Hello {user?.username}!</h1>
<h2>{`Current Song: ${currentSong?.id}`}</h2>
<label>
Create song with file:
<input id="name" type="file" onChange={createSongWithImage} />
</label>
<label>
Add / update song image:
<input
id="name"
type="file"
onChange={addNewImageToSong}
disabled={!currentSong}
/>
</label>
<button
onClick={getImageForCurrentSong}
disabled={!currentSong || !currentImageUrl}
>
Get image for current song
</button>
<button
onClick={removeImageFromSong}
disabled={!currentSong || !currentImageUrl}
>
Remove image from current song (does not delete image)
</button>
<button
onClick={deleteImageForCurrentSong}
disabled={!currentSong || !currentImageUrl}
>
Remove image from current song, then delete image
</button>
<button onClick={deleteCurrentSongAndImage} disabled={!currentSong}>
Delete current song (and image, if it exists)
</button>
<button onClick={signOut}>Sign out</button>
{currentImageUrl && (
<img src={currentImageUrl} alt="Storage file"></img>
)}
</main>
)}
</Authenticator>
);
}
export default App;
import { useState } from 'react';
import { Amplify } from 'aws-amplify';
import { generateClient } from 'aws-amplify/api';
import { getUrl, uploadData, remove } from 'aws-amplify/storage';
import { Authenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import config from './amplifyconfiguration.json';
import * as queries from './graphql/queries';
import * as mutations from './graphql/mutations';
Amplify.configure(config, {
Storage: {
S3: {
// configures default access level
defaultAccessLevel: 'private'
}
}
});
const client = generateClient();
function App() {
const [currentSong, setCurrentSong] = useState(null);
// Used to display image for current song:
const [currentImageUrl, setCurrentImageUrl] = useState('');
async function createSongWithImage(e) {
if (!e.target.files) return;
const file = e.target.files[0];
try {
const createSongDetails = {
name: `My first song`
};
// Create the API record:
const response = await client.graphql({
query: mutations.createSong,
variables: { input: createSongDetails }
});
const song = response.data.createSong;
if (!song) return;
// Upload the Storage file:
const result = await uploadData({
key: `${song.id}-${file.name}`,
data: file,
options: {
contentType: 'image/png' // contentType is optional
}
}).result;
const updateSongDetails = {
id: song.id,
coverArtKey: result?.key
};
// Add the file association to the record:
const updateResponse = await client.graphql({
query: mutations.updateSong,
variables: { input: updateSongDetails }
});
const updatedSong = updateResponse.data.updateSong;
setCurrentSong(updatedSong);
// If the record has no associated file, we can return early.
if (!updatedSong.coverArtKey) return;
// Retrieve the file's signed URL:
const signedURL = await getUrl({ key: updatedSong.coverArtKey });
setCurrentImageUrl(signedURL.url.toString());
} catch (error) {
console.error('Error create song / file:', error);
}
}
// Upload image, add to song, retrieve signed URL and retrieve the image.
// Also updates image if one already exists.
async function addNewImageToSong(e) {
if (!currentSong) return;
if (!e.target.files) return;
const file = e.target.files[0];
try {
// Upload the Storage file:
const result = await uploadData({
key: `${currentSong.id}-${file.name}`,
data: file,
options: {
contentType: 'image/png' // contentType is optional
}
}).result;
const updateSongDetails = {
id: currentSong.id,
coverArtKey: result?.key
};
// Add the file association to the record:
const response = await client.graphql({
query: mutations.updateSong,
variables: { input: updateSongDetails }
});
const updatedSong = response.data.updateSong;
setCurrentSong(updatedSong);
// If the record has no associated file, we can return early.
if (!updatedSong?.coverArtKey) return;
// Retrieve the file's signed URL:
const signedURL = await getUrl({ key: updatedSong.coverArtKey });
setCurrentImageUrl(signedURL.url.toString());
} catch (error) {
console.error('Error uploading image / adding image to song: ', error);
}
}
async function getImageForCurrentSong() {
if (!currentSong) return;
try {
// Query the record to get the file key:
const response = await client.graphql({
query: queries.getSong,
variables: { id: currentSong.id }
});
const song = response.data.getSong;
// If the record has no associated file, we can return early.
if (!song?.coverArtKey) return;
// Retrieve the signed URL:
const signedURL = await getUrl({ key: song.coverArtKey });
setCurrentImageUrl(signedURL.url.toString());
} catch (error) {
console.error('Error getting song / image:', error);
}
}
// Remove the file association, continue to persist both file and record
async function removeImageFromSong() {
if (!currentSong) return;
try {
const response = await client.graphql({
query: queries.getSong,
variables: { id: currentSong.id }
});
const song = response.data.getSong;
// If the record has no associated file, we can return early.
if (!song?.coverArtKey) return;
const songDetails = {
id: song.id,
coverArtKey: null
};
const updatedSong = await client.graphql({
query: mutations.updateSong,
variables: { input: songDetails }
});
// If successful, the response here will be `null`:
setCurrentSong(updatedSong.data.updateSong);
setCurrentImageUrl(updatedSong.data.updateSong.coverArtKey);
} catch (error) {
console.error('Error removing image from song: ', error);
}
}
// Remove the record association and delete the file
async function deleteImageForCurrentSong() {
if (!currentSong) return;
try {
const response = await client.graphql({
query: queries.getSong,
variables: { id: currentSong.id }
});
const song = response?.data?.getSong;
// If the record has no associated file, we can return early.
if (!song?.coverArtKey) return;
const songDetails = {
id: song.id,
coverArtKey: null // Set the file association to `null`
};
// Remove associated file from record
const updatedSong = await client.graphql({
query: mutations.updateSong,
variables: { input: songDetails }
});
// Delete the file from S3:
await remove({ key: song.coverArtKey });
// If successful, the response here will be `null`:
setCurrentSong(updatedSong.data.updateSong);
setCurrentImageUrl(updatedSong.data.updateSong.coverArtKey);
} catch (error) {
console.error('Error deleting image: ', error);
}
}
// Delete both file and record
async function deleteCurrentSongAndImage() {
if (!currentSong) return;
try {
const response = await client.graphql({
query: queries.getSong,
variables: { id: currentSong.id }
});
const song = response.data.getSong;
// If the record has no associated file, we can return early.
if (!song?.coverArtKey) return;
await remove({ key: song.coverArtKey });
const songDetails = {
id: song.id
};
await client.graphql({
query: mutations.deleteSong,
variables: { input: songDetails }
});
clearLocalState();
} catch (error) {
console.error('Error deleting song: ', error);
}
}
function clearLocalState() {
setCurrentSong(null);
setCurrentImageUrl('');
}
return (
<Authenticator>
{({ signOut, user }) => (
<main
style={{
alignItems: 'center',
display: 'flex',
flexDirection: 'column'
}}
>
<h1>Hello {user?.username}!</h1>
<h2>{`Current Song: ${currentSong?.id}`}</h2>
<label>
Create song with file:
<input id="name" type="file" onChange={createSongWithImage} />
</label>
<label>
Add / update song image:
<input
id="name"
type="file"
onChange={addNewImageToSong}
disabled={!currentSong}
/>
</label>
<button
onClick={getImageForCurrentSong}
disabled={!currentSong || !currentImageUrl}
>
Get image for current song
</button>
<button
onClick={removeImageFromSong}
disabled={!currentSong || !currentImageUrl}
>
Remove image from current song (does not delete image)
</button>
<button
onClick={deleteImageForCurrentSong}
disabled={!currentSong || !currentImageUrl}
>
Remove image from current song, then delete image
</button>
<button onClick={deleteCurrentSongAndImage} disabled={!currentSong}>
Delete current song (and image, if it exists)
</button>
<button onClick={signOut}>Sign out</button>
{currentImageUrl && (
<img src={currentImageUrl} alt="Storage file"></img>
)}
</main>
)}
</Authenticator>
);
}
export default App;
import { useState } from "react";
import { Amplify } from 'aws-amplify'
import { generateClient } from 'aws-amplify/api'
import { getUrl, uploadData, remove } from 'aws-amplify/storage'
import { Authenticator } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";
import config from './amplifyconfiguration.json'
import * as queries from "./graphql/queries";
import * as mutations from "./graphql/mutations";
import { PhotoAlbum } from "./API";
Amplify.configure(config, {
Storage: {
S3: {
defaultAccessLevel: 'private'
}
}
})
const client = generateClient()
function App() {
const [currentPhotoAlbum, setCurrentPhotoAlbum] = useState<PhotoAlbum | null>(null);
// Used to display images for current photoAlbum:
const [currentImages, setCurrentImages] = useState<
(string | null | undefined)[] | null | undefined
>([]);
async function createPhotoAlbumWithFirstImage(
e: React.ChangeEvent<HTMLInputElement>
) {
if (!e.target.files) return;
const file = e.target.files[0];
try {
const photoAlbumDetails = {
name: `My first photoAlbum`,
};
// Create the API record:
const response = await client.graphql({
query: mutations.createPhotoAlbum,
variables: { input: photoAlbumDetails },
});
const photoAlbum = response.data.createPhotoAlbum;
if (!photoAlbum) return;
// Upload the Storage file:
const result = await uploadData({
key: `${photoAlbum.id}-${file.name}`,
data: file,
options: {
contentType: "image/png", // contentType is optional
}
}).result;
const updatePhotoAlbumDetails = {
id: photoAlbum.id,
imageKeys: [result.key],
};
// Add the file association to the record:
const updateResponse = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: updatePhotoAlbumDetails },
});
const updatedPhotoAlbum = updateResponse.data.updatePhotoAlbum;
setCurrentPhotoAlbum(updatedPhotoAlbum);
// If the record has no associated file, we can return early.
if (!updatedPhotoAlbum.imageKeys?.length) return;
// Retrieve the file's signed URL:
const signedURL = await getUrl({ key: updatedPhotoAlbum.imageKeys[0]! });
setCurrentImages([signedURL.url.toString()]);
} catch (error) {
console.error("Error create photoAlbum / file:", error);
}
}
async function createPhotoAlbumWithMultipleImages(
e: React.ChangeEvent<HTMLInputElement>
) {
if (!e.target.files) return;
try {
const photoAlbumDetails = {
name: `My first photoAlbum`,
};
// Create the API record:
const response = await client.graphql({
query: mutations.createPhotoAlbum,
variables: { input: photoAlbumDetails },
});
const photoAlbum = response.data.createPhotoAlbum;
if (!photoAlbum) return;
// Upload all files to Storage:
const imageKeys = await Promise.all(
Array.from(e.target.files).map(async (file) => {
const result = await uploadData({
key: `${photoAlbum.id}-${file.name}`,
data: file,
options: {
contentType: "image/png", // contentType is optional
}
}).result;
return result.key;
})
);
const updatePhotoAlbumDetails = {
id: photoAlbum.id,
imageKeys: imageKeys,
};
// Add the file association to the record:
const updateResponse = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: updatePhotoAlbumDetails },
});
const updatedPhotoAlbum = updateResponse.data.updatePhotoAlbum;
setCurrentPhotoAlbum(updatedPhotoAlbum);
// If the record has no associated file, we can return early.
if (!updatedPhotoAlbum.imageKeys?.length) return;
// Retrieve signed urls for all files:
const signedUrls = await Promise.all(
updatedPhotoAlbum.imageKeys.map(async (key) => await getUrl({ key: key! }))
);
if (!signedUrls) return;
setCurrentImages(signedUrls.map(signedUrl => signedUrl.url.toString()));
} catch (error) {
console.error("Error create photoAlbum / file:", error);
}
}
async function addNewImagesToPhotoAlbum(
e: React.ChangeEvent<HTMLInputElement>
) {
if (!currentPhotoAlbum) return;
if (!e.target.files) return;
try {
// Upload all files to Storage:
const newImageKeys = await Promise.all(
Array.from(e.target.files).map(async (file) => {
const result = await uploadData({
key: `${currentPhotoAlbum.id}-${file.name}`,
data: file,
options: {
contentType: "image/png", // contentType is optional
}
}).result;
return result.key;
})
);
// Query existing record to retrieve currently associated files:
const queriedResponse = await client.graphql({
query: queries.getPhotoAlbum,
variables: { id: currentPhotoAlbum.id },
});
const photoAlbum = queriedResponse.data.getPhotoAlbum;
if (!photoAlbum?.imageKeys) return;
// Merge existing and new file keys:
const updatedImageKeys = [...newImageKeys, ...photoAlbum.imageKeys];
const photoAlbumDetails = {
id: currentPhotoAlbum.id,
imageKeys: updatedImageKeys,
};
// Update record with merged file associations:
const response = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: photoAlbumDetails },
});
const updatedPhotoAlbum = response.data.updatePhotoAlbum;
setCurrentPhotoAlbum(updatedPhotoAlbum);
// If the record has no associated file, we can return early.
if (!updatedPhotoAlbum?.imageKeys) return;
// Retrieve signed urls for merged image keys:
const signedUrls = await Promise.all(
updatedPhotoAlbum?.imageKeys.map(async (key) => await getUrl({ key: key! }))
);
if (!signedUrls) return;
setCurrentImages(signedUrls.map(signedUrl => signedUrl.url.toString()));
} catch (error) {
console.error(
"Error uploading image / adding image to photoAlbum: ",
error
);
}
}
// Replace last image associated with current photoAlbum:
async function updateLastImage(e: React.ChangeEvent<HTMLInputElement>) {
if (!currentPhotoAlbum) return;
if (!e.target.files) return;
const file = e.target.files[0];
try {
// Upload new file to Storage:
const result = await uploadData({
key: `${currentPhotoAlbum.id}-${file.name}`,
data: file,
options: {
contentType: "image/png", // contentType is optional
}
}).result;
const newFileKey = result.key;
// Query existing record to retrieve currently associated files:
const queriedResponse = await client.graphql({
query: queries.getPhotoAlbum,
variables: { id: currentPhotoAlbum.id },
});
const photoAlbum = queriedResponse.data.getPhotoAlbum;
if (!photoAlbum?.imageKeys?.length) return;
// Retrieve last image key:
const [lastImageKey] = photoAlbum.imageKeys.slice(-1);
// Remove last file association by key
const updatedImageKeys = [
...photoAlbum.imageKeys.filter((key) => key !== lastImageKey),
newFileKey,
];
const photoAlbumDetails = {
id: currentPhotoAlbum.id,
imageKeys: updatedImageKeys,
};
// Update record with updated file associations:
const response = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: photoAlbumDetails },
});
const updatedPhotoAlbum = response.data.updatePhotoAlbum;
setCurrentPhotoAlbum(updatedPhotoAlbum);
// If the record has no associated file, we can return early.
if (!updatedPhotoAlbum?.imageKeys) return;
// Retrieve signed urls for merged image keys:
const signedUrls = await Promise.all(
updatedPhotoAlbum?.imageKeys.map(async (key) => await getUrl({ key: key! }))
);
if (!signedUrls) return;
setCurrentImages(signedUrls.map(signedUrl => signedUrl.url.toString()));
} catch (error) {
console.error(
"Error uploading image / adding image to photoAlbum: ",
error
);
}
}
async function getImagesForPhotoAlbum() {
if (!currentPhotoAlbum) { return }
try {
// Query the record to get the file keys:
const response = await client.graphql({
query: queries.getPhotoAlbum,
variables: { id: currentPhotoAlbum.id },
});
const photoAlbum = response.data.getPhotoAlbum;
// If the record has no associated files, we can return early.
if (!photoAlbum?.imageKeys) return;
// Retrieve the signed URLs for the associated images:
const signedUrls = await Promise.all(
photoAlbum.imageKeys.map(async (imageKey) => {
if (!imageKey) return;
return await getUrl({ key: imageKey });
})
);
setCurrentImages(signedUrls.map(signedUrl => signedUrl?.url.toString()));
} catch (error) {
console.error("Error getting photoAlbum / image:", error);
}
}
// Remove the file associations, continue to persist both files and record
async function removeImagesFromPhotoAlbum() {
if (!currentPhotoAlbum) return;
try {
const response = await client.graphql({
query: queries.getPhotoAlbum,
variables: { id: currentPhotoAlbum.id },
});
const photoAlbum = response.data.getPhotoAlbum;
// If the record has no associated file, we can return early.
if (!photoAlbum?.imageKeys) return;
const photoAlbumDetails = {
id: photoAlbum.id,
imageKeys: null,
};
const updatedPhotoAlbum = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: photoAlbumDetails },
});
// If successful, the response here will be `null`:
setCurrentPhotoAlbum(updatedPhotoAlbum.data.updatePhotoAlbum);
setCurrentImages(updatedPhotoAlbum.data.updatePhotoAlbum?.imageKeys);
} catch (error) {
console.error("Error removing image from photoAlbum: ", error);
}
}
// Remove the record association and delete the file
async function deleteImagesForCurrentPhotoAlbum() {
if (!currentPhotoAlbum) return;
try {
const response = await client.graphql({
query: queries.getPhotoAlbum,
variables: { id: currentPhotoAlbum.id },
});
const photoAlbum = response.data.getPhotoAlbum;
// If the record has no associated files, we can return early.
if (!photoAlbum?.imageKeys) return;
const photoAlbumDetails = {
id: photoAlbum.id,
imageKeys: null, // Set the file association to `null`
};
// Remove associated files from record
const updateResponse = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: photoAlbumDetails },
});
const updatedPhotoAlbum = updateResponse.data.updatePhotoAlbum;
// Delete the files from S3:
await Promise.all(
photoAlbum?.imageKeys.map(async (imageKey) => {
if (!imageKey) return;
await remove({ key: imageKey });
})
);
// If successful, the response here will be `null`:
setCurrentPhotoAlbum(updatedPhotoAlbum);
setCurrentImages(null);
} catch (error) {
console.error("Error deleting image: ", error);
}
}
// Delete both files and record
async function deleteCurrentPhotoAlbumAndImages() {
if (!currentPhotoAlbum) return;
try {
const response = await client.graphql({
query: queries.getPhotoAlbum,
variables: { id: currentPhotoAlbum.id },
});
const photoAlbum = response.data.getPhotoAlbum;
if (!photoAlbum) return;
const photoAlbumDetails = {
id: photoAlbum.id,
};
await client.graphql({
query: mutations.deletePhotoAlbum,
variables: { input: photoAlbumDetails },
});
setCurrentPhotoAlbum(null);
// If the record has no associated file, we can return early.
if (!photoAlbum?.imageKeys) return;
await Promise.all(
photoAlbum?.imageKeys.map(async (imageKey) => {
if (!imageKey) return;
await remove({ key: imageKey });
})
);
clearLocalState();
} catch (error) {
console.error("Error deleting photoAlbum: ", error);
}
}
function clearLocalState() {
setCurrentPhotoAlbum(null);
setCurrentImages([]);
}
return (
<Authenticator>
{({ signOut, user }) => (
<main
style={{
alignItems: "center",
display: "flex",
flexDirection: "column",
}}
>
<h1>Hello {user?.username}!</h1>
<h2>{`Current PhotoAlbum: ${currentPhotoAlbum?.id}`}</h2>
<label>
Create photoAlbum with one file:
<input
type="file"
accept="image/*"
onChange={createPhotoAlbumWithFirstImage}
/>
</label>
<label>
Create photoAlbum with multiple files:
<input
type="file"
accept="image/*"
onChange={createPhotoAlbumWithMultipleImages}
multiple
/>
</label>
<label>
Add multiple images to current photoAlbum:
<input
type="file"
accept="image/*"
onChange={addNewImagesToPhotoAlbum}
disabled={!currentPhotoAlbum}
multiple
/>
</label>
<label>
Replace last image:
<input
type="file"
accept="image/*"
onChange={updateLastImage}
disabled={!currentPhotoAlbum || !currentImages}
/>
</label>
<button
onClick={getImagesForPhotoAlbum}
disabled={!currentPhotoAlbum || !currentImages}
>
Get Images for Current Photo Album
</button>
<button
onClick={removeImagesFromPhotoAlbum}
disabled={!currentPhotoAlbum || !currentImages}
>
Remove images from current PhotoAlbum (does not delete images)
</button>
<button
onClick={deleteImagesForCurrentPhotoAlbum}
disabled={!currentPhotoAlbum || !currentImages}
>
Remove images from current PhotoAlbum, then delete images
</button>
<button
onClick={deleteCurrentPhotoAlbumAndImages}
disabled={!currentPhotoAlbum}
>
Delete current PhotoAlbum (and images, if they exist)
</button>
<button onClick={signOut}>Sign out</button>
{currentImages &&
currentImages.map((url, idx) => {
if (!url) return undefined;
return <img src={url} key={idx} alt="Storage file"></img>;
})}
</main>
)}
</Authenticator>
);
}
export default App;
import { useState } from 'react';
import { Amplify } from 'aws-amplify';
import { generateClient } from 'aws-amplify/api';
import { getUrl, uploadData, remove } from 'aws-amplify/storage';
import { Authenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import config from './amplifyconfiguration.json';
import * as queries from './graphql/queries';
import * as mutations from './graphql/mutations';
Amplify.configure(config, {
Storage: {
S3: {
defaultAccessLevel: 'private'
}
}
});
const client = generateClient();
function App() {
const [currentPhotoAlbum, setCurrentPhotoAlbum] = useState(null);
// Used to display images for current photoAlbum:
const [currentImages, setCurrentImages] = useState([]);
async function createPhotoAlbumWithFirstImage(e) {
if (!e.target.files) return;
const file = e.target.files[0];
try {
const photoAlbumDetails = {
name: `My first photoAlbum`
};
// Create the API record:
const response = await client.graphql({
query: mutations.createPhotoAlbum,
variables: { input: photoAlbumDetails }
});
const photoAlbum = response.data.createPhotoAlbum;
if (!photoAlbum) return;
// Upload the Storage file:
const result = await uploadData({
key: `${photoAlbum.id}-${file.name}`,
data: file,
options: {
contentType: 'image/png' // contentType is optional
}
}).result;
const updatePhotoAlbumDetails = {
id: photoAlbum.id,
imageKeys: [result.key]
};
// Add the file association to the record:
const updateResponse = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: updatePhotoAlbumDetails }
});
const updatedPhotoAlbum = updateResponse.data.updatePhotoAlbum;
setCurrentPhotoAlbum(updatedPhotoAlbum);
// If the record has no associated file, we can return early.
if (!updatedPhotoAlbum.imageKeys?.length) return;
// Retrieve the file's signed URL:
const signedURL = await getUrl({ key: updatedPhotoAlbum.imageKeys[0] });
setCurrentImages([signedURL.url.toString()]);
} catch (error) {
console.error('Error create photoAlbum / file:', error);
}
}
async function createPhotoAlbumWithMultipleImages(e) {
if (!e.target.files) return;
try {
const photoAlbumDetails = {
name: `My first photoAlbum`
};
// Create the API record:
const response = await client.graphql({
query: mutations.createPhotoAlbum,
variables: { input: photoAlbumDetails }
});
const photoAlbum = response.data.createPhotoAlbum;
if (!photoAlbum) return;
// Upload all files to Storage:
const imageKeys = await Promise.all(
Array.from(e.target.files).map(async (file) => {
const result = await uploadData({
key: `${photoAlbum.id}-${file.name}`,
data: file,
options: {
contentType: 'image/png' // contentType is optional
}
}).result;
return result.key;
})
);
const updatePhotoAlbumDetails = {
id: photoAlbum.id,
imageKeys: imageKeys
};
// Add the file association to the record:
const updateResponse = await client.graphql({
query: mutations.updatePhotoAlbum,
variables: { input: updatePhotoAlbumDetails }
});
const updatedPhotoAlbum = updateResponse.data.updatePhotoAlbum;
setCurrentPhotoAlbum(updatedPhotoAlbum);
// If the record has no associated file, we can return early.
if (!updatedPhotoAlbum.imageKeys?.length) return;
// Retrieve signed urls for all files:
const signedUrls = await Promise.all(
updatedPhotoAlbum.imageKeys.map(async (key) => await getUrl({ key }))
);
if (!signedUrls) return;
setCurrentImages(signedUrls.map((signedUrl) => signedUrl.url.toString()));
} catch (error) {
console.error('Error create photoAlbum / file:', error);
}
}
async function addNewImagesToPhotoAlbum(e) {
if (