chore: import clean Agent Desk snapshot (2/7)

This commit is contained in:
2026-10-16 11:57:21 +00:00
parent 79176541c7
commit 89c5231c4c
461 changed files with 60092 additions and 0 deletions

188
internal/pkg/ginx/static.go Normal file
View 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)
}

View File

@@ -0,0 +1,218 @@
package ginx
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"testing/fstest"
"github.com/gin-gonic/gin"
)
func TestDirHandlerWithSPA(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "index.html"), []byte("<html>spa</html>"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "manifest"), []byte("manifest content"), 0o644); err != nil {
t.Fatal(err)
}
tests := []struct {
path string
wantStatus int
wantBody string
}{
{path: "/assets/not-exists.css", wantStatus: http.StatusOK, wantBody: "<html>spa</html>"},
{path: "/images/not-exists.png", wantStatus: http.StatusOK, wantBody: "<html>spa</html>"},
{path: "/fonts/not-exists", wantStatus: http.StatusOK, wantBody: "<html>spa</html>"},
{path: "/manifest", wantStatus: http.StatusOK, wantBody: "manifest content"},
{path: "/support", wantStatus: http.StatusOK, wantBody: "<html>spa</html>"},
}
handler := DirHandler(http.Dir(root), DirOptions{
ShowList: false,
SPA: true,
IndexName: "index.html",
})
for _, tt := range tests {
rec := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(rec)
ctx.Request = httptest.NewRequest(http.MethodGet, tt.path, nil)
handler(ctx)
if rec.Code != tt.wantStatus {
t.Fatalf("%s status=%d want %d; body=%q", tt.path, rec.Code, tt.wantStatus, rec.Body.String())
}
if tt.wantBody != "" && strings.TrimSpace(rec.Body.String()) != tt.wantBody {
t.Fatalf("%s body=%q want %q", tt.path, strings.TrimSpace(rec.Body.String()), tt.wantBody)
}
}
}
func TestStaticFilesDoesNotOpenDirectories(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "file.txt"), []byte("content"), 0o644); err != nil {
t.Fatal(err)
}
fileSystem := StaticFiles(root)
if file, err := fileSystem.Open("/file.txt"); err != nil {
t.Fatalf("Open(file.txt) err=%v", err)
} else {
_ = file.Close()
}
if file, err := fileSystem.Open("/"); err == nil {
_ = file.Close()
t.Fatal("Open(/) succeeded, want error")
} else if !os.IsNotExist(err) {
t.Fatalf("Open(/) err=%v, want not exist", err)
}
}
func TestHandleSPAKeepsNotFoundPrefixes(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "index.html"), []byte("<html>spa</html>"), 0o644); err != nil {
t.Fatal(err)
}
engine := gin.New()
HandleSPA(engine, SPAOptions{
Root: root,
DirOptions: DirOptions{
ShowList: false,
SPA: true,
IndexName: "index.html",
},
NotFoundPrefixes: []string{"/api/"},
})
tests := []struct {
path string
wantStatus int
wantBody string
}{
{path: "/", wantStatus: http.StatusOK, wantBody: "<html>spa</html>"},
{path: "/support", wantStatus: http.StatusOK, wantBody: "<html>spa</html>"},
{path: "/api/not-exists", wantStatus: http.StatusNotFound},
}
for _, tt := range tests {
rec := httptest.NewRecorder()
engine.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, tt.path, nil))
if rec.Code != tt.wantStatus {
t.Fatalf("%s status=%d want %d", tt.path, rec.Code, tt.wantStatus)
}
if tt.wantBody != "" && strings.TrimSpace(rec.Body.String()) != tt.wantBody {
t.Fatalf("%s body=%q want %q", tt.path, strings.TrimSpace(rec.Body.String()), tt.wantBody)
}
}
}
func TestHandleSPARootRedirect(t *testing.T) {
// Mirrors the real export: no index.html at the root, only the exported
// route shells.
engine := gin.New()
HandleSPA(engine, SPAOptions{
EmbeddedFS: fstest.MapFS{
"dashboard.html": {Data: []byte("<html>dashboard</html>")},
"support.html": {Data: []byte("<html>support</html>")},
},
EmbeddedRoot: ".",
RootRedirect: "/support",
DirOptions: DirOptions{ShowList: false, SPA: true, IndexName: "index.html"},
NotFoundPrefixes: []string{"/api/"},
})
t.Run("root redirects", func(t *testing.T) {
for _, method := range []string{http.MethodGet, http.MethodHead} {
rec := httptest.NewRecorder()
engine.ServeHTTP(rec, httptest.NewRequest(method, "/", nil))
if rec.Code != http.StatusFound {
t.Fatalf("%s / status=%d want %d", method, rec.Code, http.StatusFound)
}
if location := rec.Header().Get("Location"); location != "/support" {
t.Fatalf("%s / Location=%q want %q", method, location, "/support")
}
}
})
t.Run("other routes still serve the SPA", func(t *testing.T) {
rec := httptest.NewRecorder()
engine.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/support", nil))
if rec.Code != http.StatusOK {
t.Fatalf("/support status=%d want %d", rec.Code, http.StatusOK)
}
if body := strings.TrimSpace(rec.Body.String()); body != "<html>support</html>" {
t.Fatalf("/support body=%q", body)
}
})
t.Run("api 404 is unaffected", func(t *testing.T) {
rec := httptest.NewRecorder()
engine.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/not-exists", nil))
if rec.Code != http.StatusNotFound {
t.Fatalf("/api/not-exists status=%d want %d", rec.Code, http.StatusNotFound)
}
})
}
func TestDirHandlerWithNextExportPages(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "index.html"), []byte("<html>home</html>"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "dashboard.html"), []byte("<html>dashboard</html>"), 0o644); err != nil {
t.Fatal(err)
}
dashboardDir := filepath.Join(root, "dashboard")
if err := os.MkdirAll(dashboardDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dashboardDir, "login.html"), []byte("<html>login</html>"), 0o644); err != nil {
t.Fatal(err)
}
handler := DirHandler(http.Dir(root), DirOptions{
ShowList: false,
SPA: true,
IndexName: "index.html",
})
tests := []struct {
path string
wantStatus int
wantBody string
}{
{path: "/", wantStatus: http.StatusOK, wantBody: "<html>home</html>"},
{path: "/dashboard", wantStatus: http.StatusOK, wantBody: "<html>dashboard</html>"},
{path: "/dashboard/", wantStatus: http.StatusOK, wantBody: "<html>dashboard</html>"},
{path: "/dashboard/login", wantStatus: http.StatusOK, wantBody: "<html>login</html>"},
{path: "/dashboard/login/", wantStatus: http.StatusOK, wantBody: "<html>login</html>"},
{path: "/other-spa-route", wantStatus: http.StatusOK, wantBody: "<html>home</html>"},
}
for _, tt := range tests {
rec := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(rec)
ctx.Request = httptest.NewRequest(http.MethodGet, tt.path, nil)
handler(ctx)
if rec.Code != tt.wantStatus {
t.Fatalf("%s status=%d want %d", tt.path, rec.Code, tt.wantStatus)
}
if tt.wantBody != "" && strings.TrimSpace(rec.Body.String()) != tt.wantBody {
t.Fatalf("%s body=%q want %q", tt.path, strings.TrimSpace(rec.Body.String()), tt.wantBody)
}
}
}