166 lines
3.8 KiB
Go
166 lines
3.8 KiB
Go
|
|
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
|
||
|
|
}
|
||
|
|
|
||
|
|
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)
|
||
|
|
engine.GET("/", handler)
|
||
|
|
engine.HEAD("/", handler)
|
||
|
|
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 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)
|
||
|
|
indexFile, err := fileSystem.Open(indexName)
|
||
|
|
if err == nil {
|
||
|
|
defer indexFile.Close()
|
||
|
|
indexInfo, statErr := indexFile.Stat()
|
||
|
|
if statErr != nil {
|
||
|
|
ctx.AbortWithStatus(http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if !indexInfo.IsDir() {
|
||
|
|
serveFile(ctx, indexName, indexFile, indexInfo)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if err != nil && !os.IsNotExist(err) {
|
||
|
|
ctx.AbortWithStatus(http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !options.ShowList {
|
||
|
|
ctx.AbortWithStatus(http.StatusNotFound)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
serveFile(ctx, name, file, info)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
} else if !os.IsNotExist(err) {
|
||
|
|
ctx.AbortWithStatus(http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if options.SPA && options.IndexName != "" {
|
||
|
|
indexName := "/" + options.IndexName
|
||
|
|
indexFile, err := fileSystem.Open(indexName)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
ctx.AbortWithStatus(http.StatusNotFound)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ctx.AbortWithStatus(http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer indexFile.Close()
|
||
|
|
indexInfo, err := indexFile.Stat()
|
||
|
|
if err != nil {
|
||
|
|
ctx.AbortWithStatus(http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
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)
|
||
|
|
}
|