Add Unraid and Gotify notifications

This commit is contained in:
Mikei386
2026-06-14 13:50:02 +02:00
parent b7fe6ac86f
commit 1af23831f4
15 changed files with 232 additions and 44 deletions
+20 -1
View File
@@ -155,7 +155,7 @@ func DefaultConfig() Config {
SchemaVersion: SchemaVersion,
Jobs: []Job{},
Repositories: []Repository{},
Notifications: []NotificationTarget{},
Notifications: []NotificationTarget{{SchemaVersion: SchemaVersion, ID: "unraid", Name: "Unraid", Type: "unraid", Enabled: true, Events: []string{"success", "warning", "failed"}}},
Settings: Settings{
ResticPath: "/usr/local/libexec/backupper/restic",
RestoreRoot: "/mnt/user/backupper-restores",
@@ -165,3 +165,22 @@ func DefaultConfig() Config {
},
}
}
func NormalizeConfig(c Config) Config {
notifications := make([]NotificationTarget, 0, len(c.Notifications)+1)
hasUnraid := false
for _, target := range c.Notifications {
if target.Type == "ntfy" {
continue
}
if target.Type == "unraid" {
hasUnraid = true
}
notifications = append(notifications, target)
}
if !hasUnraid {
notifications = append(notifications, NotificationTarget{SchemaVersion: SchemaVersion, ID: "unraid", Name: "Unraid", Type: "unraid", Enabled: true, Events: []string{"success", "warning", "failed"}})
}
c.Notifications = notifications
return c
}
+43
View File
@@ -3,6 +3,7 @@ package model
import (
"errors"
"fmt"
"net/url"
"path/filepath"
"regexp"
"strings"
@@ -42,12 +43,54 @@ func ValidateConfig(c Config) error {
}
repositoryOwners[job.RepositoryID] = job.ID
}
notifications := make(map[string]struct{}, len(c.Notifications))
for _, target := range c.Notifications {
if err := ValidateNotification(target); err != nil {
return fmt.Errorf("notification %q: %w", target.ID, err)
}
if _, exists := notifications[target.ID]; exists {
return fmt.Errorf("duplicate notification id %q", target.ID)
}
notifications[target.ID] = struct{}{}
}
if !filepath.IsAbs(c.Settings.RestoreRoot) {
return errors.New("restoreRoot must be absolute")
}
return nil
}
func ValidateNotification(target NotificationTarget) error {
if target.SchemaVersion != SchemaVersion || !idPattern.MatchString(target.ID) {
return errors.New("invalid schemaVersion or id")
}
if strings.TrimSpace(target.Name) == "" {
return errors.New("name is required")
}
switch target.Type {
case "unraid":
case "gotify":
parsed, err := url.Parse(target.URL)
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
return errors.New("Gotify URL must be an absolute HTTP or HTTPS URL")
}
if !idPattern.MatchString(target.TokenRef) {
return errors.New("Gotify tokenRef must be a valid secret ID")
}
default:
return fmt.Errorf("unsupported notification type %q", target.Type)
}
allowed := map[string]bool{"success": true, "warning": true, "failed": true}
if target.Enabled && len(target.Events) == 0 {
return errors.New("enabled notification requires at least one event")
}
for _, event := range target.Events {
if !allowed[event] {
return fmt.Errorf("unsupported event %q", event)
}
}
return nil
}
func ValidateJob(j Job) error {
if j.SchemaVersion != SchemaVersion || !idPattern.MatchString(j.ID) {
return errors.New("invalid schemaVersion or id")
+20
View File
@@ -15,6 +15,26 @@ func TestValidateConfig(t *testing.T) {
}
}
func TestNormalizeConfigReplacesNtfyAndAddsUnraid(t *testing.T) {
c := DefaultConfig()
c.Notifications = []NotificationTarget{{SchemaVersion: 1, ID: "old-ntfy", Name: "ntfy", Type: "ntfy", Enabled: true}}
c = NormalizeConfig(c)
if len(c.Notifications) != 1 || c.Notifications[0].Type != "unraid" || !c.Notifications[0].Enabled {
t.Fatalf("notifications = %#v", c.Notifications)
}
}
func TestValidateGotifyNotification(t *testing.T) {
target := NotificationTarget{SchemaVersion: 1, ID: "gotify", Name: "Gotify", Type: "gotify", Enabled: true, URL: "https://gotify.example.test", TokenRef: "gotify-token", Events: []string{"failed"}}
if err := ValidateNotification(target); err != nil {
t.Fatal(err)
}
target.URL = "javascript:alert(1)"
if err := ValidateNotification(target); err == nil {
t.Fatal("unsafe Gotify URL accepted")
}
}
func TestRepositoryIsolation(t *testing.T) {
c := DefaultConfig()
c.Jobs = []Job{{SchemaVersion: 1, ID: "job", Name: "Broken", Type: JobFlash, RepositoryID: "missing", Consistency: Consistency{Mode: "live"}, Compression: "auto"}}