mirror of
https://github.com/yusing/godoxy.git
synced 2025-06-04 02:42:34 +02:00
updated ls-icon
and icon fetching mechanism
This commit is contained in:
parent
75dc12d875
commit
a24b0d4729
5 changed files with 192 additions and 74 deletions
|
@ -16,6 +16,7 @@ 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/homepage"
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
@ -78,12 +79,15 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||||
var status int
|
var status int
|
||||||
var errMsg string
|
var errMsg string
|
||||||
|
|
||||||
homepage := r.RawEntry().Homepage
|
hp := r.RawEntry().Homepage
|
||||||
if homepage != nil && homepage.Icon != nil {
|
if hp != nil && hp.Icon != nil {
|
||||||
if homepage.Icon.IsRelative {
|
switch hp.Icon.IconSource {
|
||||||
icon, status, errMsg = findIcon(r, req, homepage.Icon.Value)
|
case homepage.IconSourceAbsolute:
|
||||||
} else {
|
icon, status, errMsg = fetchIconAbsolute(hp.Icon.Value)
|
||||||
icon, status, errMsg = getIconAbsolute(homepage.Icon.Value)
|
case homepage.IconSourceRelative:
|
||||||
|
icon, status, errMsg = findIcon(r, req, hp.Icon.Value)
|
||||||
|
case homepage.IconSourceWalkXCode:
|
||||||
|
icon, status, errMsg = fetchWalkxcodeIcon(hp.Icon.Extra.FileType, hp.Icon.Extra.Name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// try extract from "link[rel=icon]"
|
// try extract from "link[rel=icon]"
|
||||||
|
@ -124,7 +128,7 @@ func storeIconCache(key string, icon []byte) {
|
||||||
iconCache[key] = icon
|
iconCache[key] = icon
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIconAbsolute(url string) ([]byte, int, string) {
|
func fetchIconAbsolute(url string) ([]byte, int, string) {
|
||||||
icon, ok := loadIconCache(url)
|
icon, ok := loadIconCache(url)
|
||||||
if ok {
|
if ok {
|
||||||
return icon, http.StatusOK, ""
|
return icon, http.StatusOK, ""
|
||||||
|
@ -165,6 +169,25 @@ func sanitizeName(name string) string {
|
||||||
return strings.ToLower(nameSanitizer.Replace(name))
|
return strings.ToLower(nameSanitizer.Replace(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchWalkxcodeIcon(filetype string, name string) ([]byte, int, string) {
|
||||||
|
// if icon isn't in the list, no need to fetch
|
||||||
|
if !internal.HasIcon(name, filetype) {
|
||||||
|
logging.Debug().
|
||||||
|
Str("filetype", filetype).
|
||||||
|
Str("name", name).
|
||||||
|
Msg("icon not found")
|
||||||
|
return nil, http.StatusNotFound, "icon not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
icon, ok := loadIconCache("walkxcode/" + filetype + "/" + name)
|
||||||
|
if ok {
|
||||||
|
return icon, http.StatusOK, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
url := homepage.DashboardIconBaseURL + filetype + "/" + name + "." + filetype
|
||||||
|
return fetchIconAbsolute(url)
|
||||||
|
}
|
||||||
|
|
||||||
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.TargetName()
|
||||||
icon, ok := loadIconCache(key)
|
icon, ok := loadIconCache(key)
|
||||||
|
@ -175,10 +198,10 @@ func findIcon(r route.HTTPRoute, req *http.Request, uri string) (icon []byte, st
|
||||||
return icon, http.StatusOK, ""
|
return icon, http.StatusOK, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
icon, status, errMsg = getIconAbsolute(homepage.DashboardIconBaseURL + "png/" + sanitizeName(r.TargetName()) + ".png")
|
icon, status, errMsg = fetchWalkxcodeIcon("png", sanitizeName(r.TargetName()))
|
||||||
cont := r.RawEntry().Container
|
cont := r.RawEntry().Container
|
||||||
if icon == nil && cont != nil {
|
if icon == nil && cont != nil {
|
||||||
icon, status, errMsg = getIconAbsolute(homepage.DashboardIconBaseURL + "png/" + sanitizeName(cont.ImageName) + ".png")
|
icon, status, errMsg = fetchWalkxcodeIcon("png", sanitizeName(cont.ImageName))
|
||||||
}
|
}
|
||||||
if icon == nil {
|
if icon == nil {
|
||||||
// fallback to parse html
|
// fallback to parse html
|
||||||
|
@ -224,10 +247,6 @@ func findIconSlow(r route.HTTPRoute, req *http.Request, uri string) (icon []byte
|
||||||
if loc == newReq.URL.Path {
|
if loc == newReq.URL.Path {
|
||||||
return nil, http.StatusBadGateway, "circular redirect"
|
return nil, http.StatusBadGateway, "circular redirect"
|
||||||
}
|
}
|
||||||
logging.Debug().Str("route", r.TargetName()).
|
|
||||||
Str("from", uri).
|
|
||||||
Str("to", loc).
|
|
||||||
Msg("favicon redirect")
|
|
||||||
return findIconSlow(r, req, loc)
|
return findIconSlow(r, req, loc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -264,8 +283,10 @@ func findIconSlow(r route.HTTPRoute, req *http.Request, uri string) (icon []byte
|
||||||
}
|
}
|
||||||
return dataURI.Data, http.StatusOK, ""
|
return dataURI.Data, http.StatusOK, ""
|
||||||
}
|
}
|
||||||
if href[0] != '/' {
|
switch {
|
||||||
return getIconAbsolute(href)
|
case strings.HasPrefix(href, "http://"), strings.HasPrefix(href, "https://"):
|
||||||
|
return fetchIconAbsolute(href)
|
||||||
|
default:
|
||||||
|
return findIconSlow(r, req, path.Clean(href))
|
||||||
}
|
}
|
||||||
return findIconSlow(r, req, href)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,26 @@ import (
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IconURL struct {
|
type (
|
||||||
Value string `json:"value"`
|
IconURL struct {
|
||||||
IsRelative bool `json:"is_relative"`
|
Value string `json:"value"`
|
||||||
}
|
IconSource
|
||||||
|
Extra *IconExtra `json:"extra"`
|
||||||
|
}
|
||||||
|
|
||||||
|
IconExtra struct {
|
||||||
|
FileType string `json:"file_type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
IconSource int
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IconSourceAbsolute IconSource = iota
|
||||||
|
IconSourceRelative
|
||||||
|
IconSourceWalkXCode
|
||||||
|
)
|
||||||
|
|
||||||
const DashboardIconBaseURL = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/"
|
const DashboardIconBaseURL = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/"
|
||||||
|
|
||||||
|
@ -28,13 +44,19 @@ func (u *IconURL) Parse(v string) error {
|
||||||
switch beforeSlash {
|
switch beforeSlash {
|
||||||
case "http:", "https:":
|
case "http:", "https:":
|
||||||
u.Value = v
|
u.Value = v
|
||||||
|
u.IconSource = IconSourceAbsolute
|
||||||
return nil
|
return nil
|
||||||
case "@target":
|
case "@target":
|
||||||
u.Value = v[slashIndex:]
|
u.Value = v[slashIndex:]
|
||||||
u.IsRelative = true
|
u.IconSource = IconSourceRelative
|
||||||
return nil
|
return nil
|
||||||
case "png", "svg": // walkXCode Icons
|
case "png", "svg", "webp": // walkXCode Icons
|
||||||
u.Value = DashboardIconBaseURL + v
|
u.Value = v
|
||||||
|
u.IconSource = IconSourceWalkXCode
|
||||||
|
u.Extra = &IconExtra{
|
||||||
|
FileType: beforeSlash,
|
||||||
|
Name: strings.TrimSuffix(v[slashIndex+1:], "."+beforeSlash),
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return ErrInvalidIconURL
|
return ErrInvalidIconURL
|
||||||
|
|
67
internal/homepage/icon_url_test.go
Normal file
67
internal/homepage/icon_url_test.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package homepage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIconURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
wantValue *IconURL
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "absolute",
|
||||||
|
input: "http://example.com/icon.png",
|
||||||
|
wantValue: &IconURL{
|
||||||
|
Value: "http://example.com/icon.png",
|
||||||
|
IconSource: IconSourceAbsolute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relative",
|
||||||
|
input: "@target/icon.png",
|
||||||
|
wantValue: &IconURL{
|
||||||
|
Value: "/icon.png",
|
||||||
|
IconSource: IconSourceRelative,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "walkxcode",
|
||||||
|
input: "png/walkxcode.png",
|
||||||
|
wantValue: &IconURL{
|
||||||
|
Value: "png/walkxcode.png",
|
||||||
|
IconSource: IconSourceWalkXCode,
|
||||||
|
Extra: &IconExtra{
|
||||||
|
FileType: "png",
|
||||||
|
Name: "walkxcode",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
input: "invalid",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
input: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
u := &IconURL{}
|
||||||
|
err := u.Parse(tc.input)
|
||||||
|
if tc.wantErr {
|
||||||
|
ExpectError(t, ErrInvalidIconURL, err)
|
||||||
|
} else {
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
ExpectDeepEqual(t, u, tc.wantValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,14 +2,12 @@ package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GitHubContents struct { //! keep this, may reuse in future
|
type GitHubContents struct { //! keep this, may reuse in future
|
||||||
|
@ -20,54 +18,71 @@ type GitHubContents struct { //! keep this, may reuse in future
|
||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
type Icons map[string]map[string]struct{}
|
||||||
iconsCachePath = "/tmp/icons_cache.json"
|
|
||||||
updateInterval = 1 * time.Hour
|
// no longer cache for `godoxy ls-icons`
|
||||||
|
|
||||||
|
const updateInterval = 1 * time.Hour
|
||||||
|
|
||||||
|
var (
|
||||||
|
iconsCache = make(Icons)
|
||||||
|
iconsCahceMu sync.Mutex
|
||||||
|
lastUpdate time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListAvailableIcons() ([]string, error) {
|
const walkxcodeIcons = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/tree.json"
|
||||||
owner := "walkxcode"
|
|
||||||
repo := "dashboard-icons"
|
|
||||||
ref := "main"
|
|
||||||
|
|
||||||
var lastUpdate time.Time
|
func ListAvailableIcons() (Icons, error) {
|
||||||
|
iconsCahceMu.Lock()
|
||||||
|
defer iconsCahceMu.Unlock()
|
||||||
|
|
||||||
icons := make([]string, 0)
|
|
||||||
info, err := os.Stat(iconsCachePath)
|
|
||||||
if err == nil {
|
|
||||||
lastUpdate = info.ModTime().Local()
|
|
||||||
}
|
|
||||||
if time.Since(lastUpdate) < updateInterval {
|
if time.Since(lastUpdate) < updateInterval {
|
||||||
err := utils.LoadJSON(iconsCachePath, &icons)
|
if len(iconsCache) > 0 {
|
||||||
if err == nil {
|
return iconsCache, nil
|
||||||
return icons, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contents, err := getRepoContents(http.DefaultClient, owner, repo, ref, "")
|
icons, err := getIcons()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, content := range contents {
|
|
||||||
if content.Type != "dir" {
|
iconsCache = icons
|
||||||
icons = append(icons, content.Path)
|
lastUpdate = time.Now()
|
||||||
}
|
|
||||||
}
|
|
||||||
err = utils.SaveJSON(iconsCachePath, &icons, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
log.Print("error saving cache", err)
|
|
||||||
}
|
|
||||||
return icons, nil
|
return icons, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRepoContents(client *http.Client, owner string, repo string, ref string, path string) ([]GitHubContents, error) {
|
func HasIcon(name string, filetype string) bool {
|
||||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s?ref=%s", owner, repo, path, ref), nil)
|
icons, err := ListAvailableIcons()
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).Msg("failed to list icons")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, ok := icons[filetype]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := icons[filetype][name+"."+filetype]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
format:
|
||||||
|
|
||||||
|
{
|
||||||
|
"png": [
|
||||||
|
"*.png",
|
||||||
|
],
|
||||||
|
"svg": [
|
||||||
|
"*.svg",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
func getIcons() (Icons, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, walkxcodeIcons, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("Accept", "application/json")
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -78,24 +93,17 @@ func getRepoContents(client *http.Client, owner string, repo string, ref string,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var contents []GitHubContents
|
data := make(map[string][]string)
|
||||||
err = json.Unmarshal(body, &contents)
|
err = json.Unmarshal(body, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
icons := make(Icons, len(data))
|
||||||
filesAndDirs := make([]GitHubContents, 0)
|
for fileType, files := range data {
|
||||||
for _, content := range contents {
|
icons[fileType] = make(map[string]struct{}, len(files))
|
||||||
if content.Type == "dir" {
|
for _, icon := range files {
|
||||||
subContents, err := getRepoContents(client, owner, repo, ref, content.Path)
|
icons[fileType][icon] = struct{}{}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
filesAndDirs = append(filesAndDirs, subContents...)
|
|
||||||
} else {
|
|
||||||
filesAndDirs = append(filesAndDirs, content)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return icons, nil
|
||||||
return filesAndDirs, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"pattern": "^(png|svg)\\/[\\w\\d\\-_]+\\.\\1$",
|
"pattern": "^(png|svg|webp)\\/[\\w\\d\\-_]+\\.\\1$",
|
||||||
"title": "Icon from walkxcode/dashboard-icons"
|
"title": "Icon from walkxcode/dashboard-icons"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Reference in a new issue