diff --git a/dist/backupper-2026.06.14.9.6-x86_64-1.txz.sha256 b/dist/backupper-2026.06.14.9.6-x86_64-1.txz.sha256 deleted file mode 100644 index 56cb36d..0000000 --- a/dist/backupper-2026.06.14.9.6-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -6438688ae2884c69f23c7e2ec11eb7bab95d138b6f7d25c3c0335a6e319470a0 backupper-2026.06.14.9.6-x86_64-1.txz diff --git a/dist/backupper-2026.06.14.9.6-x86_64-1.txz b/dist/backupper-2026.06.14.9.7-x86_64-1.txz similarity index 55% rename from dist/backupper-2026.06.14.9.6-x86_64-1.txz rename to dist/backupper-2026.06.14.9.7-x86_64-1.txz index 4a7ed23..d96d6c1 100644 Binary files a/dist/backupper-2026.06.14.9.6-x86_64-1.txz and b/dist/backupper-2026.06.14.9.7-x86_64-1.txz differ diff --git a/dist/backupper-2026.06.14.9.7-x86_64-1.txz.sha256 b/dist/backupper-2026.06.14.9.7-x86_64-1.txz.sha256 new file mode 100644 index 0000000..0dfffd4 --- /dev/null +++ b/dist/backupper-2026.06.14.9.7-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +91325b4f694386ba23ad5ea43e51f6f3b41ae02ce5e66cf0bb04b0473fce9231 backupper-2026.06.14.9.7-x86_64-1.txz diff --git a/dist/backupper.plg b/dist/backupper.plg index 1fc1926..4a6d6a0 100644 --- a/dist/backupper.plg +++ b/dist/backupper.plg @@ -2,13 +2,17 @@ - + - + ]> +### 2026.06.14.9.7 +- Explain already initialized repositories and incorrect or damaged Restic keys with actionable errors. +- Strengthen the Initialize confirmation for existing repository destinations. + ### 2026.06.14.9.6 - Hide technical secret IDs and manage them automatically for repositories, SMB, SFTP, and Gotify. - Require a Restic password when creating a new repository while preserving existing secrets during edits. diff --git a/internal/restic/restic.go b/internal/restic/restic.go index 806746c..e9870a6 100644 --- a/internal/restic/restic.go +++ b/internal/restic/restic.go @@ -239,18 +239,32 @@ func (r *Runner) run(ctx context.Context, repo model.Repository, args []string, *capture = captured } if err != nil { - message := redact(string(errBytes), repo) + message := strings.TrimSpace(redact(string(errBytes), repo)) if errors.Is(ctx.Err(), context.Canceled) { return fmt.Errorf("cancelled: %w", ctx.Err()) } - if strings.Contains(message, "repository does not exist") || strings.Contains(message, "Is there a repository at the following location?") { - return errors.New("repository: destination is not initialized; open Repositories and select Initialize") - } - return fmt.Errorf("restic: %s: %w", strings.TrimSpace(message), err) + return resticError(message, err) } return nil } +func resticError(message string, commandErr error) error { + lower := strings.ToLower(message) + switch { + case strings.Contains(lower, "config file already exists") || strings.Contains(lower, "repository master key and config already initialized"): + return errors.New("repository: destination is already initialized; do not select Initialize again. Enter the existing repository password and select Test") + case strings.Contains(lower, "ciphertext verification failed"): + return errors.New("authentication: the saved repository password does not match this existing repository, or its Restic key is damaged. Enter the original repository password under Edit and select Test") + case strings.Contains(lower, "wrong password") || strings.Contains(lower, "no key found"): + return errors.New("authentication: wrong repository password. Enter the original Restic password under Edit and select Test") + case strings.Contains(lower, "repository does not exist") || strings.Contains(lower, "is there a repository at the following location?"): + return errors.New("repository: destination is not initialized; open Repositories and select Initialize once") + default: + message = strings.TrimSpace(strings.TrimPrefix(message, "Fatal:")) + return fmt.Errorf("restic: %s: %w", message, commandErr) + } +} + func repositoryLocation(repo model.Repository) string { if repo.Type == model.RepositorySFTP && !strings.HasPrefix(repo.Location, "sftp:") { return "sftp:" + repo.Location diff --git a/internal/restic/restic_test.go b/internal/restic/restic_test.go index 20637da..93c9bd9 100644 --- a/internal/restic/restic_test.go +++ b/internal/restic/restic_test.go @@ -74,3 +74,17 @@ func TestForgetUsesAgeAndCalendarRetention(t *testing.T) { t.Fatalf("disabled daily retention was included: %s", args) } } + +func TestResticErrorExplainsExistingRepository(t *testing.T) { + err := resticError("Fatal: create repository failed: config file already exists", fmt.Errorf("exit status 1")) + if !strings.Contains(err.Error(), "already initialized") || !strings.Contains(err.Error(), "select Test") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestResticErrorExplainsCiphertextVerification(t *testing.T) { + err := resticError(`{"message_type":"exit_error","message":"Fatal: config or key abc is damaged: ciphertext verification failed"}`, fmt.Errorf("exit status 1")) + if !strings.Contains(err.Error(), "password does not match") || strings.Contains(err.Error(), "abc") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/plugin/backupper.plg b/plugin/backupper.plg index 3532253..a4535ff 100644 --- a/plugin/backupper.plg +++ b/plugin/backupper.plg @@ -2,13 +2,17 @@ - + ]> +### 2026.06.14.9.7 +- Explain already initialized repositories and incorrect or damaged Restic keys with actionable errors. +- Strengthen the Initialize confirmation for existing repository destinations. + ### 2026.06.14.9.6 - Hide technical secret IDs and manage them automatically for repositories, SMB, SFTP, and Gotify. - Require a Restic password when creating a new repository while preserving existing secrets during edits. diff --git a/webgui/Backupper.page b/webgui/Backupper.page index 410c3c6..8be0785 100644 --- a/webgui/Backupper.page +++ b/webgui/Backupper.page @@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore" - +
- -

URBM 2026.06.14.9.6

Restic Backup Manager for Unraid

+ +

URBM 2026.06.14.9.7

Restic Backup Manager for Unraid

Connecting...
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/backupper';
- + diff --git a/webgui/assets/backupper.js b/webgui/assets/backupper.js index 9e0b938..fd93ed1 100644 --- a/webgui/assets/backupper.js +++ b/webgui/assets/backupper.js @@ -55,7 +55,7 @@ 'submit-job': 'Prüft und speichert diesen Backup-Job.', 'new-repo': 'Legt ein neues, isoliertes Restic-Speicherziel an.', 'submit-repo': 'Speichert Ziel und Zugangsdaten. Das Repository wird dadurch noch nicht automatisch initialisiert.', - 'init-repo': 'Initialisiert am konfigurierten Ziel ein neues, leeres Restic-Repository. Beispiel: nach dem erstmaligen Speichern eines neuen Backup-Ziels. Nicht für ein bereits vorhandenes Repository verwenden.', + 'init-repo': 'Initialisiert am Ziel einmalig ein neues, leeres Restic-Repository. Bei einem vorhandenen Repository nur das ursprüngliche Passwort eintragen und Test verwenden.', 'load-snapshots': 'Lädt alle Sicherungsstände des ausgewählten Repositorys.', 'restore-selected': 'Übernimmt die markierten Dateien oder Ordner in den Restore-Assistenten.', 'submit-restore': 'Stellt den Restore in die globale Warteschlange; Backups und Restores laufen nacheinander.', @@ -101,7 +101,7 @@ 'duplicate-job': 'Erstellt eine deaktivierte Kopie als Vorlage. Danach Name, Repository und Quellen anpassen.', 'delete-job': 'Löscht die Job-Konfiguration. Vorhandene Restic-Snapshots im Repository bleiben erhalten.', 'test-repo': 'Prüft Erreichbarkeit, Passwort und Restic-Zugriff, ohne ein Backup zu starten.', - 'init-repo': 'Erstellt die Restic-Struktur am Speicherziel. Diese Aktion ist einmalig für ein neues, leeres Ziel erforderlich.', + 'init-repo': 'Erstellt einmalig ein neues Restic-Repository. Nicht erneut ausführen und nicht für vorhandene Restic-Repositories verwenden.', 'repo-snapshots': 'Wechselt zu den Sicherungsständen dieses Repositorys.', 'maint': action.includes(':check:') ? 'Prüft Struktur und Integrität des Restic-Repositorys.' : 'Entfernt nach Retention nicht mehr benötigte Datenblöcke und gibt Speicher frei.', 'edit-repo': 'Öffnet Ziel-, Mount- und Zugangseinstellungen dieses Repositorys.', @@ -507,7 +507,7 @@ if (name === 'run-job') { await api(`/v1/jobs/${a}/run`,{method:'POST'}); notify('Backup queued'); return load(); } if (name === 'cancel-run') { await api(`/v1/runs/${a}/cancel`,{method:'POST'}); return load(); } if (name === 'test-repo') { await api(`/v1/repositories/${a}/test`,{method:'POST'}); return notify('Repository connection succeeded'); } - if (name === 'init-repo') { if(confirm('Initialize a new Restic repository at this destination? Use this only for a new, empty backup destination.')) { await api(`/v1/repositories/${a}/init`,{method:'POST'}); notify('Repository initialized successfully'); } return; } + if (name === 'init-repo') { if(confirm('Create a NEW Restic repository here?\n\nOnly continue if this destination does not already contain a Restic repository. For an existing repository, enter its original password under Edit and use Test.')) { await api(`/v1/repositories/${a}/init`,{method:'POST'}); notify('Repository initialized successfully'); } return; } if (name === 'maint') { await api(`/v1/repositories/${b}/${a}`,{method:'POST'}); notify(`${a} queued`); return load(); } if (name === 'repo-snapshots') { state.view='snapshots'; document.querySelector('[data-view="snapshots"]').click(); render(); setTimeout(()=>{document.getElementById('bu-snapshot-repo').value=a; handle('load-snapshots');},0); return; } if (name === 'load-snapshots') { const repo=document.getElementById('bu-snapshot-repo').value; state.snapshots=await api(`/v1/repositories/${repo}/snapshots`); state.snapshotRepo=repo; return render(); }