kra-new/internal/server/staticfiles/staticfiles.go

112 lines
3.2 KiB
Go

// Package staticfiles serves files from the configured local upload store.
// It owns the Gin/storage adapter so the server root only has to compose it.
package staticfiles
import (
"mime"
"net/http"
"os"
"path"
"strings"
"kra/internal/config"
"kra/internal/utils/uploadpolicy"
"github.com/gin-gonic/gin"
)
// Register adds local upload routes when local storage is enabled. A route is
// skipped when its root conflicts with an existing GET/HEAD route.
func Register(engine *gin.Engine, runtime *config.Store) {
config := localConfig(runtime)
if config == nil || strings.Trim(config.PathPrefix, "/") == "" {
return
}
prefix := "/" + strings.Trim(config.PathPrefix, "/")
if localStorageRouteConflicts(engine.Routes(), prefix) {
return
}
handler := func(c *gin.Context) {
if !serveAt(c, runtime, prefix) {
c.Status(http.StatusNotFound)
}
}
engine.GET(prefix+"/*filepath", handler)
engine.HEAD(prefix+"/*filepath", handler)
}
// Serve resolves the current runtime configuration on every request so a
// configuration reload takes effect without rebuilding the Gin engine.
func Serve(c *gin.Context, runtime *config.Store) bool {
config := localConfig(runtime)
if config == nil {
return false
}
prefix := "/" + strings.Trim(config.PathPrefix, "/")
return serveAt(c, runtime, prefix)
}
func localConfig(runtime *config.Store) *config.Local {
if runtime == nil {
return nil
}
snapshot := runtime.Snapshot()
if snapshot == nil || snapshot.Admin == nil || snapshot.Admin.Local == nil || snapshot.Admin.Local.StorePath == "" {
return nil
}
if snapshot.Admin.Storage != nil && snapshot.Admin.Storage.Type != "" && snapshot.Admin.Storage.Type != "local" {
return nil
}
return snapshot.Admin.Local
}
func localStorageRouteConflicts(routes []gin.RouteInfo, prefix string) bool {
staticRoot := strings.Split(strings.TrimPrefix(prefix, "/"), "/")[0]
for _, route := range routes {
if route.Method != http.MethodGet && route.Method != http.MethodHead {
continue
}
routeRoot := strings.Split(strings.TrimPrefix(route.Path, "/"), "/")[0]
if routeRoot == staticRoot {
return true
}
}
return false
}
func serveAt(c *gin.Context, runtime *config.Store, prefix string) bool {
config := localConfig(runtime)
if config == nil || "/"+strings.Trim(config.PathPrefix, "/") != prefix {
return false
}
if prefix == "/" || (c.Request.URL.Path != prefix && !strings.HasPrefix(c.Request.URL.Path, prefix+"/")) {
return false
}
c.Header("X-Content-Type-Options", "nosniff")
filename := path.Base(c.Request.URL.Path)
if !uploadpolicy.CanServeInline(filename) {
c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
}
http.StripPrefix(prefix, http.FileServer(filesOnly{FileSystem: http.Dir(config.StorePath)})).ServeHTTP(c.Writer, c.Request)
return true
}
type filesOnly struct{ http.FileSystem }
func (f filesOnly) Open(name string) (http.File, error) {
file, err := f.FileSystem.Open(name)
if err != nil {
return nil, err
}
info, statErr := file.Stat()
if statErr != nil {
_ = file.Close()
return nil, statErr
}
if info.IsDir() {
_ = file.Close()
return nil, os.ErrPermission
}
return file, nil
}