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 deleted file mode 100644 index 89451ec..0000000 --- a/dist/urbm-2026.07.10.r002-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -ec72032752aaae7a2bde7587781f65b2b5b4d9ef74a6aad79a3e121491b30958 urbm-2026.07.10.r002-x86_64-1.txz diff --git a/dist/urbm-2026.07.10.r002-x86_64-1.txz b/dist/urbm-2026.07.10.r003-x86_64-1.txz similarity index 56% rename from dist/urbm-2026.07.10.r002-x86_64-1.txz rename to dist/urbm-2026.07.10.r003-x86_64-1.txz index 3b28c71..5143a69 100644 Binary files a/dist/urbm-2026.07.10.r002-x86_64-1.txz and b/dist/urbm-2026.07.10.r003-x86_64-1.txz differ diff --git a/dist/urbm-2026.07.10.r003-x86_64-1.txz.sha256 b/dist/urbm-2026.07.10.r003-x86_64-1.txz.sha256 new file mode 100644 index 0000000..838a5ba --- /dev/null +++ b/dist/urbm-2026.07.10.r003-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +e08f4f2ee2a9589531051a519eceed485dde2d0b456e4e9bf160dd9796be2a8e urbm-2026.07.10.r003-x86_64-1.txz diff --git a/dist/urbm.plg b/dist/urbm.plg index 5ba322e..f3c875b 100644 --- a/dist/urbm.plg +++ b/dist/urbm.plg @@ -2,13 +2,18 @@ - + - + ]> +### 2026.07.10.r003 +- Stream Restic snapshot file listings instead of buffering the full `restic ls --json` output in memory. +- Extend snapshot file-listing timeouts across the daemon and PHP bridge to reduce generic 500 errors on large snapshots. +- Return a clearer timeout error when Restic snapshot listing exceeds the daemon limit. + ### 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. diff --git a/internal/api/server.go b/internal/api/server.go index 36a62ec..5b86908 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -268,7 +268,7 @@ func (s *Server) repositoryStats(w http.ResponseWriter, r *http.Request) { writeJSON(w, 200, s.service.RepositoryStats(ctx)) } func (s *Server) snapshotFiles(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithTimeout(r.Context(), 10*time.Minute) + ctx, cancel := context.WithTimeout(r.Context(), 20*time.Minute) defer cancel() items, err := s.service.SnapshotFiles(ctx, r.PathValue("id"), r.PathValue("snapshot"), r.URL.Query().Get("path")) if err != nil { diff --git a/internal/restic/restic.go b/internal/restic/restic.go index fdef455..2f59c05 100644 --- a/internal/restic/restic.go +++ b/internal/restic/restic.go @@ -274,20 +274,16 @@ func (r *Runner) List(ctx context.Context, repo model.Repository, snapshot, path if path != "" { args = append(args, path) } - var output []byte - if err := r.run(ctx, repo, args, nil, &output); err != nil { - return nil, err - } items := []map[string]any{} - scanner := bufio.NewScanner(strings.NewReader(string(output))) - scanner.Buffer(make([]byte, 0, 64*1024), 2*1024*1024) - for scanner.Scan() { + if err := r.run(ctx, repo, args, func(line []byte) { var item map[string]any - if json.Unmarshal(scanner.Bytes(), &item) == nil && item["struct_type"] == "node" { + if json.Unmarshal(line, &item) == nil && item["struct_type"] == "node" { items = append(items, item) } + }, nil); err != nil { + return nil, err } - return items, scanner.Err() + return items, nil } func (r *Runner) Diff(ctx context.Context, repo model.Repository, before, after string, limit int) (DiffResult, error) { @@ -466,6 +462,9 @@ func (r *Runner) runWithInputEnvPassword(ctx context.Context, repo model.Reposit if errors.Is(ctx.Err(), context.Canceled) { return fmt.Errorf("cancelled: %w", ctx.Err()) } + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return fmt.Errorf("environment: Restic-Aufruf hat das Zeitlimit überschritten: %w", ctx.Err()) + } return resticError(message, err) } return nil diff --git a/internal/restic/restic_test.go b/internal/restic/restic_test.go index b82c1d0..21f61b4 100644 --- a/internal/restic/restic_test.go +++ b/internal/restic/restic_test.go @@ -233,6 +233,36 @@ printf '%%s\n' 'Files: 1 new, 1 removed, 2 changed' } } +func TestListStreamsSnapshotNodes(t *testing.T) { + dir := t.TempDir() + argsPath := filepath.Join(dir, "args") + script := filepath.Join(dir, "restic") + body := fmt.Sprintf(`#!/bin/sh +printf '%%s\n' "$@" > '%s' +printf '%%s\n' '{"struct_type":"snapshot","id":"snap"}' +printf '%%s\n' '{"struct_type":"node","path":"/mnt/user","type":"dir"}' +printf '%%s\n' '{"struct_type":"node","path":"/mnt/user/file.txt","type":"file","size":12}' +`, argsPath) + 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"} + items, err := runner.List(context.Background(), repo, "snap", "/mnt/user") + if err != nil { + t.Fatal(err) + } + if len(items) != 2 || items[0]["path"] != "/mnt/user" || items[1]["path"] != "/mnt/user/file.txt" { + t.Fatalf("unexpected items: %+v", items) + } + args, _ := os.ReadFile(argsPath) + for _, expected := range []string{"ls", "snap", "--json", "/mnt/user"} { + if !strings.Contains(string(args), expected) { + t.Fatalf("arguments missing %q: %s", expected, args) + } + } +} + func TestStatsUsesRequestedRepositoryCountingMode(t *testing.T) { dir := t.TempDir() argsPath := filepath.Join(dir, "args") diff --git a/plugin/urbm.plg b/plugin/urbm.plg index 594935f..f551308 100644 --- a/plugin/urbm.plg +++ b/plugin/urbm.plg @@ -2,13 +2,18 @@ - + ]> +### 2026.07.10.r003 +- Stream Restic snapshot file listings instead of buffering the full `restic ls --json` output in memory. +- Extend snapshot file-listing timeouts across the daemon and PHP bridge to reduce generic 500 errors on large snapshots. +- Return a clearer timeout error when Restic snapshot listing exceeds the daemon limit. + ### 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. diff --git a/webgui/URBM.page b/webgui/URBM.page index 5a29e54..6c96bd6 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.r002

Restic Backup Manager für Unraid

+ +

URBM 2026.07.10.r003

Restic Backup Manager für Unraid

Verbindung wird hergestellt...
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/urbm';
- + diff --git a/webgui/api.php b/webgui/api.php index 364d024..8ab1b72 100644 --- a/webgui/api.php +++ b/webgui/api.php @@ -6,6 +6,7 @@ header('Content-Type: application/json'); $socket = '/run/urbm/urbm.sock'; $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $path = $_GET['path'] ?? '/v1/health'; +$isSnapshotFileRequest = preg_match('#/snapshots/[^/]+/files(?:\?|$)#', $path) === 1; if (!preg_match('#^/v1/[A-Za-z0-9_./?=&%:-]*$#', $path) || str_contains($path, '..')) { http_response_code(400); @@ -26,7 +27,7 @@ curl_setopt_array($curl, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 3, - CURLOPT_TIMEOUT => 310, + CURLOPT_TIMEOUT => $isSnapshotFileRequest ? 1250 : 310, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], ]);