Files
godoxy-yusing/internal/route/fileserver.go
yusing 322bb70f02 feat(monitor): add display name support for health monitor logging
Add a DisplayNameKey struct to pass display names from routes through the task
parent hierarchy to the health monitor. This allows the health monitor to use
more descriptive names for logging instead of internal task names.

BREAKING CHANGE: The monitor.DisplayNameKey struct is now part of the public API
2026-02-13 22:12:19 +08:00

160 lines
3.6 KiB
Go

package route
import (
"errors"
"net/http"
"os"
"path"
"path/filepath"
"github.com/rs/zerolog/log"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/health/monitor"
"github.com/yusing/godoxy/internal/logging/accesslog"
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/task"
)
type (
FileServer struct {
*Route
middleware *middleware.Middleware
handler http.Handler
accessLogger accesslog.AccessLogger
}
)
var _ types.FileServerRoute = (*FileServer)(nil)
func handler(root string, spa bool, index string) http.Handler {
if !spa {
return http.FileServer(http.Dir(root))
}
indexPath := filepath.Join(root, index)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlPath := path.Clean(r.URL.Path)
if urlPath == "/" {
http.ServeFile(w, r, indexPath)
return
}
fullPath := filepath.Join(root, filepath.FromSlash(urlPath))
stat, err := os.Stat(fullPath)
if err == nil && !stat.IsDir() {
http.ServeFile(w, r, fullPath)
return
}
http.ServeFile(w, r, indexPath)
})
}
func NewFileServer(base *Route) (*FileServer, error) {
s := &FileServer{Route: base}
s.Root = filepath.Clean(s.Root)
if !path.IsAbs(s.Root) {
return nil, errors.New("`root` must be an absolute path")
}
if s.Index == "" {
s.Index = "/index.html"
} else if s.Index[0] != '/' {
s.Index = "/" + s.Index
}
s.handler = handler(s.Root, s.SPA, s.Index)
if len(s.Middlewares) > 0 {
mid, err := middleware.BuildMiddlewareFromMap(s.Alias, s.Middlewares)
if err != nil {
return nil, err
}
s.middleware = mid
}
return s, nil
}
// Start implements task.TaskStarter.
func (s *FileServer) Start(parent task.Parent) error {
s.task = parent.Subtask("fileserver."+s.Name(), false)
s.task.SetValue(monitor.DisplayNameKey{}, s.DisplayName())
pathPatterns := s.PathPatterns
switch {
case len(pathPatterns) == 0:
case len(pathPatterns) == 1 && pathPatterns[0] == "/":
default:
mux := gphttp.NewServeMux()
patErrs := gperr.NewBuilder("invalid path pattern(s)")
for _, p := range pathPatterns {
patErrs.Add(mux.Handle(p, s.handler))
}
if err := patErrs.Error(); err != nil {
s.task.Finish(err)
return err
}
s.handler = mux
}
if s.middleware != nil {
next := s.handler
s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.middleware.ServeHTTP(next.ServeHTTP, w, r)
})
}
if s.UseAccessLog() {
var err error
s.accessLogger, err = accesslog.NewAccessLogger(s.task, s.AccessLog)
if err != nil {
s.task.Finish(err)
return err
}
}
if len(s.Rules) > 0 {
s.handler = s.Rules.BuildHandler(s.handler.ServeHTTP)
}
if s.UseHealthCheck() {
s.HealthMon = monitor.NewMonitor(s)
if err := s.HealthMon.Start(s.task); err != nil {
log.Warn().EmbedObject(s).Err(err).Msg("health monitor error")
s.HealthMon = nil
}
}
ep := entrypoint.FromCtx(parent.Context())
if ep == nil {
err := errors.New("entrypoint not initialized")
s.task.Finish(err)
return err
}
if err := ep.StartAddRoute(s); err != nil {
s.task.Finish(err)
return err
}
return nil
}
func (s *FileServer) RootPath() string {
return s.Root
}
// ServeHTTP implements http.Handler.
func (s *FileServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if s.accessLogger != nil {
rec := accesslog.GetResponseRecorder(w)
w = rec
defer func() {
s.accessLogger.LogRequest(req, rec.Response())
accesslog.PutResponseRecorder(rec)
}()
}
s.handler.ServeHTTP(w, req)
}