Include retention and prune details in notifications
This commit is contained in:
@@ -11,9 +11,9 @@ import (
|
||||
func TestNotificationMessageIncludesBackupStatistics(t *testing.T) {
|
||||
started := time.Date(2026, 6, 14, 12, 0, 0, 0, time.UTC)
|
||||
finished := started.Add(2*time.Minute + 8*time.Second)
|
||||
run := model.Run{TaskType: "backup", Status: "success", StartedAt: &started, FinishedAt: &finished, SnapshotID: "abc123456789abcdef", BytesAdded: 1536, FilesNew: 7, FilesChanged: 2, BytesProcessed: 5 * 1024 * 1024, FilesProcessed: 120}
|
||||
run := model.Run{TaskType: "backup", Status: "success", StartedAt: &started, FinishedAt: &finished, SnapshotID: "abc123456789abcdef", BytesAdded: 1536, FilesNew: 7, FilesChanged: 2, BytesProcessed: 5 * 1024 * 1024, FilesProcessed: 120, RetentionRemoved: 3, RetentionAfter: 14}
|
||||
message := notificationMessage("Freigaben", "LV-426", run, finished)
|
||||
for _, expected := range []string{"📊 Backup-Zusammenfassung", "📦 Repository: LV-426", "🗂️ Job: Freigaben", "📸 Snapshot: abc123456789", "⏱️ Dauer: 2 Min. 8 Sek.", "📂 Dateien: 120 verarbeitet", "💾 Verarbeitet: 5.0 MiB", "➕ Neu: 7 Dateien", "✏️ Geändert: 2 Dateien", "📤 Neu gespeichert: 1.5 KiB", "🔐 Ziel: Restic Repository", "✅ Status: Erfolgreich"} {
|
||||
for _, expected := range []string{"📊 Backup-Zusammenfassung", "📦 Repository: LV-426", "🗂️ Job: Freigaben", "📸 Snapshot: abc123456789", "⏱️ Dauer: 2 Min. 8 Sek.", "📂 Dateien: 120 verarbeitet", "💾 Verarbeitet: 5.0 MiB", "➕ Neu: 7 Dateien", "✏️ Geändert: 2 Dateien", "📤 Neu gespeichert: 1.5 KiB", "🧹 Aufbewahrung: 3 Snapshot(s) entfernt", "📸 Verbleibende Snapshots: 14", "🔐 Ziel: Restic Repository", "✅ Status: Erfolgreich"} {
|
||||
if !strings.Contains(message, expected) {
|
||||
t.Errorf("message %q does not contain %q", message, expected)
|
||||
}
|
||||
@@ -31,9 +31,9 @@ func TestNotificationTitleIncludesRepositoryAndStatus(t *testing.T) {
|
||||
func TestNotificationMessageIncludesPruneSummary(t *testing.T) {
|
||||
started := time.Date(2026, 6, 22, 3, 0, 0, 0, time.UTC)
|
||||
finished := started.Add(42 * time.Second)
|
||||
run := model.Run{TaskType: "prune", Status: "success", StartedAt: &started, FinishedAt: &finished, Message: "prune completed"}
|
||||
run := model.Run{TaskType: "prune", Status: "success", StartedAt: &started, FinishedAt: &finished, Message: "prune completed", RepositoryBefore: 100 * 1024 * 1024, RepositoryAfter: 75 * 1024 * 1024, RepositoryFreed: 25 * 1024 * 1024}
|
||||
message := notificationMessage("Repository-Bereinigung", "LV-426", run, finished)
|
||||
for _, expected := range []string{"🧹 Prune-Zusammenfassung", "📦 Repository: LV-426", "🗂️ Job: Repository-Bereinigung", "⏱️ Dauer: 42 Sekunden", "🔐 Ziel: Restic Repository", "✅ Status: Erfolgreich"} {
|
||||
for _, expected := range []string{"🧹 Prune-Zusammenfassung", "📦 Repository: LV-426", "🗂️ Job: Repository-Bereinigung", "⏱️ Dauer: 42 Sekunden", "📦 Vorher: 100.0 MiB", "📦 Nachher: 75.0 MiB", "🧹 Freigegeben: 25.0 MiB", "🔐 Ziel: Restic Repository", "✅ Status: Erfolgreich"} {
|
||||
if !strings.Contains(message, expected) {
|
||||
t.Errorf("message %q does not contain %q", message, expected)
|
||||
}
|
||||
|
||||
@@ -449,6 +449,8 @@ func (s *Service) execute(ctx context.Context, run model.Run) (result model.Run)
|
||||
return failed(run, "validation", "repository no longer exists")
|
||||
}
|
||||
maintenanceRepoName = repo.Name
|
||||
var pruneStatsBefore restic.Stats
|
||||
pruneStatsBeforeOK := false
|
||||
live := run
|
||||
appendLiveLog(&live, strings.ToUpper(run.TaskType)+" wird vorbereitet")
|
||||
s.updateActive(live)
|
||||
@@ -465,6 +467,13 @@ func (s *Service) execute(ctx context.Context, run model.Run) (result model.Run)
|
||||
if run.TaskType == "check" {
|
||||
err = s.restic.Check(ctx, mounted.Repository)
|
||||
} else {
|
||||
if stats, statsErr := s.restic.Stats(ctx, mounted.Repository, "raw-data"); statsErr == nil {
|
||||
pruneStatsBefore, pruneStatsBeforeOK = stats, true
|
||||
appendLiveLog(&live, "Repository-Größe vor Bereinigung: "+formatBytes(stats.TotalSize))
|
||||
s.updateActive(live)
|
||||
} else {
|
||||
appendLiveLog(&live, "Repository-Größe vor Bereinigung konnte nicht ermittelt werden: "+statsErr.Error())
|
||||
}
|
||||
err = s.restic.Prune(ctx, mounted.Repository)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -474,6 +483,18 @@ func (s *Service) execute(ctx context.Context, run model.Run) (result model.Run)
|
||||
return failedRun
|
||||
}
|
||||
run.Status, run.Message = "success", run.TaskType+" completed"
|
||||
if run.TaskType == "prune" && pruneStatsBeforeOK {
|
||||
run.RepositoryBefore = pruneStatsBefore.TotalSize
|
||||
if stats, statsErr := s.restic.Stats(ctx, mounted.Repository, "raw-data"); statsErr == nil {
|
||||
run.RepositoryAfter = stats.TotalSize
|
||||
if run.RepositoryBefore > run.RepositoryAfter {
|
||||
run.RepositoryFreed = run.RepositoryBefore - run.RepositoryAfter
|
||||
}
|
||||
appendLiveLog(&live, fmt.Sprintf("Repository-Größe nach Bereinigung: %s, freigegeben: %s", formatBytes(run.RepositoryAfter), formatBytes(run.RepositoryFreed)))
|
||||
} else {
|
||||
appendLiveLog(&live, "Repository-Größe nach Bereinigung konnte nicht ermittelt werden: "+statsErr.Error())
|
||||
}
|
||||
}
|
||||
appendLiveLog(&live, strings.ToUpper(run.TaskType)+" abgeschlossen")
|
||||
run.LiveLog = live.LiveLog
|
||||
return run
|
||||
@@ -586,7 +607,8 @@ func (s *Service) executeBackup(ctx context.Context, run model.Run) (result mode
|
||||
result.Status, result.ErrorCode, result.Message = "warning", "connectivity", result.Message+"; repository unmount failed: "+err.Error()
|
||||
}
|
||||
}()
|
||||
previousSnapshot, previousSnapshotOK, previousSnapshotErr := s.latestJobSnapshot(ctx, mounted.Repository, job.ID)
|
||||
previousSnapshots, previousSnapshotErr := s.jobSnapshots(ctx, mounted.Repository, job.ID)
|
||||
previousSnapshot, previousSnapshotOK := latestSnapshot(previousSnapshots)
|
||||
if previousSnapshotErr != nil {
|
||||
appendLiveLog(&live, "Änderungsliste: vorheriger Snapshot konnte nicht ermittelt werden: "+previousSnapshotErr.Error())
|
||||
s.updateActive(live)
|
||||
@@ -686,6 +708,20 @@ func (s *Service) executeBackup(ctx context.Context, run model.Run) (result mode
|
||||
run.Status, run.Message, run.ErrorCode = "warning", "backup succeeded but retention failed: "+err.Error(), "repository"
|
||||
} else {
|
||||
run.Status, run.Message = "success", "backup completed"
|
||||
if summary.SnapshotID != "" && previousSnapshotErr == nil {
|
||||
run.RetentionBefore = len(previousSnapshots)
|
||||
if remainingSnapshots, remainingErr := s.jobSnapshots(ctx, mounted.Repository, job.ID); remainingErr == nil {
|
||||
run.RetentionAfter = len(remainingSnapshots)
|
||||
expectedAfter := run.RetentionBefore + 1
|
||||
if expectedAfter > run.RetentionAfter {
|
||||
run.RetentionRemoved = expectedAfter - run.RetentionAfter
|
||||
}
|
||||
appendLiveLog(&live, fmt.Sprintf("Aufbewahrung angewendet: %d Snapshot(s) entfernt, %d bleiben", run.RetentionRemoved, run.RetentionAfter))
|
||||
s.updateActive(live)
|
||||
} else {
|
||||
appendLiveLog(&live, "Aufbewahrung angewendet; verbleibende Snapshots konnten nicht ermittelt werden: "+remainingErr.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
run.SnapshotID = summary.SnapshotID
|
||||
run.BytesAdded = summary.DataAdded + imageSummary.DataAdded
|
||||
@@ -701,24 +737,31 @@ func (s *Service) executeBackup(ctx context.Context, run model.Run) (result mode
|
||||
return run
|
||||
}
|
||||
|
||||
func (s *Service) latestJobSnapshot(ctx context.Context, repo model.Repository, jobID string) (model.Snapshot, bool, error) {
|
||||
func (s *Service) jobSnapshots(ctx context.Context, repo model.Repository, jobID string) ([]model.Snapshot, error) {
|
||||
snapshots, err := s.restic.Snapshots(ctx, repo)
|
||||
if err != nil {
|
||||
return model.Snapshot{}, false, err
|
||||
return nil, err
|
||||
}
|
||||
tag := "job:" + jobID
|
||||
filtered := make([]model.Snapshot, 0, len(snapshots))
|
||||
for _, snapshot := range snapshots {
|
||||
if hasTag(snapshot.Tags, tag) && !hasTag(snapshot.Tags, "usb-image") {
|
||||
filtered = append(filtered, snapshot)
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func latestSnapshot(snapshots []model.Snapshot) (model.Snapshot, bool) {
|
||||
var latest model.Snapshot
|
||||
found := false
|
||||
for _, snapshot := range snapshots {
|
||||
if !hasTag(snapshot.Tags, tag) || hasTag(snapshot.Tags, "usb-image") {
|
||||
continue
|
||||
}
|
||||
if !found || snapshot.Time.After(latest.Time) {
|
||||
latest = snapshot
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return latest, found, nil
|
||||
return latest, found
|
||||
}
|
||||
|
||||
func hasTag(tags []string, wanted string) bool {
|
||||
@@ -934,6 +977,21 @@ func notificationMessage(name, repository string, run model.Run, now time.Time)
|
||||
lines = append(lines, "📤 Kopiert: "+formatBytes(run.BytesAdded))
|
||||
}
|
||||
}
|
||||
if run.TaskType == "backup" && run.RetentionAfter > 0 {
|
||||
lines = append(lines,
|
||||
"",
|
||||
fmt.Sprintf("🧹 Aufbewahrung: %d Snapshot(s) entfernt", run.RetentionRemoved),
|
||||
fmt.Sprintf("📸 Verbleibende Snapshots: %d", run.RetentionAfter),
|
||||
)
|
||||
}
|
||||
if run.TaskType == "prune" && run.RepositoryBefore > 0 && run.RepositoryAfter > 0 {
|
||||
lines = append(lines,
|
||||
"",
|
||||
"📦 Vorher: "+formatBytes(run.RepositoryBefore),
|
||||
"📦 Nachher: "+formatBytes(run.RepositoryAfter),
|
||||
"🧹 Freigegeben: "+formatBytes(run.RepositoryFreed),
|
||||
)
|
||||
}
|
||||
lines = append(lines, "", "🔐 Ziel: "+notificationTargetLabel(run.TaskType), statusIcon(run.Status)+" Status: "+status)
|
||||
if run.Message != "" && run.Message != run.TaskType+" completed" {
|
||||
lines = append(lines, "⚠️ Details: "+run.Message)
|
||||
|
||||
Reference in New Issue
Block a user