mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 00:25:43 +00:00
feat(extension): expose request mutation methods on task.meta.req (#1538)
This commit is contained in:
13 files changed
+523
-41
No files matched your search
@@ -25,24 +25,6 @@ type Request struct {
|
||||
SkipVerifyCert bool `json:"skipVerifyCert"`
|
||||
}
|
||||
|
||||
// SetLabels replaces all labels on the request.
|
||||
func (r *Request) SetLabels(labels map[string]string) {
|
||||
r.Labels = labels
|
||||
}
|
||||
|
||||
// PutLabel sets a label on the request.
|
||||
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.
|
||||
func (r *Request) DelLabel(key string) {
|
||||
delete(r.Labels, key)
|
||||
}
|
||||
|
||||
func (r *Request) Validate() error {
|
||||
if r.URL == "" {
|
||||
return fmt.Errorf("invalid request url")
|
||||
|
||||
@@ -45,6 +45,32 @@ 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"} {
|
||||
if _, ok := typ.MethodByName(name); ok {
|
||||
t.Fatalf("%v unexpectedly exposes %s", typ, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
requestType := reflect.TypeOf((*ExtensionTaskRequest)(nil))
|
||||
for _, name := range []string{"SetUrl", "SetLabels", "PutLabel", "DelLabel", "SetMethod", "SetBody", "SetHeaders", "PutHeader", "DelHeader", "SetTrackers"} {
|
||||
if _, ok := requestType.MethodByName(name); !ok {
|
||||
t.Fatalf("request wrapper does not expose %s", name)
|
||||
}
|
||||
}
|
||||
for _, typ := range []reflect.Type{reflect.TypeOf(ExtensionTask{}), reflect.TypeOf(OnErrorExtensionTask{})} {
|
||||
if _, ok := typ.FieldByName("Req"); ok {
|
||||
t.Fatalf("%v exposes the removed task.req entry", typ)
|
||||
}
|
||||
field, ok := typ.FieldByName("Meta")
|
||||
if !ok || field.Type != reflect.TypeOf((*ExtensionTaskMeta)(nil)) {
|
||||
t.Fatalf("%v has no metadata wrapper", typ)
|
||||
}
|
||||
}
|
||||
field, ok := reflect.TypeOf(ExtensionTaskMeta{}).FieldByName("Req")
|
||||
if !ok || field.Type != requestType {
|
||||
t.Fatal("meta.req has no request wrapper")
|
||||
}
|
||||
if _, ok := taskType.MethodByName("Continue"); ok {
|
||||
t.Fatal("regular task unexpectedly exposes Continue")
|
||||
}
|
||||
@@ -53,17 +79,11 @@ func TestExtensionTaskMethodsMatchEventCapabilities(t *testing.T) {
|
||||
}
|
||||
|
||||
extensionTaskType := reflect.TypeOf((*ExtensionTask)(nil))
|
||||
if _, ok := extensionTaskType.MethodByName("SetUrl"); !ok {
|
||||
t.Fatal("extension task does not expose SetUrl")
|
||||
}
|
||||
if _, ok := extensionTaskType.MethodByName("Continue"); ok {
|
||||
t.Fatal("non-error extension task unexpectedly exposes Continue")
|
||||
}
|
||||
|
||||
errorTaskType := reflect.TypeOf((*OnErrorExtensionTask)(nil))
|
||||
if _, ok := errorTaskType.MethodByName("SetUrl"); !ok {
|
||||
t.Fatal("onError task does not expose SetUrl")
|
||||
}
|
||||
if _, ok := errorTaskType.MethodByName("Continue"); !ok {
|
||||
t.Fatal("onError task does not expose Continue")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/logger"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine"
|
||||
@@ -733,6 +734,14 @@ type OnDoneContext struct {
|
||||
// only some fields can be modified, such as request info.
|
||||
type ExtensionTask struct {
|
||||
*Task
|
||||
Meta *ExtensionTaskMeta `json:"meta"`
|
||||
}
|
||||
|
||||
// ExtensionTaskMeta preserves metadata fields while exposing request operations
|
||||
// at the existing meta.req path.
|
||||
type ExtensionTaskMeta struct {
|
||||
*fetcher.FetcherMeta
|
||||
Req *ExtensionTaskRequest `json:"req"`
|
||||
}
|
||||
|
||||
// OnErrorExtensionTask adds error-recovery controls to ExtensionTask.
|
||||
@@ -750,7 +759,14 @@ func cloneExtensionTask(task *Task) *Task {
|
||||
}
|
||||
|
||||
func newExtensionTask(task *Task) *ExtensionTask {
|
||||
return &ExtensionTask{Task: cloneExtensionTask(task)}
|
||||
view := cloneExtensionTask(task)
|
||||
return &ExtensionTask{
|
||||
Task: view,
|
||||
Meta: &ExtensionTaskMeta{
|
||||
FetcherMeta: view.Meta,
|
||||
Req: &ExtensionTaskRequest{Request: view.Meta.Req, task: view},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newOnStartExtensionTask(task *Task) *ExtensionTask {
|
||||
@@ -768,11 +784,6 @@ func newOnDoneExtensionTask(task *Task) *Task {
|
||||
return cloneExtensionTask(task)
|
||||
}
|
||||
|
||||
// SetUrl replaces the task request URL.
|
||||
func (t *ExtensionTask) SetUrl(url string) {
|
||||
t.Meta.Req.URL = url
|
||||
}
|
||||
|
||||
func (t *OnErrorExtensionTask) Continue() error {
|
||||
return t.download.Continue(&TaskFilter{
|
||||
IDs: []string{t.ID},
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
btProtocol "github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
httpProtocol "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func TestExtensionProtocolMethods(t *testing.T) {
|
||||
for _, extra := range []any{nil, map[string]any{"header": map[string]any{"Keep": "yes"}}, &httpProtocol.ReqExtra{Header: map[string]string{"Keep": "yes"}}} {
|
||||
vm := goja.New()
|
||||
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
||||
req := &base.Request{Extra: extra}
|
||||
task := &Task{Protocol: "http", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}}
|
||||
if err := vm.Set("task", newOnStartExtensionTask(task)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := vm.RunString(`task.meta.req.setMethod('POST'); task.meta.req.setBody('payload'); task.meta.req.putHeader('Authorization','Bearer token'); task.meta.req.putHeader('Remove','remove'); task.meta.req.delHeader('REMOVE');`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := base.ParseReqExtra[httpProtocol.ReqExtra](req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := req.Extra.(*httpProtocol.ReqExtra)
|
||||
if got.Method != "POST" || got.Body != "payload" || got.Header["Authorization"] != "Bearer token" {
|
||||
t.Fatalf("mutation not applied: %#v", got)
|
||||
}
|
||||
if _, ok := got.Header["Remove"]; ok {
|
||||
t.Fatal("header deletion failed")
|
||||
}
|
||||
if extra != nil && got.Header["Keep"] != "yes" {
|
||||
t.Fatal("existing header lost")
|
||||
}
|
||||
}
|
||||
vm := goja.New()
|
||||
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
||||
req := &base.Request{Extra: &btProtocol.ReqExtra{Trackers: []string{"old"}}}
|
||||
task := &Task{Protocol: "bt", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}}
|
||||
if err := vm.Set("task", newOnStartExtensionTask(task)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := vm.RunString(`task.meta.req.setTrackers(['new','another']);`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := req.Extra.(*btProtocol.ReqExtra).Trackers
|
||||
if len(got) != 2 || got[0] != "new" || got[1] != "another" {
|
||||
t.Fatalf("trackers = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionProtocolMethodsRejectInvalidMutation(t *testing.T) {
|
||||
vm := goja.New()
|
||||
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
||||
original := &httpProtocol.ReqExtra{Method: "POST", Header: map[string]string{"authorization": "old", "Keep": "yes"}}
|
||||
req := &base.Request{Extra: original}
|
||||
task := newOnStartExtensionTask(&Task{Protocol: "http", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}})
|
||||
if err := vm.Set("task", task); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, script := range []string{`task.meta.req.setHeaders({Keep:null})`, `task.meta.req.setHeaders({Keep:123})`, `task.meta.req.setHeaders({a:'1',A:'2'})`} {
|
||||
if _, err := vm.RunString(script); err == nil {
|
||||
t.Fatalf("accepted invalid mutation: %s", script)
|
||||
}
|
||||
if req.Extra != original || original.Header["Keep"] != "yes" {
|
||||
t.Fatal("invalid operation changed original extra")
|
||||
}
|
||||
}
|
||||
if _, err := vm.RunString(`task.meta.req.putHeader('Authorization','new');`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := req.Extra.(*httpProtocol.ReqExtra)
|
||||
if len(got.Header) != 2 || got.Header["Authorization"] != "new" || original.Header["authorization"] != "old" {
|
||||
t.Fatalf("header update = %#v", got.Header)
|
||||
}
|
||||
if _, err := vm.RunString(`task.meta.req.setHeaders({Only:'one'});`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got = req.Extra.(*httpProtocol.ReqExtra)
|
||||
if got.Method != "POST" || len(got.Header) != 1 {
|
||||
t.Fatalf("setHeaders = %#v", got)
|
||||
}
|
||||
req.Extra = map[string]any{"header": 123}
|
||||
if _, err := vm.RunString(`task.meta.req.setBody('new')`); err == nil {
|
||||
t.Fatal("invalid protocol extra accepted")
|
||||
}
|
||||
if req.Extra.(map[string]any)["header"] != 123 {
|
||||
t.Fatal("conversion failure mutated extra")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionProtocolMethodsOtherProtocolsAreNoOp(t *testing.T) {
|
||||
for _, protocol := range []string{"http", "bt", "hls"} {
|
||||
for _, onError := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("%s/onError=%v", protocol, onError), func(t *testing.T) {
|
||||
vm := goja.New()
|
||||
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
||||
// An invalid extra proves that unrelated methods skip conversion entirely.
|
||||
original := map[string]any{"unchanged": true, "header": 123}
|
||||
req := &base.Request{Extra: original}
|
||||
task := &Task{Protocol: protocol, Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}}
|
||||
var wrapper any = newOnStartExtensionTask(task)
|
||||
if onError {
|
||||
wrapper = newOnErrorExtensionTask(nil, task)
|
||||
}
|
||||
if err := vm.Set("task", wrapper); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if protocol != "http" {
|
||||
if _, err := vm.RunString(`task.meta.req.setMethod('POST');task.meta.req.setBody('body');task.meta.req.setHeaders({a:null});task.meta.req.putHeader('A','value');task.meta.req.delHeader('A');`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if protocol != "bt" {
|
||||
if _, err := vm.RunString(`task.meta.req.setTrackers(['udp://example.com']);`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(req.Extra, original) {
|
||||
t.Fatalf("extra changed: %#v", req.Extra)
|
||||
}
|
||||
original["same-object"] = true
|
||||
if req.Extra.(map[string]any)["same-object"] != true {
|
||||
t.Fatal("no-op replaced extra")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionOnErrorRequestWritesToOriginalTask(t *testing.T) {
|
||||
vm := goja.New()
|
||||
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
||||
req := &base.Request{URL: "https://example.com", Extra: &httpProtocol.ReqExtra{Header: map[string]string{"Keep": "yes"}}}
|
||||
task := &Task{Protocol: "http", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}}
|
||||
if err := vm.Set("task", newOnErrorExtensionTask(nil, task)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := vm.RunString(`
|
||||
if (typeof task.continue !== 'function') throw new Error('missing continue');
|
||||
if (typeof task.putHeader !== 'undefined') throw new Error('task still exposes protocol methods');
|
||||
if (typeof task.req !== 'undefined') throw new Error('legacy task.req');
|
||||
const request = task.meta.req;
|
||||
if (request.url !== 'https://example.com' || request.extra.header.Keep !== 'yes') throw new Error('request data missing');
|
||||
if (!task.meta.opts) throw new Error('metadata fields missing');
|
||||
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');
|
||||
request.setUrl('https://example.com/new');
|
||||
request.setLabels({keep:'yes',remove:'yes'});
|
||||
request.putLabel('new','value');
|
||||
request.delLabel('remove');
|
||||
if (request.url !== 'https://example.com/new' || request.labels.new !== 'value' || request.extra.method !== 'POST' || request.extra.header.Authorization !== 'Bearer new') throw new Error('request data did not refresh');
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(req.Labels, map[string]string{"keep": "yes", "new": "value"}) {
|
||||
t.Fatalf("labels = %#v", req.Labels)
|
||||
}
|
||||
got := req.Extra.(*httpProtocol.ReqExtra)
|
||||
if got.Method != "POST" || got.Header["Keep"] != "new" || got.Header["Authorization"] != "Bearer new" || req.URL != "https://example.com/new" {
|
||||
t.Fatalf("original request not updated: %#v, %#v", req, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionBTRequestInitializesNilExtra(t *testing.T) {
|
||||
vm := goja.New()
|
||||
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
||||
req := &base.Request{}
|
||||
task := &Task{Protocol: "bt", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}}
|
||||
if err := vm.Set("task", newOnStartExtensionTask(task)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := vm.RunString(`task.meta.req.setTrackers(['udp://example.com']);`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, ok := req.Extra.(*btProtocol.ReqExtra)
|
||||
if !ok || !reflect.DeepEqual(got.Trackers, []string{"udp://example.com"}) {
|
||||
t.Fatalf("extra = %#v", req.Extra)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
httpProtocol "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
)
|
||||
|
||||
func TestExtensionExtraMutationHTTP(t *testing.T) {
|
||||
for _, resolved := range []bool{false, true} {
|
||||
for _, invalid := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("resolved=%v/invalid=%v", resolved, invalid), func(t *testing.T) {
|
||||
var updatedRequests atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("X-New") == "new" {
|
||||
updatedRequests.Add(1)
|
||||
if r.Header.Get("X-Old") != "" {
|
||||
t.Error("extra mutation retained the old header")
|
||||
}
|
||||
}
|
||||
http.ServeContent(w, r, "test.txt", time.Time{}, strings.NewReader("extra mutation payload"))
|
||||
}))
|
||||
defer server.Close()
|
||||
d := NewDownloader(&DownloaderConfig{Storage: NewMemStorage(), StorageDir: t.TempDir()})
|
||||
if err := d.Setup(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.Clear()
|
||||
extDir := t.TempDir()
|
||||
manifest := `{"name":"http-methods","title":"HTTP methods test","version":"0.0.1","scripts":[{"event":"onStart","match":{"labels":["test"]},"entry":"index.js"}]}`
|
||||
mutation := `await ctx.task.meta.req.setMethod('GET');
|
||||
await ctx.task.meta.req.setBody('');
|
||||
await ctx.task.meta.req.delHeader('x-old');
|
||||
await ctx.task.meta.req.putHeader('X-New', 'new');`
|
||||
if invalid {
|
||||
mutation = `try { await ctx.task.meta.req.setHeaders({'X-New':123}); }
|
||||
catch (err) { await ctx.task.meta.req.putLabel('rejected','true'); } ` + mutation
|
||||
}
|
||||
script := `gopeed.events.onStart(async ctx => { ` + mutation + ` });`
|
||||
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)
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
d.Listener(func(event *Event) {
|
||||
if event.Key == EventKeyFinally {
|
||||
done <- event.Err
|
||||
}
|
||||
})
|
||||
req := &base.Request{URL: server.URL + "/test.txt", Labels: map[string]string{"test": "true"}, Extra: &httpProtocol.ReqExtra{Header: map[string]string{"X-Old": "old"}}}
|
||||
opts := &base.Options{Path: t.TempDir()}
|
||||
var id string
|
||||
var err error
|
||||
if resolved {
|
||||
rr, resolveErr := d.Resolve(req, opts)
|
||||
if resolveErr != nil {
|
||||
t.Fatal(resolveErr)
|
||||
}
|
||||
id, err = d.Create(rr.ID)
|
||||
} else {
|
||||
id, err = d.CreateDirect(req, opts)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err = <-done:
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("download timed out")
|
||||
}
|
||||
if invalid && d.GetTask(id).Meta.Req.Labels["rejected"] != "true" {
|
||||
t.Fatal("invalid header was not rejected")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, ok := d.GetTask(id).Meta.Req.Extra.(*httpProtocol.ReqExtra)
|
||||
if !ok || got.Header["X-New"] != "new" || len(got.Header) != 1 {
|
||||
t.Fatalf("unexpected protocol extra: %#v", d.GetTask(id).Meta.Req.Extra)
|
||||
}
|
||||
if !resolved && updatedRequests.Load() == 0 {
|
||||
t.Fatal("download did not use the extension headers")
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(opts.Path, "test.txt"))
|
||||
if err != nil || string(data) != "extra mutation payload" {
|
||||
t.Fatalf("download content = %q, error = %v", data, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
btProtocol "github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
httpProtocol "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
// ExtensionTaskRequest exposes request operations without exposing the backing task.
|
||||
// It always reads the current request so a retained handle sees subsequent updates.
|
||||
type ExtensionTaskRequest struct {
|
||||
*base.Request
|
||||
task *Task
|
||||
}
|
||||
|
||||
// SetUrl replaces the task request URL.
|
||||
func (r *ExtensionTaskRequest) SetUrl(url string) { r.task.Meta.Req.URL = url }
|
||||
|
||||
// SetLabels replaces all labels on the request.
|
||||
func (r *ExtensionTaskRequest) SetLabels(labels map[string]string) {
|
||||
r.Labels = labels
|
||||
}
|
||||
|
||||
// PutLabel sets a label on the request.
|
||||
func (r *ExtensionTaskRequest) 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.
|
||||
func (r *ExtensionTaskRequest) DelLabel(key string) {
|
||||
delete(r.Labels, key)
|
||||
}
|
||||
|
||||
// mutateTaskExtra converts into an independent protocol value before mutation.
|
||||
// Other protocols are a no-op. Failed conversion or mutation must not change the live request.
|
||||
func mutateTaskExtra[E any](task *Task, protocol string, mutate func(*E) error) error {
|
||||
if task.Protocol != protocol {
|
||||
return nil
|
||||
}
|
||||
if task.Meta == nil || task.Meta.Req == nil {
|
||||
return fmt.Errorf("task request is missing")
|
||||
}
|
||||
var extra E
|
||||
if err := util.MapToStruct(task.Meta.Req.Extra, &extra); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mutate(&extra); err != nil {
|
||||
return err
|
||||
}
|
||||
task.Meta.Req.Extra = &extra
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ExtensionTaskRequest) SetMethod(method string) error {
|
||||
return mutateTaskExtra(t.task, "http", func(extra *httpProtocol.ReqExtra) error { extra.Method = method; return nil })
|
||||
}
|
||||
|
||||
func (t *ExtensionTaskRequest) SetBody(body string) error {
|
||||
return mutateTaskExtra(t.task, "http", func(extra *httpProtocol.ReqExtra) error { extra.Body = body; return nil })
|
||||
}
|
||||
|
||||
// decodeHeaders omits undefined properties and rejects non-string values.
|
||||
func decodeHeaders(value *goja.Object) (map[string]string, error) {
|
||||
if value == nil {
|
||||
return nil, fmt.Errorf("headers must be an object")
|
||||
}
|
||||
data, err := value.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var input map[string]*string
|
||||
if err := json.Unmarshal(data, &input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input == nil {
|
||||
return nil, fmt.Errorf("headers must be an object")
|
||||
}
|
||||
headers := make(map[string]string, len(input))
|
||||
seen := make(map[string]bool, len(input))
|
||||
for name, value := range input {
|
||||
if value == nil {
|
||||
return nil, fmt.Errorf("header %q must be a string", name)
|
||||
}
|
||||
lower := strings.ToLower(name)
|
||||
if seen[lower] {
|
||||
return nil, fmt.Errorf("duplicate header name %q", name)
|
||||
}
|
||||
seen[lower] = true
|
||||
headers[http.CanonicalHeaderKey(name)] = *value
|
||||
}
|
||||
return headers, nil
|
||||
}
|
||||
|
||||
func (t *ExtensionTaskRequest) SetHeaders(headers *goja.Object) error {
|
||||
return mutateTaskExtra(t.task, "http", func(extra *httpProtocol.ReqExtra) error {
|
||||
values, err := decodeHeaders(headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
extra.Header = values
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ExtensionTaskRequest) PutHeader(name, value string) error {
|
||||
return mutateTaskExtra(t.task, "http", func(extra *httpProtocol.ReqExtra) error {
|
||||
if extra.Header == nil {
|
||||
extra.Header = make(map[string]string)
|
||||
}
|
||||
deleteHeader(extra.Header, name)
|
||||
extra.Header[http.CanonicalHeaderKey(name)] = value
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ExtensionTaskRequest) DelHeader(name string) error {
|
||||
return mutateTaskExtra(t.task, "http", func(extra *httpProtocol.ReqExtra) error { deleteHeader(extra.Header, name); return nil })
|
||||
}
|
||||
|
||||
func deleteHeader(headers map[string]string, name string) {
|
||||
for key := range headers {
|
||||
if strings.EqualFold(key, name) {
|
||||
delete(headers, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ExtensionTaskRequest) SetTrackers(trackers []string) error {
|
||||
return mutateTaskExtra(t.task, "bt", func(extra *btProtocol.ReqExtra) error { extra.Trackers = append([]string{}, trackers...); return nil })
|
||||
}
|
||||
@@ -14,10 +14,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/logger"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
|
||||
httpProtocol "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
@@ -38,13 +40,13 @@ func TestDownloader_InstallExtensionByFolder(t *testing.T) {
|
||||
t.Fatal("resolve error")
|
||||
}
|
||||
if req.Labels["replaced"] != "true" || req.Labels["modified"] != "true" {
|
||||
t.Fatalf("request label methods did not update labels: %#v", req.Labels)
|
||||
t.Fatalf("declarative labels did not update: %#v", req.Labels)
|
||||
}
|
||||
if _, ok := req.Labels["original"]; ok {
|
||||
t.Fatalf("setLabels did not replace existing labels: %#v", req.Labels)
|
||||
t.Fatalf("label assignment did not replace existing labels: %#v", req.Labels)
|
||||
}
|
||||
if _, ok := req.Labels["removed"]; ok {
|
||||
t.Fatalf("delLabel did not remove label: %#v", req.Labels)
|
||||
t.Fatalf("declarative label deletion failed: %#v", req.Labels)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1188,6 +1190,37 @@ func TestDownloader_UpgradeExtension(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloader_Extension_OnStartExtraMutation(t *testing.T) {
|
||||
d := NewDownloader(&DownloaderConfig{Storage: NewMemStorage(), StorageDir: t.TempDir()})
|
||||
if err := d.Setup(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.Clear()
|
||||
if _, err := d.InstallExtensionByFolder("./testdata/extensions/on_start", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, extra := range []any{nil, &httpProtocol.ReqExtra{
|
||||
Method: "POST", Body: "old body", Header: map[string]string{"Old": "value"},
|
||||
}} {
|
||||
req := &base.Request{URL: "https://github.com/test", Extra: extra}
|
||||
task := &Task{Protocol: "http", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}}
|
||||
d.triggerOnStart(task)
|
||||
if err := base.ParseReqExtra[httpProtocol.ReqExtra](req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, ok := req.Extra.(*httpProtocol.ReqExtra)
|
||||
if !ok || got.Header["X-Gopeed-Test"] != "on-start" {
|
||||
t.Fatalf("setHeaders did not update the original request: %#v", req.Extra)
|
||||
}
|
||||
if len(got.Header) != 1 {
|
||||
t.Fatalf("headers not replaced: %#v", got)
|
||||
}
|
||||
if original, ok := extra.(*httpProtocol.ReqExtra); ok && (got.Method != original.Method || got.Body != original.Body) {
|
||||
t.Fatalf("setHeaders changed unrelated fields: %#v", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloader_Extension_OnStart(t *testing.T) {
|
||||
downloadAndCheck := func(req *base.Request) {
|
||||
setupDownloader(func(downloader *Downloader) {
|
||||
|
||||
+6
-3
@@ -1,7 +1,10 @@
|
||||
gopeed.events.onResolve(async function (ctx) {
|
||||
await ctx.req.setLabels({ replaced: "true", removed: "true" });
|
||||
await ctx.req.putLabel("modified", "true");
|
||||
await ctx.req.delLabel("removed");
|
||||
for (const method of ['setUrl', 'setLabels', 'putLabel', 'delLabel', 'setMethod', 'putHeader', 'setTrackers']) {
|
||||
if (typeof ctx.req[method] !== 'undefined') throw new Error(`onResolve exposes ${method}`);
|
||||
}
|
||||
ctx.req.labels = { replaced: "true", removed: "true" };
|
||||
ctx.req.labels.modified = "true";
|
||||
delete ctx.req.labels.removed;
|
||||
ctx.res = {
|
||||
name: "test",
|
||||
files: Array(2).fill(true).map((_, i) => ({
|
||||
|
||||
@@ -132,7 +132,7 @@ gopeed.events.onError(async function (ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.task.setUrl(await createPayloadURL(source, false));
|
||||
await ctx.task.meta.req.setUrl(await createPayloadURL(source, false));
|
||||
await req.putLabel('started', 'true');
|
||||
await req.putLabel('rebuilt', 'true');
|
||||
await ctx.task.continue();
|
||||
|
||||
+1
-1
@@ -41,6 +41,6 @@ gopeed.events.onError(async function (ctx) {
|
||||
}
|
||||
|
||||
await req.putLabel("recovered", "true");
|
||||
await ctx.task.setUrl(await gopeed.runtime.blob.createObjectURL(new Blob(["ok\n"], { type: "text/plain" })));
|
||||
await ctx.task.meta.req.setUrl(await gopeed.runtime.blob.createObjectURL(new Blob(["ok\n"], { type: "text/plain" })));
|
||||
await ctx.task.continue();
|
||||
});
|
||||
+1
-1
@@ -46,7 +46,7 @@ gopeed.events.onError(async function (ctx) {
|
||||
}
|
||||
|
||||
try {
|
||||
await ctx.task.setUrl(await createRangeBlobUrl(req.rawUrl));
|
||||
await ctx.task.meta.req.setUrl(await createRangeBlobUrl(req.rawUrl));
|
||||
await req.putLabel('started', 'true');
|
||||
await req.putLabel('rebuilt', 'true');
|
||||
await ctx.task.continue();
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
gopeed.events.onError(async function (ctx) {
|
||||
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||
gopeed.logger.info("error", ctx.error);
|
||||
await ctx.task.setUrl("https://github.com");
|
||||
await ctx.task.meta.req.setUrl("https://github.com");
|
||||
await ctx.task.continue();
|
||||
});
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
gopeed.events.onStart(async function (ctx) {
|
||||
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||
await ctx.task.setUrl("https://github.com");
|
||||
await ctx.task.meta.req.setUrl("https://github.com");
|
||||
await ctx.task.meta.req.setHeaders({ "X-Gopeed-Test": "on-start" });
|
||||
await ctx.task.meta.req.putLabel('modified', 'true');
|
||||
});
|
||||
Reference in new issue
Block a user