mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-19 20:32:35 +02:00
refactor: improve error handling and response formatting in API
This commit is contained in:
parent
82c829de18
commit
98e90d7a0b
31 changed files with 657 additions and 185 deletions
|
@ -1,6 +1,7 @@
|
||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -51,12 +52,12 @@ func (t FileType) GetPath(filename string) string {
|
||||||
func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
|
func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
|
||||||
fileType = FileType(r.PathValue("type"))
|
fileType = FileType(r.PathValue("type"))
|
||||||
if !fileType.IsValid() {
|
if !fileType.IsValid() {
|
||||||
err = gphttp.ErrInvalidKey("type")
|
err = fmt.Errorf("invalid file type: %s", fileType)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
filename = r.PathValue("filename")
|
filename = r.PathValue("filename")
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
err = gphttp.ErrMissingKey("filename")
|
err = fmt.Errorf("missing filename")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
package favicon
|
package favicon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
"github.com/yusing/go-proxy/internal/homepage"
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/route/routes"
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
|
@ -21,11 +19,11 @@ import (
|
||||||
func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||||
url, alias := req.FormValue("url"), req.FormValue("alias")
|
url, alias := req.FormValue("url"), req.FormValue("alias")
|
||||||
if url == "" && alias == "" {
|
if url == "" && alias == "" {
|
||||||
gphttp.ClientError(w, gphttp.ErrMissingKey("url or alias"), http.StatusBadRequest)
|
gphttp.MissingKey(w, "url or alias")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if url != "" && alias != "" {
|
if url != "" && alias != "" {
|
||||||
gphttp.ClientError(w, gperr.New("url and alias are mutually exclusive"), http.StatusBadRequest)
|
gphttp.BadRequest(w, "url and alias are mutually exclusive")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +31,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||||
if url != "" {
|
if url != "" {
|
||||||
var iconURL homepage.IconURL
|
var iconURL homepage.IconURL
|
||||||
if err := iconURL.Parse(url); err != nil {
|
if err := iconURL.Parse(url); err != nil {
|
||||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
gphttp.ClientError(w, req, err, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fetchResult := homepage.FetchFavIconFromURL(req.Context(), &iconURL)
|
fetchResult := homepage.FetchFavIconFromURL(req.Context(), &iconURL)
|
||||||
|
@ -49,7 +47,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||||
// try with route.Icon
|
// try with route.Icon
|
||||||
r, ok := routes.HTTP.Get(alias)
|
r, ok := routes.HTTP.Get(alias)
|
||||||
if !ok {
|
if !ok {
|
||||||
gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound)
|
gphttp.ValueNotFound(w, "route", alias)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
icons, err := homepage.SearchIcons(r.FormValue("keyword"), limit)
|
icons, err := homepage.SearchIcons(r.FormValue("keyword"), limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gphttp.ClientError(w, err)
|
gphttp.ClientError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gphttp.RespondJSON(w, r, icons)
|
gphttp.RespondJSON(w, r, icons)
|
||||||
|
|
|
@ -20,27 +20,27 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
name := q.Get("name")
|
name := q.Get("name")
|
||||||
if name == "" {
|
if name == "" {
|
||||||
gphttp.ClientError(w, gphttp.ErrMissingKey("name"))
|
gphttp.MissingKey(w, "name")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
host := q.Get("host")
|
host := q.Get("host")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
gphttp.ClientError(w, gphttp.ErrMissingKey("host"))
|
gphttp.MissingKey(w, "host")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
portStr := q.Get("port")
|
portStr := q.Get("port")
|
||||||
if portStr == "" {
|
if portStr == "" {
|
||||||
gphttp.ClientError(w, gphttp.ErrMissingKey("port"))
|
gphttp.MissingKey(w, "port")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
port, err := strconv.Atoi(portStr)
|
port, err := strconv.Atoi(portStr)
|
||||||
if err != nil || port < 1 || port > 65535 {
|
if err != nil || port < 1 || port > 65535 {
|
||||||
gphttp.ClientError(w, gphttp.ErrInvalidKey("port"))
|
gphttp.InvalidKey(w, "port")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hostport := fmt.Sprintf("%s:%d", host, port)
|
hostport := fmt.Sprintf("%s:%d", host, port)
|
||||||
if _, ok := config.GetInstance().GetAgent(hostport); ok {
|
if _, ok := config.GetInstance().GetAgent(hostport); ok {
|
||||||
gphttp.ClientError(w, gphttp.ErrAlreadyExists("agent", hostport), http.StatusConflict)
|
gphttp.KeyAlreadyExists(w, "agent", hostport)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t := q.Get("type")
|
t := q.Get("type")
|
||||||
|
@ -48,10 +48,10 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
case "docker", "system":
|
case "docker", "system":
|
||||||
break
|
break
|
||||||
case "":
|
case "":
|
||||||
gphttp.ClientError(w, gphttp.ErrMissingKey("type"))
|
gphttp.MissingKey(w, "type")
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
gphttp.ClientError(w, gphttp.ErrInvalidKey("type"))
|
gphttp.InvalidKey(w, "type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,13 +109,13 @@ func VerifyNewAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(clientPEMData, &data); err != nil {
|
if err := json.Unmarshal(clientPEMData, &data); err != nil {
|
||||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
gphttp.ClientError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(data.Host, data.CA, data.Client)
|
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(data.Host, data.CA, data.Client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gphttp.ClientError(w, err)
|
gphttp.ClientError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ func VerifyNewAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
filename, ok := certs.AgentCertsFilepath(data.Host)
|
filename, ok := certs.AgentCertsFilepath(data.Host)
|
||||||
if !ok {
|
if !ok {
|
||||||
gphttp.ClientError(w, gphttp.ErrInvalidKey("host"))
|
gphttp.InvalidKey(w, "host")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||||
r = r.WithContext(context.WithValue(r.Context(), nextHandlerContextKey, next))
|
r = r.WithContext(context.WithValue(r.Context(), nextHandlerContextKey, next))
|
||||||
defaultAuth.LoginHandler(w, r)
|
defaultAuth.LoginHandler(w, r)
|
||||||
} else {
|
} else {
|
||||||
gphttp.ClientError(w, err, http.StatusUnauthorized)
|
gphttp.Unauthorized(w, err.Error())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -70,7 +69,7 @@ func (cfg *Config) Validate() gperr.Error {
|
||||||
if !ok {
|
if !ok {
|
||||||
b.Add(ErrUnknownProvider.
|
b.Add(ErrUnknownProvider.
|
||||||
Subject(cfg.Provider).
|
Subject(cfg.Provider).
|
||||||
Withf(strutils.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
|
With(gperr.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
|
||||||
} else {
|
} else {
|
||||||
_, err := providerConstructor(cfg.Options)
|
_, err := providerConstructor(cfg.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -37,11 +37,11 @@ func (err *baseError) Subjectf(format string, args ...any) Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err baseError) With(extra error) Error {
|
func (err baseError) With(extra error) Error {
|
||||||
return &nestedError{&err, []error{extra}}
|
return &nestedError{err.Err, []error{extra}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err baseError) Withf(format string, args ...any) Error {
|
func (err baseError) Withf(format string, args ...any) Error {
|
||||||
return &nestedError{&err, []error{fmt.Errorf(format, args...)}}
|
return &nestedError{err.Err, []error{fmt.Errorf(format, args...)}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err *baseError) Error() string {
|
func (err *baseError) Error() string {
|
||||||
|
@ -62,3 +62,11 @@ func (err *baseError) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(err.Error())
|
return json.Marshal(err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (err *baseError) Plain() []byte {
|
||||||
|
return Plain(err.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *baseError) Markdown() []byte {
|
||||||
|
return Markdown(err.Err)
|
||||||
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ func TestBuilderNested(t *testing.T) {
|
||||||
• Inner: 1
|
• Inner: 1
|
||||||
• Inner: 2
|
• Inner: 2
|
||||||
• Action 2
|
• Action 2
|
||||||
• Inner: 3`
|
• Inner: 3
|
||||||
|
`
|
||||||
ExpectEqual(t, got, expected)
|
ExpectEqual(t, got, expected)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,16 @@ type Error interface {
|
||||||
Subject(subject string) Error
|
Subject(subject string) Error
|
||||||
// Subjectf is a wrapper for Subject(fmt.Sprintf(format, args...)).
|
// Subjectf is a wrapper for Subject(fmt.Sprintf(format, args...)).
|
||||||
Subjectf(format string, args ...any) Error
|
Subjectf(format string, args ...any) Error
|
||||||
|
PlainError
|
||||||
|
MarkdownError
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlainError interface {
|
||||||
|
Plain() []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarkdownError interface {
|
||||||
|
Markdown() []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// this makes JSON marshaling work,
|
// this makes JSON marshaling work,
|
||||||
|
|
|
@ -153,6 +153,7 @@ func TestErrorStringNested(t *testing.T) {
|
||||||
• 2
|
• 2
|
||||||
• action 3 > inner3: generic failure
|
• action 3 > inner3: generic failure
|
||||||
• 3
|
• 3
|
||||||
• 3`
|
• 3
|
||||||
|
`
|
||||||
expect.Equal(t, ansi.StripANSI(ne.Error()), want)
|
expect.Equal(t, ansi.StripANSI(ne.Error()), want)
|
||||||
}
|
}
|
||||||
|
|
43
internal/gperr/hint.go
Normal file
43
internal/gperr/hint.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package gperr
|
||||||
|
|
||||||
|
import "github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||||
|
|
||||||
|
type Hint struct {
|
||||||
|
Prefix string
|
||||||
|
Message string
|
||||||
|
Suffix string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ PlainError = (*Hint)(nil)
|
||||||
|
var _ MarkdownError = (*Hint)(nil)
|
||||||
|
|
||||||
|
func (h *Hint) Error() string {
|
||||||
|
return h.Prefix + ansi.Info(h.Message) + h.Suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hint) Plain() []byte {
|
||||||
|
return []byte(h.Prefix + h.Message + h.Suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hint) Markdown() []byte {
|
||||||
|
return []byte(h.Prefix + "**" + h.Message + "**" + h.Suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hint) MarshalText() ([]byte, error) {
|
||||||
|
return h.Plain(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hint) String() string {
|
||||||
|
return h.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DoYouMean(s string) *Hint {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &Hint{
|
||||||
|
Prefix: "Do you mean ",
|
||||||
|
Message: s,
|
||||||
|
Suffix: "?",
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ func log(msg string, err error, level zerolog.Level, logger ...*zerolog.Logger)
|
||||||
} else {
|
} else {
|
||||||
l = logging.GetLogger()
|
l = logging.GetLogger()
|
||||||
}
|
}
|
||||||
l.WithLevel(level).Msg(New(highlight(msg)).With(err).Error())
|
l.WithLevel(level).Msg(New(highlightANSI(msg)).With(err).Error())
|
||||||
switch level {
|
switch level {
|
||||||
case zerolog.FatalLevel:
|
case zerolog.FatalLevel:
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
@ -3,8 +3,6 @@ package gperr
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:recvcheck
|
//nolint:recvcheck
|
||||||
|
@ -67,48 +65,98 @@ func (err *nestedError) Is(other error) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var nilError = newError("<nil>")
|
||||||
|
var bulletPrefix = []byte("• ")
|
||||||
|
var markdownBulletPrefix = []byte("- ")
|
||||||
|
var spaces = []byte(" ")
|
||||||
|
|
||||||
|
type appendLineFunc func(buf []byte, err error, level int) []byte
|
||||||
|
|
||||||
func (err *nestedError) Error() string {
|
func (err *nestedError) Error() string {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return makeLine("<nil>", 0)
|
return nilError.Error()
|
||||||
}
|
}
|
||||||
|
buf := appendLineNormal(nil, err.Err, 0)
|
||||||
if err.Err != nil {
|
if len(err.Extras) > 0 {
|
||||||
lines := make([]string, 0, 1+len(err.Extras))
|
buf = append(buf, '\n')
|
||||||
lines = append(lines, makeLine(err.Err.Error(), 0))
|
buf = appendLines(buf, err.Extras, 1, appendLineNormal)
|
||||||
lines = append(lines, makeLines(err.Extras, 1)...)
|
|
||||||
return strutils.JoinLines(lines)
|
|
||||||
}
|
}
|
||||||
return strutils.JoinLines(makeLines(err.Extras, 0))
|
return string(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
func (err *nestedError) Plain() []byte {
|
||||||
func makeLine(err string, level int) string {
|
if err == nil {
|
||||||
const bulletPrefix = "• "
|
return appendLinePlain(nil, nilError, 0)
|
||||||
const spaces = " "
|
}
|
||||||
|
buf := appendLinePlain(nil, err.Err, 0)
|
||||||
|
if len(err.Extras) > 0 {
|
||||||
|
buf = append(buf, '\n')
|
||||||
|
buf = appendLines(buf, err.Extras, 1, appendLinePlain)
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *nestedError) Markdown() []byte {
|
||||||
|
if err == nil {
|
||||||
|
return appendLineMd(nil, nilError, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := appendLineMd(nil, err.Err, 0)
|
||||||
|
if len(err.Extras) > 0 {
|
||||||
|
buf = append(buf, '\n')
|
||||||
|
buf = appendLines(buf, err.Extras, 1, appendLineMd)
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendLineNormal(buf []byte, err error, level int) []byte {
|
||||||
if level == 0 {
|
if level == 0 {
|
||||||
return err
|
return append(buf, err.Error()...)
|
||||||
}
|
}
|
||||||
return spaces[:2*level] + bulletPrefix + err
|
buf = append(buf, spaces[:2*level]...)
|
||||||
|
buf = append(buf, bulletPrefix...)
|
||||||
|
buf = append(buf, err.Error()...)
|
||||||
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeLines(errs []error, level int) []string {
|
func appendLinePlain(buf []byte, err error, level int) []byte {
|
||||||
if len(errs) == 0 {
|
if level == 0 {
|
||||||
return nil
|
return append(buf, Plain(err)...)
|
||||||
|
}
|
||||||
|
buf = append(buf, spaces[:2*level]...)
|
||||||
|
buf = append(buf, bulletPrefix...)
|
||||||
|
buf = append(buf, Plain(err)...)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendLineMd(buf []byte, err error, level int) []byte {
|
||||||
|
if level == 0 {
|
||||||
|
return append(buf, Markdown(err)...)
|
||||||
|
}
|
||||||
|
buf = append(buf, spaces[:2*level]...)
|
||||||
|
buf = append(buf, markdownBulletPrefix...)
|
||||||
|
buf = append(buf, Markdown(err)...)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendLines(buf []byte, errs []error, level int, appendLine appendLineFunc) []byte {
|
||||||
|
if len(errs) == 0 {
|
||||||
|
return buf
|
||||||
}
|
}
|
||||||
lines := make([]string, 0, len(errs))
|
|
||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
switch err := wrap(err).(type) {
|
switch err := wrap(err).(type) {
|
||||||
case *nestedError:
|
case *nestedError:
|
||||||
if err.Err != nil {
|
if err.Err != nil {
|
||||||
lines = append(lines, makeLine(err.Err.Error(), level))
|
buf = appendLine(buf, err.Err, level)
|
||||||
lines = append(lines, makeLines(err.Extras, level+1)...)
|
buf = append(buf, '\n')
|
||||||
|
buf = appendLines(buf, err.Extras, level+1, appendLine)
|
||||||
} else {
|
} else {
|
||||||
lines = append(lines, makeLines(err.Extras, level)...)
|
buf = appendLines(buf, err.Extras, level, appendLine)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
lines = append(lines, makeLine(err.Error(), level))
|
buf = appendLine(buf, err, level)
|
||||||
|
buf = append(buf, '\n')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lines
|
return buf
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package gperr
|
package gperr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||||
)
|
)
|
||||||
|
@ -19,10 +19,23 @@ type withSubject struct {
|
||||||
|
|
||||||
const subjectSep = " > "
|
const subjectSep = " > "
|
||||||
|
|
||||||
func highlight(subject string) string {
|
type highlightFunc func(subject string) string
|
||||||
|
|
||||||
|
var _ PlainError = (*withSubject)(nil)
|
||||||
|
var _ MarkdownError = (*withSubject)(nil)
|
||||||
|
|
||||||
|
func highlightANSI(subject string) string {
|
||||||
return ansi.HighlightRed + subject + ansi.Reset
|
return ansi.HighlightRed + subject + ansi.Reset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func highlightMarkdown(subject string) string {
|
||||||
|
return "**" + subject + "**"
|
||||||
|
}
|
||||||
|
|
||||||
|
func noHighlight(subject string) string {
|
||||||
|
return subject
|
||||||
|
}
|
||||||
|
|
||||||
func PrependSubject(subject string, err error) error {
|
func PrependSubject(subject string, err error) error {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -69,24 +82,38 @@ func (err *withSubject) Unwrap() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err *withSubject) Error() string {
|
func (err *withSubject) Error() string {
|
||||||
|
return string(err.fmtError(highlightANSI))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *withSubject) Plain() []byte {
|
||||||
|
return err.fmtError(noHighlight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *withSubject) Markdown() []byte {
|
||||||
|
return err.fmtError(highlightMarkdown)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *withSubject) fmtError(highlight highlightFunc) []byte {
|
||||||
// subject is in reversed order
|
// subject is in reversed order
|
||||||
n := len(err.Subjects)
|
n := len(err.Subjects)
|
||||||
size := 0
|
size := 0
|
||||||
errStr := err.Err.Error()
|
errStr := err.Err.Error()
|
||||||
var sb strings.Builder
|
var buf bytes.Buffer
|
||||||
for _, s := range err.Subjects {
|
for _, s := range err.Subjects {
|
||||||
size += len(s)
|
size += len(s)
|
||||||
}
|
}
|
||||||
sb.Grow(size + 2 + n*len(subjectSep) + len(errStr) + len(highlight("")))
|
buf.Grow(size + 2 + n*len(subjectSep) + len(errStr) + len(highlight("")))
|
||||||
|
|
||||||
for i := n - 1; i > 0; i-- {
|
for i := n - 1; i > 0; i-- {
|
||||||
sb.WriteString(err.Subjects[i])
|
buf.WriteString(err.Subjects[i])
|
||||||
sb.WriteString(subjectSep)
|
buf.WriteString(subjectSep)
|
||||||
}
|
}
|
||||||
sb.WriteString(highlight(err.Subjects[0]))
|
buf.WriteString(highlight(err.Subjects[0]))
|
||||||
sb.WriteString(": ")
|
if errStr != "" {
|
||||||
sb.WriteString(errStr)
|
buf.WriteString(": ")
|
||||||
return sb.String()
|
buf.WriteString(errStr)
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements the json.Marshaler interface.
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package gperr
|
package gperr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,16 +27,17 @@ func Wrap(err error, message ...string) Error {
|
||||||
if len(message) == 0 || message[0] == "" {
|
if len(message) == 0 || message[0] == "" {
|
||||||
return wrap(err)
|
return wrap(err)
|
||||||
}
|
}
|
||||||
|
wrapped := &wrappedError{err, message[0]}
|
||||||
//nolint:errorlint
|
//nolint:errorlint
|
||||||
switch err := err.(type) {
|
switch err := err.(type) {
|
||||||
case *baseError:
|
case *baseError:
|
||||||
err.Err = fmt.Errorf("%s: %w", message[0], err.Err)
|
err.Err = wrapped
|
||||||
return err
|
return err
|
||||||
case *nestedError:
|
case *nestedError:
|
||||||
err.Err = fmt.Errorf("%s: %w", message[0], err.Err)
|
err.Err = wrapped
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return &baseError{fmt.Errorf("%s: %w", message[0], err)}
|
return &baseError{wrapped}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Unwrap(err error) Error {
|
func Unwrap(err error) Error {
|
||||||
|
@ -65,18 +64,6 @@ func wrap(err error) Error {
|
||||||
return &baseError{err}
|
return &baseError{err}
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsJSONMarshallable(err error) bool {
|
|
||||||
switch err := err.(type) {
|
|
||||||
case *nestedError, *withSubject:
|
|
||||||
return true
|
|
||||||
case *baseError:
|
|
||||||
return IsJSONMarshallable(err.Err)
|
|
||||||
default:
|
|
||||||
var v json.Marshaler
|
|
||||||
return errors.As(err, &v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Join(errors ...error) Error {
|
func Join(errors ...error) Error {
|
||||||
n := 0
|
n := 0
|
||||||
for _, err := range errors {
|
for _, err := range errors {
|
||||||
|
@ -103,3 +90,27 @@ func Collect[T any, Err error, Arg any, Func func(Arg) (T, Err)](eb *Builder, fn
|
||||||
eb.Add(err)
|
eb.Add(err)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Plain(err error) []byte {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if p, ok := err.(PlainError); ok {
|
||||||
|
return p.Plain()
|
||||||
|
}
|
||||||
|
return []byte(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Markdown(err error) []byte {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch err := err.(type) {
|
||||||
|
case MarkdownError:
|
||||||
|
return err.Markdown()
|
||||||
|
case interface{ Unwrap() []error }:
|
||||||
|
return appendLines(nil, err.Unwrap(), 0, appendLineMd)
|
||||||
|
default:
|
||||||
|
return []byte(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,63 +1,55 @@
|
||||||
package gperr
|
package gperr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testErr struct{}
|
type testErr struct{}
|
||||||
|
|
||||||
func (e *testErr) Error() string {
|
func (e testErr) Error() string {
|
||||||
return "test error"
|
return "test error"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *testErr) MarshalJSON() ([]byte, error) {
|
func (e testErr) Plain() []byte {
|
||||||
return nil, nil
|
return []byte("test error")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsJSONMarshallable(t *testing.T) {
|
func (e testErr) Markdown() []byte {
|
||||||
tests := []struct {
|
return []byte("**test error**")
|
||||||
name string
|
}
|
||||||
err error
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "testErr",
|
|
||||||
err: &testErr{},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "baseError",
|
|
||||||
err: &baseError{},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "baseError with json marshallable error",
|
|
||||||
err: &baseError{&testErr{}},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nestedError",
|
|
||||||
err: &nestedError{},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "withSubject",
|
|
||||||
err: &withSubject{},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "standard error",
|
|
||||||
err: errors.New("test error"),
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
type testMultiErr struct {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
errors []error
|
||||||
if got := IsJSONMarshallable(test.err); got != test.want {
|
}
|
||||||
t.Errorf("IsJSONMarshallable(%v) = %v, want %v", test.err, got, test.want)
|
|
||||||
}
|
func (e testMultiErr) Error() string {
|
||||||
})
|
return Join(e.errors...).Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e testMultiErr) Unwrap() []error {
|
||||||
|
return e.errors
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatting(t *testing.T) {
|
||||||
|
err := testErr{}
|
||||||
|
plain := Plain(err)
|
||||||
|
if string(plain) != "test error" {
|
||||||
|
t.Errorf("expected test error, got %s", string(plain))
|
||||||
|
}
|
||||||
|
md := Markdown(err)
|
||||||
|
if string(md) != "**test error**" {
|
||||||
|
t.Errorf("expected test error, got %s", string(md))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiError(t *testing.T) {
|
||||||
|
err := testMultiErr{[]error{testErr{}, testErr{}}}
|
||||||
|
plain := Plain(err)
|
||||||
|
if string(plain) != "test error\ntest error" {
|
||||||
|
t.Errorf("expected test error, got %s", string(plain))
|
||||||
|
}
|
||||||
|
md := Markdown(err)
|
||||||
|
if string(md) != "**test error**\n**test error**" {
|
||||||
|
t.Errorf("expected test error, got %s", string(md))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
34
internal/gperr/wrapped.go
Normal file
34
internal/gperr/wrapped.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package gperr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wrappedError struct {
|
||||||
|
Err error
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ PlainError = (*wrappedError)(nil)
|
||||||
|
var _ MarkdownError = (*wrappedError)(nil)
|
||||||
|
|
||||||
|
func (e *wrappedError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Message, e.Err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *wrappedError) Plain() []byte {
|
||||||
|
return fmt.Appendf(nil, "%s: %s", e.Message, e.Err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *wrappedError) Markdown() []byte {
|
||||||
|
return fmt.Appendf(nil, "**%s**: %s", e.Message, e.Err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *wrappedError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *wrappedError) Is(target error) bool {
|
||||||
|
return errors.Is(e.Err, target)
|
||||||
|
}
|
|
@ -2,9 +2,8 @@ package homepage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
|
||||||
|
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
"github.com/yusing/go-proxy/internal/homepage/widgets"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,20 +12,21 @@ type (
|
||||||
Category []*Item
|
Category []*Item
|
||||||
|
|
||||||
ItemConfig struct {
|
ItemConfig struct {
|
||||||
Show bool `json:"show"`
|
Show bool `json:"show"`
|
||||||
Name string `json:"name"` // display name
|
Name string `json:"name"` // display name
|
||||||
Icon *IconURL `json:"icon"`
|
Icon *IconURL `json:"icon"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Description string `json:"description" aliases:"desc"`
|
Description string `json:"description" aliases:"desc"`
|
||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
WidgetConfig map[string]any `json:"widget_config" aliases:"widget"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item struct {
|
Item struct {
|
||||||
*ItemConfig
|
*ItemConfig
|
||||||
|
WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget"`
|
||||||
|
|
||||||
Alias string
|
Alias string
|
||||||
Provider string
|
Provider string
|
||||||
|
OriginURL string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,23 +43,10 @@ func (cfg *ItemConfig) GetOverride(alias string) *ItemConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (item *Item) MarshalJSON() ([]byte, error) {
|
func (item *Item) MarshalJSON() ([]byte, error) {
|
||||||
var url *string
|
|
||||||
if !strings.ContainsRune(item.Alias, '.') {
|
|
||||||
godoxyCfg := config.GetInstance().Value()
|
|
||||||
// use first domain as base domain
|
|
||||||
domains := godoxyCfg.MatchDomains
|
|
||||||
if len(domains) > 0 {
|
|
||||||
url = new(string)
|
|
||||||
*url = item.Alias + domains[0]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
url = &item.Alias
|
|
||||||
}
|
|
||||||
return json.Marshal(map[string]any{
|
return json.Marshal(map[string]any{
|
||||||
"show": item.Show,
|
"show": item.Show,
|
||||||
"alias": item.Alias,
|
"alias": item.Alias,
|
||||||
"provider": item.Provider,
|
"provider": item.Provider,
|
||||||
"url": url,
|
|
||||||
"name": item.Name,
|
"name": item.Name,
|
||||||
"icon": item.Icon,
|
"icon": item.Icon,
|
||||||
"category": item.Category,
|
"category": item.Category,
|
||||||
|
|
68
internal/homepage/integrations/qbittorrent/client.go
Normal file
68
internal/homepage/integrations/qbittorrent/client.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package qbittorrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/homepage/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
URL string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Initialize(ctx context.Context, url string, cfg map[string]any) error {
|
||||||
|
c.URL = url
|
||||||
|
c.Username = cfg["username"].(string)
|
||||||
|
c.Password = cfg["password"].(string)
|
||||||
|
|
||||||
|
_, err := c.Version(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) doRequest(ctx context.Context, method, endpoint string, query url.Values, body io.Reader) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, c.URL+endpoint+query.Encode(), body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Username != "" && c.Password != "" {
|
||||||
|
req.SetBasicAuth(c.Username, c.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := widgets.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, gperr.Errorf("%w: %d %s", widgets.ErrHTTPStatus, resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonRequest[T any](ctx context.Context, client *Client, endpoint string, query url.Values) (result T, err error) {
|
||||||
|
resp, err := client.doRequest(ctx, http.MethodGet, endpoint, query, nil)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
102
internal/homepage/integrations/qbittorrent/logs.go
Normal file
102
internal/homepage/integrations/qbittorrent/logs.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package qbittorrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const endpointLogs = "/api/v2/log/main"
|
||||||
|
|
||||||
|
type LogEntry struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Timestamp int `json:"timestamp"`
|
||||||
|
Type int `json:"type"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogSeverityNormal = 1 << iota
|
||||||
|
LogSeverityInfo
|
||||||
|
LogSeverityWarning
|
||||||
|
LogSeverityCritical
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *LogEntry) Time() time.Time {
|
||||||
|
return time.Unix(int64(l.Timestamp), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LogEntry) Level() string {
|
||||||
|
switch l.Type {
|
||||||
|
case LogSeverityNormal:
|
||||||
|
return "Normal"
|
||||||
|
case LogSeverityInfo:
|
||||||
|
return "Info"
|
||||||
|
case LogSeverityWarning:
|
||||||
|
return "Warning"
|
||||||
|
case LogSeverityCritical:
|
||||||
|
return "Critical"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LogEntry) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[string]any{
|
||||||
|
"id": l.ID,
|
||||||
|
"timestamp": l.Timestamp,
|
||||||
|
"level": l.Level(),
|
||||||
|
"message": l.Message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// params:
|
||||||
|
//
|
||||||
|
// normal: bool
|
||||||
|
// info: bool
|
||||||
|
// warning: bool
|
||||||
|
// critical: bool
|
||||||
|
// last_known_id: int
|
||||||
|
func (c *Client) GetLogs(ctx context.Context, lastKnownID int) ([]*LogEntry, error) {
|
||||||
|
return jsonRequest[[]*LogEntry](ctx, c, endpointLogs, url.Values{
|
||||||
|
"last_known_id": {strconv.Itoa(lastKnownID)},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) WatchLogs(ctx context.Context) (<-chan *LogEntry, <-chan error) {
|
||||||
|
ch := make(chan *LogEntry)
|
||||||
|
errCh := make(chan error)
|
||||||
|
|
||||||
|
lastKnownID := -1
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
defer close(errCh)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
logs, err := c.GetLogs(ctx, lastKnownID)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(logs) == 0 {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, log := range logs {
|
||||||
|
ch <- log
|
||||||
|
}
|
||||||
|
lastKnownID = logs[len(logs)-1].ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch, errCh
|
||||||
|
}
|
32
internal/homepage/integrations/qbittorrent/transfer_info.go
Normal file
32
internal/homepage/integrations/qbittorrent/transfer_info.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package qbittorrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/homepage/widgets"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const endpointTransferInfo = "/api/v2/transfer/info"
|
||||||
|
|
||||||
|
type TransferInfo struct {
|
||||||
|
ConnectionStatus string `json:"connection_status"`
|
||||||
|
SessionDownloads uint64 `json:"dl_info_data"`
|
||||||
|
SessionUploads uint64 `json:"up_info_data"`
|
||||||
|
DownloadSpeed uint64 `json:"dl_info_speed"`
|
||||||
|
UploadSpeed uint64 `json:"up_info_speed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Data(ctx context.Context) ([]widgets.NameValue, error) {
|
||||||
|
info, err := jsonRequest[TransferInfo](ctx, c, endpointTransferInfo, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []widgets.NameValue{
|
||||||
|
{Name: "Status", Value: info.ConnectionStatus},
|
||||||
|
{Name: "Download", Value: strutils.FormatByteSize(info.SessionDownloads)},
|
||||||
|
{Name: "Upload", Value: strutils.FormatByteSize(info.SessionUploads)},
|
||||||
|
{Name: "Download Speed", Value: strutils.FormatByteSize(info.DownloadSpeed) + "/s"},
|
||||||
|
{Name: "Upload Speed", Value: strutils.FormatByteSize(info.UploadSpeed) + "/s"},
|
||||||
|
}, nil
|
||||||
|
}
|
21
internal/homepage/integrations/qbittorrent/version.go
Normal file
21
internal/homepage/integrations/qbittorrent/version.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package qbittorrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) Version(ctx context.Context) (string, error) {
|
||||||
|
resp, err := c.doRequest(ctx, "GET", "/api/v2/app/version", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(body), nil
|
||||||
|
}
|
14
internal/homepage/widgets/http.go
Normal file
14
internal/homepage/widgets/http.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var HTTPClient = &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrHTTPStatus = gperr.New("http status")
|
49
internal/homepage/widgets/widgets.go
Normal file
49
internal/homepage/widgets/widgets.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Config struct {
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Config Widget `json:"config"`
|
||||||
|
}
|
||||||
|
Widget interface {
|
||||||
|
Initialize(ctx context.Context, url string, cfg map[string]any) error
|
||||||
|
Data(ctx context.Context) ([]NameValue, error)
|
||||||
|
}
|
||||||
|
NameValue struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WidgetProviderQbittorrent = "qbittorrent"
|
||||||
|
)
|
||||||
|
|
||||||
|
var widgetProviders = map[string]struct{}{
|
||||||
|
WidgetProviderQbittorrent: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrInvalidProvider = gperr.New("invalid provider")
|
||||||
|
|
||||||
|
func (cfg *Config) UnmarshalMap(m map[string]any) error {
|
||||||
|
cfg.Provider = m["provider"].(string)
|
||||||
|
if _, ok := widgetProviders[cfg.Provider]; !ok {
|
||||||
|
return ErrInvalidProvider.Subject(cfg.Provider)
|
||||||
|
}
|
||||||
|
delete(m, "provider")
|
||||||
|
m, ok := m["config"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return gperr.New("invalid config")
|
||||||
|
}
|
||||||
|
if err := utils.MapUnmarshalValidate(m, &cfg.Config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -10,6 +10,15 @@ type (
|
||||||
AcceptContentType []ContentType
|
AcceptContentType []ContentType
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContentTypeJSON = ContentType("application/json")
|
||||||
|
ContentTypeTextPlain = ContentType("text/plain")
|
||||||
|
ContentTypeTextHTML = ContentType("text/html")
|
||||||
|
ContentTypeTextMarkdown = ContentType("text/markdown")
|
||||||
|
ContentTypeTextXML = ContentType("text/xml")
|
||||||
|
ContentTypeXHTML = ContentType("application/xhtml+xml")
|
||||||
|
)
|
||||||
|
|
||||||
func GetContentType(h http.Header) ContentType {
|
func GetContentType(h http.Header) ContentType {
|
||||||
ct := h.Get("Content-Type")
|
ct := h.Get("Content-Type")
|
||||||
if ct == "" {
|
if ct == "" {
|
||||||
|
@ -35,15 +44,15 @@ func GetAccept(h http.Header) AcceptContentType {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ct ContentType) IsHTML() bool {
|
func (ct ContentType) IsHTML() bool {
|
||||||
return ct == "text/html" || ct == "application/xhtml+xml"
|
return ct == ContentTypeTextHTML || ct == ContentTypeXHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ct ContentType) IsJSON() bool {
|
func (ct ContentType) IsJSON() bool {
|
||||||
return ct == "application/json"
|
return ct == ContentTypeJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ct ContentType) IsPlainText() bool {
|
func (ct ContentType) IsPlainText() bool {
|
||||||
return ct == "text/plain"
|
return ct == ContentTypeTextPlain
|
||||||
}
|
}
|
||||||
|
|
||||||
func (act AcceptContentType) IsEmpty() bool {
|
func (act AcceptContentType) IsEmpty() bool {
|
||||||
|
@ -68,6 +77,15 @@ func (act AcceptContentType) AcceptJSON() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (act AcceptContentType) AcceptMarkdown() bool {
|
||||||
|
for _, v := range act {
|
||||||
|
if v == ContentTypeTextMarkdown || v == "*/*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (act AcceptContentType) AcceptPlainText() bool {
|
func (act AcceptContentType) AcceptPlainText() bool {
|
||||||
for _, v := range act {
|
for _, v := range act {
|
||||||
if v.IsPlainText() || v == "text/*" || v == "*/*" {
|
if v.IsPlainText() || v == "text/*" || v == "*/*" {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
@ -40,15 +41,22 @@ func ServerError(w http.ResponseWriter, r *http.Request, err error, code ...int)
|
||||||
//
|
//
|
||||||
// For JSON marshallable errors (e.g. gperr.Error), it returns the error details as JSON.
|
// For JSON marshallable errors (e.g. gperr.Error), it returns the error details as JSON.
|
||||||
// Otherwise, it returns the error details as plain text.
|
// Otherwise, it returns the error details as plain text.
|
||||||
func ClientError(w http.ResponseWriter, err error, code ...int) {
|
func ClientError(w http.ResponseWriter, r *http.Request, err error, code ...int) {
|
||||||
if len(code) == 0 {
|
if len(code) == 0 {
|
||||||
code = []int{http.StatusBadRequest}
|
code = []int{http.StatusBadRequest}
|
||||||
}
|
}
|
||||||
if gperr.IsJSONMarshallable(err) {
|
w.WriteHeader(code[0])
|
||||||
|
accept := GetAccept(r.Header)
|
||||||
|
switch {
|
||||||
|
case accept.AcceptJSON():
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(err)
|
json.NewEncoder(w).Encode(err)
|
||||||
} else {
|
case accept.AcceptMarkdown():
|
||||||
http.Error(w, err.Error(), code[0])
|
w.Header().Set("Content-Type", "text/markdown")
|
||||||
|
w.Write(gperr.Markdown(err))
|
||||||
|
default:
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
w.Write(gperr.Plain(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,18 +91,18 @@ func NotFound(w http.ResponseWriter, err string) {
|
||||||
BadRequest(w, err, http.StatusNotFound)
|
BadRequest(w, err, http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrMissingKey(k string) error {
|
func MissingKey(w http.ResponseWriter, k string) {
|
||||||
return gperr.New(k + " is required")
|
BadRequest(w, k+" is required", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrInvalidKey(k string) error {
|
func InvalidKey(w http.ResponseWriter, k string) {
|
||||||
return gperr.New(k + " is invalid")
|
BadRequest(w, k+" is invalid", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrAlreadyExists(k, v string) error {
|
func KeyAlreadyExists(w http.ResponseWriter, k, v string) {
|
||||||
return gperr.Errorf("%s %q already exists", k, v)
|
BadRequest(w, fmt.Sprintf("%s %q already exists", k, v), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrNotFound(k, v string) error {
|
func ValueNotFound(w http.ResponseWriter, k, v string) {
|
||||||
return gperr.Errorf("%s %q not found", k, v)
|
BadRequest(w, fmt.Sprintf("%s %q not found", k, v), http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ func Get(name string) (*Middleware, Error) {
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrUnknownMiddleware.
|
return nil, ErrUnknownMiddleware.
|
||||||
Subject(name).
|
Subject(name).
|
||||||
Withf(strutils.DoYouMean(utils.NearestField(name, allMiddlewares)))
|
With(gperr.DoYouMean(utils.NearestField(name, allMiddlewares)))
|
||||||
}
|
}
|
||||||
return middleware, nil
|
return middleware, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,9 @@ type (
|
||||||
FieldsBody []LogField
|
FieldsBody []LogField
|
||||||
ListBody []string
|
ListBody []string
|
||||||
MessageBody string
|
MessageBody string
|
||||||
|
ErrorBody struct {
|
||||||
|
Error error
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -113,3 +116,15 @@ func (m MessageBody) Format(format *LogFormat) ([]byte, error) {
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("unknown format: %v", format)
|
return nil, fmt.Errorf("unknown format: %v", format)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e ErrorBody) Format(format *LogFormat) ([]byte, error) {
|
||||||
|
switch format {
|
||||||
|
case LogFormatRawJSON:
|
||||||
|
return json.Marshal(e)
|
||||||
|
case LogFormatPlain:
|
||||||
|
return gperr.Plain(e.Error), nil
|
||||||
|
case LogFormatMarkdown:
|
||||||
|
return gperr.Markdown(e.Error), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unknown format: %v", format)
|
||||||
|
}
|
||||||
|
|
|
@ -259,7 +259,7 @@ func mapUnmarshalValidate(src SerializedObject, dst any, checkValidateTag bool)
|
||||||
errs.Add(err)
|
errs.Add(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errs.Add(ErrUnknownField.Subject(k).Withf(strutils.DoYouMean(NearestField(k, mapping))))
|
errs.Add(ErrUnknownField.Subject(k).With(gperr.DoYouMean(NearestField(k, mapping))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasValidateTag && checkValidateTag {
|
if hasValidateTag && checkValidateTag {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package ansi
|
package ansi
|
||||||
|
|
||||||
import "regexp"
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
var ansiRegexp = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
var ansiRegexp = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||||
|
|
||||||
|
@ -43,11 +45,3 @@ func WithANSI(s string, ansi string) string {
|
||||||
func StripANSI(s string) string {
|
func StripANSI(s string) string {
|
||||||
return ansiRegexp.ReplaceAllString(s, "")
|
return ansiRegexp.ReplaceAllString(s, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
var ToHTMLClass = map[string]string{
|
|
||||||
"[91": "log-red",
|
|
||||||
"[92": "log-green",
|
|
||||||
"[93": "log-yellow",
|
|
||||||
"[96": "log-cyan",
|
|
||||||
"[97": "log-white",
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AppendDuration appends a duration to a buffer with the following format:
|
// AppendDuration appends a duration to a buffer with the following format:
|
||||||
|
@ -215,13 +213,6 @@ func AppendByteSize[T ~int | ~uint | ~int64 | ~uint64 | ~float64](size T, buf []
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func DoYouMean(s string) string {
|
|
||||||
if s == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "Did you mean " + ansi.Info(s) + "?"
|
|
||||||
}
|
|
||||||
|
|
||||||
func Pluralize(n int64) string {
|
func Pluralize(n int64) string {
|
||||||
if n > 1 {
|
if n > 1 {
|
||||||
return "s"
|
return "s"
|
||||||
|
|
Loading…
Add table
Reference in a new issue