export type ImageStorageProvider = 'database' | 's3'; export type S3ImageStorageConfig = { provider: 's3'; endpoint: string; region: string; bucket: string; accessKeyId: string; secretAccessKey: string; publicBaseUrl: string | null; keyPrefix: string; }; const trimOptional = (value: string | undefined) => { const trimmed = value?.trim(); return trimmed ? trimmed : null; }; export const getImageStorageProvider = (): ImageStorageProvider => process.env.IMAGE_STORAGE_PROVIDER === 's3' ? 's3' : 'database'; export const getS3ImageStorageConfig = (): S3ImageStorageConfig | null => { if (getImageStorageProvider() !== 's3') { return null; } const endpoint = trimOptional(process.env.S3_ENDPOINT); const region = trimOptional(process.env.S3_REGION); const bucket = trimOptional(process.env.S3_BUCKET); const accessKeyId = trimOptional(process.env.S3_ACCESS_KEY_ID); const secretAccessKey = trimOptional(process.env.S3_SECRET_ACCESS_KEY); if (!endpoint || !region || !bucket || !accessKeyId || !secretAccessKey) { return null; } return { provider: 's3', endpoint, region, bucket, accessKeyId, secretAccessKey, publicBaseUrl: trimOptional(process.env.S3_PUBLIC_BASE_URL), keyPrefix: trimOptional(process.env.S3_KEY_PREFIX) ?? 'bird-photos', }; }; export const isS3ImageStorageConfigured = () => getS3ImageStorageConfig() !== null; export const buildBirdPhotoObjectKey = ({ workspaceId, birdId, extension, now = new Date(), }: { workspaceId: number; birdId: string; extension: string; now?: Date; }) => { const prefix = trimOptional(process.env.S3_KEY_PREFIX) ?? 'bird-photos'; const safeExtension = extension.replace(/^\./, '').toLowerCase() || 'bin'; const timestamp = now.toISOString().replace(/[:.]/g, '-'); return `${prefix}/workspace-${workspaceId}/${birdId}/${timestamp}.${safeExtension}`; }; export const getImageExtensionFromContentType = (contentType: string) => { switch (contentType.toLowerCase()) { case 'image/jpeg': case 'image/jpg': return 'jpg'; case 'image/png': return 'png'; case 'image/webp': return 'webp'; case 'image/gif': return 'gif'; default: return 'bin'; } };