import { readFile } from "node:fs/promises" import path from "node:path" import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3" export type MediaStorageProvider = "local" | "s3" type S3Config = { bucket: string region: string endpoint?: string accessKeyId: string secretAccessKey: string forcePathStyle?: boolean } function parseBoolean(value: string | undefined): boolean { return value?.toLowerCase() === "true" } export function resolveMediaStorageProvider(raw: string | undefined): MediaStorageProvider { if (raw?.toLowerCase() === "local") { return "local" } return "s3" } function resolveS3Config(): S3Config { const bucket = process.env.CMS_MEDIA_S3_BUCKET?.trim() const region = process.env.CMS_MEDIA_S3_REGION?.trim() const accessKeyId = process.env.CMS_MEDIA_S3_ACCESS_KEY_ID?.trim() const secretAccessKey = process.env.CMS_MEDIA_S3_SECRET_ACCESS_KEY?.trim() const endpoint = process.env.CMS_MEDIA_S3_ENDPOINT?.trim() || undefined if (!bucket || !region || !accessKeyId || !secretAccessKey) { throw new Error( "S3 storage selected but required env vars are missing: CMS_MEDIA_S3_BUCKET, CMS_MEDIA_S3_REGION, CMS_MEDIA_S3_ACCESS_KEY_ID, CMS_MEDIA_S3_SECRET_ACCESS_KEY", ) } return { bucket, region, endpoint, accessKeyId, secretAccessKey, forcePathStyle: parseBoolean(process.env.CMS_MEDIA_S3_FORCE_PATH_STYLE), } } function createS3Client(config: S3Config): S3Client { return new S3Client({ region: config.region, endpoint: config.endpoint, forcePathStyle: config.forcePathStyle, credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey, }, }) } function resolveLocalMediaBaseDirectory(): string { const configured = process.env.CMS_MEDIA_LOCAL_STORAGE_DIR?.trim() if (configured) { return path.resolve(configured) } return path.resolve(process.cwd(), ".data", "media") } async function readFromLocalStorage(storageKey: string): Promise { const baseDirectory = resolveLocalMediaBaseDirectory() const outputPath = path.join(baseDirectory, storageKey) return readFile(outputPath) } async function readFromS3Storage(storageKey: string): Promise { const config = resolveS3Config() const client = createS3Client(config) const response = await client.send( new GetObjectCommand({ Bucket: config.bucket, Key: storageKey, }), ) if (!response.Body) { throw new Error("S3 object body is empty") } return response.Body.transformToByteArray() } export async function readMediaStorageObject(storageKey: string): Promise { const preferred = resolveMediaStorageProvider(process.env.CMS_MEDIA_STORAGE_PROVIDER) const reads = preferred === "s3" ? [() => readFromS3Storage(storageKey), () => readFromLocalStorage(storageKey)] : [() => readFromLocalStorage(storageKey), () => readFromS3Storage(storageKey)] for (const read of reads) { try { return await read() } catch { // Try next backend. } } throw new Error("Unable to read media file from configured storage backends") }