feat(storage): add verified offline MinIO migration pilot

This commit is contained in:
2026-09-27 17:22:13 +02:00
parent 0005bfc4cf
commit 46ee819029
22 changed files with 3527 additions and 0 deletions

39
scripts/storage/cli.mjs Normal file
View File

@@ -0,0 +1,39 @@
import { readFile } from 'node:fs/promises';
import { client, call, inventory, transfer, contract, verifyManifest, atomicJson, cleanupProbe } from './s3.mjs';
// Secrets enter through a root-only mounted JSON file, never through argv or logs.
const [command, configPath = '/config/job.json'] = process.argv.slice(2);
if (command === 'contract') process.env.AWS_REQUEST_CHECKSUM_CALCULATION = 'WHEN_REQUIRED';
let source, target;
try {
const config = JSON.parse(await readFile(configPath, 'utf8'));
source = config.source && client(config.source);
target = config.target && client(config.target);
const bucket = config.bucket;
let result;
if (command === 'inventory') result = await inventory(source, bucket);
else if (command === 'copy' || command === 'verify') {
result = await transfer(source, target, bucket, config.directory || '/work', { verifyOnly: command === 'verify' });
result = { ...result, objects: undefined };
} else if (command === 'init') {
try { await call(target, 'HeadBucket', { Bucket: bucket }); }
catch (error) {
if (error.$metadata?.httpStatusCode !== 404) throw error;
await call(target, 'CreateBucket', { Bucket: bucket });
}
result = { bucket, initialized: true };
} else if (command === 'contract') result = await contract(target, config.target, bucket, { ...config, probePath: '/work/probe.json' });
else if (command === 'cleanup-probe') { await cleanupProbe(target, bucket, '/work/probe.json'); result = { cleaned: true }; }
else if (command === 'restore-verify') {
result = await verifyManifest(target, bucket, JSON.parse(await readFile(config.manifest || '/work/verified.json', 'utf8')));
} else throw new Error('Unbekannter Befehl.');
await atomicJson(config.result || '/work/result.json', result);
console.log(JSON.stringify(command === 'inventory' ? { count: result.count, bytes: result.bytes } : result));
} catch (error) {
// SDK error messages/URLs may contain identifiers; keep public output bounded.
const detail = { error: error.name, code: error.code, status: error.$metadata?.httpStatusCode,
message: error.$metadata ? 'S3-Prüfung fehlgeschlagen; Zugriff/Konfiguration prüfen.' : error.message };
try { await atomicJson('/work/error.json', detail); } catch { /* Preserve the original error. */ }
console.error(JSON.stringify(detail));
process.exitCode = 1;
} finally { source?.destroy(); target?.destroy(); }