nidus-sync/html/fileserver.go

148 lines
3.7 KiB
Go
Raw Normal View History

package html
2025-11-06 22:31:51 +00:00
import (
"embed"
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/static"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
)
// fileServer conveniently sets up a http.FileServer handler to serve
// static files from a http.FileSystem.
var startedTime time.Time = time.Now()
var localFS http.Dir
func AddStaticRoute(r chi.Router, path string) {
if localFS == "" {
localFS = http.Dir("./static")
}
fileServer(r, "/static", localFS, static.EmbeddedStaticFS, "static")
}
func fileServer(r chi.Router, path string, root http.FileSystem, embeddedFS embed.FS, embeddedPath string) {
2025-11-06 22:31:51 +00:00
if strings.ContainsAny(path, "{}*") {
panic("FileServer does not permit any URL parameters.")
}
2025-11-06 22:31:51 +00:00
if path != "/" && path[len(path)-1] != '/' {
r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
path += "/"
}
path += "*"
2025-11-06 22:31:51 +00:00
r.Get(path, func(w http.ResponseWriter, r *http.Request) {
rctx := chi.RouteContext(r.Context())
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
2025-11-06 22:31:51 +00:00
// Determine the actual file path
requestedPath := strings.TrimPrefix(r.URL.Path, pathPrefix)
var err error
2025-11-06 22:31:51 +00:00
var fileToServe http.File
found := false
// For dev, try the current filesystem
if !config.IsProductionEnvironment() {
// Try to open from local filesystem for development
fileToServe, err = root.Open(requestedPath)
if err != nil {
log.Warn().Str("path", requestedPath).Msg("Failed to read static file for dev")
found = false
} else {
found = true
}
}
// For production use the embedded filesystem
if !found {
2025-11-06 22:31:51 +00:00
embeddedFilePath := filepath.Join(embeddedPath, requestedPath)
embeddedFile, err := embeddedFS.Open(embeddedFilePath)
2025-11-06 22:31:51 +00:00
if err != nil {
http.NotFound(w, r)
return
}
2025-11-06 22:31:51 +00:00
// Wrap the embedded file to implement http.File interface
fileToServe = &embeddedFileWrapper{embeddedFile}
}
2025-11-06 22:31:51 +00:00
// Create a custom ResponseWriter that allows us to modify headers
crw := &customResponseWriter{ResponseWriter: w}
// Add caching headers
if config.IsProductionEnvironment() {
ext := filepath.Ext(requestedPath)
switch ext {
case ".css", ".js", ".jpg", ".jpeg", ".png", ".gif", ".svg", ".woff", ".woff2", ".ttf":
// Cache for 1 week (604800 seconds)
crw.Header().Set("Cache-Control", "public, max-age=604800, stale-while-revalidate=86400")
default:
// Other files, 1 hour
crw.Header().Set("Cache-Control", "public, max-age=3600")
}
}
2025-11-06 22:31:51 +00:00
// Serve the file
http.ServeContent(crw, r, requestedPath, startedTime, fileToServe)
2025-11-06 22:31:51 +00:00
// Close the file
fileToServe.Close()
})
}
2025-11-06 22:31:51 +00:00
type embeddedFileWrapper struct {
2025-11-06 22:31:51 +00:00
file fs.File
}
func (e *embeddedFileWrapper) Close() error {
2025-11-06 22:31:51 +00:00
return e.file.Close()
}
func (e *embeddedFileWrapper) Read(p []byte) (n int, err error) {
2025-11-06 22:31:51 +00:00
return e.file.Read(p)
}
type Seeker interface {
2025-11-06 22:31:51 +00:00
Seek(offset int64, whence int) (int64, error)
}
func (e *embeddedFileWrapper) Seek(offset int64, whence int) (int64, error) {
2025-11-06 22:31:51 +00:00
if seeker, ok := e.file.(Seeker); ok {
return seeker.Seek(offset, whence)
}
return 0, fmt.Errorf("Seek not supported")
}
func (e *embeddedFileWrapper) Readdir(count int) ([]os.FileInfo, error) {
2025-11-06 22:31:51 +00:00
// This is a bit tricky with embedded files
if dirFile, ok := e.file.(fs.ReadDirFile); ok {
entries, err := dirFile.ReadDir(count)
if err != nil {
return nil, err
}
fileInfos := make([]os.FileInfo, len(entries))
for i, entry := range entries {
fileInfos[i], err = entry.Info()
if err != nil {
return nil, err
}
}
return fileInfos, nil
}
return nil, fmt.Errorf("Readdir not supported")
}
func (e *embeddedFileWrapper) Stat() (os.FileInfo, error) {
2025-11-06 22:31:51 +00:00
return e.file.Stat()
}