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.3 KiB
Go
83 lines
2.3 KiB
Go
package reports
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// FormatBytes stellt eine Datenmenge lesbar dar.
|
|
//
|
|
// Binaerpraefixe, weil Speicher so verkauft und angezeigt wird und ein Bericht
|
|
// nicht gegen die Anzeige des Betriebssystems arbeiten soll.
|
|
func FormatBytes(byteCount float64) string {
|
|
const unitStep = 1024.0
|
|
|
|
if byteCount < unitStep {
|
|
return fmt.Sprintf("%.0f B", byteCount)
|
|
}
|
|
|
|
unitNames := []string{"KiB", "MiB", "GiB", "TiB", "PiB"}
|
|
scaledValue := byteCount / unitStep
|
|
|
|
for _, unitName := range unitNames {
|
|
if scaledValue < unitStep {
|
|
return fmt.Sprintf("%.1f %s", scaledValue, unitName)
|
|
}
|
|
|
|
scaledValue /= unitStep
|
|
}
|
|
|
|
return fmt.Sprintf("%.1f EiB", scaledValue)
|
|
}
|
|
|
|
// FormatDuration stellt eine Dauer in Sekunden lesbar dar.
|
|
func FormatDuration(seconds float64) string {
|
|
if seconds < 1 {
|
|
return fmt.Sprintf("%.0f ms", seconds*1000)
|
|
}
|
|
|
|
wholeDuration := time.Duration(seconds * float64(time.Second))
|
|
|
|
switch {
|
|
case wholeDuration < time.Minute:
|
|
return fmt.Sprintf("%.1f s", seconds)
|
|
case wholeDuration < time.Hour:
|
|
return fmt.Sprintf("%d min %d s", int(wholeDuration.Minutes()),
|
|
int(wholeDuration.Seconds())%60)
|
|
case wholeDuration < 24*time.Hour:
|
|
return fmt.Sprintf("%d h %d min", int(wholeDuration.Hours()),
|
|
int(wholeDuration.Minutes())%60)
|
|
default:
|
|
return fmt.Sprintf("%d Tage %d h", int(wholeDuration.Hours())/24,
|
|
int(wholeDuration.Hours())%24)
|
|
}
|
|
}
|
|
|
|
// FormatPercent stellt einen Anteil dar.
|
|
//
|
|
// Ein kleiner Wert wird als „< 0,1 %" ausgewiesen, nicht als „0,0 %": Null
|
|
// Prozent liest sich wie „nichts vorhanden" — dieselbe Regel wie in der
|
|
// Uebersicht (Phase 12).
|
|
func FormatPercent(percentValue float64) string {
|
|
if percentValue > 0 && percentValue < 0.1 {
|
|
return "< 0,1 %"
|
|
}
|
|
|
|
return strings.Replace(fmt.Sprintf("%.1f %%", percentValue), ".", ",", 1)
|
|
}
|
|
|
|
// FormatTimestamp stellt einen Zeitpunkt in UTC dar.
|
|
//
|
|
// Immer mit ausgeschriebener Zeitzone: Ein Bericht wandert zwischen Standorten,
|
|
// und „14:00" ohne Zone hat je nach Leser drei Bedeutungen.
|
|
func FormatTimestamp(pointInTime time.Time) string {
|
|
return pointInTime.UTC().Format("2006-01-02 15:04:05 UTC")
|
|
}
|
|
|
|
// trimNumber stellt eine Zahl ohne ueberfluessige Nachkommastellen dar.
|
|
func trimNumber(numericValue float64) string {
|
|
return strconv.FormatFloat(numericValue, 'f', -1, 64)
|
|
}
|