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>
78 lines
2.9 KiB
TypeScript
78 lines
2.9 KiB
TypeScript
import { z } from "zod";
|
||
|
||
/**
|
||
* Austauschformat fuer Projekte.
|
||
*
|
||
* Bewusst schlank und ohne Geheimnisse:
|
||
* - keine 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.
|
||
* - keine Repo-Snapshots. Die sind reproduzierbar und blaehen die Datei auf;
|
||
* ein Abgleich nach dem Import holt sie zurueck.
|
||
* - keine Datenbank-IDs. Die gelten nur in der Instanz, aus der exportiert
|
||
* wurde.
|
||
*/
|
||
|
||
export const EXPORT_KIND = "nebula.projects";
|
||
export const EXPORT_VERSION = 1;
|
||
|
||
const STATUS = ["IDEA", "OPEN", "PLANNING", "IN_DEVELOPMENT", "DONE", "ARCHIVED"] as const;
|
||
const PROVIDERS = ["GITHUB", "GITLAB", "GITEA"] as const;
|
||
|
||
export const exportedNoteSchema = z.object({
|
||
title: z.string().max(160).nullable().optional(),
|
||
content: z.string().max(20000),
|
||
pinned: z.boolean().optional().default(false),
|
||
createdAt: z.string().optional(),
|
||
});
|
||
|
||
export const exportedConnectionSchema = z.object({
|
||
name: z.string().max(80),
|
||
provider: z.enum(PROVIDERS),
|
||
baseUrl: z.string().max(300),
|
||
});
|
||
|
||
export const exportedProjectSchema = z.object({
|
||
name: z.string().min(1).max(120),
|
||
slug: z.string().max(60).optional(),
|
||
summary: z.string().max(280).nullable().optional(),
|
||
description: z.string().max(20000).nullable().optional(),
|
||
status: z.enum(STATUS).optional().default("IDEA"),
|
||
priority: z.number().int().min(1).max(4).optional().default(2),
|
||
progress: z.number().int().min(0).max(100).optional().default(0),
|
||
accent: z.string().max(20).optional().default("violet"),
|
||
tags: z.array(z.string().max(30)).max(12).optional().default([]),
|
||
favorite: z.boolean().optional().default(false),
|
||
autoSync: z.boolean().optional().default(true),
|
||
repoUrl: z.string().max(500).nullable().optional(),
|
||
repoOwner: z.string().max(200).nullable().optional(),
|
||
repoName: z.string().max(200).nullable().optional(),
|
||
/** Nur zur Wiedererkennung – ohne Token. */
|
||
connection: exportedConnectionSchema.nullable().optional(),
|
||
notes: z.array(exportedNoteSchema).max(500).optional().default([]),
|
||
createdAt: z.string().optional(),
|
||
});
|
||
|
||
export const exportFileSchema = z.object({
|
||
kind: z.literal(EXPORT_KIND),
|
||
version: z.number().int().min(1).max(EXPORT_VERSION),
|
||
exportedAt: z.string().optional(),
|
||
app: z.string().optional(),
|
||
projects: z.array(exportedProjectSchema).min(1).max(1000),
|
||
});
|
||
|
||
export type ExportedProject = z.infer<typeof exportedProjectSchema>;
|
||
export type ExportFile = z.infer<typeof exportFileSchema>;
|
||
|
||
/** Dateiname ohne Sonderzeichen, mit Datumsstempel. */
|
||
export function exportFileName(prefix: string, date: Date, extension: string): string {
|
||
const stamp = date.toISOString().slice(0, 19).replace(/[:T]/g, "-");
|
||
const safe = prefix
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9]+/g, "-")
|
||
.replace(/^-+|-+$/g, "")
|
||
.slice(0, 40);
|
||
return `${safe || "nebula"}-${stamp}.${extension}`;
|
||
}
|