simplify icon caching and homepage item override

This commit is contained in:
yusing 2025-01-21 06:16:00 +08:00
parent d429374924
commit 8b1a3a31ff
21 changed files with 395 additions and 331 deletions

View file

@ -12,6 +12,7 @@ import (
"github.com/yusing/go-proxy/internal" "github.com/yusing/go-proxy/internal"
v1 "github.com/yusing/go-proxy/internal/api/v1" v1 "github.com/yusing/go-proxy/internal/api/v1"
"github.com/yusing/go-proxy/internal/api/v1/auth" "github.com/yusing/go-proxy/internal/api/v1/auth"
"github.com/yusing/go-proxy/internal/api/v1/favicon"
"github.com/yusing/go-proxy/internal/api/v1/query" "github.com/yusing/go-proxy/internal/api/v1/query"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config" "github.com/yusing/go-proxy/internal/config"
@ -32,6 +33,9 @@ func init() {
out = io.MultiWriter(out, v1.MemLogger()) out = io.MultiWriter(out, v1.MemLogger())
} }
logging.InitLogger(out) logging.InitLogger(out)
internal.InitIconListCache()
homepage.InitOverridesConfig()
favicon.InitIconCache()
} }
func main() { func main() {
@ -97,8 +101,6 @@ func main() {
} }
middleware.LoadComposeFiles() middleware.LoadComposeFiles()
internal.InitIconListCache()
homepage.InitOverridesConfig()
var cfg *config.Config var cfg *config.Config
var err E.Error var err E.Error

3
go.mod
View file

@ -14,8 +14,9 @@ require (
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/gotify/server/v2 v2.6.1 github.com/gotify/server/v2 v2.6.1
github.com/lithammer/fuzzysearch v1.1.8
github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_golang v1.20.5
github.com/puzpuzpuz/xsync/v3 v3.4.0 github.com/puzpuzpuz/xsync/v3 v3.4.1
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
github.com/vincent-petithory/dataurl v1.0.0 github.com/vincent-petithory/dataurl v1.0.0
golang.org/x/crypto v0.32.0 golang.org/x/crypto v0.32.0

6
go.sum
View file

@ -92,6 +92,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/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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@ -130,8 +132,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= github.com/puzpuzpuz/xsync/v3 v3.4.1 h1:wWXLKXwzpsduC3kUSahiL45MWxkGb+AQG0dsri4iftA=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.4.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=

View file

@ -47,6 +47,10 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
}) })
mux.HandleFunc("GET,POST", "/v1/auth/callback", defaultAuth.LoginCallbackHandler) mux.HandleFunc("GET,POST", "/v1/auth/callback", defaultAuth.LoginCallbackHandler)
mux.HandleFunc("GET,POST", "/v1/auth/logout", auth.LogoutCallbackHandler(defaultAuth)) mux.HandleFunc("GET,POST", "/v1/auth/logout", auth.LogoutCallbackHandler(defaultAuth))
} else {
mux.HandleFunc("GET", "/v1/auth/check", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
} }
return mux return mux
} }

View file

@ -5,7 +5,6 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
@ -17,13 +16,15 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/vincent-petithory/dataurl" "github.com/vincent-petithory/dataurl"
"github.com/yusing/go-proxy/internal"
U "github.com/yusing/go-proxy/internal/api/v1/utils" U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
gphttp "github.com/yusing/go-proxy/internal/net/http" gphttp "github.com/yusing/go-proxy/internal/net/http"
"github.com/yusing/go-proxy/internal/route/routes" "github.com/yusing/go-proxy/internal/route/routes"
route "github.com/yusing/go-proxy/internal/route/types" route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
) )
type content struct { type content struct {
@ -80,17 +81,15 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
var status int var status int
var errMsg string var errMsg string
hp := r.RawEntry().Homepage hp := r.RawEntry().Homepage.GetOverride()
if hp != nil && hp.Icon != nil { if !hp.IsEmpty() && hp.Icon != nil {
switch hp.Icon.IconSource { switch hp.Icon.IconSource {
case homepage.IconSourceAbsolute: case homepage.IconSourceAbsolute:
icon, status, errMsg = fetchIconAbsolute(hp.Icon.Value) icon, status, errMsg = fetchIconAbsolute(hp.Icon.Value)
case homepage.IconSourceRelative: case homepage.IconSourceRelative:
icon, status, errMsg = findIcon(r, req, hp.Icon.Value) icon, status, errMsg = findIcon(r, req, hp.Icon.Value)
case homepage.IconSourceWalkXCode: case homepage.IconSourceWalkXCode, homepage.IconSourceSelfhSt:
icon, status, errMsg = fetchWalkxcodeIcon(hp.Icon.Extra.FileType, hp.Icon.Extra.Name) icon, status, errMsg = fetchKnownIcon(hp.Icon)
case homepage.IconSourceSelfhSt:
icon, status, errMsg = fetchSelfhStIcon(hp.Icon.Extra.FileType, hp.Icon.Extra.Name)
} }
} else { } else {
// try extract from "link[rel=icon]" // try extract from "link[rel=icon]"
@ -112,6 +111,24 @@ var (
iconCacheMu sync.RWMutex iconCacheMu sync.RWMutex
) )
func InitIconCache() {
err := utils.LoadJSONIfExist(common.IconCachePath, &iconCache)
if err != nil {
logging.Error().Err(err).Msg("failed to load icon cache")
} else {
logging.Info().Msgf("icon cache loaded (%d icons)", len(iconCache))
}
task.OnProgramExit("save_favicon_cache", func() {
iconCacheMu.Lock()
defer iconCacheMu.Unlock()
if err := utils.SaveJSON(common.IconCachePath, &iconCache, 0o644); err != nil {
logging.Error().Err(err).Msg("failed to save icon cache")
}
})
}
func ResetIconCache(route route.HTTPRoute) { func ResetIconCache(route route.HTTPRoute) {
iconCacheMu.Lock() iconCacheMu.Lock()
defer iconCacheMu.Unlock() defer iconCacheMu.Unlock()
@ -122,6 +139,11 @@ func loadIconCache(key string) (icon []byte, ok bool) {
iconCacheMu.RLock() iconCacheMu.RLock()
defer iconCacheMu.RUnlock() defer iconCacheMu.RUnlock()
icon, ok = iconCache[key] icon, ok = iconCache[key]
if ok {
logging.Debug().
Str("key", key).
Msg("icon found in cache")
}
return return
} }
@ -172,56 +194,30 @@ func sanitizeName(name string) string {
return strings.ToLower(nameSanitizer.Replace(name)) return strings.ToLower(nameSanitizer.Replace(name))
} }
func fetchWalkxcodeIcon(filetype, name string) ([]byte, int, string) { func fetchKnownIcon(url *homepage.IconURL) ([]byte, int, string) {
// if icon isn't in the list, no need to fetch // if icon isn't in the list, no need to fetch
if !internal.HasWalkxCodeIcon(name, filetype) { if !url.HasIcon() {
logging.Debug(). logging.Debug().
Str("filetype", filetype). Str("value", url.String()).
Str("name", name). Str("url", url.URL()).
Msg("icon not found") Msg("no such icon")
return nil, http.StatusNotFound, "icon not found" return nil, http.StatusNotFound, "no such icon"
} }
icon, ok := loadIconCache("walkxcode/" + filetype + "/" + name) return fetchIconAbsolute(url.URL())
if ok {
return icon, http.StatusOK, ""
}
// url := homepage.DashboardIconBaseURL + filetype + "/" + name + "." + filetype
url := fmt.Sprintf("https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/%s/%s.%s", filetype, name, filetype)
return fetchIconAbsolute(url)
}
func fetchSelfhStIcon(filetype, reference string) ([]byte, int, string) {
// if icon isn't in the list, no need to fetch
if !internal.HasSelfhstIcon(reference, filetype) {
logging.Debug().
Str("filetype", filetype).
Str("reference", reference).
Msg("icon not found")
return nil, http.StatusNotFound, "icon not found"
}
icon, ok := loadIconCache("selfh.st/" + filetype + "/" + reference)
if ok {
return icon, http.StatusOK, ""
}
url := fmt.Sprintf("https://cdn.jsdelivr.net/gh/selfhst/icons/%s/%s.%s", filetype, reference, filetype)
return fetchIconAbsolute(url)
} }
func fetchIcon(filetype, filename string) (icon []byte, status int, errMsg string) { func fetchIcon(filetype, filename string) (icon []byte, status int, errMsg string) {
icon, status, errMsg = fetchSelfhStIcon(filetype, filename) icon, status, errMsg = fetchKnownIcon(homepage.NewSelfhStIconURL(filename, filetype))
if icon != nil { if icon != nil {
return return
} }
icon, status, errMsg = fetchWalkxcodeIcon(filetype, filename) icon, status, errMsg = fetchKnownIcon(homepage.NewWalkXCodeIconURL(filename, filetype))
return return
} }
func findIcon(r route.HTTPRoute, req *http.Request, uri string) (icon []byte, status int, errMsg string) { func findIcon(r route.HTTPRoute, req *http.Request, uri string) (icon []byte, status int, errMsg string) {
key := r.TargetName() key := r.RawEntry().Provider + ":" + r.TargetName()
icon, ok := loadIconCache(key) icon, ok := loadIconCache(key)
if ok { if ok {
if icon == nil { if icon == nil {
@ -239,8 +235,9 @@ func findIcon(r route.HTTPRoute, req *http.Request, uri string) (icon []byte, st
// fallback to parse html // fallback to parse html
icon, status, errMsg = findIconSlow(r, req, uri) icon, status, errMsg = findIconSlow(r, req, uri)
} }
// set even if error (nil) if icon != nil {
storeIconCache(key, icon) storeIconCache(key, icon)
}
return return
} }

View file

@ -4,19 +4,14 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils"
) )
const ( const (
HomepageOverrideDisplayname = "display_name" HomepageOverrideItem = "item"
HomepageOverrideDisplayOrder = "display_order"
HomepageOverrideDisplayCategory = "display_category"
HomepageOverrideCategoryOrder = "category_order" HomepageOverrideCategoryOrder = "category_order"
HomepageOverrideCategoryName = "category_name" HomepageOverrideCategoryName = "category_name"
HomepageOverrideIcon = "icon"
HomepageOverrideShow = "show"
) )
func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) { func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
@ -29,30 +24,27 @@ func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
http.Error(w, "missing value", http.StatusBadRequest) http.Error(w, "missing value", http.StatusBadRequest)
return return
} }
overrides := homepage.GetJSONConfig() overrides := homepage.GetOverrideConfig()
switch what { switch what {
case HomepageOverrideDisplayname: case HomepageOverrideItem:
utils.RespondError(w, overrides.SetDisplayNameOverride(which, value)) var override homepage.ItemConfig
case HomepageOverrideDisplayCategory: if err := utils.DeserializeJSON([]byte(value), &override); err != nil {
utils.RespondError(w, overrides.SetDisplayCategoryOverride(which, value)) http.Error(w, err.Error(), http.StatusBadRequest)
return
}
overrides.OverrideItem(which, &override)
case HomepageOverrideCategoryName: case HomepageOverrideCategoryName:
utils.RespondError(w, overrides.SetCategoryNameOverride(which, value)) overrides.SetCategoryNameOverride(which, value)
case HomepageOverrideIcon: case HomepageOverrideCategoryOrder:
utils.RespondError(w, overrides.SetIconOverride(which, value))
case HomepageOverrideShow:
utils.RespondError(w, overrides.SetShowItemOverride(which, strutils.ParseBool(value)))
case HomepageOverrideDisplayOrder, HomepageOverrideCategoryOrder:
v, err := strconv.Atoi(value) v, err := strconv.Atoi(value)
if err != nil { if err != nil {
http.Error(w, "invalid integer", http.StatusBadRequest) http.Error(w, "invalid integer", http.StatusBadRequest)
return return
} }
if what == HomepageOverrideDisplayOrder { overrides.SetCategoryOrder(which, v)
utils.RespondError(w, overrides.SetDisplayOrder(which, v))
} else {
utils.RespondError(w, overrides.SetCategoryOrder(which, v))
}
default: default:
http.Error(w, "invalid what", http.StatusBadRequest) http.Error(w, "invalid what", http.StatusBadRequest)
return
} }
w.WriteHeader(http.StatusOK)
} }

View file

@ -2,6 +2,7 @@ package v1
import ( import (
"net/http" "net/http"
"strconv"
"strings" "strings"
"github.com/yusing/go-proxy/internal" "github.com/yusing/go-proxy/internal"
@ -61,7 +62,11 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
case ListHomepageCategories: case ListHomepageCategories:
U.RespondJSON(w, r, routequery.HomepageCategories()) U.RespondJSON(w, r, routequery.HomepageCategories())
case ListIcons: case ListIcons:
icons, err := internal.ListAvailableIcons() limit, err := strconv.Atoi(r.FormValue("limit"))
if err != nil {
limit = 0
}
icons, err := internal.SearchIcons(r.FormValue("keyword"), limit)
if err != nil { if err != nil {
U.RespondError(w, err) U.RespondError(w, err)
return return

View file

@ -60,7 +60,7 @@ func PeriodicWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
return return
case <-ticker.C: case <-ticker.C:
if err := do(conn); err != nil { if err := do(conn); err != nil {
HandleErr(w, r, err) LogError(r).Msg(err.Error())
return return
} }
} }

View file

@ -22,6 +22,7 @@ const (
ConfigPath = ConfigBasePath + "/" + ConfigFileName ConfigPath = ConfigBasePath + "/" + ConfigFileName
HomepageJSONConfigPath = ConfigBasePath + "/.homepage.json" HomepageJSONConfigPath = ConfigBasePath + "/.homepage.json"
IconListCachePath = ConfigBasePath + "/.icon_list_cache.json" IconListCachePath = ConfigBasePath + "/.icon_list_cache.json"
IconCachePath = ConfigBasePath + "/.icon_cache.json"
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares" MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"

View file

@ -5,39 +5,45 @@ type (
Config map[string]Category Config map[string]Category
Category []*Item Category []*Item
Item struct { ItemConfig struct {
Show bool `json:"show"` Show bool `json:"show"`
Name string `json:"name"` // display name Name string `json:"name"` // display name
Icon *IconURL `json:"icon"` Icon *IconURL `json:"icon"`
URL string `json:"url"` // alias + domain
Category string `json:"category"` Category string `json:"category"`
Description string `json:"description" aliases:"desc"` Description string `json:"description" aliases:"desc"`
SortOrder int `json:"sort_order"`
WidgetConfig map[string]any `json:"widget_config" aliases:"widget"` WidgetConfig map[string]any `json:"widget_config" aliases:"widget"`
URL string `json:"url"` // alias + domain
}
Item struct {
*ItemConfig
Alias string `json:"alias"` // proxy alias Alias string `json:"alias"` // proxy alias
SourceType string `json:"source_type"` SourceType string `json:"source_type"`
AltURL string `json:"alt_url"` // original proxy target AltURL string `json:"alt_url"` // original proxy target
Provider string `json:"provider"` Provider string `json:"provider"`
IsUnset bool
} }
) )
func (item *Item) IsEmpty() bool { func NewItem(alias string) *Item {
return item == nil || (item.Name == "" && return &Item{
item.Icon == nil && ItemConfig: &ItemConfig{
item.URL == "" && Show: true,
item.Category == "" && },
item.Description == "" && Alias: alias,
len(item.WidgetConfig) == 0) IsUnset: true,
}
} }
func (item *Item) GetOverriddenItem() *Item { func (item *Item) IsEmpty() bool {
overrides := GetJSONConfig() return item == nil || item.IsUnset || item.ItemConfig == nil
clone := *item }
clone.Name = overrides.GetDisplayName(item)
clone.Icon = overrides.GetDisplayIcon(item) func (item *Item) GetOverride() *Item {
clone.Category = overrides.GetCategory(item) return overrideConfigInstance.GetOverride(item)
clone.Show = overrides.GetShowItem(item)
return &clone
} }
func NewHomePageConfig() Config { func NewHomePageConfig() Config {

View file

@ -7,25 +7,25 @@ import (
) )
func TestOverrideItem(t *testing.T) { func TestOverrideItem(t *testing.T) {
a := &Item{ // a := &Item{
Show: false, // Show: false,
Alias: "foo", // Alias: "foo",
Name: "Foo", // Name: "Foo",
Icon: &IconURL{ // Icon: &IconURL{
Value: "/favicon.ico", // Value: "/favicon.ico",
IconSource: IconSourceRelative, // IconSource: IconSourceRelative,
}, // },
Category: "App", // Category: "App",
} // }
overrides := GetJSONConfig() // overrides := GetJSONConfig()
ExpectNoError(t, overrides.SetShowItemOverride(a.Alias, true)) // overrides.SetShowItemOverride(a.Alias, true)
ExpectNoError(t, overrides.SetDisplayNameOverride(a.Alias, "Bar")) // overrides.SetDisplayNameOverride(a.Alias, "Bar")
ExpectNoError(t, overrides.SetDisplayCategoryOverride(a.Alias, "Test")) // overrides.SetDisplayCategoryOverride(a.Alias, "Test")
ExpectNoError(t, overrides.SetIconOverride(a.Alias, "png/example.png")) // ExpectNoError(t, overrides.SetIconOverride(a.Alias, "@walkxcode/example.png"))
overridden := a.GetOverriddenItem() // overridden := a.GetOverriddenItem()
ExpectTrue(t, overridden.Show) // ExpectTrue(t, overridden.Show)
ExpectEqual(t, overridden.Name, "Bar") // ExpectEqual(t, overridden.Name, "Bar")
ExpectEqual(t, overridden.Category, "Test") // ExpectEqual(t, overridden.Category, "Test")
ExpectEqual(t, overridden.Icon.String(), "png/example.png") // ExpectEqual(t, overridden.Icon.String(), "png/example.png")
} }

View file

@ -1,6 +1,7 @@
package homepage package homepage
import ( import (
"fmt"
"strings" "strings"
"github.com/yusing/go-proxy/internal" "github.com/yusing/go-proxy/internal"
@ -10,6 +11,7 @@ import (
type ( type (
IconURL struct { IconURL struct {
Value string `json:"value"` Value string `json:"value"`
FullValue string `json:"full_value"`
IconSource `json:"source"` IconSource `json:"source"`
Extra *IconExtra `json:"extra"` Extra *IconExtra `json:"extra"`
} }
@ -31,14 +33,39 @@ const (
var ErrInvalidIconURL = E.New("invalid icon url") var ErrInvalidIconURL = E.New("invalid icon url")
func (u *IconURL) HasIcon() bool { func NewSelfhStIconURL(reference, format string) *IconURL {
if u.IconSource == IconSourceSelfhSt && return &IconURL{
!internal.HasSelfhstIcon(u.Extra.Name, u.Extra.FileType) { Value: reference + "." + format,
return false FullValue: fmt.Sprintf("@selfhst/%s.%s", reference, format),
IconSource: IconSourceSelfhSt,
Extra: &IconExtra{
FileType: format,
Name: reference,
},
} }
if u.IconSource == IconSourceWalkXCode && }
!internal.HasWalkxCodeIcon(u.Extra.Name, u.Extra.FileType) {
return false func NewWalkXCodeIconURL(name, format string) *IconURL {
return &IconURL{
Value: name + "." + format,
FullValue: fmt.Sprintf("@walkxcode/%s.%s", name, format),
IconSource: IconSourceWalkXCode,
Extra: &IconExtra{
FileType: format,
Name: name,
},
}
}
// 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 {
if u.IconSource == IconSourceSelfhSt {
return internal.HasSelfhstIcon(u.Extra.Name, u.Extra.FileType)
}
if u.IconSource == IconSourceWalkXCode {
return internal.HasWalkxCodeIcon(u.Extra.Name, u.Extra.FileType)
} }
return true return true
} }
@ -52,6 +79,7 @@ func (u *IconURL) Parse(v string) error {
if slashIndex == -1 { if slashIndex == -1 {
return ErrInvalidIconURL return ErrInvalidIconURL
} }
u.FullValue = v
beforeSlash := v[:slashIndex] beforeSlash := v[:slashIndex]
switch beforeSlash { switch beforeSlash {
case "http:", "https:": case "http:", "https:":
@ -63,19 +91,23 @@ func (u *IconURL) Parse(v string) error {
if u.Value == "/" { if u.Value == "/" {
return ErrInvalidIconURL.Withf("%s", "empty path") return ErrInvalidIconURL.Withf("%s", "empty path")
} }
case "png", "svg", "webp": // walkXCode Icons case "png", "svg", "webp": // walkxcode Icons
u.Value = v u.Value = v
u.IconSource = IconSourceWalkXCode u.IconSource = IconSourceWalkXCode
u.Extra = &IconExtra{ u.Extra = &IconExtra{
FileType: beforeSlash, FileType: beforeSlash,
Name: strings.TrimSuffix(v[slashIndex+1:], "."+beforeSlash), Name: strings.TrimSuffix(v[slashIndex+1:], "."+beforeSlash),
} }
case "@selfhst": // selfh.st Icons, @selfhst/<reference>.<format> case "@selfhst", "@walkxcode": // selfh.st / walkxcode Icons, @selfhst/<reference>.<format>
u.Value = v[slashIndex:] u.Value = v[slashIndex+1:]
if beforeSlash == "@selfhst" {
u.IconSource = IconSourceSelfhSt u.IconSource = IconSourceSelfhSt
parts := strings.Split(v[slashIndex+1:], ".") } else {
u.IconSource = IconSourceWalkXCode
}
parts := strings.Split(u.Value, ".")
if len(parts) != 2 { if len(parts) != 2 {
return ErrInvalidIconURL.Withf("%s", "expect @selfhst/<reference>.<format>, e.g. @selfhst/adguard-home.webp") return ErrInvalidIconURL.Withf("expect @%s/<reference>.<format>, e.g. @%s/adguard-home.webp", beforeSlash, beforeSlash)
} }
reference, format := parts[0], strings.ToLower(parts[1]) reference, format := parts[0], strings.ToLower(parts[1])
if reference == "" || format == "" { if reference == "" || format == "" {
@ -84,7 +116,7 @@ func (u *IconURL) Parse(v string) error {
switch format { switch format {
case "svg", "png", "webp": case "svg", "png", "webp":
default: default:
return ErrInvalidIconURL.Withf("%s", "invalid format, expect svg/png/webp") return ErrInvalidIconURL.Withf("%s", "invalid image format, expect svg/png/webp")
} }
u.Extra = &IconExtra{ u.Extra = &IconExtra{
FileType: format, FileType: format,
@ -99,11 +131,33 @@ func (u *IconURL) Parse(v string) error {
} }
if !u.HasIcon() { if !u.HasIcon() {
return ErrInvalidIconURL.Withf("no such icon %s", u.Value) return ErrInvalidIconURL.Withf("no such icon %s from %s", u.Value, beforeSlash)
} }
return nil return nil
} }
func (u *IconURL) String() string { func (u *IconURL) URL() string {
switch u.IconSource {
case IconSourceAbsolute:
return u.Value return u.Value
case IconSourceRelative:
return "/" + u.Value
case IconSourceWalkXCode:
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/%s/%s.%s", u.Extra.FileType, u.Extra.Name, u.Extra.FileType)
case IconSourceSelfhSt:
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/selfhst/icons/%s/%s.%s", u.Extra.FileType, u.Extra.Name, u.Extra.FileType)
}
return ""
}
func (u *IconURL) String() string {
return u.FullValue
}
func (u *IconURL) MarshalText() ([]byte, error) {
return []byte(u.String()), nil
}
func (u *IconURL) UnmarshalText(data []byte) error {
return u.Parse(string(data))
} }

View file

@ -49,13 +49,25 @@ func TestIconURL(t *testing.T) {
}, },
{ {
name: "walkxcode", name: "walkxcode",
input: "png/walkxcode.png", input: "png/adguard-home.png",
wantValue: &IconURL{ wantValue: &IconURL{
Value: "png/walkxcode.png", Value: "png/adguard-home.png",
IconSource: IconSourceWalkXCode, IconSource: IconSourceWalkXCode,
Extra: &IconExtra{ Extra: &IconExtra{
FileType: "png", FileType: "png",
Name: "walkxcode", Name: "adguard-home",
},
},
},
{
name: "walkxcode_alt",
input: "@walkxcode/adguard-home.png",
wantValue: &IconURL{
Value: "adguard-home.png",
IconSource: IconSourceWalkXCode,
Extra: &IconExtra{
FileType: "png",
Name: "adguard-home",
}, },
}, },
}, },
@ -66,13 +78,13 @@ func TestIconURL(t *testing.T) {
}, },
{ {
name: "selfh.st_valid", name: "selfh.st_valid",
input: "@selfhst/foo.png", input: "@selfhst/adguard-home.png",
wantValue: &IconURL{ wantValue: &IconURL{
Value: "/foo.png", Value: "adguard-home.png",
IconSource: IconSourceSelfhSt, IconSource: IconSourceSelfhSt,
Extra: &IconExtra{ Extra: &IconExtra{
FileType: "png", FileType: "png",
Name: "foo", Name: "adguard-home",
}, },
}, },
}, },

View file

@ -1,147 +0,0 @@
package homepage
import (
"errors"
"os"
"sync"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/utils"
)
type JSONConfig struct {
DisplayNameOverride map[string]string `json:"display_name_override"`
DisplayCategoryOverride map[string]string `json:"display_category_override"`
DisplayOrder map[string]int `json:"display_order"` // TODO: implement this
CategoryNameOverride map[string]string `json:"category_name_override"`
CategoryOrder map[string]int `json:"category_order"` // TODO: implement this
IconOverride map[string]*IconURL `json:"icon_override"`
ShowItemOverride map[string]bool `json:"show_item_override"`
mu sync.RWMutex
}
var jsonConfigInstance *JSONConfig
func InitOverridesConfig() {
jsonConfigInstance = &JSONConfig{
DisplayNameOverride: make(map[string]string),
DisplayCategoryOverride: make(map[string]string),
DisplayOrder: make(map[string]int),
CategoryNameOverride: make(map[string]string),
CategoryOrder: make(map[string]int),
IconOverride: make(map[string]*IconURL),
ShowItemOverride: make(map[string]bool),
}
err := utils.LoadJSON(common.HomepageJSONConfigPath, jsonConfigInstance)
if err != nil && !os.IsNotExist(err) {
logging.Fatal().Err(err).Msg("failed to load homepage overrides config")
}
}
func GetJSONConfig() *JSONConfig {
return jsonConfigInstance
}
func (c *JSONConfig) save() error {
if common.IsTest {
return nil
}
return utils.SaveJSON(common.HomepageJSONConfigPath, c, 0o644)
}
func (c *JSONConfig) GetDisplayName(item *Item) string {
c.mu.RLock()
defer c.mu.RUnlock()
if override, ok := c.DisplayNameOverride[item.Alias]; ok {
return override
}
return item.Name
}
func (c *JSONConfig) SetDisplayNameOverride(key, value string) error {
c.mu.Lock()
defer c.mu.Unlock()
c.DisplayNameOverride[key] = value
return c.save()
}
func (c *JSONConfig) GetCategory(item *Item) string {
c.mu.RLock()
defer c.mu.RUnlock()
category := item.Category
if override, ok := c.DisplayCategoryOverride[item.Alias]; ok {
category = override
}
if override, ok := c.CategoryNameOverride[category]; ok {
return override
}
return category
}
func (c *JSONConfig) SetDisplayCategoryOverride(key, value string) error {
c.mu.Lock()
defer c.mu.Unlock()
c.DisplayCategoryOverride[key] = value
return c.save()
}
func (c *JSONConfig) SetDisplayOrder(key string, value int) error {
c.mu.Lock()
defer c.mu.Unlock()
c.DisplayOrder[key] = value
return c.save()
}
func (c *JSONConfig) SetCategoryNameOverride(key, value string) error {
c.mu.Lock()
defer c.mu.Unlock()
c.CategoryNameOverride[key] = value
return c.save()
}
func (c *JSONConfig) SetCategoryOrder(key string, value int) error {
c.mu.Lock()
defer c.mu.Unlock()
c.CategoryOrder[key] = value
return c.save()
}
func (c *JSONConfig) GetDisplayIcon(item *Item) *IconURL {
c.mu.RLock()
defer c.mu.RUnlock()
if override, ok := c.IconOverride[item.Alias]; ok {
return override
}
return item.Icon
}
func (c *JSONConfig) SetIconOverride(key, value string) error {
c.mu.Lock()
defer c.mu.Unlock()
var url IconURL
if err := url.Parse(value); err != nil {
return err
}
if !url.HasIcon() {
return errors.New("no such icon")
}
c.IconOverride[key] = &url
return c.save()
}
func (c *JSONConfig) GetShowItem(item *Item) bool {
c.mu.RLock()
defer c.mu.RUnlock()
if override, ok := c.ShowItemOverride[item.Alias]; ok {
return override
}
return true
}
func (c *JSONConfig) SetShowItemOverride(key string, value bool) error {
c.mu.Lock()
defer c.mu.Unlock()
c.ShowItemOverride[key] = value
return c.save()
}

View file

@ -0,0 +1,94 @@
package homepage
import (
"sync"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
)
type OverrideConfig struct {
ItemOverrides map[string]*ItemConfig `json:"item_overrides"`
DisplayOrder map[string]int `json:"display_order"` // TODO: implement this
CategoryName map[string]string `json:"category_name"`
CategoryOrder map[string]int `json:"category_order"` // TODO: implement this
mu sync.RWMutex
}
var overrideConfigInstance *OverrideConfig
func must(b []byte, err error) []byte {
if err != nil {
panic(err)
}
return b
}
func InitOverridesConfig() {
overrideConfigInstance = &OverrideConfig{
ItemOverrides: make(map[string]*ItemConfig),
DisplayOrder: make(map[string]int),
CategoryName: make(map[string]string),
CategoryOrder: make(map[string]int),
}
err := utils.LoadJSONIfExist(common.HomepageJSONConfigPath, overrideConfigInstance)
if err != nil {
logging.Error().Err(err).Msg("failed to load homepage overrides config")
} else {
logging.Info().Msgf("homepage overrides config loaded, %d items", len(overrideConfigInstance.ItemOverrides))
}
task.OnProgramExit("save_homepage_json_config", func() {
if err := utils.SaveJSON(common.HomepageJSONConfigPath, overrideConfigInstance, 0o644); err != nil {
logging.Error().Err(err).Msg("failed to save homepage overrides config")
}
})
}
func GetOverrideConfig() *OverrideConfig {
return overrideConfigInstance
}
func (c *OverrideConfig) UnmarshalJSON(data []byte) error {
return utils.DeserializeJSON(data, c)
}
func (c *OverrideConfig) OverrideItem(alias string, override *ItemConfig) {
c.mu.Lock()
defer c.mu.Unlock()
c.ItemOverrides[alias] = override
}
func (c *OverrideConfig) GetOverride(item *Item) *Item {
c.mu.RLock()
defer c.mu.RUnlock()
itemOverride, ok := c.ItemOverrides[item.Alias]
if !ok {
if catOverride, ok := c.CategoryName[item.Category]; ok {
clone := *item
clone.Category = catOverride
return &clone
}
return item
} else {
clone := *item
clone.ItemConfig = itemOverride
if catOverride, ok := c.CategoryName[clone.Category]; ok {
clone.Category = catOverride
}
return &clone
}
}
func (c *OverrideConfig) SetCategoryNameOverride(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.CategoryName[key] = value
}
func (c *OverrideConfig) SetCategoryOrder(key string, value int) {
c.mu.Lock()
defer c.mu.Unlock()
c.CategoryOrder[key] = value
}

View file

@ -8,9 +8,11 @@ import (
"sync" "sync"
"time" "time"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils"
) )
type GitHubContents struct { //! keep this, may reuse in future type GitHubContents struct { //! keep this, may reuse in future
@ -23,25 +25,20 @@ type GitHubContents struct { //! keep this, may reuse in future
type ( type (
IconsMap map[string]map[string]struct{} IconsMap map[string]map[string]struct{}
IconList []string
Cache struct { Cache struct {
WalkxCode, Selfhst IconsMap WalkxCode, Selfhst IconsMap
DisplayNames ReferenceDisplayNameMap DisplayNames ReferenceDisplayNameMap
IconList IconList // combined into a single list
} }
ReferenceDisplayNameMap map[string]string ReferenceDisplayNameMap map[string]string
) )
func (icons *Cache) isEmpty() bool { func (icons *Cache) needUpdate() bool {
return len(icons.WalkxCode) == 0 && len(icons.Selfhst) == 0 return len(icons.WalkxCode) == 0 || len(icons.Selfhst) == 0 || len(icons.IconList) == 0 || len(icons.DisplayNames) == 0
} }
func (icons *Cache) MarshalJSON() ([]byte, error) { const updateInterval = 2 * time.Hour
return json.Marshal(map[string]any{
"walkxcode": icons.WalkxCode,
"selfhst": icons.Selfhst,
})
}
const updateInterval = 1 * time.Hour
var ( var (
iconsCache *Cache iconsCache *Cache
@ -59,16 +56,17 @@ func InitIconListCache() {
WalkxCode: make(IconsMap), WalkxCode: make(IconsMap),
Selfhst: make(IconsMap), Selfhst: make(IconsMap),
DisplayNames: make(ReferenceDisplayNameMap), DisplayNames: make(ReferenceDisplayNameMap),
IconList: []string{},
} }
err := utils.LoadJSON(common.IconListCachePath, iconsCache) err := utils.LoadJSONIfExist(common.IconListCachePath, iconsCache)
if err != nil && !os.IsNotExist(err) { if err != nil {
logging.Fatal().Err(err).Msg("failed to load icon list cache config") logging.Error().Err(err).Msg("failed to load icon list cache config")
} else if err == nil { } else if stats, err := os.Stat(common.IconListCachePath); err == nil {
if stats, err := os.Stat(common.IconListCachePath); err != nil {
logging.Fatal().Err(err).Msg("failed to load icon list cache config")
} else {
lastUpdate = stats.ModTime() lastUpdate = stats.ModTime()
} logging.Info().Msgf("icon list cache loaded (%d icons, %d display names), last updated at %s",
len(iconsCache.IconList),
len(iconsCache.DisplayNames),
strutils.FormatTime(lastUpdate))
} }
} }
@ -77,7 +75,7 @@ func ListAvailableIcons() (*Cache, error) {
defer iconsCahceMu.Unlock() defer iconsCahceMu.Unlock()
if time.Since(lastUpdate) < updateInterval { if time.Since(lastUpdate) < updateInterval {
if !iconsCache.isEmpty() { if !iconsCache.needUpdate() {
return iconsCache, nil return iconsCache, nil
} }
} }
@ -87,6 +85,8 @@ func ListAvailableIcons() (*Cache, error) {
return nil, err return nil, err
} }
logging.Info().Msg("icons list updated")
iconsCache = icons iconsCache = icons
lastUpdate = time.Now() lastUpdate = time.Now()
@ -97,6 +97,17 @@ func ListAvailableIcons() (*Cache, error) {
return icons, nil return icons, nil
} }
func SearchIcons(keyword string, limit int) ([]string, error) {
icons, err := ListAvailableIcons()
if err != nil {
return nil, err
}
if keyword == "" {
return utils.Slice(icons.IconList, limit), nil
}
return utils.Slice(fuzzy.Find(keyword, icons.IconList), limit), nil
}
func HasWalkxCodeIcon(name string, filetype string) bool { func HasWalkxCodeIcon(name string, filetype string) bool {
icons, err := ListAvailableIcons() icons, err := ListAvailableIcons()
if err != nil { if err != nil {
@ -134,20 +145,26 @@ func GetDisplayName(reference string) (string, bool) {
} }
func fetchIconData() (*Cache, error) { func fetchIconData() (*Cache, error) {
walkxCodeIcons, err := fetchWalkxCodeIcons() walkxCodeIconMap, walkxCodeIconList, err := fetchWalkxCodeIcons()
if err != nil { if err != nil {
return nil, err return nil, err
} }
selfhstIcons, referenceToNames, err := fetchSelfhstIcons() n := 0
for _, items := range walkxCodeIconMap {
n += len(items)
}
selfhstIconMap, selfhstIconList, referenceToNames, err := fetchSelfhstIcons()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Cache{ return &Cache{
WalkxCode: walkxCodeIcons, WalkxCode: walkxCodeIconMap,
Selfhst: selfhstIcons, Selfhst: selfhstIconMap,
DisplayNames: referenceToNames, DisplayNames: referenceToNames,
IconList: append(walkxCodeIconList, selfhstIconList...),
}, nil }, nil
} }
@ -166,35 +183,37 @@ format:
] ]
} }
*/ */
func fetchWalkxCodeIcons() (IconsMap, error) { func fetchWalkxCodeIcons() (IconsMap, IconList, error) {
req, err := http.NewRequest(http.MethodGet, walkxcodeIcons, nil) req, err := http.NewRequest(http.MethodGet, walkxcodeIcons, nil)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
data := make(map[string][]string) data := make(map[string][]string)
err = json.Unmarshal(body, &data) err = json.Unmarshal(body, &data)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
icons := make(IconsMap, len(data)) icons := make(IconsMap, len(data))
iconList := make(IconList, 0, 2000)
for fileType, files := range data { for fileType, files := range data {
icons[fileType] = make(map[string]struct{}, len(files)) icons[fileType] = make(map[string]struct{}, len(files))
for _, icon := range files { for _, icon := range files {
icons[fileType][icon] = struct{}{} icons[fileType][icon] = struct{}{}
iconList = append(iconList, "@walkxcode/"+icon)
} }
} }
return icons, nil return icons, iconList, nil
} }
/* /*
@ -211,7 +230,7 @@ format:
"CreatedAt": "2024-08-16 00:27:23+00:00" "CreatedAt": "2024-08-16 00:27:23+00:00"
} }
*/ */
func fetchSelfhstIcons() (IconsMap, ReferenceDisplayNameMap, error) { func fetchSelfhstIcons() (IconsMap, IconList, ReferenceDisplayNameMap, error) {
type SelfhStIcon struct { type SelfhStIcon struct {
Name string `json:"Name"` Name string `json:"Name"`
Reference string `json:"Reference"` Reference string `json:"Reference"`
@ -225,25 +244,26 @@ func fetchSelfhstIcons() (IconsMap, ReferenceDisplayNameMap, error) {
req, err := http.NewRequest(http.MethodGet, selfhstIcons, nil) req, err := http.NewRequest(http.MethodGet, selfhstIcons, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
data := make([]SelfhStIcon, 0, 2000) data := make([]SelfhStIcon, 0, 2000)
err = json.Unmarshal(body, &data) err = json.Unmarshal(body, &data)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
iconList := make(IconList, 0, len(data)*3)
icons := make(IconsMap) icons := make(IconsMap)
icons["svg"] = make(map[string]struct{}, len(data)) icons["svg"] = make(map[string]struct{}, len(data))
icons["png"] = make(map[string]struct{}, len(data)) icons["png"] = make(map[string]struct{}, len(data))
@ -254,15 +274,18 @@ func fetchSelfhstIcons() (IconsMap, ReferenceDisplayNameMap, error) {
for _, item := range data { for _, item := range data {
if item.SVG == "Yes" { if item.SVG == "Yes" {
icons["svg"][item.Reference+".svg"] = struct{}{} icons["svg"][item.Reference+".svg"] = struct{}{}
iconList = append(iconList, "@selfhst/"+item.Reference+".svg")
} }
if item.PNG == "Yes" { if item.PNG == "Yes" {
icons["png"][item.Reference+".png"] = struct{}{} icons["png"][item.Reference+".png"] = struct{}{}
iconList = append(iconList, "@selfhst/"+item.Reference+".png")
} }
if item.WebP == "Yes" { if item.WebP == "Yes" {
icons["webp"][item.Reference+".webp"] = struct{}{} icons["webp"][item.Reference+".webp"] = struct{}{}
iconList = append(iconList, "@selfhst/"+item.Reference+".webp")
} }
referenceToNames[item.Reference] = item.Name referenceToNames[item.Reference] = item.Name
} }
return icons, referenceToNames, nil return icons, iconList, referenceToNames, nil
} }

View file

@ -194,7 +194,7 @@ func (r *HTTPRoute) addToLoadBalancer(parent task.Parent) {
linked = l.(*HTTPRoute) linked = l.(*HTTPRoute)
lb = linked.loadBalancer lb = linked.loadBalancer
lb.UpdateConfigIfNeeded(cfg) lb.UpdateConfigIfNeeded(cfg)
if linked.Raw.Homepage == nil && r.Raw.Homepage != nil { if linked.Raw.Homepage.IsEmpty() && !r.Raw.Homepage.IsEmpty() {
linked.Raw.Homepage = r.Raw.Homepage linked.Raw.Homepage = r.Raw.Homepage
} }
} else { } else {

View file

@ -45,7 +45,7 @@ func HomepageCategories() []string {
categories := make([]string, 0) categories := make([]string, 0)
routes.GetHTTPRoutes().RangeAll(func(alias string, r route.HTTPRoute) { routes.GetHTTPRoutes().RangeAll(func(alias string, r route.HTTPRoute) {
en := r.RawEntry() en := r.RawEntry()
if en.Homepage == nil || en.Homepage.Category == "" { if en.Homepage.IsEmpty() || en.Homepage.Category == "" {
return return
} }
if _, ok := check[en.Homepage.Category]; ok { if _, ok := check[en.Homepage.Category]; ok {
@ -63,13 +63,14 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
routes.GetHTTPRoutes().RangeAll(func(alias string, r route.HTTPRoute) { routes.GetHTTPRoutes().RangeAll(func(alias string, r route.HTTPRoute) {
en := r.RawEntry() en := r.RawEntry()
item := en.Homepage item := en.Homepage
if item == nil {
item = new(homepage.Item) if item.IsEmpty() {
item.Show = true item = homepage.NewItem(alias)
} }
if !item.IsEmpty() { if override := item.GetOverride(); override != item {
item.Show = true hpCfg.Add(override)
return
} }
if !item.Show { if !item.Show {
@ -85,8 +86,9 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
if item.Name == "" { if item.Name == "" {
reference := r.TargetName() reference := r.TargetName()
if r.RawEntry().Container != nil { cont := r.RawEntry().Container
reference = r.RawEntry().Container.ImageName if cont != nil {
reference = cont.ImageName
} }
name, ok := internal.GetDisplayName(reference) name, ok := internal.GetDisplayName(reference)
if ok { if ok {
@ -138,7 +140,7 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
} }
item.AltURL = r.TargetURL().String() item.AltURL = r.TargetURL().String()
hpCfg.Add(item.GetOverriddenItem()) hpCfg.Add(item)
}) })
return hpCfg return hpCfg
} }

View file

@ -172,6 +172,10 @@ func (e *RawEntry) Finalize() {
} }
} }
if e.Homepage == nil {
e.Homepage = homepage.NewItem(e.Alias)
}
e.finalized = true e.finalized = true
} }

View file

@ -18,3 +18,11 @@ func Intersect[T comparable, Slice ~[]T](slice1 Slice, slice2 Slice) Slice {
return result return result
} }
// Slice returns a slice of the first n elements in slice like javascript's slice.
func Slice[T any](slice []T, n int) []T {
if n >= len(slice) {
return slice
}
return slice[:n]
}

View file

@ -113,6 +113,10 @@ func (h *DirWatcher) start() {
relPath := strings.TrimPrefix(fsEvent.Name, h.dir) relPath := strings.TrimPrefix(fsEvent.Name, h.dir)
relPath = strings.TrimPrefix(relPath, "/") relPath = strings.TrimPrefix(relPath, "/")
if len(relPath) > 0 && relPath[0] == '.' { // hideden file
continue
}
msg := Event{ msg := Event{
Type: events.EventTypeFile, Type: events.EventTypeFile,
ActorName: relPath, ActorName: relPath,