diff --git a/.env.example b/.env.example index 34084df..73dcdc2 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,11 @@ # Datenbank (Postgres) DATABASE_URL=postgres://klarbild:CHANGEME@klarbild-db:5432/klarbild -# Objektspeicher (S3-kompatibel, z. B. MinIO) +# Objektspeicher: fs (Volume, Default) oder s3 (MinIO) +STORAGE_DRIVER=fs +STORAGE_DIR=/data + +# Nur bei STORAGE_DRIVER=s3 (MinIO): S3_ENDPOINT=http://klarbild-minio:9000 S3_REGION=us-east-1 S3_BUCKET=klarbild diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 7a87319..e431cce 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -1,48 +1,87 @@ -import { - S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, - CreateBucketCommand, HeadBucketCommand, -} from '@aws-sdk/client-s3'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +// Objektspeicher-Abstraktion. STORAGE_DRIVER=fs (Volume, Default) | s3 (MinIO/S3). +// Presigned URLs werden nicht benötigt — Originale gehen als Base64-Data-URL an die Bild-API. +import { mkdir, readFile, writeFile, unlink } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +const DRIVER = (process.env.STORAGE_DRIVER || 'fs').toLowerCase(); +const DIR = process.env.STORAGE_DIR || '/data'; const bucket = process.env.S3_BUCKET || 'klarbild'; -export const s3 = new S3Client({ - endpoint: process.env.S3_ENDPOINT, - region: process.env.S3_REGION || 'us-east-1', - forcePathStyle: (process.env.S3_FORCE_PATH_STYLE ?? 'true') === 'true', - credentials: { - accessKeyId: process.env.S3_ACCESS_KEY || '', - secretAccessKey: process.env.S3_SECRET_KEY || '', - }, -}); +// --- S3 (lazy, nur wenn genutzt) -------------------------------------------- +let _s3: any = null; +async function s3client() { + if (!_s3) { + const { S3Client } = await import('@aws-sdk/client-s3'); + _s3 = new S3Client({ + endpoint: process.env.S3_ENDPOINT, + region: process.env.S3_REGION || 'us-east-1', + forcePathStyle: (process.env.S3_FORCE_PATH_STYLE ?? 'true') === 'true', + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY || '', + secretAccessKey: process.env.S3_SECRET_KEY || '', + }, + }); + } + return _s3; +} +// --- API -------------------------------------------------------------------- export async function ensureBucket(): Promise { - try { - await s3.send(new HeadBucketCommand({ Bucket: bucket })); - } catch { - await s3.send(new CreateBucketCommand({ Bucket: bucket })); + if (DRIVER === 's3') { + const { CreateBucketCommand, HeadBucketCommand } = await import('@aws-sdk/client-s3'); + const s3 = await s3client(); + try { await s3.send(new HeadBucketCommand({ Bucket: bucket })); } + catch { await s3.send(new CreateBucketCommand({ Bucket: bucket })); } + } else { + await mkdir(DIR, { recursive: true }); } } export async function putObject(key: string, body: Buffer, contentType: string): Promise { - await s3.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: body, ContentType: contentType })); + if (DRIVER === 's3') { + const { PutObjectCommand } = await import('@aws-sdk/client-s3'); + await (await s3client()).send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: body, ContentType: contentType })); + } else { + const p = join(DIR, key); + await mkdir(dirname(p), { recursive: true }); + await writeFile(p, body); + } return key; } export async function getObject(key: string): Promise { - const res = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })); - const chunks: Buffer[] = []; - for await (const c of res.Body as any) chunks.push(Buffer.from(c)); - return Buffer.concat(chunks); + if (DRIVER === 's3') { + const { GetObjectCommand } = await import('@aws-sdk/client-s3'); + const res = await (await s3client()).send(new GetObjectCommand({ Bucket: bucket, Key: key })); + const chunks: Buffer[] = []; + for await (const c of res.Body as any) chunks.push(Buffer.from(c)); + return Buffer.concat(chunks); + } + return readFile(join(DIR, key)); } export async function deleteObject(key: string): Promise { - await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key })); + if (DRIVER === 's3') { + const { DeleteObjectCommand } = await import('@aws-sdk/client-s3'); + await (await s3client()).send(new DeleteObjectCommand({ Bucket: bucket, Key: key })); + } else { + await unlink(join(DIR, key)).catch(() => {}); + } } -/** Vorsignierte GET-URL (Standard 15 Min) — u.a. für OpenRouter input_references. */ -export function presignGet(key: string, expiresIn = 900): Promise { - return getSignedUrl(s3, new GetObjectCommand({ Bucket: bucket, Key: key }), { expiresIn }); +/** Storage-Healthcheck (Verzeichnis beschreibbar bzw. Bucket erreichbar). */ +export async function storageOk(): Promise { + try { + if (DRIVER === 's3') { + const { HeadBucketCommand } = await import('@aws-sdk/client-s3'); + await (await s3client()).send(new HeadBucketCommand({ Bucket: bucket })); + } else { + await mkdir(DIR, { recursive: true }); + const probe = join(DIR, '.health'); + await writeFile(probe, 'ok'); await unlink(probe).catch(() => {}); + } + return true; + } catch { return false; } } export function sourceKey(uuid: string, ext: string): string { diff --git a/src/pages/api/health.ts b/src/pages/api/health.ts index 05966dd..73bd81b 100644 --- a/src/pages/api/health.ts +++ b/src/pages/api/health.ts @@ -1,7 +1,6 @@ import type { APIRoute } from 'astro'; import { pool } from '../../lib/db'; -import { s3 } from '../../lib/storage'; -import { HeadBucketCommand } from '@aws-sdk/client-s3'; +import { storageOk } from '../../lib/storage'; export const prerender = false; @@ -12,12 +11,9 @@ export const GET: APIRoute = async () => { try { await pool.query('SELECT 1'); out.db = 'ok'; } catch { out.db = 'fehler'; ok = false; } - try { - await s3.send(new HeadBucketCommand({ Bucket: process.env.S3_BUCKET || 'klarbild' })); - out.storage = 'ok'; - } catch { out.storage = 'fehler'; ok = false; } + out.storage = (await storageOk()) ? 'ok' : 'fehler'; + if (out.storage !== 'ok') ok = false; - out.queue = 'todo'; // wird mit pg-boss-Phase geprüft out.status = ok ? 'ok' : 'degraded'; return new Response(JSON.stringify(out), { status: ok ? 200 : 503,