mjackson / remix-the-web

Open source tools for Remix (or any framework!)
MIT License
712 stars 14 forks source link

S3 storage driver #10

Closed KubaJastrz closed 2 months ago

KubaJastrz commented 2 months ago

I'm sure someone will be looking for it eventually, so I want to share my simple S3 storage driver that I wrote for my project. You can extend it however you like, for example by adding ACL, file metadata, nested directories, etc.

Here's the code:

import {
    DeleteObjectCommand,
    GetObjectCommand,
    HeadObjectCommand,
    NoSuchKey,
    NotFound,
    PutObjectCommand,
    S3Client,
} from "@aws-sdk/client-s3";
import type { FileStorage } from "@mjackson/file-storage";

const s3Client = new S3Client({
    region: process.env.AWS_S3_REGION!,
    credentials: {
        accessKeyId: process.env.AWS_S3_ACCESS_KEY_ID!,
        secretAccessKey: process.env.AWS_S3_SECRET_ACCESS_KEY!,
    },
});

export class S3Storage implements FileStorage {
    bucket = "my-bucket";

    async has(key: string): Promise<boolean> {
        try {
            await s3Client.send(
                new HeadObjectCommand({
                    Bucket: this.bucket,
                    Key: key,
                }),
            );

            return true;
        } catch (error) {
            if (error instanceof NotFound) {
                return false;
            }
            console.error(
                `Failed to check for a file ${key} in bucket ${this.bucket}`,
                error,
            );
            return false;
        }
    }

    async get(key: string): Promise<File | null> {
        try {
            const response = await s3Client.send(
                new GetObjectCommand({
                    Bucket: this.bucket,
                    Key: key,
                }),
            );
            if (!response.Body) {
                return null;
            }
            const bytes = await response.Body.transformToByteArray();
            return new File([bytes], key, {
                type: response.ContentType,
                lastModified: response.LastModified?.getTime(),
            });
        } catch (error) {
            if (error instanceof NoSuchKey) {
                return null;
            }
            console.error(
                `Failed to get a file ${key} in bucket ${this.bucket}`,
                error,
            );
            return null;
        }
    }

    async set(key: string, value: File): Promise<void> {
        try {
            await s3Client.send(
                new PutObjectCommand({
                    Bucket: this.bucket,
                    Key: key,
                    Body: value,
                    ContentType: value.type,
                }),
            );
        } catch (error) {
            console.error(
                `Failed to set a file ${key} in bucket ${this.bucket}`,
                error,
            );
        }
    }

    async remove(key: string): Promise<void> {
        try {
            await s3Client.send(
                new DeleteObjectCommand({
                    Bucket: this.bucket,
                    Key: key,
                }),
            );
        } catch (error) {
            console.error(
                `Failed to remove a file ${key} in bucket ${this.bucket}`,
                error,
            );
        }
    }
}