Cloud File Management with Booster
--
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:
- Backend part: “Build Scalable Low-Code Backends with Booster”
- Frontend part: “From iOS Dev to Full-Stack in No Time with Booster”
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 dependencies
part:
"@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.