Cloud File Management with Booster

Juan Sagasti
The Agile Monkeys’ Journey
6 min readFeb 13, 2023

--

Storage is one of the essential features for most projects. This tutorial will show you how to easily add cloud file management to your Booster backend.

As a reminder, our previous articles showed you how to create a cloud-based backend application using Booster. We also demonstrated how to connect this backend with an iOS application easily. All this process was documented in these two articles:

To showcase this new file feature, we will use the Live Questions app documented in those articles to allow users to set a profile picture that will be uploaded to the Booster backend.

Rockets are cool 🚀

Booster utilizes Rockets to extend your backend infrastructure and runtime with features not included in the framework core. Think of rockets as reusable and composable plug-ins that anyone can create to extend Booster functionality.

In this case, for adding file management to our Live Questions Booster backend, we will integrate the File Uploads Rocket already created and available to the Booster community.

Backend part

This rocket allows file storage in AWS or Azure as the cloud providers. We are going to use AWS for our backend and examples. Here’s a quick overview of the things you can do with the 1.0.0 of this rocket, as you can see in the Readme file in the repository:

  • Get the pre-signed upload URL to which you will do the multipart file upload.
  • Get the pre-signed download URL of an existing file.
  • List all the files in a directory.
  • Delete a file.
  • Add authorization to those operations easily through your Booster commands and user roles.
  • When a file is uploaded to the cloud, the rocket will generate a new Booster event and entity. This way, you can create a Booster ReadModel in your backend to provide a view of the files uploaded to a directory.

As a reminder, let’s ask ChatGPT what a pre-signed URL is and why we need them. Answer:

A pre-signed URL is a URL that has been signed with authentication credentials, allowing anyone with the URL to upload or download the associated object in your Amazon S3 bucket, without requiring AWS security credentials or permissions.

Pre-signed URLs are useful when you want to give time-limited permission to upload or download a specific object. For example, you might use a pre-signed URL to allow a user to upload a file to your S3 bucket, or to download a specific file that they have been granted access to. The pre-signed URL contains all the information required to perform the upload or download, including the bucket name, object key, and a signature that verifies the authenticity of the request. The signature is generated using your AWS access key, which is used to calculate the signature for the URL.

Pre-signed URLs are valid for a limited period of time, which is specified when the URL is generated. Once the URL has expired, it can no longer be used to upload or download the associated object. This helps to ensure that your data remains secure, as it cannot be accessed after the pre-determined expiration time.

Update dependencies

To include the AWS Rocket in your project, go to your package.json file and add the following packages in the dependenciespart:

  "@boostercloud/rocket-file-uploads-aws": "VERSION",
"@boostercloud/rocket-file-uploads-core": "VERSION",
"@boostercloud/rocket-file-uploads-types": "VERSION",

And the the infrastructure package in the devDependencies part:

"@boostercloud/rocket-file-uploads-aws-infrastructure": "VERSION"

Note: If you are using Azure as your cloud provider, make sure to use the appropriate packages (-azure instead of -aws).

Configure the rocket in your Booster backend

As you can see in our config.ts file:

 const rocketFilesConfigurationDefault: RocketFilesUserConfiguration = {
storageName: '<STORAGE_NAME>', // The name you want for your AWS S3 bucket.
containerName: '', // You can leave this empty, not needed in AWS.
directories: [<DIRECTORY_NAME_1>] // Root directories for your files.
}


Booster.configure(environment.name, (config: BoosterConfig): void => {

[...]

config.rockets = [new BoosterRocketFiles(config,
[rocketFilesConfigurationDefault]).rocketForAWS()]

Note: The containerName parameter is not used in AWS, so the final structure created will be storage > directory > files.

Create a Command for getting the Pre-signed Upload URL

As we will use this rocket to upload and download profile pictures, we created a command that abstracts the clients without specifying parameters like the file name and the file directory. Our profile-picture-upload-url.ts command make that decision internally (and extra validations). The image will be stored in “files/<userId>/profilePicture.jpg”. Our command interacts with the rocket through the FileHandler API:

@Command({
authorize: [UserRole],
before: [CommonValidations.userValidation]
})
export class ProfilePictureUploadURL {
public constructor() {}

public static async handle(command: ProfilePictureUploadURL, register: Register): Promise<PresignedPostResponse> {
const boosterConfig = Booster.config
const fileHandler = new FileHandler(boosterConfig, ConfigConstants.rocketFilesConfigurationDefault.storageName)
return await fileHandler.presignedPut(ConfigConstants.rocketFilesConfigurationDefault.directories[0], `${getUserId(register)}/${profilePictureKey}`) as Promise<PresignedPostResponse>
}
}

The PresignedPostResponse contains the upload URL and the metadata fields needed to perform the multipart upload from the client:

export class PresignedPostResponse {
public constructor(
readonly url: string,
readonly fields: { [key: string]: string }
){}
}

Create a Command for getting the Pre-signed Download URL

The command for getting the download URL is similar, but the response will be just the download URL string this time. As you can see inprofile-picture-download-url.ts:

@Command({
authorize: [UserRole],
before: [CommonValidations.userValidation]
})
export class ProfilePictureDownloadURL {
public constructor() {}

public static async handle(command: ProfilePictureDownloadURL, register: Register): Promise<string> {
const boosterConfig = Booster.config
const fileHandler = new FileHandler(boosterConfig, ConfigConstants.rocketFilesConfigurationDefault.storageName)
return await fileHandler.presignedGet(ConfigConstants.rocketFilesConfigurationDefault.directories[0], `${getUserId(register)}/${profilePictureKey}`)
}
}

You can also check the files-list.ts and delete-profile-picture.ts commands.

And that’s it. That was the backend part. Easy, right?

iOS part

Mutations and client API generation

Once you have deployed your Booster changes and the new GraphQL schema is generated, we can start adding the mutations needed to communicate with our new commands in the app.

ProfilePictureUploadURL.graphql

mutation ProfilePictureUploadURL {
ProfilePictureUploadURL {
url
fields
}
}

ProfilePictureDownloadURL.graphql

mutation ProfilePictureDownloadURL {
ProfilePictureDownloadURL
}

You can also check FilesList.graphql and DeleteProfilePicture.graphql.

As we previously mentioned in the other article, you can use Apollo’s Code Generation tool to download your schema and generate the Swift API for the types defined in it. Execute the following command in the terminal on the root folder of your iOS project:

./apollo-ios-cli generate -f

Profile image upload

To upload the profile picture, you will first need to call the ProfilePictureUploadURL mutation to obtain the necessary information for the file upload, such as the URL and the fields required for the pre-signed multipart request:

func uploadProfilePicture(data: Data) async throws {
let mutation = BoosterSchema.ProfilePictureUploadURLMutation()

guard let result = try await networkClient.mutate(mutation: mutation)?.profilePictureUploadURL,
let fields = result.fields.value as? [String: String] else { throw FileError.presignPostFailure }

let presignedPost = PresignedPost(url: result.url, fields: fields)
let uploadEndpoint = API.uploadFile(data: data, metadata: presignedPost)
let uploadRequest = try URLRequest(endpoint: uploadEndpoint)


_ = try await URLSession.shared.data(for: uploadRequest)
}

Profile image download

You need to call the ProfilePictureDownloadURL mutation to get the pre-signed URL of the file you want to download:

 func downloadProfilePicture() async throws -> Data {
let mutation = BoosterSchema.ProfilePictureDownloadURLMutation()

guard let urlString = try await networkClient.mutate(mutation: mutation)?.profilePictureDownloadURL,
let url = URL(string: urlString) else { throw FileError.presignGetFailure }

let fileRequest = URLRequest(url: url)
let (imageData, _) = try await URLSession.shared.data(for: fileRequest)
return imageData
}

You can also see examples of the removeProfilePicture() and filesList() methods in FileService.swift.

Conclusion

Integrating file upload and download into your Booster app was easy with this rocket. With a few new mutations and requests, you now have file management capabilities!

Booster offers a convenient way to add file management with access control to enterprise-grade apps without cloud expertise. Reach out to our supportive developer community on Discord for any questions or help. Join us on Discord and become part of our thriving community!

This article was co-authored by Damien Vieira and Juan Sagasti.

--

--

Juan Sagasti
The Agile Monkeys’ Journey

Software Engineer  & Co-founder at The Agile Monkeys 🐒