diff --git a/cmd/main.go b/cmd/main.go index 3c8e9694..11c1f20d 100755 --- a/cmd/main.go +++ b/cmd/main.go @@ -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, ) diff --git a/internal/api/v1/favicon.go b/internal/api/v1/favicon.go index 131525eb..44dfb4d8 100644 --- a/internal/api/v1/favicon.go +++ b/internal/api/v1/favicon.go @@ -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" @@ -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 diff --git a/internal/api/v1/icons.go b/internal/api/v1/icons.go index 9e052f2b..52e0c4a7 100644 --- a/internal/api/v1/icons.go +++ b/internal/api/v1/icons.go @@ -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" ) @@ -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) } diff --git a/internal/homepage/homepage.go b/internal/homepage/homepage.go index 739496c6..ad064c76 100644 --- a/internal/homepage/homepage.go +++ b/internal/homepage/homepage.go @@ -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 diff --git a/internal/homepage/homepage_test.go b/internal/homepage/homepage_test.go index 482c7370..0cad0fe9 100644 --- a/internal/homepage/homepage_test.go +++ b/internal/homepage/homepage_test.go @@ -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() diff --git a/internal/homepage/content.go b/internal/homepage/icons/fetch/content.go similarity index 96% rename from internal/homepage/content.go rename to internal/homepage/icons/fetch/content.go index fa0ecdce..e3ac4573 100644 --- a/internal/homepage/content.go +++ b/internal/homepage/icons/fetch/content.go @@ -1,4 +1,4 @@ -package homepage +package iconfetch import ( "bufio" diff --git a/internal/homepage/favicon.go b/internal/homepage/icons/fetch/fetch.go similarity index 84% rename from internal/homepage/favicon.go rename to internal/homepage/icons/fetch/fetch.go index 0960dcc1..ef5b34fa 100644 --- a/internal/homepage/favicon.go +++ b/internal/homepage/icons/fetch/fetch.go @@ -1,4 +1,4 @@ -package homepage +package iconfetch import ( "bytes" @@ -15,6 +15,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 +23,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 +51,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(". 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) } diff --git a/internal/homepage/icon_url_test.go b/internal/homepage/icons/url_test.go similarity index 64% rename from internal/homepage/icon_url_test.go rename to internal/homepage/icons/url_test.go index 3ba4b8a7..5bfbd3a5 100644 --- a/internal/homepage/icon_url_test.go +++ b/internal/homepage/icons/url_test.go @@ -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) diff --git a/internal/idlewatcher/handle_http.go b/internal/idlewatcher/handle_http.go index 032fd383..2778c0e8 100644 --- a/internal/idlewatcher/handle_http.go +++ b/internal/idlewatcher/handle_http.go @@ -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 diff --git a/internal/route/route.go b/internal/route/route.go index 77bc20b3..c1c39749 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -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" @@ -849,7 +850,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