feat(export): Projekte exportieren und importieren
Bislang lagen alle Daten ausschliesslich in der Datenbank - ein "docker compose down -v" haette alles geloescht, ohne Rueckweg. Export: - alle eigenen Projekte oder ein einzelnes als JSON - ein Projekt zusaetzlich als lesbares Markdown, inklusive letztem bekannten Git-Stand Was bewusst nicht in der Datei steht: - Access Tokens. Die Verbindung wird nur beschrieben (Name, System, Basis-URL), das Token muss beim Import neu hinterlegt werden. Eine Exportdatei landet sonst schnell in einem Download-Ordner oder einem Cloud-Abgleich. - Repo-Snapshots. Reproduzierbar und gross; ein Abgleich holt sie zurueck. - Datenbank-IDs und der Besitzer. Beides gilt nur in der Quellinstanz. Import: - die hochgeladene Datei ist Fremdinhalt und wird vor jeder Verwendung streng gegen ein Zod-Schema geprueft - Abgleich ueber den Slug, drei Strategien: ueberspringen, zusaetzlich anlegen, ersetzen - dryRun liefert dieselbe Auswertung, ohne zu schreiben - Grundlage der Vorschau in der Oberflaeche - Verbindungen werden ueber Name oder Provider samt Basis-URL zugeordnet; fehlt eine, wird das Projekt ohne Verknuepfung angelegt und das gemeldet - beim Ersetzen werden Notizen ausgetauscht statt zusammengefuehrt: das ist vorhersagbar, und ein erneuter Import erzeugt keine Dubletten Die Instanz-Sicherung fuer Administratoren enthaelt dagegen alles, was zum Wiederherstellen noetig ist - Passwort-Hashes und Token-Chiffrate eingeschlossen. Ohne dasselbe APP_SECRET sind die Tokens wertlos. Sitzungen und Passkeys bleiben draussen: erstere sind kurzlebig, letztere haengen an Geraet und rpID. readJsonBody bekommt eine einstellbare Obergrenze - 256 KB reichen fuer eine Exportdatei mit vielen Notizen nicht. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { requireUserApi } from "@/lib/auth/guard";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { exportFileName } from "@/lib/export/format";
|
||||
import { buildExportFile, toMarkdown } from "@/lib/export/project";
|
||||
import { jsonError } from "@/lib/security/request";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
/**
|
||||
* Ein einzelnes Projekt exportieren.
|
||||
*
|
||||
* ?format=json (Vorgabe) – zum Wiedereinlesen
|
||||
* ?format=md – lesbar, zum Archivieren
|
||||
*/
|
||||
export async function GET(request: Request, { params }: Params) {
|
||||
const auth = await requireUserApi();
|
||||
if (!auth.ok) return auth.response;
|
||||
|
||||
const { id } = await params;
|
||||
const format = new URL(request.url).searchParams.get("format") ?? "json";
|
||||
|
||||
const project = await prisma.project.findFirst({
|
||||
where: { id, ownerId: auth.user.id },
|
||||
include: {
|
||||
notes: { orderBy: [{ pinned: "desc" }, { createdAt: "asc" }] },
|
||||
connection: { select: { name: true, provider: true, baseUrl: true } },
|
||||
snapshot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) return jsonError("Projekt nicht gefunden", 404);
|
||||
|
||||
if (format === "md" || format === "markdown") {
|
||||
const filename = exportFileName(project.slug, new Date(), "md");
|
||||
return new Response(toMarkdown(project), {
|
||||
headers: {
|
||||
"Content-Type": "text/markdown; charset=utf-8",
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const payload = buildExportFile([project]);
|
||||
const filename = exportFileName(project.slug, new Date(), "json");
|
||||
|
||||
return new Response(JSON.stringify(payload, null, 2), {
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user