mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 23:57:54 +00:00
feat: direct call download api by cgo
This commit is contained in:
1 parent
85fe92a378
commit
467ada5500
22 files changed
+1040
-709
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package rest
|
||||
|
||||
type Config struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user