hyper op 2
This commit is contained in:
parent
9d326eaec1
commit
23239b00fc
10
Moonshark.go
10
Moonshark.go
@ -217,16 +217,6 @@ func (s *Moonshark) setupWatchers() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up watcher for static files
|
|
||||||
if s.Config.Watchers.Static {
|
|
||||||
staticWatcher, err := watchers.WatchStaticRouter(s.StaticRouter, s.Config.StaticDir)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("Failed to watch static directory: %v", err)
|
|
||||||
} else {
|
|
||||||
s.cleanupFuncs = append(s.cleanupFuncs, staticWatcher.Close)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up watchers for Lua modules libraries
|
// Set up watchers for Lua modules libraries
|
||||||
if s.Config.Watchers.Modules && len(s.Config.LibDirs) > 0 {
|
if s.Config.Watchers.Modules && len(s.Config.LibDirs) > 0 {
|
||||||
moduleWatchers, err := watchers.WatchLuaModules(s.LuaRunner, s.Config.LibDirs)
|
moduleWatchers, err := watchers.WatchLuaModules(s.LuaRunner, s.Config.LibDirs)
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"git.sharkk.net/Sky/Moonshark/core/config"
|
"git.sharkk.net/Sky/Moonshark/core/config"
|
||||||
"git.sharkk.net/Sky/Moonshark/core/logger"
|
"git.sharkk.net/Sky/Moonshark/core/logger"
|
||||||
|
"git.sharkk.net/Sky/Moonshark/core/metadata"
|
||||||
"git.sharkk.net/Sky/Moonshark/core/routers"
|
"git.sharkk.net/Sky/Moonshark/core/routers"
|
||||||
"git.sharkk.net/Sky/Moonshark/core/runner"
|
"git.sharkk.net/Sky/Moonshark/core/runner"
|
||||||
"git.sharkk.net/Sky/Moonshark/core/utils"
|
"git.sharkk.net/Sky/Moonshark/core/utils"
|
||||||
@ -47,7 +48,7 @@ func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter, runne
|
|||||||
// Configure fasthttp server
|
// Configure fasthttp server
|
||||||
server.fasthttpServer = &fasthttp.Server{
|
server.fasthttpServer = &fasthttp.Server{
|
||||||
Handler: server.handleRequest,
|
Handler: server.handleRequest,
|
||||||
Name: "Moonshark",
|
Name: "Moonshark/" + metadata.Version,
|
||||||
ReadTimeout: 30 * time.Second,
|
ReadTimeout: 30 * time.Second,
|
||||||
WriteTimeout: 30 * time.Second,
|
WriteTimeout: 30 * time.Second,
|
||||||
MaxRequestBodySize: 16 << 20, // 16MB - consistent with Forms.go
|
MaxRequestBodySize: 16 << 20, // 16MB - consistent with Forms.go
|
||||||
@ -137,8 +138,8 @@ func (s *Server) processRequest(ctx *fasthttp.RequestCtx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Then try static files
|
// Then try static files
|
||||||
if filePath, found := s.staticRouter.Match(path); found {
|
if _, found := s.staticRouter.Match(path); found {
|
||||||
ctx.SendFile(filePath)
|
s.staticRouter.ServeHTTP(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,52 +1,30 @@
|
|||||||
package routers
|
package routers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
"container/list"
|
|
||||||
"errors"
|
"errors"
|
||||||
"mime"
|
"io/fs"
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sharkk.net/Sky/Moonshark/core/logger"
|
"git.sharkk.net/Sky/Moonshark/core/logger"
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CacheEntry represents a cached file with metadata
|
// StaticRouter is a simplified router for static files using FastHTTP's built-in capabilities
|
||||||
type CacheEntry struct {
|
|
||||||
Path string // URL path
|
|
||||||
GzippedContent []byte // Gzipped content
|
|
||||||
Size int // Original size
|
|
||||||
GzippedSize int // Compressed size
|
|
||||||
ModTime int64 // Modification time
|
|
||||||
ContentType string // Content type
|
|
||||||
}
|
|
||||||
|
|
||||||
// StaticRouter is a caching router for static files
|
|
||||||
// It provides an LRU cache for gzipped static assets
|
|
||||||
type StaticRouter struct {
|
type StaticRouter struct {
|
||||||
rootDir string // Root directory containing files
|
fs *fasthttp.FS
|
||||||
cache map[string]*list.Element // Cache map (URL path -> list element)
|
fsHandler fasthttp.RequestHandler
|
||||||
lruList *list.List // LRU tracking list
|
urlPrefix string
|
||||||
mu sync.RWMutex // Lock for concurrent access
|
rootDir string
|
||||||
maxItems int // Maximum number of items in cache
|
log bool
|
||||||
maxItemSize int // Maximum size per item (gzipped)
|
// Additional compression options
|
||||||
totalCapacity int // Total cache capacity in bytes
|
useBrotli bool
|
||||||
currentSize int // Current cache size in bytes
|
useZstd bool
|
||||||
fileServer http.Handler // Underlying file server
|
|
||||||
bufferPool sync.Pool // Buffer pool for compression
|
|
||||||
urlPrefix string // URL prefix for static assets
|
|
||||||
log bool // Whether to log debug info
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStaticRouterWithLogger creates a new StaticRouter instance
|
// NewStaticRouter creates a new StaticRouter instance
|
||||||
func NewStaticRouter(rootDir string) (*StaticRouter, error) {
|
func NewStaticRouter(rootDir string) (*StaticRouter, error) {
|
||||||
// Verify root directory exists
|
// Verify root directory exists
|
||||||
info, err := os.Stat(rootDir)
|
info, err := os.Stat(rootDir)
|
||||||
@ -57,60 +35,77 @@ func NewStaticRouter(rootDir string) (*StaticRouter, error) {
|
|||||||
return nil, errors.New("root path is not a directory")
|
return nil, errors.New("root path is not a directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the router with default settings
|
// Create the FS handler with optimized settings
|
||||||
r := &StaticRouter{
|
fs := &fasthttp.FS{
|
||||||
rootDir: rootDir,
|
Root: rootDir,
|
||||||
cache: make(map[string]*list.Element),
|
IndexNames: []string{"index.html"},
|
||||||
lruList: list.New(),
|
GenerateIndexPages: false,
|
||||||
maxItems: 100, // Default: cache 100 files
|
AcceptByteRange: true,
|
||||||
maxItemSize: 1 << 20, // Default: 1MB per file (gzipped)
|
Compress: true,
|
||||||
totalCapacity: 20 << 20, // Default: 20MB total cache
|
CacheDuration: 24 * time.Hour,
|
||||||
fileServer: http.FileServer(http.Dir(rootDir)),
|
CompressedFileSuffix: ".gz",
|
||||||
urlPrefix: "/static", // Default prefix for static assets
|
CompressBrotli: true,
|
||||||
log: false, // Debug logging off by default
|
CompressZstd: true,
|
||||||
bufferPool: sync.Pool{
|
CompressedFileSuffixes: map[string]string{
|
||||||
New: func() any {
|
"gzip": ".gz",
|
||||||
return new(bytes.Buffer)
|
"br": ".br",
|
||||||
},
|
"zstd": ".zst",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize mime package with common types
|
r := &StaticRouter{
|
||||||
mime.AddExtensionType(".js", "application/javascript")
|
fs: fs,
|
||||||
mime.AddExtensionType(".css", "text/css")
|
urlPrefix: "/static", // Default prefix
|
||||||
mime.AddExtensionType(".svg", "image/svg+xml")
|
rootDir: rootDir,
|
||||||
|
log: false,
|
||||||
|
useBrotli: true,
|
||||||
|
useZstd: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the path rewrite based on the prefix
|
||||||
|
r.updatePathRewrite()
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMaxItems sets the maximum number of items in the cache
|
// WithEmbeddedFS sets an embedded filesystem instead of using the rootDir
|
||||||
func (r *StaticRouter) SetMaxItems(n int) {
|
func (r *StaticRouter) WithEmbeddedFS(embedded fs.FS) *StaticRouter {
|
||||||
if n <= 0 {
|
r.fs.FS = embedded
|
||||||
return
|
r.fsHandler = r.fs.NewRequestHandler()
|
||||||
}
|
return r
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
r.maxItems = n
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMaxItemSize sets the maximum size per cached item (in bytes)
|
// SetCompression configures the compression options
|
||||||
func (r *StaticRouter) SetMaxItemSize(n int) {
|
func (r *StaticRouter) SetCompression(useGzip, useBrotli, useZstd bool) {
|
||||||
if n <= 0 {
|
r.fs.Compress = useGzip
|
||||||
return
|
r.fs.CompressBrotli = useBrotli
|
||||||
}
|
r.fs.CompressZstd = useZstd
|
||||||
r.mu.Lock()
|
r.useBrotli = useBrotli
|
||||||
defer r.mu.Unlock()
|
r.useZstd = useZstd
|
||||||
r.maxItemSize = n
|
|
||||||
|
// Update handler to reflect changes
|
||||||
|
r.fsHandler = r.fs.NewRequestHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTotalCapacity sets the total cache capacity (in bytes)
|
// SetCacheDuration sets the cache duration for HTTP headers
|
||||||
func (r *StaticRouter) SetTotalCapacity(n int) {
|
func (r *StaticRouter) SetCacheDuration(duration time.Duration) {
|
||||||
if n <= 0 {
|
r.fs.CacheDuration = duration
|
||||||
return
|
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.mu.Lock()
|
r.urlPrefix = prefix
|
||||||
defer r.mu.Unlock()
|
r.updatePathRewrite()
|
||||||
r.totalCapacity = n
|
}
|
||||||
|
|
||||||
|
// 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
|
// EnableDebugLog enables debug logging
|
||||||
@ -123,93 +118,22 @@ func (r *StaticRouter) DisableDebugLog() {
|
|||||||
r.log = false
|
r.log = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetURLPrefix sets the URL prefix for static assets
|
// ServeHTTP implements the http.Handler interface through fasthttpadaptor
|
||||||
func (r *StaticRouter) SetURLPrefix(prefix string) {
|
func (r *StaticRouter) ServeHTTP(ctx *fasthttp.RequestCtx) {
|
||||||
if !strings.HasPrefix(prefix, "/") {
|
path := string(ctx.Path())
|
||||||
prefix = "/" + prefix
|
|
||||||
}
|
|
||||||
r.urlPrefix = prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler interface
|
|
||||||
func (r *StaticRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
||||||
// Check if path starts with the prefix
|
// Check if path starts with the prefix
|
||||||
if !strings.HasPrefix(req.URL.Path, r.urlPrefix) {
|
if !strings.HasPrefix(path, r.urlPrefix) {
|
||||||
http.NotFound(w, req)
|
ctx.NotFound()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip prefix to get the file path
|
|
||||||
origPath := req.URL.Path
|
|
||||||
fileURLPath := strings.TrimPrefix(req.URL.Path, r.urlPrefix)
|
|
||||||
|
|
||||||
// Make sure path starts with /
|
|
||||||
if !strings.HasPrefix(fileURLPath, "/") {
|
|
||||||
fileURLPath = "/" + fileURLPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for directory access without trailing slash
|
|
||||||
if strings.HasSuffix(fileURLPath, "/index.html") {
|
|
||||||
dirPath := fileURLPath[:len(fileURLPath)-10] // remove "/index.html"
|
|
||||||
filePath := filepath.Join(r.rootDir, filepath.FromSlash(strings.TrimPrefix(dirPath, "/")))
|
|
||||||
if info, err := os.Stat(filePath); err == nil && info.IsDir() {
|
|
||||||
// This is a directory with index.html, handle it directly
|
|
||||||
fileURLPath = dirPath + "/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if client accepts gzip encoding
|
|
||||||
acceptsGzip := strings.Contains(req.Header.Get("Accept-Encoding"), "gzip")
|
|
||||||
|
|
||||||
// Copy the original request for modification
|
|
||||||
newReq := *req
|
|
||||||
newReq.URL = new(url.URL)
|
|
||||||
*newReq.URL = *req.URL
|
|
||||||
newReq.URL.Path = fileURLPath
|
|
||||||
|
|
||||||
// Try to serve from cache if client accepts gzip
|
|
||||||
if acceptsGzip && r.serveFromCache(w, &newReq, fileURLPath) {
|
|
||||||
if r.log {
|
|
||||||
logger.Debug("[StaticRouter] CACHE HIT: %s", origPath)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.log {
|
if r.log {
|
||||||
logger.Debug("[StaticRouter] CACHE MISS: %s", origPath)
|
logger.Debug("[StaticRouter] Serving: %s", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to standard file serving
|
// Handle the request with the FS handler
|
||||||
wrappedWriter := httptest.NewRecorder()
|
r.fsHandler(ctx)
|
||||||
r.fileServer.ServeHTTP(wrappedWriter, &newReq)
|
|
||||||
|
|
||||||
// Check if we got a redirect - might need to add the prefix back
|
|
||||||
if wrappedWriter.Code == http.StatusMovedPermanently || wrappedWriter.Code == http.StatusPermanentRedirect {
|
|
||||||
location := wrappedWriter.Header().Get("Location")
|
|
||||||
if location != "" && !strings.HasPrefix(location, r.urlPrefix) {
|
|
||||||
// Prepend our prefix to the redirect location
|
|
||||||
newLocation := r.urlPrefix
|
|
||||||
if !strings.HasPrefix(location, "/") {
|
|
||||||
newLocation += "/"
|
|
||||||
}
|
|
||||||
newLocation += strings.TrimPrefix(location, "/")
|
|
||||||
wrappedWriter.Header().Set("Location", newLocation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the response from the recorder to the real response writer
|
|
||||||
for k, v := range wrappedWriter.Header() {
|
|
||||||
w.Header()[k] = v
|
|
||||||
}
|
|
||||||
w.WriteHeader(wrappedWriter.Code)
|
|
||||||
w.Write(wrappedWriter.Body.Bytes())
|
|
||||||
|
|
||||||
// Try to cache the file for next time if client accepts gzip
|
|
||||||
if acceptsGzip && wrappedWriter.Code == http.StatusOK {
|
|
||||||
filePath := filepath.Join(r.rootDir, filepath.FromSlash(strings.TrimPrefix(fileURLPath, "/")))
|
|
||||||
// Cache synchronously for tests
|
|
||||||
r.cacheFile(fileURLPath, filePath)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match finds a file path for the given URL path
|
// Match finds a file path for the given URL path
|
||||||
@ -237,253 +161,44 @@ func (r *StaticRouter) Match(urlPath string) (string, bool) {
|
|||||||
return filePath, err == nil
|
return filePath, err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveFromCache tries to serve a file from cache
|
// Refresh is a no-op in this implementation as there's no cache to refresh
|
||||||
// Returns true if successful, false otherwise
|
|
||||||
func (r *StaticRouter) serveFromCache(w http.ResponseWriter, req *http.Request, urlPath string) bool {
|
|
||||||
// Check cache first with read lock
|
|
||||||
r.mu.RLock()
|
|
||||||
elem, found := r.cache[urlPath]
|
|
||||||
if !found {
|
|
||||||
r.mu.RUnlock()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get cache entry
|
|
||||||
entry := elem.Value.(*CacheEntry)
|
|
||||||
content := entry.GzippedContent
|
|
||||||
contentType := entry.ContentType
|
|
||||||
modTime := time.Unix(entry.ModTime, 0)
|
|
||||||
r.mu.RUnlock()
|
|
||||||
|
|
||||||
// Update LRU order with write lock
|
|
||||||
r.mu.Lock()
|
|
||||||
r.lruList.MoveToFront(elem)
|
|
||||||
r.mu.Unlock()
|
|
||||||
|
|
||||||
// Check if client cache is still valid (If-Modified-Since)
|
|
||||||
if !isModified(req, modTime) {
|
|
||||||
w.Header().Set("Content-Type", contentType)
|
|
||||||
w.Header().Set("Last-Modified", modTime.UTC().Format(http.TimeFormat))
|
|
||||||
w.Header().Set("Vary", "Accept-Encoding")
|
|
||||||
w.WriteHeader(http.StatusNotModified)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set appropriate headers
|
|
||||||
w.Header().Set("Content-Encoding", "gzip")
|
|
||||||
w.Header().Set("Content-Type", contentType)
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(content)))
|
|
||||||
w.Header().Set("Vary", "Accept-Encoding")
|
|
||||||
w.Header().Set("Last-Modified", modTime.UTC().Format(http.TimeFormat))
|
|
||||||
|
|
||||||
// Write the content directly instead of using ServeContent
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(content)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// isModified checks if the file has been modified since the client's last request
|
|
||||||
func isModified(req *http.Request, modTime time.Time) bool {
|
|
||||||
// Parse If-Modified-Since header
|
|
||||||
if ims := req.Header.Get("If-Modified-Since"); ims != "" {
|
|
||||||
t, err := http.ParseTime(ims)
|
|
||||||
if err == nil && !modTime.After(t.Add(time.Second)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// cacheFile adds a file to the cache
|
|
||||||
func (r *StaticRouter) cacheFile(urlPath, filePath string) {
|
|
||||||
// Stat the file
|
|
||||||
fileInfo, err := os.Stat(filePath)
|
|
||||||
if err != nil || fileInfo.IsDir() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't cache files that are too large
|
|
||||||
if fileInfo.Size() > int64(r.maxItemSize*2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file
|
|
||||||
data, err := os.ReadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compress data using buffer from pool
|
|
||||||
buf := r.bufferPool.Get().(*bytes.Buffer)
|
|
||||||
buf.Reset()
|
|
||||||
defer r.bufferPool.Put(buf)
|
|
||||||
|
|
||||||
gzWriter, err := gzip.NewWriterLevel(buf, gzip.BestCompression)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := gzWriter.Write(data); err != nil {
|
|
||||||
gzWriter.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := gzWriter.Close(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
gzippedData := make([]byte, buf.Len())
|
|
||||||
copy(gzippedData, buf.Bytes())
|
|
||||||
|
|
||||||
// Don't cache if compressed size is too large
|
|
||||||
if len(gzippedData) > r.maxItemSize {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get content type by extension or detection
|
|
||||||
contentType := getMimeType(filePath, data)
|
|
||||||
|
|
||||||
// Update cache
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
|
|
||||||
// Check if already in cache
|
|
||||||
if elem, exists := r.cache[urlPath]; exists {
|
|
||||||
// Update existing entry
|
|
||||||
oldEntry := elem.Value.(*CacheEntry)
|
|
||||||
r.currentSize -= oldEntry.GzippedSize
|
|
||||||
|
|
||||||
newEntry := &CacheEntry{
|
|
||||||
Path: urlPath,
|
|
||||||
GzippedContent: gzippedData,
|
|
||||||
Size: len(data),
|
|
||||||
GzippedSize: len(gzippedData),
|
|
||||||
ModTime: fileInfo.ModTime().Unix(),
|
|
||||||
ContentType: contentType,
|
|
||||||
}
|
|
||||||
|
|
||||||
elem.Value = newEntry
|
|
||||||
r.lruList.MoveToFront(elem)
|
|
||||||
r.currentSize += newEntry.GzippedSize
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make room in cache if needed
|
|
||||||
for r.lruList.Len() >= r.maxItems ||
|
|
||||||
(r.currentSize+len(gzippedData) > r.totalCapacity && r.lruList.Len() > 0) {
|
|
||||||
|
|
||||||
// Remove least recently used item
|
|
||||||
elem := r.lruList.Back()
|
|
||||||
if elem == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := elem.Value.(*CacheEntry)
|
|
||||||
r.lruList.Remove(elem)
|
|
||||||
delete(r.cache, entry.Path)
|
|
||||||
r.currentSize -= entry.GzippedSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new item to cache
|
|
||||||
entry := &CacheEntry{
|
|
||||||
Path: urlPath,
|
|
||||||
GzippedContent: gzippedData,
|
|
||||||
Size: len(data),
|
|
||||||
GzippedSize: len(gzippedData),
|
|
||||||
ModTime: fileInfo.ModTime().Unix(),
|
|
||||||
ContentType: contentType,
|
|
||||||
}
|
|
||||||
|
|
||||||
elem := r.lruList.PushFront(entry)
|
|
||||||
r.cache[urlPath] = elem
|
|
||||||
r.currentSize += entry.GzippedSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// getMimeType returns the content type for a file
|
|
||||||
func getMimeType(filePath string, data []byte) string {
|
|
||||||
// Try to get content type from extension first
|
|
||||||
ext := strings.ToLower(filepath.Ext(filePath))
|
|
||||||
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
|
|
||||||
return mimeType
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to detection
|
|
||||||
return http.DetectContentType(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh clears the cache
|
|
||||||
func (r *StaticRouter) Refresh() error {
|
func (r *StaticRouter) Refresh() error {
|
||||||
r.mu.Lock()
|
// No cache to refresh in this implementation
|
||||||
defer r.mu.Unlock()
|
|
||||||
|
|
||||||
r.cache = make(map[string]*list.Element)
|
|
||||||
r.lruList.Init()
|
|
||||||
r.currentSize = 0
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PreloadCommonFiles loads common static file types into the cache
|
// GetStats returns basic stats about the router
|
||||||
func (r *StaticRouter) PreloadCommonFiles() {
|
|
||||||
// Common file extensions to preload
|
|
||||||
extensions := map[string]bool{
|
|
||||||
".css": true,
|
|
||||||
".js": true,
|
|
||||||
".svg": true,
|
|
||||||
".ico": true,
|
|
||||||
".png": true,
|
|
||||||
".jpg": true,
|
|
||||||
".jpeg": true,
|
|
||||||
".gif": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.log {
|
|
||||||
logger.Debug("[StaticRouter] Preloading common files from %s", r.rootDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
count := 0
|
|
||||||
|
|
||||||
// Walk the directory
|
|
||||||
_ = filepath.Walk(r.rootDir, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil || info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check extension and file size
|
|
||||||
ext := strings.ToLower(filepath.Ext(path))
|
|
||||||
if !extensions[ext] || info.Size() > int64(r.maxItemSize*2) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get URL path
|
|
||||||
relPath, err := filepath.Rel(r.rootDir, path)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't include prefix here - will be applied in ServeHTTP
|
|
||||||
urlPath := "/" + strings.ReplaceAll(relPath, "\\", "/")
|
|
||||||
|
|
||||||
// Cache the file synchronously for tests
|
|
||||||
r.cacheFile(urlPath, path)
|
|
||||||
count++
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if r.log {
|
|
||||||
logger.Debug("[StaticRouter] Preloaded %d files", count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStats returns cache statistics
|
|
||||||
func (r *StaticRouter) GetStats() map[string]any {
|
func (r *StaticRouter) GetStats() map[string]any {
|
||||||
r.mu.RLock()
|
|
||||||
defer r.mu.RUnlock()
|
|
||||||
|
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"items": r.lruList.Len(),
|
"type": "StaticRouter",
|
||||||
"maxItems": r.maxItems,
|
"rootDir": r.rootDir,
|
||||||
"currentSize": r.currentSize,
|
"urlPrefix": r.urlPrefix,
|
||||||
"totalCapacity": r.totalCapacity,
|
"useBrotli": r.useBrotli,
|
||||||
"usagePercent": float64(r.currentSize) * 100 / float64(r.totalCapacity),
|
"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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -295,9 +295,9 @@ func (r *Runner) executeTask(i interface{}) {
|
|||||||
select {
|
select {
|
||||||
case stateIndex = <-r.statePool:
|
case stateIndex = <-r.statePool:
|
||||||
// Got a state
|
// Got a state
|
||||||
default:
|
case <-time.After(5 * time.Second): // 5-second timeout
|
||||||
// No state available - this shouldn't happen since we limit tasks
|
// Timed out waiting for a state
|
||||||
task.result <- taskResult{nil, errors.New("no states available")}
|
task.result <- taskResult{nil, errors.New("server busy - timed out waiting for a Lua state")}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,25 +83,6 @@ func WatchLuaRouter(router *routers.LuaRouter, runner *runner.Runner, routesDir
|
|||||||
return watcher, nil
|
return watcher, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WatchStaticRouter sets up a watcher for a StaticRouter's root directory
|
|
||||||
func WatchStaticRouter(router *routers.StaticRouter, staticDir string) (*Watcher, error) {
|
|
||||||
manager := GetWatcherManager(true)
|
|
||||||
|
|
||||||
config := DirectoryWatcherConfig{
|
|
||||||
Dir: staticDir,
|
|
||||||
Callback: router.Refresh,
|
|
||||||
Recursive: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
watcher, err := WatchDirectory(config, manager)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Started watching static files directory: %s", staticDir)
|
|
||||||
return watcher, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WatchLuaModules sets up watchers for Lua module directories
|
// WatchLuaModules sets up watchers for Lua module directories
|
||||||
func WatchLuaModules(luaRunner *runner.Runner, libDirs []string) ([]*Watcher, error) {
|
func WatchLuaModules(luaRunner *runner.Runner, libDirs []string) ([]*Watcher, error) {
|
||||||
manager := GetWatcherManager(true)
|
manager := GetWatcherManager(true)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user