Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
83 lines
2.4 KiB
Go
83 lines
2.4 KiB
Go
package reports
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// WriteJSON schreibt einen Bericht als JSON.
|
|
//
|
|
// Das Modell wird unveraendert ausgegeben. Eine eigene Darstellung fuer die
|
|
// Ausgabe waere ein zweiter Ort, an dem eine Kennzahl ihr Feld „gemessen"
|
|
// verlieren koennte.
|
|
func WriteJSON(outputWriter io.Writer, report *Report) error {
|
|
jsonEncoder := json.NewEncoder(outputWriter)
|
|
jsonEncoder.SetIndent("", " ")
|
|
|
|
return jsonEncoder.Encode(report)
|
|
}
|
|
|
|
// Write gibt einen Bericht im gewaehlten Format aus.
|
|
func Write(outputWriter io.Writer, report *Report, outputFormat Format) error {
|
|
switch outputFormat {
|
|
case FormatCSV:
|
|
return WriteCSV(outputWriter, report)
|
|
case FormatPDF:
|
|
return WritePDF(outputWriter, report)
|
|
case FormatJSON:
|
|
return WriteJSON(outputWriter, report)
|
|
default:
|
|
return fmt.Errorf("das format %q ist unbekannt", outputFormat)
|
|
}
|
|
}
|
|
|
|
// FileName bildet den Dateinamen eines Berichts.
|
|
//
|
|
// Enthalten sind Art und Erzeugungszeitpunkt. Ohne den Zeitpunkt liegen nach
|
|
// drei Abrufen drei Dateien „bericht.pdf (2)" im Download-Ordner, und keine
|
|
// laesst sich mehr zuordnen.
|
|
func FileName(report *Report, outputFormat Format) string {
|
|
return fmt.Sprintf("syncova-%s-%s.%s", report.Type,
|
|
report.GeneratedAt.UTC().Format("20060102-150405"), outputFormat.FileExtension())
|
|
}
|
|
|
|
// SanitizeFileName entfernt Zeichen, die in einem Dateinamen stoeren.
|
|
//
|
|
// Der Name entsteht zwar aus festen Bestandteilen, geht aber in einen
|
|
// Content-Disposition-Kopf ein. Ein Anfuehrungszeichen oder ein Zeilenumbruch
|
|
// darin liesse sich zum Einschleusen weiterer Kopffelder nutzen.
|
|
func SanitizeFileName(fileName string) string {
|
|
var sanitizedName strings.Builder
|
|
|
|
for _, nameRune := range fileName {
|
|
switch {
|
|
case nameRune >= 'a' && nameRune <= 'z',
|
|
nameRune >= 'A' && nameRune <= 'Z',
|
|
nameRune >= '0' && nameRune <= '9',
|
|
nameRune == '-', nameRune == '_', nameRune == '.':
|
|
sanitizedName.WriteRune(nameRune)
|
|
default:
|
|
sanitizedName.WriteRune('-')
|
|
}
|
|
}
|
|
|
|
return sanitizedName.String()
|
|
}
|
|
|
|
// newReport legt einen Bericht mit den Kopfangaben einer Definition an.
|
|
func newReport(definition Definition, periodFrom, periodTo time.Time, generatedBy string) *Report {
|
|
return &Report{
|
|
Type: definition.Type,
|
|
Title: definition.Title,
|
|
Description: definition.Description,
|
|
PeriodFrom: periodFrom,
|
|
PeriodTo: periodTo,
|
|
GeneratedAt: time.Now().UTC(),
|
|
GeneratedBy: generatedBy,
|
|
Sections: make([]Section, 0, 4),
|
|
}
|
|
}
|