mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 22:30:47 +01:00
Compare commits
13 Commits
feat/agent
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaa3c9a8d8 | ||
|
|
bc44de3196 | ||
|
|
12b784d126 | ||
|
|
71f6636cc3 | ||
|
|
cc1fe30045 | ||
|
|
4ec352f1f6 | ||
|
|
df530245bd | ||
|
|
1a022bb3f4 | ||
|
|
2e57ca7743 | ||
|
|
69d04f1b76 | ||
|
|
74f97a6621 | ||
|
|
dc1b70d2d7 | ||
|
|
6fac5d2d3e |
4
Makefile
4
Makefile
@@ -3,6 +3,8 @@ export VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
|
||||
export GOOS = linux
|
||||
|
||||
REPO_URL ?= https://github.com/yusing/godoxy
|
||||
|
||||
WEBUI_DIR ?= ../godoxy-webui
|
||||
DOCS_DIR ?= ${WEBUI_DIR}/wiki
|
||||
|
||||
@@ -175,4 +177,4 @@ gen-api-types: gen-swagger
|
||||
|
||||
.PHONY: update-wiki
|
||||
update-wiki:
|
||||
DOCS_DIR=${DOCS_DIR} bun --bun scripts/update-wiki/main.ts
|
||||
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
stdlog "log"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
@@ -18,7 +20,6 @@ import (
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httpServer "github.com/yusing/goutils/server"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"github.com/yusing/goutils/task"
|
||||
"github.com/yusing/goutils/version"
|
||||
@@ -145,12 +146,19 @@ Tips:
|
||||
runtime := strutils.Title(string(env.Runtime))
|
||||
|
||||
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
|
||||
opts := httpServer.Options{
|
||||
Name: runtime,
|
||||
HTTPAddr: socketproxy.ListenAddr,
|
||||
Handler: socketproxy.NewHandler(),
|
||||
l, err := net.Listen("tcp", socketproxy.ListenAddr)
|
||||
if err != nil {
|
||||
gperr.LogFatal("failed to listen on port", err)
|
||||
}
|
||||
httpServer.StartServer(t, opts)
|
||||
errLog := log.Logger.With().Str("level", "error").Str("component", "socketproxy").Logger()
|
||||
srv := http.Server{
|
||||
Handler: socketproxy.NewHandler(),
|
||||
BaseContext: func(net.Listener) context.Context {
|
||||
return t.Context()
|
||||
},
|
||||
ErrorLog: stdlog.New(&errLog, "", 0),
|
||||
}
|
||||
srv.Serve(l)
|
||||
}
|
||||
|
||||
systeminfo.Poller.Start()
|
||||
|
||||
22
agent/go.mod
22
agent/go.mod
@@ -22,20 +22,16 @@ require (
|
||||
github.com/pion/transport/v3 v3.1.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/yusing/godoxy v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/godoxy v0.23.1
|
||||
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/goutils v0.7.0
|
||||
github.com/yusing/goutils/server v0.0.0-20260103043911-785deb23bd64
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.11.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
@@ -56,13 +52,12 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.1 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -76,15 +71,11 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.58.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.19.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
@@ -92,12 +83,11 @@ require (
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.68.0 // indirect
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
github.com/yusing/ds v0.3.1 // indirect
|
||||
github.com/yusing/gointernals v0.1.16 // indirect
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260103043911-785deb23bd64 // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260103043911-785deb23bd64 // indirect
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260109021609-78fda75d1e58 // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260109021609-78fda75d1e58 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||
@@ -107,7 +97,7 @@ require (
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
84
agent/go.sum
84
agent/go.sum
@@ -55,8 +55,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-acme/lego/v4 v4.30.1 h1:tmb6U0lvy8Mc3lQbqKwTat7oAhE8FUYNJ3D0gSg6pJU=
|
||||
github.com/go-acme/lego/v4 v4.30.1/go.mod h1:V7m/Ip+EeFkjOe028+zeH+SwWtESxw1LHelwMIfAjm4=
|
||||
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -79,12 +79,11 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -114,8 +113,8 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.3.1 h1:h64s4/zIEQ06TBo0phFKcckV441YpvUPgLfRAptYsjY=
|
||||
github.com/luthermonson/go-proxmox v0.3.1/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=
|
||||
github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -204,13 +203,12 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
|
||||
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||
@@ -237,93 +235,31 @@ go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -27,26 +27,26 @@ graph TD
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Purpose |
|
||||
| -------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [`config.go`](agent/pkg/agent/config.go) | Core configuration, initialization, and API client logic. |
|
||||
| [`new_agent.go`](agent/pkg/agent/new_agent.go) | Agent creation and certificate generation logic. |
|
||||
| [`docker_compose.go`](agent/pkg/agent/docker_compose.go) | Generator for agent Docker Compose configurations. |
|
||||
| [`bare_metal.go`](agent/pkg/agent/bare_metal.go) | Generator for bare metal installation scripts. |
|
||||
| [`env.go`](agent/pkg/agent/env.go) | Environment configuration types and constants. |
|
||||
| [`common/`](agent/pkg/agent/common) | Shared constants and utilities for agents. |
|
||||
| File | Purpose |
|
||||
| ---------------------------------------- | --------------------------------------------------------- |
|
||||
| [`config.go`](config.go) | Core configuration, initialization, and API client logic. |
|
||||
| [`new_agent.go`](new_agent.go) | Agent creation and certificate generation logic. |
|
||||
| [`docker_compose.go`](docker_compose.go) | Generator for agent Docker Compose configurations. |
|
||||
| [`bare_metal.go`](bare_metal.go) | Generator for bare metal installation scripts. |
|
||||
| [`env.go`](env.go) | Environment configuration types and constants. |
|
||||
| `common/` | Shared constants and utilities for agents. |
|
||||
|
||||
## Core Types
|
||||
|
||||
### [`AgentConfig`](agent/pkg/agent/config.go:29)
|
||||
### [`AgentConfig`](config.go:29)
|
||||
|
||||
The primary struct used by the GoDoxy server to manage a connection to an agent. It stores the agent's address, metadata, and TLS configuration.
|
||||
|
||||
### [`AgentInfo`](agent/pkg/agent/config.go:45)
|
||||
### [`AgentInfo`](config.go:45)
|
||||
|
||||
Contains basic metadata about the agent, including its version, name, and container runtime (Docker or Podman).
|
||||
|
||||
### [`PEMPair`](agent/pkg/agent/new_agent.go:53)
|
||||
### [`PEMPair`](new_agent.go:53)
|
||||
|
||||
A utility struct for handling PEM-encoded certificate and key pairs, supporting encryption, decryption, and conversion to `tls.Certificate`.
|
||||
|
||||
@@ -54,7 +54,7 @@ A utility struct for handling PEM-encoded certificate and key pairs, supporting
|
||||
|
||||
### Certificate Generation
|
||||
|
||||
The [`NewAgent`](agent/pkg/agent/new_agent.go:147) function creates a complete certificate infrastructure for an agent:
|
||||
The [`NewAgent`](new_agent.go:147) function creates a complete certificate infrastructure for an agent:
|
||||
|
||||
- **CA Certificate**: Self-signed root certificate with 1000-year validity.
|
||||
- **Server Certificate**: For the agent's HTTPS server, signed by the CA.
|
||||
@@ -65,18 +65,18 @@ All certificates use ECDSA with P-256 curve and SHA-256 signatures.
|
||||
### Certificate Security
|
||||
|
||||
- Certificates are encrypted using AES-GCM with a provided encryption key.
|
||||
- The [`PEMPair`](agent/pkg/agent/new_agent.go:53) struct provides methods for encryption, decryption, and conversion to `tls.Certificate`.
|
||||
- The [`PEMPair`](new_agent.go:53) struct provides methods for encryption, decryption, and conversion to `tls.Certificate`.
|
||||
- Base64 encoding is used for certificate storage and transmission.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Secure Communication
|
||||
|
||||
All communication between the GoDoxy server and agents is secured using mutual TLS (mTLS). The [`AgentConfig`](agent/pkg/agent/config.go:29) handles the loading of CA and client certificates to establish secure connections.
|
||||
All communication between the GoDoxy server and agents is secured using mutual TLS (mTLS). The [`AgentConfig`](config.go:29) handles the loading of CA and client certificates to establish secure connections.
|
||||
|
||||
### 2. Agent Discovery and Initialization
|
||||
|
||||
The [`Init`](agent/pkg/agent/config.go:231) and [`InitWithCerts`](agent/pkg/agent/config.go:110) methods allow the server to:
|
||||
The [`Init`](config.go:231) and [`InitWithCerts`](config.go:110) methods allow the server to:
|
||||
|
||||
- Fetch agent metadata (version, name, runtime).
|
||||
- Verify compatibility between server and agent versions.
|
||||
@@ -86,12 +86,12 @@ The [`Init`](agent/pkg/agent/config.go:231) and [`InitWithCerts`](agent/pkg/agen
|
||||
|
||||
The package provides interfaces and implementations for generating deployment artifacts:
|
||||
|
||||
- **Docker Compose**: Generates a `docker-compose.yml` for running the agent as a container via [`AgentComposeConfig.Generate()`](agent/pkg/agent/docker_compose.go:21).
|
||||
- **Bare Metal**: Generates a shell script to install and run the agent as a systemd service via [`AgentEnvConfig.Generate()`](agent/pkg/agent/bare_metal.go:27).
|
||||
- **Docker Compose**: Generates a `docker-compose.yml` for running the agent as a container via [`AgentComposeConfig.Generate()`](docker_compose.go:21).
|
||||
- **Bare Metal**: Generates a shell script to install and run the agent as a systemd service via [`AgentEnvConfig.Generate()`](bare_metal.go:27).
|
||||
|
||||
### 4. Fake Docker Host
|
||||
|
||||
The package supports a "fake" Docker host scheme (`agent://<addr>`) to identify containers managed by an agent, allowing the GoDoxy server to route requests appropriately. See [`IsDockerHostAgent`](agent/pkg/agent/config.go:90) and [`GetAgentAddrFromDockerHost`](agent/pkg/agent/config.go:94).
|
||||
The package supports a "fake" Docker host scheme (`agent://<addr>`) to identify containers managed by an agent, allowing the GoDoxy server to route requests appropriately. See [`IsDockerHostAgent`](config.go:90) and [`GetAgentAddrFromDockerHost`](config.go:94).
|
||||
|
||||
## Usage Example
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -147,9 +148,11 @@ func (s *TCPServer) handle(conn net.Conn) {
|
||||
func (s *TCPServer) redirect(conn net.Conn) (net.Conn, error) {
|
||||
// Read the stream header once as a handshake.
|
||||
var headerBuf [headerSize]byte
|
||||
_ = conn.SetReadDeadline(time.Now().Add(dialTimeout))
|
||||
if _, err := io.ReadFull(conn, headerBuf[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
|
||||
header := ToHeader(&headerBuf)
|
||||
if !header.Validate() {
|
||||
|
||||
@@ -102,10 +102,13 @@ func (s *UDPServer) handleDTLSConnection(clientConn net.Conn) {
|
||||
|
||||
// Read the stream header once as a handshake.
|
||||
var headerBuf [headerSize]byte
|
||||
_ = clientConn.SetReadDeadline(time.Now().Add(dialTimeout))
|
||||
if _, err := io.ReadFull(clientConn, headerBuf[:]); err != nil {
|
||||
s.logger(clientConn).Err(err).Msg("failed to read stream header")
|
||||
return
|
||||
}
|
||||
_ = clientConn.SetReadDeadline(time.Time{})
|
||||
|
||||
header := ToHeader(&headerBuf)
|
||||
if !header.Validate() {
|
||||
s.logger(clientConn).Error().Bytes("header", headerBuf[:]).Msg("invalid stream header received")
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/env"
|
||||
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||
"github.com/yusing/goutils/server"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
CACert, ServerCert *tls.Certificate
|
||||
Port int
|
||||
}
|
||||
|
||||
func StartAgentServer(parent task.Parent, opt Options) {
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(opt.CACert.Leaf)
|
||||
|
||||
// Configure TLS
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*opt.ServerCert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
}
|
||||
|
||||
if env.AgentSkipClientCertCheck {
|
||||
tlsConfig.ClientAuth = tls.NoClientCert
|
||||
}
|
||||
|
||||
agentServer := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", opt.Port),
|
||||
Handler: handler.NewAgentHandler(),
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
server.Start(parent.Subtask("agent-server", false), agentServer, server.WithLogger(&log.Logger))
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/config"
|
||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
"github.com/yusing/godoxy/internal/logging"
|
||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
@@ -39,7 +39,7 @@ func main() {
|
||||
log.Trace().Msg("trace enabled")
|
||||
parallel(
|
||||
dnsproviders.InitProviders,
|
||||
homepage.InitIconListCache,
|
||||
iconlist.InitCache,
|
||||
systeminfo.Poller.Start,
|
||||
middleware.LoadComposeFiles,
|
||||
)
|
||||
|
||||
26
go.mod
26
go.mod
@@ -18,7 +18,7 @@ require (
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
|
||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||
github.com/gin-gonic/gin v1.11.0 // api server
|
||||
github.com/go-acme/lego/v4 v4.30.1 // acme client
|
||||
github.com/go-acme/lego/v4 v4.31.0 // acme client
|
||||
github.com/go-playground/validator/v10 v10.30.1 // validator
|
||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||
@@ -39,9 +39,9 @@ require (
|
||||
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||
github.com/bytedance/sonic v1.14.2 // fast json parsing
|
||||
github.com/docker/cli v29.1.3+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||
github.com/goccy/go-yaml v1.19.1 // yaml parsing for different config files
|
||||
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
|
||||
github.com/luthermonson/go-proxmox v0.3.1 // proxmox API client
|
||||
github.com/luthermonson/go-proxmox v0.3.2 // proxmox API client
|
||||
github.com/moby/moby/api v1.52.0 // docker API
|
||||
github.com/moby/moby/client v0.2.1 // docker client
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
|
||||
@@ -49,15 +49,15 @@ require (
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 // system information
|
||||
github.com/spf13/afero v1.15.0 // afero for file system operations
|
||||
github.com/stretchr/testify v1.11.1 // testing framework
|
||||
github.com/valyala/fasthttp v1.68.0 // fast http for health check
|
||||
github.com/valyala/fasthttp v1.69.0 // fast http for health check
|
||||
github.com/yusing/ds v0.3.1 // data structures and algorithms
|
||||
github.com/yusing/godoxy/agent v0.0.0-20260104140148-1c2515cb298d
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260104140148-1c2515cb298d
|
||||
github.com/yusing/godoxy/agent v0.0.0-20260109022755-4275cdae3854
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260109022755-4275cdae3854
|
||||
github.com/yusing/gointernals v0.1.16
|
||||
github.com/yusing/goutils v0.7.0
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260103043911-785deb23bd64
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260103043911-785deb23bd64
|
||||
github.com/yusing/goutils/server v0.0.0-20260103043911-785deb23bd64
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260109021609-78fda75d1e58
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260109021609-78fda75d1e58
|
||||
github.com/yusing/goutils/server v0.0.0-20260109021609-78fda75d1e58
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -92,7 +92,7 @@ require (
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
@@ -132,10 +132,10 @@ require (
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/api v0.258.0 // indirect
|
||||
google.golang.org/api v0.259.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
@@ -166,7 +166,7 @@ require (
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/linode/linodego v1.63.0 // indirect
|
||||
github.com/linode/linodego v1.64.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 // indirect
|
||||
|
||||
32
go.sum
32
go.sum
@@ -100,8 +100,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-acme/lego/v4 v4.30.1 h1:tmb6U0lvy8Mc3lQbqKwTat7oAhE8FUYNJ3D0gSg6pJU=
|
||||
github.com/go-acme/lego/v4 v4.30.1/go.mod h1:V7m/Ip+EeFkjOe028+zeH+SwWtESxw1LHelwMIfAjm4=
|
||||
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -132,8 +132,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
@@ -151,8 +151,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9 h1:TOpi/QG8iDcZlkQlGlFUti/ZtyLkliXvHDcyUIMuFrU=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
@@ -191,14 +191,14 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/linode/linodego v1.63.0 h1:MdjizfXNJDVJU6ggoJmMO5O9h4KGPGivNX0fzrAnstk=
|
||||
github.com/linode/linodego v1.63.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||
github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY=
|
||||
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.3.1 h1:h64s4/zIEQ06TBo0phFKcckV441YpvUPgLfRAptYsjY=
|
||||
github.com/luthermonson/go-proxmox v0.3.1/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=
|
||||
github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -319,8 +319,8 @@ github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
|
||||
@@ -416,8 +416,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -451,8 +451,8 @@ golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
|
||||
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
|
||||
google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ=
|
||||
google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
|
||||
2
goutils
2
goutils
Submodule goutils updated: 78fda75d1e...326c1f1eb3
@@ -811,7 +811,7 @@
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/homepage.FetchResult"
|
||||
"$ref": "#/definitions/iconfetch.Result"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1698,7 +1698,7 @@
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/homepage.IconMetaSearch"
|
||||
"$ref": "#/definitions/iconlist.IconMetaSearch"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4335,8 +4335,7 @@
|
||||
"items": {
|
||||
"$ref": "#/definitions/rules.Rule"
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
"x-nullable": true
|
||||
},
|
||||
"scheme": {
|
||||
"type": "string",
|
||||
@@ -5202,7 +5201,7 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"homepage.FetchResult": {
|
||||
"iconfetch.Result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
@@ -5223,7 +5222,7 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"homepage.IconMetaSearch": {
|
||||
"iconlist.IconMetaSearch": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Dark": {
|
||||
@@ -5252,7 +5251,7 @@
|
||||
"x-omitempty": false
|
||||
},
|
||||
"Source": {
|
||||
"$ref": "#/definitions/homepage.IconSource",
|
||||
"$ref": "#/definitions/icons.Source",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
@@ -5265,7 +5264,7 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"homepage.IconSource": {
|
||||
"icons.Source": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"https://",
|
||||
@@ -5274,10 +5273,10 @@
|
||||
"@selfhst"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"IconSourceAbsolute",
|
||||
"IconSourceRelative",
|
||||
"IconSourceWalkXCode",
|
||||
"IconSourceSelfhSt"
|
||||
"SourceAbsolute",
|
||||
"SourceRelative",
|
||||
"SourceWalkXCode",
|
||||
"SourceSelfhSt"
|
||||
],
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
@@ -5515,8 +5514,7 @@
|
||||
"items": {
|
||||
"$ref": "#/definitions/rules.Rule"
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
"x-nullable": true
|
||||
},
|
||||
"scheme": {
|
||||
"type": "string",
|
||||
|
||||
@@ -959,6 +959,7 @@ definitions:
|
||||
items:
|
||||
$ref: '#/definitions/rules.Rule'
|
||||
type: array
|
||||
x-nullable: true
|
||||
scheme:
|
||||
enum:
|
||||
- http
|
||||
@@ -1428,7 +1429,7 @@ definitions:
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
homepage.FetchResult:
|
||||
iconfetch.Result:
|
||||
properties:
|
||||
icon:
|
||||
items:
|
||||
@@ -1438,7 +1439,7 @@ definitions:
|
||||
statusCode:
|
||||
type: integer
|
||||
type: object
|
||||
homepage.IconMetaSearch:
|
||||
iconlist.IconMetaSearch:
|
||||
properties:
|
||||
Dark:
|
||||
type: boolean
|
||||
@@ -1451,11 +1452,11 @@ definitions:
|
||||
SVG:
|
||||
type: boolean
|
||||
Source:
|
||||
$ref: '#/definitions/homepage.IconSource'
|
||||
$ref: '#/definitions/icons.Source'
|
||||
WebP:
|
||||
type: boolean
|
||||
type: object
|
||||
homepage.IconSource:
|
||||
icons.Source:
|
||||
enum:
|
||||
- https://
|
||||
- '@target'
|
||||
@@ -1463,10 +1464,10 @@ definitions:
|
||||
- '@selfhst'
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- IconSourceAbsolute
|
||||
- IconSourceRelative
|
||||
- IconSourceWalkXCode
|
||||
- IconSourceSelfhSt
|
||||
- SourceAbsolute
|
||||
- SourceRelative
|
||||
- SourceWalkXCode
|
||||
- SourceSelfhSt
|
||||
mem.VirtualMemoryStat:
|
||||
properties:
|
||||
available:
|
||||
@@ -1594,6 +1595,7 @@ definitions:
|
||||
items:
|
||||
$ref: '#/definitions/rules.Rule'
|
||||
type: array
|
||||
x-nullable: true
|
||||
scheme:
|
||||
enum:
|
||||
- http
|
||||
@@ -2229,7 +2231,7 @@ paths:
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/homepage.FetchResult'
|
||||
$ref: '#/definitions/iconfetch.Result'
|
||||
type: array
|
||||
"400":
|
||||
description: 'Bad Request: alias is empty or route is not HTTPRoute'
|
||||
@@ -2811,7 +2813,7 @@ paths:
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/homepage.IconMetaSearch'
|
||||
$ref: '#/definitions/iconlist.IconMetaSearch'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
|
||||
@@ -13,9 +14,9 @@ import (
|
||||
)
|
||||
|
||||
type GetFavIconRequest struct {
|
||||
URL string `form:"url" binding:"required_without=Alias"`
|
||||
Alias string `form:"alias" binding:"required_without=URL"`
|
||||
Variant homepage.IconVariant `form:"variant" binding:"omitempty,oneof=light dark"`
|
||||
URL string `form:"url" binding:"required_without=Alias"`
|
||||
Alias string `form:"alias" binding:"required_without=URL"`
|
||||
Variant icons.Variant `form:"variant" binding:"omitempty,oneof=light dark"`
|
||||
} // @name GetFavIconRequest
|
||||
|
||||
// @x-id "favicon"
|
||||
@@ -27,7 +28,7 @@ type GetFavIconRequest struct {
|
||||
// @Produce image/svg+xml,image/x-icon,image/png,image/webp
|
||||
// @Param url query string false "URL of the route"
|
||||
// @Param alias query string false "Alias of the route"
|
||||
// @Success 200 {array} homepage.FetchResult
|
||||
// @Success 200 {array} iconfetch.Result
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Bad Request: alias is empty or route is not HTTPRoute"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden: unauthorized"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Not Found: route or icon not found"
|
||||
@@ -42,18 +43,18 @@ func FavIcon(c *gin.Context) {
|
||||
|
||||
// try with url
|
||||
if request.URL != "" {
|
||||
var iconURL homepage.IconURL
|
||||
var iconURL icons.URL
|
||||
if err := iconURL.Parse(request.URL); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid url", err))
|
||||
return
|
||||
}
|
||||
icon := &iconURL
|
||||
if request.Variant != homepage.IconVariantNone {
|
||||
if request.Variant != icons.VariantNone {
|
||||
icon = icon.WithVariant(request.Variant)
|
||||
}
|
||||
fetchResult, err := homepage.FetchFavIconFromURL(c.Request.Context(), icon)
|
||||
fetchResult, err := iconfetch.FetchFavIconFromURL(c.Request.Context(), icon)
|
||||
if err != nil {
|
||||
homepage.GinFetchError(c, fetchResult.StatusCode, err)
|
||||
iconfetch.GinError(c, fetchResult.StatusCode, err)
|
||||
return
|
||||
}
|
||||
c.Data(fetchResult.StatusCode, fetchResult.ContentType(), fetchResult.Icon)
|
||||
@@ -63,40 +64,40 @@ func FavIcon(c *gin.Context) {
|
||||
// try with alias
|
||||
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias, request.Variant)
|
||||
if err != nil {
|
||||
homepage.GinFetchError(c, result.StatusCode, err)
|
||||
iconfetch.GinError(c, result.StatusCode, err)
|
||||
return
|
||||
}
|
||||
c.Data(result.StatusCode, result.ContentType(), result.Icon)
|
||||
}
|
||||
|
||||
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
|
||||
func GetFavIconFromAlias(ctx context.Context, alias string, variant homepage.IconVariant) (homepage.FetchResult, error) {
|
||||
func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) {
|
||||
// try with route.Icon
|
||||
r, ok := routes.HTTP.Get(alias)
|
||||
if !ok {
|
||||
return homepage.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||
return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||
}
|
||||
|
||||
var (
|
||||
result homepage.FetchResult
|
||||
result iconfetch.Result
|
||||
err error
|
||||
)
|
||||
hp := r.HomepageItem()
|
||||
if hp.Icon != nil {
|
||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, variant)
|
||||
} else if variant != homepage.IconVariantNone {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant))
|
||||
if hp.Icon.Source == icons.SourceRelative {
|
||||
result, err = iconfetch.FindIcon(ctx, r, *hp.Icon.FullURL, variant)
|
||||
} else if variant != icons.VariantNone {
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant))
|
||||
if err != nil {
|
||||
// fallback to no variant
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(homepage.IconVariantNone))
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(icons.VariantNone))
|
||||
}
|
||||
} else {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
}
|
||||
} else {
|
||||
// try extract from "link[rel=icon]"
|
||||
result, err = homepage.FindIcon(ctx, r, "/", variant)
|
||||
result, err = iconfetch.FindIcon(ctx, r, "/", variant)
|
||||
}
|
||||
if result.StatusCode == 0 {
|
||||
result.StatusCode = http.StatusOK
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ type ListIconsRequest struct {
|
||||
// @Produce json
|
||||
// @Param limit query int false "Limit"
|
||||
// @Param keyword query string false "Keyword"
|
||||
// @Success 200 {array} homepage.IconMetaSearch
|
||||
// @Success 200 {array} iconlist.IconMetaSearch
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Router /icons [get]
|
||||
@@ -32,6 +32,6 @@ func Icons(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
icons := homepage.SearchIcons(request.Keyword, request.Limit)
|
||||
icons := iconlist.SearchIcons(request.Keyword, request.Limit)
|
||||
c.JSON(http.StatusOK, icons)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ go 1.25.5
|
||||
replace github.com/yusing/godoxy => ../..
|
||||
|
||||
require (
|
||||
github.com/go-acme/lego/v4 v4.30.1
|
||||
github.com/yusing/godoxy v0.23.0
|
||||
github.com/go-acme/lego/v4 v4.31.0
|
||||
github.com/yusing/godoxy v0.23.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -42,13 +42,13 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/go-resty/resty/v2 v2.17.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.1 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/gotify/server/v2 v2.8.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
@@ -57,7 +57,7 @@ require (
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/linode/linodego v1.63.0 // indirect
|
||||
github.com/linode/linodego v1.64.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/maxatome/go-testdeep v1.14.0 // indirect
|
||||
@@ -95,10 +95,10 @@ require (
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/api v0.258.0 // indirect
|
||||
google.golang.org/api v0.259.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
|
||||
@@ -62,8 +62,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/go-acme/lego/v4 v4.30.1 h1:tmb6U0lvy8Mc3lQbqKwTat7oAhE8FUYNJ3D0gSg6pJU=
|
||||
github.com/go-acme/lego/v4 v4.30.1/go.mod h1:V7m/Ip+EeFkjOe028+zeH+SwWtESxw1LHelwMIfAjm4=
|
||||
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -85,8 +85,8 @@ github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6x
|
||||
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
@@ -103,8 +103,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9 h1:TOpi/QG8iDcZlkQlGlFUti/ZtyLkliXvHDcyUIMuFrU=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||
@@ -131,8 +131,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/linode/linodego v1.63.0 h1:MdjizfXNJDVJU6ggoJmMO5O9h4KGPGivNX0fzrAnstk=
|
||||
github.com/linode/linodego v1.63.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||
github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY=
|
||||
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
@@ -237,8 +237,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
@@ -249,8 +249,8 @@ golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
|
||||
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
|
||||
google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ=
|
||||
google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/ds/ordered"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
"github.com/yusing/godoxy/internal/homepage/widgets"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
@@ -22,13 +23,13 @@ type (
|
||||
} // @name HomepageCategory
|
||||
|
||||
ItemConfig struct {
|
||||
Show bool `json:"show"`
|
||||
Name string `json:"name"` // display name
|
||||
Icon *IconURL `json:"icon" swaggertype:"string"`
|
||||
Category string `json:"category" validate:"omitempty"`
|
||||
Description string `json:"description" aliases:"desc"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Favorite bool `json:"favorite"`
|
||||
Show bool `json:"show"`
|
||||
Name string `json:"name"` // display name
|
||||
Icon *icons.URL `json:"icon" swaggertype:"string"`
|
||||
Category string `json:"category" validate:"omitempty"`
|
||||
Description string `json:"description" aliases:"desc"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Favorite bool `json:"favorite"`
|
||||
|
||||
WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget" extensions:"x-nullable"`
|
||||
} // @name HomepageItemConfig
|
||||
|
||||
@@ -4,19 +4,24 @@ import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func TestOverrideItem(t *testing.T) {
|
||||
a := &Item{
|
||||
Alias: "foo",
|
||||
ItemConfig: ItemConfig{
|
||||
Show: false,
|
||||
Name: "Foo",
|
||||
Icon: &IconURL{
|
||||
FullURL: strPtr("/favicon.ico"),
|
||||
IconSource: IconSourceRelative,
|
||||
Icon: &icons.URL{
|
||||
FullURL: strPtr("/favicon.ico"),
|
||||
Source: icons.SourceRelative,
|
||||
},
|
||||
Category: "App",
|
||||
},
|
||||
@@ -25,9 +30,9 @@ func TestOverrideItem(t *testing.T) {
|
||||
Show: true,
|
||||
Name: "Bar",
|
||||
Category: "Test",
|
||||
Icon: &IconURL{
|
||||
FullURL: strPtr("@walkxcode/example.png"),
|
||||
IconSource: IconSourceWalkXCode,
|
||||
Icon: &icons.URL{
|
||||
FullURL: strPtr("@walkxcode/example.png"),
|
||||
Source: icons.SourceWalkXCode,
|
||||
},
|
||||
}
|
||||
overrides := GetOverrideConfig()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package homepage
|
||||
package iconfetch
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -1,4 +1,4 @@
|
||||
package homepage
|
||||
package iconfetch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/vincent-petithory/dataurl"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/cache"
|
||||
@@ -22,22 +24,22 @@ import (
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
type FetchResult struct {
|
||||
type Result struct {
|
||||
Icon []byte
|
||||
StatusCode int
|
||||
|
||||
contentType string
|
||||
}
|
||||
|
||||
func FetchResultWithErrorf(statusCode int, msgFmt string, args ...any) (FetchResult, error) {
|
||||
return FetchResult{StatusCode: statusCode}, fmt.Errorf(msgFmt, args...)
|
||||
func FetchResultWithErrorf(statusCode int, msgFmt string, args ...any) (Result, error) {
|
||||
return Result{StatusCode: statusCode}, fmt.Errorf(msgFmt, args...)
|
||||
}
|
||||
|
||||
func FetchResultOK(icon []byte, contentType string) (FetchResult, error) {
|
||||
return FetchResult{Icon: icon, contentType: contentType}, nil
|
||||
func FetchResultOK(icon []byte, contentType string) (Result, error) {
|
||||
return Result{Icon: icon, contentType: contentType}, nil
|
||||
}
|
||||
|
||||
func GinFetchError(c *gin.Context, statusCode int, err error) {
|
||||
func GinError(c *gin.Context, statusCode int, err error) {
|
||||
if statusCode == 0 {
|
||||
statusCode = http.StatusInternalServerError
|
||||
}
|
||||
@@ -50,7 +52,7 @@ func GinFetchError(c *gin.Context, statusCode int, err error) {
|
||||
|
||||
const faviconFetchTimeout = 3 * time.Second
|
||||
|
||||
func (res *FetchResult) ContentType() string {
|
||||
func (res *Result) ContentType() string {
|
||||
if res.contentType == "" {
|
||||
if bytes.HasPrefix(res.Icon, []byte("<svg")) || bytes.HasPrefix(res.Icon, []byte("<?xml")) {
|
||||
return "image/svg+xml"
|
||||
@@ -62,19 +64,19 @@ func (res *FetchResult) ContentType() string {
|
||||
|
||||
const maxRedirectDepth = 5
|
||||
|
||||
func FetchFavIconFromURL(ctx context.Context, iconURL *IconURL) (FetchResult, error) {
|
||||
switch iconURL.IconSource {
|
||||
case IconSourceAbsolute:
|
||||
func FetchFavIconFromURL(ctx context.Context, iconURL *icons.URL) (Result, error) {
|
||||
switch iconURL.Source {
|
||||
case icons.SourceAbsolute:
|
||||
return FetchIconAbsolute(ctx, iconURL.URL())
|
||||
case IconSourceRelative:
|
||||
case icons.SourceRelative:
|
||||
return FetchResultWithErrorf(http.StatusBadRequest, "unexpected relative icon")
|
||||
case IconSourceWalkXCode, IconSourceSelfhSt:
|
||||
case icons.SourceWalkXCode, icons.SourceSelfhSt:
|
||||
return fetchKnownIcon(ctx, iconURL)
|
||||
}
|
||||
return FetchResultWithErrorf(http.StatusBadRequest, "invalid icon source")
|
||||
}
|
||||
|
||||
var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (FetchResult, error) {
|
||||
var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (Result, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return FetchResultWithErrorf(http.StatusInternalServerError, "cannot create request: %w", err)
|
||||
@@ -103,7 +105,7 @@ var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (
|
||||
return FetchResultWithErrorf(http.StatusNotFound, "empty icon")
|
||||
}
|
||||
|
||||
res := FetchResult{Icon: icon}
|
||||
res := Result{Icon: icon}
|
||||
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
|
||||
res.contentType = contentType
|
||||
}
|
||||
@@ -122,22 +124,22 @@ func sanitizeName(name string) string {
|
||||
return strings.ToLower(nameSanitizer.Replace(name))
|
||||
}
|
||||
|
||||
func fetchKnownIcon(ctx context.Context, url *IconURL) (FetchResult, error) {
|
||||
func fetchKnownIcon(ctx context.Context, url *icons.URL) (Result, error) {
|
||||
// if icon isn't in the list, no need to fetch
|
||||
if !url.HasIcon() {
|
||||
return FetchResult{StatusCode: http.StatusNotFound}, errors.New("no such icon")
|
||||
return Result{StatusCode: http.StatusNotFound}, errors.New("no such icon")
|
||||
}
|
||||
|
||||
return FetchIconAbsolute(ctx, url.URL())
|
||||
}
|
||||
|
||||
func fetchIcon(ctx context.Context, filename string) (FetchResult, error) {
|
||||
func fetchIcon(ctx context.Context, filename string) (Result, error) {
|
||||
for _, fileType := range []string{"svg", "webp", "png"} {
|
||||
result, err := fetchKnownIcon(ctx, NewSelfhStIconURL(filename, fileType))
|
||||
result, err := fetchKnownIcon(ctx, icons.NewURL(icons.SourceSelfhSt, filename, fileType))
|
||||
if err == nil {
|
||||
return result, err
|
||||
}
|
||||
result, err = fetchKnownIcon(ctx, NewWalkXCodeIconURL(filename, fileType))
|
||||
result, err = fetchKnownIcon(ctx, icons.NewURL(icons.SourceWalkXCode, filename, fileType))
|
||||
if err == nil {
|
||||
return result, err
|
||||
}
|
||||
@@ -150,10 +152,10 @@ type contextValue struct {
|
||||
uri string
|
||||
}
|
||||
|
||||
func FindIcon(ctx context.Context, r route, uri string, variant IconVariant) (FetchResult, error) {
|
||||
func FindIcon(ctx context.Context, r route, uri string, variant icons.Variant) (Result, error) {
|
||||
for _, ref := range r.References() {
|
||||
ref = sanitizeName(ref)
|
||||
if variant != IconVariantNone {
|
||||
if variant != icons.VariantNone {
|
||||
ref += "-" + string(variant)
|
||||
}
|
||||
result, err := fetchIcon(ctx, ref)
|
||||
@@ -162,18 +164,21 @@ func FindIcon(ctx context.Context, r route, uri string, variant IconVariant) (Fe
|
||||
}
|
||||
}
|
||||
if r, ok := r.(httpRoute); ok {
|
||||
if mon := r.HealthMonitor(); mon != nil && !mon.Status().Good() {
|
||||
return FetchResultWithErrorf(http.StatusServiceUnavailable, "service unavailable")
|
||||
}
|
||||
// fallback to parse html
|
||||
return findIconSlowCached(context.WithValue(ctx, "route", contextValue{r: r, uri: uri}), r.Key())
|
||||
}
|
||||
return FetchResultWithErrorf(http.StatusNotFound, "no icon found")
|
||||
}
|
||||
|
||||
var findIconSlowCached = cache.NewKeyFunc(func(ctx context.Context, key string) (FetchResult, error) {
|
||||
var findIconSlowCached = cache.NewKeyFunc(func(ctx context.Context, key string) (Result, error) {
|
||||
v := ctx.Value("route").(contextValue)
|
||||
return findIconSlow(ctx, v.r, v.uri, nil)
|
||||
}).WithMaxEntries(200).Build() // no retries, no ttl
|
||||
}).WithMaxEntries(200).WithRetriesConstantBackoff(math.MaxInt, 15*time.Second).Build() // infinite retries, 15 seconds interval
|
||||
|
||||
func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string) (FetchResult, error) {
|
||||
func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string) (Result, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return FetchResultWithErrorf(http.StatusBadGateway, "request timeout")
|
||||
@@ -1,9 +1,10 @@
|
||||
package homepage
|
||||
package iconfetch
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/goutils/pool"
|
||||
)
|
||||
|
||||
@@ -12,6 +13,7 @@ type route interface {
|
||||
ProviderName() string
|
||||
References() []string
|
||||
TargetURL() *nettypes.URL
|
||||
HealthMonitor() types.HealthMonitor
|
||||
}
|
||||
|
||||
type httpRoute interface {
|
||||
17
internal/homepage/icons/key.go
Normal file
17
internal/homepage/icons/key.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package icons
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Key string
|
||||
|
||||
func NewKey(source Source, reference string) Key {
|
||||
return Key(fmt.Sprintf("%s/%s", source, reference))
|
||||
}
|
||||
|
||||
func (k Key) SourceRef() (Source, string) {
|
||||
source, ref, _ := strings.Cut(string(k), "/")
|
||||
return Source(source), ref
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package homepage
|
||||
package iconlist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/goutils/intern"
|
||||
@@ -21,60 +21,19 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
IconKey string
|
||||
IconMap map[IconKey]*IconMeta
|
||||
IconMap map[icons.Key]*icons.Meta
|
||||
IconList []string
|
||||
IconMeta struct {
|
||||
SVG bool `json:"SVG"`
|
||||
PNG bool `json:"PNG"`
|
||||
WebP bool `json:"WebP"`
|
||||
Light bool `json:"Light"`
|
||||
Dark bool `json:"Dark"`
|
||||
DisplayName string `json:"-"`
|
||||
Tag string `json:"-"`
|
||||
}
|
||||
IconMetaSearch struct {
|
||||
*IconMeta
|
||||
|
||||
Source IconSource `json:"Source"`
|
||||
Ref string `json:"Ref"`
|
||||
IconMetaSearch struct {
|
||||
*icons.Meta
|
||||
|
||||
Source icons.Source `json:"Source"`
|
||||
Ref string `json:"Ref"`
|
||||
|
||||
rank int
|
||||
}
|
||||
)
|
||||
|
||||
func (icon *IconMeta) Filenames(ref string) []string {
|
||||
filenames := make([]string, 0)
|
||||
if icon.SVG {
|
||||
filenames = append(filenames, ref+".svg")
|
||||
if icon.Light {
|
||||
filenames = append(filenames, ref+"-light.svg")
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, ref+"-dark.svg")
|
||||
}
|
||||
}
|
||||
if icon.PNG {
|
||||
filenames = append(filenames, ref+".png")
|
||||
if icon.Light {
|
||||
filenames = append(filenames, ref+"-light.png")
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, ref+"-dark.png")
|
||||
}
|
||||
}
|
||||
if icon.WebP {
|
||||
filenames = append(filenames, ref+".webp")
|
||||
if icon.Light {
|
||||
filenames = append(filenames, ref+"-light.webp")
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, ref+"-dark.webp")
|
||||
}
|
||||
}
|
||||
return filenames
|
||||
}
|
||||
|
||||
const updateInterval = 2 * time.Hour
|
||||
|
||||
var iconsCache synk.Value[IconMap]
|
||||
@@ -84,16 +43,17 @@ const (
|
||||
selfhstIcons = "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/index.json"
|
||||
)
|
||||
|
||||
func NewIconKey(source IconSource, reference string) IconKey {
|
||||
return IconKey(fmt.Sprintf("%s/%s", source, reference))
|
||||
type provider struct{}
|
||||
|
||||
func (p provider) HasIcon(u *icons.URL) bool {
|
||||
return HasIcon(u)
|
||||
}
|
||||
|
||||
func (k IconKey) SourceRef() (IconSource, string) {
|
||||
source, ref, _ := strings.Cut(string(k), "/")
|
||||
return IconSource(source), ref
|
||||
func init() {
|
||||
icons.SetProvider(provider{})
|
||||
}
|
||||
|
||||
func InitIconListCache() {
|
||||
func InitCache() {
|
||||
m := make(IconMap)
|
||||
err := serialization.LoadJSONIfExist(common.IconListCachePath, &m)
|
||||
if err != nil {
|
||||
@@ -196,10 +156,10 @@ func SearchIcons(keyword string, limit int) []*IconMetaSearch {
|
||||
|
||||
source, ref := k.SourceRef()
|
||||
ranked := &IconMetaSearch{
|
||||
Source: source,
|
||||
Ref: ref,
|
||||
IconMeta: icon,
|
||||
rank: rank,
|
||||
Source: source,
|
||||
Ref: ref,
|
||||
Meta: icon,
|
||||
rank: rank,
|
||||
}
|
||||
// Sorted insert based on rank (lower rank = better match)
|
||||
insertPos, _ := slices.BinarySearchFunc(results, ranked, sortByRank)
|
||||
@@ -213,7 +173,7 @@ func SearchIcons(keyword string, limit int) []*IconMetaSearch {
|
||||
return results[:min(len(results), limit)]
|
||||
}
|
||||
|
||||
func HasIcon(icon *IconURL) bool {
|
||||
func HasIcon(icon *icons.URL) bool {
|
||||
if icon.Extra == nil {
|
||||
return false
|
||||
}
|
||||
@@ -241,11 +201,11 @@ type HomepageMeta struct {
|
||||
Tag string
|
||||
}
|
||||
|
||||
func GetHomepageMeta(ref string) (HomepageMeta, bool) {
|
||||
meta, ok := ListAvailableIcons()[NewIconKey(IconSourceSelfhSt, ref)]
|
||||
func GetMetadata(ref string) (HomepageMeta, bool) {
|
||||
meta, ok := ListAvailableIcons()[icons.NewKey(icons.SourceSelfhSt, ref)]
|
||||
// these info is not available in walkxcode
|
||||
// if !ok {
|
||||
// meta, ok = iconsCache.Icons[NewIconKey(IconSourceWalkXCode, ref)]
|
||||
// meta, ok = iconsCache.Icons[icons.NewIconKey(icons.IconSourceWalkXCode, ref)]
|
||||
// }
|
||||
if !ok {
|
||||
return HomepageMeta{}, false
|
||||
@@ -317,14 +277,14 @@ func UpdateWalkxCodeIcons(m IconMap) error {
|
||||
}
|
||||
|
||||
for fileType, files := range data {
|
||||
var setExt func(icon *IconMeta)
|
||||
var setExt func(icon *icons.Meta)
|
||||
switch fileType {
|
||||
case "png":
|
||||
setExt = func(icon *IconMeta) { icon.PNG = true }
|
||||
setExt = func(icon *icons.Meta) { icon.PNG = true }
|
||||
case "svg":
|
||||
setExt = func(icon *IconMeta) { icon.SVG = true }
|
||||
setExt = func(icon *icons.Meta) { icon.SVG = true }
|
||||
case "webp":
|
||||
setExt = func(icon *IconMeta) { icon.WebP = true }
|
||||
setExt = func(icon *icons.Meta) { icon.WebP = true }
|
||||
}
|
||||
for _, f := range files {
|
||||
f = strings.TrimSuffix(f, "."+fileType)
|
||||
@@ -336,10 +296,10 @@ func UpdateWalkxCodeIcons(m IconMap) error {
|
||||
if isDark {
|
||||
f = strings.TrimSuffix(f, "-dark")
|
||||
}
|
||||
key := NewIconKey(IconSourceWalkXCode, f)
|
||||
key := icons.NewKey(icons.SourceWalkXCode, f)
|
||||
icon, ok := m[key]
|
||||
if !ok {
|
||||
icon = new(IconMeta)
|
||||
icon = new(icons.Meta)
|
||||
m[key] = icon
|
||||
}
|
||||
setExt(icon)
|
||||
@@ -401,7 +361,7 @@ func UpdateSelfhstIcons(m IconMap) error {
|
||||
tag, _, _ = strings.Cut(item.Tags, ",")
|
||||
tag = strings.TrimSpace(tag)
|
||||
}
|
||||
icon := &IconMeta{
|
||||
icon := &icons.Meta{
|
||||
DisplayName: item.Name,
|
||||
Tag: intern.Make(tag).Value(),
|
||||
SVG: item.SVG == "Yes",
|
||||
@@ -410,7 +370,7 @@ func UpdateSelfhstIcons(m IconMap) error {
|
||||
Light: item.Light == "Yes",
|
||||
Dark: item.Dark == "Yes",
|
||||
}
|
||||
key := NewIconKey(IconSourceSelfhSt, item.Reference)
|
||||
key := icons.NewKey(icons.SourceSelfhSt, item.Reference)
|
||||
m[key] = icon
|
||||
}
|
||||
return nil
|
||||
@@ -1,9 +1,10 @@
|
||||
package homepage_test
|
||||
package iconlist_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/godoxy/internal/homepage"
|
||||
. "github.com/yusing/godoxy/internal/homepage/icons"
|
||||
. "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
)
|
||||
|
||||
const walkxcodeIcons = `{
|
||||
@@ -69,8 +70,8 @@ const selfhstIcons = `[
|
||||
]`
|
||||
|
||||
type testCases struct {
|
||||
Key IconKey
|
||||
IconMeta
|
||||
Key Key
|
||||
Meta
|
||||
}
|
||||
|
||||
func runTests(t *testing.T, iconsCache IconMap, test []testCases) {
|
||||
@@ -109,8 +110,8 @@ func TestListWalkxCodeIcons(t *testing.T) {
|
||||
}
|
||||
test := []testCases{
|
||||
{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "app1"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceWalkXCode, "app1"),
|
||||
Meta: Meta{
|
||||
SVG: true,
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
@@ -118,15 +119,15 @@ func TestListWalkxCodeIcons(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "app2"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceWalkXCode, "app2"),
|
||||
Meta: Meta{
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "karakeep"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceWalkXCode, "karakeep"),
|
||||
Meta: Meta{
|
||||
SVG: true,
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
@@ -149,8 +150,8 @@ func TestListSelfhstIcons(t *testing.T) {
|
||||
}
|
||||
test := []testCases{
|
||||
{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "2fauth"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceSelfhSt, "2fauth"),
|
||||
Meta: Meta{
|
||||
SVG: true,
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
@@ -160,16 +161,16 @@ func TestListSelfhstIcons(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "dittofeed"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceSelfhSt, "dittofeed"),
|
||||
Meta: Meta{
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
DisplayName: "Dittofeed",
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "ars-technica"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceSelfhSt, "ars-technica"),
|
||||
Meta: Meta{
|
||||
SVG: true,
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
43
internal/homepage/icons/metadata.go
Normal file
43
internal/homepage/icons/metadata.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package icons
|
||||
|
||||
type Meta struct {
|
||||
SVG bool `json:"SVG"`
|
||||
PNG bool `json:"PNG"`
|
||||
WebP bool `json:"WebP"`
|
||||
Light bool `json:"Light"`
|
||||
Dark bool `json:"Dark"`
|
||||
DisplayName string `json:"-"`
|
||||
Tag string `json:"-"`
|
||||
}
|
||||
|
||||
func (icon *Meta) Filenames(ref string) []string {
|
||||
filenames := make([]string, 0)
|
||||
if icon.SVG {
|
||||
filenames = append(filenames, ref+".svg")
|
||||
if icon.Light {
|
||||
filenames = append(filenames, ref+"-light.svg")
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, ref+"-dark.svg")
|
||||
}
|
||||
}
|
||||
if icon.PNG {
|
||||
filenames = append(filenames, ref+".png")
|
||||
if icon.Light {
|
||||
filenames = append(filenames, ref+"-light.png")
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, ref+"-dark.png")
|
||||
}
|
||||
}
|
||||
if icon.WebP {
|
||||
filenames = append(filenames, ref+".webp")
|
||||
if icon.Light {
|
||||
filenames = append(filenames, ref+"-light.webp")
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, ref+"-dark.webp")
|
||||
}
|
||||
}
|
||||
return filenames
|
||||
}
|
||||
21
internal/homepage/icons/provider.go
Normal file
21
internal/homepage/icons/provider.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package icons
|
||||
|
||||
import "sync/atomic"
|
||||
|
||||
type Provider interface {
|
||||
HasIcon(u *URL) bool
|
||||
}
|
||||
|
||||
var provider atomic.Value
|
||||
|
||||
func SetProvider(p Provider) {
|
||||
provider.Store(p)
|
||||
}
|
||||
|
||||
func hasIcon(u *URL) bool {
|
||||
v := provider.Load()
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
return v.(Provider).HasIcon(u)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package homepage
|
||||
package icons
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -8,43 +8,43 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
IconURL struct {
|
||||
IconSource `json:"source"`
|
||||
URL struct {
|
||||
Source `json:"source"`
|
||||
|
||||
FullURL *string `json:"value,omitempty"` // only for absolute/relative icons
|
||||
Extra *IconExtra `json:"extra,omitempty"` // only for walkxcode/selfhst icons
|
||||
FullURL *string `json:"value,omitempty"` // only for absolute/relative icons
|
||||
Extra *Extra `json:"extra,omitempty"` // only for walkxcode/selfhst icons
|
||||
}
|
||||
|
||||
IconExtra struct {
|
||||
Key IconKey `json:"key"`
|
||||
Ref string `json:"ref"`
|
||||
FileType string `json:"file_type"`
|
||||
IsLight bool `json:"is_light"`
|
||||
IsDark bool `json:"is_dark"`
|
||||
Extra struct {
|
||||
Key Key `json:"key"`
|
||||
Ref string `json:"ref"`
|
||||
FileType string `json:"file_type"`
|
||||
IsLight bool `json:"is_light"`
|
||||
IsDark bool `json:"is_dark"`
|
||||
}
|
||||
|
||||
IconSource string
|
||||
IconVariant string
|
||||
Source string
|
||||
Variant string
|
||||
)
|
||||
|
||||
const (
|
||||
IconSourceAbsolute IconSource = "https://"
|
||||
IconSourceRelative IconSource = "@target"
|
||||
IconSourceWalkXCode IconSource = "@walkxcode"
|
||||
IconSourceSelfhSt IconSource = "@selfhst"
|
||||
SourceAbsolute Source = "https://"
|
||||
SourceRelative Source = "@target"
|
||||
SourceWalkXCode Source = "@walkxcode"
|
||||
SourceSelfhSt Source = "@selfhst"
|
||||
)
|
||||
|
||||
const (
|
||||
IconVariantNone IconVariant = ""
|
||||
IconVariantLight IconVariant = "light"
|
||||
IconVariantDark IconVariant = "dark"
|
||||
VariantNone Variant = ""
|
||||
VariantLight Variant = "light"
|
||||
VariantDark Variant = "dark"
|
||||
)
|
||||
|
||||
var ErrInvalidIconURL = gperr.New("invalid icon url")
|
||||
|
||||
func NewIconURL(source IconSource, refOrName, format string) *IconURL {
|
||||
func NewURL(source Source, refOrName, format string) *URL {
|
||||
switch source {
|
||||
case IconSourceWalkXCode, IconSourceSelfhSt:
|
||||
case SourceWalkXCode, SourceSelfhSt:
|
||||
default:
|
||||
panic("invalid icon source")
|
||||
}
|
||||
@@ -56,10 +56,10 @@ func NewIconURL(source IconSource, refOrName, format string) *IconURL {
|
||||
isDark = true
|
||||
refOrName = strings.TrimSuffix(refOrName, "-dark")
|
||||
}
|
||||
return &IconURL{
|
||||
IconSource: source,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(source, refOrName),
|
||||
return &URL{
|
||||
Source: source,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(source, refOrName),
|
||||
FileType: format,
|
||||
Ref: refOrName,
|
||||
IsLight: isLight,
|
||||
@@ -68,53 +68,42 @@ func NewIconURL(source IconSource, refOrName, format string) *IconURL {
|
||||
}
|
||||
}
|
||||
|
||||
func NewSelfhStIconURL(refOrName, format string) *IconURL {
|
||||
return NewIconURL(IconSourceSelfhSt, refOrName, format)
|
||||
func (u *URL) HasIcon() bool {
|
||||
return hasIcon(u)
|
||||
}
|
||||
|
||||
func NewWalkXCodeIconURL(name, format string) *IconURL {
|
||||
return NewIconURL(IconSourceWalkXCode, name, format)
|
||||
}
|
||||
|
||||
// HasIcon checks if the icon referenced by the IconURL exists in the cache based on its source.
|
||||
// Returns false if the icon does not exist for IconSourceSelfhSt or IconSourceWalkXCode,
|
||||
// otherwise returns true.
|
||||
func (u *IconURL) HasIcon() bool {
|
||||
return HasIcon(u)
|
||||
}
|
||||
|
||||
func (u *IconURL) WithVariant(variant IconVariant) *IconURL {
|
||||
switch u.IconSource {
|
||||
case IconSourceWalkXCode, IconSourceSelfhSt:
|
||||
func (u *URL) WithVariant(variant Variant) *URL {
|
||||
switch u.Source {
|
||||
case SourceWalkXCode, SourceSelfhSt:
|
||||
default:
|
||||
return u // no variant for absolute/relative icons
|
||||
}
|
||||
|
||||
var extra *IconExtra
|
||||
var extra *Extra
|
||||
if u.Extra != nil {
|
||||
extra = &IconExtra{
|
||||
extra = &Extra{
|
||||
Key: u.Extra.Key,
|
||||
Ref: u.Extra.Ref,
|
||||
FileType: u.Extra.FileType,
|
||||
IsLight: variant == IconVariantLight,
|
||||
IsDark: variant == IconVariantDark,
|
||||
IsLight: variant == VariantLight,
|
||||
IsDark: variant == VariantDark,
|
||||
}
|
||||
extra.Ref = strings.TrimSuffix(extra.Ref, "-light")
|
||||
extra.Ref = strings.TrimSuffix(extra.Ref, "-dark")
|
||||
}
|
||||
return &IconURL{
|
||||
IconSource: u.IconSource,
|
||||
FullURL: u.FullURL,
|
||||
Extra: extra,
|
||||
return &URL{
|
||||
Source: u.Source,
|
||||
FullURL: u.FullURL,
|
||||
Extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse implements strutils.Parser.
|
||||
func (u *IconURL) Parse(v string) error {
|
||||
func (u *URL) Parse(v string) error {
|
||||
return u.parse(v, true)
|
||||
}
|
||||
|
||||
func (u *IconURL) parse(v string, checkExists bool) error {
|
||||
func (u *URL) parse(v string, checkExists bool) error {
|
||||
if v == "" {
|
||||
return ErrInvalidIconURL
|
||||
}
|
||||
@@ -126,19 +115,19 @@ func (u *IconURL) parse(v string, checkExists bool) error {
|
||||
switch beforeSlash {
|
||||
case "http:", "https:":
|
||||
u.FullURL = &v
|
||||
u.IconSource = IconSourceAbsolute
|
||||
u.Source = SourceAbsolute
|
||||
case "@target", "": // @target/favicon.ico, /favicon.ico
|
||||
url := v[slashIndex:]
|
||||
if url == "/" {
|
||||
return ErrInvalidIconURL.Withf("%s", "empty path")
|
||||
}
|
||||
u.FullURL = &url
|
||||
u.IconSource = IconSourceRelative
|
||||
u.Source = SourceRelative
|
||||
case "@selfhst", "@walkxcode": // selfh.st / walkxcode Icons, @selfhst/<reference>.<format>
|
||||
if beforeSlash == "@selfhst" {
|
||||
u.IconSource = IconSourceSelfhSt
|
||||
u.Source = SourceSelfhSt
|
||||
} else {
|
||||
u.IconSource = IconSourceWalkXCode
|
||||
u.Source = SourceWalkXCode
|
||||
}
|
||||
parts := strings.Split(v[slashIndex+1:], ".")
|
||||
if len(parts) != 2 {
|
||||
@@ -161,15 +150,15 @@ func (u *IconURL) parse(v string, checkExists bool) error {
|
||||
isDark = true
|
||||
reference = strings.TrimSuffix(reference, "-dark")
|
||||
}
|
||||
u.Extra = &IconExtra{
|
||||
Key: NewIconKey(u.IconSource, reference),
|
||||
u.Extra = &Extra{
|
||||
Key: NewKey(u.Source, reference),
|
||||
FileType: format,
|
||||
Ref: reference,
|
||||
IsLight: isLight,
|
||||
IsDark: isDark,
|
||||
}
|
||||
if checkExists && !u.HasIcon() {
|
||||
return ErrInvalidIconURL.Withf("no such icon %s.%s from %s", reference, format, u.IconSource)
|
||||
return ErrInvalidIconURL.Withf("no such icon %s.%s from %s", reference, format, u.Source)
|
||||
}
|
||||
default:
|
||||
return ErrInvalidIconURL.Subject(v)
|
||||
@@ -178,7 +167,7 @@ func (u *IconURL) parse(v string, checkExists bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *IconURL) URL() string {
|
||||
func (u *URL) URL() string {
|
||||
if u.FullURL != nil {
|
||||
return *u.FullURL
|
||||
}
|
||||
@@ -191,16 +180,16 @@ func (u *IconURL) URL() string {
|
||||
} else if u.Extra.IsDark {
|
||||
filename += "-dark"
|
||||
}
|
||||
switch u.IconSource {
|
||||
case IconSourceWalkXCode:
|
||||
switch u.Source {
|
||||
case SourceWalkXCode:
|
||||
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/%s/%s.%s", u.Extra.FileType, filename, u.Extra.FileType)
|
||||
case IconSourceSelfhSt:
|
||||
case SourceSelfhSt:
|
||||
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/selfhst/icons/%s/%s.%s", u.Extra.FileType, filename, u.Extra.FileType)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (u *IconURL) String() string {
|
||||
func (u *URL) String() string {
|
||||
if u.FullURL != nil {
|
||||
return *u.FullURL
|
||||
}
|
||||
@@ -213,14 +202,14 @@ func (u *IconURL) String() string {
|
||||
} else if u.Extra.IsDark {
|
||||
suffix = "-dark"
|
||||
}
|
||||
return fmt.Sprintf("%s/%s%s.%s", u.IconSource, u.Extra.Ref, suffix, u.Extra.FileType)
|
||||
return fmt.Sprintf("%s/%s%s.%s", u.Source, u.Extra.Ref, suffix, u.Extra.FileType)
|
||||
}
|
||||
|
||||
func (u *IconURL) MarshalText() ([]byte, error) {
|
||||
func (u *URL) MarshalText() ([]byte, error) {
|
||||
return []byte(u.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
func (u *IconURL) UnmarshalText(data []byte) error {
|
||||
func (u *URL) UnmarshalText(data []byte) error {
|
||||
return u.parse(string(data), false)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package homepage_test
|
||||
package icons_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/godoxy/internal/homepage"
|
||||
. "github.com/yusing/godoxy/internal/homepage/icons"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
@@ -15,31 +15,31 @@ func TestIconURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantValue *IconURL
|
||||
wantValue *URL
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "absolute",
|
||||
input: "http://example.com/icon.png",
|
||||
wantValue: &IconURL{
|
||||
FullURL: strPtr("http://example.com/icon.png"),
|
||||
IconSource: IconSourceAbsolute,
|
||||
wantValue: &URL{
|
||||
FullURL: strPtr("http://example.com/icon.png"),
|
||||
Source: SourceAbsolute,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "relative",
|
||||
input: "@target/icon.png",
|
||||
wantValue: &IconURL{
|
||||
FullURL: strPtr("/icon.png"),
|
||||
IconSource: IconSourceRelative,
|
||||
wantValue: &URL{
|
||||
FullURL: strPtr("/icon.png"),
|
||||
Source: SourceRelative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "relative2",
|
||||
input: "/icon.png",
|
||||
wantValue: &IconURL{
|
||||
FullURL: strPtr("/icon.png"),
|
||||
IconSource: IconSourceRelative,
|
||||
wantValue: &URL{
|
||||
FullURL: strPtr("/icon.png"),
|
||||
Source: SourceRelative,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -55,10 +55,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "walkxcode",
|
||||
input: "@walkxcode/adguard-home.png",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceWalkXCode,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "adguard-home"),
|
||||
wantValue: &URL{
|
||||
Source: SourceWalkXCode,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceWalkXCode, "adguard-home"),
|
||||
FileType: "png",
|
||||
Ref: "adguard-home",
|
||||
},
|
||||
@@ -67,10 +67,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "walkxcode_light",
|
||||
input: "@walkxcode/pfsense-light.png",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceWalkXCode,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "pfsense"),
|
||||
wantValue: &URL{
|
||||
Source: SourceWalkXCode,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceWalkXCode, "pfsense"),
|
||||
FileType: "png",
|
||||
Ref: "pfsense",
|
||||
IsLight: true,
|
||||
@@ -85,10 +85,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "selfh.st_valid",
|
||||
input: "@selfhst/adguard-home.webp",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceSelfhSt,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"),
|
||||
wantValue: &URL{
|
||||
Source: SourceSelfhSt,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceSelfhSt, "adguard-home"),
|
||||
FileType: "webp",
|
||||
Ref: "adguard-home",
|
||||
},
|
||||
@@ -97,10 +97,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "selfh.st_light",
|
||||
input: "@selfhst/adguard-home-light.png",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceSelfhSt,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"),
|
||||
wantValue: &URL{
|
||||
Source: SourceSelfhSt,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceSelfhSt, "adguard-home"),
|
||||
FileType: "png",
|
||||
Ref: "adguard-home",
|
||||
IsLight: true,
|
||||
@@ -110,10 +110,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "selfh.st_dark",
|
||||
input: "@selfhst/adguard-home-dark.svg",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceSelfhSt,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"),
|
||||
wantValue: &URL{
|
||||
Source: SourceSelfhSt,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceSelfhSt, "adguard-home"),
|
||||
FileType: "svg",
|
||||
Ref: "adguard-home",
|
||||
IsDark: true,
|
||||
@@ -143,7 +143,7 @@ func TestIconURL(t *testing.T) {
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
u := &IconURL{}
|
||||
u := &URL{}
|
||||
err := u.Parse(tc.input)
|
||||
if tc.wantErr {
|
||||
expect.ErrorIs(t, ErrInvalidIconURL, err)
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
|
||||
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
@@ -99,18 +100,18 @@ func (w *Watcher) handleWakeEventsSSE(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) getFavIcon(ctx context.Context) (result homepage.FetchResult, err error) {
|
||||
func (w *Watcher) getFavIcon(ctx context.Context) (result iconfetch.Result, err error) {
|
||||
r := w.route
|
||||
hp := r.HomepageItem()
|
||||
if hp.Icon != nil {
|
||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, homepage.IconVariantNone)
|
||||
if hp.Icon.Source == icons.SourceRelative {
|
||||
result, err = iconfetch.FindIcon(ctx, r, *hp.Icon.FullURL, icons.VariantNone)
|
||||
} else {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
}
|
||||
} else {
|
||||
// try extract from "link[rel=icon]"
|
||||
result, err = homepage.FindIcon(ctx, r, "/", homepage.IconVariantNone)
|
||||
result, err = iconfetch.FindIcon(ctx, r, "/", icons.VariantNone)
|
||||
}
|
||||
if result.StatusCode == 0 {
|
||||
result.StatusCode = http.StatusOK
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/health/monitor"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
homepagecfg "github.com/yusing/godoxy/internal/homepage/types"
|
||||
netutils "github.com/yusing/godoxy/internal/net"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
@@ -54,7 +55,7 @@ type (
|
||||
|
||||
route.HTTPConfig
|
||||
PathPatterns []string `json:"path_patterns,omitempty" extensions:"x-nullable"`
|
||||
Rules rules.Rules `json:"rules,omitempty" extension:"x-nullable"`
|
||||
Rules rules.Rules `json:"rules,omitempty" extensions:"x-nullable"`
|
||||
RuleFile string `json:"rule_file,omitempty" extensions:"x-nullable"`
|
||||
HealthCheck types.HealthCheckConfig `json:"healthcheck,omitempty" extensions:"x-nullable"` // null on load-balancer routes
|
||||
LoadBalance *types.LoadBalancerConfig `json:"load_balance,omitempty" extensions:"x-nullable"`
|
||||
@@ -478,13 +479,18 @@ func (r *Route) TargetURL() *nettypes.URL {
|
||||
}
|
||||
|
||||
func (r *Route) References() []string {
|
||||
aliasRef, _, ok := strings.Cut(r.Alias, ".")
|
||||
if !ok {
|
||||
aliasRef = r.Alias
|
||||
}
|
||||
|
||||
if r.Container != nil {
|
||||
if r.Container.ContainerName != r.Alias {
|
||||
return []string{r.Container.ContainerName, r.Alias, r.Container.Image.Name, r.Container.Image.Author}
|
||||
return []string{r.Container.ContainerName, aliasRef, r.Container.Image.Name, r.Container.Image.Author}
|
||||
}
|
||||
return []string{r.Container.Image.Name, r.Alias, r.Container.Image.Author}
|
||||
return []string{r.Container.Image.Name, aliasRef, r.Container.Image.Author}
|
||||
}
|
||||
return []string{r.Alias}
|
||||
return []string{aliasRef}
|
||||
}
|
||||
|
||||
// Name implements pool.Object.
|
||||
@@ -849,7 +855,7 @@ func (r *Route) FinalizeHomepageConfig() {
|
||||
hp := r.Homepage
|
||||
refs := r.References()
|
||||
for _, ref := range refs {
|
||||
meta, ok := homepage.GetHomepageMeta(ref)
|
||||
meta, ok := iconlist.GetMetadata(ref)
|
||||
if ok {
|
||||
if hp.Name == "" {
|
||||
hp.Name = meta.DisplayName
|
||||
|
||||
@@ -15,6 +15,7 @@ var (
|
||||
ErrInvalidArguments = gperr.New("invalid arguments")
|
||||
ErrInvalidOnTarget = gperr.New("invalid `rule.on` target")
|
||||
ErrInvalidCommandSequence = gperr.New("invalid command sequence")
|
||||
ErrMultipleDefaultRules = gperr.New("multiple default rules")
|
||||
|
||||
// vars errors
|
||||
ErrNoArgProvided = gperr.New("no argument provided")
|
||||
|
||||
@@ -26,6 +26,7 @@ func (on *RuleOn) Check(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
|
||||
const (
|
||||
OnDefault = "default"
|
||||
OnHeader = "header"
|
||||
OnQuery = "query"
|
||||
OnCookie = "cookie"
|
||||
@@ -50,6 +51,22 @@ var checkers = map[string]struct {
|
||||
builder func(args any) CheckFunc
|
||||
isResponseChecker bool
|
||||
}{
|
||||
OnDefault: {
|
||||
help: Help{
|
||||
command: OnDefault,
|
||||
description: makeLines(
|
||||
"The default rule is matched when no other rules are matched.",
|
||||
),
|
||||
args: map[string]string{},
|
||||
},
|
||||
validate: func(args []string) (any, gperr.Error) {
|
||||
if len(args) != 0 {
|
||||
return nil, ErrExpectNoArg
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
builder: func(args any) CheckFunc { return func(w http.ResponseWriter, r *http.Request) bool { return false } }, // this should never be called
|
||||
},
|
||||
OnHeader: {
|
||||
help: Help{
|
||||
command: OnHeader,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"github.com/rs/zerolog/log"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
@@ -57,6 +58,19 @@ func (rule *Rule) IsResponseRule() bool {
|
||||
return rule.On.IsResponseChecker() || rule.Do.IsResponseHandler()
|
||||
}
|
||||
|
||||
func (rules Rules) Validate() gperr.Error {
|
||||
var defaultRulesFound []int
|
||||
for i, rule := range rules {
|
||||
if rule.Name == "default" || rule.On.raw == OnDefault {
|
||||
defaultRulesFound = append(defaultRulesFound, i)
|
||||
}
|
||||
}
|
||||
if len(defaultRulesFound) > 1 {
|
||||
return ErrMultipleDefaultRules.Withf("found %d", len(defaultRulesFound))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildHandler returns a http.HandlerFunc that implements the rules.
|
||||
func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
|
||||
if len(rules) == 0 {
|
||||
@@ -74,7 +88,7 @@ func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
|
||||
var nonDefaultRules Rules
|
||||
hasDefaultRule := false
|
||||
for i, rule := range rules {
|
||||
if rule.Name == "default" {
|
||||
if rule.Name == "default" || rule.On.raw == OnDefault {
|
||||
defaultRule = rule
|
||||
hasDefaultRule = true
|
||||
} else {
|
||||
|
||||
52
internal/route/rules/rules_test.go
Normal file
52
internal/route/rules/rules_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
)
|
||||
|
||||
func TestRulesValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rules string
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "no default rule",
|
||||
rules: `
|
||||
- name: rule1
|
||||
on: header Host example.com
|
||||
do: pass
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "multiple default rules",
|
||||
rules: `
|
||||
- name: default
|
||||
do: pass
|
||||
- name: rule1
|
||||
on: default
|
||||
do: pass
|
||||
`,
|
||||
want: ErrMultipleDefaultRules,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var rules Rules
|
||||
convertible, err := serialization.ConvertString(strings.TrimSpace(tt.rules), reflect.ValueOf(&rules))
|
||||
require.True(t, convertible)
|
||||
|
||||
if tt.want == nil {
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
assert.ErrorIs(t, err, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -449,51 +449,66 @@ func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.
|
||||
}
|
||||
return mapUnmarshalValidate(obj, dst.Addr(), checkValidateTag)
|
||||
case srcKind == reflect.Slice: // slice to slice
|
||||
srcLen := src.Len()
|
||||
if srcLen == 0 {
|
||||
dst.SetZero()
|
||||
return nil
|
||||
}
|
||||
if dstT.Kind() != reflect.Slice {
|
||||
return ErrUnsupportedConversion.Subject(dstT.String() + " to " + srcT.String())
|
||||
}
|
||||
var sliceErrs gperr.Builder
|
||||
i := 0
|
||||
gi.ReflectInitSlice(dst, srcLen, srcLen)
|
||||
for j, v := range src.Seq2() {
|
||||
err := Convert(v, dst.Index(i), checkValidateTag)
|
||||
if err != nil {
|
||||
sliceErrs.Add(err.Subjectf("[%d]", j))
|
||||
continue
|
||||
}
|
||||
i++
|
||||
}
|
||||
if err := sliceErrs.Error(); err != nil {
|
||||
dst.SetLen(i) // shrink to number of elements that were successfully converted
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return ConvertSlice(src, dst, checkValidateTag)
|
||||
}
|
||||
|
||||
return ErrUnsupportedConversion.Subjectf("%s to %s", srcT.String(), dstT.String())
|
||||
return ErrUnsupportedConversion.Subjectf("%s to %s", srcT, dstT)
|
||||
}
|
||||
|
||||
var parserType = reflect.TypeFor[strutils.Parser]()
|
||||
func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.Error {
|
||||
if src.Kind() != reflect.Slice {
|
||||
return Convert(src, dst, checkValidateTag)
|
||||
}
|
||||
|
||||
srcLen := src.Len()
|
||||
if srcLen == 0 {
|
||||
dst.SetZero()
|
||||
return nil
|
||||
}
|
||||
if dst.Kind() != reflect.Slice {
|
||||
return ErrUnsupportedConversion.Subjectf("%s to %s", dst.Type(), src.Type())
|
||||
}
|
||||
|
||||
var sliceErrs gperr.Builder
|
||||
numValid := 0
|
||||
gi.ReflectInitSlice(dst, srcLen, srcLen)
|
||||
for j := range srcLen {
|
||||
err := Convert(src.Index(j), dst.Index(numValid), checkValidateTag)
|
||||
if err != nil {
|
||||
sliceErrs.Add(err.Subjectf("[%d]", j))
|
||||
continue
|
||||
}
|
||||
numValid++
|
||||
}
|
||||
|
||||
if dst.Type().Implements(reflect.TypeFor[CustomValidator]()) {
|
||||
err := dst.Interface().(CustomValidator).Validate()
|
||||
if err != nil {
|
||||
sliceErrs.Add(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := sliceErrs.Error(); err != nil {
|
||||
dst.SetLen(numValid) // shrink to number of elements that were successfully converted
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gperr.Error) {
|
||||
convertible = true
|
||||
dstT := dst.Type()
|
||||
if dst.Kind() == reflect.Pointer {
|
||||
if dst.IsNil() {
|
||||
// Early return for empty string
|
||||
if src == "" {
|
||||
return true, nil
|
||||
}
|
||||
initPtr(dst)
|
||||
}
|
||||
dst = dst.Elem()
|
||||
dstT = dst.Type()
|
||||
}
|
||||
if dst.Kind() == reflect.String {
|
||||
dst.SetString(src)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Early return for empty string
|
||||
if src == "" {
|
||||
@@ -501,6 +516,17 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if dst.Kind() == reflect.String {
|
||||
dst.SetString(src)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// check if (*T).Convertor is implemented
|
||||
if addr := dst.Addr(); addr.Type().Implements(reflect.TypeFor[strutils.Parser]()) {
|
||||
parser := addr.Interface().(strutils.Parser)
|
||||
return true, gperr.Wrap(parser.Parse(src))
|
||||
}
|
||||
|
||||
switch dstT {
|
||||
case reflect.TypeFor[time.Duration]():
|
||||
d, err := time.ParseDuration(src)
|
||||
@@ -512,12 +538,6 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
|
||||
default:
|
||||
}
|
||||
|
||||
// check if (*T).Convertor is implemented
|
||||
if dst.Addr().Type().Implements(parserType) {
|
||||
parser := dst.Addr().Interface().(strutils.Parser)
|
||||
return true, gperr.Wrap(parser.Parse(src))
|
||||
}
|
||||
|
||||
if gi.ReflectIsNumeric(dst) || dst.Kind() == reflect.Bool {
|
||||
err := gi.ReflectStrToNumBool(dst, src)
|
||||
if err != nil {
|
||||
@@ -527,29 +547,25 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
|
||||
}
|
||||
|
||||
// yaml like
|
||||
var tmp any
|
||||
switch dst.Kind() {
|
||||
case reflect.Slice:
|
||||
// Avoid unnecessary TrimSpace if we can detect the format early
|
||||
srcLen := len(src)
|
||||
if srcLen == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// one liner is comma separated list
|
||||
isMultiline := strings.ContainsRune(src, '\n')
|
||||
if !isMultiline && src[0] != '-' {
|
||||
isMultiline := strings.IndexByte(src, '\n') != -1
|
||||
if !isMultiline && src[0] != '-' && src[0] != '[' {
|
||||
values := strutils.CommaSeperatedList(src)
|
||||
gi.ReflectInitSlice(dst, len(values), len(values))
|
||||
size := len(values)
|
||||
gi.ReflectInitSlice(dst, size, size)
|
||||
var errs gperr.Builder
|
||||
for i, v := range values {
|
||||
_, err := ConvertString(v, dst.Index(i))
|
||||
if err != nil {
|
||||
errs.Add(err.Subjectf("[%d]", i))
|
||||
errs.AddSubjectf(err, "[%d]", i)
|
||||
}
|
||||
}
|
||||
err := errs.Error()
|
||||
return true, err
|
||||
if errs.HasError() {
|
||||
return true, errs.Error()
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
sl := []any{}
|
||||
@@ -557,18 +573,17 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
|
||||
if err != nil {
|
||||
return true, gperr.Wrap(err)
|
||||
}
|
||||
tmp = sl
|
||||
return true, ConvertSlice(reflect.ValueOf(sl), dst, true)
|
||||
case reflect.Map, reflect.Struct:
|
||||
rawMap := SerializedObject{}
|
||||
err := yaml.Unmarshal(unsafe.Slice(unsafe.StringData(src), len(src)), &rawMap)
|
||||
if err != nil {
|
||||
return true, gperr.Wrap(err)
|
||||
}
|
||||
tmp = rawMap
|
||||
return true, mapUnmarshalValidate(rawMap, dst, true)
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
return true, Convert(reflect.ValueOf(tmp), dst, true)
|
||||
}
|
||||
|
||||
var envRegex = regexp.MustCompile(`\$\{([^}]+)\}`) // e.g. ${CLOUDFLARE_API_KEY}
|
||||
|
||||
@@ -32,9 +32,9 @@ func BenchmarkDeserialize(b *testing.B) {
|
||||
"c": "1,2,3",
|
||||
"d": "a: a\nb: b\nc: c",
|
||||
"e": "- a: a\n b: b\n c: c",
|
||||
"f": map[string]any{"a": "a", "b": "456", "c": []string{"1", "2", "3"}},
|
||||
"f": map[string]any{"a": "a", "b": "456", "c": `1,2,3`},
|
||||
"g": map[string]any{"g1": "1.23", "g2": 123},
|
||||
"h": []map[string]any{{"a": 123, "b": "456", "c": []string{"1", "2", "3"}}},
|
||||
"h": []map[string]any{{"a": 123, "b": "456", "c": `["1","2","3"]`}},
|
||||
"j": "1.23",
|
||||
"k": 123,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Glob } from "bun";
|
||||
import { linkSync } from "fs";
|
||||
import { mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
@@ -8,6 +7,8 @@ type ImplDoc = {
|
||||
pkgPath: string;
|
||||
/** File name in wiki `src/impl/`, e.g. "internal-health-check.md" */
|
||||
docFileName: string;
|
||||
/** VitePress route path (extensionless), e.g. "/impl/internal-health-check" */
|
||||
docRoute: string;
|
||||
/** Absolute source README path */
|
||||
srcPathAbs: string;
|
||||
/** Absolute destination doc path */
|
||||
@@ -17,6 +18,8 @@ type ImplDoc = {
|
||||
const START_MARKER = "// GENERATED-IMPL-SIDEBAR-START";
|
||||
const END_MARKER = "// GENERATED-IMPL-SIDEBAR-END";
|
||||
|
||||
const skipSubmodules = ["internal/go-oidc/", "internal/gopsutil/"];
|
||||
|
||||
function escapeRegex(s: string) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -25,6 +28,16 @@ function escapeSingleQuotedTs(s: string) {
|
||||
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
||||
}
|
||||
|
||||
function normalizeRepoUrl(raw: string) {
|
||||
let url = (raw ?? "").trim();
|
||||
if (!url) return "";
|
||||
// Common typo: "https://https://github.com/..."
|
||||
url = url.replace(/^https?:\/\/https?:\/\//i, "https://");
|
||||
if (!/^https?:\/\//i.test(url)) url = `https://${url}`;
|
||||
url = url.replace(/\/+$/, "");
|
||||
return url;
|
||||
}
|
||||
|
||||
function sanitizeFileStemFromPkgPath(pkgPath: string) {
|
||||
// Convert a package path into a stable filename.
|
||||
// Example: "internal/go-oidc/example" -> "internal-go-oidc-example"
|
||||
@@ -37,6 +50,133 @@ function sanitizeFileStemFromPkgPath(pkgPath: string) {
|
||||
return joined.replace(/-+/g, "-").replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
function splitUrlAndFragment(url: string): {
|
||||
urlNoFragment: string;
|
||||
fragment: string;
|
||||
} {
|
||||
const i = url.indexOf("#");
|
||||
if (i === -1) return { urlNoFragment: url, fragment: "" };
|
||||
return { urlNoFragment: url.slice(0, i), fragment: url.slice(i) };
|
||||
}
|
||||
|
||||
function isExternalOrAbsoluteUrl(url: string) {
|
||||
// - absolute site links: "/foo"
|
||||
// - pure fragments: "#bar"
|
||||
// - external schemes: "https:", "mailto:", "vscode:", etc.
|
||||
// IMPORTANT: don't treat "config.go:29" as a scheme.
|
||||
if (url.startsWith("/") || url.startsWith("#")) return true;
|
||||
if (url.includes("://")) return true;
|
||||
return /^(https?|mailto|tel|vscode|file|data|ssh|git):/i.test(url);
|
||||
}
|
||||
|
||||
function isRepoSourceFilePath(filePath: string) {
|
||||
// Conservative allow-list: avoid rewriting .md (non-README) which may be VitePress docs.
|
||||
return /\.(go|ts|tsx|js|jsx|py|sh|yml|yaml|json|toml|env|css|html|txt)$/i.test(
|
||||
filePath
|
||||
);
|
||||
}
|
||||
|
||||
function parseFileLineSuffix(urlNoFragment: string): {
|
||||
filePath: string;
|
||||
line?: string;
|
||||
} {
|
||||
// Match "file.ext:123" (line suffix), while leaving "file.ext" untouched.
|
||||
const m = urlNoFragment.match(/^(.*?):(\d+)$/);
|
||||
if (!m) return { filePath: urlNoFragment };
|
||||
return { filePath: m[1] ?? urlNoFragment, line: m[2] };
|
||||
}
|
||||
|
||||
function rewriteMarkdownLinksOutsideFences(
|
||||
md: string,
|
||||
rewriteInline: (url: string) => string
|
||||
) {
|
||||
const lines = md.split("\n");
|
||||
let inFence = false;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i] ?? "";
|
||||
const trimmed = line.trimStart();
|
||||
if (trimmed.startsWith("```")) {
|
||||
inFence = !inFence;
|
||||
continue;
|
||||
}
|
||||
if (inFence) continue;
|
||||
|
||||
// Inline markdown links/images: [text](url "title") / 
|
||||
lines[i] = line.replace(
|
||||
/\]\(([^)\s]+)(\s+"[^"]*")?\)/g,
|
||||
(_full, urlRaw: string, maybeTitle: string | undefined) => {
|
||||
const rewritten = rewriteInline(urlRaw);
|
||||
return `](${rewritten}${maybeTitle ?? ""})`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function rewriteImplMarkdown(params: {
|
||||
md: string;
|
||||
pkgPath: string;
|
||||
readmeRelToDocRoute: Map<string, string>;
|
||||
dirPathToDocRoute: Map<string, string>;
|
||||
repoUrl: string;
|
||||
}) {
|
||||
const { md, pkgPath, readmeRelToDocRoute, dirPathToDocRoute, repoUrl } =
|
||||
params;
|
||||
|
||||
return rewriteMarkdownLinksOutsideFences(md, (urlRaw) => {
|
||||
// Handle angle-bracketed destinations: (<./foo/README.md>)
|
||||
const angleWrapped =
|
||||
urlRaw.startsWith("<") && urlRaw.endsWith(">")
|
||||
? urlRaw.slice(1, -1)
|
||||
: urlRaw;
|
||||
|
||||
const { urlNoFragment, fragment } = splitUrlAndFragment(angleWrapped);
|
||||
if (!urlNoFragment) return urlRaw;
|
||||
if (isExternalOrAbsoluteUrl(urlNoFragment)) return urlRaw;
|
||||
|
||||
// 1) Directory links like "common" or "common/" that have a README
|
||||
const dirPathNormalized = urlNoFragment.replace(/\/+$/, "");
|
||||
if (dirPathToDocRoute.has(dirPathNormalized)) {
|
||||
const rewritten = `${dirPathToDocRoute.get(
|
||||
dirPathNormalized
|
||||
)!}${fragment}`;
|
||||
return angleWrapped === urlRaw ? rewritten : `<${rewritten}>`;
|
||||
}
|
||||
|
||||
// 2) Intra-repo README links -> VitePress impl routes
|
||||
if (/(^|\/)README\.md$/.test(urlNoFragment)) {
|
||||
const targetReadmeRel = path.posix.normalize(
|
||||
path.posix.join(pkgPath, urlNoFragment)
|
||||
);
|
||||
const route = readmeRelToDocRoute.get(targetReadmeRel);
|
||||
if (route) {
|
||||
const rewritten = `${route}${fragment}`;
|
||||
return angleWrapped === urlRaw ? rewritten : `<${rewritten}>`;
|
||||
}
|
||||
return urlRaw;
|
||||
}
|
||||
|
||||
// 3) Local source-file references like "config.go:29" -> GitHub blob link
|
||||
if (repoUrl) {
|
||||
const { filePath, line } = parseFileLineSuffix(urlNoFragment);
|
||||
if (isRepoSourceFilePath(filePath)) {
|
||||
const repoRel = path.posix.normalize(
|
||||
path.posix.join(pkgPath, filePath)
|
||||
);
|
||||
const githubUrl = `${repoUrl}/blob/main/${repoRel}${
|
||||
line ? `#L${line}` : ""
|
||||
}`;
|
||||
const rewritten = `${githubUrl}${fragment}`;
|
||||
return angleWrapped === urlRaw ? rewritten : `<${rewritten}>`;
|
||||
}
|
||||
}
|
||||
|
||||
return urlRaw;
|
||||
});
|
||||
}
|
||||
|
||||
async function listRepoReadmes(repoRootAbs: string): Promise<string[]> {
|
||||
const glob = new Glob("**/README.md");
|
||||
const readmes: string[] = [];
|
||||
@@ -51,8 +191,14 @@ async function listRepoReadmes(repoRootAbs: string): Promise<string[]> {
|
||||
if (rel.startsWith(".git/") || rel.includes("/.git/")) continue;
|
||||
if (rel.startsWith("node_modules/") || rel.includes("/node_modules/"))
|
||||
continue;
|
||||
if (rel.startsWith("internal/go-oidc/")) continue;
|
||||
if (rel.startsWith("internal/gopsutil/")) continue;
|
||||
let skip = false;
|
||||
for (const submodule of skipSubmodules) {
|
||||
if (rel.startsWith(submodule)) {
|
||||
skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (skip) continue;
|
||||
readmes.push(rel);
|
||||
}
|
||||
|
||||
@@ -61,11 +207,34 @@ async function listRepoReadmes(repoRootAbs: string): Promise<string[]> {
|
||||
return readmes;
|
||||
}
|
||||
|
||||
async function ensureHardLink(srcAbs: string, dstAbs: string) {
|
||||
async function writeImplDocCopy(params: {
|
||||
srcAbs: string;
|
||||
dstAbs: string;
|
||||
pkgPath: string;
|
||||
readmeRelToDocRoute: Map<string, string>;
|
||||
dirPathToDocRoute: Map<string, string>;
|
||||
repoUrl: string;
|
||||
}) {
|
||||
const {
|
||||
srcAbs,
|
||||
dstAbs,
|
||||
pkgPath,
|
||||
readmeRelToDocRoute,
|
||||
dirPathToDocRoute,
|
||||
repoUrl,
|
||||
} = params;
|
||||
await mkdir(path.dirname(dstAbs), { recursive: true });
|
||||
await rm(dstAbs, { force: true });
|
||||
// Prefer sync for better error surfaces in Bun on some platforms.
|
||||
linkSync(srcAbs, dstAbs);
|
||||
|
||||
const original = await readFile(srcAbs, "utf8");
|
||||
const rewritten = rewriteImplMarkdown({
|
||||
md: original,
|
||||
pkgPath,
|
||||
readmeRelToDocRoute,
|
||||
dirPathToDocRoute,
|
||||
repoUrl,
|
||||
});
|
||||
await writeFile(dstAbs, rewritten);
|
||||
}
|
||||
|
||||
async function syncImplDocs(
|
||||
@@ -78,6 +247,30 @@ async function syncImplDocs(
|
||||
const readmes = await listRepoReadmes(repoRootAbs);
|
||||
const docs: ImplDoc[] = [];
|
||||
const expectedFileNames = new Set<string>();
|
||||
expectedFileNames.add("introduction.md");
|
||||
|
||||
const repoUrl = normalizeRepoUrl(
|
||||
Bun.env.REPO_URL ?? "https://github.com/yusing/godoxy"
|
||||
);
|
||||
|
||||
// Precompute mapping from repo-relative README path -> VitePress route.
|
||||
// This lets us rewrite intra-repo README links when copying content.
|
||||
const readmeRelToDocRoute = new Map<string, string>();
|
||||
|
||||
// Also precompute mapping from directory path -> VitePress route.
|
||||
// This handles links like "[`common/`](common)" that point to directories with READMEs.
|
||||
const dirPathToDocRoute = new Map<string, string>();
|
||||
|
||||
for (const readmeRel of readmes) {
|
||||
const pkgPath = path.posix.dirname(readmeRel);
|
||||
if (!pkgPath || pkgPath === ".") continue;
|
||||
|
||||
const docStem = sanitizeFileStemFromPkgPath(pkgPath);
|
||||
if (!docStem) continue;
|
||||
const route = `/impl/${docStem}`;
|
||||
readmeRelToDocRoute.set(readmeRel, route);
|
||||
dirPathToDocRoute.set(pkgPath, route);
|
||||
}
|
||||
|
||||
for (const readmeRel of readmes) {
|
||||
const pkgPath = path.posix.dirname(readmeRel);
|
||||
@@ -86,13 +279,21 @@ async function syncImplDocs(
|
||||
const docStem = sanitizeFileStemFromPkgPath(pkgPath);
|
||||
if (!docStem) continue;
|
||||
const docFileName = `${docStem}.md`;
|
||||
const docRoute = `/impl/${docStem}`;
|
||||
|
||||
const srcPathAbs = path.join(repoRootAbs, readmeRel);
|
||||
const dstPathAbs = path.join(implDirAbs, docFileName);
|
||||
|
||||
await ensureHardLink(srcPathAbs, dstPathAbs);
|
||||
await writeImplDocCopy({
|
||||
srcAbs: srcPathAbs,
|
||||
dstAbs: dstPathAbs,
|
||||
pkgPath,
|
||||
readmeRelToDocRoute,
|
||||
dirPathToDocRoute,
|
||||
repoUrl,
|
||||
});
|
||||
|
||||
docs.push({ pkgPath, docFileName, srcPathAbs, dstPathAbs });
|
||||
docs.push({ pkgPath, docFileName, docRoute, srcPathAbs, dstPathAbs });
|
||||
expectedFileNames.add(docFileName);
|
||||
}
|
||||
|
||||
@@ -111,13 +312,13 @@ async function syncImplDocs(
|
||||
}
|
||||
|
||||
function renderSidebarItems(docs: ImplDoc[], indent: string) {
|
||||
// link: '/impl/<file>.md' because VitePress `srcDir = "src"`.
|
||||
// link: '/impl/<stem>' (extensionless) because VitePress `srcDir = "src"`.
|
||||
if (docs.length === 0) return "";
|
||||
return (
|
||||
docs
|
||||
.map((d) => {
|
||||
const text = escapeSingleQuotedTs(d.pkgPath);
|
||||
const link = escapeSingleQuotedTs(`/impl/${d.docFileName}`);
|
||||
const link = escapeSingleQuotedTs(d.docRoute);
|
||||
return `${indent}{ text: '${text}', link: '${link}' },`;
|
||||
})
|
||||
.join("\n") + "\n"
|
||||
|
||||
@@ -17,6 +17,6 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
)
|
||||
|
||||
@@ -26,8 +26,8 @@ golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
Reference in New Issue
Block a user