feat: direct call download api by cgo

This commit is contained in:
liwei committed 2024-11-26 23:32:54 +08:00
1 parent 85fe92a378
commit 467ada5500
22 files changed
+1040 -709

No files matched your search

+414
View File
@@ -0,0 +1,414 @@
package api
import (
"context"
"encoding/json"
"fmt"
"github.com/GopeedLab/gopeed/pkg/api/model"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/pkg/errors"
"net"
"net/http"
"reflect"
"runtime"
"sync"
)
type Instance struct {
startCfg *model.StartConfig
downloader *download.Downloader
running bool
httpLock sync.Mutex
srv *http.Server
listener net.Listener
runningPort int
}
func Create(startCfg *model.StartConfig) (*Instance, error) {
if startCfg == nil {
startCfg = &model.StartConfig{}
}
startCfg.Init()
i := &Instance{
startCfg: startCfg,
}
return i, nil
}
func (i *Instance) Start() *model.Result[*model.StartResult] {
// avoid repeat start
if i.running {
// If the http server is not enabled, return nil
if i.runningPort == 0 {
return model.NewOkResult[*model.StartResult](nil)
}
host, _, _ := net.SplitHostPort(i.listener.Addr().String())
return model.NewOkResult(&model.StartResult{
Host: host,
Port: i.runningPort,
})
}
startCfg := i.startCfg
downloadCfg := &download.DownloaderConfig{
ProductionMode: startCfg.ProductionMode,
RefreshInterval: startCfg.RefreshInterval,
}
if startCfg.Storage == model.StorageBolt {
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
} else {
downloadCfg.Storage = download.NewMemStorage()
}
downloadCfg.StorageDir = startCfg.StorageDir
downloadCfg.Init()
downloader := download.NewDownloader(downloadCfg)
if err := downloader.Setup(); err != nil {
return model.NewErrorResult[*model.StartResult](err.Error())
}
i.downloader = downloader
i.running = true
config, err := i.downloader.GetConfig()
if err != nil {
return model.NewErrorResult[*model.StartResult](err.Error())
}
httpCfg := config.Http
if httpCfg == nil || !httpCfg.Enable {
return model.NewOkResult[*model.StartResult](nil)
}
i.httpLock = sync.Mutex{}
startResult := i.StartHttp(httpCfg)
if startResult.HasError() {
i.downloader.Logger.Error().Err(err).Msgf("listen http failed: %s", startResult.Msg)
return model.NewOkResult[*model.StartResult](nil)
}
go func() {
if err := i.srv.Serve(i.listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}()
addr, _ := i.listener.Addr().(*net.TCPAddr)
i.runningPort = addr.Port
return model.NewOkResult(&model.StartResult{
Host: httpCfg.Host,
Port: i.runningPort,
})
}
func (i *Instance) Stop() *model.Result[any] {
i.StopHttp()
if i.downloader != nil {
if err := i.downloader.Close(); err != nil {
i.downloader.Logger.Warn().Err(err).Msg("close downloader failed")
}
i.downloader = nil
i.running = false
}
return model.NewNilResult()
}
func (i *Instance) StartHttp(httpCfg *base.DownloaderHttpConfig) *model.Result[int] {
i.httpLock.Lock()
defer i.httpLock.Unlock()
return i.startHttp(httpCfg)
}
func (i *Instance) startHttp(httpCfg *base.DownloaderHttpConfig) *model.Result[int] {
if i.srv != nil {
return model.NewErrorResult[int]("server already started")
}
if err := ListenHttp(httpCfg, i); err != nil {
return model.NewErrorResult[int](err.Error())
}
return model.NewOkResult(i.runningPort)
}
func (i *Instance) StopHttp() *model.Result[any] {
i.httpLock.Lock()
defer i.httpLock.Unlock()
return i.stopHttp()
}
func (i *Instance) stopHttp() *model.Result[any] {
if i.srv != nil {
if err := i.srv.Shutdown(context.Background()); err != nil {
i.downloader.Logger.Warn().Err(err).Msg("shutdown http server failed")
}
i.srv = nil
i.listener = nil
i.runningPort = 0
}
return model.NewNilResult()
}
func (i *Instance) RestartHttp(httpCfg *base.DownloaderHttpConfig) *model.Result[int] {
i.httpLock.Lock()
defer i.httpLock.Unlock()
return i.restartHttp(httpCfg)
}
func (i *Instance) restartHttp(httpCfg *base.DownloaderHttpConfig) *model.Result[int] {
i.stopHttp()
return i.startHttp(httpCfg)
}
func (i *Instance) Info() *model.Result[map[string]any] {
return model.NewOkResult(map[string]any{
"version": base.Version,
"runtime": runtime.Version(),
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"inDocker": base.InDocker == "true",
})
}
func (i *Instance) Resolve(req *base.Request) *model.Result[*download.ResolveResult] {
rr, err := i.downloader.Resolve(req)
if err != nil {
return model.NewErrorResult[*download.ResolveResult](err.Error())
}
return model.NewOkResult(rr)
}
func (i *Instance) CreateTask(req *model.CreateTask) *model.Result[string] {
var (
taskId string
err error
)
if req.Rid != "" {
taskId, err = i.downloader.Create(req.Rid, req.Opt)
} else if req.Req != nil {
taskId, err = i.downloader.CreateDirect(req.Req, req.Opt)
} else {
return model.NewErrorResult[string]("param invalid: rid or req", model.CodeInvalidParam)
}
if err != nil {
return model.NewErrorResult[string](err.Error())
}
return model.NewOkResult(taskId)
}
func (i *Instance) CreateTaskBatch(req *model.CreateTaskBatch) *model.Result[[]string] {
if len(req.Reqs) == 0 {
return model.NewErrorResult[[]string]("param invalid: reqs", model.CodeInvalidParam)
}
taskIds, err := i.downloader.CreateDirectBatch(req.Reqs, req.Opt)
if err != nil {
return model.NewErrorResult[[]string](err.Error())
}
return model.NewOkResult(taskIds)
}
func (i *Instance) PauseTask(taskId string) *model.Result[any] {
err := i.downloader.Pause(&download.TaskFilter{
IDs: []string{taskId},
})
if err != nil {
return model.NewErrorResult[any](err.Error())
}
return model.NewNilResult()
}
func (i *Instance) PauseTasks(filter *download.TaskFilter) *model.Result[any] {
err := i.downloader.Pause(filter)
if err != nil {
return model.NewErrorResult[any](err.Error())
}
return model.NewNilResult()
}
func (i *Instance) ContinueTask(taskId string) *model.Result[any] {
err := i.downloader.Continue(&download.TaskFilter{
IDs: []string{taskId},
})
if err != nil {
return model.NewErrorResult[any](err.Error())
}
return model.NewNilResult()
}
func (i *Instance) ContinueTasks(filter *download.TaskFilter) *model.Result[any] {
err := i.downloader.Continue(filter)
if err != nil {
return model.NewErrorResult[any](err.Error())
}
return model.NewNilResult()
}
func (i *Instance) DeleteTask(taskId string, force bool) *model.Result[any] {
err := i.downloader.Delete(&download.TaskFilter{
IDs: []string{taskId},
}, force)
if err != nil {
return model.NewErrorResult[any](err.Error())
}
return model.NewNilResult()
}
func (i *Instance) DeleteTasks(filter *download.TaskFilter, force bool) *model.Result[any] {
err := i.downloader.Delete(filter, force)
if err != nil {
return model.NewErrorResult[any](err.Error())
}
return model.NewNilResult()
}
func (i *Instance) GetTask(taskId string) *model.Result[*download.Task] {
if taskId == "" {
return model.NewErrorResult[*download.Task]("param invalid: id", model.CodeInvalidParam)
}
task := i.downloader.GetTask(taskId)
if task == nil {
return model.NewErrorResult[*download.Task]("task not found", model.CodeTaskNotFound)
}
return model.NewOkResult(task)
}
func (i *Instance) GetTasks(filter *download.TaskFilter) *model.Result[[]*download.Task] {
return model.NewOkResult(i.downloader.GetTasksByFilter(filter))
}
func (i *Instance) GetTaskStats(taskId string) *model.Result[any] {
stats, err := i.downloader.GetTaskStats(taskId)
if err != nil {
return model.NewErrorResult[any](err.Error())
}
return model.NewOkResult(stats)
}
func (i *Instance) GetConfig() *model.Result[*base.DownloaderStoreConfig] {
config, err := i.downloader.GetConfig()
if err != nil {
return model.NewErrorResult[*base.DownloaderStoreConfig](err.Error())
}
return model.NewOkResult(config)
}
func (i *Instance) PutConfig(cfg *base.DownloaderStoreConfig) *model.Result[any] {
err := i.downloader.PutConfig(cfg)
if err != nil {
return model.NewErrorResult[any](err.Error())
}
return model.NewNilResult()
}
func (i *Instance) InstallExtension(req *model.InstallExtension) *model.Result[string] {
var (
installedExt *download.Extension
err error
)
if req.DevMode {
installedExt, err = i.downloader.InstallExtensionByFolder(req.URL, true)
} else {
installedExt, err = i.downloader.InstallExtensionByGit(req.URL)
}
if err != nil {
return model.NewErrorResult[string](err.Error())
}
return model.NewOkResult(installedExt.Identity)
}
func (i *Instance) GetExtension(identity string) *model.Result[*download.Extension] {
extension, err := i.downloader.GetExtension(identity)
if err != nil {
return model.NewErrorResult[*download.Extension](err.Error())
}
return model.NewOkResult(extension)
}
func (i *Instance) GetExtensions() *model.Result[[]*download.Extension] {
return model.NewOkResult(i.downloader.GetExtensions())
}
func (i *Instance) UpdateExtensionSettings(identity string, req *model.UpdateExtensionSettings) *model.Result[any] {
err := i.downloader.UpdateExtensionSettings(identity, req.Settings)
if err != nil {
return model.NewErrorResult[any](err.Error())
}
return model.NewNilResult()
}
func (i *Instance) SwitchExtension(identity string, req *model.SwitchExtension) *model.Result[any] {
err := i.downloader.SwitchExtension(identity, req.Status)
if err != nil {
return model.NewErrorResult[any](err.Error())
}
return model.NewNilResult()
}
func (i *Instance) DeleteExtension(identity string) *model.Result[any] {
err := i.downloader.DeleteExtension(identity)
if err != nil {
return model.NewErrorResult[any](err.Error())
}
return model.NewNilResult()
}
func (i *Instance) UpgradeCheckExtension(identity string) *model.Result[*model.UpgradeCheckExtensionResp] {
newVersion, err := i.downloader.UpgradeCheckExtension(identity)
if err != nil {
return model.NewErrorResult[*model.UpgradeCheckExtensionResp](err.Error())
}
return model.NewOkResult(&model.UpgradeCheckExtensionResp{
NewVersion: newVersion,
})
}
func (i *Instance) UpgradeExtension(identity string) *model.Result[any] {
err := i.downloader.UpgradeExtension(identity)
if err != nil {
return model.NewErrorResult[any](err.Error())
}
return model.NewNilResult()
}
type Request struct {
Method string `json:"method"`
Params []string `json:"params"`
}
// Invoke support dynamic call method
func Invoke(server *Instance, request *Request) (ret any) {
defer func() {
if err := recover(); err != nil {
ret = model.NewErrorResult[any](fmt.Sprintf("%v", err))
}
}()
method, args := request.Method, request.Params
dsType := reflect.ValueOf(server)
fn := dsType.MethodByName(method)
numIn := fn.Type().NumIn()
in := make([]reflect.Value, numIn)
for i := 0; i < numIn; i++ {
paramType := fn.Type().In(i)
var param reflect.Value
var paramPtr any
if paramType.Kind() == reflect.Ptr {
param = reflect.New(paramType.Elem())
paramPtr = param.Interface()
} else {
param = reflect.New(paramType).Elem()
paramPtr = param.Addr().Interface()
}
if err := json.Unmarshal([]byte(args[i]), paramPtr); err != nil {
panic(err)
}
in[i] = param
}
retVals := fn.Call(in)
return retVals[0]
}
+668
View File
@@ -0,0 +1,668 @@
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
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))
}
}
+447
View File
@@ -0,0 +1,447 @@
package api
import (
"context"
"encoding/json"
"fmt"
"github.com/GopeedLab/gopeed/pkg/api/model"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
)
const instanceKey string = "apiInstance"
func ListenHttp(httpCfg *base.DownloaderHttpConfig, ai *Instance) 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)
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
r.Methods(http.MethodPost).Path("/api/v1/tasks/batch").HandlerFunc(CreateTaskBatch)
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseTasks)
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueTasks)
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
r.Methods(http.MethodDelete).Path("/api/v1/tasks").HandlerFunc(DeleteTasks)
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}/stats").HandlerFunc(GetTaskStats)
r.Methods(http.MethodGet).Path("/api/v1/config").HandlerFunc(GetConfig)
r.Methods(http.MethodPut).Path("/api/v1/config").HandlerFunc(PutConfig)
r.Methods(http.MethodPost).Path("/api/v1/extensions").HandlerFunc(InstallExtension)
r.Methods(http.MethodGet).Path("/api/v1/extensions").HandlerFunc(GetExtensions)
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}").HandlerFunc(GetExtension)
r.Methods(http.MethodPut).Path("/api/v1/extensions/{identity}/settings").HandlerFunc(UpdateExtensionSettings)
r.Methods(http.MethodPut).Path("/api/v1/extensions/{identity}/switch").HandlerFunc(SwitchExtension)
r.Methods(http.MethodDelete).Path("/api/v1/extensions/{identity}").HandlerFunc(DeleteExtension)
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/upgrade").HandlerFunc(UpgradeCheckExtension)
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/upgrade").HandlerFunc(UpgradeExtension)
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
if ai.startCfg.WebEnable {
r.PathPrefix("/fs/tasks").Handler(http.FileServer(&taskFileSystem{
downloader: ai.downloader,
}))
r.PathPrefix("/fs/extensions").Handler(http.FileServer(&extensionFileSystem{
downloader: ai.downloader,
}))
r.PathPrefix("/").Handler(http.FileServer(http.FS(ai.startCfg.WebFS)))
}
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), instanceKey, ai)
h.ServeHTTP(w, r.WithContext(ctx))
})
})
if httpCfg.ApiToken != "" || (ai.startCfg.WebEnable && ai.startCfg.WebBasicAuth != nil) {
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if httpCfg.ApiToken != "" && r.Header.Get("X-Api-Token") == httpCfg.ApiToken {
h.ServeHTTP(w, r)
return
}
if ai.startCfg.WebEnable && ai.startCfg.WebBasicAuth != nil {
if r.Header.Get("Authorization") == ai.startCfg.WebBasicAuth.Authorization() {
h.ServeHTTP(w, r)
return
}
w.Header().Set("WWW-Authenticate", "Basic realm=\"gopeed web\"")
}
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult[any]("unauthorized", model.CodeUnauthorized))
})
})
}
// recover panic
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
err := errors.WithStack(fmt.Errorf("%v", v))
ai.downloader.Logger.Error().Stack().Err(err).Msgf("http server panic: %s %s", r.Method, r.RequestURI)
WriteJson(w, model.NewErrorResult[any](err.Error(), model.CodeError))
}
}()
h.ServeHTTP(w, r)
})
})
srv := &http.Server{Handler: handlers.CORS(
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
handlers.AllowedOrigins([]string{"*"}),
)(r)}
listener, err := net.Listen("tcp", net.JoinHostPort(httpCfg.Host, fmt.Sprintf("%d", httpCfg.Port)))
if err != nil {
return err
}
ai.srv = srv
ai.listener = listener
return nil
}
func getInstance(r *http.Request) *Instance {
return r.Context().Value(instanceKey).(*Instance)
}
func Info(w http.ResponseWriter, r *http.Request) {
WriteJson(w, getInstance(r).Info())
}
func Resolve(w http.ResponseWriter, r *http.Request) {
var req base.Request
if ReadJson(r, w, &req) {
WriteJson(w, getInstance(r).Resolve(&req))
}
}
func CreateTask(w http.ResponseWriter, r *http.Request) {
var req model.CreateTask
if ReadJson(r, w, &req) {
WriteJson(w, getInstance(r).CreateTask(&req))
}
}
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
var req model.CreateTaskBatch
if ReadJson(r, w, &req) {
WriteJson(w, getInstance(r).CreateTaskBatch(&req))
}
}
func PauseTask(w http.ResponseWriter, r *http.Request) {
var taskId string
if !parseTaskId(r, w, &taskId) {
return
}
WriteJson(w, getInstance(r).PauseTask(taskId))
}
func PauseTasks(w http.ResponseWriter, r *http.Request) {
var filter download.TaskFilter
if !parseFilter(r, w, &filter) {
return
}
WriteJson(w, getInstance(r).PauseTasks(&filter))
}
func ContinueTask(w http.ResponseWriter, r *http.Request) {
var taskId string
if !parseTaskId(r, w, &taskId) {
return
}
WriteJson(w, getInstance(r).ContinueTask(taskId))
}
func ContinueTasks(w http.ResponseWriter, r *http.Request) {
var filter download.TaskFilter
if !parseFilter(r, w, &filter) {
return
}
WriteJson(w, getInstance(r).ContinueTasks(&filter))
}
func DeleteTask(w http.ResponseWriter, r *http.Request) {
var taskId string
if !parseTaskId(r, w, &taskId) {
return
}
force := r.FormValue("force")
WriteJson(w, getInstance(r).DeleteTask(taskId, force == "true"))
}
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
var filter download.TaskFilter
if !parseFilter(r, w, &filter) {
return
}
force := r.FormValue("force")
WriteJson(w, getInstance(r).DeleteTasks(&filter, force == "true"))
}
func GetTask(w http.ResponseWriter, r *http.Request) {
var taskId string
if !parseTaskId(r, w, &taskId) {
return
}
WriteJson(w, getInstance(r).GetTask(taskId))
}
func GetTasks(w http.ResponseWriter, r *http.Request) {
var filter download.TaskFilter
if !parseFilter(r, w, &filter) {
return
}
WriteJson(w, getInstance(r).GetTasks(&filter))
}
func GetTaskStats(w http.ResponseWriter, r *http.Request) {
var taskId string
if !parseTaskId(r, w, &taskId) {
return
}
WriteJson(w, getInstance(r).GetTaskStats(taskId))
}
func GetConfig(w http.ResponseWriter, r *http.Request) {
WriteJson(w, getInstance(r).GetConfig())
}
func PutConfig(w http.ResponseWriter, r *http.Request) {
var cfg base.DownloaderStoreConfig
if !ReadJson(r, w, &cfg) {
return
}
WriteJson(w, getInstance(r).PutConfig(&cfg))
}
func InstallExtension(w http.ResponseWriter, r *http.Request) {
var req model.InstallExtension
if !ReadJson(r, w, &req) {
return
}
WriteJson(w, getInstance(r).InstallExtension(&req))
}
func GetExtensions(w http.ResponseWriter, r *http.Request) {
WriteJson(w, getInstance(r).GetExtensions())
}
func GetExtension(w http.ResponseWriter, r *http.Request) {
var identity string
if !parseIdentity(r, w, &identity) {
return
}
WriteJson(w, getInstance(r).GetExtension(identity))
}
func UpdateExtensionSettings(w http.ResponseWriter, r *http.Request) {
var identity string
if !parseIdentity(r, w, &identity) {
return
}
var req model.UpdateExtensionSettings
if !ReadJson(r, w, &req) {
return
}
WriteJson(w, getInstance(r).UpdateExtensionSettings(identity, &req))
}
func SwitchExtension(w http.ResponseWriter, r *http.Request) {
var identity string
if !parseIdentity(r, w, &identity) {
return
}
var switchExtension model.SwitchExtension
if !ReadJson(r, w, &switchExtension) {
return
}
WriteJson(w, getInstance(r).SwitchExtension(identity, &switchExtension))
}
func DeleteExtension(w http.ResponseWriter, r *http.Request) {
var identity string
if !parseIdentity(r, w, &identity) {
return
}
WriteJson(w, getInstance(r).DeleteExtension(identity))
}
func UpgradeCheckExtension(w http.ResponseWriter, r *http.Request) {
var identity string
if !parseIdentity(r, w, &identity) {
return
}
WriteJson(w, getInstance(r).UpgradeCheckExtension(identity))
}
func UpgradeExtension(w http.ResponseWriter, r *http.Request) {
var identity string
if !parseIdentity(r, w, &identity) {
return
}
WriteJson(w, getInstance(r).UpgradeExtension(identity))
}
func DoProxy(w http.ResponseWriter, r *http.Request) {
target := r.Header.Get("X-Target-Uri")
if target == "" {
writeError(w, "param invalid: X-Target-Uri")
return
}
targetUrl, err := url.Parse(target)
if err != nil {
writeError(w, err.Error())
return
}
r.RequestURI = ""
r.URL = targetUrl
r.Host = targetUrl.Host
resp, err := http.DefaultClient.Do(r)
if err != nil {
writeError(w, err.Error())
return
}
defer resp.Body.Close()
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Set(k, v)
}
}
w.WriteHeader(resp.StatusCode)
buf, err := io.ReadAll(resp.Body)
if err != nil {
writeError(w, err.Error())
return
}
w.Write(buf)
}
func parsePathParam(r *http.Request, w http.ResponseWriter, name string, value *string) bool {
vars := mux.Vars(r)
*value = vars[name]
if *value == "" {
WriteJson(w, model.NewErrorResult[any]("param invalid: id", model.CodeInvalidParam))
return false
}
return true
}
func parseTaskId(r *http.Request, w http.ResponseWriter, taskId *string) bool {
return parsePathParam(r, w, "id", taskId)
}
func parseIdentity(r *http.Request, w http.ResponseWriter, identity *string) bool {
return parsePathParam(r, w, "identity", identity)
}
func parseFilter(r *http.Request, w http.ResponseWriter, filter *download.TaskFilter) bool {
if err := r.ParseForm(); err != nil {
WriteJson(w, model.NewErrorResult[any](err.Error()))
return false
}
filter.IDs = r.Form["id"]
filter.Statuses = convertStatues(r.Form["status"])
filter.NotStatuses = convertStatues(r.Form["notStatus"])
return true
}
func convertStatues(statues []string) []base.Status {
result := make([]base.Status, 0)
for _, status := range statues {
result = append(result, base.Status(status))
}
return result
}
func writeError(w http.ResponseWriter, msg string) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(msg))
}
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
WriteJson(w, model.NewErrorResult[any](err.Error()))
return false
}
return true
}
func WriteJson(w http.ResponseWriter, v any) {
WriteStatusJson(w, http.StatusOK, v)
}
func WriteStatusJson(w http.ResponseWriter, statusCode int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(v)
}
func resolvePath(urlPath string, prefix string) (identity string, path string, err error) {
// remove prefix
clearPath := strings.TrimPrefix(urlPath, prefix)
// match extension identity, eg: /fs/extensions/identity/xxx
reg := regexp.MustCompile(`^/([^/]+)/(.*)$`)
if !reg.MatchString(clearPath) {
err = os.ErrNotExist
return
}
matched := reg.FindStringSubmatch(clearPath)
if len(matched) != 3 {
err = os.ErrNotExist
return
}
return matched[1], matched[2], nil
}
// handle task file resource
type taskFileSystem struct {
downloader *download.Downloader
}
func (e *taskFileSystem) Open(name string) (http.File, error) {
// get extension identity
identity, path, err := resolvePath(name, "/fs/tasks")
if err != nil {
return nil, err
}
task := e.downloader.GetTask(identity)
if task == nil {
return nil, os.ErrNotExist
}
return os.Open(filepath.Join(task.Meta.RootDirPath(), path))
}
// handle extension file resource
type extensionFileSystem struct {
downloader *download.Downloader
}
func (e *extensionFileSystem) Open(name string) (http.File, error) {
// get extension identity
identity, path, err := resolvePath(name, "/fs/extensions")
if err != nil {
return nil, err
}
extension, err := e.downloader.GetExtension(identity)
if err != nil {
return nil, os.ErrNotExist
}
extensionPath := e.downloader.ExtensionPath(extension)
return os.Open(filepath.Join(extensionPath, path))
}
+58
View File
@@ -0,0 +1,58 @@
package model
import (
"encoding/base64"
"github.com/GopeedLab/gopeed/pkg/base"
"io/fs"
)
type Storage string
const (
StorageMem Storage = "mem"
StorageBolt Storage = "bolt"
)
type StartConfig struct {
RefreshInterval int `json:"refreshInterval"`
Storage Storage `json:"storage"`
StorageDir string `json:"storageDir"`
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
ProductionMode bool
WebEnable bool
WebFS fs.FS
WebBasicAuth *WebBasicAuth
}
func (cfg *StartConfig) Init() *StartConfig {
if cfg.RefreshInterval == 0 {
cfg.RefreshInterval = 350
}
if cfg.Storage == "" {
cfg.Storage = StorageBolt
}
if cfg.StorageDir == "" {
cfg.StorageDir = "./"
}
return cfg
}
type WebBasicAuth struct {
Username string
Password string
}
// Authorization returns the value of the Authorization header to be used in HTTP requests.
func (cfg *WebBasicAuth) Authorization() string {
userId := cfg.Username + ":" + cfg.Password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userId))
}
type StartResult struct {
Host string `json:"host"`
// Port is the http server real listen port.
Port int `json:"port"`
}
+18
View File
@@ -0,0 +1,18 @@
package model
type InstallExtension struct {
DevMode bool `json:"devMode"`
URL string `json:"url"`
}
type UpdateExtensionSettings struct {
Settings map[string]any `json:"settings"`
}
type SwitchExtension struct {
Status bool `json:"status"`
}
type UpgradeCheckExtensionResp struct {
NewVersion string `json:"newVersion"`
}
+58
View File
@@ -0,0 +1,58 @@
package model
import "encoding/json"
type RespCode int
const (
CodeOk RespCode = 0
// CodeError is the common error code
CodeError RespCode = 1000
// CodeUnauthorized is the error code for unauthorized
CodeUnauthorized RespCode = 1001
// CodeInvalidParam is the error code for invalid parameter
CodeInvalidParam RespCode = 1002
// CodeTaskNotFound is the error code for task not found
CodeTaskNotFound RespCode = 2001
)
type Result[T any] struct {
Code RespCode `json:"code"`
Msg string `json:"msg"`
Data T `json:"data"`
}
func (r *Result[T]) Json() string {
buf, _ := json.Marshal(r)
return string(buf)
}
func (r *Result[T]) HasError() bool {
return r.Code != CodeOk
}
func NewOkResult[T any](data T) *Result[T] {
return &Result[T]{
Code: CodeOk,
Data: data,
}
}
func NewNilResult() *Result[any] {
return &Result[any]{
Code: CodeOk,
}
}
func NewErrorResult[T any](msg string, code ...RespCode) *Result[T] {
// if code is not provided, the default code is CodeError
c := CodeError
if len(code) > 0 {
c = code[0]
}
return &Result[T]{
Code: c,
Msg: msg,
}
}
+14
View File
@@ -0,0 +1,14 @@
package model
import "github.com/GopeedLab/gopeed/pkg/base"
type CreateTask struct {
Rid string `json:"rid"`
Req *base.Request `json:"req"`
Opt *base.Options `json:"opt"`
}
type CreateTaskBatch struct {
Reqs []*base.Request `json:"reqs"`
Opt *base.Options `json:"opt"`
}