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

+64
View File
@@ -0,0 +1,64 @@
package bind
import (
"encoding/json"
"errors"
"github.com/GopeedLab/gopeed/pkg/api"
"github.com/GopeedLab/gopeed/pkg/api/model"
"sync"
)
type instanceArray struct {
mu sync.RWMutex
array []*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
}
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))
}
func Invoke(index int, request *api.Request) string {
instance, ok := instances.get(index)
if !ok {
return BuildResult(errors.New("instance not found"))
}
return BuildResult(api.Invoke(instance, request))
}
func BuildResult(data any) string {
if err, ok := data.(error); ok {
buf, _ := json.Marshal(model.NewErrorResult[any](err.Error()))
return string(buf)
}
buf, _ := json.Marshal(model.NewOkResult(data))
return string(buf)
}
+22 -14
View File
@@ -1,29 +1,37 @@
package main
/*
#include <stdlib.h>
*/
import "C"
import (
"encoding/json"
"github.com/GopeedLab/gopeed/pkg/rest"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/bind"
"github.com/GopeedLab/gopeed/pkg/api"
"github.com/GopeedLab/gopeed/pkg/api/model"
"unsafe"
)
func main() {}
//export Start
func Start(cfg *C.char) (int, *C.char) {
//export Create
func Create(cfg *C.char) *C.char {
var config model.StartConfig
if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil {
return 0, C.CString(err.Error())
return C.CString(bind.BuildResult(err))
}
config.ProductionMode = true
realPort, err := rest.Start(&config)
if err != nil {
return 0, C.CString(err.Error())
}
return realPort, nil
defer C.free(unsafe.Pointer(cfg))
return C.CString(bind.Create(&config))
}
//export Stop
func Stop() {
rest.Stop()
//export Invoke
func Invoke(index int, 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))
}
defer C.free(unsafe.Pointer(req))
return C.CString(bind.Invoke(index, &request))
}
+7 -12
View File
@@ -3,20 +3,15 @@ package libgopeed
// #cgo LDFLAGS: -static-libstdc++
import "C"
import (
"encoding/json"
"github.com/GopeedLab/gopeed/pkg/rest"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/bind"
"github.com/GopeedLab/gopeed/pkg/api"
"github.com/GopeedLab/gopeed/pkg/api/model"
)
func Start(cfg string) (int, error) {
var config model.StartConfig
if err := json.Unmarshal([]byte(cfg), &config); err != nil {
return 0, err
}
config.ProductionMode = true
return rest.Start(&config)
func Create(config *model.StartConfig) string {
return bind.Create(config)
}
func Stop() {
rest.Stop()
func Invoke(index int, request *api.Request) string {
return bind.Invoke(index, request)
}
+1 -1
View File
@@ -2,7 +2,7 @@ package main
import (
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/pkg/api/model"
)
// only for local development
+17 -16
View File
@@ -3,10 +3,9 @@ package cmd
import (
_ "embed"
"fmt"
"github.com/GopeedLab/gopeed/pkg/api"
"github.com/GopeedLab/gopeed/pkg/api/model"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/rest"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"net/http"
"os"
"os/signal"
"path/filepath"
@@ -18,20 +17,21 @@ var banner string
func Start(cfg *model.StartConfig) {
fmt.Println(banner)
srv, listener, err := rest.BuildServer(cfg)
instance, err := api.Create(cfg)
if err != nil {
panic(err)
}
downloadCfg, err := rest.Downloader.GetConfig()
if err != nil {
panic(err)
downloadCfgResult := instance.GetConfig()
if downloadCfgResult.HasError() {
panic(downloadCfgResult.Msg)
}
downloadCfg := downloadCfgResult.Data
if downloadCfg.FirstLoad {
// Set default download config
if cfg.DownloadConfig != nil {
cfg.DownloadConfig.Merge(downloadCfg)
// TODO Use PatchConfig
rest.Downloader.PutConfig(cfg.DownloadConfig)
instance.PutConfig(cfg.DownloadConfig)
downloadCfg = cfg.DownloadConfig
}
@@ -48,25 +48,26 @@ func Start(cfg *model.StartConfig) {
}
if downloadDir != "" {
downloadCfg.DownloadDir = downloadDir
rest.Downloader.PutConfig(downloadCfg)
instance.PutConfig(downloadCfg)
}
}
}
watchExit()
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
panic(err)
startResult := instance.Start()
if startResult.HasError() {
panic(startResult.Msg)
}
fmt.Printf("Server start success on http://%s:%s\n", startResult.Data.Host, startResult.Data.Port)
watchExit(instance)
}
func watchExit() {
func watchExit(instance *api.Instance) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigs
fmt.Printf("Server is shutting down due to signal: %s\n", sig)
rest.Downloader.Close()
instance.Stop()
os.Exit(0)
}()
}
+2 -2
View File
@@ -10,7 +10,7 @@ import (
)
type args struct {
Address *string `json:"address"`
Host *string `json:"host"`
Port *int `json:"port"`
Username *string `json:"username"`
Password *string `json:"password"`
@@ -24,7 +24,7 @@ type args struct {
func parse() *args {
var cliArgs args
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
cliArgs.Host = flag.String("H", "0.0.0.0", "Bind Host")
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
+11 -5
View File
@@ -5,9 +5,9 @@ package main
import (
"embed"
"fmt"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/pkg/api/model"
"github.com/GopeedLab/gopeed/pkg/base"
"io/fs"
"os"
"path/filepath"
@@ -42,12 +42,18 @@ func main() {
dir = filepath.Dir(exe)
}
if args.DownloadConfig == nil {
args.DownloadConfig = &base.DownloaderStoreConfig{}
}
args.DownloadConfig.Http = &base.DownloaderHttpConfig{
Host: *args.Host,
Port: *args.Port,
ApiToken: *args.ApiToken,
}
cfg := &model.StartConfig{
Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt,
StorageDir: filepath.Join(dir, "storage"),
ApiToken: *args.ApiToken,
DownloadConfig: args.DownloadConfig,
ProductionMode: true,
WebEnable: true,
+1 -1
View File
@@ -332,7 +332,7 @@ func (f *Fetcher) doUpload(fromUpload bool) {
}
}
// Get torrent stats maybe panic, see https://github.com/anacrolix/torrent/issues/972
// get torrent stats maybe panic, see https://github.com/anacrolix/torrent/issues/972
func (f *Fetcher) torrentStats() torrent.TorrentStats {
defer func() {
if r := recover(); r != nil {
+2 -2
View File
@@ -146,7 +146,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
file.Name = filename
}
}
// Get file filePath by URL
// get file filePath by URL
if file.Name == "" {
file.Name = path.Base(httpReq.URL.Path)
}
@@ -496,7 +496,7 @@ func (fm *FetcherManager) ParseName(u string) string {
if err != nil {
return ""
}
// Get filePath by URL
// get filePath by URL
name = path.Base(url.Path)
// If file name is empty, use host name
if name == "" || name == "/" || name == "." {
+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]
}
@@ -1,4 +1,4 @@
package rest
package api
import (
"bytes"
@@ -7,9 +7,9 @@ import (
"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"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"io"
"net"
"net/http"
@@ -354,7 +354,10 @@ func TestGetAndPutConfig(t *testing.T) {
cfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
cfg.DownloadDir = "./download"
cfg.Extra = map[string]any{
"serverConfig": &Config{
"serverConfig": &struct {
Host string `json:"host"`
Port int `json:"port"`
}{
Host: "127.0.0.1",
Port: 8080,
},
@@ -449,10 +452,10 @@ func TestDeleteExtension(t *testing.T) {
func TestUpdateCheckExtension(t *testing.T) {
doTest(func() {
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
resp := httpRequestCheckOk[*model.UpdateCheckExtensionResp](http.MethodGet, "/api/v1/extensions/"+identity+"/update", nil)
resp := httpRequestCheckOk[*model.UpgradeCheckExtensionResp](http.MethodGet, "/api/v1/extensions/"+identity+"/update", nil)
// no new version
if resp.NewVersion != "" {
t.Errorf("UpdateCheckExtension() got = %v, want %v", resp.NewVersion, "")
t.Errorf("UpgradeCheckExtension() got = %v, want %v", resp.NewVersion, "")
}
// force update
httpRequestCheckOk[any](http.MethodPost, "/api/v1/extensions/"+identity+"/update", nil)
+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))
}
@@ -14,13 +14,10 @@ const (
)
type StartConfig struct {
Network string `json:"network"`
Address string `json:"address"`
RefreshInterval int `json:"refreshInterval"`
Storage Storage `json:"storage"`
StorageDir string `json:"storageDir"`
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
ApiToken string `json:"apiToken"`
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
ProductionMode bool
@@ -31,12 +28,6 @@ type StartConfig struct {
}
func (cfg *StartConfig) Init() *StartConfig {
if cfg.Network == "" {
cfg.Network = "tcp"
}
if cfg.Address == "" {
cfg.Address = "127.0.0.1:0"
}
if cfg.RefreshInterval == 0 {
cfg.RefreshInterval = 350
}
@@ -59,3 +50,9 @@ 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"`
}
@@ -13,6 +13,6 @@ type SwitchExtension struct {
Status bool `json:"status"`
}
type UpdateCheckExtensionResp struct {
type UpgradeCheckExtensionResp struct {
NewVersion string `json:"newVersion"`
}
@@ -1,5 +1,7 @@
package model
import "encoding/json"
type RespCode int
const (
@@ -20,6 +22,15 @@ type Result[T any] struct {
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,
@@ -33,14 +44,14 @@ func NewNilResult() *Result[any] {
}
}
func NewErrorResult(msg string, code ...RespCode) *Result[any] {
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[any]{
return &Result[T]{
Code: c,
Msg: msg,
}
File renamed without changes.
+21
View File
@@ -171,6 +171,7 @@ type DownloaderStoreConfig struct {
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"`
Http *DownloaderHttpConfig `json:"http"`
Proxy *DownloaderProxyConfig `json:"proxy"`
}
@@ -181,6 +182,12 @@ func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
if cfg.ProtocolConfig == nil {
cfg.ProtocolConfig = make(map[string]any)
}
if cfg.Extra == nil {
cfg.Extra = make(map[string]any)
}
if cfg.Http == nil {
cfg.Http = &DownloaderHttpConfig{}
}
if cfg.Proxy == nil {
cfg.Proxy = &DownloaderProxyConfig{}
}
@@ -203,12 +210,26 @@ func (cfg *DownloaderStoreConfig) Merge(beforeCfg *DownloaderStoreConfig) *Downl
if cfg.Extra == nil {
cfg.Extra = beforeCfg.Extra
}
if cfg.Http == nil {
cfg.Http = beforeCfg.Http
}
if cfg.Proxy == nil {
cfg.Proxy = beforeCfg.Proxy
}
return cfg
}
type DownloaderHttpConfig struct {
// Enable is the flag that enable http server
Enable bool `json:"Enable"`
// Host is the http server listen address.
Host string `json:"host"`
// Port is the http server listen port.
Port int `json:"port"`
// ApiToken is the auth token for http API
ApiToken string `json:"apiToken"`
}
type DownloaderProxyConfig struct {
Enable bool `json:"enable"`
// System is the flag that use system proxy
+2 -2
View File
@@ -256,7 +256,7 @@ func (d *Downloader) setupFetcher(fm fetcher.FetcherManager, fetcher fetcher.Fet
ctl.GetConfig = func(v any) {
d.getProtocolConfig(fm.Name(), v)
}
// Get proxy config, task request proxy config has higher priority, then use global proxy config
// get proxy config, task request proxy config has higher priority, then use global proxy config
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
if requestProxy == nil {
return d.cfg.Proxy.ToHandler()
@@ -576,7 +576,7 @@ func (d *Downloader) deleteAll() (err error) {
return
}
func (d *Downloader) Stats(id string) (sr any, err error) {
func (d *Downloader) GetTaskStats(id string) (sr any, err error) {
task := d.GetTask(id)
if task == nil {
return sr, ErrTaskNotFound
+1 -1
View File
@@ -75,7 +75,7 @@ func (t *Task) Name() string {
return t.Meta.Res.Name
}
// Get the name of the first file
// get the name of the first file
return t.Meta.Res.Files[0].Name
}
-389
View File
@@ -1,389 +0,0 @@
package rest
import (
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/gorilla/mux"
"io"
"net/http"
"net/url"
"runtime"
)
func Info(w http.ResponseWriter, r *http.Request) {
info := map[string]any{
"version": base.Version,
"runtime": runtime.Version(),
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"inDocker": base.InDocker == "true",
}
WriteJson(w, model.NewOkResult(info))
}
func Resolve(w http.ResponseWriter, r *http.Request) {
var req base.Request
if ReadJson(r, w, &req) {
rr, err := Downloader.Resolve(&req)
if err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewOkResult(rr))
}
}
func CreateTask(w http.ResponseWriter, r *http.Request) {
var req model.CreateTask
if ReadJson(r, w, &req) {
var (
taskId string
err error
)
if req.Rid != "" {
taskId, err = Downloader.Create(req.Rid, req.Opt)
} else if req.Req != nil {
taskId, err = Downloader.CreateDirect(req.Req, req.Opt)
} else {
WriteJson(w, model.NewErrorResult("param invalid: rid or req", model.CodeInvalidParam))
return
}
if err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewOkResult(taskId))
}
}
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
var req model.CreateTaskBatch
if ReadJson(r, w, &req) {
if len(req.Reqs) == 0 {
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
return
}
taskIds, err := Downloader.CreateDirectBatch(req.Reqs, req.Opt)
if err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewOkResult(taskIds))
}
}
func PauseTask(w http.ResponseWriter, r *http.Request) {
filter, errResult := parseIdFilter(r)
if errResult != nil {
WriteJson(w, errResult)
return
}
if err := Downloader.Pause(filter); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func PauseTasks(w http.ResponseWriter, r *http.Request) {
filter, errResult := parseFilter(r)
if errResult != nil {
WriteJson(w, errResult)
return
}
if err := Downloader.Pause(filter); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func ContinueTask(w http.ResponseWriter, r *http.Request) {
filter, errResult := parseIdFilter(r)
if errResult != nil {
WriteJson(w, errResult)
return
}
if err := Downloader.Continue(filter); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func ContinueTasks(w http.ResponseWriter, r *http.Request) {
filter, errResult := parseFilter(r)
if errResult != nil {
WriteJson(w, errResult)
return
}
if err := Downloader.Continue(filter); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func DeleteTask(w http.ResponseWriter, r *http.Request) {
filter, errResult := parseIdFilter(r)
if errResult != nil {
WriteJson(w, errResult)
return
}
force := r.FormValue("force")
if err := Downloader.Delete(filter, force == "true"); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
filter, errResult := parseFilter(r)
if errResult != nil {
WriteJson(w, errResult)
return
}
force := r.FormValue("force")
if err := Downloader.Delete(filter, force == "true"); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func GetTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
if taskId == "" {
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
return
}
task := Downloader.GetTask(taskId)
if task == nil {
WriteJson(w, model.NewErrorResult("task not found", model.CodeTaskNotFound))
return
}
WriteJson(w, model.NewOkResult(task))
}
func GetTasks(w http.ResponseWriter, r *http.Request) {
filter, errResult := parseFilter(r)
if errResult != nil {
WriteJson(w, errResult)
return
}
tasks := Downloader.GetTasksByFilter(filter)
WriteJson(w, model.NewOkResult(tasks))
}
func GetConfig(w http.ResponseWriter, r *http.Request) {
WriteJson(w, model.NewOkResult(getServerConfig()))
}
func PutConfig(w http.ResponseWriter, r *http.Request) {
var cfg base.DownloaderStoreConfig
if ReadJson(r, w, &cfg) {
if err := Downloader.PutConfig(&cfg); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
}
WriteJson(w, model.NewNilResult())
}
func InstallExtension(w http.ResponseWriter, r *http.Request) {
var req model.InstallExtension
if ReadJson(r, w, &req) {
var (
installedExt *download.Extension
err error
)
if req.DevMode {
installedExt, err = Downloader.InstallExtensionByFolder(req.URL, true)
} else {
installedExt, err = Downloader.InstallExtensionByGit(req.URL)
}
if err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewOkResult(installedExt.Identity))
}
}
func GetExtensions(w http.ResponseWriter, r *http.Request) {
list := Downloader.GetExtensions()
WriteJson(w, model.NewOkResult(list))
}
func GetExtension(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
identity := vars["identity"]
ext, err := Downloader.GetExtension(identity)
if err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewOkResult(ext))
}
func UpdateExtensionSettings(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
identity := vars["identity"]
var req model.UpdateExtensionSettings
if ReadJson(r, w, &req) {
if err := Downloader.UpdateExtensionSettings(identity, req.Settings); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
}
WriteJson(w, model.NewNilResult())
}
func SwitchExtension(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
identity := vars["identity"]
var switchExtension model.SwitchExtension
if ReadJson(r, w, &switchExtension) {
if err := Downloader.SwitchExtension(identity, switchExtension.Status); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
}
WriteJson(w, model.NewNilResult())
}
func DeleteExtension(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
identity := vars["identity"]
if err := Downloader.DeleteExtension(identity); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func UpdateCheckExtension(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
identity := vars["identity"]
newVersion, err := Downloader.UpgradeCheckExtension(identity)
if err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewOkResult(&model.UpdateCheckExtensionResp{
NewVersion: newVersion,
}))
}
func UpdateExtension(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
identity := vars["identity"]
if err := Downloader.UpgradeExtension(identity); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
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 GetStats(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
if taskId == "" {
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
return
}
statsResult, err := Downloader.Stats(taskId)
if err != nil {
writeError(w, err.Error())
return
}
WriteJson(w, model.NewOkResult(statsResult))
}
func parseIdFilter(r *http.Request) (*download.TaskFilter, any) {
vars := mux.Vars(r)
taskId := vars["id"]
if taskId == "" {
return nil, model.NewErrorResult("param invalid: id", model.CodeInvalidParam)
}
filter := &download.TaskFilter{
IDs: []string{taskId},
}
return filter, nil
}
func parseFilter(r *http.Request) (*download.TaskFilter, any) {
if err := r.ParseForm(); err != nil {
return nil, model.NewErrorResult(err.Error())
}
filter := &download.TaskFilter{
IDs: r.Form["id"],
Statuses: convertStatues(r.Form["status"]),
NotStatuses: convertStatues(r.Form["notStatus"]),
}
return filter, nil
}
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 getServerConfig() *base.DownloaderStoreConfig {
cfg, _ := Downloader.GetConfig()
return cfg
}
-6
View File
@@ -1,6 +0,0 @@
package rest
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
}
-241
View File
@@ -1,241 +0,0 @@
package rest
import (
"context"
"encoding/json"
"fmt"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
)
var (
srv *http.Server
runningPort int
Downloader *download.Downloader
)
func Start(startCfg *model.StartConfig) (port int, err error) {
// avoid repeat start
if srv != nil {
return runningPort, nil
}
var listener net.Listener
srv, listener, err = BuildServer(startCfg)
if err != nil {
return
}
go func() {
if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}()
if addr, ok := listener.Addr().(*net.TCPAddr); ok {
port = addr.Port
runningPort = port
}
return
}
func Stop() {
defer func() {
srv = nil
}()
if srv != nil {
if err := srv.Shutdown(context.TODO()); err != nil {
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
}
}
if Downloader != nil {
if err := Downloader.Close(); err != nil {
Downloader.Logger.Warn().Err(err).Msg("close downloader failed")
}
}
}
func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error) {
if startCfg == nil {
startCfg = &model.StartConfig{}
}
startCfg.Init()
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 nil, nil, err
}
if startCfg.Network == "unix" {
util.SafeRemove(startCfg.Address)
}
listener, err := net.Listen(startCfg.Network, startCfg.Address)
if err != nil {
return nil, nil, err
}
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(GetStats)
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}/update").HandlerFunc(UpdateCheckExtension)
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
if startCfg.WebEnable {
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS)))
}
if startCfg.ApiToken != "" || (startCfg.WebEnable && startCfg.WebBasicAuth != nil) {
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if startCfg.ApiToken != "" && r.Header.Get("X-Api-Token") == startCfg.ApiToken {
h.ServeHTTP(w, r)
return
}
if startCfg.WebEnable && startCfg.WebBasicAuth != nil {
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
h.ServeHTTP(w, r)
return
}
w.Header().Set("WWW-Authenticate", "Basic realm=\"gopeed web\"")
}
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("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))
Downloader.Logger.Error().Stack().Err(err).Msgf("http server panic: %s %s", r.Method, r.RequestURI)
WriteJson(w, model.NewErrorResult(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)}
return srv, listener, nil
}
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 {
}
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 := 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 {
}
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 := Downloader.GetExtension(identity)
if err != nil {
return nil, os.ErrNotExist
}
extensionPath := Downloader.ExtensionPath(extension)
return os.Open(filepath.Join(extensionPath, path))
}
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(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)
}