diff --git a/dist/urbm-2026.06.15.r009-x86_64-1.txz.sha256 b/dist/urbm-2026.06.15.r009-x86_64-1.txz.sha256 deleted file mode 100644 index eba8ef8..0000000 --- a/dist/urbm-2026.06.15.r009-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -648afa0c0b1348302f882422c5a199e56ecdcdc0f817bc9a402c1a73549bbeea urbm-2026.06.15.r009-x86_64-1.txz diff --git a/dist/urbm-2026.06.15.r009-x86_64-1.txz b/dist/urbm-2026.06.15.r010-x86_64-1.txz similarity index 55% rename from dist/urbm-2026.06.15.r009-x86_64-1.txz rename to dist/urbm-2026.06.15.r010-x86_64-1.txz index f8738bf..be8f6cf 100644 Binary files a/dist/urbm-2026.06.15.r009-x86_64-1.txz and b/dist/urbm-2026.06.15.r010-x86_64-1.txz differ diff --git a/dist/urbm-2026.06.15.r010-x86_64-1.txz.sha256 b/dist/urbm-2026.06.15.r010-x86_64-1.txz.sha256 new file mode 100644 index 0000000..f03a0a2 --- /dev/null +++ b/dist/urbm-2026.06.15.r010-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +7d5a1c417b66e7c318ccc182b61b48fc725dd561e829e34ff593d88434fa334a urbm-2026.06.15.r010-x86_64-1.txz diff --git a/dist/urbm.plg b/dist/urbm.plg index 6d801ba..05a5a4b 100644 --- a/dist/urbm.plg +++ b/dist/urbm.plg @@ -2,13 +2,17 @@ - + - + ]> +### 2026.06.15.r010 +- Retry repository dashboard statistics once after safely removing stale Restic locks. +- Show the affected repository name and concrete Restic error when statistics still cannot be loaded. + ### 2026.06.15.r009 - Add a per-backup-job CPU core limit using Restic's GOMAXPROCS setting. - Apply the configured limit to normal backups and optional USB raw-image backups without affecting restore or maintenance tasks. diff --git a/internal/restic/restic.go b/internal/restic/restic.go index d50fe33..2704047 100644 --- a/internal/restic/restic.go +++ b/internal/restic/restic.go @@ -202,6 +202,17 @@ func (r *Runner) Stats(ctx context.Context, repo model.Repository, mode string) if mode != "" { args = append(args, "--mode", mode) } + stats, err := r.stats(ctx, repo, args) + if err == nil || !isRepositoryLocked(err) { + return stats, err + } + if unlockErr := r.Unlock(ctx, repo); unlockErr != nil { + return Stats{}, fmt.Errorf("repository statistics locked; remove stale lock: %w", unlockErr) + } + return r.stats(ctx, repo, args) +} + +func (r *Runner) stats(ctx context.Context, repo model.Repository, args []string) (Stats, error) { var output []byte if err := r.run(ctx, repo, args, nil, &output); err != nil { return Stats{}, err diff --git a/internal/restic/restic_test.go b/internal/restic/restic_test.go index 883dc11..97e118f 100644 --- a/internal/restic/restic_test.go +++ b/internal/restic/restic_test.go @@ -130,6 +130,49 @@ func TestStatsUsesRequestedRepositoryCountingMode(t *testing.T) { } } +func TestStatsUnlocksStaleRepositoryLockAndRetries(t *testing.T) { + dir := t.TempDir() + logPath := filepath.Join(dir, "commands") + countPath := filepath.Join(dir, "stats-count") + script := filepath.Join(dir, "restic") + body := fmt.Sprintf(`#!/bin/sh +printf '%%s\n' "$*" >> '%s' +case " $* " in + *" stats "*) + count=0 + [ -f '%s' ] && count=$(cat '%s') + count=$((count + 1)) + printf '%%s' "$count" > '%s' + if [ "$count" -eq 1 ]; then + printf 'Fatal: unable to create lock in backend: repository is already locked\n' >&2 + exit 11 + fi + printf '%%s\n' '{"total_size":4096,"total_file_count":12}' + ;; +esac +`, logPath, countPath, countPath, countPath) + if err := os.WriteFile(script, []byte(body), 0700); err != nil { + t.Fatal(err) + } + runner := &Runner{Binary: script, RuntimeDir: dir, Secrets: fakeSecrets{"password": "secret"}} + repo := model.Repository{Type: model.RepositoryLocal, Location: "/repo", PasswordRef: "password"} + stats, err := runner.Stats(context.Background(), repo, "raw-data") + if err != nil { + t.Fatal(err) + } + if stats.TotalSize != 4096 { + t.Fatalf("unexpected stats: %+v", stats) + } + commands, err := os.ReadFile(logPath) + if err != nil { + t.Fatal(err) + } + lines := strings.Split(strings.TrimSpace(string(commands)), "\n") + if len(lines) != 3 || !strings.Contains(lines[0], "stats") || !strings.Contains(lines[1], "unlock") || !strings.Contains(lines[2], "stats") { + t.Fatalf("expected stats, unlock, stats; got %q", lines) + } +} + func TestForgetUsesAgeAndCalendarRetention(t *testing.T) { dir := t.TempDir() argsPath := filepath.Join(dir, "args") diff --git a/plugin/urbm.plg b/plugin/urbm.plg index 5c228f8..e2381c6 100644 --- a/plugin/urbm.plg +++ b/plugin/urbm.plg @@ -2,13 +2,17 @@ - + ]> +### 2026.06.15.r010 +- Retry repository dashboard statistics once after safely removing stale Restic locks. +- Show the affected repository name and concrete Restic error when statistics still cannot be loaded. + ### 2026.06.15.r009 - Add a per-backup-job CPU core limit using Restic's GOMAXPROCS setting. - Apply the configured limit to normal backups and optional USB raw-image backups without affecting restore or maintenance tasks. diff --git a/webgui/URBM.page b/webgui/URBM.page index 9a93926..3e354e4 100644 --- a/webgui/URBM.page +++ b/webgui/URBM.page @@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore" - +
- -

URBM 2026.06.15.r009

Restic Backup Manager für Unraid

+ +

URBM 2026.06.15.r010

Restic Backup Manager für Unraid

Verbindung wird hergestellt...
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/urbm';
- + diff --git a/webgui/assets/urbm.js b/webgui/assets/urbm.js index 9a44c60..22d894b 100644 --- a/webgui/assets/urbm.js +++ b/webgui/assets/urbm.js @@ -338,7 +338,7 @@ const last = [...state.runs].reverse().slice(0, 8); const stats=state.repositoryStats; const statsErrors=stats.filter(item=>item.error); - const loading=state.repositoryStatsLoading?'
Repository-Werte werden geladen…
':statsErrors.length?`
${statsErrors.length} Repository(s) konnten nicht gelesen werden.
`:''; + const loading=state.repositoryStatsLoading?'
Repository-Werte werden geladen…
':statsErrors.length?`
${statsErrors.map(item=>`${esc(item.repositoryName)}: ${esc(displayMessage(item.error))}`).join('
')}
`:''; return `
Aktive Jobs
${state.config.jobs.filter(j => j.enabled).length}
Repositorys
${state.config.repositories.length}