mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 07:45:30 +00:00
458 lines
13 KiB
Go
458 lines
13 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/GopeedLab/gopeed/pkg/base"
|
|
"github.com/GopeedLab/gopeed/pkg/download"
|
|
restmodel "github.com/GopeedLab/gopeed/pkg/rest/model"
|
|
)
|
|
|
|
type Context struct {
|
|
Method string
|
|
Path string
|
|
Query url.Values
|
|
Body []byte
|
|
Headers http.Header
|
|
PathParams map[string]string
|
|
}
|
|
|
|
func (c *Context) PathParam(name string) string {
|
|
if c == nil || c.PathParams == nil {
|
|
return ""
|
|
}
|
|
return c.PathParams[name]
|
|
}
|
|
|
|
type Response struct {
|
|
StatusCode int
|
|
Body any
|
|
}
|
|
|
|
type Service struct {
|
|
Downloader *download.Downloader
|
|
routes []RouteSpec
|
|
taskEvents taskEventSubscription
|
|
}
|
|
|
|
func NewService(downloader *download.Downloader) (*Service, error) {
|
|
if downloader == nil {
|
|
return nil, fmt.Errorf("downloader is nil")
|
|
}
|
|
service := &Service{Downloader: downloader}
|
|
service.routes = []RouteSpec{
|
|
newRoute(http.MethodGet, "/api/v1/info", service.info),
|
|
newRoute(http.MethodPost, "/api/v1/resolve", service.resolve),
|
|
newRoute(http.MethodPost, "/api/v1/tasks", service.createTask),
|
|
newRoute(http.MethodPost, "/api/v1/tasks/batch", service.createTaskBatch),
|
|
newRoute(http.MethodPatch, "/api/v1/tasks/{id}", service.patchTask),
|
|
newRoute(http.MethodPut, "/api/v1/tasks/{id}/pause", service.pauseTask),
|
|
newRoute(http.MethodPut, "/api/v1/tasks/pause", service.pauseTasks),
|
|
newRoute(http.MethodPut, "/api/v1/tasks/{id}/continue", service.continueTask),
|
|
newRoute(http.MethodPut, "/api/v1/tasks/continue", service.continueTasks),
|
|
newRoute(http.MethodDelete, "/api/v1/tasks/{id}", service.deleteTask),
|
|
newRoute(http.MethodDelete, "/api/v1/tasks", service.deleteTasks),
|
|
newRoute(http.MethodGet, "/api/v1/tasks/{id}", service.getTask),
|
|
newRoute(http.MethodGet, "/api/v1/tasks/{id}/status", service.getTaskStatus),
|
|
newRoute(http.MethodGet, "/api/v1/tasks", service.getTasks),
|
|
newRoute(http.MethodGet, "/api/v1/tasks/{id}/stats", service.getStats),
|
|
newRoute(http.MethodGet, "/api/v1/config", service.getConfig),
|
|
newRoute(http.MethodPut, "/api/v1/config", service.putConfig),
|
|
newRoute(http.MethodPost, "/api/v1/extensions", service.installExtension),
|
|
newRoute(http.MethodGet, "/api/v1/extensions", service.getExtensions),
|
|
newRoute(http.MethodGet, "/api/v1/extensions/{identity}", service.getExtension),
|
|
newRoute(http.MethodPut, "/api/v1/extensions/{identity}/settings", service.updateExtensionSettings),
|
|
newRoute(http.MethodPut, "/api/v1/extensions/{identity}/switch", service.switchExtension),
|
|
newRoute(http.MethodDelete, "/api/v1/extensions/{identity}", service.deleteExtension),
|
|
newRoute(http.MethodGet, "/api/v1/extensions/{identity}/update", service.updateCheckExtension),
|
|
newRoute(http.MethodPost, "/api/v1/extensions/{identity}/update", service.updateExtension),
|
|
newRoute(http.MethodPost, "/api/v1/webhook/test", service.testWebhook),
|
|
}
|
|
if err := validateRoutes(service.routes); err != nil {
|
|
return nil, err
|
|
}
|
|
downloader.Listener(service.emitTaskEvent)
|
|
return service, nil
|
|
}
|
|
|
|
func (s *Service) RouteSpecs() []RouteSpec {
|
|
return append([]RouteSpec(nil), s.routes...)
|
|
}
|
|
|
|
func (s *Service) Dispatch(method, rawPath string, query url.Values, body []byte, headers http.Header) (response *Response) {
|
|
defer func() {
|
|
if recovered := recover(); recovered != nil {
|
|
response = newErrorResponse(fmt.Sprintf("%v", recovered))
|
|
}
|
|
}()
|
|
|
|
method = strings.ToUpper(strings.TrimSpace(method))
|
|
path := cleanPath(rawPath)
|
|
for _, route := range s.routes {
|
|
if route.Method != method {
|
|
continue
|
|
}
|
|
pathParams, matched := route.Match(path)
|
|
if !matched {
|
|
continue
|
|
}
|
|
return route.Handler(&Context{
|
|
Method: method,
|
|
Path: path,
|
|
Query: query,
|
|
Body: body,
|
|
Headers: headers,
|
|
PathParams: pathParams,
|
|
})
|
|
}
|
|
return newStatusResponse(http.StatusNotFound, restmodel.NewErrorResult("not found"))
|
|
}
|
|
|
|
func (s *Service) JSON(method, rawPath string, query url.Values, body []byte, headers http.Header) (int, []byte) {
|
|
response := s.Dispatch(method, rawPath, query, body, headers)
|
|
data, err := json.Marshal(response.Body)
|
|
if err != nil {
|
|
response = newErrorResponse(err.Error())
|
|
data, _ = json.Marshal(response.Body)
|
|
}
|
|
return response.StatusCode, data
|
|
}
|
|
|
|
func (s *Service) info(_ *Context) *Response {
|
|
return newOKResponse(map[string]any{
|
|
"version": base.Version,
|
|
"runtime": runtime.Version(),
|
|
"os": runtime.GOOS,
|
|
"arch": runtime.GOARCH,
|
|
"inDocker": base.InDocker == "true",
|
|
})
|
|
}
|
|
|
|
func (s *Service) resolve(ctx *Context) *Response {
|
|
var request restmodel.ResolveTask
|
|
if response := decodeRequest(ctx.Body, &request); response != nil {
|
|
return response
|
|
}
|
|
result, err := s.Downloader.Resolve(request.Req, request.Opts)
|
|
if err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newOKResponse(result)
|
|
}
|
|
|
|
func (s *Service) createTask(ctx *Context) *Response {
|
|
var request restmodel.CreateTask
|
|
if response := decodeRequest(ctx.Body, &request); response != nil {
|
|
return response
|
|
}
|
|
var (
|
|
taskID string
|
|
err error
|
|
)
|
|
if request.Rid != "" {
|
|
taskID, err = s.Downloader.Create(request.Rid)
|
|
} else if request.Req != nil {
|
|
taskID, err = s.Downloader.CreateDirect(request.Req, request.Opts)
|
|
} else {
|
|
return newErrorResponse("param invalid: rid or req", restmodel.CodeInvalidParam)
|
|
}
|
|
if err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newOKResponse(taskID)
|
|
}
|
|
|
|
func (s *Service) createTaskBatch(ctx *Context) *Response {
|
|
var request base.CreateTaskBatch
|
|
if response := decodeRequest(ctx.Body, &request); response != nil {
|
|
return response
|
|
}
|
|
if len(request.Reqs) == 0 {
|
|
return newErrorResponse("param invalid: reqs", restmodel.CodeInvalidParam)
|
|
}
|
|
taskIDs, err := s.Downloader.CreateDirectBatch(&request)
|
|
if err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newOKResponse(taskIDs)
|
|
}
|
|
|
|
func (s *Service) patchTask(ctx *Context) *Response {
|
|
taskID := ctx.PathParam("id")
|
|
if taskID == "" {
|
|
return invalidIDResponse()
|
|
}
|
|
var request restmodel.ResolveTask
|
|
if response := decodeRequest(ctx.Body, &request); response != nil {
|
|
return response
|
|
}
|
|
if err := s.Downloader.Patch(taskID, request.Req, request.Opts); err != nil {
|
|
if err == download.ErrTaskNotFound {
|
|
return taskNotFoundResponse()
|
|
}
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newNilResponse()
|
|
}
|
|
|
|
func (s *Service) pauseTask(ctx *Context) *Response {
|
|
return s.applyTaskAction(ctx, s.Downloader.Pause)
|
|
}
|
|
|
|
func (s *Service) pauseTasks(ctx *Context) *Response {
|
|
return applyFilterAction(taskFilterFromQuery(ctx.Query), s.Downloader.Pause)
|
|
}
|
|
|
|
func (s *Service) continueTask(ctx *Context) *Response {
|
|
return s.applyTaskAction(ctx, s.Downloader.Continue)
|
|
}
|
|
|
|
func (s *Service) continueTasks(ctx *Context) *Response {
|
|
return applyFilterAction(taskFilterFromQuery(ctx.Query), s.Downloader.Continue)
|
|
}
|
|
|
|
func (s *Service) applyTaskAction(ctx *Context, action func(*download.TaskFilter) error) *Response {
|
|
taskID := ctx.PathParam("id")
|
|
if taskID == "" {
|
|
return invalidIDResponse()
|
|
}
|
|
return applyFilterAction(&download.TaskFilter{IDs: []string{taskID}}, action)
|
|
}
|
|
|
|
func applyFilterAction(filter *download.TaskFilter, action func(*download.TaskFilter) error) *Response {
|
|
if err := action(filter); err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newNilResponse()
|
|
}
|
|
|
|
func (s *Service) deleteTask(ctx *Context) *Response {
|
|
taskID := ctx.PathParam("id")
|
|
if taskID == "" {
|
|
return invalidIDResponse()
|
|
}
|
|
return deleteTasks(s.Downloader, &download.TaskFilter{IDs: []string{taskID}}, ctx.Query.Get("force") == "true")
|
|
}
|
|
|
|
func (s *Service) deleteTasks(ctx *Context) *Response {
|
|
return deleteTasks(s.Downloader, taskFilterFromQuery(ctx.Query), ctx.Query.Get("force") == "true")
|
|
}
|
|
|
|
func deleteTasks(downloader *download.Downloader, filter *download.TaskFilter, force bool) *Response {
|
|
if err := downloader.Delete(filter, force); err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newNilResponse()
|
|
}
|
|
|
|
func (s *Service) getTask(ctx *Context) *Response {
|
|
taskID := ctx.PathParam("id")
|
|
if taskID == "" {
|
|
return invalidIDResponse()
|
|
}
|
|
task := s.Downloader.GetTask(taskID)
|
|
if task == nil {
|
|
return taskNotFoundResponse()
|
|
}
|
|
return newOKResponse(task)
|
|
}
|
|
|
|
func (s *Service) getTaskStatus(ctx *Context) *Response {
|
|
taskID := ctx.PathParam("id")
|
|
if taskID == "" {
|
|
return invalidIDResponse()
|
|
}
|
|
status, err := s.Downloader.RuntimeStatus(taskID)
|
|
if err != nil {
|
|
if err == download.ErrTaskNotFound {
|
|
return taskNotFoundResponse()
|
|
}
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newOKResponse(status)
|
|
}
|
|
|
|
func (s *Service) getTasks(ctx *Context) *Response {
|
|
return newOKResponse(s.Downloader.GetTasksByFilter(taskFilterFromQuery(ctx.Query)))
|
|
}
|
|
|
|
func (s *Service) getStats(ctx *Context) *Response {
|
|
taskID := ctx.PathParam("id")
|
|
if taskID == "" {
|
|
return invalidIDResponse()
|
|
}
|
|
stats, err := s.Downloader.Stats(taskID)
|
|
if err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newOKResponse(stats)
|
|
}
|
|
|
|
func (s *Service) getConfig(_ *Context) *Response {
|
|
config, err := s.Downloader.GetConfig()
|
|
if err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newOKResponse(config)
|
|
}
|
|
|
|
func (s *Service) putConfig(ctx *Context) *Response {
|
|
var config base.DownloaderStoreConfig
|
|
if response := decodeRequest(ctx.Body, &config); response != nil {
|
|
return response
|
|
}
|
|
if err := s.Downloader.PutConfig(&config); err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newNilResponse()
|
|
}
|
|
|
|
func (s *Service) installExtension(ctx *Context) *Response {
|
|
var request restmodel.InstallExtension
|
|
if response := decodeRequest(ctx.Body, &request); response != nil {
|
|
return response
|
|
}
|
|
var (
|
|
extension *download.Extension
|
|
err error
|
|
)
|
|
if request.DevMode {
|
|
extension, err = s.Downloader.InstallExtensionByFolder(request.URL, true)
|
|
} else {
|
|
extension, err = s.Downloader.InstallExtensionByGit(request.URL)
|
|
}
|
|
if err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newOKResponse(extension.Identity)
|
|
}
|
|
|
|
func (s *Service) getExtensions(_ *Context) *Response {
|
|
return newOKResponse(s.Downloader.GetExtensions())
|
|
}
|
|
|
|
func (s *Service) getExtension(ctx *Context) *Response {
|
|
identity := ctx.PathParam("identity")
|
|
if identity == "" {
|
|
return newErrorResponse("param invalid: identity", restmodel.CodeInvalidParam)
|
|
}
|
|
extension, err := s.Downloader.GetExtension(identity)
|
|
if err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newOKResponse(extension)
|
|
}
|
|
|
|
func (s *Service) updateExtensionSettings(ctx *Context) *Response {
|
|
identity := ctx.PathParam("identity")
|
|
var request restmodel.UpdateExtensionSettings
|
|
if response := decodeRequest(ctx.Body, &request); response != nil {
|
|
return response
|
|
}
|
|
if err := s.Downloader.UpdateExtensionSettings(identity, request.Settings); err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newNilResponse()
|
|
}
|
|
|
|
func (s *Service) switchExtension(ctx *Context) *Response {
|
|
identity := ctx.PathParam("identity")
|
|
var request restmodel.SwitchExtension
|
|
if response := decodeRequest(ctx.Body, &request); response != nil {
|
|
return response
|
|
}
|
|
if err := s.Downloader.SwitchExtension(identity, request.Status); err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newNilResponse()
|
|
}
|
|
|
|
func (s *Service) deleteExtension(ctx *Context) *Response {
|
|
if err := s.Downloader.DeleteExtension(ctx.PathParam("identity")); err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newNilResponse()
|
|
}
|
|
|
|
func (s *Service) updateCheckExtension(ctx *Context) *Response {
|
|
version, err := s.Downloader.UpgradeCheckExtension(ctx.PathParam("identity"))
|
|
if err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newOKResponse(&restmodel.UpdateCheckExtensionResp{NewVersion: version})
|
|
}
|
|
|
|
func (s *Service) updateExtension(ctx *Context) *Response {
|
|
if err := s.Downloader.UpgradeExtension(ctx.PathParam("identity")); err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newNilResponse()
|
|
}
|
|
|
|
func (s *Service) testWebhook(ctx *Context) *Response {
|
|
var request restmodel.TestWebhookReq
|
|
if response := decodeRequest(ctx.Body, &request); response != nil {
|
|
return response
|
|
}
|
|
if err := s.Downloader.TestWebhookUrl(request.URL); err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return newNilResponse()
|
|
}
|
|
|
|
func decodeRequest(body []byte, target any) *Response {
|
|
if err := json.NewDecoder(bytes.NewReader(body)).Decode(target); err != nil {
|
|
return newErrorResponse(err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func taskFilterFromQuery(query url.Values) *download.TaskFilter {
|
|
if query == nil {
|
|
query = url.Values{}
|
|
}
|
|
return &download.TaskFilter{
|
|
IDs: query["id"],
|
|
Statuses: convertStatuses(query["status"]),
|
|
NotStatuses: convertStatuses(query["notStatus"]),
|
|
}
|
|
}
|
|
|
|
func convertStatuses(values []string) []base.Status {
|
|
statuses := make([]base.Status, 0, len(values))
|
|
for _, value := range values {
|
|
statuses = append(statuses, base.Status(value))
|
|
}
|
|
return statuses
|
|
}
|
|
|
|
func invalidIDResponse() *Response {
|
|
return newErrorResponse("param invalid: id", restmodel.CodeInvalidParam)
|
|
}
|
|
|
|
func taskNotFoundResponse() *Response {
|
|
return newErrorResponse("task not found", restmodel.CodeTaskNotFound)
|
|
}
|
|
|
|
func newOKResponse(data any) *Response {
|
|
return &Response{StatusCode: http.StatusOK, Body: restmodel.NewOkResult(data)}
|
|
}
|
|
|
|
func newNilResponse() *Response {
|
|
return &Response{StatusCode: http.StatusOK, Body: restmodel.NewNilResult()}
|
|
}
|
|
|
|
func newErrorResponse(message string, code ...restmodel.RespCode) *Response {
|
|
return &Response{StatusCode: http.StatusOK, Body: restmodel.NewErrorResult(message, code...)}
|
|
}
|
|
|
|
func newStatusResponse(statusCode int, body any) *Response {
|
|
return &Response{StatusCode: statusCode, Body: body}
|
|
}
|