Unter Verwaltung -> Daten laedt der Admin alles als ZIP (Daten plus hochgeladene Dateien), nur die Daten als JSON oder die Sammlung als CSV - letzteres mit Semikolon und BOM, damit deutsches Excel es ohne Rueckfragen oeffnet. Die CSV steht auch Mitgliedern offen, das vollstaendige Archiv nur Administratoren. Bewusst nicht im Export: Konten, Passwoerter und Passkeys. Ein Export voller Passwort-Hashes im Download-Ordner waere ein Risiko und kein Backup - das bleibt ./data. Ebenfalls draussen: Reservierungen und Teilen-Links, die zur jeweiligen Instanz gehoeren. Der Import ersetzt alle Anwendungsdaten in einer Transaktion, statt zu vermischen: Zusammenfuehren zweier Sammlungen braeuchte Konfliktregeln fuer jede Tabelle und waere im Ergebnis nicht vorhersagbar. Deshalb ist er Administratoren vorbehalten und verlangt ein getipptes ERSETZEN. Wunschlisten gehoeren danach dem importierenden Admin und bekommen frische, zunaechst nicht geteilte Links. Pfade aus dem Archiv werden gegen Ausbrueche aus dem Dateiverzeichnis geprueft. 10 Tests decken den Rundweg ab: Export, Loeschen, Import, Ersetzen statt Vermischen, und dass ein fremdes Archiv abgelehnt wird, ohne etwas zu loeschen. Refs Phase 2 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
46 lines
1.5 KiB
TypeScript
46 lines
1.5 KiB
TypeScript
'use server';
|
|
|
|
import { revalidatePath } from 'next/cache';
|
|
import { actionData, actionError, type ActionResult } from '@/lib/action-result';
|
|
import { importArchive, ImportError, type ImportSummary } from '@/lib/export';
|
|
import { requireAdmin } from '@/lib/session';
|
|
|
|
const MAX_ARCHIVE_BYTES = 512 * 1024 * 1024;
|
|
|
|
/** Typed by hand before anything is deleted. */
|
|
const CONFIRMATION = 'ERSETZEN';
|
|
|
|
export async function importArchiveAction(
|
|
formData: FormData,
|
|
): Promise<ActionResult<ImportSummary>> {
|
|
const admin = await requireAdmin();
|
|
|
|
if (formData.get('confirmation') !== CONFIRMATION) {
|
|
return actionError(`Bitte tipp ${CONFIRMATION} ein, um den Austausch zu bestätigen.`);
|
|
}
|
|
|
|
const file = formData.get('archive');
|
|
if (!(file instanceof File) || file.size === 0) {
|
|
return actionError('Bitte wähle ein Bricktrack-Archiv (.zip) aus.');
|
|
}
|
|
if (file.size > MAX_ARCHIVE_BYTES) {
|
|
return actionError('Das Archiv ist größer als 512 MB und wird nicht eingelesen.');
|
|
}
|
|
|
|
try {
|
|
const summary = importArchive(
|
|
new Uint8Array(await file.arrayBuffer()),
|
|
admin.id,
|
|
);
|
|
|
|
revalidatePath('/', 'layout');
|
|
return actionData(summary);
|
|
} catch (error) {
|
|
if (error instanceof ImportError) return actionError(error.message);
|
|
console.error('[import] Archiv konnte nicht eingelesen werden', error);
|
|
return actionError(
|
|
'Das Archiv konnte nicht eingelesen werden. Die vorhandenen Daten wurden nicht verändert.',
|
|
);
|
|
}
|
|
}
|