---
title: "Working with files/attachments"
section: "frontend/data"
platforms: ["android", "angular", "flutter", "javascript", "nextjs", "react", "react-native", "swift", "vue"]
gen: 2
last-updated: "2026-03-25T17:40:00.000Z"
url: "https://docs.amplify.aws/react/frontend/data/working-with-files/"
---

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.

## Set up the project

Set up your project by following the instructions in the [Quickstart guide](/[platform]/start/quickstart/).

## Define the model

Open `amplify/data/resource.ts` and add the following model as shown below:

```ts title="amplify/data/resource.ts"
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";

const schema = a.schema({
  Song: a
    .model({
      id: a.id().required(),
      name: a.string().required(),
      coverArtPath: a.string(),
    })
    .authorization((allow) => [allow.publicApiKey()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "apiKey",

    apiKeyAuthorizationMode: {
      expiresInDays: 30,
    },
  },
});
```
## Setup the Storage

Next, Let's configure Storage and allow access to all authenticated(signed in) users of your application. create a file `amplify/storage/resource.ts` and add the following code,This will restrict file access to only the signed-in user.

```ts title="amplify/storage/resource.ts"

import { defineStorage } from "@aws-amplify/backend";

export const storage = defineStorage({
  name: "amplify-gen2-files",
  access: (allow) => ({
    "images/*": [allow.authenticated.to(["read", "write", "delete"])],
  }),
});
```

Configure the storage in the `amplify/backend.ts` file as demonstrated below:

```ts title="amplify/backend.ts"
import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource";
import { data } from "./data/resource";
import { storage } from "./storage/resource";

export const backend = defineBackend({
  auth,
  data,
  storage,
});
```

## Configuring authorization

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

The Storage and Data 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 `allow.authenticated()` to the Data 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.

When you configure Storage, Amplify 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, all Storage access is still `guest` 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 `authenticated` users.

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 `entity_id` CRUD access on an individual Storage function call for files that should only be accessible by the owner (such as personal files), `authenticated` read access to allow all logged in users to view common files (such as images in a shared photo album), and `guest` 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](/[platform]/build-a-backend/storage/authorization/). For more on configuring Data authorization, see the [API documentation](/[platform]/build-a-backend/data/customize-authz/).

## Create a record with an associated file

You can create a record via the Amplify Data client, upload a file to Storage, and finally update the record to associate it with the uploaded file. Use the following example with the Amplify Data client and Amplify Storage library helpers, `uploadData` and `getUrl`, to create a record and associate it the file with the record.

<Callout>

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 path unintentionally.

</Callout>

<!-- Platform: swift -->
```swift title="ContentView"
let song = Song(name: name)

guard let imageData = artCover.pngData() else {
    print("Could not get data from image.")
    return
}

// Create the song record
var createdSong = try await Amplify.API.mutate(request: .create(song)).get()
let coverArtPath = "images/\(createdSong.id)"

// Upload the art cover image
_ = try await Amplify.Storage.uploadData(path: .fromString(coverArtPath), data: imageData).value

// Update the song record with the image path
createdSong.coverArtPath = coverArtPath
let updatedSong = try await Amplify.API.mutate(request: .update(createdSong)).get()
```
<!-- /Platform -->

<!-- Platform: javascript,  react-native, angular, nextjs, react, vue -->
```ts title="src/App.tsx"

import { generateClient } from "aws-amplify/api";
import { uploadData, getUrl } from "aws-amplify/storage";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

// Create the API record:
const response = await client.models.Song.create({
  name: `My first song`,
});

const song = response.data;

if (!song) return;

// Upload the Storage file:
const result = await uploadData({
  path: `images/${song.id}-${file.name}`,
  data: file,
  options: {
    contentType: "image/png", // contentType is optional
  },
}).result;

// Add the file association to the record:
const updateResponse = await client.models.Song.update({
  id: song.id,
  coverArtPath: result?.path,
});

const updatedSong = updateResponse.data;

setCurrentSong(updatedSong);

// If the record has no associated file, we can return early.
if (!updatedSong.coverArtPath) return;

// Retrieve the file's signed URL:
const signedURL = await getUrl({ path: updatedSong.coverArtPath });
```
<!-- /Platform -->

## Add or update a file for an associated record

To associate a file with a record, update the record with the path returned by the Storage upload. The following example uploads the file using Storage, updates the record with the file's path, 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.

<!-- Platform: swift -->
```swift title="ContentView"
guard var currentSong = currentSong else {
    print("There is no song to associated the image with. Create a Song first.")
    return
}
guard let imageData = artCover.pngData() else {
    print("Could not get data from UIImage.")
    return
}

let coverArtPath = "images/\(currentSong.id)"

// Upload the new art image
_ = try await Amplify.Storage.uploadData(path: .fromString(coverArtPath), data: imageData).value

// Update the song record
currentSong.coverArtPath = coverArtPath
let updatedSong = try await Amplify.API.mutate(request: .update(currentSong)).get()
```
<!-- /Platform -->

<InlineFilter filters={[
  "javascript", "react-native", "angular", "nextjs", "react", "vue"
]}>

```ts title="src/App.tsx"
import { generateClient } from "aws-amplify/api";
import { uploadData, getUrl } from "aws-amplify/storage";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

// Upload the Storage file:
const result = await uploadData({
  path: `images/${currentSong.id}-${file.name}`,
  data: file,
  options: {
    contentType: "image/png", // contentType is optional
  },
}).result;

// Add the file association to the record:
const response = await client.models.Song.update({
  id: currentSong.id,
  coverArtPath: result?.path,
});

const updatedSong = response.data;

setCurrentSong(updatedSong);

// If the record has no associated file, we can return early.
if (!updatedSong?.coverArtPath) return;

// Retrieve the file's signed URL:
const signedURL = await getUrl({ path: updatedSong.coverArtPath });
```

</InlineFilter>

## 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:
<!-- Platform: swift -->
```swift title="ContentView"
// Get the song record
guard let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else {
    print("Song may have been deleted, no song by id: ", currentSong.id)
    return
}

// If the record has no associated file, we can return early.
guard let coverArtPath = song.coverArtPath else {
    print("Song does not contain cover art")
    return
}

// Download the art cover
print("coverArtPath: ", coverArtPath)
let imageData = try await Amplify.Storage.downloadData(path: .fromString(coverArtPath)).value

let image = UIImage(data: imageData)
```
<!-- /Platform -->

<InlineFilter filters={[
  "javascript", "react-native", "angular", "nextjs", "react", "vue"
]}>
```ts title="src/App.tsx"
import { generateClient } from "aws-amplify/api";
import { getUrl } from "aws-amplify/storage";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

const response = await client.models.Song.get({
  id: currentSong.id,
});

const song = response.data;

// If the record has no associated file, we can return early.
if (!song?.coverArtPath) return;

// Retrieve the signed URL:
const signedURL = await getUrl({ path: song.coverArtPath });
```
</InlineFilter>

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

<!-- Platform: swift -->
```swift title="ContentView"
// Get the song record
guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else {
    print("Song may have been deleted, no song by id: ", currentSong.id)
    return
}

guard song.coverArtPath != nil else {
    print("There is no cover art path to remove image association")
    return
}

// Set the association to nil and update it
song.coverArtPath = nil

let updatedSong = try await Amplify.API.mutate(request: .update(song)).get()
```
<!-- /Platform -->

<InlineFilter filters={[
  "javascript", "react-native", "angular", "nextjs", "react", "vue"
]}>
```ts title="src/App.tsx"

import { generateClient } from "aws-amplify/api";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

const response = await client.models.Song.get({
  id: currentSong.id,
});

const song = response.data;

// If the record has no associated file, we can return early.
if (!song?.coverArtPath) return;

const updatedSong = await client.models.Song.update({
  id: song.id,
  coverArtPath: null,
});
```
</InlineFilter>

### Remove the record association and delete the file

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

<!-- Platform: swift -->
```swift title="ContentView"
// Get the song record
guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else {
    print("Song may have been deleted, no song by id: ", currentSong.id)
    return
}

guard let coverArtPath = song.coverArtPath else {
    print("There is no cover art path to remove image association")
    return
}

// Set the association to nil and update it
song.coverArtPath = nil
let updatedSong = try await Amplify.API.mutate(request: .update(song)).get()

// Remove the image
try await Amplify.Storage.remove(path: .fromString(coverArtPath))
```
<!-- /Platform -->

<InlineFilter filters={[
  "javascript", "react-native", "angular", "nextjs", "react", "vue"
]}>

```ts title="src/App.tsx"
import { generateClient } from "aws-amplify/api";
import { remove } from "aws-amplify/storage";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});
const response = await client.models.Song.get({
  id: currentSong.id,
});
const song = response?.data;

// If the record has no associated file, we can return early.
if (!song?.coverArtPath) return;

// Remove associated file from record
const updatedSong = await client.models.Song.update({
  id: song.id,
  coverArtPath: null,
});

// Delete the file from S3:
await remove({ path: song.coverArtPath });

```
</InlineFilter>

### Delete both file and record

<!-- Platform: swift -->
```swift title="ContentView"
// Get the song record
guard let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)).get() else {
    print("Song may have been deleted, no song by id: ", currentSong.id)
    return
}

if let coverArt = song.coverArtPath {
    // Delete the file from S3
    try await Amplify.Storage.remove(path: .fromString(coverArt))
}

// Delete the song record
_ = try await Amplify.API.mutate(request: .delete(song)).get()
```
<!-- /Platform -->

<InlineFilter filters={[
  "javascript", "react-native", "angular", "nextjs", "react", "vue"
]}>
```ts title="src/App.tsx"
import { generateClient } from "aws-amplify/api";
import { remove } from "aws-amplify/storage";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});
const response = await client.models.Song.get({
  id: currentSong.id,
});

const song = response.data;

// If the record has no associated file, we can return early.
if (!song?.coverArtPath) return;

await remove({ path: song.coverArtPath });

// Delete the record from the API:
await client.models.Song.delete({ id: song.id });

```
</InlineFilter>

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

Add the following model in `amplify/data/resource.ts" file.

```ts title="amplify/data/resource.ts"
const schema = a.schema({
  PhotoAlbum: a
    .model({
      id: a.id().required(),
      name: a.string().required(),
      imagePaths: a.string().array(),
    })
    .authorization((allow) => [allow.publicApiKey()]),
});
```

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.

<!-- Platform: swift -->
```swift title="ContentView"
// Create the photo album record
let album = PhotoAlbum(name: name)
var createdAlbum = try await Amplify.API.mutate(request: .create(album)).get()

// Upload the photo album images
let imagePaths = await withTaskGroup(of: String?.self) { group in
    for imageData in imagesData {
        group.addTask {
            let path = "images/\(album.id)-\(UUID().uuidString)"
            do {
                _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value
                return path
            } catch {
                print("Failed with error:", error)
                return nil
            }
        }
    }

    var imagePaths: [String?] = []
    for await imagePath in group {
        imagePaths.append(imagePath)
    }
    return imagePaths.compactMap { $0 }
}

// Update the album with the image paths
createdAlbum.imagePaths = imagePaths
let updatedAlbum = try await Amplify.API.mutate(request: .update(createdAlbum)).get()
```
<!-- /Platform -->

<InlineFilter filters={[
  "javascript", "react-native", "angular", "nextjs", "react", "vue"
]}>
```ts title="src/App.tsx"
import { generateClient } from "aws-amplify/api";
import { uploadData, getUrl } from "aws-amplify/storage";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

// Create the API record:
const response = await client.models.PhotoAlbum.create({
  name: `My first photoAlbum`,
});

const photoAlbum = response.data.createPhotoAlbum;

if (!photoAlbum) return;

// Upload all files to Storage:
const imagePaths = await Promise.all(
  Array.from(e.target.files).map(async (file) => {
    const result = await uploadData({
      path: `images/${photoAlbum.id}-${file.name}`,
      data: file,
      options: {
        contentType: "image/png", // contentType is optional
      },
    }).result;

    return result.path;
  })
);

const updatePhotoAlbumDetails = {
  id: photoAlbum.id,
  imagePaths: imagePaths,
};

// 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?.imagePaths.map(
    async (path) => await getUrl({ path: path! })
  )
);
```
</InlineFilter>

### Add new files to an associated record

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

<!-- Platform: swift -->
```swift title="ContentView"
// Upload the new photo album image
let path = "images/\(currentAlbum.id)-\(UUID().uuidString)"
_ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value

// Get the latest album
guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else {
    print("Album may have been deleted, no album by id: ", currentAlbum.id)
    return
}

guard var imagePaths = album.imagePaths else {
    print("Album does not contain images")
    await setCurrentAlbum(album)
    await setCurrentImages([])
    return
}

// Add new to the existing paths
imagePaths.append(path)

// Update the album with the image paths
album.imagePaths = imagePaths
let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get()
```
<!-- /Platform -->

<InlineFilter filters={[
  "javascript", "react-native", "angular", "nextjs", "react", "vue"
]}>
```ts title="src/App.tsx"

import { generateClient } from "aws-amplify/api";
import { uploadData, getUrl } from "aws-amplify/storage";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

// Upload all files to Storage:
const newimagePaths = await Promise.all(
  Array.from(e.target.files).map(async (file) => {
    const result = await uploadData({
      path: `images/${currentPhotoAlbum.id}-${file.name}`,
      data: file,
      options: {
        contentType: "image/png", // contentType is optional
      },
    }).result;

    return result.path;
  })
);

// Query existing record to retrieve currently associated files:
const queriedResponse = await client.models.PhotoAlbum.get({
  id: currentPhotoAlbum.id,
});

const photoAlbum = queriedResponse.data;

if (!photoAlbum?.imagePaths) return;

// Merge existing and new file paths:
const updatedimagePaths = [...newimagePaths, ...photoAlbum.imagePaths];

// Update record with merged file associations:
const response = await client.models.PhotoAlbum.update({
  id: currentPhotoAlbum.id,
  imagePaths: updatedimagePaths,
});

const updatedPhotoAlbum = response.data;

// If the record has no associated file, we can return early.
if (!updatedPhotoAlbum?.imageKeys) return;

// Retrieve signed urls for merged image paths:
const signedUrls = await Promise.all(
  updatedPhotoAlbum?.imagePaths.map(
    async (path) => await getUrl({ path: path! })
  )
);
```
</InlineFilter>

### 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.
<InlineFilter filters={[
  "swift"
]}>
```swift title="ContentView"
// Upload new file to Storage:
let path = "images/\(currentAlbum.id)-\(UUID().uuidString)"

_ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value

// Update the album with the image keys
var album = currentAlbum

if var imagePaths = album.imagePaths {
    imagePaths.removeLast()
    imagePaths.append(path)
    album.imagePaths = imagePaths
} else {
    album.imagePaths = [path]
}

// Update record with updated file associations:
let updateResult = try await Amplify.API.mutate(request: .update(album)).get()
```
</InlineFilter>
<InlineFilter filters={[
  "javascript", "react-native", "angular", "nextjs", "react", "vue"
]}>
```ts title="src/App.tsx"
import { generateClient } from "aws-amplify/api";
import { uploadData, getUrl } from "aws-amplify/storage";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

// Upload new file to Storage:
const result = await uploadData({
  path: `images/${currentPhotoAlbum.id}-${file.name}`,
  data: file,
  options: {
    contentType: "image/png", // contentType is optional
  },
}).result;

const newFilePath = result.path;

// Query existing record to retrieve currently associated files:
const queriedResponse = await client.models.PhotoAlbum.get({
  id: currentPhotoAlbum.id,
});

const photoAlbum = queriedResponse.data;

if (!photoAlbum?.imagePaths?.length) return;

// Retrieve last image path:
const [lastImagePath] = photoAlbum.imagePaths.slice(-1);

// Remove last file association by path
const updatedimagePaths = [
  ...photoAlbum.imagePaths.filter((path) => path !== lastImagePath),
  newFilePath,
];

// Update record with updated file associations:
const response = await client.models.PhotoAlbum.update({
  id: currentPhotoAlbum.id,
  imagePaths: updatedimagePaths,
});

const updatedPhotoAlbum = response.data;

// If the record has no associated file, we can return early.
if (!updatedPhotoAlbum?.imagePaths) return;

// Retrieve signed urls for merged image paths:
const signedUrls = await Promise.all(
  updatedPhotoAlbum?.imagePaths.map(
    async (path) => await getUrl({ path: path! })
  )
);

```
</InlineFilter>
### 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.

<!-- Platform: swift -->
```swift title="ContentView"
// Query the record to get the file paths:
guard let album = try await Amplify.API.query(
    request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else {
    print("Album may have been deleted, no album by id: ", currentAlbum.id)
    return
}

guard let imagePathsOptional = album.imagePaths else {
    print("Album does not contain images")
    await setCurrentAlbum(album)
    await setCurrentImages([])
    return
}

let imagePaths = imagePathsOptional.compactMap { $0 }

// Download the photos
let images = await withTaskGroup(of: UIImage?.self) { group in
    for path in imagePaths {
        group.addTask {
            do {
                let imageData = try await Amplify.Storage.downloadData(path: .fromString(path)).value
                return UIImage(data: imageData)
            } catch {
                print("Failed with error:", error)
                return nil
            }
        }
    }

    var images: [UIImage?] = []
    for await image in group {
        images.append(image)
    }
    return images.compactMap { $0 }
}
```
<!-- /Platform -->
<InlineFilter filters={[
  "javascript", "react-native", "angular", "nextjs", "react", "vue"
]}>
```ts title="src/App.tsx"
async function getImagesForPhotoAlbum() {
import { generateClient } from "aws-amplify/api";
import { uploadData, getUrl } from "aws-amplify/storage";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

// Query the record to get the file paths:
const response = await client.models.PhotoAlbum.get({
  id: currentPhotoAlbum.id,
});

const photoAlbum = response.data;

// If the record has no associated files, we can return early.
if (!photoAlbum?.imagePaths) return;

// Retrieve the signed URLs for the associated images:
const signedUrls = await Promise.all(
  photoAlbum.imagePaths.map(async (imagePath) => {
    if (!imagePath) return;
    return await getUrl({ path: imagePath });
  })
);
}
```
</InlineFilter>

### 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 file paths and call `Storage.remove()` for each file.

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

<InlineFilter filters={[
  "swift"
]}>
```swift title="ContentView"
// Get the album record
guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else {
    print("Album may have been deleted, no album by id: ", currentAlbum.id)
    return
}

guard let imagePaths = album.imagePaths, !imagePaths.isEmpty else {
    print("There are no images to remove association")
    return
}

// Set the association to nil and update it
album.imagePaths = nil
let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get()
```
</InlineFilter>

<InlineFilter filters={[
  "javascript", "react-native", "angular", "nextjs", "react", "vue"
]}>
```ts title="src/App.tsx"

import { generateClient } from "aws-amplify/api";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

const response = await client.models.PhotoAlbum.get({
  id: currentPhotoAlbum.id,
});

const photoAlbum = response.data;

// If the record has no associated file, we can return early.
if (!photoAlbum?.imagePaths) return;

const updatedPhotoAlbum = await client.models.PhotoAlbum.update({
  id: photoAlbum.id,
  imagePaths: null,
});
```
</InlineFilter>

#### Remove the record association and delete the files

<InlineFilter filters={[
  "swift"
]}>
```swift title="ContentView"
// Get the album record
guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else {
    print("Album may have been deleted, no album by id: ", currentAlbum.id)
    return
}

guard let imagePathsOptional = album.imagePaths else {
    print("Album does not contain images")
    await setCurrentAlbum(album)
    await setCurrentImages([])
    return
}
let imagePaths = imagePathsOptional.compactMap { $0 }

// Set the associations to nil and update it
album.imagePaths = nil
let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get()

// Remove the photos
await withTaskGroup(of: Void.self) { group in
    for path in imagePaths {
        group.addTask {
            do {
                try await Amplify.Storage.remove(path: .fromString(path))
            } catch {
                print("Failed with error:", error)
            }
        }
    }

    for await _ in group {
    }
}
```
</InlineFilter>
<InlineFilter filters={[
  "javascript", "react-native", "angular", "nextjs", "react", "vue"
]}>
```ts title="src/App.tsx"
import { generateClient } from "aws-amplify/api";
import { remove } from "aws-amplify/storage";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

const response = await client.models.PhotoAlbum.get({
  id: currentPhotoAlbum.id,
});

const photoAlbum = response.data;

// If the record has no associated files, we can return early.
if (!photoAlbum?.imagePaths) return;

// Remove associated files from record
const updateResponse = await client.models.PhotoAlbum.update({
  id: photoAlbum.id,
  imagePaths: null, // Set the file association to `null`
});

const updatedPhotoAlbum = updateResponse.data;

// Delete the files from S3:
await Promise.all(
  photoAlbum?.imagePaths.map(async (imagePath) => {
    if (!imagePath) return;
    await remove({ path: imagePath });
  })
);
```
</InlineFilter>

#### Delete record and all associated files

<InlineFilter>
```swift title="ContentView"
// Get the album record
guard let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else {
    print("Album may have been deleted, no album by id: ", currentAlbum.id)
    return
}

guard let imagePathsOptional = album.imagePaths else {
    print("Album does not contain images")

    // Delete the album record
    _ = try await Amplify.API.mutate(request: .delete(album))

    await setCurrentAlbum(nil)
    await setCurrentImages([])
    return
}

let imagePaths = imagePathsOptional.compactMap { $0 }

// Remove the photos
await withTaskGroup(of: Void.self) { group in
    for path in imagePaths {
        group.addTask {
            do {
                try await Amplify.Storage.remove(path: .fromString(path))
            } catch {
                print("Failed with error:", error)
            }
        }
    }

    for await _ in group {
    }
}

// Delete the album record
_ = try await Amplify.API.mutate(request: .delete(album)).get()
```
</InlineFilter>

<InlineFilter filters={[
  "javascript", "react-native", "angular", "nextjs", "react", "vue"
]}>
```ts title="src/App.tsx"

import { generateClient } from "aws-amplify/api";
import { remove } from "aws-amplify/storage";
import type { Schema } from "../amplify/data/resource";

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

const response = await client.models.PhotoAlbum.get({
  id: currentPhotoAlbum.id,
});

const photoAlbum = response.data;

if (!photoAlbum) return;

await client.models.PhotoAlbum.delete({
  id: photoAlbum.id,
});

setCurrentPhotoAlbum(null);

// If the record has no associated file, we can return early.
if (!photoAlbum?.imagePaths) return;

await Promise.all(
  photoAlbum?.imagePaths.map(async (imagePath) => {
    if (!imagePath) return;
    await remove({ path: imagePath });
  })
);

```
</InlineFilter>

## 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](#create-a-record-with-an-associated-file). "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](/[platform]/frontend/data/subscribe-data/).

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
<InlineFilter filters={[
  "swift"
]}>

#### [Main App]
```swift title="AmplifySwiftApp"
import SwiftUI
import Amplify
import AWSAPIPlugin
import AWSCognitoAuthPlugin
import AWSS3StoragePlugin
import Authenticator
import PhotosUI

@main
struct WorkingWithFilesApp: App {

    init() {
        do {
            Amplify.Logging.logLevel = .verbose
            try Amplify.add(plugin: AWSCognitoAuthPlugin())
            try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels()))
            try Amplify.add(plugin: AWSS3StoragePlugin())
            try Amplify.configure(with: .amplifyOutputs)
            print("Amplify configured with Auth, API, and Storage plugins")
        } catch {
            print("Unable to configure Amplify \(error)")
        }
    }

    var body: some Scene {
        WindowGroup {
            Authenticator { state in
                TabView {
                    SongView()
                        .tabItem {
                            Label("Song", systemImage: "music.note")
                        }

                    PhotoAlbumView()
                        .tabItem {
                            Label("PhotoAlbum", systemImage: "photo")
                        }
                }

            }
        }
    }
}

struct SignOutButton: View {
    var body: some View {
        Button("Sign out") {
            Task {
                await Amplify.Auth.signOut()
            }
        }.foregroundColor(.black)
    }
}

struct TappedButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding(10)
            .background(configuration.isPressed ? Color.teal.opacity(0.8) : Color.teal)
            .foregroundColor(.white)
            .clipShape(RoundedRectangle(cornerRadius: 10))
    }
}

extension Color {
    static let teal = Color(red: 45/255, green: 111/255, blue: 138/255)
}

struct DimmedBackgroundView: View {
    var body: some View {
        Color.gray.opacity(0.5)
            .ignoresSafeArea()
    }
}

struct ImagePicker: UIViewControllerRepresentable {
    @Binding var selectedImage: UIImage?
    @Environment(\.presentationMode) var presentationMode

    class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
        let parent: ImagePicker

        init(_ parent: ImagePicker) {
            self.parent = parent
        }

        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
            if let uiImage = info[.originalImage] as? UIImage {
                parent.selectedImage = uiImage
            }
            parent.presentationMode.wrappedValue.dismiss()
        }

        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            parent.presentationMode.wrappedValue.dismiss()
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        let imagePicker = UIImagePickerController()
        imagePicker.delegate = context.coordinator
        return imagePicker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
    }
}

struct MultiImagePicker: UIViewControllerRepresentable {
    @Binding var selectedImages: [UIImage]

    func makeUIViewController(context: Context) -> PHPickerViewController {
        var configuration = PHPickerConfiguration()
        configuration.filter = .images
        configuration.selectionLimit = 0

        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
        // No need for updates in this case
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    class Coordinator: PHPickerViewControllerDelegate {
        private let parent: MultiImagePicker

        init(parent: MultiImagePicker) {
            self.parent = parent
        }

        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            picker.dismiss(animated: true, completion: nil)
            DispatchQueue.main.async {
                self.parent.selectedImages = []
            }
            for result in results {
                if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
                    result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
                        if let image = image as? UIImage {
                            DispatchQueue.main.async {
                                self.parent.selectedImages.append(image)
                            }
                        }
                    }
                }
            }
        }
    }
}
```

#### [Song]
```swift title="SongView"
import SwiftUI
import Amplify

class SongViewModel: ObservableObject {

    @Published var currentSong: Song? = nil
    @Published var currentImage: UIImage? = nil
    @Published var isLoading: Bool = false

    // Create a song with an associated image
    func createSong(name: String, artCover: UIImage) async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }
                let song = Song(name: name)

        guard let imageData = artCover.pngData() else {
            print("Could not get data from image.")
            return
        }

        // Create the song record
        var createdSong = try await Amplify.API.mutate(request: .create(song)).get()
        let coverArtPath = "images/\(createdSong.id)"

        // Upload the art cover image
        _ = try await Amplify.Storage.uploadData(path: .fromString(coverArtPath), data: imageData).value

        // Update the song record with the image path
        createdSong.coverArtPath = coverArtPath
        let updatedSong = try await Amplify.API.mutate(request: .update(createdSong)).get()

        await setCurrentSong(updatedSong)
    }

    func getSongAndFile(currentSong: Song, imageData: Data) async throws {
        // Get the song record
        guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else {
            print("Song may have been deleted, no song by id: ", currentSong.id)
            return
        }

        guard let coverArtPath = song.coverArtPath else {
            print("There is no cover art path to retrieve image")
            return
        }

        // Download the art cover
        let imageData = try await Amplify.Storage.downloadData(path: .fromString(coverArtPath)).value

        let image = UIImage(data: imageData)
    }

    // Add or update an image for an associated record
    func updateArtCover(artCover: UIImage) async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }

        guard var currentSong = currentSong else {
            print("There is no song to associated the image with. Create a Song first.")
            return
        }

        guard let imageData = artCover.pngData() else {
            print("Could not get data from UIImage.")
            return
        }

        let coverArtPath = "images/\(currentSong.id)"

        // Upload the new art image
        _ = try await Amplify.Storage.uploadData(path: .fromString(coverArtPath), data: imageData).value

        // Update the song record
        currentSong.coverArtPath = coverArtPath
        let updatedSong = try await Amplify.API.mutate(request: .update(currentSong)).get()

        await setCurrentSong(updatedSong)
    }

    func refreshSongAndArtCover() async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }
        guard let currentSong = currentSong else {
            print("There is no song to refresh the record and image. Create a song first.")
            return
        }
        await setCurrentSong(nil)
        await setCurrentImage(nil)

        // Get the song record
        guard let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else {
            print("Song may have been deleted, no song by id: ", currentSong.id)
            return
        }

        guard let coverArtPath = song.coverArtPath else {
            print("Song does not contain cover art")
            await setCurrentSong(song)
            await setCurrentImage(nil)
            return
        }

        // Download the art cover
        let imageData = try await Amplify.Storage.downloadData(path: .fromString(coverArtPath)).value

        let image = UIImage(data: imageData)

        await setCurrentSong(song)
        await setCurrentImage(image)
    }

    func removeImageAssociationFromSong() async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }
        guard let currentSong = currentSong else {
            print("There is no song to remove art cover from it. Create a song first.")
            return
        }

        // Get the song record
        guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else {
            print("Song may have been deleted, no song by id: ", currentSong.id)
            return
        }

        guard song.coverArtPath != nil else {
            print("There is no cover art path to remove image association")
            return
        }

        // Set the association to nil and update it
        song.coverArtPath = nil

        let updatedSong = try await Amplify.API.mutate(request: .update(song)).get()

        await setCurrentSong(updatedSong)
    }

    func removeImageAssociationAndDeleteImage() async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }
        guard let currentSong = currentSong else {
            print("There is no song to remove art cover from it. Create a song first.")
            return
        }

        // Get the song record
        guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else {
            print("Song may have been deleted, no song by id: ", currentSong.id)
            return
        }

        guard let coverArtPath = song.coverArtPath else {
            print("There is no cover art path to remove image association")
            return
        }

        // Set the association to nil and update it
        song.coverArtPath = nil
        let updatedSong = try await Amplify.API.mutate(request: .update(song)).get()

        // Remove the image
        try await Amplify.Storage.remove(path: .fromString(coverArtPath))

        await setCurrentSong(updatedSong)
        await setCurrentImage(nil)
    }

    func deleteSongAndArtCover() async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }
        guard let currentSong = currentSong else {
            print("There is no song to delete. Create a song first.")
            return
        }

        // Get the song record
        guard var song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)).get() else {
            print("Song may have been deleted, no song by id: ", currentSong.id)
            return
        }

        if let coverArt = song.coverArtPath {
            // Remove the image
            try await Amplify.Storage.remove(path: .fromString(coverArt))
        }

        // Delete the song record
        _ = try await Amplify.API.mutate(request: .delete(song)).get()

        await setCurrentSong(nil)
        await setCurrentImage(nil)
    }

    @MainActor
    func setCurrentSong(_ song: Song?) {
        self.currentSong = song
    }

    @MainActor
    func setCurrentImage(_ image: UIImage?) {
        self.currentImage = image
    }

    @MainActor
    func setIsLoading(_ isLoading: Bool) {
        self.isLoading = isLoading
    }
}

struct SongView: View {

    @State private var isImagePickerPresented = false
    @State private var songName: String = ""

    @StateObject var viewModel = SongViewModel()

    var body: some View {
        NavigationView {
            ZStack {
                VStack {
                    SongInformation()
                    DisplayImage()
                    OpenImagePickerButton()
                    SongNameTextField()
                    CreateOrUpdateSongButton()
                    AdditionalOperations()
                    Spacer()
                }
                .padding()
                .sheet(isPresented: $isImagePickerPresented) {
                    ImagePicker(selectedImage: $viewModel.currentImage)
                }
                VStack {
                    IsLoadingView()
                }
            }
            .navigationBarItems(trailing: SignOutButton())
        }
    }

    @ViewBuilder
    func SongInformation() -> some View {
        if let song = viewModel.currentSong {
            Text("Song Id: \(song.id)").font(.caption)
            if song.name != "" {
                Text("Song Name: \(song.name)").font(.caption)
            }
        }
    }

    @ViewBuilder
    func DisplayImage() -> some View {
        if let image = viewModel.currentImage {
            Image(uiImage: image)
                .resizable()
                .aspectRatio(contentMode: .fit)
        } else {
            Text("No Image Selected")
                .foregroundColor(.gray)
        }

    }

    func OpenImagePickerButton() -> some View {
        Button("Select \(viewModel.currentImage != nil ? "a new ": "" )song album cover") {
            isImagePickerPresented.toggle()
        }.buttonStyle(TappedButtonStyle())
    }

    @ViewBuilder
    func SongNameTextField() -> some View {
        TextField("\(viewModel.currentSong != nil ? "Update": "Enter") song name", text: $songName)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .multilineTextAlignment(.center)
    }

    @ViewBuilder
    func CreateOrUpdateSongButton() -> some View {
        if viewModel.currentSong == nil, let image = viewModel.currentImage {
            Button("Save") {
                Task {
                    try? await viewModel.createSong(name: songName,
                                                    artCover: image)
                }
            }
            .buttonStyle(TappedButtonStyle())
            .disabled(viewModel.isLoading)
        } else if viewModel.currentSong != nil, let image = viewModel.currentImage {
            Button("Update") {
                Task {
                    try? await viewModel.updateArtCover(artCover: image)
                }
            }
            .buttonStyle(TappedButtonStyle())
            .disabled(viewModel.isLoading)
        }
    }

    @ViewBuilder
    func AdditionalOperations() -> some View {
        if viewModel.currentSong != nil {
            VStack {
                Button("Refresh") {
                    Task {
                        try? await viewModel.refreshSongAndArtCover()
                    }
                }.buttonStyle(TappedButtonStyle())
                Button("Remove association from song") {
                    Task {
                        try? await viewModel.removeImageAssociationFromSong()
                    }
                }.buttonStyle(TappedButtonStyle())
                Button("Remove association and delete image") {
                    Task {
                        try? await viewModel.removeImageAssociationAndDeleteImage()
                    }
                }.buttonStyle(TappedButtonStyle())
                Button("Delete song and art cover") {
                    Task {
                        try? await viewModel.deleteSongAndArtCover()
                    }
                    songName = ""
                }.buttonStyle(TappedButtonStyle())
            }.disabled(viewModel.isLoading)
        }
    }

    @ViewBuilder
    func IsLoadingView() -> some View {
        if viewModel.isLoading {
            ZStack {
                DimmedBackgroundView()
                ProgressView()
            }
        }
    }
}

struct SongView_Previews: PreviewProvider {
    static var previews: some View {
        SongView()
    }
}
```

#### [Photo Album]
```swift title="PhotoAlbumView"
import SwiftUI
import Amplify
import Photos

class PhotoAlbumViewModel: ObservableObject {
    @Published var currentImages: [UIImage] = []
    @Published var currentAlbum: PhotoAlbum? = nil
    @Published var isLoading: Bool = false

    // Create a record with multiple associated files
    func createPhotoAlbum(name: String, photos: [UIImage]) async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }

        let imagesData = photos.compactMap { $0.pngData() }
        guard !imagesData.isEmpty else {
            print("Could not get data from [UIImage]")
            return
        }

        // Create the photo album record
        let album = PhotoAlbum(name: name)
        var createdAlbum = try await Amplify.API.mutate(request: .create(album)).get()

        // Upload the photo album images
        let imagePaths = await withTaskGroup(of: String?.self) { group in
            for imageData in imagesData {
                group.addTask {
                    let path = "images/\(album.id)-\(UUID().uuidString)"
                    do {
                        _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value
                        return path
                    } catch {
                        print("Failed with error:", error)
                        return nil
                    }
                }
            }

            var imagePaths: [String?] = []
            for await imagePath in group {
                imagePaths.append(imagePath)
            }
            return imagePaths.compactMap { $0 }
        }

        // Update the album with the image paths
        createdAlbum.imagePaths = imagePaths
        let updatedAlbum = try await Amplify.API.mutate(request: .update(createdAlbum)).get()

        await setCurrentAlbum(updatedAlbum)
    }

    // Create a record with a single associated file
    func createPhotoAlbum(name: String, photo: UIImage) async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }

        guard let imageData = photo.pngData() else {
            print("Could not get data from UIImage")
            return
        }

        // Create the photo album record
        let album = PhotoAlbum(name: name)
        var createdAlbum = try await Amplify.API.mutate(request: .create(album)).get()

        // Upload the photo album image
        let path = "images/\(album.id)-\(UUID().uuidString)"
        _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value

        // Update the album with the image path
        createdAlbum.imagePaths = [path]
        let updatedAlbum = try await Amplify.API.mutate(request: .update(createdAlbum)).get()

        await setCurrentAlbum(updatedAlbum)
    }

    // Add new file to an associated record
    func addAdditionalPhotos(_ photo: UIImage) async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }

        guard let currentAlbum = currentAlbum else {
            print("There is no album to associated the images with. Create an Album first.")
            return
        }

        guard let imageData = photo.pngData() else {
            print("Could not get data from UIImage.")
            return
        }

        // Upload the new photo album image
        let path = "images/\(currentAlbum.id)-\(UUID().uuidString)"
        _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value

        // Get the latest album
        guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else {
            print("Album may have been deleted, no album by id: ", currentAlbum.id)
            return
        }

        guard var imagePaths = album.imagePaths else {
            print("Album does not contain images")
            await setCurrentAlbum(album)
            await setCurrentImages([])
            return
        }

        // Add new to the existing paths
        imagePaths.append(path)

        // Update the album with the image paths
        album.imagePaths = imagePaths
        let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get()

        await setCurrentAlbum(updatedAlbum)
    }

    func replaceLastImage(_ photo: UIImage) async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }

        guard let currentAlbum = currentAlbum else {
            print("There is no album to associated the images with. Create an Album first.")
            return
        }

        guard let imageData = photo.pngData() else {
            print("Could not get data from UIImage")
            return
        }

        // Upload the new photo album image
        let path = "images/\(currentAlbum.id)-\(UUID().uuidString)"
        _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value

        // Update the album with the image paths
        var album = currentAlbum
        if var imagePaths = album.imagePaths {
            imagePaths.removeLast()
            imagePaths.append(path)
            album.imagePaths = imagePaths
        } else {
            album.imagePaths = [path]
        }

        let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get()

        await setCurrentAlbum(updatedAlbum)
    }

    // Query a record and retrieve the associated files
    func refreshAlbumAndPhotos() async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }
        guard let currentAlbum = currentAlbum else {
            print("There is no album to associate the images with. Create an Album first.")
            return
        }

        await setCurrentAlbum(nil)
        await setCurrentImages([])

        // Get the song record
        guard let album = try await Amplify.API.query(
            request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else {
            print("Album may have been deleted, no album by id: ", currentAlbum.id)
            return
        }

        guard let imagePathsOptional = album.imagePaths else {
            print("Album does not contain images")
            await setCurrentAlbum(album)
            await setCurrentImages([])
            return
        }

        let imagePaths = imagePathsOptional.compactMap { $0 }

        // Download the photos
        let images = await withTaskGroup(of: UIImage?.self) { group in
            for path in imagePaths {
                group.addTask {
                    do {
                        let imageData = try await Amplify.Storage.downloadData(path: .fromString(path)).value
                        return UIImage(data: imageData)
                    } catch {
                        print("Failed with error:", error)
                        return nil
                    }
                }
            }

            var images: [UIImage?] = []
            for await image in group {
                images.append(image)
            }
            return images.compactMap { $0 }
        }

        await setCurrentAlbum(album)
        await setCurrentImages(images)
    }

    // Remove the file association
    func removeStorageAssociationsFromAlbum() async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }
        guard let currentAlbum = currentAlbum else {
            print("There is no album to associated the images with. Create an Album first.")
            return
        }

        // Get the album record
        guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else {
            print("Album may have been deleted, no album by id: ", currentAlbum.id)
            return
        }

        guard let imagePaths = album.imagePaths, !imagePaths.isEmpty else {
            print("There are no images to remove association")
            return
        }

        // Set the association to nil and update it
        album.imagePaths = nil
        let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get()

        await setCurrentAlbum(updatedAlbum)
    }

    // Remove the record association and delete the files
    func removeStorageAssociationsAndDeletePhotos() async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }

        guard let currentAlbum = currentAlbum else {
            print("There is no album to associated the images with. Create an Album first.")
            return
        }

        // Get the album record
        guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else {
            print("Album may have been deleted, no album by id: ", currentAlbum.id)
            return
        }

        guard let imagePathsOptional = album.imagePaths else {
            print("Album does not contain images")
            await setCurrentAlbum(album)
            await setCurrentImages([])
            return
        }
        let imagePaths = imagePathsOptional.compactMap { $0 }

        // Set the associations to nil and update it
        album.imagePaths = nil
        let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get()

        // Remove the photos
        await withTaskGroup(of: Void.self) { group in
            for path in imagePaths {
                group.addTask {
                    do {
                        try await Amplify.Storage.remove(path: .fromString(path))
                    } catch {
                        print("Failed with error:", error)
                    }
                }
            }

            for await _ in group {
            }
        }

        await setCurrentAlbum(updatedAlbum)
        await setCurrentImages([])
    }

    // Delete record and all associated files
    func deleteAlbumAndPhotos() async throws {
        await setIsLoading(true)
        defer {
            Task {
                await setIsLoading(false)
            }
        }

        guard let currentAlbum = currentAlbum else {
            print("There is no album to associated the images with. Create an Album first.")
            return
        }

        // Get the album record
        guard let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else {
            print("Album may have been deleted, no album by id: ", currentAlbum.id)
            return
        }

        guard let imagePathsOptional = album.imagePaths else {
            print("Album does not contain images")

            // Delete the album record
            _ = try await Amplify.API.mutate(request: .delete(album))

            await setCurrentAlbum(nil)
            await setCurrentImages([])
            return
        }

        let imagePaths = imagePathsOptional.compactMap { $0 }

        // Remove the photos
        await withTaskGroup(of: Void.self) { group in
            for path in imagePaths {
                group.addTask {
                    do {
                        try await Amplify.Storage.remove(path: .fromString(path))
                    } catch {
                        print("Failed with error:", error)
                    }
                }
            }

            for await _ in group {
            }
        }

        // Delete the album record
        _ = try await Amplify.API.mutate(request: .delete(album)).get()

        await setCurrentAlbum(nil)
        await setCurrentImages([])
    }

    @MainActor
    func setCurrentAlbum(_ album: PhotoAlbum?) {
        self.currentAlbum = album
    }

    @MainActor
    func setCurrentImages(_ images: [UIImage]) {
        self.currentImages = images
    }

    @MainActor
    func setIsLoading(_ isLoading: Bool) {
        self.isLoading = isLoading
    }
}

struct PhotoAlbumView: View {
    @State private var isImagePickerPresented: Bool = false
    @State private var albumName: String = ""
    @State private var isLastImagePickerPresented = false
    @State private var lastImage: UIImage? = nil
    @StateObject var viewModel = PhotoAlbumViewModel()

    var body: some View {
        NavigationView {
            ZStack {
                VStack {
                    AlbumInformation()
                    DisplayImages()
                    OpenImagePickerButton()
                    PhotoAlbumNameTextField()
                    CreateOrUpdateAlbumButton()
                    AdditionalOperations()
                }
                .padding()
                .sheet(isPresented: $isImagePickerPresented) {
                    MultiImagePicker(selectedImages: $viewModel.currentImages)
                }
                .sheet(isPresented: $isLastImagePickerPresented) {
                    ImagePicker(selectedImage: $lastImage)
                }
                VStack {
                    IsLoadingView()
                }
            }
            .navigationBarItems(trailing: SignOutButton())
        }
    }

    @ViewBuilder
    func AlbumInformation() -> some View {
        if let album = viewModel.currentAlbum {
            Text("Album Id: \(album.id)").font(.caption)
            if album.name != "" {
                Text("Album Name: \(album.name)").font(.caption)
            }
        }
    }

    @ViewBuilder
    func DisplayImages() -> some View {
        // Display selected images
        ScrollView(.horizontal) {
            HStack {
                ForEach($viewModel.currentImages, id: \.self) { image in
                    Image(uiImage: image.wrappedValue)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 100, height: 100)
                }
            }
        }
        if $viewModel.currentImages.isEmpty {
            Text("No Images Selected")
                .foregroundColor(.gray)
        }
    }

    func OpenImagePickerButton() -> some View {
        // Button to open the image picker
        Button("Select \(!viewModel.currentImages.isEmpty ? "new " : "")photo album images") {
            isImagePickerPresented.toggle()
        }.buttonStyle(TappedButtonStyle())
    }

    @ViewBuilder
    func PhotoAlbumNameTextField() -> some View {
        TextField("\(viewModel.currentAlbum != nil ? "Update": "Enter") album name", text: $albumName)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .multilineTextAlignment(.center)
    }

    @ViewBuilder
    func CreateOrUpdateAlbumButton() -> some View {
        if viewModel.currentAlbum == nil, !viewModel.currentImages.isEmpty {
            Button("Save") {
                Task {
                    try? await viewModel.createPhotoAlbum(name: albumName,
                                                          photos: viewModel.currentImages)
                }
            }
            .buttonStyle(TappedButtonStyle())
            .disabled(viewModel.isLoading)
        } else if viewModel.currentAlbum != nil {
            Button("Select \(lastImage != nil ? "another ": "")photo to replace last photo in the album") {
                isLastImagePickerPresented.toggle()
            }
            .buttonStyle(TappedButtonStyle())
            .disabled(viewModel.isLoading)

            if let lastImage = lastImage {
                Image(uiImage: lastImage)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                Button("Replace last image in album with above") {
                    Task {
                        try? await viewModel.replaceLastImage(lastImage)
                        self.lastImage = nil
                        try? await viewModel.refreshAlbumAndPhotos()
                    }
                }
                .buttonStyle(TappedButtonStyle())
                .disabled(viewModel.isLoading)
                Button("Append above image to album") {
                    Task {
                        try? await viewModel.addAdditionalPhotos(lastImage)
                        self.lastImage = nil
                        try? await viewModel.refreshAlbumAndPhotos()
                    }
                }
                .buttonStyle(TappedButtonStyle())
                .disabled(viewModel.isLoading)
            }
        }
    }

    @ViewBuilder
    func AdditionalOperations() -> some View {
        if viewModel.currentAlbum != nil {
            VStack {
                Button("Refresh") {
                    Task {
                        try? await viewModel.refreshAlbumAndPhotos()
                    }
                }.buttonStyle(TappedButtonStyle())
                Button("Remove associations from album") {
                    Task {
                        try? await viewModel.removeStorageAssociationsFromAlbum()
                        try? await viewModel.refreshAlbumAndPhotos()
                    }
                }.buttonStyle(TappedButtonStyle())
                Button("Remove association and delete photos") {
                    Task {
                        try? await viewModel.removeStorageAssociationsAndDeletePhotos()
                        try? await viewModel.refreshAlbumAndPhotos()
                    }
                }.buttonStyle(TappedButtonStyle())
                Button("Delete album and images") {
                    Task {
                        try? await viewModel.deleteAlbumAndPhotos()
                    }
                    albumName = ""
                }.buttonStyle(TappedButtonStyle())
            }.disabled(viewModel.isLoading)
        }
    }

    @ViewBuilder
    func IsLoadingView() -> some View {
        if viewModel.isLoading {
            ZStack {
                DimmedBackgroundView()
                ProgressView()
            }
        }
    }
}

struct PhotoAlbumView_Previews: PreviewProvider {
    static var previews: some View {
        PhotoAlbumView()
    }
}
```

</InlineFilter>

<InlineFilter filters={[
  "javascript", "react-native", "nextjs", "react"
]}>

#### [Single File (TS)]

```ts title="src/App.tsx"

import "./App.css";
import { generateClient } from "aws-amplify/api";
import { uploadData, getUrl, remove } from "aws-amplify/storage";
import React, { useState } from "react";
import type { Schema } from "../amplify/data/resource";
import "@aws-amplify/ui-react/styles.css";
import {
  type WithAuthenticatorProps,
  withAuthenticator,
} from "@aws-amplify/ui-react";
import { Amplify } from "aws-amplify";
import outputs from "../amplify_outputs.json";

Amplify.configure(outputs);

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

type Song = Schema["Song"]["type"];

function App({ signOut, user }: WithAuthenticatorProps) {

  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 {

      // Create the API record:
      const response = await client.models.Song.create({
        name: `My first song`,
      });

      const song = response.data;

      if (!song) return;

      // Upload the Storage file:
      const result = await uploadData({
        path: `images/${song.id}-${file.name}`,
        data: file,
        options: {
          contentType: "image/png", // contentType is optional
        },
      }).result;

      // Add the file association to the record:
      const updateResponse = await client.models.Song.update({
        id: song.id,
        coverArtPath: result?.path,
      });

      const updatedSong = updateResponse.data;
      setCurrentSong(updatedSong);

      // If the record has no associated file, we can return early.
      if (!updatedSong?.coverArtPath) return;

      // Retrieve the file's signed URL:
      const signedURL = await getUrl({ path: updatedSong.coverArtPath });

      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({
        path: `images/${currentSong.id}-${file.name}`,
        data: file,
        options: {
          contentType: "image/png", // contentType is optional
        },
      }).result;

      // Add the file association to the record:
      const response = await client.models.Song.update({
        id: currentSong.id,
        coverArtPath: result?.path,
      });

      const updatedSong = response.data;

      setCurrentSong(updatedSong);

      // If the record has no associated file, we can return early.
      if (!updatedSong?.coverArtPath) return;

      // Retrieve the file's signed URL:
      const signedURL = await getUrl({ path: updatedSong.coverArtPath });
      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 path:
      const response = await client.models.Song.get({
        id: currentSong.id,
      });

      const song = response.data;

      // If the record has no associated file, we can return early.
      if (!song?.coverArtPath) return;

      // Retrieve the signed URL:
      const signedURL = await getUrl({ path: song.coverArtPath });
      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.models.Song.get({
        id: currentSong.id,
      });

      const song = response.data;

      // If the record has no associated file, we can return early.
      if (!song?.coverArtPath) return;

      const updatedSong = await client.models.Song.update({
        id: song.id,
        coverArtPath: null,
      });

      // If successful, the response here will be `null`:
      setCurrentSong(updatedSong.data);

      setCurrentImageUrl(updatedSong.data?.coverArtPath);

    } 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.models.Song.get({
        id: currentSong.id,
      });

      const song = response?.data;

      // If the record has no associated file, we can return early.
      if (!song?.coverArtPath) return;

      // Remove associated file from record
      const updatedSong = await client.models.Song.update({
        id: song.id,
        coverArtPath: null,
      });

      // Delete the file from S3:
      await remove({ path: song.coverArtPath });

      // If successful, the response here will be `null`:
      setCurrentSong(updatedSong.data);

      setCurrentImageUrl(updatedSong.data?.coverArtPath);

    } catch (error) {
      console.error("Error deleting image: ", error);
    }
  }

  // Delete both file and record
  async function deleteCurrentSongAndImage() {

    if (!currentSong) return;
    try {
      const response = await client.models.Song.get({
        id: currentSong.id,
      });
      const song = response.data;

      // If the record has no associated file, we can return early.
      if (!song?.coverArtPath) return;

      await remove({ path: song.coverArtPath });

      // Delete the record from the API:
      await client.models.Song.delete({ id: song.id });

      clearLocalState();

    } catch (error) {
      console.error("Error deleting song: ", error);
    }
  }

  function clearLocalState() {
    setCurrentSong(null);
    setCurrentImageUrl("");
  }

  return (
    <>
      <h1>Hello {user?.username}</h1>
      <button onClick={signOut}>Sign out</button>
      <div>
        <label>
          <h2>{`Current Song: ${currentSong?.id}`}</h2>
          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} className="app-button">
          Sign out
        </button>
      </div>
    </>
  );
}

export default withAuthenticator(App);

```

#### [Multi-File (TS)]

```ts title="src/App.tsx"

import "./App.css";
import { generateClient } from "aws-amplify/api";
import { uploadData, getUrl, remove } from "aws-amplify/storage";
import React, { useState } from "react";
import type { Schema } from "../amplify/data/resource";
import "@aws-amplify/ui-react/styles.css";
import {
  type WithAuthenticatorProps,
  withAuthenticator,
} from "@aws-amplify/ui-react";
import { Amplify } from "aws-amplify";
import outputs from "../amplify_outputs.json";

Amplify.configure(outputs);

// Generating the client
const client = generateClient<Schema>({
  authMode: "apiKey",
});

type PhotoAlbum = Schema["PhotoAlbum"]["type"];

function App({ signOut, user }: WithAuthenticatorProps) {
  // State to hold the recognized text
  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 {
      // Create the API record:
      const response = await client.models.PhotoAlbum.create({
        name: `My first photoAlbum`,
      });

      const photoAlbum = response.data;

      if (!photoAlbum) return;

      // Upload the Storage file:
      const result = await uploadData({
        path: `images/${photoAlbum.id}-${file.name}`,
        data: file,
        options: {
          contentType: "image/png", // contentType is optional
        },
      }).result;

      const updatePhotoAlbumDetails = {
        id: photoAlbum.id,
        imagePaths: [result.path],
      };

      // Add the file association to the record:
      const updateResponse = await client.models.PhotoAlbum.update({
        id: photoAlbum.id,
        imagePaths: [result.path],
      });

      const updatedPhotoAlbum = updateResponse.data;

      setCurrentPhotoAlbum(updatedPhotoAlbum);

      // If the record has no associated file, we can return early.
      if (!updatedPhotoAlbum?.imagePaths?.length) return;

      // Retrieve the file's signed URL:
      const signedURL = await getUrl({
        path: updatedPhotoAlbum.imagePaths[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.models.PhotoAlbum.create({
        name: `My first photoAlbum`,
      });

      const photoAlbum = response.data;

      if (!photoAlbum) return;

      // Upload all files to Storage:
      const imagePaths = await Promise.all(
        Array.from(e.target.files).map(async (file) => {
          const result = await uploadData({
            path: `images/${photoAlbum.id}-${file.name}`,
            data: file,
            options: {
              contentType: "image/png", // contentType is optional
            },
          }).result;

          return result.path;
        })
      );

      // Add the file association to the record:
      const updateResponse = await client.models.PhotoAlbum.update({
        id: photoAlbum.id,
        imagePaths: imagePaths,
      });
      const updatedPhotoAlbum = updateResponse.data;

      setCurrentPhotoAlbum(updatedPhotoAlbum);

      // If the record has no associated file, we can return early.
      if (!updatedPhotoAlbum?.imagePaths?.length) return;

      // Retrieve signed urls for all files:
      const signedUrls = await Promise.all(
        updatedPhotoAlbum.imagePaths.map(
          async (path) => await getUrl({ path: path! })
        )
      );

      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 newimagePaths = await Promise.all(
        Array.from(e.target.files).map(async (file) => {
          const result = await uploadData({
            path: `images/${currentPhotoAlbum.id}-${file.name}`,
            data: file,
            options: {
              contentType: "image/png", // contentType is optional
            },
          }).result;

          return result.path;
        })
      );

      // Query existing record to retrieve currently associated files:
      const queriedResponse = await client.models.PhotoAlbum.get({
        id: currentPhotoAlbum.id,
      });

      const photoAlbum = queriedResponse.data;

      if (!photoAlbum?.imagePaths) return;

      // Merge existing and new file paths:
      const updatedimagePaths = [...newimagePaths, ...photoAlbum.imagePaths];

      // Update record with merged file associations:
      const response = await client.models.PhotoAlbum.update({
        id: currentPhotoAlbum.id,
        imagePaths: updatedimagePaths,
      });

      const updatedPhotoAlbum = response.data;
      setCurrentPhotoAlbum(updatedPhotoAlbum);

      // If the record has no associated file, we can return early.
      if (!updatedPhotoAlbum?.imagePaths) return;

      // Retrieve signed urls for merged image paths:
      const signedUrls = await Promise.all(
        updatedPhotoAlbum?.imagePaths.map(
          async (path) => await getUrl({ path: path! })
        )
      );

      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({
        path: `images/${currentPhotoAlbum.id}-${file.name}`,
        data: file,
        options: {
          contentType: "image/png", // contentType is optional
        },
      }).result;

      const newFilePath = result.path;

      // Query existing record to retrieve currently associated files:
      const queriedResponse = await client.models.PhotoAlbum.get({
        id: currentPhotoAlbum.id,
      });

      const photoAlbum = queriedResponse.data;

      if (!photoAlbum?.imagePaths?.length) return;

      // Retrieve last image path:
      const [lastImagePath] = photoAlbum.imagePaths.slice(-1);

      // Remove last file association by path
      const updatedimagePaths = [
        ...photoAlbum.imagePaths.filter((path) => path !== lastImagePath),
        newFilePath,
      ];

      // Update record with updated file associations:
      const response = await client.models.PhotoAlbum.update({
        id: currentPhotoAlbum.id,
        imagePaths: updatedimagePaths,
      });

      const updatedPhotoAlbum = response.data;

      setCurrentPhotoAlbum(updatedPhotoAlbum);

      // If the record has no associated file, we can return early.
      if (!updatedPhotoAlbum?.imagePaths) return;

      // Retrieve signed urls for merged image paths:
      const signedUrls = await Promise.all(
        updatedPhotoAlbum?.imagePaths.map(
          async (path) => await getUrl({ path: path! })
        )
      );

      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 paths:
      const response = await client.models.PhotoAlbum.get({
        id: currentPhotoAlbum.id,
      });
      const photoAlbum = response.data;

      // If the record has no associated files, we can return early.
      if (!photoAlbum?.imagePaths) return;

      // Retrieve the signed URLs for the associated images:
      const signedUrls = await Promise.all(
        photoAlbum.imagePaths.map(async (imagePath) => {
          if (!imagePath) return;
          return await getUrl({ path: imagePath });
        })
      );

      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.models.PhotoAlbum.get({
        id: currentPhotoAlbum.id,
      });

      const photoAlbum = response.data;

      // If the record has no associated file, we can return early.
      if (!photoAlbum?.imagePaths) return;

      const updatedPhotoAlbum = await client.models.PhotoAlbum.update({
        id: photoAlbum.id,
        imagePaths: null,
      });

      // If successful, the response here will be `null`:
      setCurrentPhotoAlbum(updatedPhotoAlbum.data);
      setCurrentImages(updatedPhotoAlbum.data?.imagePaths);
    } 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.models.PhotoAlbum.get({
        id: currentPhotoAlbum.id,
      });

      const photoAlbum = response.data;

      // If the record has no associated files, we can return early.
      if (!photoAlbum?.imagePaths) return;

      // Remove associated files from record
      const updateResponse = await client.models.PhotoAlbum.update({
        id: photoAlbum.id,
        imagePaths: null, // Set the file association to `null`
      });

      const updatedPhotoAlbum = updateResponse.data;

      // Delete the files from S3:
      await Promise.all(
        photoAlbum?.imagePaths.map(async (imagePath) => {
          if (!imagePath) return;
          await remove({ path: imagePath });
        })
      );

      // 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.models.PhotoAlbum.get({
        id: currentPhotoAlbum.id,
      });

      const photoAlbum = response.data;

      if (!photoAlbum) return;

      await client.models.PhotoAlbum.delete({
        id: photoAlbum.id,
      });

      setCurrentPhotoAlbum(null);

      // If the record has no associated file, we can return early.
      if (!photoAlbum?.imagePaths) return;

      await Promise.all(
        photoAlbum?.imagePaths.map(async (imagePath) => {
          if (!imagePath) return;
          await remove({ path: imagePath });
        })
      );

      clearLocalState();
    } catch (error) {
      console.error("Error deleting photoAlbum: ", error);
    }
  }

  function clearLocalState() {
    setCurrentPhotoAlbum(null);
    setCurrentImages([]);
  }

  return (
    <main className="app-container">
      <h1 className="greeting">Hello {user?.username}!</h1>
      <h2 className="current-album">
        Current PhotoAlbum: {currentPhotoAlbum?.id}
      </h2>

      <div className="file-input-container">
        <label className="file-input-label">
          Create photoAlbum with one file:
          <input
            type="file"
            accept="image/*"
            onChange={createPhotoAlbumWithFirstImage}
            className="file-input"
          />
        </label>

        <label className="file-input-label">
          Create photoAlbum with multiple files:
          <input
            type="file"
            accept="image/*"
            onChange={createPhotoAlbumWithMultipleImages}
            multiple
            className="file-input"
          />
        </label>

        <label className="file-input-label">
          Add multiple images to current photoAlbum:
          <input
            type="file"
            accept="image/*"
            onChange={addNewImagesToPhotoAlbum}
            disabled={!currentPhotoAlbum}
            multiple
            className="file-input"
          />
        </label>

        <label className="file-input-label">
          Replace last image:
          <input
            type="file"
            accept="image/*"
            onChange={updateLastImage}
            disabled={!currentPhotoAlbum || !currentImages}
            className="file-input"
          />
        </label>
      </div>

      <div className="button-container">
        <button
          onClick={getImagesForPhotoAlbum}
          disabled={!currentPhotoAlbum || !currentImages}
          className="app-button"
        >
          Get Images for Current Photo Album
        </button>
        <button
          onClick={removeImagesFromPhotoAlbum}
          disabled={!currentPhotoAlbum || !currentImages}
          className="app-button"
        >
          Remove images from current PhotoAlbum (does not delete images)
        </button>
        <button
          onClick={deleteImagesForCurrentPhotoAlbum}
          disabled={!currentPhotoAlbum || !currentImages}
          className="app-button"
        >
          Remove images from current PhotoAlbum, then delete images
        </button>
        <button
          onClick={deleteCurrentPhotoAlbumAndImages}
          disabled={!currentPhotoAlbum}
          className="app-button"
        >
          Delete current PhotoAlbum (and images, if they exist)
        </button>
        <button onClick={signOut} className="app-button">
          Sign out
        </button>
      </div>

      <div className="image-container">
        {currentImages &&
          currentImages.map((url, idx) => {
            if (!url) return undefined;
            return (
              <img src={url} key={idx} alt="Storage file" className="image" />
            );
          })}
      </div>
    </main>
  );
}

export default withAuthenticator(App);

```

</InlineFilter>
