diff --git a/pkg/base/model.go b/pkg/base/model.go index 4c0af40a..a61ab69f 100644 --- a/pkg/base/model.go +++ b/pkg/base/model.go @@ -27,6 +27,33 @@ type Request struct { SkipVerifyCert bool `json:"skipVerifyCert"` } +// SetLabels replaces all labels on the request. +// +// Deprecated: In onResolve and onDone, assign request labels directly. +// In onStart and onError, use task.meta.req.setLabels instead. +func (r *Request) SetLabels(labels map[string]string) { + r.Labels = labels +} + +// PutLabel sets a label on the request. +// +// Deprecated: In onResolve and onDone, assign request labels directly. +// In onStart and onError, use task.meta.req.putLabel instead. +func (r *Request) PutLabel(key, value string) { + if r.Labels == nil { + r.Labels = make(map[string]string) + } + r.Labels[key] = value +} + +// DelLabel deletes a label from the request. +// +// Deprecated: In onResolve and onDone, delete request labels directly. +// In onStart and onError, use task.meta.req.delLabel instead. +func (r *Request) DelLabel(key string) { + delete(r.Labels, key) +} + func (r *Request) Validate() error { if r.URL == "" { return fmt.Errorf("invalid request url") diff --git a/pkg/download/downloader_test.go b/pkg/download/downloader_test.go index 8b2e5073..91e02a23 100644 --- a/pkg/download/downloader_test.go +++ b/pkg/download/downloader_test.go @@ -46,7 +46,7 @@ type generationTestManager struct { func TestExtensionTaskMethodsMatchEventCapabilities(t *testing.T) { taskType := reflect.TypeOf((*Task)(nil)) for _, typ := range []reflect.Type{taskType, reflect.TypeOf((*ExtensionTask)(nil)), reflect.TypeOf((*OnErrorExtensionTask)(nil))} { - for _, name := range []string{"SetUrl", "SetExtra", "PatchExtra", "PatchHeaders", "SetMethod", "SetBody", "SetHeaders", "PutHeader", "DelHeader", "SetTrackers"} { + for _, name := range []string{"SetExtra", "PatchExtra", "PatchHeaders", "SetMethod", "SetBody", "SetHeaders", "PutHeader", "DelHeader", "SetTrackers"} { if _, ok := typ.MethodByName(name); ok { t.Fatalf("%v unexpectedly exposes %s", typ, name) } @@ -79,6 +79,11 @@ func TestExtensionTaskMethodsMatchEventCapabilities(t *testing.T) { } extensionTaskType := reflect.TypeOf((*ExtensionTask)(nil)) + for _, typ := range []reflect.Type{extensionTaskType, reflect.TypeOf((*OnErrorExtensionTask)(nil))} { + if _, ok := typ.MethodByName("SetUrl"); !ok { + t.Fatalf("%v does not expose the deprecated SetUrl alias", typ) + } + } if _, ok := extensionTaskType.MethodByName("Continue"); ok { t.Fatal("non-error extension task unexpectedly exposes Continue") } diff --git a/pkg/download/extension.go b/pkg/download/extension.go index ea2c8300..b160b0e4 100644 --- a/pkg/download/extension.go +++ b/pkg/download/extension.go @@ -825,6 +825,13 @@ func newOnDoneExtensionTask(task *Task) *Task { return cloneExtensionTask(task) } +// SetUrl replaces the task request URL. +// +// Deprecated: Use task.meta.req.setUrl instead. +func (t *ExtensionTask) SetUrl(url string) { + t.Meta.Req.SetUrl(url) +} + func (t *OnErrorExtensionTask) Continue() error { return t.download.Continue(&TaskFilter{ IDs: []string{t.ID}, diff --git a/pkg/download/extension_compat_test.go b/pkg/download/extension_compat_test.go new file mode 100644 index 00000000..d61c0a51 --- /dev/null +++ b/pkg/download/extension_compat_test.go @@ -0,0 +1,89 @@ +package download + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/GopeedLab/gopeed/internal/fetcher" + "github.com/GopeedLab/gopeed/pkg/base" +) + +func TestExtensionDeprecatedRequestAPIs(t *testing.T) { + for _, event := range []ActivationEvent{EventOnResolve, EventOnStart, EventOnError, EventOnDone} { + for _, withLabels := range []bool{false, true} { + t.Run(fmt.Sprintf("%s/labels=%v", event, withLabels), func(t *testing.T) { + d := NewDownloader(&DownloaderConfig{Storage: NewMemStorage(), StorageDir: t.TempDir()}) + if err := d.Setup(); err != nil { + t.Fatal(err) + } + defer d.Clear() + + extDir := t.TempDir() + manifest := fmt.Sprintf(`{"name":"legacy-request","title":"Legacy request APIs","version":"0.0.1","scripts":[{"event":%q,"match":{"urls":["*://example.com/*"]},"entry":"index.js"}]}`, event) + request := "ctx.task.meta.req" + urlMutation := "" + if event == EventOnResolve { + request = "ctx.req" + } else if event == EventOnStart || event == EventOnError { + urlMutation = `await ctx.task.setUrl('https://example.com/legacy'); + if (req.url !== 'https://example.com/legacy') throw new Error('legacy URL did not write through'); + await req.setUrl('https://example.com/new'); + if (req.url !== 'https://example.com/new') throw new Error('new URL did not write through'); + await ctx.task.setUrl('https://example.com/final');` + } + script := fmt.Sprintf(`gopeed.events.%s(async ctx => { + const req = %s; + await req.delLabel('missing'); + await req.putLabel('initial', 'yes'); + if (req.labels.initial !== 'yes') throw new Error('putLabel did not initialize labels'); + await req.setLabels({keep: 'yes', remove: 'yes'}); + if (req.labels.initial !== undefined || req.labels.old !== undefined) throw new Error('setLabels did not replace labels'); + await req.putLabel('added', 'yes'); + await req.delLabel('remove'); + if (req.labels.keep !== 'yes' || req.labels.added !== 'yes' || req.labels.remove !== undefined) throw new Error('label mutation failed'); + %s + await req.putLabel('completed', 'yes'); +});`, event, request, urlMutation) + for name, content := range map[string]string{"manifest.json": manifest, "index.js": script} { + if err := os.WriteFile(filepath.Join(extDir, name), []byte(content), 0600); err != nil { + t.Fatal(err) + } + } + if _, err := d.InstallExtensionByFolder(extDir, false); err != nil { + t.Fatal(err) + } + req := &base.Request{URL: "https://example.com/original"} + if withLabels { + req.Labels = map[string]string{"old": "yes"} + } + task := &Task{Protocol: "http", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}} + switch event { + case EventOnResolve: + if _, err := d.triggerOnResolve(req); err != nil { + t.Fatal(err) + } + case EventOnStart: + d.triggerOnStart(task) + case EventOnError: + d.triggerOnError(task, errors.New("retry")) + case EventOnDone: + d.triggerOnDone(task) + } + if !reflect.DeepEqual(req.Labels, map[string]string{"keep": "yes", "added": "yes", "completed": "yes"}) { + t.Fatalf("legacy handler did not update the original request: %#v", req.Labels) + } + expectedURL := "https://example.com/original" + if urlMutation != "" { + expectedURL = "https://example.com/final" + } + if req.URL != expectedURL { + t.Fatalf("URL = %q, want %q", req.URL, expectedURL) + } + }) + } + } +} diff --git a/pkg/download/extension_extra_mutation_test.go b/pkg/download/extension_extra_mutation_test.go index d95ec369..a32e50f4 100644 --- a/pkg/download/extension_extra_mutation_test.go +++ b/pkg/download/extension_extra_mutation_test.go @@ -152,7 +152,7 @@ func TestExtensionOnErrorRequestWritesToOriginalTask(t *testing.T) { request.setHeaders({Keep:'new'}); request.putHeader('Authorization','Bearer new'); request.setMethod('POST'); - if (typeof task.setUrl !== 'undefined' || typeof task.extra !== 'undefined') throw new Error('legacy task API'); + if (typeof task.setUrl !== 'function' || typeof task.extra !== 'undefined') throw new Error('unexpected legacy task API'); request.setUrl('https://example.com/new'); request.setLabels({keep:'yes',remove:'yes'}); request.putLabel('new','value'); diff --git a/pkg/download/testdata/extensions/basic/index.js b/pkg/download/testdata/extensions/basic/index.js index eae932f4..5c70f018 100644 --- a/pkg/download/testdata/extensions/basic/index.js +++ b/pkg/download/testdata/extensions/basic/index.js @@ -1,5 +1,5 @@ gopeed.events.onResolve(async function (ctx) { - for (const method of ['setUrl', 'setLabels', 'putLabel', 'delLabel', 'setMethod', 'putHeader', 'setTrackers']) { + for (const method of ['setUrl', 'setMethod', 'putHeader', 'setTrackers']) { if (typeof ctx.req[method] !== 'undefined') throw new Error(`onResolve exposes ${method}`); } ctx.req.labels = { replaced: "true", removed: "true" };