internal/tenantbackup: datenbank-scharfes pg_dump/pg_restore statt BAK-01s Cluster-weitem pg_basebackup - bei Modell C (TEN-01, physisch isolierte DB je Mandant) wuerde ein Cluster-Restore zwangslaeufig ALLE Mandanten ueberschreiben. Objekt-Seite nutzt BAK-02 direkt (Mandanten haben eigene Buckets/Pfad-Roots). Eigene Postgres-Rolle nexarch_tenantbackup (CREATEDB, kein Superuser, getrennt von nexarch_backup). Zwei-Tenant-Isolation real in beide Richtungen bewiesen (Markerwert-Nachweis), JSONL-Protokoll fuer Sicherung UND Restore. Realer End-zu-Ende-Lauf ueber tenantbackup-cli auf 131.
77 lines
2.1 KiB
Go
77 lines
2.1 KiB
Go
package tenantbackup
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// Operation unterscheidet Sicherung und Wiederherstellung im Protokoll.
|
|
type Operation string
|
|
|
|
const (
|
|
OpBackupDB Operation = "backup_database"
|
|
OpRestoreDB Operation = "restore_database"
|
|
OpBackupObj Operation = "backup_objects"
|
|
OpRestoreObj Operation = "restore_objects"
|
|
)
|
|
|
|
// LogEntry ist EIN Protokolleintrag (Akzeptanzkriterium 3: Tenant-
|
|
// Sicherung UND -Restore vollständig protokolliert).
|
|
type LogEntry struct {
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Operation Operation `json:"operation"`
|
|
TenantID string `json:"tenant_id"`
|
|
Source string `json:"source,omitempty"` // Dump-Pfad oder Snapshot-ID
|
|
Target string `json:"target,omitempty"` // Zieldatenbank oder Zielverzeichnis
|
|
Result string `json:"result"` // "ok" oder Fehlertext
|
|
}
|
|
|
|
// AppendLog hängt entry an die JSONL-Protokolldatei an (append-only,
|
|
// nichts wird überschrieben).
|
|
func AppendLog(logPath string, entry LogEntry) error {
|
|
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("tenantbackup: protokolldatei öffnen: %w", err)
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
line, err := json.Marshal(entry)
|
|
if err != nil {
|
|
return fmt.Errorf("tenantbackup: protokolleintrag kodieren: %w", err)
|
|
}
|
|
if _, err := f.Write(append(line, '\n')); err != nil {
|
|
return fmt.Errorf("tenantbackup: protokolleintrag schreiben: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReadLog liest die vollständige Protokollhistorie.
|
|
func ReadLog(logPath string) ([]LogEntry, error) {
|
|
data, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("tenantbackup: protokolldatei lesen: %w", err)
|
|
}
|
|
var entries []LogEntry
|
|
start := 0
|
|
for i := 0; i < len(data); i++ {
|
|
if data[i] == '\n' {
|
|
line := data[start:i]
|
|
start = i + 1
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
var e LogEntry
|
|
if err := json.Unmarshal(line, &e); err != nil {
|
|
return nil, fmt.Errorf("tenantbackup: protokollzeile dekodieren: %w", err)
|
|
}
|
|
entries = append(entries, e)
|
|
}
|
|
}
|
|
return entries, nil
|
|
}
|