diff --git a/config-example.lua b/config-example.lua index 95df3b5..110380f 100644 --- a/config-example.lua +++ b/config-example.lua @@ -10,12 +10,12 @@ runner = { } dirs = { - routes = "./routes", - static = "./static", - fs = "./fs", - data = "./data", - override = "./override", + routes = "routes", + static = "public", + fs = "fs", + data = "data", + override = "override", libs = { - "./libs" + "libs" } } diff --git a/core/http/server.go b/core/http/server.go index 4a0e6d2..d2e5d55 100644 --- a/core/http/server.go +++ b/core/http/server.go @@ -107,14 +107,14 @@ func (s *Server) processRequest(ctx *fasthttp.RequestCtx) { logger.Debug("Processing request %s %s", method, path) params := &routers.Params{} - bytecode, scriptPath, found := s.luaRouter.GetBytecode(method, path, params) + bytecode, scriptPath, routeErr, found := s.luaRouter.GetRouteInfo(method, path, params) - // Check if we found a route but it has no valid bytecode (compile error) - if found && len(bytecode) == 0 { + // Check if we found a route but it has an error or no valid bytecode + if found && (len(bytecode) == 0 || routeErr != nil) { errorMsg := "Route exists but failed to compile. Check server logs for details." - if node, _ := s.luaRouter.GetNodeWithError(method, path, params); node != nil && node.Error != nil { - errorMsg = node.Error.Error() + if routeErr != nil { + errorMsg = routeErr.Error() } logger.Error("%s %s - %s", method, path, errorMsg) diff --git a/core/http/utils.go b/core/http/utils.go index 15b0118..ac36b53 100644 --- a/core/http/utils.go +++ b/core/http/utils.go @@ -17,7 +17,6 @@ import ( func LogRequest(statusCode int, method, path string, duration time.Duration) { var statusColor, resetColor, methodColor string - // Status code colors if statusCode >= 200 && statusCode < 300 { statusColor = "\u001b[32m" // Green for 2xx } else if statusCode >= 300 && statusCode < 400 { @@ -28,7 +27,6 @@ func LogRequest(statusCode int, method, path string, duration time.Duration) { statusColor = "\u001b[31m" // Red for 5xx and others } - // Method colors switch method { case "GET": methodColor = "\u001b[32m" // Green @@ -44,7 +42,6 @@ func LogRequest(statusCode int, method, path string, duration time.Duration) { resetColor = "\u001b[0m" - // Format duration var durationStr string if duration.Milliseconds() < 1 { durationStr = fmt.Sprintf("%.2fµs", float64(duration.Microseconds())) @@ -54,7 +51,6 @@ func LogRequest(statusCode int, method, path string, duration time.Duration) { durationStr = fmt.Sprintf("%.2fs", duration.Seconds()) } - // Log with colors logger.Server("%s%d%s %s%s%s %s %s", statusColor, statusCode, resetColor, methodColor, method, resetColor, @@ -65,23 +61,17 @@ func LogRequest(statusCode int, method, path string, duration time.Duration) { func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any { queryMap := make(map[string]any) - // Visit all query parameters ctx.QueryArgs().VisitAll(func(key, value []byte) { - // Convert to string k := string(key) v := string(value) - // Check if this key already exists as an array if existing, ok := queryMap[k]; ok { - // If it's already an array, append to it if arr, ok := existing.([]string); ok { queryMap[k] = append(arr, v) } else if str, ok := existing.(string); ok { - // Convert existing string to array and append new value queryMap[k] = []string{str, v} } } else { - // New key, store as string queryMap[k] = v } }) @@ -93,27 +83,21 @@ func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any { func ParseForm(ctx *fasthttp.RequestCtx) (map[string]any, error) { formData := make(map[string]any) - // Check if multipart form if strings.Contains(string(ctx.Request.Header.ContentType()), "multipart/form-data") { return parseMultipartForm(ctx) } - // Regular form ctx.PostArgs().VisitAll(func(key, value []byte) { k := string(key) v := string(value) - // Check if this key already exists if existing, ok := formData[k]; ok { - // If it's already an array, append to it if arr, ok := existing.([]string); ok { formData[k] = append(arr, v) } else if str, ok := existing.(string); ok { - // Convert existing string to array and append new value formData[k] = []string{str, v} } } else { - // New key, store as string formData[k] = v } }) @@ -125,13 +109,11 @@ func ParseForm(ctx *fasthttp.RequestCtx) (map[string]any, error) { func parseMultipartForm(ctx *fasthttp.RequestCtx) (map[string]any, error) { formData := make(map[string]any) - // Parse multipart form form, err := ctx.MultipartForm() if err != nil { return nil, err } - // Process form values for key, values := range form.Value { if len(values) == 1 { formData[key] = values[0] @@ -140,7 +122,6 @@ func parseMultipartForm(ctx *fasthttp.RequestCtx) (map[string]any, error) { } } - // Process files (store file info, not the content) if len(form.File) > 0 { files := make(map[string]any) diff --git a/core/routers/luaRouter.go b/core/routers/luaRouter.go index 72b6872..d07129a 100644 --- a/core/routers/luaRouter.go +++ b/core/routers/luaRouter.go @@ -214,10 +214,8 @@ func getBytecodeKey(handlerPath string) []byte { // Match finds a handler for the given method and path // Uses the pre-allocated params struct to avoid allocations func (r *LuaRouter) Match(method, path string, params *Params) (*node, bool) { - // Reset params params.Count = 0 - // Get route tree for method r.mu.RLock() root, exists := r.routes[method] r.mu.RUnlock() @@ -226,10 +224,8 @@ func (r *LuaRouter) Match(method, path string, params *Params) (*node, bool) { return nil, false } - // Split path segments := strings.Split(strings.Trim(path, "/"), "/") - // Match path return r.matchPath(root, segments, params, 0) } @@ -279,122 +275,100 @@ func (r *LuaRouter) compileHandler(n *node) error { return nil } - // Read the Lua file content, err := os.ReadFile(n.handler) if err != nil { - n.err = err // Store the error in the node + n.err = err return err } - // Compile to bytecode state := luajit.New() if state == nil { compileErr := errors.New("failed to create Lua state") - n.err = compileErr // Store the error in the node + n.err = compileErr return compileErr } defer state.Close() bytecode, err := state.CompileBytecode(string(content), n.handler) if err != nil { - n.err = err // Store the error in the node + n.err = err return err } - // Store bytecode in cache bytecodeKey := getBytecodeKey(n.handler) r.bytecodeCache.Set(bytecodeKey, bytecode) - n.err = nil // Clear any previous error + n.err = nil return nil } -// GetBytecode returns the compiled bytecode for a matched route -// Uses FastCache for both route matching and bytecode retrieval -func (r *LuaRouter) GetBytecode(method, path string, params *Params) ([]byte, string, bool) { - // Check route cache first +// GetRouteInfo returns the bytecode, script path, and any error for a matched route +func (r *LuaRouter) GetRouteInfo(method, path string, params *Params) ([]byte, string, error, bool) { routeCacheKey := getCacheKey(method, path) routeCacheData := r.routeCache.Get(nil, routeCacheKey) if len(routeCacheData) > 0 { - // Cache hit - first 8 bytes are bytecode hash, rest is handler path handlerPath := string(routeCacheData[8:]) bytecodeKey := routeCacheData[:8] - // Get bytecode from cache bytecode := r.bytecodeCache.Get(nil, bytecodeKey) - if len(bytecode) > 0 { - return bytecode, handlerPath, true - } - // Bytecode not found, check if file was modified n, exists := r.nodeForHandler(handlerPath) if !exists { - // Handler no longer exists r.routeCache.Del(routeCacheKey) - return nil, "", false + return nil, "", nil, false + } + + if len(bytecode) > 0 { + return bytecode, handlerPath, n.err, true } - // Check if file was modified fileInfo, err := os.Stat(handlerPath) if err != nil || fileInfo.ModTime().After(n.modTime) { - // Recompile if file was modified if err := r.compileHandler(n); err != nil { - return nil, handlerPath, true // Return with error + return nil, handlerPath, n.err, true } - // Update cache newBytecodeKey := getBytecodeKey(handlerPath) bytecode = r.bytecodeCache.Get(nil, newBytecodeKey) - // Update route cache newCacheData := make([]byte, 8+len(handlerPath)) copy(newCacheData[:8], newBytecodeKey) copy(newCacheData[8:], handlerPath) r.routeCache.Set(routeCacheKey, newCacheData) - return bytecode, handlerPath, true + return bytecode, handlerPath, n.err, true } - // Strange case - bytecode not in cache but file not modified - // Recompile + // Strange case - bytecode not in cache but file not modified; recompile if err := r.compileHandler(n); err != nil { - return nil, handlerPath, true + return nil, handlerPath, n.err, true } bytecode = r.bytecodeCache.Get(nil, bytecodeKey) - return bytecode, handlerPath, true + return bytecode, handlerPath, n.err, true } - // Cache miss - do normal routing node, found := r.Match(method, path, params) if !found { - return nil, "", false + return nil, "", nil, false } - // If the route exists but has a compilation error - if node.err != nil { - return nil, node.handler, true - } - - // Get bytecode from cache bytecodeKey := getBytecodeKey(node.handler) bytecode := r.bytecodeCache.Get(nil, bytecodeKey) if len(bytecode) == 0 { - // Compile if not in cache if err := r.compileHandler(node); err != nil { - return nil, node.handler, true + return nil, node.handler, node.err, true } bytecode = r.bytecodeCache.Get(nil, bytecodeKey) } - // Add to route cache cacheData := make([]byte, 8+len(node.handler)) copy(cacheData[:8], bytecodeKey) copy(cacheData[8:], node.handler) r.routeCache.Set(routeCacheKey, cacheData) - return bytecode, node.handler, true + return bytecode, node.handler, node.err, true } // nodeForHandler finds a node by its handler path @@ -428,7 +402,6 @@ func findNodeByHandler(current *node, handlerPath string) *node { } } - // Check param child if current.paramChild != nil { if node := findNodeByHandler(current.paramChild, handlerPath); node != nil { return node @@ -443,20 +416,16 @@ func (r *LuaRouter) Refresh() error { r.mu.Lock() defer r.mu.Unlock() - // Reset routes for method := range r.routes { r.routes[method] = &node{ staticChild: make(map[string]*node), } } - // Clear failed routes r.failedRoutes = make(map[string]*RouteError) - // Rebuild routes err := r.buildRoutes() - // If some routes failed to compile, return a warning error if len(r.failedRoutes) > 0 { return ErrRoutesCompilationErrors } @@ -484,14 +453,14 @@ func (r *LuaRouter) ClearCache() { } // GetCacheStats returns statistics about the cache -func (r *LuaRouter) GetCacheStats() map[string]interface{} { +func (r *LuaRouter) GetCacheStats() map[string]any { var routeStats fastcache.Stats var bytecodeStats fastcache.Stats r.routeCache.UpdateStats(&routeStats) r.bytecodeCache.UpdateStats(&bytecodeStats) - return map[string]interface{}{ + return map[string]any{ "routeEntries": routeStats.EntriesCount, "routeBytes": routeStats.BytesSize, "routeCollisions": routeStats.Collisions, @@ -509,7 +478,6 @@ func (r *LuaRouter) GetRouteStats() (int, int64) { routeCount := 0 bytecodeBytes := int64(0) - // Count routes and estimate bytecode size for _, root := range r.routes { count, bytes := countNodesAndBytecode(root) routeCount += count @@ -525,21 +493,18 @@ func countNodesAndBytecode(n *node) (count int, bytecodeBytes int64) { return 0, 0 } - // Count this node if it has a handler if n.handler != "" { count = 1 - // Estimate bytecode size (average of 2KB per script) + // Average of 2KB per script bytecodeBytes = 2048 } - // Count static children for _, child := range n.staticChild { childCount, childBytes := countNodesAndBytecode(child) count += childCount bytecodeBytes += childBytes } - // Count parameter child if n.paramChild != nil { childCount, childBytes := countNodesAndBytecode(n.paramChild) count += childCount @@ -553,35 +518,3 @@ type NodeWithError struct { ScriptPath string Error error } - -// GetNodeWithError returns the node with its error for a given path -func (r *LuaRouter) GetNodeWithError(method, path string, params *Params) (*NodeWithError, bool) { - // Try route cache first - routeCacheKey := getCacheKey(method, path) - routeCacheData := r.routeCache.Get(nil, routeCacheKey) - - if len(routeCacheData) > 0 { - // Cache hit - get handler path - handlerPath := string(routeCacheData[8:]) - - // Find the node for this handler - node, found := r.nodeForHandler(handlerPath) - if found { - return &NodeWithError{ - ScriptPath: node.handler, - Error: node.err, - }, true - } - } - - // Cache miss - do normal routing - node, found := r.Match(method, path, params) - if !found { - return nil, false - } - - return &NodeWithError{ - ScriptPath: node.handler, - Error: node.err, - }, true -} diff --git a/core/routers/staticRouter.go b/core/routers/staticRouter.go index b3044da..ce7187d 100644 --- a/core/routers/staticRouter.go +++ b/core/routers/staticRouter.go @@ -26,7 +26,6 @@ type StaticRouter struct { // 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 @@ -49,7 +48,7 @@ func NewStaticRouter(rootDir string) (*StaticRouter, error) { r := &StaticRouter{ fs: fs, - urlPrefix: "/", + urlPrefix: "", rootDir: rootDir, log: false, useBrotli: true, diff --git a/core/utils/config/config.go b/core/utils/config/config.go index 4851433..389c1bf 100644 --- a/core/utils/config/config.go +++ b/core/utils/config/config.go @@ -56,12 +56,12 @@ func New() *Config { config.Runner.PoolSize = runtime.GOMAXPROCS(0) // Dirs defaults - config.Dirs.Routes = "./routes" - config.Dirs.Static = "./static" - config.Dirs.FS = "./fs" - config.Dirs.Data = "./data" - config.Dirs.Override = "./override" - config.Dirs.Libs = []string{"./libs"} + config.Dirs.Routes = "routes" + config.Dirs.Static = "public" + config.Dirs.FS = "fs" + config.Dirs.Data = "data" + config.Dirs.Override = "override" + config.Dirs.Libs = []string{"libs"} return config } diff --git a/go.mod b/go.mod index 748a337..e0eab7c 100644 --- a/go.mod +++ b/go.mod @@ -3,22 +3,22 @@ module Moonshark go 1.24.1 require ( - git.sharkk.net/Sky/LuaJIT-to-Go v0.0.0 + git.sharkk.net/Sky/LuaJIT-to-Go v0.1.2 github.com/VictoriaMetrics/fastcache v1.12.2 github.com/deneonet/benc v1.1.7 github.com/goccy/go-json v0.10.5 github.com/matoous/go-nanoid/v2 v2.1.0 github.com/valyala/bytebufferpool v1.0.0 - github.com/valyala/fasthttp v1.60.0 + github.com/valyala/fasthttp v1.61.0 ) require ( github.com/andybalholm/brotli v1.1.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/sys v0.32.0 // indirect ) replace git.sharkk.net/Sky/LuaJIT-to-Go => ./luajit diff --git a/go.sum b/go.sum index 25ecc84..342e6f3 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,9 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,8 +14,9 @@ github.com/deneonet/benc v1.1.7 h1:0XPxTTVJZq/ulxXvMn2Mzjx5XquekVky3wX6eTgA0vA= github.com/deneonet/benc v1.1.7/go.mod h1:UCfkM5Od0B2huwv/ZItvtUb7QnALFt9YXtX8NXX4Lts= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= @@ -27,14 +29,14 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw= -github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc= +github.com/valyala/fasthttp v1.61.0 h1:VV08V0AfoRaFurP1EWKvQQdPTZHiUzaVoulX1aBDgzU= +github.com/valyala/fasthttp v1.61.0/go.mod h1:wRIV/4cMwUPWnRcDno9hGnYZGh78QzODFfo1LTUhBog= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/luajit b/luajit index a2b4b1c..6b9e2a0 160000 --- a/luajit +++ b/luajit @@ -1 +1 @@ -Subproject commit a2b4b1c9272f849d9c1c913366f822e0be904ba2 +Subproject commit 6b9e2a0e201bdd34fba0972441434354aa5c67c5