diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index 9834a3b9..a294c7f3 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -88,10 +88,10 @@ jobs: - name: Output image info run: | - echo "🐳 Images built and pushed:" + echo "Images built and pushed:" echo "${{ steps.meta.outputs.tags }}" | sed 's/^/ - /' echo "" - echo "🏗️ Supported architectures:" - echo " - linux/amd64 (Intel/AMD x86_64)" - echo " - linux/arm64 (ARM 64-bit, Apple M1, Raspberry Pi 4)" - echo " - linux/arm/v7 (ARM 32-bit, Raspberry Pi 2/3)" \ No newline at end of file + echo "Supported architectures:" + echo " - linux/amd64" + echo " - linux/arm64" + echo " - linux/arm/v7" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fbcbdf0..94a08a9c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# v3.0.0 + +## Breaking Changes + +The legacy custom color URL ```domain.com/icon.svg?color=000000``` is no longer supported. See the [supported methods for building URLs](https://github.com/selfhst/icons/wiki#building-links) in the project's wiki if this change impacts you. + +## What's Changed + +* Added support for the new AVIF and ICO formats +* Updated Go version to [v1.25](https://go.dev/doc/go1.25) +* Removed unmaintained [gorilla/mux](https://github.com/gorilla/mux) external dependency in favor of [net/http enhancements introduced in Go 1.22](https://go.dev/blog/routing-enhancements) + # v2.2.0 ## What's Changed diff --git a/build/.dockerignore b/build/.dockerignore index 526398d9..974cf28e 100755 --- a/build/.dockerignore +++ b/build/.dockerignore @@ -1,13 +1 @@ -node_modules -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.git -.gitignore -README.md -.env -.nyc_output -coverage -.vscode -.DS_Store -*.log \ No newline at end of file +VERSION \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile index 7a8067da..699b7e95 100755 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,17 +1,14 @@ -FROM golang:1.21-alpine AS builder +FROM golang:1.25-alpine AS builder WORKDIR /app COPY go.mod main.go ./ -RUN go mod tidy && \ - go mod download && \ - CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server . +RUN CGO_ENABLED=0 go build -o server . FROM scratch COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ - COPY --from=builder /app/server /server USER 65534:65534 diff --git a/build/VERSION b/build/VERSION index e3a4f193..56fea8a0 100755 --- a/build/VERSION +++ b/build/VERSION @@ -1 +1 @@ -2.2.0 \ No newline at end of file +3.0.0 \ No newline at end of file diff --git a/build/go.mod b/build/go.mod index c6ff5d2b..7096bbd0 100755 --- a/build/go.mod +++ b/build/go.mod @@ -1,7 +1,3 @@ module selfhst-icons -go 1.21 - -require ( - github.com/gorilla/mux v1.8.1 -) \ No newline at end of file +go 1.25 \ No newline at end of file diff --git a/build/main.go b/build/main.go index 70747f3c..d47b8f5b 100755 --- a/build/main.go +++ b/build/main.go @@ -12,8 +12,6 @@ import ( "strings" "sync" "time" - - "github.com/gorilla/mux" ) type Config struct { @@ -67,7 +65,6 @@ func (c *Cache) Set(key, value string) { c.mutex.Lock() defer c.mutex.Unlock() - // Remove oldest item if cache is full if len(c.items) >= c.max { var oldestKey string var oldestTime time.Time @@ -109,8 +106,8 @@ func loadConfig() *Config { if standardFormat == "" { standardFormat = "svg" } - // Validate format - if standardFormat != "svg" && standardFormat != "png" && standardFormat != "webp" { + + if standardFormat != "svg" && standardFormat != "png" && standardFormat != "webp" && standardFormat != "avif" && standardFormat != "ico" { standardFormat = "svg" } @@ -173,14 +170,12 @@ func fetchRemoteFile(url string) (string, error) { func applySVGColor(svgContent, colorCode string) string { color := "#" + colorCode - // Replace style fill attributes re1 := regexp.MustCompile(`style="[^"]*fill:\s*#fff[^"]*"`) svgContent = re1.ReplaceAllStringFunc(svgContent, func(match string) string { re2 := regexp.MustCompile(`fill:\s*#fff`) return re2.ReplaceAllString(match, "fill:"+color) }) - // Replace direct fill attributes re3 := regexp.MustCompile(`fill="#fff"`) svgContent = re3.ReplaceAllString(svgContent, `fill="`+color+`"`) @@ -193,6 +188,10 @@ func getContentType(format string) string { return "image/png" case "webp": return "image/webp" + case "avif": + return "image/avif" + case "ico": + return "image/x-icon" case "svg": return "image/svg+xml" default: @@ -208,16 +207,14 @@ func getCacheKey(iconName, colorCode string) string { } func handleIcon(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - iconName := vars["iconname"] - colorCode := vars["colorcode"] + iconName := r.PathValue("iconname") + colorCode := r.PathValue("colorcode") if iconName == "" { http.Error(w, "Icon name is required", http.StatusBadRequest) return } - // Validate color if provided if colorCode != "" && !isValidHexColor(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) @@ -226,21 +223,17 @@ func handleIcon(w http.ResponseWriter, r *http.Request) { cacheKey := getCacheKey(iconName, colorCode) - // Determine content type and format to serve var contentType string var formatToServe string if colorCode != "" { - // Always use SVG for colorization contentType = "image/svg+xml" formatToServe = "svg" } else { - // Use standard format when no color specified contentType = getContentType(config.StandardIconFormat) formatToServe = config.StandardIconFormat } - // Check cache first if cached, found := cache.Get(cacheKey); found { log.Printf("[CACHE] Serving cached icon: \"%s\"%s (%s)", iconName, 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 if config.IconSource == "local" { - // Use local volume if colorCode != "" { - // Try to find -light version for colorization (always SVG) lightPath := filepath.Join(config.LocalPath, "svg", iconName+"-light.svg") if fileExists(lightPath) { iconContent, err = readLocalFile(lightPath) @@ -265,12 +256,10 @@ func handleIcon(w http.ResponseWriter, r *http.Request) { } } } else { - // No color - try to serve standard format var standardPath string if formatToServe == "svg" { standardPath = filepath.Join(config.LocalPath, "svg", iconName+".svg") } else { - // For PNG/WebP, use format-specific directories 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 == "" { svgPath := filepath.Join(config.LocalPath, "svg", iconName+".svg") if fileExists(svgPath) { @@ -289,9 +277,7 @@ func handleIcon(w http.ResponseWriter, r *http.Request) { } } } else { - // Use remote CDN if colorCode != "" { - // Try to find -light version for colorization (always SVG) lightURL := config.JSDelivrURL + "/svg/" + iconName + "-light.svg" if urlExists(lightURL) { iconContent, err = fetchRemoteFile(lightURL) @@ -300,7 +286,6 @@ func handleIcon(w http.ResponseWriter, r *http.Request) { } } } else { - // No color - try to serve standard format var standardURL string if formatToServe == "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 == "" { svgURL := config.JSDelivrURL + "/svg/" + iconName + ".svg" iconContent, err = fetchRemoteFile(svgURL) @@ -330,7 +314,6 @@ func handleIcon(w http.ResponseWriter, r *http.Request) { return } - // Cache the result cache.Set(cacheKey, iconContent) 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)) } -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) { - vars := mux.Vars(r) - filename := vars["filename"] + filename := r.PathValue("filename") if filename == "" { http.Error(w, "Filename is required", http.StatusBadRequest) return } - // Build the path to the custom icon file customPath := filepath.Join("/app/icons/custom", filename) - // Debug logging log.Printf("[DEBUG] Looking for custom icon at: %s", customPath) - // Check if file exists if !fileExists(customPath) { - // List directory contents for debugging if files, err := os.ReadDir("/app/icons/custom"); err == nil { var fileList []string for _, file := range files { @@ -390,7 +351,6 @@ func handleCustomIcon(w http.ResponseWriter, r *http.Request) { return } - // Read the file data, err := os.ReadFile(customPath) if err != nil { 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 } - // Determine content type based on file extension ext := strings.ToLower(filepath.Ext(filename)) var contentType string switch ext { @@ -412,6 +371,8 @@ func handleCustomIcon(w http.ResponseWriter, r *http.Request) { contentType = "image/svg+xml" case ".webp": contentType = "image/webp" + case ".avif": + contentType = "image/avif" case ".ico": contentType = "image/x-icon" default: @@ -454,20 +415,14 @@ func main() { config = loadConfig() cache = NewCache(config.CacheTTL, config.CacheSize) - r := mux.NewRouter() - - // Custom icons route: /custom/filename - r.HandleFunc("/custom/{filename}", handleCustomIcon).Methods("GET") - - // Main route: /iconname or /iconname/colorcode - r.HandleFunc("/{iconname}", handleIcon).Methods("GET") - r.HandleFunc("/{iconname}/{colorcode}", handleIcon).Methods("GET") - - // Legacy route: /iconname.svg?color=colorcode - r.HandleFunc("/{iconname}.svg", handleLegacyIcon).Methods("GET") - - // Root endpoint - r.HandleFunc("/", handleRoot).Methods("GET") + mux := http.NewServeMux() + + mux.HandleFunc("GET /custom/{filename}", handleCustomIcon) + + mux.HandleFunc("GET /{iconname}/{colorcode}", handleIcon) + mux.HandleFunc("GET /{iconname}", handleIcon) + + mux.HandleFunc("GET /", handleRoot) log.Printf("Icon server listening on port %s", config.Port) 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.Fatal(http.ListenAndServe(":"+config.Port, r)) + log.Fatal(http.ListenAndServe(":"+config.Port, mux)) } \ No newline at end of file