Skip to main content
Version: 3.0.0-beta

Defining Custom Storage in FireCMS

FireCMS provides flexibility when it comes to integrating custom storage solutions. While it comes with built-in support for Firebase Storage, you might want to define your own storage solutions to handle file uploads, downloads, and metadata management. This guide will help you set up a custom storage solution.

Creating a Custom Storage Source

A custom storage source in FireCMS is defined by implementing the StorageSource interface. Here is a template for creating a custom storage source.

Beware: This is an example on how you can implement and use S3, directly from a custom storage source. This is not the preferred way of using S3 since it'd discourage to consume it directly from the frontend. As you will have to insert the API key and secret in the frontend, which is not secure. Instead, you could use Amplify/Storage or a custom IAM auth.

Below is a template for creating a custom storage source:

import {StorageSource, UploadFileProps, UploadFileResult, DownloadConfig} from "@firecms/core";
import {S3Client, PutObjectCommand, GetObjectCommand} from "@aws-sdk/client-s3";
import {getSignedUrl} from "@aws-sdk/s3-request-presigner";

export interface S3StorageSourceProps {
apiKey: string;
apiSecret: string;
region: string;
defaultBucket?: string;
}

function initializeCustomClient({apiKey, apiSecret, region, defaultBucket}: S3StorageSourceProps) {
const s3Client = new S3Client({region, credentials: {accessKeyId: apiKey, secretAccessKey: apiSecret}});

return {
uploadFile: async (destinationPath: string, file: File, bucket?: string, metadata?: any) => {
await s3Client.send(new PutObjectCommand({
Bucket: bucket || defaultBucket,
Key: destinationPath,
Body: file,
ContentType: metadata?.contentType,
Metadata: metadata
}));
return {
path: destinationPath,
bucket: bucket || defaultBucket || ""
}
},
getFile: async (path: string, bucket?: string) => {
return await s3Client.send(new GetObjectCommand({
Bucket: bucket || defaultBucket || "",
Key: path
}));
},
getDownloadURL: async (path: string, bucket?: string) => {
const command = new GetObjectCommand({Bucket: bucket || defaultBucket, Key: path});
return await getSignedUrl(s3Client, command, {expiresIn: 3600});
},
getMetadata: async (path: string, bucket?: string) => {
const s3Object = await s3Client.send(new GetObjectCommand({
Bucket: bucket || defaultBucket,
Key: path
}));
return {
bucket: (bucket || defaultBucket) ?? "",
fullPath: path,
name: path,
size: s3Object.ContentLength || 0,
contentType: s3Object.ContentType || "",
customMetadata: s3Object.Metadata || {}
}
}
};
}

export function useCustomStorageSource(props: S3StorageSourceProps): StorageSource {
// Initialize your storage client based on the props provided
// For example, it could be a client for Amazon S3, Google Cloud Storage, etc.
const customClient = initializeCustomClient(props);

return {
async uploadFile({file, fileName, path, metadata, bucket}: UploadFileProps): Promise<UploadFileResult> {
const usedFilename = fileName ?? file.name;
const destinationPath = `${path}/${usedFilename}`;
// Logic to upload file using your storage client
return await customClient.uploadFile(destinationPath, file, bucket, metadata);
},

async getFile(path: string, bucket?: string): Promise<File | null> {
const targetBucket = bucket ?? props.defaultBucket;
try {
// Logic to retrieve the file using your storage client
const fileData = await customClient.getFile(path, targetBucket);
if (fileData && fileData.Body) {
const byteArray = await fileData.Body.transformToByteArray();
const blob = new Blob([byteArray], {type: fileData.ContentType});
return new File([blob], path);
} else {
return null;
}
} catch (e) {
return null; // File not found
}
},

async getDownloadURL(path: string, bucket?: string): Promise<DownloadConfig> {
const targetBucket = bucket || props.defaultBucket;
try {
// Logic to get the download URL using your storage client
const url = await customClient.getDownloadURL(path, targetBucket);
const metadata = await customClient.getMetadata(path, targetBucket);
return {url, metadata};
} catch (e) {
return {url: null, fileNotFound: true};
}
}
};
}

Using the Custom Storage Source

After creating the custom storage source, you can use it in your FireCMS application by initializing it in your component and passing it to the FireCMS component.

Example Usage

Here is an example of how to use the custom storage source in your FireCMS application

import React from "react";
import "typeface-rubik";
import "@fontsource/jetbrains-mono";
import {
FireCMS,
ModeControllerProvider,
Scaffold,
AppBar,
Drawer,
NavigationRoutes,
SideDialogs,
SnackbarProvider,
useBuildLocalConfigurationPersistence,
useBuildModeController,
useBuildNavigationController,
useValidateAuthenticator,
CenteredView,
CircularProgressCenter
} from "@firecms/core";
import { productsCollection } from "./collections/products_collection";
import { useCustomStorageSource, CustomStorageSourceProps } from "./hooks/useCustomStorageSource";

const customStorageConfig: CustomStorageSourceProps = {
apiKey: "your-api-key",
apiSecret: "your-api-secret",
region: "your-region",
defaultBucket: "your-bucket-name"
// ... other necessary properties
};

const CustomStorageApp: React.FC = () => {
const name = "My Custom Storage FireCMS App";

const modeController = useBuildModeController();
const userConfigPersistence = useBuildLocalConfigurationPersistence();
const storageSource = useCustomStorageSource(customStorageConfig);

// const authController = useFirebaseAuthController(); // your auth controller
// const dataSourceDelegate = {}; // Your data source delegate implementation

const navigationController = useBuildNavigationController({
collections: [productsCollection],
// authController,
// dataSourceDelegate
});

// if (authLoading) {
// return <CircularProgressCenter />;
// }

return (
<SnackbarProvider>
<ModeControllerProvider value={modeController}>
<FireCMS
navigationController={navigationController}
userConfigPersistence={userConfigPersistence}
storageSource={storageSource}
// authController={authController}
// dataSourceDelegate={dataSourceDelegate}
>
{({ context, loading }) => {
if (loading || authLoading) {
return <CircularProgressCenter size="large" />;
}

if (!canAccessMainView) {
return <CenteredView>{notAllowedError}</CenteredView>;
}

return (
<Scaffold>
<AppBar title={"My app"} />
<Drawer />
<NavigationRoutes />
<SideDialogs />
</Scaffold>
);
}}
</FireCMS>
</ModeControllerProvider>
</SnackbarProvider>
);
};

export default CustomStorageApp;

As this example uses AWS S3, you will also need to enable cors in the bucket, as described in the AWS documentation - Configuring cross-origin resource sharing (CORS).

[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST",
"DELETE"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"x-amz-server-side-encryption",
"x-amz-request-id",
"x-amz-id-2"
],
"MaxAgeSeconds": 3000
}
]

This documentation provides a clear guide for defining custom storage solutions in FireCMS. Follow the template to integrate other storage services as per your requirements.

Sign up to our newsletter to get the latest news and updates. No spam!