优化结构
This commit is contained in:
parent
b4d843290d
commit
7d89e2ea4d
|
|
@ -6,7 +6,9 @@ grouped by role:
|
||||||
- `handler/`: resource handlers and HTTP boundary validation
|
- `handler/`: resource handlers and HTTP boundary validation
|
||||||
- `middleware/`: request metadata, auth, access control, audit, recovery, CORS
|
- `middleware/`: request metadata, auth, access control, audit, recovery, CORS
|
||||||
- `router/`: resource route registration and the system route registrar
|
- `router/`: resource route registration and the system route registrar
|
||||||
- `httpx/`: system adapter for shared response and cookie helpers
|
- `pkg/httpx/`: transport-level response and cookie helpers shared by handlers
|
||||||
|
and middleware
|
||||||
|
- `staticfiles/`: local upload storage route registration and file serving
|
||||||
|
|
||||||
Cross-cutting route policy lives in `internal/routecatalog`: public/private
|
Cross-cutting route policy lives in `internal/routecatalog`: public/private
|
||||||
Swagger security, operation-audit flags, sensitive request-body handling and
|
Swagger security, operation-audit flags, sensitive request-body handling and
|
||||||
|
|
@ -15,3 +17,9 @@ keep the catalog aligned with the Gin registrations.
|
||||||
|
|
||||||
Keep new files in the matching role directory instead of adding transport files
|
Keep new files in the matching role directory instead of adding transport files
|
||||||
to the root package.
|
to the root package.
|
||||||
|
|
||||||
|
`middleware/` intentionally remains under `internal/server`: authentication,
|
||||||
|
access control, audit, rate limiting and CORS depend on application config and
|
||||||
|
services. Only dependency-free transport helpers belong in `pkg`; they should
|
||||||
|
not be moved to `internal/utils`, which is reserved for stateless application
|
||||||
|
helpers.
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"mime"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -14,6 +11,7 @@ import (
|
||||||
"kra/internal/server/handler"
|
"kra/internal/server/handler"
|
||||||
"kra/internal/server/middleware"
|
"kra/internal/server/middleware"
|
||||||
"kra/internal/server/router"
|
"kra/internal/server/router"
|
||||||
|
"kra/internal/server/staticfiles"
|
||||||
"kra/internal/service"
|
"kra/internal/service"
|
||||||
"kra/pkg/httpx"
|
"kra/pkg/httpx"
|
||||||
platformmodule "kra/pkg/module"
|
platformmodule "kra/pkg/module"
|
||||||
|
|
@ -70,7 +68,7 @@ func NewGinEngineWithRuntime(runtime *conf.Runtime, access *service.AccessContro
|
||||||
engine.GET(ws.Path(), handleWebSocket)
|
engine.GET(ws.Path(), handleWebSocket)
|
||||||
}
|
}
|
||||||
registerSwagger(engine, prefix, version, logger)
|
registerSwagger(engine, prefix, version, logger)
|
||||||
registerLocalStorage(engine, runtime)
|
staticfiles.Register(engine, runtime)
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
for _, route := range engine.Routes() {
|
for _, route := range engine.Routes() {
|
||||||
logger.Info("router registered", "method", route.Method, "path", route.Path)
|
logger.Info("router registered", "method", route.Method, "path", route.Path)
|
||||||
|
|
@ -83,7 +81,7 @@ func NewGinEngineWithRuntime(runtime *conf.Runtime, access *service.AccessContro
|
||||||
handleWebSocket(c)
|
handleWebSocket(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if serveLocalStorage(c, runtime) {
|
if staticfiles.Serve(c, runtime) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
httpx.Fail(c, "请求的接口不存在")
|
httpx.Fail(c, "请求的接口不存在")
|
||||||
|
|
@ -114,102 +112,3 @@ func NewGinServer(c *conf.Server, engine *gin.Engine) *kratoshttp.Server {
|
||||||
server.Server.MaxHeaderBytes = 1 << 20
|
server.Server.MaxHeaderBytes = 1 << 20
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveLocalStorage resolves the local path for every request so a config
|
|
||||||
// reload takes effect without rebuilding the Gin engine.
|
|
||||||
func serveLocalStorage(c *gin.Context, runtime *conf.Runtime) bool {
|
|
||||||
config := runtime.Admin()
|
|
||||||
if config == nil || config.Local == nil || config.Local.StorePath == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
prefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
|
|
||||||
return serveLocalStorageAt(c, runtime, prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerLocalStorage(engine *gin.Engine, runtime *conf.Runtime) {
|
|
||||||
config := runtime.Admin()
|
|
||||||
if config == nil || config.Local == nil || config.Local.StorePath == "" || strings.Trim(config.Local.PathPrefix, "/") == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
prefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
|
|
||||||
if localStorageRouteConflicts(engine.Routes(), prefix) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handler := func(c *gin.Context) {
|
|
||||||
if !serveLocalStorageAt(c, runtime, prefix) {
|
|
||||||
c.Status(http.StatusNotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
engine.GET(prefix+"/*filepath", handler)
|
|
||||||
engine.HEAD(prefix+"/*filepath", handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 serveLocalStorageAt(c *gin.Context, runtime *conf.Runtime, prefix string) bool {
|
|
||||||
config := runtime.Admin()
|
|
||||||
if config == nil || config.Local == nil || config.Local.StorePath == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if currentPrefix := "/" + strings.Trim(config.Local.PathPrefix, "/"); currentPrefix != 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 !canServeUploadInline(filename) {
|
|
||||||
c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
|
|
||||||
}
|
|
||||||
http.StripPrefix(prefix, http.FileServer(filesOnly{FileSystem: http.Dir(config.Local.StorePath)})).ServeHTTP(c.Writer, c.Request)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func canServeUploadInline(filename string) bool {
|
|
||||||
switch strings.ToLower(path.Ext(filename)) {
|
|
||||||
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".avif",
|
|
||||||
".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac",
|
|
||||||
".mp4", ".webm", ".mov", ".avi", ".mkv":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
// 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/conf"
|
||||||
|
|
||||||
|
"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 *conf.Runtime) {
|
||||||
|
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 *conf.Runtime) bool {
|
||||||
|
config := localConfig(runtime)
|
||||||
|
if config == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
prefix := "/" + strings.Trim(config.PathPrefix, "/")
|
||||||
|
return serveAt(c, runtime, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func localConfig(runtime *conf.Runtime) *conf.AdminBackend_Local {
|
||||||
|
if runtime == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
config := runtime.Admin()
|
||||||
|
if config == nil || config.Local == nil || config.Local.StorePath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return config.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 *conf.Runtime, 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 !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
|
||||||
|
}
|
||||||
|
|
||||||
|
func canServeInline(filename string) bool {
|
||||||
|
switch strings.ToLower(path.Ext(filename)) {
|
||||||
|
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".avif",
|
||||||
|
".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac",
|
||||||
|
".mp4", ".webm", ".mov", ".avi", ".mkv":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue