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>
143 lines
4.2 KiB
Go
143 lines
4.2 KiB
Go
package reports
|
|
|
|
// Zeichenbreiten der Schrift Helvetica in 1/1000 em.
|
|
//
|
|
// Die Werte stammen aus den Metriken der Standardschrift. Ohne sie liesse sich
|
|
// kein Zeilenumbruch rechnen und keine Tabellenspalte bemessen: Eine geschaetzte
|
|
// Durchschnittsbreite laesst „Wiederherstellung fehlgeschlagen" in die
|
|
// Nachbarspalte laufen, waehrend „111111" viel Platz uebrig liesse.
|
|
//
|
|
// Der Index ist der Zeichenwert minus 32; abgedeckt ist 32 bis 126.
|
|
var helveticaWidths = [95]int{
|
|
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, // 32-41
|
|
389, 584, 278, 333, 278, 278, 556, 556, 556, 556, // 42-51
|
|
556, 556, 556, 556, 556, 556, 278, 278, 584, 584, // 52-61
|
|
584, 556, 1015, 667, 667, 722, 722, 667, 611, 778, // 62-71
|
|
722, 278, 500, 667, 556, 833, 722, 778, 667, 778, // 72-81
|
|
722, 667, 611, 722, 667, 944, 667, 667, 611, 278, // 82-91
|
|
278, 278, 469, 556, 333, 556, 556, 500, 556, 556, // 92-101
|
|
278, 556, 556, 222, 222, 500, 222, 833, 556, 556, // 102-111
|
|
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, // 112-121
|
|
500, 334, 260, 334, 584, // 122-126
|
|
}
|
|
|
|
// boldWidthFactor naehert die Breite der fetten Schnitte an.
|
|
//
|
|
// Helvetica-Bold ist rund drei Prozent breiter als Helvetica. Eine zweite
|
|
// vollstaendige Tabelle dafuer waere Aufwand ohne Gewinn: Fett gesetzt sind hier
|
|
// nur Ueberschriften und Spaltenkoepfe, und die stehen fuer sich.
|
|
const boldWidthFactor = 1.03
|
|
|
|
// defaultGlyphWidth ist die angenommene Breite eines Zeichens ausserhalb der Tabelle.
|
|
//
|
|
// Umlaute liegen in dieser Schrift bei rund 556; das ist deutlich naeher an der
|
|
// Wahrheit als der Rueckgriff auf null, der eine Zeile beliebig lang erscheinen
|
|
// liesse.
|
|
const defaultGlyphWidth = 556
|
|
|
|
// textWidth misst einen Text in Punkten.
|
|
func textWidth(text string, font pdfFont, fontSize float64) float64 {
|
|
totalWidth := 0
|
|
|
|
for _, textRune := range text {
|
|
glyphIndex := int(textRune) - 32
|
|
|
|
if glyphIndex >= 0 && glyphIndex < len(helveticaWidths) {
|
|
totalWidth += helveticaWidths[glyphIndex]
|
|
|
|
continue
|
|
}
|
|
|
|
totalWidth += defaultGlyphWidth
|
|
}
|
|
|
|
measuredWidth := float64(totalWidth) * fontSize / 1000
|
|
|
|
if font == fontBold {
|
|
return measuredWidth * boldWidthFactor
|
|
}
|
|
|
|
return measuredWidth
|
|
}
|
|
|
|
// wrapText bricht einen Text auf die vorgegebene Breite um.
|
|
//
|
|
// Umgebrochen wird an Leerzeichen. Ein Wort, das allein schon zu breit ist,
|
|
// bleibt ungebrochen stehen und ragt heraus — es zeichenweise zu zerlegen
|
|
// zerrisse Pfade und Kennungen genau dort, wo sie gelesen werden muessen.
|
|
func wrapText(text string, font pdfFont, fontSize float64, availableWidth float64) []string {
|
|
words := splitWords(text)
|
|
if len(words) == 0 {
|
|
return []string{""}
|
|
}
|
|
|
|
wrappedLines := make([]string, 0, 4)
|
|
currentLine := words[0]
|
|
|
|
for _, word := range words[1:] {
|
|
candidateLine := currentLine + " " + word
|
|
|
|
if textWidth(candidateLine, font, fontSize) <= availableWidth {
|
|
currentLine = candidateLine
|
|
|
|
continue
|
|
}
|
|
|
|
wrappedLines = append(wrappedLines, currentLine)
|
|
currentLine = word
|
|
}
|
|
|
|
return append(wrappedLines, currentLine)
|
|
}
|
|
|
|
// splitWords zerlegt einen Text an Leerzeichen.
|
|
func splitWords(text string) []string {
|
|
words := make([]string, 0, 16)
|
|
currentWord := make([]rune, 0, 16)
|
|
|
|
for _, textRune := range text {
|
|
if textRune == ' ' || textRune == '\t' || textRune == '\n' {
|
|
if len(currentWord) > 0 {
|
|
words = append(words, string(currentWord))
|
|
currentWord = currentWord[:0]
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
currentWord = append(currentWord, textRune)
|
|
}
|
|
|
|
if len(currentWord) > 0 {
|
|
words = append(words, string(currentWord))
|
|
}
|
|
|
|
return words
|
|
}
|
|
|
|
// truncateToWidth kuerzt einen Text auf die vorgegebene Breite.
|
|
//
|
|
// Gekuerzt wird mit Auslassungspunkten, damit erkennbar bleibt, dass etwas
|
|
// fehlt. Ein stillschweigend abgeschnittener Pfad sieht aus wie ein anderer
|
|
// Pfad.
|
|
func truncateToWidth(text string, font pdfFont, fontSize float64, availableWidth float64) string {
|
|
if textWidth(text, font, fontSize) <= availableWidth {
|
|
return text
|
|
}
|
|
|
|
const ellipsis = "…"
|
|
|
|
textRunes := []rune(text)
|
|
ellipsisWidth := textWidth(ellipsis, font, fontSize)
|
|
|
|
for runeCount := len(textRunes) - 1; runeCount > 0; runeCount-- {
|
|
candidate := string(textRunes[:runeCount])
|
|
|
|
if textWidth(candidate, font, fontSize)+ellipsisWidth <= availableWidth {
|
|
return candidate + ellipsis
|
|
}
|
|
}
|
|
|
|
return ellipsis
|
|
}
|