**Warum keine Wiederherstellung funktionierte.** Der Dienst laeuft mit
`ProtectSystem=strict` und `ReadWritePaths` nur auf Repository und
Sicherungsordner — alles andere ist fuer ihn schreibgeschuetzt. Jedes Ziel
ausserhalb endete mit "mkdir: permission denied", und zwar **nach** der
Vorabpruefung. `/tmp` scheiterte anders: Mit `PrivateTmp=yes` hat der Dienst ein
eigenes /tmp, und was dort landet, sieht man von aussen gar nicht.
`setup.sh` legt jetzt `/srv/syncova-restore` an und traegt es in
`ReadWritePaths` ein; `--wiederherstellungsziel` ergaenzt weitere. Der Ort liegt
unter /srv und nicht unter /var/lib — Letzteres steht auf der Sperrliste des
Zielschutzes. Beide Regeln zugleich zu erfuellen laesst genau /srv uebrig; das
ist mir erst aufgefallen, nachdem ich die Flaeche zunaechst falsch gelegt hatte
und der eigene Zielschutz sie ablehnte.
**Zwei neue Endpunkte** (Vertrag entsprechend erweitert):
- `GET /filesystem/browse` — Verzeichnisse mit der Angabe, ob der **Dienst**
dort schreiben darf. **Gemessen** durch eine Probedatei, nicht aus den
Rechtebits geraten: Unter ProtectSystem=strict sagen die Bits nichts ueber
das aus, was der Namensraum zulaesst. Gesperrte Orte werden gezeigt, nicht
versteckt — sonst bliebe offen, warum ein Pfad fehlt.
- `GET /backups/{id}/contents` — das Manifest als Ebene eines Baums. Der Baum
entsteht aus den **Pfaden**, nicht aus Verzeichniseintraegen: Ein Manifest
kann eine Datei enthalten, deren Elternverzeichnis nicht als eigener Eintrag
vorliegt, und wer nur `directory`-Eintraege auflistet, verliert ganze
Teilbaeume. Durch Mutation bestaetigt.
**Auswahl statt Textfeld.** Der Assistent hat jetzt einen Ordnerbaum fuer das
Ziel und einen Browser fuer den Backup-Inhalt. Ordner **und** einzelne Dateien
lassen sich waehlen; beides geht als `path_prefix` in die Anfrage, weil der
Server auf Gleichheit oder Praefix mit Verzeichnisgrenze vergleicht. Bewusste
Grenze: eine Auswahl je Lauf — eine Liste kennt die API nicht, und mehrere
Laeufe vorzutaeuschen ergaebe mehrere Ausgaenge, die niemand mehr erklaeren
kann.
**Integritaetslauf.** "can't access property toLocaleString, chunks_checked is
undefined" — die Ergebnisse liegen unter `details`, und die Felder heissen
`missing_chunks`/`corrupted_chunks`, nicht umgekehrt. Betrifft alle vier
Pruefendpunkte; sie tragen dieselbe Huelle. Derselbe Fehler wie bei
/retention-policies: die Antwortform angenommen statt geprueft.
**Geist Mono liegt jetzt im Paket** (drei Schnitte, 128 KB, OFL-Lizenz dabei).
Ausgeliefert vom eigenen Ursprung — das verlangt die CSP, und ein
Backup-Server, dessen Oberflaeche von einem CDN abhaengt, waere auch ohne CSP
falsch. `font-display: swap`, damit der Text sofort steht.
Nachgewiesen gegen Debian 12: Vollwiederherstellung (5 Dateien), nur ein Ordner
(2 Dateien), nur eine Datei (1 Datei) — alle drei bitgenau. Der Server nennt
/srv/syncova-restore als beschreibbar und /etc, /usr, /var als gesperrt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
91 lines
3.4 KiB
Go
91 lines
3.4 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/syncova/syncova/packages/repository"
|
|
)
|
|
|
|
// TestCollectLevelBuildsTreeWithoutDirectoryEntries haelt fest, dass der Baum
|
|
// aus den Pfaden entsteht und nicht aus Verzeichniseintraegen.
|
|
//
|
|
// Ein Manifest kann eine Datei enthalten, deren Elternverzeichnis nicht als
|
|
// eigener Eintrag vorliegt — etwa bei einer Quelle, die nur Dateien meldet. Wer
|
|
// nur die Eintraege vom Typ "directory" auflistet, verliert dann ganze
|
|
// Teilbaeume, und die Datei ist ueber die Oberflaeche nicht mehr erreichbar.
|
|
func TestCollectLevelBuildsTreeWithoutDirectoryEntries(testInstance *testing.T) {
|
|
manifestEntries := []repository.ManifestEntry{
|
|
// Kein Eintrag fuer "berichte" selbst.
|
|
{Path: "berichte/2026/jahr.pdf", EntryType: "file", SizeBytes: 900},
|
|
{Path: "berichte/2025/jahr.pdf", EntryType: "file", SizeBytes: 100},
|
|
{Path: "notiz.txt", EntryType: "file", SizeBytes: 6},
|
|
}
|
|
|
|
rootLevel := collectLevel(manifestEntries, "")
|
|
|
|
if len(rootLevel) != 2 {
|
|
testInstance.Fatalf("erwartet 2 Eintraege auf der Wurzel, erhalten %d", len(rootLevel))
|
|
}
|
|
|
|
// Verzeichnisse stehen vorn.
|
|
if !rootLevel[0].IsDirectory || rootLevel[0].Name != "berichte" {
|
|
testInstance.Errorf("das Verzeichnis berichte fehlt oder steht nicht vorn: %+v", rootLevel[0])
|
|
}
|
|
|
|
// Die Kennzahlen summieren den ganzen Teilbaum: Ein Betreiber will vor dem
|
|
// Zurueckholen wissen, wie viel an einem Ordner haengt.
|
|
if rootLevel[0].ChildCount != 2 || rootLevel[0].TotalBytes != 1000 {
|
|
testInstance.Errorf("Kennzahlen des Ordners falsch: %d Objekte, %d Byte",
|
|
rootLevel[0].ChildCount, rootLevel[0].TotalBytes)
|
|
}
|
|
|
|
// Und eine Ebene tiefer erscheinen die Jahresordner.
|
|
deeperLevel := collectLevel(manifestEntries, "berichte")
|
|
|
|
if len(deeperLevel) != 2 {
|
|
testInstance.Fatalf("erwartet 2 Jahresordner, erhalten %d", len(deeperLevel))
|
|
}
|
|
}
|
|
|
|
// TestCollectLevelRespectsDirectoryBoundary haelt die Verzeichnisgrenze fest.
|
|
//
|
|
// "dokumente" darf nicht auch "dokumentation" treffen — sonst holte eine
|
|
// Wiederherstellung Daten zurueck, die niemand ausgewaehlt hat.
|
|
func TestCollectLevelRespectsDirectoryBoundary(testInstance *testing.T) {
|
|
manifestEntries := []repository.ManifestEntry{
|
|
{Path: "dokumente/a.txt", EntryType: "file", SizeBytes: 1},
|
|
{Path: "dokumentation/b.txt", EntryType: "file", SizeBytes: 1},
|
|
}
|
|
|
|
level := collectLevel(manifestEntries, "dokumente")
|
|
|
|
if len(level) != 1 || level[0].Name != "a.txt" {
|
|
testInstance.Errorf("die Verzeichnisgrenze wird nicht beachtet: %+v", level)
|
|
}
|
|
}
|
|
|
|
// TestCollectLevelKeepsFileMetadata prueft die Angaben je Datei.
|
|
func TestCollectLevelKeepsFileMetadata(testInstance *testing.T) {
|
|
modificationTime := time.Date(2026, 8, 18, 10, 0, 0, 0, time.UTC)
|
|
|
|
level := collectLevel([]repository.ManifestEntry{
|
|
{Path: "notiz.txt", EntryType: "file", SizeBytes: 42, Mode: "0644", ModifiedAt: modificationTime},
|
|
}, "")
|
|
|
|
if len(level) != 1 {
|
|
testInstance.Fatalf("erwartet einen Eintrag, erhalten %d", len(level))
|
|
}
|
|
|
|
// Der Pfad ist genau der Wert, den eine Wiederherstellung als
|
|
// `path_prefix` erwartet — eine Abweichung faellt sonst erst beim
|
|
// Zurueckschreiben auf.
|
|
if level[0].Path != "notiz.txt" || level[0].SizeBytes != 42 || level[0].Mode != "0644" {
|
|
testInstance.Errorf("Angaben der Datei unvollstaendig: %+v", level[0])
|
|
}
|
|
|
|
if level[0].ModifiedAt != "2026-08-18T10:00:00Z" {
|
|
testInstance.Errorf("Zeitstempel falsch: %q", level[0].ModifiedAt)
|
|
}
|
|
}
|