From 9fe0bb3520dcd0b75e0fa1bb943d745e739b7de2 Mon Sep 17 00:00:00 2001 From: saltbo Date: Sat, 6 Jun 2026 16:04:19 -0400 Subject: [PATCH] fix(downloader): make aria2 reset idempotent --- cmd/internal/engine/aria2.go | 34 ++++++++++++++++++++--- cmd/internal/engine/engine_test.go | 43 ++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/cmd/internal/engine/aria2.go b/cmd/internal/engine/aria2.go index 25098203..b8234fe1 100644 --- a/cmd/internal/engine/aria2.go +++ b/cmd/internal/engine/aria2.go @@ -144,11 +144,18 @@ func (a Aria2) ResetTask(ctx context.Context, task client.DownloadTask) error { if !aria2StatusBelongsToTask(status, taskDir, aria2TaskGID(task.ID)) { continue } - if err := aria.ForceRemove(status.GID); err != nil { - resetErrs = append(resetErrs, fmt.Errorf("force remove aria2 gid %s: %w", status.GID, err)) + removeActive, removeResult := aria2ResetOperations(status) + if removeActive { + err := aria.ForceRemove(status.GID) + if err != nil && !isAria2DownloadNotFound(err) { + resetErrs = append(resetErrs, fmt.Errorf("force remove aria2 gid %s: %w", status.GID, err)) + } } - if err := aria.RemoveDownloadResult(status.GID); err != nil { - resetErrs = append(resetErrs, fmt.Errorf("remove aria2 result %s: %w", status.GID, err)) + if removeResult { + err := aria.RemoveDownloadResult(status.GID) + if err != nil && !isAria2DownloadNotFound(err) { + resetErrs = append(resetErrs, fmt.Errorf("remove aria2 result %s: %w", status.GID, err)) + } } } if err := os.RemoveAll(taskDir); err != nil { @@ -157,6 +164,25 @@ func (a Aria2) ResetTask(ctx context.Context, task client.DownloadTask) error { return errors.Join(resetErrs...) } +func aria2ResetOperations(status arigo.Status) (removeActive bool, removeResult bool) { + switch string(status.Status) { + case string(arigo.StatusCompleted), string(arigo.StatusError), string(arigo.StatusRemoved), "complete": + return false, true + case string(arigo.StatusActive), string(arigo.StatusWaiting), string(arigo.StatusPaused): + return true, true + default: + return true, true + } +} + +func isAria2DownloadNotFound(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "download") && strings.Contains(msg, "not found") +} + func (a Aria2) SaveSession(ctx context.Context) error { client, err := a.client(ctx) if err != nil { diff --git a/cmd/internal/engine/engine_test.go b/cmd/internal/engine/engine_test.go index d0972d07..2e027f91 100644 --- a/cmd/internal/engine/engine_test.go +++ b/cmd/internal/engine/engine_test.go @@ -150,6 +150,49 @@ func TestAria2StartArgsForceSaveCompletedSeeds(t *testing.T) { } } +func TestAria2ResetOperations(t *testing.T) { + tests := []struct { + name string + status arigo.DownloadStatus + wantRemoveActive bool + wantRemoveResult bool + }{ + {name: "active", status: arigo.StatusActive, wantRemoveActive: true, wantRemoveResult: true}, + {name: "waiting", status: arigo.StatusWaiting, wantRemoveActive: true, wantRemoveResult: true}, + {name: "paused", status: arigo.StatusPaused, wantRemoveActive: true, wantRemoveResult: true}, + {name: "completed", status: arigo.StatusCompleted, wantRemoveActive: false, wantRemoveResult: true}, + {name: "error", status: arigo.StatusError, wantRemoveActive: false, wantRemoveResult: true}, + {name: "removed", status: arigo.StatusRemoved, wantRemoveActive: false, wantRemoveResult: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + removeActive, removeResult := aria2ResetOperations(arigo.Status{Status: tt.status}) + if removeActive != tt.wantRemoveActive || removeResult != tt.wantRemoveResult { + t.Fatalf( + "expected removeActive=%v removeResult=%v, got removeActive=%v removeResult=%v", + tt.wantRemoveActive, + tt.wantRemoveResult, + removeActive, + removeResult, + ) + } + }) + } +} + +func TestIsAria2DownloadNotFound(t *testing.T) { + if !isAria2DownloadNotFound(errors.New("Active Download not found for GID#b384ccaa7eae88da")) { + t.Fatal("expected aria2 active download not found to be ignored during reset") + } + if !isAria2DownloadNotFound(errors.New("Download result not found for GID#b384ccaa7eae88da")) { + t.Fatal("expected aria2 download result not found to be ignored during reset") + } + if isAria2DownloadNotFound(errors.New("aria2 download ended with status error")) { + t.Fatal("expected ordinary aria2 download errors to stay visible") + } +} + func TestHTTPDownloadResumesExistingFile(t *testing.T) { var rangeHeader string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {