mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 16:40:32 +00:00
fix: ffi instance be singleton
This commit is contained in:
1 parent
91dfd78b77
commit
4b4c03fd90
18 files changed
+844
-5336
No files matched your search
+10
-37
@@ -5,51 +5,24 @@ import (
|
||||
"errors"
|
||||
"github.com/GopeedLab/gopeed/pkg/api"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type instanceArray struct {
|
||||
mu sync.RWMutex
|
||||
array []*api.Instance
|
||||
}
|
||||
// Global singleton instance
|
||||
var instance *api.Instance
|
||||
|
||||
// append appends an instance to the array.
|
||||
// Returns the index of the instance in the array.
|
||||
func (ia *instanceArray) append(instance *api.Instance) int {
|
||||
ia.mu.Lock()
|
||||
defer ia.mu.Unlock()
|
||||
|
||||
ia.array = append(ia.array, instance)
|
||||
return len(ia.array) - 1
|
||||
}
|
||||
|
||||
func (ia *instanceArray) get(index int) (*api.Instance, bool) {
|
||||
ia.mu.RLock()
|
||||
defer ia.mu.RUnlock()
|
||||
if index < 0 || index >= len(ia.array) {
|
||||
return nil, false
|
||||
func Init(config *model.StartConfig) (err error) {
|
||||
if instance != nil {
|
||||
return nil
|
||||
}
|
||||
return ia.array[index], true
|
||||
}
|
||||
|
||||
var instances = &instanceArray{
|
||||
mu: sync.RWMutex{},
|
||||
array: make([]*api.Instance, 0),
|
||||
}
|
||||
|
||||
func Create(config *model.StartConfig) string {
|
||||
config.ProductionMode = true
|
||||
instance, err := api.Create(config)
|
||||
if err != nil {
|
||||
return BuildResult(err)
|
||||
}
|
||||
return BuildResult(instances.append(instance))
|
||||
instance, err = api.Create(config)
|
||||
return
|
||||
}
|
||||
|
||||
func Invoke(index int, request *api.Request) string {
|
||||
instance, ok := instances.get(index)
|
||||
if !ok {
|
||||
return BuildResult(errors.New("instance not found"))
|
||||
func Invoke(request *api.Request) string {
|
||||
if instance == nil {
|
||||
return BuildResult(errors.New("instance not initialized"))
|
||||
}
|
||||
return jsonEncode(api.Invoke(instance, request))
|
||||
}
|
||||
|
||||
@@ -10,22 +10,25 @@ import (
|
||||
|
||||
func main() {}
|
||||
|
||||
//export Create
|
||||
func Create(cfg *C.char) *C.char {
|
||||
//export Init
|
||||
func Init(cfg *C.char) *C.char {
|
||||
var config model.StartConfig
|
||||
if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil {
|
||||
return C.CString(bind.BuildResult(err))
|
||||
return C.CString(err.Error())
|
||||
}
|
||||
if err := bind.Init(&config); err != nil {
|
||||
return C.CString(err.Error())
|
||||
}
|
||||
|
||||
return C.CString(bind.Create(&config))
|
||||
return nil
|
||||
}
|
||||
|
||||
//export Invoke
|
||||
func Invoke(index int, req *C.char) *C.char {
|
||||
func Invoke(req *C.char) *C.char {
|
||||
var request api.Request
|
||||
if err := json.Unmarshal([]byte(C.GoString(req)), &request); err != nil {
|
||||
return C.CString(bind.BuildResult(err))
|
||||
}
|
||||
|
||||
return C.CString(bind.Invoke(index, &request))
|
||||
return C.CString(bind.Invoke(&request))
|
||||
}
|
||||
+4
-4
@@ -8,10 +8,10 @@ import (
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
)
|
||||
|
||||
func Create(cfg *model.StartConfig) string {
|
||||
return bind.Create(cfg)
|
||||
func Init(cfg *model.StartConfig) error {
|
||||
return bind.Init(cfg)
|
||||
}
|
||||
|
||||
func Invoke(index int, request *api.Request) string {
|
||||
return bind.Invoke(index, request)
|
||||
func Invoke(request *api.Request) string {
|
||||
return bind.Invoke(request)
|
||||
}
|
||||
+1
-1
@@ -60,7 +60,7 @@ func Start(cfg *model.StartConfig) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Server start success on http://%s:%d\n", result.Host, result.Port)
|
||||
fmt.Printf("Server start success on http://%s:%d\n", cfg.DownloadConfig.Http.Host, result)
|
||||
start()
|
||||
}
|
||||
|
||||
|
||||
+33
-32
@@ -17,12 +17,10 @@ import (
|
||||
type Instance struct {
|
||||
startCfg *model.StartConfig
|
||||
downloader *download.Downloader
|
||||
running bool
|
||||
|
||||
httpLock sync.Mutex
|
||||
srv *http.Server
|
||||
listener net.Listener
|
||||
runningPort int
|
||||
httpLock sync.Mutex
|
||||
srv *http.Server
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
func Create(startCfg *model.StartConfig) (*Instance, error) {
|
||||
@@ -51,44 +49,45 @@ func Create(startCfg *model.StartConfig) (*Instance, error) {
|
||||
return nil, err
|
||||
}
|
||||
i.downloader = downloader
|
||||
i.running = true
|
||||
i.httpLock = sync.Mutex{}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *Instance) StartHttp() *model.Result[*model.HttpListenResult] {
|
||||
func (i *Instance) StartHttp() *model.Result[any] {
|
||||
i.httpLock.Lock()
|
||||
defer i.httpLock.Unlock()
|
||||
|
||||
return i.startHttp()
|
||||
}
|
||||
|
||||
func (i *Instance) startHttp() *model.Result[*model.HttpListenResult] {
|
||||
func (i *Instance) startHttp() *model.Result[any] {
|
||||
httpCfg := i.getHttpConfig()
|
||||
if httpCfg == nil || !httpCfg.Enable {
|
||||
return model.NewErrorResult[any]("HTTP API server not enabled")
|
||||
}
|
||||
if i.srv != nil {
|
||||
return model.NewErrorResult[any]("HTTP API server already started")
|
||||
}
|
||||
|
||||
port, start, err := ListenHttp(httpCfg, i)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
httpCfg.RunningPort = port
|
||||
go start()
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) getHttpConfig() *base.DownloaderHttpConfig {
|
||||
var httpCfg *base.DownloaderHttpConfig
|
||||
// if startCfg has http config, first use it
|
||||
if i.startCfg.DownloadConfig != nil && i.startCfg.DownloadConfig.Http != nil {
|
||||
httpCfg = i.startCfg.DownloadConfig.Http
|
||||
} else {
|
||||
cfg, err := i.downloader.GetConfig()
|
||||
if err != nil {
|
||||
return model.NewErrorResult[*model.HttpListenResult](err.Error())
|
||||
}
|
||||
cfg, _ := i.downloader.GetConfig()
|
||||
httpCfg = cfg.Http
|
||||
}
|
||||
|
||||
if httpCfg == nil || !httpCfg.Enable {
|
||||
return model.NewErrorResult[*model.HttpListenResult]("HTTP API server not enabled")
|
||||
}
|
||||
if i.srv != nil {
|
||||
return model.NewErrorResult[*model.HttpListenResult]("HTTP API server already started")
|
||||
}
|
||||
result, start, err := ListenHttp(httpCfg, i)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[*model.HttpListenResult](err.Error())
|
||||
}
|
||||
go start()
|
||||
return model.NewOkResult[*model.HttpListenResult](result)
|
||||
return httpCfg
|
||||
}
|
||||
|
||||
func (i *Instance) StopHttp() *model.Result[any] {
|
||||
@@ -105,12 +104,15 @@ func (i *Instance) stopHttp() *model.Result[any] {
|
||||
}
|
||||
i.srv = nil
|
||||
i.listener = nil
|
||||
i.runningPort = 0
|
||||
}
|
||||
httpCfg := i.getHttpConfig()
|
||||
if httpCfg != nil {
|
||||
httpCfg.RunningPort = 0
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) RestartHttp() *model.Result[*model.HttpListenResult] {
|
||||
func (i *Instance) RestartHttp() *model.Result[any] {
|
||||
i.httpLock.Lock()
|
||||
defer i.httpLock.Unlock()
|
||||
|
||||
@@ -337,7 +339,6 @@ func (i *Instance) Close() *model.Result[any] {
|
||||
i.downloader.Logger.Warn().Err(err).Msg("close downloader failed")
|
||||
}
|
||||
i.downloader = nil
|
||||
i.running = false
|
||||
}
|
||||
|
||||
return model.NewNilResult()
|
||||
@@ -349,7 +350,7 @@ type Request struct {
|
||||
}
|
||||
|
||||
// Invoke support dynamic call method
|
||||
func Invoke(server *Instance, request *Request) (ret any) {
|
||||
func Invoke(instance *Instance, request *Request) (ret any) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
ret = model.NewErrorResult[any](fmt.Sprintf("%v", err))
|
||||
@@ -357,7 +358,7 @@ func Invoke(server *Instance, request *Request) (ret any) {
|
||||
}()
|
||||
|
||||
method, args := request.Method, request.Params
|
||||
dsType := reflect.ValueOf(server)
|
||||
dsType := reflect.ValueOf(instance)
|
||||
fn := dsType.MethodByName(method)
|
||||
numIn := fn.Type().NumIn()
|
||||
in := make([]reflect.Value, numIn)
|
||||
@@ -378,5 +379,5 @@ func Invoke(server *Instance, request *Request) (ret any) {
|
||||
in[i] = param
|
||||
}
|
||||
retVals := fn.Call(in)
|
||||
return retVals[0]
|
||||
return retVals[0].Interface()
|
||||
}
|
||||
+24
-648
@@ -1,668 +1,44 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
restPort int
|
||||
var instance, _ = Create(nil)
|
||||
|
||||
taskReq = &base.Request{
|
||||
Extra: map[string]any{
|
||||
"method": "",
|
||||
"header": map[string]string{
|
||||
"Usr-Agent": "gopeed",
|
||||
},
|
||||
"body": "",
|
||||
},
|
||||
}
|
||||
taskRes = &base.Resource{
|
||||
Size: test.BuildSize,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: test.BuildName,
|
||||
Path: "",
|
||||
Size: test.BuildSize,
|
||||
},
|
||||
},
|
||||
}
|
||||
createOpts = &base.Options{
|
||||
Path: test.Dir,
|
||||
Name: test.DownloadName,
|
||||
Extra: map[string]any{
|
||||
"connections": 2,
|
||||
},
|
||||
}
|
||||
createReq = &model.CreateTask{
|
||||
Req: taskReq,
|
||||
Opt: createOpts,
|
||||
}
|
||||
createResoledReq = &model.CreateTask{
|
||||
Req: taskReq,
|
||||
Opt: createOpts,
|
||||
}
|
||||
installExtensionReq = &model.InstallExtension{
|
||||
URL: "https://github.com/GopeedLab/gopeed-extension-samples#github-contributor-avatars-sample",
|
||||
}
|
||||
)
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
matchKeys := []string{"version", "runtime", "os", "arch", "inDocker"}
|
||||
doTest(func() {
|
||||
resp := httpRequestCheckOk[map[string]any](http.MethodGet, "/api/v1/info", nil)
|
||||
for _, key := range matchKeys {
|
||||
if _, ok := resp[key]; !ok {
|
||||
t.Errorf("Info() missing key = %v", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
func TestInvoke(t *testing.T) {
|
||||
doTestInvoke(t, "Info", []any{})
|
||||
doTestInvoke(t, "GetTasks", nil)
|
||||
doTestInvoke(t, "GetTasks", []any{})
|
||||
doTestInvoke(t, "GetTasks", []any{nil})
|
||||
doTestInvoke(t, "GetTasks", []any{&download.TaskFilter{}})
|
||||
}
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
doTest(func() {
|
||||
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
|
||||
if !test.AssertResourceEqual(taskRes, resp.Res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", test.ToJson(resp.Res), test.ToJson(taskRes))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", &model.CreateTask{
|
||||
Rid: resp.ID,
|
||||
Opt: createOpts,
|
||||
})
|
||||
if taskId == "" {
|
||||
t.Fatal("create task failed")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("CreateTask() got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateDirectTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
if taskId == "" {
|
||||
t.Fatal("create task failed")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("CreateDirectTask() got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateDirectTaskBatch(t *testing.T) {
|
||||
doTest(func() {
|
||||
reqs := make([]*base.Request, 0)
|
||||
for i := 0; i < 5; i++ {
|
||||
reqs = append(reqs, createReq.Req)
|
||||
}
|
||||
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{
|
||||
Reqs: reqs,
|
||||
})
|
||||
if len(taskIds) != len(reqs) {
|
||||
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", len(taskIds), len(reqs))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateDirectTaskWithResoled(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createResoledReq)
|
||||
if taskId == "" {
|
||||
t.Fatal("create task failed")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("CreateDirectTaskWithResoled() got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPauseAndContinueTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
switch event.Key {
|
||||
case download.EventKeyFinally:
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
t1 := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
if t1.Status != base.DownloadStatusRunning {
|
||||
t.Errorf("CreateTask() got = %v, want %v", t1.Status, base.DownloadStatusRunning)
|
||||
}
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/"+taskId+"/pause", nil)
|
||||
t2 := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
if t2.Status != base.DownloadStatusPause {
|
||||
t.Errorf("PauseTask() got = %v, want %v", t2.Status, base.DownloadStatusPause)
|
||||
}
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/"+taskId+"/continue", nil)
|
||||
t3 := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
if t3.Status != base.DownloadStatusRunning {
|
||||
t.Errorf("ContinueTask() got = %v, want %v", t3.Status, base.DownloadStatusRunning)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("PauseAndContinueTask() got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPauseAllAndContinueALLTasks(t *testing.T) {
|
||||
doTest(func() {
|
||||
cfg, err := Downloader.GetConfig()
|
||||
func doTestInvoke(t *testing.T, method string, params []any) *model.Result[any] {
|
||||
reqParams := make([]string, len(params))
|
||||
for i, p := range params {
|
||||
bytes, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
createAndPause := func() {
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
httpRequestCheckOk[*download.Task](http.MethodPut, "/api/v1/tasks/"+taskId+"/pause", nil)
|
||||
}
|
||||
|
||||
total := cfg.MaxRunning + 2
|
||||
for i := 0; i < total; i++ {
|
||||
createAndPause()
|
||||
}
|
||||
|
||||
// continue all
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil)
|
||||
if len(tasks) != cfg.MaxRunning {
|
||||
t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning)
|
||||
}
|
||||
// pause all
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil)
|
||||
if len(tasks) != total {
|
||||
t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId, nil)
|
||||
code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
checkCode(code, model.CodeTaskNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteTaskForce(t *testing.T) {
|
||||
doTest(func() {
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId+"?force=true", nil)
|
||||
code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
checkCode(code, model.CodeTaskNotFound)
|
||||
if _, err := os.Stat(test.DownloadFile); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Errorf("DeleteTaskForce() got = %v, want %v", err, os.ErrNotExist)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteAllTasks(t *testing.T) {
|
||||
doTest(func() {
|
||||
taskCount := 3
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(taskCount)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
for i := 0; i < taskCount; i++ {
|
||||
httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks?force=true", nil)
|
||||
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("DeleteTasks() got = %v, want %v", len(tasks), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteTasksByStatues(t *testing.T) {
|
||||
doTest(func() {
|
||||
taskCount := 3
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(taskCount)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
for i := 0; i < taskCount; i++ {
|
||||
httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
httpRequestCheckOk[any](http.MethodDelete, fmt.Sprintf("/api/v1/tasks?status=%s&force=true", base.DownloadStatusDone), nil)
|
||||
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("DeleteTasks() got = %v, want %v", len(tasks), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetTasks(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
httpRequestCheckOk[string](http.MethodPost, fmt.Sprintf("/api/v1/tasks?status=%s&status=%s",
|
||||
base.DownloadStatusReady, base.DownloadStatusRunning), createReq)
|
||||
httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
|
||||
|
||||
wg.Wait()
|
||||
r := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s",
|
||||
base.DownloadStatusDone), nil)
|
||||
if r[0].Status != base.DownloadStatusDone {
|
||||
t.Errorf("GetTasks() got = %v, want %v", r[0].Status, base.DownloadStatusDone)
|
||||
}
|
||||
r = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s,%s",
|
||||
base.DownloadStatusReady, base.DownloadStatusRunning), nil)
|
||||
if len(r) > 0 {
|
||||
t.Errorf("GetTasks() got = %v, want %v", len(r), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAndPutConfig(t *testing.T) {
|
||||
doTest(func() {
|
||||
cfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
||||
cfg.DownloadDir = "./download"
|
||||
cfg.Extra = map[string]any{
|
||||
"serverConfig": &struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}{
|
||||
Host: "127.0.0.1",
|
||||
Port: 8080,
|
||||
},
|
||||
"theme": "dark",
|
||||
}
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/config", cfg)
|
||||
|
||||
newCfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
||||
if !test.JsonEqual(cfg, newCfg) {
|
||||
t.Errorf("GetAndPutConfig() got = %v, want %v", test.ToJson(newCfg), test.ToJson(cfg))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstallExtension(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
if identity == "" {
|
||||
t.Errorf("InstallExtension() got = %v, want %v", identity, "not empty")
|
||||
}
|
||||
|
||||
// not a valid extension repository
|
||||
code, _ := httpRequest[string](http.MethodPost, "/api/v1/extensions", &model.InstallExtension{
|
||||
URL: "https://github.com/GopeedLab/gopeed",
|
||||
})
|
||||
checkCode(code, model.CodeError)
|
||||
|
||||
// not a git repository
|
||||
code, _ = httpRequest[string](http.MethodPost, "/api/v1/extensions", &model.InstallExtension{
|
||||
URL: "https://github.com",
|
||||
})
|
||||
checkCode(code, model.CodeError)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetExtensions(t *testing.T) {
|
||||
doTest(func() {
|
||||
httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
extensions := httpRequestCheckOk[[]*download.Extension](http.MethodGet, "/api/v1/extensions", nil)
|
||||
if len(extensions) == 0 {
|
||||
t.Errorf("GetExtensions() got = %v, want %v", len(extensions), "not empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateExtensionSettings(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/extensions/"+identity+"/settings", &model.UpdateExtensionSettings{
|
||||
Settings: map[string]any{
|
||||
"undefined": "test",
|
||||
"ua": "test",
|
||||
},
|
||||
})
|
||||
|
||||
settings := httpRequestCheckOk[*download.Extension](http.MethodGet, "/api/v1/extensions/"+identity, nil).Settings
|
||||
if len(settings) != 1 {
|
||||
t.Errorf("UpdateExtensionSettings() got = %v, want %v", len(settings), 1)
|
||||
}
|
||||
|
||||
if settings[0].Name != "ua" || settings[0].Value != "test" {
|
||||
t.Errorf("UpdateExtensionSettings() got = %v, want %v", settings[0].Value, "test")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSwitchExtension(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/extensions/"+identity+"/switch", &model.SwitchExtension{
|
||||
Status: false,
|
||||
})
|
||||
extensions := httpRequestCheckOk[[]*download.Extension](http.MethodGet, "/api/v1/extensions", nil)
|
||||
if !extensions[0].Disabled {
|
||||
t.Errorf("TestSwitchExtension() got = %v, want %v", extensions[0].Disabled, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteExtension(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/extensions/"+identity, nil)
|
||||
extensions := httpRequestCheckOk[[]*download.Extension](http.MethodGet, "/api/v1/extensions", nil)
|
||||
if len(extensions) != 0 {
|
||||
t.Errorf("TestDeleteExtension() got = %v, want %v", len(extensions), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateCheckExtension(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
resp := httpRequestCheckOk[*model.UpgradeCheckExtensionResp](http.MethodGet, "/api/v1/extensions/"+identity+"/update", nil)
|
||||
// no new version
|
||||
if resp.NewVersion != "" {
|
||||
t.Errorf("UpgradeCheckExtension() got = %v, want %v", resp.NewVersion, "")
|
||||
}
|
||||
// force update
|
||||
httpRequestCheckOk[any](http.MethodPost, "/api/v1/extensions/"+identity+"/update", nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFsExtension(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
statusCode, _ := doHttpRequest0(http.MethodGet, "/fs/extensions/"+identity+"/icon.png", nil, nil)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Errorf("FsExtension() got = %v, want %v", statusCode, http.StatusOK)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFsExtensionFail(t *testing.T) {
|
||||
doTest(func() {
|
||||
statusCode, _ := doHttpRequest0(http.MethodGet, "/fs/extensions/not_exist/icon.png", nil, nil)
|
||||
if statusCode != http.StatusNotFound {
|
||||
t.Errorf("TestFsExtensionFail() got = %v, want %v", statusCode, http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDoProxy(t *testing.T) {
|
||||
doTest(func() {
|
||||
code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
|
||||
"X-Target-Uri": "https://github.com/GopeedLab/gopeed/raw/695da7ea87d2b455552b709d3cb4d7879484d4d1/README.md",
|
||||
}, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Errorf("DoProxy() got = %v, want %v", code, http.StatusOK)
|
||||
}
|
||||
want := "4ee193b676f1ebb2ad810e016350d52a"
|
||||
got := fmt.Sprintf("%x", md5.Sum(respBody))
|
||||
if got != want {
|
||||
t.Errorf("DoProxy() got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
doTest(func() {
|
||||
code, _ := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
|
||||
"X-Target-Uri": "https://github.com/GopeedLab/gopeed/raw/695da7ea87d2b455552b709d3cb4d7879484d4d1/NOT_FOUND",
|
||||
}, nil)
|
||||
if code != http.StatusNotFound {
|
||||
t.Errorf("DoProxy() got = %v, want %v", code, http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestApiToken(t *testing.T) {
|
||||
var cfg = &model.StartConfig{}
|
||||
cfg.Init()
|
||||
cfg.ApiToken = "123456"
|
||||
fileListener := doStart(cfg)
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Stop()
|
||||
}()
|
||||
|
||||
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||
if status != http.StatusUnauthorized {
|
||||
t.Errorf("TestApiToken() got = %v, want %v", status, http.StatusUnauthorized)
|
||||
reqParams[i] = string(bytes)
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"X-Api-Token": cfg.ApiToken,
|
||||
}, nil)
|
||||
if status != http.StatusOK {
|
||||
t.Errorf("TestApiToken() got = %v, want %v", status, http.StatusOK)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAuthorization(t *testing.T) {
|
||||
var cfg = &model.StartConfig{}
|
||||
cfg.Init()
|
||||
cfg.ApiToken = "123456"
|
||||
cfg.WebEnable = true
|
||||
cfg.WebBasicAuth = &model.WebBasicAuth{
|
||||
Username: "admin",
|
||||
Password: "123456",
|
||||
}
|
||||
fileListener := doStart(cfg)
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Stop()
|
||||
}()
|
||||
|
||||
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||
if status != http.StatusUnauthorized {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"Authorization": cfg.WebBasicAuth.Authorization(),
|
||||
}, nil)
|
||||
if status != http.StatusOK {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"X-Api-Token": cfg.ApiToken,
|
||||
}, nil)
|
||||
if status != http.StatusOK {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func doTest(handler func()) {
|
||||
testFunc := func(storage model.Storage) {
|
||||
var cfg = &model.StartConfig{}
|
||||
cfg.Init()
|
||||
cfg.Storage = storage
|
||||
cfg.StorageDir = ".test_storage"
|
||||
cfg.WebEnable = true
|
||||
fileListener := doStart(cfg)
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Stop()
|
||||
Downloader.Clear()
|
||||
}()
|
||||
defer func() {
|
||||
Downloader.Pause(nil)
|
||||
Downloader.Delete(nil, true)
|
||||
os.RemoveAll(cfg.StorageDir)
|
||||
}()
|
||||
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
||||
handler()
|
||||
}
|
||||
testFunc(model.StorageMem)
|
||||
testFunc(model.StorageBolt)
|
||||
}
|
||||
|
||||
func doStart(cfg *model.StartConfig) net.Listener {
|
||||
port, err := Start(cfg)
|
||||
result := Invoke(instance, &Request{
|
||||
Method: method,
|
||||
Params: reqParams,
|
||||
})
|
||||
buf, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
restPort = port
|
||||
return test.StartTestFileServer()
|
||||
}
|
||||
|
||||
func doHttpRequest0(method string, path string, headers map[string]string, body any) (int, []byte) {
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
buf, _ := json.Marshal(body)
|
||||
reader = bytes.NewBuffer(buf)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(method, fmt.Sprintf("http://127.0.0.1:%d%s", restPort, path), reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if headers != nil {
|
||||
for k, v := range headers {
|
||||
request.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
respBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return response.StatusCode, respBody
|
||||
}
|
||||
|
||||
func doHttpRequest[T any](method string, path string, headers map[string]string, body any) (int, *model.Result[T]) {
|
||||
statusCode, respBody := doHttpRequest0(method, path, headers, body)
|
||||
if statusCode != http.StatusOK {
|
||||
panic(fmt.Sprintf("http request failed, status code: %d", statusCode))
|
||||
}
|
||||
|
||||
var r model.Result[T]
|
||||
if err := json.Unmarshal(respBody, &r); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return int(r.Code), &r
|
||||
}
|
||||
|
||||
func httpRequest[T any](method string, path string, body any) (int, *model.Result[T]) {
|
||||
return doHttpRequest[T](method, path, nil, body)
|
||||
}
|
||||
|
||||
func httpRequestCheckOk[T any](method string, path string, body any) T {
|
||||
code, result := httpRequest[T](method, path, body)
|
||||
checkOk(code)
|
||||
return result.Data
|
||||
}
|
||||
|
||||
func checkOk(code int) {
|
||||
checkCode(code, model.CodeOk)
|
||||
}
|
||||
|
||||
func checkCode(code int, exceptCode model.RespCode) {
|
||||
if code != int(exceptCode) {
|
||||
panic(fmt.Sprintf("code got = %d, want %d", code, exceptCode))
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var res model.Result[any]
|
||||
if err := json.Unmarshal(buf, &res); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &res
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
instance, _ = Create(nil)
|
||||
restPort int
|
||||
|
||||
taskReq = &base.Request{
|
||||
Extra: map[string]any{
|
||||
"method": "",
|
||||
"header": map[string]string{
|
||||
"Usr-Agent": "gopeed",
|
||||
},
|
||||
"body": "",
|
||||
},
|
||||
}
|
||||
taskRes = &base.Resource{
|
||||
Size: test.BuildSize,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: test.BuildName,
|
||||
Path: "",
|
||||
Size: test.BuildSize,
|
||||
},
|
||||
},
|
||||
}
|
||||
createOpts = &base.Options{
|
||||
Path: test.Dir,
|
||||
Name: test.DownloadName,
|
||||
Extra: map[string]any{
|
||||
"connections": 2,
|
||||
},
|
||||
}
|
||||
createReq = &model.CreateTask{
|
||||
Req: taskReq,
|
||||
Opt: createOpts,
|
||||
}
|
||||
createResoledReq = &model.CreateTask{
|
||||
Req: taskReq,
|
||||
Opt: createOpts,
|
||||
}
|
||||
installExtensionReq = &model.InstallExtension{
|
||||
URL: "https://github.com/GopeedLab/gopeed-extension-samples#github-contributor-avatars-sample",
|
||||
}
|
||||
)
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
matchKeys := []string{"version", "runtime", "os", "arch", "inDocker"}
|
||||
doTest(func() {
|
||||
resp := httpRequestCheckOk[map[string]any](http.MethodGet, "/api/v1/info", nil)
|
||||
for _, key := range matchKeys {
|
||||
if _, ok := resp[key]; !ok {
|
||||
t.Errorf("Info() missing key = %v", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
doTest(func() {
|
||||
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
|
||||
if !test.AssertResourceEqual(taskRes, resp.Res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", test.ToJson(resp.Res), test.ToJson(taskRes))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", &model.CreateTask{
|
||||
Rid: resp.ID,
|
||||
Opt: createOpts,
|
||||
})
|
||||
if taskId == "" {
|
||||
t.Fatal("create task failed")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("CreateTask() got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateDirectTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
if taskId == "" {
|
||||
t.Fatal("create task failed")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("CreateDirectTask() got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateDirectTaskBatch(t *testing.T) {
|
||||
doTest(func() {
|
||||
reqs := make([]*base.Request, 0)
|
||||
for i := 0; i < 5; i++ {
|
||||
reqs = append(reqs, createReq.Req)
|
||||
}
|
||||
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{
|
||||
Reqs: reqs,
|
||||
})
|
||||
if len(taskIds) != len(reqs) {
|
||||
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", len(taskIds), len(reqs))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateDirectTaskWithResoled(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createResoledReq)
|
||||
if taskId == "" {
|
||||
t.Fatal("create task failed")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("CreateDirectTaskWithResoled() got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPauseAndContinueTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
switch event.Key {
|
||||
case download.EventKeyFinally:
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
t1 := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
if t1.Status != base.DownloadStatusRunning {
|
||||
t.Errorf("CreateTask() got = %v, want %v", t1.Status, base.DownloadStatusRunning)
|
||||
}
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/"+taskId+"/pause", nil)
|
||||
t2 := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
if t2.Status != base.DownloadStatusPause {
|
||||
t.Errorf("PauseTask() got = %v, want %v", t2.Status, base.DownloadStatusPause)
|
||||
}
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/"+taskId+"/continue", nil)
|
||||
t3 := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
if t3.Status != base.DownloadStatusRunning {
|
||||
t.Errorf("ContinueTask() got = %v, want %v", t3.Status, base.DownloadStatusRunning)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("PauseAndContinueTask() got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPauseAllAndContinueALLTasks(t *testing.T) {
|
||||
doTest(func() {
|
||||
cfg, err := Downloader.GetConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
createAndPause := func() {
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
httpRequestCheckOk[*download.Task](http.MethodPut, "/api/v1/tasks/"+taskId+"/pause", nil)
|
||||
}
|
||||
|
||||
total := cfg.MaxRunning + 2
|
||||
for i := 0; i < total; i++ {
|
||||
createAndPause()
|
||||
}
|
||||
|
||||
// continue all
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil)
|
||||
if len(tasks) != cfg.MaxRunning {
|
||||
t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning)
|
||||
}
|
||||
// pause all
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil)
|
||||
if len(tasks) != total {
|
||||
t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId, nil)
|
||||
code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
checkCode(code, model.CodeTaskNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteTaskForce(t *testing.T) {
|
||||
doTest(func() {
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId+"?force=true", nil)
|
||||
code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
checkCode(code, model.CodeTaskNotFound)
|
||||
if _, err := os.Stat(test.DownloadFile); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Errorf("DeleteTaskForce() got = %v, want %v", err, os.ErrNotExist)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteAllTasks(t *testing.T) {
|
||||
doTest(func() {
|
||||
taskCount := 3
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(taskCount)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
for i := 0; i < taskCount; i++ {
|
||||
httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks?force=true", nil)
|
||||
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("DeleteTasks() got = %v, want %v", len(tasks), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteTasksByStatues(t *testing.T) {
|
||||
doTest(func() {
|
||||
taskCount := 3
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(taskCount)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
for i := 0; i < taskCount; i++ {
|
||||
httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
httpRequestCheckOk[any](http.MethodDelete, fmt.Sprintf("/api/v1/tasks?status=%s&force=true", base.DownloadStatusDone), nil)
|
||||
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("DeleteTasks() got = %v, want %v", len(tasks), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetTasks(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
httpRequestCheckOk[string](http.MethodPost, fmt.Sprintf("/api/v1/tasks?status=%s&status=%s",
|
||||
base.DownloadStatusReady, base.DownloadStatusRunning), createReq)
|
||||
httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
|
||||
|
||||
wg.Wait()
|
||||
r := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s",
|
||||
base.DownloadStatusDone), nil)
|
||||
if r[0].Status != base.DownloadStatusDone {
|
||||
t.Errorf("GetTasks() got = %v, want %v", r[0].Status, base.DownloadStatusDone)
|
||||
}
|
||||
r = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s,%s",
|
||||
base.DownloadStatusReady, base.DownloadStatusRunning), nil)
|
||||
if len(r) > 0 {
|
||||
t.Errorf("GetTasks() got = %v, want %v", len(r), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAndPutConfig(t *testing.T) {
|
||||
doTest(func() {
|
||||
cfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
||||
cfg.DownloadDir = "./download"
|
||||
cfg.Extra = map[string]any{
|
||||
"serverConfig": &struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}{
|
||||
Host: "127.0.0.1",
|
||||
Port: 8080,
|
||||
},
|
||||
"theme": "dark",
|
||||
}
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/config", cfg)
|
||||
|
||||
newCfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
||||
if !test.JsonEqual(cfg, newCfg) {
|
||||
t.Errorf("GetAndPutConfig() got = %v, want %v", test.ToJson(newCfg), test.ToJson(cfg))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstallExtension(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
if identity == "" {
|
||||
t.Errorf("InstallExtension() got = %v, want %v", identity, "not empty")
|
||||
}
|
||||
|
||||
// not a valid extension repository
|
||||
code, _ := httpRequest[string](http.MethodPost, "/api/v1/extensions", &model.InstallExtension{
|
||||
URL: "https://github.com/GopeedLab/gopeed",
|
||||
})
|
||||
checkCode(code, model.CodeError)
|
||||
|
||||
// not a git repository
|
||||
code, _ = httpRequest[string](http.MethodPost, "/api/v1/extensions", &model.InstallExtension{
|
||||
URL: "https://github.com",
|
||||
})
|
||||
checkCode(code, model.CodeError)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetExtensions(t *testing.T) {
|
||||
doTest(func() {
|
||||
httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
extensions := httpRequestCheckOk[[]*download.Extension](http.MethodGet, "/api/v1/extensions", nil)
|
||||
if len(extensions) == 0 {
|
||||
t.Errorf("GetExtensions() got = %v, want %v", len(extensions), "not empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateExtensionSettings(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/extensions/"+identity+"/settings", &model.UpdateExtensionSettings{
|
||||
Settings: map[string]any{
|
||||
"undefined": "test",
|
||||
"ua": "test",
|
||||
},
|
||||
})
|
||||
|
||||
settings := httpRequestCheckOk[*download.Extension](http.MethodGet, "/api/v1/extensions/"+identity, nil).Settings
|
||||
if len(settings) != 1 {
|
||||
t.Errorf("UpdateExtensionSettings() got = %v, want %v", len(settings), 1)
|
||||
}
|
||||
|
||||
if settings[0].Name != "ua" || settings[0].Value != "test" {
|
||||
t.Errorf("UpdateExtensionSettings() got = %v, want %v", settings[0].Value, "test")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSwitchExtension(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/extensions/"+identity+"/switch", &model.SwitchExtension{
|
||||
Status: false,
|
||||
})
|
||||
extensions := httpRequestCheckOk[[]*download.Extension](http.MethodGet, "/api/v1/extensions", nil)
|
||||
if !extensions[0].Disabled {
|
||||
t.Errorf("TestSwitchExtension() got = %v, want %v", extensions[0].Disabled, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteExtension(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/extensions/"+identity, nil)
|
||||
extensions := httpRequestCheckOk[[]*download.Extension](http.MethodGet, "/api/v1/extensions", nil)
|
||||
if len(extensions) != 0 {
|
||||
t.Errorf("TestDeleteExtension() got = %v, want %v", len(extensions), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateCheckExtension(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
resp := httpRequestCheckOk[*model.UpgradeCheckExtensionResp](http.MethodGet, "/api/v1/extensions/"+identity+"/update", nil)
|
||||
// no new version
|
||||
if resp.NewVersion != "" {
|
||||
t.Errorf("UpgradeCheckExtension() got = %v, want %v", resp.NewVersion, "")
|
||||
}
|
||||
// force update
|
||||
httpRequestCheckOk[any](http.MethodPost, "/api/v1/extensions/"+identity+"/update", nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFsExtension(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
statusCode, _ := doHttpRequest0(http.MethodGet, "/fs/extensions/"+identity+"/icon.png", nil, nil)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Errorf("FsExtension() got = %v, want %v", statusCode, http.StatusOK)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFsExtensionFail(t *testing.T) {
|
||||
doTest(func() {
|
||||
statusCode, _ := doHttpRequest0(http.MethodGet, "/fs/extensions/not_exist/icon.png", nil, nil)
|
||||
if statusCode != http.StatusNotFound {
|
||||
t.Errorf("TestFsExtensionFail() got = %v, want %v", statusCode, http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDoProxy(t *testing.T) {
|
||||
doTest(func() {
|
||||
code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
|
||||
"X-Target-Uri": "https://github.com/GopeedLab/gopeed/raw/695da7ea87d2b455552b709d3cb4d7879484d4d1/README.md",
|
||||
}, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Errorf("DoProxy() got = %v, want %v", code, http.StatusOK)
|
||||
}
|
||||
want := "4ee193b676f1ebb2ad810e016350d52a"
|
||||
got := fmt.Sprintf("%x", md5.Sum(respBody))
|
||||
if got != want {
|
||||
t.Errorf("DoProxy() got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
doTest(func() {
|
||||
code, _ := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
|
||||
"X-Target-Uri": "https://github.com/GopeedLab/gopeed/raw/695da7ea87d2b455552b709d3cb4d7879484d4d1/NOT_FOUND",
|
||||
}, nil)
|
||||
if code != http.StatusNotFound {
|
||||
t.Errorf("DoProxy() got = %v, want %v", code, http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestApiToken(t *testing.T) {
|
||||
var cfg = &model.StartConfig{}
|
||||
cfg.Init()
|
||||
cfg.ApiToken = "123456"
|
||||
fileListener := doStart(cfg)
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Stop()
|
||||
}()
|
||||
|
||||
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||
if status != http.StatusUnauthorized {
|
||||
t.Errorf("TestApiToken() got = %v, want %v", status, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"X-Api-Token": cfg.ApiToken,
|
||||
}, nil)
|
||||
if status != http.StatusOK {
|
||||
t.Errorf("TestApiToken() got = %v, want %v", status, http.StatusOK)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAuthorization(t *testing.T) {
|
||||
var cfg = &model.StartConfig{}
|
||||
cfg.Init()
|
||||
cfg.ApiToken = "123456"
|
||||
cfg.WebEnable = true
|
||||
cfg.WebBasicAuth = &model.WebBasicAuth{
|
||||
Username: "admin",
|
||||
Password: "123456",
|
||||
}
|
||||
fileListener := doStart(cfg)
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Stop()
|
||||
}()
|
||||
|
||||
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||
if status != http.StatusUnauthorized {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"Authorization": cfg.WebBasicAuth.Authorization(),
|
||||
}, nil)
|
||||
if status != http.StatusOK {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"X-Api-Token": cfg.ApiToken,
|
||||
}, nil)
|
||||
if status != http.StatusOK {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func doTest(handler func()) {
|
||||
testFunc := func(storage model.Storage) {
|
||||
var cfg = &model.StartConfig{}
|
||||
cfg.Init()
|
||||
cfg.Storage = storage
|
||||
cfg.StorageDir = ".test_storage"
|
||||
cfg.WebEnable = true
|
||||
fileListener := doStart(cfg)
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Stop()
|
||||
Downloader.Clear()
|
||||
}()
|
||||
defer func() {
|
||||
Downloader.Pause(nil)
|
||||
Downloader.Delete(nil, true)
|
||||
os.RemoveAll(cfg.StorageDir)
|
||||
}()
|
||||
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
||||
handler()
|
||||
}
|
||||
testFunc(model.StorageMem)
|
||||
testFunc(model.StorageBolt)
|
||||
}
|
||||
|
||||
func doStart(cfg *model.StartConfig) net.Listener {
|
||||
port, err := Start(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
restPort = port
|
||||
return test.StartTestFileServer()
|
||||
}
|
||||
|
||||
func doHttpRequest0(method string, path string, headers map[string]string, body any) (int, []byte) {
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
buf, _ := json.Marshal(body)
|
||||
reader = bytes.NewBuffer(buf)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(method, fmt.Sprintf("http://127.0.0.1:%d%s", restPort, path), reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if headers != nil {
|
||||
for k, v := range headers {
|
||||
request.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
respBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return response.StatusCode, respBody
|
||||
}
|
||||
|
||||
func doHttpRequest[T any](method string, path string, headers map[string]string, body any) (int, *model.Result[T]) {
|
||||
statusCode, respBody := doHttpRequest0(method, path, headers, body)
|
||||
if statusCode != http.StatusOK {
|
||||
panic(fmt.Sprintf("http request failed, status code: %d", statusCode))
|
||||
}
|
||||
|
||||
var r model.Result[T]
|
||||
if err := json.Unmarshal(respBody, &r); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return int(r.Code), &r
|
||||
}
|
||||
|
||||
func httpRequest[T any](method string, path string, body any) (int, *model.Result[T]) {
|
||||
return doHttpRequest[T](method, path, nil, body)
|
||||
}
|
||||
|
||||
func httpRequestCheckOk[T any](method string, path string, body any) T {
|
||||
code, result := httpRequest[T](method, path, body)
|
||||
checkOk(code)
|
||||
return result.Data
|
||||
}
|
||||
|
||||
func checkOk(code int) {
|
||||
checkCode(code, model.CodeOk)
|
||||
}
|
||||
|
||||
func checkCode(code int, exceptCode model.RespCode) {
|
||||
if code != int(exceptCode) {
|
||||
panic(fmt.Sprintf("code got = %d, want %d", code, exceptCode))
|
||||
}
|
||||
}
|
||||
+7
-10
@@ -22,7 +22,7 @@ import (
|
||||
|
||||
const instanceKey string = "apiInstance"
|
||||
|
||||
func ListenHttp(httpCfg *base.DownloaderHttpConfig, ai *Instance) (*model.HttpListenResult, func(), error) {
|
||||
func ListenHttp(httpCfg *base.DownloaderHttpConfig, ai *Instance) (int, func(), error) {
|
||||
var r = mux.NewRouter()
|
||||
r.Methods(http.MethodGet).Path("/api/v1/info").HandlerFunc(Info)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/resolve").HandlerFunc(Resolve)
|
||||
@@ -106,19 +106,16 @@ func ListenHttp(httpCfg *base.DownloaderHttpConfig, ai *Instance) (*model.HttpLi
|
||||
|
||||
listener, err := net.Listen("tcp", net.JoinHostPort(httpCfg.Host, fmt.Sprintf("%d", httpCfg.Port)))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return 0, nil, err
|
||||
}
|
||||
ai.srv = srv
|
||||
ai.listener = listener
|
||||
|
||||
return &model.HttpListenResult{
|
||||
Host: httpCfg.Host,
|
||||
Port: listener.Addr().(*net.TCPAddr).Port,
|
||||
}, func() {
|
||||
if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
ai.downloader.Logger.Error().Err(err).Msg("http server listen error")
|
||||
}
|
||||
}, nil
|
||||
return listener.Addr().(*net.TCPAddr).Port, func() {
|
||||
if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
ai.downloader.Logger.Error().Err(err).Msg("http server listen error")
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getInstance(r *http.Request) *Instance {
|
||||
|
||||
@@ -228,6 +228,8 @@ type DownloaderHttpConfig struct {
|
||||
Port int `json:"port"`
|
||||
// ApiToken is the auth token for http API
|
||||
ApiToken string `json:"apiToken"`
|
||||
// RunningPort is the http server real listen port.
|
||||
RunningPort int `json:"runningPort"`
|
||||
}
|
||||
|
||||
type DownloaderProxyConfig struct {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
/* package github.com/GopeedLab/gopeed/bind/desktop */
|
||||
|
||||
|
||||
#line 1 "cgo-builtin-export-prolog"
|
||||
|
||||
#include <stddef.h>
|
||||
@@ -10,24 +11,19 @@
|
||||
#define GO_CGO_EXPORT_PROLOGUE_H
|
||||
|
||||
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||
typedef struct {
|
||||
const char *p;
|
||||
ptrdiff_t n;
|
||||
} _GoString_;
|
||||
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
/* Start of preamble from import "C" comments. */
|
||||
|
||||
#line 3 "main.go"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#line 1 "cgo-generated-wrapper"
|
||||
|
||||
/* End of preamble from import "C" comments. */
|
||||
|
||||
|
||||
/* Start of boilerplate cgo prologue. */
|
||||
#line 1 "cgo-gcc-export-header-prolog"
|
||||
|
||||
@@ -60,22 +56,15 @@ typedef double _Complex GoComplex128;
|
||||
static assertion to make sure the file is being used on architecture
|
||||
at least with matching size of GoInt.
|
||||
*/
|
||||
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void *) == 64 / 8 ? 1 : -1];
|
||||
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
|
||||
|
||||
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||
typedef _GoString_ GoString;
|
||||
#endif
|
||||
typedef void *GoMap;
|
||||
typedef void *GoChan;
|
||||
typedef struct {
|
||||
void *t;
|
||||
void *v;
|
||||
} GoInterface;
|
||||
typedef struct {
|
||||
void *data;
|
||||
GoInt len;
|
||||
GoInt cap;
|
||||
} GoSlice;
|
||||
typedef struct { void *t; void *v; } GoInterface;
|
||||
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -85,8 +74,8 @@ typedef struct {
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern __declspec(dllexport) char *Create(char *cfg);
|
||||
extern __declspec(dllexport) char *Invoke(GoInt index, char *req);
|
||||
extern __declspec(dllexport) char* Init(char* cfg);
|
||||
extern __declspec(dllexport) char* Invoke(char* req);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -33,9 +33,10 @@ import 'libgopeed_boot_base.dart';
|
||||
|
||||
LibgopeedBoot create() => LibgopeedBootNative();
|
||||
|
||||
class LibgopeedBootNative with LibgopeedBootBase implements LibgopeedBoot {
|
||||
class LibgopeedBootNative
|
||||
with LibgopeedBootBase
|
||||
implements LibgopeedBoot, LibgopeedApi {
|
||||
late LibgopeedAbi _libgopeedAbi;
|
||||
late int _apiInstance;
|
||||
late Dio _dio;
|
||||
|
||||
LibgopeedBootNative() {
|
||||
@@ -69,8 +70,7 @@ class LibgopeedBootNative with LibgopeedBootBase implements LibgopeedBoot {
|
||||
}
|
||||
return jsonEncode(e);
|
||||
}).toList());
|
||||
final resp =
|
||||
await _libgopeedAbi.invoke(_apiInstance, jsonEncode(invokeRequest));
|
||||
final resp = await _libgopeedAbi.invoke(jsonEncode(invokeRequest));
|
||||
final result =
|
||||
Result<T>.fromJson(jsonDecode(resp), fromJsonT ??= (json) => null as T);
|
||||
return handleResult(result);
|
||||
@@ -78,87 +78,86 @@ class LibgopeedBootNative with LibgopeedBootBase implements LibgopeedBoot {
|
||||
|
||||
List<dynamic> _parseTaskFilter(TaskFilter? filter) {
|
||||
if (filter == null) {
|
||||
return [];
|
||||
return [null];
|
||||
}
|
||||
return [filter];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> init(StartConfig cfg) async {
|
||||
final result = await _libgopeedAbi.create(jsonEncode(cfg.toJson()));
|
||||
_apiInstance =
|
||||
Result.fromJson(jsonDecode(result), (data) => data as int).data!;
|
||||
Future<LibgopeedApi> init(StartConfig cfg) async {
|
||||
await _libgopeedAbi.init(jsonEncode(cfg.toJson()));
|
||||
_dio = createDio();
|
||||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpListenResult> startHttp() async {
|
||||
return _invoke<HttpListenResult>(
|
||||
"startHttp", [], (json) => HttpListenResult.fromJson(json));
|
||||
"StartHttp", [], (json) => HttpListenResult.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopHttp() async {
|
||||
await _invoke<void>("stopHttp", [], null);
|
||||
await _invoke<void>("StopHttp", [], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> restartHttp() async {
|
||||
await _invoke<void>("restartHttp", [], null);
|
||||
await _invoke<void>("RestartHttp", [], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Info> info() async {
|
||||
return _invoke<Info>("info", [], (json) => Info.fromJson(json));
|
||||
return _invoke<Info>("Info", [], (json) => Info.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ResolveResult> resolve(Request request) async {
|
||||
return _invoke<ResolveResult>(
|
||||
"resolve", [request], (json) => ResolveResult.fromJson(json));
|
||||
"Resolve", [request], (json) => ResolveResult.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> createTask(CreateTask createTask) async {
|
||||
return _invoke<String>(
|
||||
"createTask", [createTask], (json) => json as String);
|
||||
"CreateTask", [createTask], (json) => json as String);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> createTaskBatch(CreateTaskBatch createTask) async {
|
||||
return _invoke<String>(
|
||||
"createTaskBatch", [createTask], (json) => json as String);
|
||||
"CreateTaskBatch", [createTask], (json) => json as String);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pauseTask(String id) async {
|
||||
await _invoke<void>("pauseTask", [id], null);
|
||||
await _invoke<void>("PauseTask", [id], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pauseTasks(TaskFilter? filter) async {
|
||||
await _invoke<void>("pauseTasks", _parseTaskFilter(filter), null);
|
||||
await _invoke<void>("PauseTasks", _parseTaskFilter(filter), null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> continueTask(String id) async {
|
||||
await _invoke<void>("continueTask", [id], null);
|
||||
await _invoke<void>("ContinueTask", [id], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> continueTasks(TaskFilter? filter) async {
|
||||
await _invoke<void>("continueTasks", _parseTaskFilter(filter), null);
|
||||
await _invoke<void>("ContinueTasks", _parseTaskFilter(filter), null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteTask(String id, bool force) async {
|
||||
await _invoke<void>("deleteTask", [id, force], null);
|
||||
await _invoke<void>("DeleteTask", [id, force], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteTasks(TaskFilter? filter, bool force) async {
|
||||
await _invoke<void>(
|
||||
"deleteTasks",
|
||||
"DeleteTasks",
|
||||
_parseTaskFilter(filter).apply((it) {
|
||||
it.add(force);
|
||||
}),
|
||||
@@ -167,40 +166,40 @@ class LibgopeedBootNative with LibgopeedBootBase implements LibgopeedBoot {
|
||||
|
||||
@override
|
||||
Future<Task> getTask(String id) async {
|
||||
return _invoke<Task>("getTask", [id], (json) => Task.fromJson(json));
|
||||
return _invoke<Task>("GetTask", [id], (json) => Task.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Task>> getTasks(TaskFilter? filter) async {
|
||||
return _invoke<List<Task>>("getTasks", _parseTaskFilter(filter),
|
||||
return _invoke<List<Task>>("GetTasks", _parseTaskFilter(filter),
|
||||
(json) => json.map((e) => Task.fromJson(e)).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getTaskStats(String id) async {
|
||||
return _invoke<Map<String, dynamic>>(
|
||||
"getTaskStats", [id], (json) => json as Map<String, dynamic>);
|
||||
"GetTaskStats", [id], (json) => json as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DownloaderConfig> getConfig() async {
|
||||
return _invoke<DownloaderConfig>(
|
||||
"getConfig", [], (json) => DownloaderConfig.fromJson(json));
|
||||
"GetConfig", [], (json) => DownloaderConfig.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> putConfig(DownloaderConfig config) async {
|
||||
await _invoke<void>("putConfig", [config], null);
|
||||
await _invoke<void>("PutConfig", [config], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> installExtension(InstallExtension installExtension) async {
|
||||
await _invoke<void>("installExtension", [installExtension], null);
|
||||
await _invoke<void>("InstallExtension", [installExtension], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Extension>> getExtensions() async {
|
||||
return _invoke<List<Extension>>("getExtensions", [],
|
||||
return _invoke<List<Extension>>("GetExtensions", [],
|
||||
(json) => json.map((e) => Extension.fromJson(e)).toList());
|
||||
}
|
||||
|
||||
@@ -208,33 +207,35 @@ class LibgopeedBootNative with LibgopeedBootBase implements LibgopeedBoot {
|
||||
Future<void> updateExtensionSettings(
|
||||
String identity, UpdateExtensionSettings updateExtensionSettings) async {
|
||||
await _invoke<void>(
|
||||
"updateExtensionSettings", [identity, updateExtensionSettings], null);
|
||||
"UpdateExtensionSettings", [identity, updateExtensionSettings], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> switchExtension(
|
||||
String identity, SwitchExtension switchExtension) async {}
|
||||
String identity, SwitchExtension switchExtension) async {
|
||||
await _invoke<void>("SwitchExtension", [identity, switchExtension], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteExtension(String identity) async {
|
||||
await _invoke<void>("deleteExtension", [identity], null);
|
||||
await _invoke<void>("DeleteExtension", [identity], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UpdateCheckExtensionResp> upgradeCheckExtension(
|
||||
String identity) async {
|
||||
return _invoke<UpdateCheckExtensionResp>("upgradeCheckExtension",
|
||||
return _invoke<UpdateCheckExtensionResp>("UpgradeCheckExtension",
|
||||
[identity], (json) => UpdateCheckExtensionResp.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> upgradeExtension(String identity) async {
|
||||
await _invoke<void>("upgradeExtension", [identity], null);
|
||||
await _invoke<void>("UpgradeExtension", [identity], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _invoke<void>("close", [], null);
|
||||
await _invoke<void>("Close", [], null);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -20,12 +20,15 @@ import '../../util/extensions.dart';
|
||||
import '../../util/util.dart';
|
||||
import '../api_exception.dart';
|
||||
import '../libgopeed_boot.dart';
|
||||
import '../native/libgopeed_interface.dart';
|
||||
import '../native/model/start_config.dart';
|
||||
import 'libgopeed_boot_base.dart';
|
||||
|
||||
LibgopeedBoot create() => LibgopeedBootWeb();
|
||||
|
||||
class LibgopeedBootWeb with LibgopeedBootBase implements LibgopeedBoot {
|
||||
class LibgopeedBootWeb
|
||||
with LibgopeedBootBase
|
||||
implements LibgopeedBoot, LibgopeedApi {
|
||||
late Dio _dio;
|
||||
static const String _apiPrefix = '/api/v1';
|
||||
|
||||
@@ -72,9 +75,10 @@ class LibgopeedBootWeb with LibgopeedBootBase implements LibgopeedBoot {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> init(StartConfig cfg) async {
|
||||
Future<LibgopeedApi> init(StartConfig cfg) async {
|
||||
_dio = createDio();
|
||||
_dio.options.baseUrl = kDebugMode ? 'http://127.0.0.1:9999' : '';
|
||||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -3,11 +3,14 @@ import "libgopeed_boot_stub.dart"
|
||||
if (dart.library.io) 'entry/libgopeed_boot_native.dart';
|
||||
import 'native/libgopeed_interface.dart';
|
||||
|
||||
abstract class LibgopeedBoot implements LibgopeedApi {
|
||||
static LibgopeedBoot? _instance;
|
||||
abstract class LibgopeedBoot implements LibgopeedApiSingleton {
|
||||
static LibgopeedApi? _instance;
|
||||
|
||||
static LibgopeedBoot get instance {
|
||||
_instance ??= LibgopeedBoot();
|
||||
static void singleton(LibgopeedApi instance) {
|
||||
_instance ??= instance;
|
||||
}
|
||||
|
||||
static LibgopeedApi get instance {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,15 @@ class LibgopeedChannel implements LibgopeedAbi {
|
||||
static const _channel = MethodChannel('gopeed.com/libgopeed');
|
||||
|
||||
@override
|
||||
Future<String> create(String cfg) async {
|
||||
final result = await _channel.invokeMethod<String>('create', {
|
||||
Future<void> init(String cfg) async {
|
||||
await _channel.invokeMethod<String>('init', {
|
||||
'cfg': cfg,
|
||||
});
|
||||
return result!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> invoke(int instance, String params) async {
|
||||
Future<String> invoke(String params) async {
|
||||
final result = await _channel.invokeMethod<String>('invoke', {
|
||||
'instance': instance,
|
||||
'params': params,
|
||||
});
|
||||
return result!;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
@@ -13,21 +14,25 @@ class LibgopeedFFi implements LibgopeedAbi {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> create(String cfg) {
|
||||
Future<void> init(String cfg) {
|
||||
final cfgPtr = cfg.toNativeUtf8();
|
||||
try {
|
||||
final result = _libgopeed.Create(cfgPtr.cast());
|
||||
return Future.value(result.cast<Utf8>().toDartString());
|
||||
final result = _libgopeed.Init(cfgPtr.cast());
|
||||
if (result != nullptr) {
|
||||
throw Exception(
|
||||
'Libgopeed init failed: ${result.cast<Utf8>().toDartString()}');
|
||||
}
|
||||
return Future.value();
|
||||
} finally {
|
||||
calloc.free(cfgPtr);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> invoke(int instance, String params) {
|
||||
Future<String> invoke(String params) {
|
||||
final paramsPtr = params.toNativeUtf8();
|
||||
try {
|
||||
final result = _libgopeed.Invoke(instance, paramsPtr.cast());
|
||||
final result = _libgopeed.Invoke(paramsPtr.cast());
|
||||
return Future.value(result.cast<Utf8>().toDartString());
|
||||
} finally {
|
||||
calloc.free(paramsPtr);
|
||||
|
||||
@@ -17,13 +17,15 @@ import '../model/update_extension_settings.dart';
|
||||
import 'model/start_config.dart';
|
||||
|
||||
abstract class LibgopeedAbi {
|
||||
Future<String> create(String cfg);
|
||||
Future<String> invoke(int instance, String params);
|
||||
Future<void> init(String cfg);
|
||||
Future<String> invoke(String params);
|
||||
}
|
||||
|
||||
abstract class LibgopeedApiSingleton {
|
||||
Future<LibgopeedApi> init(StartConfig config);
|
||||
}
|
||||
|
||||
abstract class LibgopeedApi {
|
||||
Future<void> init(StartConfig cfg);
|
||||
|
||||
Future<HttpListenResult> startHttp();
|
||||
|
||||
Future<void> stopHttp();
|
||||
|
||||
@@ -73,10 +73,11 @@ Future<void> init(Args args) async {
|
||||
|
||||
final controller = Get.put(AppController());
|
||||
try {
|
||||
await LibgopeedBoot.instance.init(StartConfig(
|
||||
final instance = await LibgopeedBoot().init(StartConfig(
|
||||
storage: "bolt",
|
||||
storageDir: Util.getStorageDir(),
|
||||
));
|
||||
LibgopeedBoot.singleton(instance);
|
||||
} catch (e) {
|
||||
logger.e("libgopeed init fail", e);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user