import { DeleteObjectCommand, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3'; /** * MinIO / S3-compatible object storage (CT 102). * * Env: * S3_ENDPOINT — e.g. http://192.168.68.102:9000 * S3_ACCESS_KEY_ID — app-scoped MinIO user * S3_SECRET_ACCESS_KEY * S3_BUCKET — e.g. deckhearth * S3_PUBLIC_BASE_URL — e.g. https://cdn.stillwell.cloud/deckhearth * S3_REGION — optional, default us-east-1 */ let client = null; function requireEnv(name) { const value = process.env[name]; if (!value?.trim()) { throw new Error(`${name} is not configured`); } return value.trim(); } function getClient() { if (!client) { client = new S3Client({ endpoint: requireEnv('S3_ENDPOINT'), region: process.env.S3_REGION || 'us-east-1', credentials: { accessKeyId: requireEnv('S3_ACCESS_KEY_ID'), secretAccessKey: requireEnv('S3_SECRET_ACCESS_KEY'), }, forcePathStyle: true, }); } return client; } function getBucket() { return requireEnv('S3_BUCKET'); } function publicUrlForKey(key) { const base = requireEnv('S3_PUBLIC_BASE_URL').replace(/\/$/, ''); return `${base}/${key.replace(/^\//, '')}`; } /** Extract object key from a stored public URL or pass through raw keys. */ export function objectKeyFromReference(reference) { if (!reference || typeof reference !== 'string') { throw new Error('Invalid object reference'); } if (!reference.startsWith('http')) { return reference.replace(/^\//, ''); } const base = process.env.S3_PUBLIC_BASE_URL?.replace(/\/$/, ''); if (base && reference.startsWith(`${base}/`)) { return reference.slice(base.length + 1); } try { const url = new URL(reference); const parts = url.pathname.split('/').filter(Boolean); const bucket = getBucket(); if (parts[0] === bucket) { return parts.slice(1).join('/'); } return parts.join('/'); } catch { throw new Error('Could not parse object URL for deletion'); } } /** * Drop-in replacement for `@vercel/blob` put (access ignored — bucket policy controls visibility). * @returns {Promise<{ url: string }>} */ export async function put(key, body, { contentType } = {}) { const objectKey = key.replace(/^\//, ''); await getClient().send( new PutObjectCommand({ Bucket: getBucket(), Key: objectKey, Body: body, ContentType: contentType, }) ); return { url: publicUrlForKey(objectKey) }; } /** Delete by object key or public URL (legacy blob URLs in DB). */ export async function del(urlOrKey) { const objectKey = objectKeyFromReference(urlOrKey); await getClient().send( new DeleteObjectCommand({ Bucket: getBucket(), Key: objectKey, }) ); }