syncova-backup/packages/reports/export_csv.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
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>
2026-08-17 09:10:54 +02:00

162 lines
4.4 KiB
Go

package reports
import (
"encoding/csv"
"io"
)
// WriteCSV schreibt einen Bericht als CSV.
//
// CSV ist ein flaches Format, ein Bericht ist es nicht. Statt die Abschnitte
// wegzulassen, werden sie mit einer eigenen Zeile eingeleitet — so bleibt die
// Datei in jeder Tabellenkalkulation lesbar und verliert nichts.
//
// Der entscheidende Punkt steht in writeMetric: Eine ungemessene Kennzahl
// erzeugt **keine Null**. Eine Null in einer Tabellenkalkulation wird summiert,
// gemittelt und in ein Diagramm gezeichnet; sie ueberlebt jede muendliche
// Erlaeuterung, mit der man sie einordnen wollte.
func WriteCSV(outputWriter io.Writer, report *Report) error {
// Byte Order Mark: Ohne ihn zeigt Excel unter Windows aus einer
// UTF-8-Datei „Sicherungslÿufe" an. Der Bericht traegt deutsche
// Beschriftungen, die Frage stellt sich also bei jeder Datei.
if _, writeError := outputWriter.Write([]byte{0xEF, 0xBB, 0xBF}); writeError != nil {
return writeError
}
csvWriter := csv.NewWriter(outputWriter)
headerRows := [][]string{
{"Bericht", report.Title},
{"Art", string(report.Type)},
{"Zeitraum von", FormatTimestamp(report.PeriodFrom)},
{"Zeitraum bis", FormatTimestamp(report.PeriodTo)},
{"Erzeugt am", FormatTimestamp(report.GeneratedAt)},
}
if report.GeneratedBy != "" {
headerRows = append(headerRows, []string{"Erzeugt von", report.GeneratedBy})
}
for _, headerRow := range headerRows {
if writeError := csvWriter.Write(headerRow); writeError != nil {
return writeError
}
}
for _, section := range report.Sections {
if writeError := writeCSVSection(csvWriter, section); writeError != nil {
return writeError
}
}
if len(report.Notes) > 0 {
if writeError := writeCSVBlank(csvWriter); writeError != nil {
return writeError
}
if writeError := csvWriter.Write([]string{"Hinweise"}); writeError != nil {
return writeError
}
for _, note := range report.Notes {
if writeError := csvWriter.Write([]string{"", note}); writeError != nil {
return writeError
}
}
}
csvWriter.Flush()
return csvWriter.Error()
}
// writeCSVSection schreibt einen Abschnitt.
func writeCSVSection(csvWriter *csv.Writer, section Section) error {
if writeError := writeCSVBlank(csvWriter); writeError != nil {
return writeError
}
if writeError := csvWriter.Write([]string{section.Title}); writeError != nil {
return writeError
}
if len(section.Metrics) > 0 {
if writeError := csvWriter.Write([]string{"Kennzahl", "Wert", "Einheit", "Anmerkung"}); writeError != nil {
return writeError
}
for _, metric := range section.Metrics {
if writeError := csvWriter.Write(metricCSVRow(metric)); writeError != nil {
return writeError
}
}
}
for _, table := range section.Tables {
if writeError := writeCSVTable(csvWriter, table); writeError != nil {
return writeError
}
}
return nil
}
// metricCSVRow bildet die Zeile einer Kennzahl.
//
// Bei einer ungemessenen Kennzahl bleibt die Wertspalte leer und die Anmerkung
// traegt den Grund. Eine leere Zelle laesst sich nicht versehentlich summieren.
func metricCSVRow(metric Metric) []string {
if !metric.IsKnown {
return []string{metric.Label, "", string(metric.Unit), "nicht gemessen: " + metric.UnknownReason}
}
if metric.Text != "" {
return []string{metric.Label, metric.Text, "", ""}
}
// Der Rohwert, nicht die aufbereitete Form: Eine Tabellenkalkulation soll
// rechnen koennen. „1,4 GiB" ist Text, 1503238553 ist eine Zahl.
return []string{metric.Label, trimNumber(metric.Value), string(metric.Unit), ""}
}
// writeCSVTable schreibt eine Tabelle.
func writeCSVTable(csvWriter *csv.Writer, table Table) error {
if writeError := writeCSVBlank(csvWriter); writeError != nil {
return writeError
}
if writeError := csvWriter.Write([]string{table.Title}); writeError != nil {
return writeError
}
if len(table.Rows) == 0 {
return csvWriter.Write([]string{"", emptyNoticeOf(table)})
}
if writeError := csvWriter.Write(table.Columns); writeError != nil {
return writeError
}
for _, tableRow := range table.Rows {
if writeError := csvWriter.Write(tableRow); writeError != nil {
return writeError
}
}
return nil
}
// writeCSVBlank schreibt eine Trennzeile.
func writeCSVBlank(csvWriter *csv.Writer) error {
return csvWriter.Write([]string{""})
}
// emptyNoticeOf liefert den Hinweis einer leeren Tabelle.
func emptyNoticeOf(table Table) string {
if table.EmptyNotice != "" {
return table.EmptyNotice
}
return "Keine Einträge im gewählten Zeitraum."
}