diff --git a/dist/urbm-2026.07.10.r001-x86_64-1.txz.sha256 b/dist/urbm-2026.07.10.r001-x86_64-1.txz.sha256 deleted file mode 100644 index 70f9408..0000000 --- a/dist/urbm-2026.07.10.r001-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -bc3619b1606e268b3e114e9ce68d6317dc7a3b17ddc5c0472cc31aec749edd39 urbm-2026.07.10.r001-x86_64-1.txz diff --git a/dist/urbm-2026.07.10.r001-x86_64-1.txz b/dist/urbm-2026.07.10.r002-x86_64-1.txz similarity index 56% rename from dist/urbm-2026.07.10.r001-x86_64-1.txz rename to dist/urbm-2026.07.10.r002-x86_64-1.txz index a1e2f9d..3b28c71 100644 Binary files a/dist/urbm-2026.07.10.r001-x86_64-1.txz and b/dist/urbm-2026.07.10.r002-x86_64-1.txz differ diff --git a/dist/urbm-2026.07.10.r002-x86_64-1.txz.sha256 b/dist/urbm-2026.07.10.r002-x86_64-1.txz.sha256 new file mode 100644 index 0000000..89451ec --- /dev/null +++ b/dist/urbm-2026.07.10.r002-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +ec72032752aaae7a2bde7587781f65b2b5b4d9ef74a6aad79a3e121491b30958 urbm-2026.07.10.r002-x86_64-1.txz diff --git a/dist/urbm.plg b/dist/urbm.plg index 76faf77..5ba322e 100644 --- a/dist/urbm.plg +++ b/dist/urbm.plg @@ -2,13 +2,17 @@ - + - + ]> +### 2026.07.10.r002 +- Include retention results in backup notifications after Restic forget runs, including removed and remaining snapshot counts when available. +- Include repository size before, after, and freed space in prune notifications when Restic statistics are available. + ### 2026.07.10.r001 - Add a bounded backup change list to the run log by diffing the previous job snapshot against the newly created snapshot. - Show up to the last 50 new, modified, or removed paths after successful Restic backups without failing the backup if diff collection is unavailable. diff --git a/internal/model/model.go b/internal/model/model.go index e7f3835..063fe8e 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -155,6 +155,12 @@ type Run struct { FilesChanged int64 `json:"filesChanged,omitempty"` BytesProcessed int64 `json:"bytesProcessed,omitempty"` FilesProcessed int64 `json:"filesProcessed,omitempty"` + RetentionBefore int `json:"retentionBefore,omitempty"` + RetentionAfter int `json:"retentionAfter,omitempty"` + RetentionRemoved int `json:"retentionRemoved,omitempty"` + RepositoryBefore int64 `json:"repositoryBefore,omitempty"` + RepositoryAfter int64 `json:"repositoryAfter,omitempty"` + RepositoryFreed int64 `json:"repositoryFreed,omitempty"` ProgressPercent float64 `json:"progressPercent,omitempty"` ProgressBytes int64 `json:"progressBytes,omitempty"` ProgressTotal int64 `json:"progressTotal,omitempty"` diff --git a/internal/service/notification_test.go b/internal/service/notification_test.go index d5a83ab..143259a 100644 --- a/internal/service/notification_test.go +++ b/internal/service/notification_test.go @@ -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) } diff --git a/internal/service/service.go b/internal/service/service.go index 7cff52c..44c326f 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -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) diff --git a/plugin/urbm.plg b/plugin/urbm.plg index a465c31..594935f 100644 --- a/plugin/urbm.plg +++ b/plugin/urbm.plg @@ -2,13 +2,17 @@ - + ]> +### 2026.07.10.r002 +- Include retention results in backup notifications after Restic forget runs, including removed and remaining snapshot counts when available. +- Include repository size before, after, and freed space in prune notifications when Restic statistics are available. + ### 2026.07.10.r001 - Add a bounded backup change list to the run log by diffing the previous job snapshot against the newly created snapshot. - Show up to the last 50 new, modified, or removed paths after successful Restic backups without failing the backup if diff collection is unavailable. diff --git a/webgui/URBM.page b/webgui/URBM.page index ef94415..5a29e54 100644 --- a/webgui/URBM.page +++ b/webgui/URBM.page @@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore" - +
- -

URBM 2026.07.10.r001

Restic Backup Manager für Unraid

+ +

URBM 2026.07.10.r002

Restic Backup Manager für Unraid

Verbindung wird hergestellt...
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/urbm';
- +