chore: import clean Agent Desk snapshot (2/7)
This commit is contained in:
188
internal/pkg/ginx/static.go
Normal file
188
internal/pkg/ginx/static.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package ginx
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type noDirFS struct {
|
||||
fs http.FileSystem
|
||||
}
|
||||
|
||||
type DirOptions struct {
|
||||
ShowList bool
|
||||
SPA bool
|
||||
IndexName string
|
||||
}
|
||||
|
||||
type SPAOptions struct {
|
||||
Root string
|
||||
EmbeddedFS fs.FS
|
||||
EmbeddedRoot string
|
||||
DirOptions DirOptions
|
||||
NotFoundPrefixes []string
|
||||
NotFoundHandler gin.HandlerFunc
|
||||
// RootRedirect, when set, answers "/" with a redirect instead of serving
|
||||
// the SPA. The static export has no index.html at the root, so without this
|
||||
// the site origin answers 404.
|
||||
RootRedirect string
|
||||
}
|
||||
|
||||
func StaticFiles(root string) http.FileSystem {
|
||||
return noDirFS{fs: http.Dir(root)}
|
||||
}
|
||||
|
||||
func (n noDirFS) Open(name string) (http.File, error) {
|
||||
file, err := n.fs.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
_ = file.Close()
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func HandleSPA(engine *gin.Engine, options SPAOptions) gin.HandlerFunc {
|
||||
handler := NewSPAHandler(options.Root, options.EmbeddedFS, options.EmbeddedRoot, options.DirOptions)
|
||||
rootHandler := handler
|
||||
if options.RootRedirect != "" {
|
||||
target := options.RootRedirect
|
||||
rootHandler = func(ctx *gin.Context) {
|
||||
ctx.Redirect(http.StatusFound, target)
|
||||
}
|
||||
}
|
||||
engine.GET("/", rootHandler)
|
||||
engine.HEAD("/", rootHandler)
|
||||
engine.NoRoute(func(ctx *gin.Context) {
|
||||
for _, prefix := range options.NotFoundPrefixes {
|
||||
if strings.HasPrefix(ctx.Request.URL.Path, prefix) {
|
||||
if options.NotFoundHandler != nil {
|
||||
options.NotFoundHandler(ctx)
|
||||
return
|
||||
}
|
||||
ctx.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
handler(ctx)
|
||||
})
|
||||
return handler
|
||||
}
|
||||
|
||||
func NewSPAHandler(root string, embeddedFS fs.FS, embeddedRoot string, options DirOptions) gin.HandlerFunc {
|
||||
if _, err := os.Stat(path.Join(root, options.IndexName)); err == nil {
|
||||
return DirHandler(http.Dir(root), options)
|
||||
}
|
||||
|
||||
spaFS, err := fs.Sub(embeddedFS, embeddedRoot)
|
||||
if err != nil {
|
||||
slog.Error("failed to load embedded SPA files", slog.Any("err", err))
|
||||
return func(ctx *gin.Context) {
|
||||
ctx.AbortWithStatus(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
return DirHandler(http.FS(spaFS), options)
|
||||
}
|
||||
|
||||
func tryOpenFile(fileSystem http.FileSystem, filePath string) (http.File, os.FileInfo, bool) {
|
||||
file, err := fileSystem.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil || info.IsDir() {
|
||||
_ = file.Close()
|
||||
return nil, nil, false
|
||||
}
|
||||
return file, info, true
|
||||
}
|
||||
|
||||
func DirHandler(fileSystem http.FileSystem, options DirOptions) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
name := path.Clean(ctx.Request.URL.Path)
|
||||
if name == "." || name == "/" {
|
||||
name = "/" + options.IndexName
|
||||
}
|
||||
|
||||
file, err := fileSystem.Open(name)
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
ctx.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if info.IsDir() {
|
||||
if options.IndexName != "" {
|
||||
indexName := path.Join(name, options.IndexName)
|
||||
if indexFile, indexInfo, ok := tryOpenFile(fileSystem, indexName); ok {
|
||||
defer indexFile.Close()
|
||||
serveFile(ctx, indexName, indexFile, indexInfo)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Support Next.js static export where directory name matches page html (e.g. /dashboard -> /dashboard.html)
|
||||
htmlName := strings.TrimRight(name, "/") + ".html"
|
||||
if htmlFile, htmlInfo, ok := tryOpenFile(fileSystem, htmlName); ok {
|
||||
defer htmlFile.Close()
|
||||
serveFile(ctx, htmlName, htmlFile, htmlInfo)
|
||||
return
|
||||
}
|
||||
if !options.ShowList {
|
||||
if options.SPA && options.IndexName != "" {
|
||||
indexName := "/" + options.IndexName
|
||||
if indexFile, indexInfo, ok := tryOpenFile(fileSystem, indexName); ok {
|
||||
defer indexFile.Close()
|
||||
serveFile(ctx, indexName, indexFile, indexInfo)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
serveFile(ctx, name, file, info)
|
||||
return
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
ctx.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Try name.html (for sub-routes like /dashboard/login -> /dashboard/login.html)
|
||||
htmlName := strings.TrimRight(name, "/") + ".html"
|
||||
if htmlFile, htmlInfo, ok := tryOpenFile(fileSystem, htmlName); ok {
|
||||
defer htmlFile.Close()
|
||||
serveFile(ctx, htmlName, htmlFile, htmlInfo)
|
||||
return
|
||||
}
|
||||
|
||||
if options.SPA && options.IndexName != "" {
|
||||
indexName := "/" + options.IndexName
|
||||
if indexFile, indexInfo, ok := tryOpenFile(fileSystem, indexName); ok {
|
||||
defer indexFile.Close()
|
||||
serveFile(ctx, indexName, indexFile, indexInfo)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.AbortWithStatus(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func serveFile(ctx *gin.Context, name string, file http.File, info os.FileInfo) {
|
||||
http.ServeContent(ctx.Writer, ctx.Request, name, info.ModTime(), file)
|
||||
}
|
||||
Reference in New Issue
Block a user