- docker-compose.coolify(.prebuilt).yml: Service craftvia-worker (Target worker, Chromium, shm_size 1gb, gleiche Härtung), Craftvia-Variablen für app und worker. - Dockerfile: worker-Stage mit HOME=/home/app (Chromium-Profil für non-root); lokaler docker build der Targets runner und worker erfolgreich, PDF-Erzeugung im Image geprüft. - .env.example/.env.prod.example/.env.coolify.example: alle Craftvia-Variablen inkl. RLS, KI-Provider, PDF_CHROMIUM_PATH, OFFLINE_MAX_DAYS, API_RATE_LIMIT_*, AI_GENERATION_RETENTION_DAYS, AI_MONTHLY_TOKEN_LIMIT. - CI (.github, .gitea): Job gate mit Postgres (pgvector) und Redis als Service: migrate deploy, seed, Passwort für craftvia_app, tsc, lint, build, npm run test. - docs/craftvia/DEPLOY.md (aus den Certvia-Deploy-Docs abgeleitet): Architektur, Domains, Secrets, Worker, Migrationen, RLS-Aktivierung, Backup/Restore, KI, Rate Limits, Aufbewahrung, Smoke, Update/Rollback. build-and-push-images.sh baut craftvia-worker. - Certvia-/ISMS-Dokumente aus docs/ nach docs/_certvia-archiv/ (mit README); Verweise in README.md und Skript-Kommentaren angepasst. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
100 lines
4.2 KiB
TypeScript
100 lines
4.2 KiB
TypeScript
// Smoke-Test des S3-Objektspeichers gegen einen echten Garage-Node (Phase C/D der
|
|
// MinIO→Garage-Migration, docs/_certvia-archiv/KONZEPT-garage-migration.md §9).
|
|
//
|
|
// Prüft den realen S3-Pfad (AWS SDK v3, path-style) über die exportierte
|
|
// `resolveBackupStore()`:
|
|
// 1. Put → Get (Byte-Identität),
|
|
// 2. List (Prefix enthält den Key),
|
|
// 3. Delete (remove räumt den Prefix),
|
|
// 4. Negativfall: fehlender Bucket → sprechender Konfigurationsfehler
|
|
// (KEIN stiller CreateBucket-Versuch mehr).
|
|
//
|
|
// Ausführung:
|
|
// - OHNE S3_ENDPOINT → der Test ÜBERSPRINGT sich (Exit 0), damit das Standard-Gate
|
|
// (npm run test) ohne laufenden Objektspeicher grün bleibt.
|
|
// - MIT gesetzten S3_*-Variablen (z. B. gegen den Compose-Garage oder einen lokalen
|
|
// Container) → echter End-to-End-Durchstich.
|
|
//
|
|
// S3_ENDPOINT=http://127.0.0.1:3900 S3_ACCESS_KEY=… S3_SECRET_KEY=… \
|
|
// S3_BUCKET=craftvia-documents S3_REGION=us-east-1 npx tsx scripts/test-garage-storage.ts
|
|
|
|
import { resolveBackupStore, type BackupTargetConfig } from "../src/server/storage/backup-store";
|
|
|
|
let failures = 0;
|
|
const ok = (cond: boolean, msg: string) => {
|
|
console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`);
|
|
if (!cond) failures++;
|
|
};
|
|
|
|
function envCfg(bucketOverride?: string): BackupTargetConfig {
|
|
return {
|
|
backupTarget: "s3",
|
|
backupLocalDir: null,
|
|
backupS3Endpoint: process.env.S3_ENDPOINT?.trim() ?? null,
|
|
backupS3Bucket: bucketOverride ?? process.env.S3_BUCKET?.trim() ?? "craftvia-documents",
|
|
backupS3Region: process.env.S3_REGION?.trim() || "us-east-1",
|
|
backupS3AccessKey: process.env.S3_ACCESS_KEY?.trim() ?? null,
|
|
backupS3SecretKey: process.env.S3_SECRET_KEY?.trim() ?? null,
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const endpoint = process.env.S3_ENDPOINT?.trim();
|
|
if (!endpoint) {
|
|
console.log("↷ übersprungen: kein S3_ENDPOINT gesetzt (kein laufender Objektspeicher).");
|
|
console.log(" Für den echten Durchstich S3_ENDPOINT/S3_ACCESS_KEY/S3_SECRET_KEY/S3_BUCKET setzen.");
|
|
return;
|
|
}
|
|
if (!process.env.S3_ACCESS_KEY || !process.env.S3_SECRET_KEY) {
|
|
console.log("✗ FEHLER S3_ENDPOINT gesetzt, aber S3_ACCESS_KEY/S3_SECRET_KEY fehlen.");
|
|
failures++;
|
|
return;
|
|
}
|
|
|
|
// ── Positiv: Put → Get → List → Delete gegen den provisionierten Bucket ──────
|
|
const store = resolveBackupStore(envCfg());
|
|
const prefix = `__smoketest__/${process.pid}-${Date.now()}/`;
|
|
const key = `${prefix}garage-probe.bin`;
|
|
const payload = Buffer.from("craftvia · garage smoke test · äöü · 🗄️", "utf8");
|
|
|
|
await store.put(key, payload);
|
|
ok(true, "Put gegen Garage erfolgreich");
|
|
|
|
const got = await store.get(key);
|
|
ok(got != null && got.equals(payload), "Get liefert identische Bytes zurück");
|
|
|
|
const missing = await store.get(`${prefix}gibtsnicht.bin`);
|
|
ok(missing === null, "Get eines fehlenden Keys → null (kein Wurf)");
|
|
|
|
const listed = await store.list(prefix);
|
|
ok(listed.includes(key), "List enthält den geschriebenen Key (Prefix-Scan)");
|
|
|
|
const removed = await store.remove(prefix);
|
|
ok(removed >= 1, `Delete räumt den Prefix (entfernt: ${removed})`);
|
|
const afterRemove = await store.list(prefix);
|
|
ok(afterRemove.length === 0, "Prefix ist nach remove leer");
|
|
|
|
// ── Negativ: fehlender Bucket → sprechender Konfigurationsfehler ─────────────
|
|
const badBucket = `does-not-exist-${process.pid}-${Date.now()}`;
|
|
const badStore = resolveBackupStore(envCfg(badBucket));
|
|
try {
|
|
await badStore.put(`${badBucket}/x.bin`, Buffer.from("x"));
|
|
ok(false, "fehlender Bucket hätte einen Fehler werfen müssen");
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
const speaks = /nicht provisioniert|nicht erreichbar/.test(msg) && msg.includes(badBucket);
|
|
ok(speaks, "fehlender Bucket → sprechender Konfigurationsfehler (kein stiller CreateBucket)");
|
|
if (!speaks) console.log(" erhaltene Meldung:", msg);
|
|
}
|
|
}
|
|
|
|
main()
|
|
.then(() => {
|
|
console.log(failures === 0 ? "\nGarage-Storage-Smoke-Test grün." : `\n${failures} Prüfung(en) fehlgeschlagen.`);
|
|
process.exit(failures === 0 ? 0 : 1);
|
|
})
|
|
.catch((err) => {
|
|
console.error("\n✗ Unerwarteter Fehler im Smoke-Test:", err);
|
|
process.exit(1);
|
|
});
|