package api import ( "io/fs" "mime" "net/http" "os" "path/filepath" "strings" ) // defaultFavicon is served when no favicon_path is configured. var defaultFavicon []byte // SetDefaultFavicon sets the fallback favicon bytes (called at startup from // the embedded web build so the binary doesn't embed a second copy). func SetDefaultFavicon(data []byte) { defaultFavicon = data } // InitDefaultFavicon reads the favicon from the embedded web build so it can // be served as the fallback for /favicon without an extra embedded copy. func InitDefaultFavicon(webFS fs.FS) error { sub, err := fs.Sub(webFS, "web/build") if err != nil { return nil } data, err := fs.ReadFile(sub, "favicon.svg") if err != nil { return nil } SetDefaultFavicon(data) return nil } func (a *API) HandleFavicon(w http.ResponseWriter, r *http.Request) { if a.Cfg.Server.FaviconPath != "" { serveFile(w, r, a.Cfg.Server.FaviconPath) return } if len(defaultFavicon) > 0 { w.Header().Set("Content-Type", "image/svg+xml") w.Header().Set("Cache-Control", "public, max-age=86400") w.Write(defaultFavicon) return } http.NotFound(w, r) } func (a *API) HandleLogo(w http.ResponseWriter, r *http.Request) { if a.Cfg.Server.LogoPath != "" { serveFile(w, r, a.Cfg.Server.LogoPath) return } http.NotFound(w, r) } func serveFile(w http.ResponseWriter, r *http.Request, path string) { ext := strings.ToLower(filepath.Ext(path)) mimeType := mime.TypeByExtension(ext) if mimeType == "" { switch ext { case ".svg": mimeType = "image/svg+xml" case ".ico": mimeType = "image/x-icon" default: mimeType = "application/octet-stream" } } f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { http.NotFound(w, r) } else { http.Error(w, "internal error", http.StatusInternalServerError) } return } defer f.Close() stat, err := f.Stat() if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", mimeType) w.Header().Set("Cache-Control", "public, max-age=86400") http.ServeContent(w, r, filepath.Base(path), stat.ModTime(), f) }