154 lines
3.9 KiB
Go
154 lines
3.9 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"os"
|
||
"path"
|
||
"path/filepath"
|
||
"strings"
|
||
)
|
||
|
||
// Frontend (Vite build) als SPA ausliefern: Dateien aus dist, sonst index.html
|
||
func registerFrontend(mux *http.ServeMux) {
|
||
// Kandidaten: zuerst ENV, dann typische Ordner
|
||
candidates := []string{
|
||
strings.TrimSpace(os.Getenv("FRONTEND_DIST")),
|
||
"web/dist",
|
||
"dist",
|
||
}
|
||
|
||
var distAbs string
|
||
for _, c := range candidates {
|
||
if c == "" {
|
||
continue
|
||
}
|
||
abs, err := resolvePathRelativeToApp(c)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
if fi, err := os.Stat(filepath.Join(abs, "index.html")); err == nil && !fi.IsDir() {
|
||
distAbs = abs
|
||
break
|
||
}
|
||
}
|
||
|
||
if distAbs == "" {
|
||
fmt.Println("⚠️ Frontend dist nicht gefunden (tried: FRONTEND_DIST, frontend/dist, dist) – API läuft trotzdem.")
|
||
return
|
||
}
|
||
|
||
fmt.Println("🖼️ Frontend dist:", distAbs)
|
||
|
||
fileServer := http.FileServer(http.Dir(distAbs))
|
||
|
||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||
// /api bleibt bei deinen API-Routen (längeres Pattern gewinnt),
|
||
// aber falls mal was durchrutscht:
|
||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
|
||
// 1) Wenn echte Datei existiert -> ausliefern
|
||
reqPath := r.URL.Path
|
||
if reqPath == "" || reqPath == "/" {
|
||
// index.html
|
||
w.Header().Set("Cache-Control", "no-store")
|
||
http.ServeFile(w, r, filepath.Join(distAbs, "index.html"))
|
||
return
|
||
}
|
||
|
||
// URL-Pfad in Dateisystem-Pfad umwandeln (ohne Traversal)
|
||
clean := path.Clean("/" + reqPath) // path.Clean (für URL-Slashes)
|
||
rel := strings.TrimPrefix(clean, "/")
|
||
onDisk := filepath.Join(distAbs, filepath.FromSlash(rel))
|
||
|
||
if fi, err := os.Stat(onDisk); err == nil && !fi.IsDir() {
|
||
// Statische Assets ruhig cachen (Vite hashed assets)
|
||
ext := strings.ToLower(filepath.Ext(onDisk))
|
||
if ext != "" && ext != ".html" {
|
||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||
} else {
|
||
w.Header().Set("Cache-Control", "no-store")
|
||
}
|
||
fileServer.ServeHTTP(w, r)
|
||
return
|
||
}
|
||
|
||
// 2) SPA-Fallback: alle "Routen" ohne Datei -> index.html
|
||
w.Header().Set("Cache-Control", "no-store")
|
||
http.ServeFile(w, r, filepath.Join(distAbs, "index.html"))
|
||
})
|
||
}
|
||
|
||
func makeFrontendHandler() (http.Handler, bool) {
|
||
// Kandidaten: zuerst ENV, dann typische Ordner
|
||
candidates := []string{
|
||
strings.TrimSpace(os.Getenv("FRONTEND_DIST")),
|
||
"web/dist",
|
||
"dist",
|
||
}
|
||
|
||
var distAbs string
|
||
for _, c := range candidates {
|
||
if c == "" {
|
||
continue
|
||
}
|
||
abs, err := resolvePathRelativeToApp(c)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
if fi, err := os.Stat(filepath.Join(abs, "index.html")); err == nil && !fi.IsDir() {
|
||
distAbs = abs
|
||
break
|
||
}
|
||
}
|
||
|
||
if distAbs == "" {
|
||
fmt.Println("⚠️ Frontend dist nicht gefunden (tried: FRONTEND_DIST, web/dist, dist) – API läuft trotzdem.")
|
||
return nil, false
|
||
}
|
||
|
||
fmt.Println("🖼️ Frontend dist:", distAbs)
|
||
|
||
fileServer := http.FileServer(http.Dir(distAbs))
|
||
|
||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
// /api bleibt API
|
||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
|
||
reqPath := r.URL.Path
|
||
if reqPath == "" || reqPath == "/" {
|
||
w.Header().Set("Cache-Control", "no-store")
|
||
http.ServeFile(w, r, filepath.Join(distAbs, "index.html"))
|
||
return
|
||
}
|
||
|
||
// URL-Pfad in Dateisystem-Pfad umwandeln (ohne Traversal)
|
||
clean := path.Clean("/" + reqPath)
|
||
rel := strings.TrimPrefix(clean, "/")
|
||
onDisk := filepath.Join(distAbs, filepath.FromSlash(rel))
|
||
|
||
if fi, err := os.Stat(onDisk); err == nil && !fi.IsDir() {
|
||
ext := strings.ToLower(filepath.Ext(onDisk))
|
||
if ext != "" && ext != ".html" {
|
||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||
} else {
|
||
w.Header().Set("Cache-Control", "no-store")
|
||
}
|
||
fileServer.ServeHTTP(w, r)
|
||
return
|
||
}
|
||
|
||
// SPA-Fallback
|
||
w.Header().Set("Cache-Control", "no-store")
|
||
http.ServeFile(w, r, filepath.Join(distAbs, "index.html"))
|
||
})
|
||
|
||
return h, true
|
||
}
|