package routers import ( "errors" "io/fs" "os" "path/filepath" "strings" "time" "git.sharkk.net/Sky/Moonshark/core/logger" "github.com/valyala/fasthttp" ) // StaticRouter is a simplified router for static files using FastHTTP's built-in capabilities type StaticRouter struct { fs *fasthttp.FS fsHandler fasthttp.RequestHandler urlPrefix string rootDir string log bool // Additional compression options useBrotli bool useZstd bool } // NewStaticRouter creates a new StaticRouter instance func NewStaticRouter(rootDir string) (*StaticRouter, error) { // Verify root directory exists info, err := os.Stat(rootDir) if err != nil { return nil, err } if !info.IsDir() { return nil, errors.New("root path is not a directory") } // Create the FS handler with optimized settings fs := &fasthttp.FS{ Root: rootDir, IndexNames: []string{"index.html"}, GenerateIndexPages: false, AcceptByteRange: true, Compress: true, CacheDuration: 24 * time.Hour, CompressedFileSuffix: ".gz", CompressBrotli: true, CompressZstd: true, CompressedFileSuffixes: map[string]string{ "gzip": ".gz", "br": ".br", "zstd": ".zst", }, } r := &StaticRouter{ fs: fs, urlPrefix: "/static", // Default prefix rootDir: rootDir, log: false, useBrotli: true, useZstd: true, } // Set up the path rewrite based on the prefix r.updatePathRewrite() return r, nil } // WithEmbeddedFS sets an embedded filesystem instead of using the rootDir func (r *StaticRouter) WithEmbeddedFS(embedded fs.FS) *StaticRouter { r.fs.FS = embedded r.fsHandler = r.fs.NewRequestHandler() return r } // SetCompression configures the compression options func (r *StaticRouter) SetCompression(useGzip, useBrotli, useZstd bool) { r.fs.Compress = useGzip r.fs.CompressBrotli = useBrotli r.fs.CompressZstd = useZstd r.useBrotli = useBrotli r.useZstd = useZstd // Update handler to reflect changes r.fsHandler = r.fs.NewRequestHandler() } // SetCacheDuration sets the cache duration for HTTP headers func (r *StaticRouter) SetCacheDuration(duration time.Duration) { r.fs.CacheDuration = duration r.fsHandler = r.fs.NewRequestHandler() } // SetURLPrefix sets the URL prefix for static assets func (r *StaticRouter) SetURLPrefix(prefix string) { if !strings.HasPrefix(prefix, "/") { prefix = "/" + prefix } r.urlPrefix = prefix r.updatePathRewrite() } // updatePathRewrite updates the path rewriter based on the current prefix func (r *StaticRouter) updatePathRewrite() { r.fs.PathRewrite = fasthttp.NewPathPrefixStripper(len(r.urlPrefix)) r.fsHandler = r.fs.NewRequestHandler() } // EnableDebugLog enables debug logging func (r *StaticRouter) EnableDebugLog() { r.log = true } // DisableDebugLog disables debug logging func (r *StaticRouter) DisableDebugLog() { r.log = false } // ServeHTTP implements the http.Handler interface through fasthttpadaptor func (r *StaticRouter) ServeHTTP(ctx *fasthttp.RequestCtx) { path := string(ctx.Path()) // Check if path starts with the prefix if !strings.HasPrefix(path, r.urlPrefix) { ctx.NotFound() return } if r.log { logger.Debug("[StaticRouter] Serving: %s", path) } // Handle the request with the FS handler r.fsHandler(ctx) } // Match finds a file path for the given URL path func (r *StaticRouter) Match(urlPath string) (string, bool) { // Check if path starts with the prefix if !strings.HasPrefix(urlPath, r.urlPrefix) { return "", false } // Strip prefix urlPath = strings.TrimPrefix(urlPath, r.urlPrefix) // Make sure path starts with / if !strings.HasPrefix(urlPath, "/") { urlPath = "/" + urlPath } filePath := filepath.Join(r.rootDir, filepath.FromSlash(strings.TrimPrefix(urlPath, "/"))) _, err := os.Stat(filePath) if r.log && err == nil { logger.Debug("[StaticRouter] MATCH: %s -> %s", urlPath, filePath) } return filePath, err == nil } // Refresh is a no-op in this implementation as there's no cache to refresh func (r *StaticRouter) Refresh() error { // No cache to refresh in this implementation return nil } // GetStats returns basic stats about the router func (r *StaticRouter) GetStats() map[string]any { return map[string]any{ "type": "StaticRouter", "rootDir": r.rootDir, "urlPrefix": r.urlPrefix, "useBrotli": r.useBrotli, "useZstd": r.useZstd, "cacheTime": r.fs.CacheDuration.String(), } } // SetMaxItems is kept for API compatibility with the old router func (r *StaticRouter) SetMaxItems(n int) { // No-op in this implementation } // SetMaxItemSize is kept for API compatibility with the old router func (r *StaticRouter) SetMaxItemSize(n int) { // No-op in this implementation } // SetTotalCapacity is kept for API compatibility with the old router func (r *StaticRouter) SetTotalCapacity(n int) { // No-op in this implementation } // PreloadCommonFiles is kept for API compatibility but is a no-op // as FastHTTP doesn't have built-in preloading func (r *StaticRouter) PreloadCommonFiles() { // No preloading in this implementation if r.log { logger.Debug("[StaticRouter] PreloadCommonFiles is a no-op in StaticRouter") } }