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>
221 lines
7.6 KiB
Go
221 lines
7.6 KiB
Go
package reports
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// Der PDF-Schreiber ist bewusst selbst geschrieben.
|
||
//
|
||
// Der Plan sagt „PDF where practical". Die bequeme Auslegung waere, es fuer
|
||
// nicht praktikabel zu erklaeren — aber ein Bericht fuer eine Pruefung wird als
|
||
// PDF verlangt, nicht als CSV. Und was hier gebraucht wird, ist der einfachste
|
||
// Teil des Formats: Text in einer der vierzehn Standardschriften, die jeder
|
||
// Betrachter mitbringt. Keine Einbettung, keine Bilder, keine Transparenz.
|
||
//
|
||
// Der Preis ist die Breitentabelle weiter unten. Ohne sie liesse sich weder
|
||
// umbrechen noch eine Spalte bemessen; mit geschaetzten Breiten ueberlappen
|
||
// Tabellenspalten, sobald ein Wert laenger wird als erwartet.
|
||
|
||
// Seitenmasse in Punkten (72 dpi). A4 ist 595 × 842.
|
||
const (
|
||
// pageWidth ist die Seitenbreite in Punkten.
|
||
pageWidth = 595.0
|
||
// pageHeight ist die Seitenhoehe in Punkten.
|
||
pageHeight = 842.0
|
||
// pageMargin ist der Seitenrand in Punkten.
|
||
pageMargin = 48.0
|
||
// contentWidth ist die nutzbare Breite.
|
||
contentWidth = pageWidth - 2*pageMargin
|
||
// bottomLimit ist die Hoehe, ab der eine neue Seite beginnt.
|
||
bottomLimit = pageMargin + 24
|
||
)
|
||
|
||
// pdfFont benennt eine der beiden verwendeten Schriften.
|
||
type pdfFont string
|
||
|
||
const (
|
||
// fontRegular ist die Grundschrift.
|
||
fontRegular pdfFont = "F1"
|
||
// fontBold ist die Auszeichnungsschrift.
|
||
fontBold pdfFont = "F2"
|
||
)
|
||
|
||
// pdfDocument sammelt die Seiten eines Dokuments.
|
||
type pdfDocument struct {
|
||
// pageStreams sind die fertigen Inhaltsstroeme je Seite.
|
||
pageStreams []*bytes.Buffer
|
||
// currentStream ist der Strom der Seite, auf die gerade geschrieben wird.
|
||
currentStream *bytes.Buffer
|
||
// cursorY ist die aktuelle Schreibhoehe, von unten gemessen.
|
||
cursorY float64
|
||
}
|
||
|
||
// newPDFDocument beginnt ein Dokument mit einer ersten Seite.
|
||
func newPDFDocument() *pdfDocument {
|
||
document := &pdfDocument{}
|
||
document.beginPage()
|
||
|
||
return document
|
||
}
|
||
|
||
// beginPage beginnt eine neue Seite.
|
||
func (document *pdfDocument) beginPage() {
|
||
document.currentStream = &bytes.Buffer{}
|
||
document.pageStreams = append(document.pageStreams, document.currentStream)
|
||
document.cursorY = pageHeight - pageMargin
|
||
}
|
||
|
||
// ensureSpace beginnt eine neue Seite, wenn der Platz nicht reicht.
|
||
func (document *pdfDocument) ensureSpace(neededHeight float64) {
|
||
if document.cursorY-neededHeight < bottomLimit {
|
||
document.beginPage()
|
||
}
|
||
}
|
||
|
||
// drawText schreibt eine Zeile an der aktuellen Position.
|
||
func (document *pdfDocument) drawText(text string, font pdfFont, fontSize float64, offsetX float64) {
|
||
document.ensureSpace(fontSize + 2)
|
||
|
||
fmt.Fprintf(document.currentStream, "BT /%s %.1f Tf %.1f %.1f Td (%s) Tj ET\n",
|
||
font, fontSize, pageMargin+offsetX, document.cursorY, escapePDFString(text))
|
||
|
||
document.cursorY -= fontSize + 3
|
||
}
|
||
|
||
// drawRule zieht eine waagerechte Linie.
|
||
func (document *pdfDocument) drawRule(lineWidth float64, greyLevel float64) {
|
||
document.ensureSpace(6)
|
||
|
||
fmt.Fprintf(document.currentStream, "%.2f G %.2f w %.1f %.1f m %.1f %.1f l S\n",
|
||
greyLevel, lineWidth, pageMargin, document.cursorY, pageMargin+contentWidth, document.cursorY)
|
||
|
||
document.cursorY -= 8
|
||
}
|
||
|
||
// addSpace laesst senkrechten Raum.
|
||
func (document *pdfDocument) addSpace(height float64) {
|
||
document.cursorY -= height
|
||
}
|
||
|
||
// escapePDFString maskiert die drei Zeichen mit Sonderbedeutung in PDF.
|
||
func escapePDFString(text string) string {
|
||
encodedText := encodeWinAnsi(text)
|
||
|
||
replacer := strings.NewReplacer(`\`, `\\`, `(`, `\(`, `)`, `\)`)
|
||
|
||
return replacer.Replace(encodedText)
|
||
}
|
||
|
||
// encodeWinAnsi wandelt UTF-8 in die Kodierung der Standardschriften.
|
||
//
|
||
// Die vierzehn Standardschriften werden hier mit /WinAnsiEncoding eingebunden;
|
||
// jedes Zeichen ist also **ein** Byte. Ein deutscher Bericht ohne diese
|
||
// Umwandlung zeigt „Sicherungsläufe" — und zwar nicht in der Vorschau des
|
||
// Entwicklers, sondern erst im Betrachter des Empfaengers.
|
||
func encodeWinAnsi(text string) string {
|
||
encodedBytes := make([]byte, 0, len(text))
|
||
|
||
for _, textRune := range text {
|
||
switch {
|
||
case textRune < 128:
|
||
encodedBytes = append(encodedBytes, byte(textRune))
|
||
case textRune <= 255:
|
||
// Latin-1 deckt die deutschen Umlaute und das scharfe S ab.
|
||
encodedBytes = append(encodedBytes, byte(textRune))
|
||
default:
|
||
if winAnsiByte, isKnown := winAnsiSpecials[textRune]; isKnown {
|
||
encodedBytes = append(encodedBytes, winAnsiByte)
|
||
|
||
continue
|
||
}
|
||
|
||
// Ein nicht darstellbares Zeichen wird zu einem Fragezeichen. Es
|
||
// stillschweigend wegzulassen erzeugte einen Text, der etwas
|
||
// anderes bedeutet als der urspruengliche.
|
||
encodedBytes = append(encodedBytes, '?')
|
||
}
|
||
}
|
||
|
||
return string(encodedBytes)
|
||
}
|
||
|
||
// winAnsiSpecials sind die Zeichen, die WinAnsi ausserhalb von Latin-1 kennt.
|
||
//
|
||
// Nur die, die in diesen Berichten wirklich vorkommen: typografische
|
||
// Anfuehrungszeichen, Gedankenstriche, Auslassungspunkte und das Eurozeichen.
|
||
var winAnsiSpecials = map[rune]byte{
|
||
'€': 0x80, '‚': 0x82, '„': 0x84, '…': 0x85, '†': 0x86,
|
||
'‡': 0x87, '‰': 0x89, '‹': 0x8B, '‘': 0x91, '’': 0x92,
|
||
'“': 0x93, '”': 0x94, '•': 0x95, '–': 0x96, '—': 0x97,
|
||
'›': 0x9B,
|
||
}
|
||
|
||
// bytes serialisiert das Dokument.
|
||
//
|
||
// Aufbau: Katalog, Seitenbaum, zwei Schriften, dann je Seite ein Seitenobjekt
|
||
// und ein Inhaltsstrom. Die Querverweistabelle am Ende braucht die Byte-Offsets
|
||
// aller Objekte — deshalb wird beim Schreiben mitgezaehlt.
|
||
func (document *pdfDocument) bytes() []byte {
|
||
pageCount := len(document.pageStreams)
|
||
|
||
// 1 Katalog, 2 Seitenbaum, 3 und 4 Schriften, danach je Seite zwei Objekte.
|
||
const firstPageObject = 5
|
||
|
||
outputBuffer := &bytes.Buffer{}
|
||
objectOffsets := make([]int, 0, 4+2*pageCount)
|
||
|
||
writeObject := func(objectBody string) {
|
||
objectOffsets = append(objectOffsets, outputBuffer.Len())
|
||
outputBuffer.WriteString(objectBody)
|
||
}
|
||
|
||
outputBuffer.WriteString("%PDF-1.4\n")
|
||
|
||
// Ein Kommentar mit Bytes ueber 127 kennzeichnet die Datei als binaer.
|
||
// Ohne ihn behandeln manche Uebertragungswege sie als Text und ersetzen
|
||
// Zeilenenden — die Querverweistabelle stimmt danach nicht mehr.
|
||
outputBuffer.WriteString("%\xE2\xE3\xCF\xD3\n")
|
||
|
||
pageReferences := make([]string, 0, pageCount)
|
||
for pageIndex := range pageCount {
|
||
pageReferences = append(pageReferences, fmt.Sprintf("%d 0 R", firstPageObject+2*pageIndex))
|
||
}
|
||
|
||
writeObject("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n")
|
||
writeObject(fmt.Sprintf("2 0 obj\n<< /Type /Pages /Count %d /Kids [%s] >>\nendobj\n",
|
||
pageCount, strings.Join(pageReferences, " ")))
|
||
writeObject("3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica " +
|
||
"/Encoding /WinAnsiEncoding >>\nendobj\n")
|
||
writeObject("4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold " +
|
||
"/Encoding /WinAnsiEncoding >>\nendobj\n")
|
||
|
||
for pageIndex, pageStream := range document.pageStreams {
|
||
pageObjectNumber := firstPageObject + 2*pageIndex
|
||
streamObjectNumber := pageObjectNumber + 1
|
||
|
||
writeObject(fmt.Sprintf("%d 0 obj\n<< /Type /Page /Parent 2 0 R "+
|
||
"/MediaBox [0 0 %.0f %.0f] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> "+
|
||
"/Contents %d 0 R >>\nendobj\n",
|
||
pageObjectNumber, pageWidth, pageHeight, streamObjectNumber))
|
||
|
||
writeObject(fmt.Sprintf("%d 0 obj\n<< /Length %d >>\nstream\n%s\nendstream\nendobj\n",
|
||
streamObjectNumber, pageStream.Len()+1, pageStream.String()))
|
||
}
|
||
|
||
crossReferenceOffset := outputBuffer.Len()
|
||
objectCount := len(objectOffsets) + 1
|
||
|
||
fmt.Fprintf(outputBuffer, "xref\n0 %d\n0000000000 65535 f \n", objectCount)
|
||
|
||
for _, objectOffset := range objectOffsets {
|
||
fmt.Fprintf(outputBuffer, "%010d 00000 n \n", objectOffset)
|
||
}
|
||
|
||
fmt.Fprintf(outputBuffer, "trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n",
|
||
objectCount, crossReferenceOffset)
|
||
|
||
return outputBuffer.Bytes()
|
||
}
|