import * as Minio from 'minio' // MinIO client singleton const globalForMinio = globalThis as unknown as { minio: Minio.Client | undefined } // Internal endpoint for server-to-server communication const MINIO_ENDPOINT = process.env.MINIO_ENDPOINT || 'http://localhost:9000' // Public endpoint for browser-accessible URLs (pre-signed URLs) // If not set, falls back to internal endpoint export const MINIO_PUBLIC_ENDPOINT = process.env.MINIO_PUBLIC_ENDPOINT || MINIO_ENDPOINT function createMinioClient(): Minio.Client { const url = new URL(MINIO_ENDPOINT) const accessKey = process.env.MINIO_ACCESS_KEY const secretKey = process.env.MINIO_SECRET_KEY if (process.env.NODE_ENV === 'production' && (!accessKey || !secretKey)) { throw new Error('MINIO_ACCESS_KEY and MINIO_SECRET_KEY environment variables are required in production') } return new Minio.Client({ endPoint: url.hostname, port: url.port ? parseInt(url.port) : (url.protocol === 'https:' ? 443 : 80), useSSL: url.protocol === 'https:', accessKey: accessKey || 'minioadmin', secretKey: secretKey || 'minioadmin', }) } export const minio = globalForMinio.minio ?? createMinioClient() if (process.env.NODE_ENV !== 'production') globalForMinio.minio = minio // Default bucket name export const BUCKET_NAME = process.env.MINIO_BUCKET || 'mopc-files' /** * Replace internal endpoint with public endpoint in a URL */ function replaceEndpoint(url: string): string { if (MINIO_PUBLIC_ENDPOINT === MINIO_ENDPOINT) { return url } try { const internalUrl = new URL(MINIO_ENDPOINT) const publicUrl = new URL(MINIO_PUBLIC_ENDPOINT) return url.replace( `${internalUrl.protocol}//${internalUrl.host}`, `${publicUrl.protocol}//${publicUrl.host}` ) } catch { return url } } // ============================================================================= // Helper Functions // ============================================================================= /** * Generate a pre-signed URL for file download or upload * Uses MINIO_PUBLIC_ENDPOINT for browser-accessible URLs */ export async function getPresignedUrl( bucket: string, objectKey: string, method: 'GET' | 'PUT' = 'GET', expirySeconds: number = 900 // 15 minutes default ): Promise { let url: string if (method === 'GET') { url = await minio.presignedGetObject(bucket, objectKey, expirySeconds) } else { url = await minio.presignedPutObject(bucket, objectKey, expirySeconds) } // Replace internal endpoint with public endpoint for browser access return replaceEndpoint(url) } /** * Check if a bucket exists, create if not */ export async function ensureBucket(bucket: string): Promise { const exists = await minio.bucketExists(bucket) if (!exists) { await minio.makeBucket(bucket) console.log(`Created MinIO bucket: ${bucket}`) } } /** * Delete an object from MinIO */ export async function deleteObject( bucket: string, objectKey: string ): Promise { await minio.removeObject(bucket, objectKey) } /** * Generate a unique object key for a project file */ export function generateObjectKey( projectId: string, fileName: string ): string { const timestamp = Date.now() const sanitizedName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_') return `projects/${projectId}/${timestamp}-${sanitizedName}` }