16 KiB
title
| title |
|---|
| Backup & Restore |
Backup & Restore
Overview
Die Backup & Restore-API ermöglicht das Exportieren eines kompletten Projekts (Collections + Media) als ZIP-Datei sowie den Import auf dem gleichen oder einem anderen Server.
Nur Admin-User können diese Endpunkte nutzen (Server-Token, Projekt-Token mit allowBackup,
oder JWT mit AdminRole).
Authentifizierung
Dreistufiges Auth-Modell (identisch mit anderen Admin-Endpunkten wie reload):
1. Server-Level Admin Token
api:
adminTokens:
- token: "backup-agent-token"
label: "ci-backup"
permissions:
- project.backup # Nur Backup/Restore
# - project.* # Alle Projekt-Operationen inkl. Backup
Gesendet via X-Admin-Token Header oder Authorization: Bearer <token>.
2. Projekt-Level Admin Token
# In der Projekt-Config (config.yml)
admin:
tokens:
- token: "ADMINTOKEN"
allowReload: true
allowRegenerateSearch: true
allowBackup: true # NEU: erlaubt Backup/Restore
Das Feld allowBackup steuert gezielt, ob dieser Token Backup/Restore verwenden darf.
3. JWT Login mit AdminRole
User mit role: 0 (AdminRole) können Backup/Restore ausführen.
Endpunkte
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | /_/:project/_/admin/backup |
Backup als ZIP herunterladen |
| GET | /_/:project/_/admin/backup/download/:file |
Temp-ZIP aus SSE-Download abholen |
| POST | /_/:project/_/admin/backup |
ZIP hochladen & analysieren → restoreToken (SSE via ?sse=1) |
| POST | /_/:project/_/admin/backup/:restoreToken |
Wiederherstellung ausführen |
| DELETE | /_/:project/_/admin/backup/:restoreToken |
Temp-Daten manuell aufräumen |
1. Backup Download
GET /_/:project/_/admin/backup?collections=contacts,pages&sse=1
| Query-Param | Default | Beschreibung |
|---|---|---|
collections |
(alle) | Komma-separierte Collection-Namen (selektiver Export) |
sse |
0 |
1 = Fortschritt als SSE-Stream, 0 = direktes ZIP |
Direkter Download (sse=0)
Response: Content-Type: application/zip mit folgender Struktur:
backup-<namespace>-<timestamp>.zip
├── backup-info.json
├── collections/
│ ├── <collection>.json # Array aller Dokumente
│ └── ...
└── media/ # Nur Dateien im konfigurierten uploadPath
└── <collection>/
└── <docId>/
└── <fieldName>/
└── <filename>
Wichtig: Es werden NUR Media-Dateien exportiert, die tatsächlich im konfigurierten
uploadPath der Collection existieren. Fehlende Dateien werden übersprungen und geloggt.
Die Datei backup-info.json enthält Metadaten, die beim Import zur Schema-Validierung
verwendet werden:
{
"type": "tibi-backup",
"version": 1,
"sourceNamespace": "mein-projekt",
"createdAt": "2026-06-24T12:00:00Z",
"collections": {
"contacts": {
"count": 42,
"fields": ["email", "firstname", "lastname", "phone"]
}
}
}
SSE-Modus (sse=1)
Response-Type: text/event-stream mit folgenden Events:
event: progress
data: {"phase":"init","totalCollections":3}
event: result
data: {"phase":"done","downloadUrl":"/_/:project/_/admin/backup/download/<token>","size":52428800,"expiresAt":"..."}
Der Client erhält eine temporäre Download-URL (TTL: 5 Minuten) und ruft die ZIP anschließend separat ab:
# Schritt 1: SSE-Download initiieren
curl -H "X-Admin-Token: $TOKEN" \
"http://localhost:8080/api/v1/_/myproject/_/admin/backup?sse=1"
# Schritt 2: ZIP vom temp-Link abholen
curl -H "X-Admin-Token: $TOKEN" \
-o backup.zip \
"http://localhost:8080/api/v1/_/myproject/_/admin/backup/download/<token>"
Der Download-Link ist One-Time-Use — ein zweiter Request erhält 404.
2. Backup Upload & Analyse (Phase 1)
POST /_/:project/_/admin/backup
Content-Type: multipart/form-data
Body: Feld file = backup.zip
| Query-Param | Default | Beschreibung |
|---|---|---|
sse |
0 |
1 = Fortschritt via SSE streamen |
Direkter Modus (sse=0)
Response (200):
{
"restoreToken": "a1b2c3d4e5f6...",
"sourceNamespace": "quelle-projekt",
"createdAt": "2026-06-24T12:00:00Z",
"expiresAt": "2026-06-24T13:00:00Z",
"collections": {
"contacts": {
"documentCount": 42,
"mediaFileCount": 5,
"backupFields": ["email","firstname","lastname","phone"],
"targetFields": ["email","firstname","lastname","phone","department"],
"warnings": [
"target has new fields not in backup: [department]"
],
"exists": true
},
"legacy_old": {
"documentCount": 7,
"mediaFileCount": 0,
"backupFields": ["data"],
"targetFields": null,
"warnings": ["collection \"legacy_old\" does not exist in target project"],
"exists": false
}
}
}
In dieser Phase werden KEINE Daten geschrieben. Das ZIP wird nur entpackt und analysiert.
Der restoreToken referenziert das Temp-Verzeichnis für die nächste Phase.
SSE-Modus (sse=1)
Response-Type: text/event-stream mit folgenden Events:
event: progress
data: {"phase":"init","restoreToken":"a1b2c3d4e5f6..."}
event: progress
data: {"phase":"save"}
event: progress
data: {"phase":"extract"}
event: progress
data: {"phase":"validate"}
event: progress
data: {"phase":"analyze","totalCollections":2}
event: result
data: {"phase":"done","restoreToken":"a1b2c3d4e5f6...","sourceNamespace":"quelle-projekt","createdAt":"2026-06-24T12:00:00Z","expiresAt":"2026-06-24T13:00:00Z","collections":{...}}
Schema-Kompatibilitäts-Prüfung
Die Prüfung ist tolerant — sie warnt, blockiert aber nicht:
| Situation | Verhalten |
|---|---|
| Backup & Ziel identisch | Kein Warning |
| Backup hat Felder, Ziel nicht | Warning (MongoDB speichert sie trotzdem) |
| Ziel hat Felder, Backup nicht | Warning (Dokumente bekommen null/leer) |
| Collection existiert nicht im Ziel | exists: false, wird beim Restore geskippt |
3. Restore ausführen (Phase 2)
POST /_/:project/_/admin/backup/:restoreToken?collections=contacts&merge=true&sse=1
| Query-Param | Default | Beschreibung |
|---|---|---|
collections |
(alle) | Welche Collections aus dem Backup importieren |
merge |
false |
true = Dokumente updaten, Orphans behalten; false = exakter 1:1-Abgleich (Orphans löschen) |
sse |
0 |
1 = Fortschritt via SSE streamen |
Direkter Restore (sse=0)
Response (200):
{
"restoredCollections": ["contacts"],
"skippedCollections": ["legacy_old"],
"totalDocuments": 42,
"mediaFiles": 5
}
Restore-Modi
merge=false (Default) — Exakter 1:1-Abgleich:
- Jedes Dokument aus dem Backup wird via atomarem Upsert (
ReplaceOne) eingefügt oder ersetzt - Nach dem Import werden alle Dokumente gelöscht, die vor dem Restore existierten, aber nicht im Backup enthalten sind (Orphans)
- Ergebnis: Die Collection ist eine exakte 1:1-Kopie des Backups — jedes Feld, jeder BSON-Typ
- Kein Zeitfenster, in dem die Collection leer ist (kein Pre-Delete mehr)
- Bricht der Import ab, sind bereits verarbeitete Docs korrekt, unverarbeitete unangetastet
merge=true — Überschreiben, Orphans behalten:
- Existierende Dokumente mit gleicher
_idwerden ersetzt (atomares Upsert) - Neue
_ids werden angelegt - Dokumente im Ziel, die nicht im Backup sind, bleiben erhalten
- Geeignet für partielle Updates / Migration einzelner Collections
SSE-Modus (sse=1)
event: progress
data: {"phase":"init"}
event: progress
data: {"phase":"collection","collection":"contacts","action":"importing"}
event: result
data: {"restoredCollections":["contacts"],"skippedCollections":[],"totalDocuments":42,"mediaFiles":5,"phase":"done"}
4. Temp-Daten aufräumen
DELETE /_/:project/_/admin/backup/:restoreToken
Explizites Cleanup des Temp-Verzeichnisses. Antwort: {"message":"ok"}.
Automatisches Cleanup: Ein Background-Job (5-Minuten-Takt) löscht Temp-Verzeichnisse, deren TTL abgelaufen ist:
- Restore-Temp-Dirs: 1 Stunde TTL
- Download-ZIPs: 5 Minuten TTL
Typischer Workflow
Szenario A: Projekt von Staging nach Production kopieren
# 1. Auf Staging: Backup herunterladen
curl -H "X-Admin-Token: $STAGING_TOKEN" \
-o staging-backup.zip \
"https://staging.example.com/api/v1/_/myproject/_/admin/backup"
# 2. Auf Production: Backup hochladen & analysieren
curl -H "X-Admin-Token: $PROD_TOKEN" \
-F "file=@staging-backup.zip" \
"https://prod.example.com/api/v1/_/myproject/_/admin/backup"
# → {"restoreToken":"abc...","collections":{...}}
# 3. Prüfung der Analyse-Warnings, dann Restore ausführen
curl -H "X-Admin-Token: $PROD_TOKEN" \
-X POST \
"https://prod.example.com/api/v1/_/myproject/_/admin/backup/abc..."
# → {"restoredCollections":["contacts","pages"],"totalDocuments":84,"mediaFiles":12}
Szenario B: Nur eine Collection aktualisieren (Selective Restore)
# 1. Full Backup herunterladen (enthält alle Collections)
curl -H "X-Admin-Token: $TOKEN" -o full-backup.zip "..."
# 2. Upload & Analyse
RESTORE_TOKEN=$(curl -H "X-Admin-Token: $TOKEN" -F "file=@full-backup.zip" "..." | jq -r .restoreToken)
# 3. Nur "contacts" wiederherstellen
curl -H "X-Admin-Token: $TOKEN" -X POST \
"http://.../_/myproject/_/admin/backup/$RESTORE_TOKEN?collections=contacts"
Szenario C: Mit Merge updaten (bestehende Daten erhalten)
# merge=true: Backup-Docs ersetzen bestehende, neue hinzufügen,
# aber Docs die nur im Ziel existieren bleiben erhalten
curl -H "X-Admin-Token: $TOKEN" -X POST \
"http://.../_/myproject/_/admin/backup/$TOKEN?merge=true"
Szenario D: Mit SSE-Fortschritt (große Projekte)
# Download mit Fortschrittsanzeige
curl -H "X-Admin-Token: $TOKEN" -N \
"http://.../_/myproject/_/admin/backup?sse=1"
# → event:progress + event:result mit downloadUrl
# Restore mit Fortschrittsanzeige
curl -H "X-Admin-Token: $TOKEN" -N -X POST \
"http://.../_/myproject/_/admin/backup/$TOKEN?sse=1"
ZIP-Struktur (Referenz)
backup-<namespace>-<timestamp>.zip
├── backup-info.json # Metadaten + Schema-Fingerprint
├── collections/
│ ├── contacts.json # Array aller Dokumente (mit _id)
│ └── pages.json
└── media/ # Nur Dateien im uploadPath
└── contacts/
└── <documentId>/
└── <fieldName>/
└── <filename>
Kein project.json oder config.json — das Projekt muss auf dem Zielsystem bereits existieren.
Media-Erkennung
Beim Export werden nur Dateien berücksichtigt, die:
- Über ein
file- oderfile[]-Feld im Dokument referenziert sind - Tatsächlich im konfigurierten
uploadPathexistieren (os.Stat→ OK) - Innerhalb des
uploadPathliegen (Path-Traversal-Schutz)
Algorithmus:
Für jedes Dokument einer Collection:
Für jedes Feld vom Typ "file" oder "file[]":
Relativer Pfad aus Feldwert extrahieren
Absoluter Pfad = <uploadPath> / <relativer Pfad>
os.Stat() → existiert → in ZIP aufnehmen
Existiert nicht → skip, Log-Warnung
Sicherheit
| Maßnahme | Beschreibung |
|---|---|
| Auth | Server-Token, Projekt-Token, oder JWT AdminRole erforderlich |
| Path Traversal | filepath.Clean + strings.HasPrefix bei ZIP-Extraktion und Media-Kopie |
| Temp-Files | Automatischer Cleanup nach TTL, keine Akkumulation |
| One-Time Download | SSE-Download-Links sind nach einmaligem Abruf ungültig |
| Media außerhalb uploadPath | Pfade außerhalb des konfigurierten uploadPath werden ignoriert |
Fehlerbehandlung
| HTTP-Status | Ursache |
|---|---|
| 400 | Keine Datei hochgeladen, ungültiges ZIP, fehlende backup-info.json, falscher Typ/Version |
| 403 | Fehlende Berechtigung (project.backup oder allowBackup) |
| 404 | restoreToken nicht gefunden oder abgelaufen, Projekt nicht gefunden |
| 410 | Download-Link abgelaufen (nur bei /download/:file) |
| 500 | Interner Fehler (DB-Lesefehler, ZIP-Erstellung fehlgeschlagen) |
Temp-Datei-Management
Upload (Phase 1) → /tmp/tibi-backup/<restoreToken>/ (TTL: 1h)
Restore (Phase 2) → importiert, dann gelöscht
DELETE /:restoreToken → sofort gelöscht
TTL-Ablauf → Background-Cleanup (alle 5 Min)
SSE Download → /tmp/tibi-backup-download/<token>.zip (TTL: 5 Min)
GET /download/:file → ausgeliefert + sofort gelöscht
Konfiguration
Server-Level Token (config.yml)
api:
adminTokens:
- token: "backup-token"
label: "backup-agent"
permissions:
- project.backup
project.* (Wildcard) inkludiert automatisch project.backup.
Projekt-Level Token
admin:
tokens:
- token: "PROJEKT-TOKEN"
allowBackup: true
Keine Projekt-Config-Änderung nötig
Der Backup/Restore-Vorgang benötigt keine Änderungen an der Projekt-Config. Collections, Felder, uploadPath etc. werden aus der bestehenden Konfiguration gelesen.
Implementierung
Dateien
| Datei | Inhalt |
|---|---|
internal/controller/admin_backup.go |
Alle Handler (Download, Analyze, Restore, Cleanup, SSE, Temp-Stores) |
internal/controller/admin_backup_auth_test.go |
12 Auth-Tests |
internal/controller/admin_backup_analyze_test.go |
8 Analyze- & Download-Tests |
internal/controller/admin_backup_restore_test.go |
9 Restore-Tests |
internal/controller/admin_backup_integration_test.go |
3 SSE/E2E-Tests |
internal/controller/setup.go |
Routen-Registrierung |
internal/models/project.go |
AdminToken.AllowBackup Feld |
cmd/server/main.go |
StartBackupCleanupJob() Aufruf |
Test-Statistik
33 Tests (alle grün):
- 12 Auth-Tests (Server-Token, Projekt-Token, JWT, Berechtigungen)
- 6 Analyze-Tests (ZIP-Validierung, Schema-Diffs, unbekannte Collections)
- 2 Download-Tests (direkt, selektiv)
- 9 Restore-Tests (Import, Merge, Selective, Media-Copy, Cleanup)
- 3 SSE/E2E-Tests (SSE-Download, DownloadFile, SSE-Restore)
- 1 E2E-Test (skipped — Mock-Projekt-API-Limitierung)
Keine Regression auf bestehenden Admin-Tests (TestAdminReload, TestAdminAuthReload etc.).
Betrieb: Speicherverhalten seit Streaming-Umbau (2026-10)
Erstellung und Wiederherstellung von Snapshots und Backups laufen in
O(Batch) statt O(Collection): Keyset-Pagination (_id $gt, Sort _id),
Datei-/ZIP-Streaming, sequenziell pro Collection. RAM ist keine Grenze mehr —
auch 10-GB-Collections (z. B. Outdoorworks: productVariant 91k × ~117 KB mit
_search-Bloat) laufen mit konstantem Heap.
Batch-Konstanten (internal/controller/snapshot_stream.go)
| Konstante | Wert | Wirkung |
|---|---|---|
SnapshotBatchDocs |
500 | Docs pro DB-Batch und Restore-Batch |
SnapshotBatchBytes |
8 MiB | reserviert (Byte-Cap, greift pro großem Einzeldoc) |
SnapshotBufSize |
64 KiB | bufio-Puffer für Snapshot-Dateien |
OrphanMapThreshold |
200.000 | darunter In-RAM-Orphan-Set, darüber Temp-Collection in Ziel-DB |
orphanChunkSize |
500 | $in-Chunks für Orphan-Deletes |
Empfehlungen
GOMEMLIMITsetzen (z. B.GOMEMLIMIT=512MiBim Container): begrenzt GC-Druck, Streaming braucht es nicht — aber es schützt vor Nachbar-Last.- Jeder Snapshot-Run loggt
snapshot <id>: done documents=… heapGrowthMB=…(heapGrowthMB= Heap-Wachstum über den Lauf, kein Peak; Peak ist per Konstruktion batch-begrenzt). - Retention
maxCountbehält die neuesten Einträge (Bugfix 2026-10-15: zuvor wurden fälschlich die neuesten gelöscht).
Nachweise (Tests)
TestExportStreaming_PeakScalesFlat/TestStreamRestore_PeakScalesFlat: 10× Docs ⇒ kein 10× Peak (Kontrollmessung Alt-Pfad liegt ein Mehrfaches darüber).- E2E Roundtrips (Mock + Real-Mongo): Snapshot NonMerge/Merge, Backup Restore-Matrix, Download-Pipelines.
- OOM-Fixture:
testdata/outdoorworks-oom/(echte_search-Form, Stats,dump-full.shfür den 8-GB-Voll-Dump nach/WM_Dev/.cortex/mcp-tmp/).