Add detailed backup notifications
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
67e2f22e189eb4aa8f350d48e23e8a9a618bf5714d9c740ef24e5ee413d045ff backupper-2026.06.14.9.1-x86_64-1.txz
|
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
8440c745eb052c38ce853789d10c1663e6dc0e77ad07b99079341bd4249546a6 backupper-2026.06.14.9.2-x86_64-1.txz
|
||||||
Vendored
+5
-2
@@ -2,13 +2,16 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "backupper">
|
<!ENTITY name "backupper">
|
||||||
<!ENTITY author "Michael Roll">
|
<!ENTITY author "Michael Roll">
|
||||||
<!ENTITY version "2026.06.14.9.1">
|
<!ENTITY version "2026.06.14.9.2">
|
||||||
<!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg">
|
<!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg">
|
||||||
<!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz">
|
<!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz">
|
||||||
<!ENTITY packageSHA256 "67e2f22e189eb4aa8f350d48e23e8a9a618bf5714d9c740ef24e5ee413d045ff">
|
<!ENTITY packageSHA256 "8440c745eb052c38ce853789d10c1663e6dc0e77ad07b99079341bd4249546a6">
|
||||||
]>
|
]>
|
||||||
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues" icon="backupper.png">
|
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues" icon="backupper.png">
|
||||||
<CHANGES>
|
<CHANGES>
|
||||||
|
### 2026.06.14.9.2
|
||||||
|
- Include duration, new and changed files, added data, total backup size, file count, and snapshot ID in backup notifications.
|
||||||
|
|
||||||
### 2026.06.14.9.1
|
### 2026.06.14.9.1
|
||||||
- Add native Unraid notifications and replace ntfy with Gotify.
|
- Add native Unraid notifications and replace ntfy with Gotify.
|
||||||
- Allow success, warning, and failure events to be selected per notification channel.
|
- Allow success, warning, and failure events to be selected per notification channel.
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ type Run struct {
|
|||||||
ErrorCode string `json:"errorCode,omitempty"`
|
ErrorCode string `json:"errorCode,omitempty"`
|
||||||
BytesAdded int64 `json:"bytesAdded,omitempty"`
|
BytesAdded int64 `json:"bytesAdded,omitempty"`
|
||||||
FilesNew int64 `json:"filesNew,omitempty"`
|
FilesNew int64 `json:"filesNew,omitempty"`
|
||||||
|
FilesChanged int64 `json:"filesChanged,omitempty"`
|
||||||
BytesProcessed int64 `json:"bytesProcessed,omitempty"`
|
BytesProcessed int64 `json:"bytesProcessed,omitempty"`
|
||||||
FilesProcessed int64 `json:"filesProcessed,omitempty"`
|
FilesProcessed int64 `json:"filesProcessed,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type Summary struct {
|
|||||||
SnapshotID string `json:"snapshot_id"`
|
SnapshotID string `json:"snapshot_id"`
|
||||||
DataAdded int64 `json:"data_added"`
|
DataAdded int64 `json:"data_added"`
|
||||||
FilesNew int64 `json:"files_new"`
|
FilesNew int64 `json:"files_new"`
|
||||||
|
FilesChanged int64 `json:"files_changed"`
|
||||||
TotalBytesProcessed int64 `json:"total_bytes_processed"`
|
TotalBytesProcessed int64 `json:"total_bytes_processed"`
|
||||||
TotalFilesProcessed int64 `json:"total_files_processed"`
|
TotalFilesProcessed int64 `json:"total_files_processed"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func TestBackupUsesPasswordFileAndStructuredArguments(t *testing.T) {
|
|||||||
argsPath := filepath.Join(dir, "args")
|
argsPath := filepath.Join(dir, "args")
|
||||||
passwordPath := filepath.Join(dir, "password")
|
passwordPath := filepath.Join(dir, "password")
|
||||||
script := filepath.Join(dir, "restic")
|
script := filepath.Join(dir, "restic")
|
||||||
body := fmt.Sprintf("#!/bin/sh\nprintf '%%s\\n' \"$@\" > '%s'\ncat \"$RESTIC_PASSWORD_FILE\" > '%s'\nprintf '%%s\\n' '{\"message_type\":\"summary\",\"snapshot_id\":\"abc123\",\"data_added\":42,\"files_new\":3,\"total_bytes_processed\":1024,\"total_files_processed\":12}'\n", argsPath, passwordPath)
|
body := fmt.Sprintf("#!/bin/sh\nprintf '%%s\\n' \"$@\" > '%s'\ncat \"$RESTIC_PASSWORD_FILE\" > '%s'\nprintf '%%s\\n' '{\"message_type\":\"summary\",\"snapshot_id\":\"abc123\",\"data_added\":42,\"files_new\":3,\"files_changed\":2,\"total_bytes_processed\":1024,\"total_files_processed\":12}'\n", argsPath, passwordPath)
|
||||||
if err := os.WriteFile(script, []byte(body), 0700); err != nil {
|
if err := os.WriteFile(script, []byte(body), 0700); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ func TestBackupUsesPasswordFileAndStructuredArguments(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if summary.SnapshotID != "abc123" || summary.DataAdded != 42 || summary.TotalBytesProcessed != 1024 || summary.TotalFilesProcessed != 12 {
|
if summary.SnapshotID != "abc123" || summary.DataAdded != 42 || summary.FilesChanged != 2 || summary.TotalBytesProcessed != 1024 || summary.TotalFilesProcessed != 12 {
|
||||||
t.Fatalf("unexpected summary: %+v", summary)
|
t.Fatalf("unexpected summary: %+v", summary)
|
||||||
}
|
}
|
||||||
args, _ := os.ReadFile(argsPath)
|
args, _ := os.ReadFile(argsPath)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/backupper-unraid/backupper/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
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: "abc123", BytesAdded: 1536, FilesNew: 7, FilesChanged: 2, BytesProcessed: 5 * 1024 * 1024, FilesProcessed: 120}
|
||||||
|
message := notificationMessage(run, finished)
|
||||||
|
for _, expected := range []string{"Status: Erfolgreich", "Dauer: 2 Min. 8 Sek.", "Neu gespeichert: 1.5 KiB", "Neue Dateien: 7", "Geänderte Dateien: 2", "Gesamtgröße dieses Backups: 5.0 MiB", "Dateien insgesamt: 120", "Snapshot: abc123"} {
|
||||||
|
if !strings.Contains(message, expected) {
|
||||||
|
t.Errorf("message %q does not contain %q", message, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -286,7 +286,7 @@ func (s *Service) executeBackup(ctx context.Context, run model.Run) (result mode
|
|||||||
} else {
|
} else {
|
||||||
run.Status, run.Message = "success", "backup completed"
|
run.Status, run.Message = "success", "backup completed"
|
||||||
}
|
}
|
||||||
run.SnapshotID, run.BytesAdded, run.FilesNew = summary.SnapshotID, summary.DataAdded, summary.FilesNew
|
run.SnapshotID, run.BytesAdded, run.FilesNew, run.FilesChanged = summary.SnapshotID, summary.DataAdded, summary.FilesNew, summary.FilesChanged
|
||||||
run.BytesProcessed, run.FilesProcessed = summary.TotalBytesProcessed, summary.TotalFilesProcessed
|
run.BytesProcessed, run.FilesProcessed = summary.TotalBytesProcessed, summary.TotalFilesProcessed
|
||||||
return run
|
return run
|
||||||
}
|
}
|
||||||
@@ -324,10 +324,11 @@ func (s *Service) finishJob(run model.Run, job model.Job, result model.Run) mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) notify(name string, run model.Run) {
|
func (s *Service) notify(name string, run model.Run) {
|
||||||
|
message := notificationMessage(run, time.Now().UTC())
|
||||||
for _, target := range s.Config().Notifications {
|
for _, target := range s.Config().Notifications {
|
||||||
for _, event := range target.Events {
|
for _, event := range target.Events {
|
||||||
if event == run.Status {
|
if event == run.Status {
|
||||||
if err := s.notifier.Send(context.Background(), target, "Backupper: "+name, run.Message, run.Status); err != nil {
|
if err := s.notifier.Send(context.Background(), target, "Backupper: "+name, message, run.Status); err != nil {
|
||||||
s.log.Warn("send notification failed", "target", target.ID, "type", target.Type, "error", err)
|
s.log.Warn("send notification failed", "target", target.ID, "type", target.Type, "error", err)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -336,6 +337,66 @@ func (s *Service) notify(name string, run model.Run) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notificationMessage(run model.Run, now time.Time) string {
|
||||||
|
status := map[string]string{"success": "Erfolgreich", "warning": "Mit Warnung", "failed": "Fehlgeschlagen"}[run.Status]
|
||||||
|
if status == "" {
|
||||||
|
status = run.Status
|
||||||
|
}
|
||||||
|
lines := []string{"Status: " + status}
|
||||||
|
if run.StartedAt != nil {
|
||||||
|
finished := now
|
||||||
|
if run.FinishedAt != nil {
|
||||||
|
finished = *run.FinishedAt
|
||||||
|
}
|
||||||
|
if duration := finished.Sub(*run.StartedAt); duration >= 0 {
|
||||||
|
lines = append(lines, "Dauer: "+formatDuration(duration))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if run.TaskType == "backup" && (run.Status == "success" || run.Status == "warning") {
|
||||||
|
lines = append(lines,
|
||||||
|
"Neu gespeichert: "+formatBytes(run.BytesAdded),
|
||||||
|
fmt.Sprintf("Neue Dateien: %d", run.FilesNew),
|
||||||
|
fmt.Sprintf("Geänderte Dateien: %d", run.FilesChanged),
|
||||||
|
"Gesamtgröße dieses Backups: "+formatBytes(run.BytesProcessed),
|
||||||
|
fmt.Sprintf("Dateien insgesamt: %d", run.FilesProcessed),
|
||||||
|
)
|
||||||
|
if run.SnapshotID != "" {
|
||||||
|
lines = append(lines, "Snapshot: "+run.SnapshotID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if run.Message != "" && run.Message != "backup completed" {
|
||||||
|
lines = append(lines, "Details: "+run.Message)
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatBytes(value int64) string {
|
||||||
|
const unit = int64(1024)
|
||||||
|
if value < unit {
|
||||||
|
return fmt.Sprintf("%d B", value)
|
||||||
|
}
|
||||||
|
div, exp := unit, 0
|
||||||
|
for n := value / unit; n >= unit && exp < 4; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %ciB", float64(value)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDuration(duration time.Duration) string {
|
||||||
|
duration = duration.Round(time.Second)
|
||||||
|
if duration < time.Minute {
|
||||||
|
return fmt.Sprintf("%d Sekunden", int(duration.Seconds()))
|
||||||
|
}
|
||||||
|
hours := int(duration / time.Hour)
|
||||||
|
minutes := int(duration/time.Minute) % 60
|
||||||
|
seconds := int(duration/time.Second) % 60
|
||||||
|
if hours > 0 {
|
||||||
|
return fmt.Sprintf("%d Std. %d Min. %d Sek.", hours, minutes, seconds)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d Min. %d Sek.", minutes, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) job(id string) (model.Job, bool) {
|
func (s *Service) job(id string) (model.Job, bool) {
|
||||||
for _, item := range s.Config().Jobs {
|
for _, item := range s.Config().Jobs {
|
||||||
if item.ID == id {
|
if item.ID == id {
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "backupper">
|
<!ENTITY name "backupper">
|
||||||
<!ENTITY author "Michael Roll">
|
<!ENTITY author "Michael Roll">
|
||||||
<!ENTITY version "2026.06.14.9.1">
|
<!ENTITY version "2026.06.14.9.2">
|
||||||
<!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg">
|
<!ENTITY pluginURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper.plg">
|
||||||
<!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz">
|
<!ENTITY packageURL "https://git.casaderoll.de/michael/BackUpper/raw/branch/main/dist/backupper-&version;-x86_64-1.txz">
|
||||||
<!ENTITY packageSHA256 "REPLACE_DURING_RELEASE">
|
<!ENTITY packageSHA256 "REPLACE_DURING_RELEASE">
|
||||||
]>
|
]>
|
||||||
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues" icon="backupper.png">
|
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/BackUpper/issues" icon="backupper.png">
|
||||||
<CHANGES>
|
<CHANGES>
|
||||||
|
### 2026.06.14.9.2
|
||||||
|
- Include duration, new and changed files, added data, total backup size, file count, and snapshot ID in backup notifications.
|
||||||
|
|
||||||
### 2026.06.14.9.1
|
### 2026.06.14.9.1
|
||||||
- Add native Unraid notifications and replace ntfy with Gotify.
|
- Add native Unraid notifications and replace ntfy with Gotify.
|
||||||
- Allow success, warning, and failure events to be selected per notification channel.
|
- Allow success, warning, and failure events to be selected per notification channel.
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ Tag="backup restic snapshots restore"
|
|||||||
<?php
|
<?php
|
||||||
$pluginRoot = '/plugins/backupper';
|
$pluginRoot = '/plugins/backupper';
|
||||||
?>
|
?>
|
||||||
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/backupper.css?v=2026061491">
|
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/backupper.css?v=2026061492">
|
||||||
<div id="backupper-app" data-api="<?= $pluginRoot ?>/api.php">
|
<div id="backupper-app" data-api="<?= $pluginRoot ?>/api.php">
|
||||||
<header class="bu-header">
|
<header class="bu-header">
|
||||||
<div class="bu-brand">
|
<div class="bu-brand">
|
||||||
<img class="bu-logo" src="<?= $pluginRoot ?>/images/backupper.png?v=2026061491" alt="Backupper logo">
|
<img class="bu-logo" src="<?= $pluginRoot ?>/images/backupper.png?v=2026061492" alt="Backupper logo">
|
||||||
<div><h1>Backupper <span class="bu-version">2026.06.14.9.1</span></h1><p>Encrypted Restic backups for Unraid</p></div>
|
<div><h1>Backupper <span class="bu-version">2026.06.14.9.2</span></h1><p>Encrypted Restic backups for Unraid</p></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die Backupper-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Connecting...</div>
|
<div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die Backupper-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Connecting...</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/backupper';
|
|||||||
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
|
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
<script>window.BACKUPPER_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
|
<script>window.BACKUPPER_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
|
||||||
<script src="<?= $pluginRoot ?>/assets/backupper-2026.06.14.9.1.js"></script>
|
<script src="<?= $pluginRoot ?>/assets/backupper-2026.06.14.9.2.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user