Update version to 3.0.0 (New icon formats, dropped legacy URL)
This commit is contained in:
+1
-13
@@ -1,13 +1 @@
|
|||||||
node_modules
|
VERSION
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
README.md
|
|
||||||
.env
|
|
||||||
.nyc_output
|
|
||||||
coverage
|
|
||||||
.vscode
|
|
||||||
.DS_Store
|
|
||||||
*.log
|
|
||||||
+2
-5
@@ -1,17 +1,14 @@
|
|||||||
FROM golang:1.21-alpine AS builder
|
FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY go.mod main.go ./
|
COPY go.mod main.go ./
|
||||||
|
|
||||||
RUN go mod tidy && \
|
RUN CGO_ENABLED=0 go build -o server .
|
||||||
go mod download && \
|
|
||||||
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server .
|
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
|
||||||
COPY --from=builder /app/server /server
|
COPY --from=builder /app/server /server
|
||||||
|
|
||||||
USER 65534:65534
|
USER 65534:65534
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.2.0
|
3.0.0
|
||||||
+1
-5
@@ -1,7 +1,3 @@
|
|||||||
module selfhst-icons
|
module selfhst-icons
|
||||||
|
|
||||||
go 1.21
|
go 1.25
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/gorilla/mux v1.8.1
|
|
||||||
)
|
|
||||||
+20
-65
@@ -12,8 +12,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -67,7 +65,6 @@ func (c *Cache) Set(key, value string) {
|
|||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
// Remove oldest item if cache is full
|
|
||||||
if len(c.items) >= c.max {
|
if len(c.items) >= c.max {
|
||||||
var oldestKey string
|
var oldestKey string
|
||||||
var oldestTime time.Time
|
var oldestTime time.Time
|
||||||
@@ -109,8 +106,8 @@ func loadConfig() *Config {
|
|||||||
if standardFormat == "" {
|
if standardFormat == "" {
|
||||||
standardFormat = "svg"
|
standardFormat = "svg"
|
||||||
}
|
}
|
||||||
// Validate format
|
|
||||||
if standardFormat != "svg" && standardFormat != "png" && standardFormat != "webp" {
|
if standardFormat != "svg" && standardFormat != "png" && standardFormat != "webp" && standardFormat != "avif" && standardFormat != "ico" {
|
||||||
standardFormat = "svg"
|
standardFormat = "svg"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,14 +170,12 @@ func fetchRemoteFile(url string) (string, error) {
|
|||||||
func applySVGColor(svgContent, colorCode string) string {
|
func applySVGColor(svgContent, colorCode string) string {
|
||||||
color := "#" + colorCode
|
color := "#" + colorCode
|
||||||
|
|
||||||
// Replace style fill attributes
|
|
||||||
re1 := regexp.MustCompile(`style="[^"]*fill:\s*#fff[^"]*"`)
|
re1 := regexp.MustCompile(`style="[^"]*fill:\s*#fff[^"]*"`)
|
||||||
svgContent = re1.ReplaceAllStringFunc(svgContent, func(match string) string {
|
svgContent = re1.ReplaceAllStringFunc(svgContent, func(match string) string {
|
||||||
re2 := regexp.MustCompile(`fill:\s*#fff`)
|
re2 := regexp.MustCompile(`fill:\s*#fff`)
|
||||||
return re2.ReplaceAllString(match, "fill:"+color)
|
return re2.ReplaceAllString(match, "fill:"+color)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Replace direct fill attributes
|
|
||||||
re3 := regexp.MustCompile(`fill="#fff"`)
|
re3 := regexp.MustCompile(`fill="#fff"`)
|
||||||
svgContent = re3.ReplaceAllString(svgContent, `fill="`+color+`"`)
|
svgContent = re3.ReplaceAllString(svgContent, `fill="`+color+`"`)
|
||||||
|
|
||||||
@@ -193,6 +188,10 @@ func getContentType(format string) string {
|
|||||||
return "image/png"
|
return "image/png"
|
||||||
case "webp":
|
case "webp":
|
||||||
return "image/webp"
|
return "image/webp"
|
||||||
|
case "avif":
|
||||||
|
return "image/avif"
|
||||||
|
case "ico":
|
||||||
|
return "image/x-icon"
|
||||||
case "svg":
|
case "svg":
|
||||||
return "image/svg+xml"
|
return "image/svg+xml"
|
||||||
default:
|
default:
|
||||||
@@ -208,16 +207,14 @@ func getCacheKey(iconName, colorCode string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleIcon(w http.ResponseWriter, r *http.Request) {
|
func handleIcon(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
iconName := r.PathValue("iconname")
|
||||||
iconName := vars["iconname"]
|
colorCode := r.PathValue("colorcode")
|
||||||
colorCode := vars["colorcode"]
|
|
||||||
|
|
||||||
if iconName == "" {
|
if iconName == "" {
|
||||||
http.Error(w, "Icon name is required", http.StatusBadRequest)
|
http.Error(w, "Icon name is required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate color if provided
|
|
||||||
if colorCode != "" && !isValidHexColor(colorCode) {
|
if colorCode != "" && !isValidHexColor(colorCode) {
|
||||||
log.Printf("[ERROR] Invalid color code for icon \"%s\": %s", iconName, colorCode)
|
log.Printf("[ERROR] Invalid color code for icon \"%s\": %s", iconName, colorCode)
|
||||||
http.Error(w, "Invalid color code. Use 6-digit hex without #", http.StatusBadRequest)
|
http.Error(w, "Invalid color code. Use 6-digit hex without #", http.StatusBadRequest)
|
||||||
@@ -226,21 +223,17 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
cacheKey := getCacheKey(iconName, colorCode)
|
cacheKey := getCacheKey(iconName, colorCode)
|
||||||
|
|
||||||
// Determine content type and format to serve
|
|
||||||
var contentType string
|
var contentType string
|
||||||
var formatToServe string
|
var formatToServe string
|
||||||
|
|
||||||
if colorCode != "" {
|
if colorCode != "" {
|
||||||
// Always use SVG for colorization
|
|
||||||
contentType = "image/svg+xml"
|
contentType = "image/svg+xml"
|
||||||
formatToServe = "svg"
|
formatToServe = "svg"
|
||||||
} else {
|
} else {
|
||||||
// Use standard format when no color specified
|
|
||||||
contentType = getContentType(config.StandardIconFormat)
|
contentType = getContentType(config.StandardIconFormat)
|
||||||
formatToServe = config.StandardIconFormat
|
formatToServe = config.StandardIconFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check cache first
|
|
||||||
if cached, found := cache.Get(cacheKey); found {
|
if cached, found := cache.Get(cacheKey); found {
|
||||||
log.Printf("[CACHE] Serving cached icon: \"%s\"%s (%s)", iconName,
|
log.Printf("[CACHE] Serving cached icon: \"%s\"%s (%s)", iconName,
|
||||||
func() string { if colorCode != "" { return " with color " + colorCode } else { return "" } }(),
|
func() string { if colorCode != "" { return " with color " + colorCode } else { return "" } }(),
|
||||||
@@ -254,9 +247,7 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if config.IconSource == "local" {
|
if config.IconSource == "local" {
|
||||||
// Use local volume
|
|
||||||
if colorCode != "" {
|
if colorCode != "" {
|
||||||
// Try to find -light version for colorization (always SVG)
|
|
||||||
lightPath := filepath.Join(config.LocalPath, "svg", iconName+"-light.svg")
|
lightPath := filepath.Join(config.LocalPath, "svg", iconName+"-light.svg")
|
||||||
if fileExists(lightPath) {
|
if fileExists(lightPath) {
|
||||||
iconContent, err = readLocalFile(lightPath)
|
iconContent, err = readLocalFile(lightPath)
|
||||||
@@ -265,12 +256,10 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No color - try to serve standard format
|
|
||||||
var standardPath string
|
var standardPath string
|
||||||
if formatToServe == "svg" {
|
if formatToServe == "svg" {
|
||||||
standardPath = filepath.Join(config.LocalPath, "svg", iconName+".svg")
|
standardPath = filepath.Join(config.LocalPath, "svg", iconName+".svg")
|
||||||
} else {
|
} else {
|
||||||
// For PNG/WebP, use format-specific directories
|
|
||||||
standardPath = filepath.Join(config.LocalPath, formatToServe, iconName+"."+formatToServe)
|
standardPath = filepath.Join(config.LocalPath, formatToServe, iconName+"."+formatToServe)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +268,6 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to SVG if standard format not found
|
|
||||||
if iconContent == "" {
|
if iconContent == "" {
|
||||||
svgPath := filepath.Join(config.LocalPath, "svg", iconName+".svg")
|
svgPath := filepath.Join(config.LocalPath, "svg", iconName+".svg")
|
||||||
if fileExists(svgPath) {
|
if fileExists(svgPath) {
|
||||||
@@ -289,9 +277,7 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use remote CDN
|
|
||||||
if colorCode != "" {
|
if colorCode != "" {
|
||||||
// Try to find -light version for colorization (always SVG)
|
|
||||||
lightURL := config.JSDelivrURL + "/svg/" + iconName + "-light.svg"
|
lightURL := config.JSDelivrURL + "/svg/" + iconName + "-light.svg"
|
||||||
if urlExists(lightURL) {
|
if urlExists(lightURL) {
|
||||||
iconContent, err = fetchRemoteFile(lightURL)
|
iconContent, err = fetchRemoteFile(lightURL)
|
||||||
@@ -300,7 +286,6 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No color - try to serve standard format
|
|
||||||
var standardURL string
|
var standardURL string
|
||||||
if formatToServe == "svg" {
|
if formatToServe == "svg" {
|
||||||
standardURL = config.JSDelivrURL + "/svg/" + iconName + ".svg"
|
standardURL = config.JSDelivrURL + "/svg/" + iconName + ".svg"
|
||||||
@@ -313,7 +298,6 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to SVG if standard format not found
|
|
||||||
if iconContent == "" {
|
if iconContent == "" {
|
||||||
svgURL := config.JSDelivrURL + "/svg/" + iconName + ".svg"
|
svgURL := config.JSDelivrURL + "/svg/" + iconName + ".svg"
|
||||||
iconContent, err = fetchRemoteFile(svgURL)
|
iconContent, err = fetchRemoteFile(svgURL)
|
||||||
@@ -330,7 +314,6 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
cache.Set(cacheKey, iconContent)
|
cache.Set(cacheKey, iconContent)
|
||||||
|
|
||||||
log.Printf("[SUCCESS] Serving icon: \"%s\"%s (%s, source: %s)", iconName,
|
log.Printf("[SUCCESS] Serving icon: \"%s\"%s (%s, source: %s)", iconName,
|
||||||
@@ -341,41 +324,19 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(iconContent))
|
w.Write([]byte(iconContent))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLegacyIcon(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
colorQuery := r.URL.Query().Get("color")
|
|
||||||
|
|
||||||
var colorCode string
|
|
||||||
if colorQuery != "" {
|
|
||||||
cleanColor := strings.TrimPrefix(colorQuery, "#")
|
|
||||||
if isValidHexColor(cleanColor) {
|
|
||||||
colorCode = cleanColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect internally to new handler
|
|
||||||
vars["colorcode"] = colorCode
|
|
||||||
handleIcon(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCustomIcon(w http.ResponseWriter, r *http.Request) {
|
func handleCustomIcon(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
filename := r.PathValue("filename")
|
||||||
filename := vars["filename"]
|
|
||||||
|
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
http.Error(w, "Filename is required", http.StatusBadRequest)
|
http.Error(w, "Filename is required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the path to the custom icon file
|
|
||||||
customPath := filepath.Join("/app/icons/custom", filename)
|
customPath := filepath.Join("/app/icons/custom", filename)
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
log.Printf("[DEBUG] Looking for custom icon at: %s", customPath)
|
log.Printf("[DEBUG] Looking for custom icon at: %s", customPath)
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
if !fileExists(customPath) {
|
if !fileExists(customPath) {
|
||||||
// List directory contents for debugging
|
|
||||||
if files, err := os.ReadDir("/app/icons/custom"); err == nil {
|
if files, err := os.ReadDir("/app/icons/custom"); err == nil {
|
||||||
var fileList []string
|
var fileList []string
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
@@ -390,7 +351,6 @@ func handleCustomIcon(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the file
|
|
||||||
data, err := os.ReadFile(customPath)
|
data, err := os.ReadFile(customPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed to read custom icon \"%s\": %v", filename, err)
|
log.Printf("[ERROR] Failed to read custom icon \"%s\": %v", filename, err)
|
||||||
@@ -398,7 +358,6 @@ func handleCustomIcon(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine content type based on file extension
|
|
||||||
ext := strings.ToLower(filepath.Ext(filename))
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
var contentType string
|
var contentType string
|
||||||
switch ext {
|
switch ext {
|
||||||
@@ -412,6 +371,8 @@ func handleCustomIcon(w http.ResponseWriter, r *http.Request) {
|
|||||||
contentType = "image/svg+xml"
|
contentType = "image/svg+xml"
|
||||||
case ".webp":
|
case ".webp":
|
||||||
contentType = "image/webp"
|
contentType = "image/webp"
|
||||||
|
case ".avif":
|
||||||
|
contentType = "image/avif"
|
||||||
case ".ico":
|
case ".ico":
|
||||||
contentType = "image/x-icon"
|
contentType = "image/x-icon"
|
||||||
default:
|
default:
|
||||||
@@ -454,20 +415,14 @@ func main() {
|
|||||||
config = loadConfig()
|
config = loadConfig()
|
||||||
cache = NewCache(config.CacheTTL, config.CacheSize)
|
cache = NewCache(config.CacheTTL, config.CacheSize)
|
||||||
|
|
||||||
r := mux.NewRouter()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
// Custom icons route: /custom/filename
|
mux.HandleFunc("GET /custom/{filename}", handleCustomIcon)
|
||||||
r.HandleFunc("/custom/{filename}", handleCustomIcon).Methods("GET")
|
|
||||||
|
mux.HandleFunc("GET /{iconname}/{colorcode}", handleIcon)
|
||||||
// Main route: /iconname or /iconname/colorcode
|
mux.HandleFunc("GET /{iconname}", handleIcon)
|
||||||
r.HandleFunc("/{iconname}", handleIcon).Methods("GET")
|
|
||||||
r.HandleFunc("/{iconname}/{colorcode}", handleIcon).Methods("GET")
|
mux.HandleFunc("GET /", handleRoot)
|
||||||
|
|
||||||
// Legacy route: /iconname.svg?color=colorcode
|
|
||||||
r.HandleFunc("/{iconname}.svg", handleLegacyIcon).Methods("GET")
|
|
||||||
|
|
||||||
// Root endpoint
|
|
||||||
r.HandleFunc("/", handleRoot).Methods("GET")
|
|
||||||
|
|
||||||
log.Printf("Icon server listening on port %s", config.Port)
|
log.Printf("Icon server listening on port %s", config.Port)
|
||||||
log.Printf("Icon source: %s", func() string {
|
log.Printf("Icon source: %s", func() string {
|
||||||
@@ -478,5 +433,5 @@ func main() {
|
|||||||
}())
|
}())
|
||||||
log.Printf("Cache settings: TTL %ds, Max %d items", int(config.CacheTTL.Seconds()), config.CacheSize)
|
log.Printf("Cache settings: TTL %ds, Max %d items", int(config.CacheTTL.Seconds()), config.CacheSize)
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":"+config.Port, r))
|
log.Fatal(http.ListenAndServe(":"+config.Port, mux))
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user