package server import ( "fmt" "io/fs" "net/http" "os" "path" "path/filepath" "regexp" "sort" "strings" "time" "fes/src/config" "github.com/fatih/color" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" "github.com/pelletier/go-toml/v2" lua "github.com/yuin/gopher-lua" "html/template" ) type reqData struct { path string params map[string]string } func handleDir(entries []os.DirEntry, dir string, routes map[string]string, base string, isStatic bool) error { for _, entry := range entries { path := filepath.Join(dir, entry.Name()) if entry.IsDir() { nextBase := joinBase(base, entry.Name()) subEntries, err := os.ReadDir(path) if err != nil { return fmt.Errorf("failed to read directory %s: %w", path, err) } if err := handleDir(subEntries, path, routes, nextBase, isStatic); err != nil { return err } continue } route := joinBase(base, entry.Name()) if !isStatic && strings.HasSuffix(entry.Name(), ".lua") { name := strings.TrimSuffix(entry.Name(), ".lua") if name == "index" { routes[basePath(base)] = path routes[route] = path continue } route = joinBase(base, name) } routes[route] = path } return nil } func joinBase(base, name string) string { if base == "" { return "/" + name } return base + "/" + name } func basePath(base string) string { if base == "" { return "/" } return base } func fixMalformedToml(content string) string { re := regexp.MustCompile(`(?m)^(\s*\w+\s*=\s*)$`) return re.ReplaceAllStringFunc(content, func(match string) string { parts := strings.Split(strings.TrimSpace(match), "=") if len(parts) == 2 && strings.TrimSpace(parts[1]) == "" { key := strings.TrimSpace(parts[0]) return key + " = \"\"" } return match }) } func markdownToHTML(mdText string) string { extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock p := parser.NewWithExtensions(extensions) doc := p.Parse([]byte(mdText)) htmlFlags := html.CommonFlags | html.HrefTargetBlank opts := html.RendererOptions{Flags: htmlFlags} renderer := html.NewRenderer(opts) return string(markdown.Render(doc, renderer)) } func loadIncludeModules(L *lua.LState, includeDir string) *lua.LTable { app := L.NewTable() ents, err := os.ReadDir(includeDir) if err != nil { return app } for _, e := range ents { if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") { continue } base := strings.TrimSuffix(e.Name(), ".lua") path := filepath.Join(includeDir, e.Name()) if _, err := os.Stat(path); err != nil { tbl := L.NewTable() tbl.RawSetString("error", lua.LString(fmt.Sprintf("file not found: %s", path))) app.RawSetString(base, tbl) continue } if err := L.DoFile(path); err != nil { tbl := L.NewTable() tbl.RawSetString("error", lua.LString(err.Error())) app.RawSetString(base, tbl) continue } val := L.Get(-1) L.Pop(1) tbl, ok := val.(*lua.LTable) if !ok || tbl == nil { tbl = L.NewTable() } app.RawSetString(base, tbl) } return app } func loadLua(luaDir string, entry string, cfg *config.MyConfig, requestData reqData) (string, error) { L := lua.NewState() defer L.Close() coreFiles, err := fs.ReadDir(config.Core, "core") if err == nil { for _, de := range coreFiles { if de.IsDir() || !strings.HasSuffix(de.Name(), ".lua") { continue } path := filepath.Join("core", de.Name()) fileData, err := config.Core.ReadFile(path) if err != nil { continue } L.DoString(string(fileData)) } } preloadLuaModule := func(name, path string) { L.PreloadModule(name, func(L *lua.LState) int { fileData, err := config.Core.ReadFile(path) if err != nil { panic(err) } if err := L.DoString(string(fileData)); err != nil { panic(err) } L.Push(L.Get(-1)) return 1 }) } preloadLuaModule("core.std", "core/std.lua") preloadLuaModule("core.symbol", "core/symbol.lua") preloadLuaModule("core.util", "core/util.lua") L.PreloadModule("fes", func(L *lua.LState) int { mod := L.NewTable() coreModules := []string{} if ents, err := fs.ReadDir(config.Core, "core"); err == nil { for _, e := range ents { if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") { continue } coreModules = append(coreModules, strings.TrimSuffix(e.Name(), ".lua")) } } for _, modName := range coreModules { path := filepath.Join("core", modName+".lua") fileData, err := config.Core.ReadFile(path) if err != nil { continue } if err := L.DoString(string(fileData)); err != nil { continue } val := L.Get(-1) L.Pop(1) tbl, ok := val.(*lua.LTable) if !ok || tbl == nil { tbl = L.NewTable() } if modName == "builtin" { tbl.ForEach(func(k, v lua.LValue) { mod.RawSet(k, v) }) } else { mod.RawSetString(modName, tbl) } } mod.RawSetString("app", loadIncludeModules(L, filepath.Join(".", "include"))) if cfg != nil { site := L.NewTable() site.RawSetString("version", lua.LString(cfg.App.Version)) site.RawSetString("name", lua.LString(cfg.App.Name)) authors := L.NewTable() for i, a := range cfg.App.Authors { authors.RawSetInt(i+1, lua.LString(a)) } site.RawSetString("authors", authors) mod.RawSetString("site", site) } bus := L.NewTable() bus.RawSetString("url", lua.LString(requestData.path)) params := L.NewTable() for k, v := range requestData.params { params.RawSetString(k, lua.LString(v)) } bus.RawSetString("params", params) mod.RawSetString("bus", bus) mod.RawSetString("markdown_to_html", L.NewFunction(func(L *lua.LState) int { L.Push(lua.LString(markdownToHTML(L.ToString(1)))) return 1 })) L.Push(mod) return 1 }) if err := L.DoFile(entry); err != nil { return "", err } if L.GetTop() == 0 { return "", nil } L.SetGlobal("__fes_result", L.Get(-1)) if err := L.DoString("return tostring(__fes_result)"); err != nil { L.GetGlobal("__fes_result") if s := L.ToString(-1); s != "" { return s, nil } return "", nil } if s := L.ToString(-1); s != "" { return s, nil } return "", nil } func generateArchiveIndex(fsPath string, urlPath string) (string, error) { info, err := os.Stat(fsPath) if err != nil { return "", err } if !info.IsDir() { return "", fmt.Errorf("not a directory") } ents, err := os.ReadDir(fsPath) if err != nil { return "", err } type entryInfo struct { name string isDir bool href string size int64 mod time.Time } var list []entryInfo for _, e := range ents { n := e.Name() full := filepath.Join(fsPath, n) st, err := os.Stat(full) if err != nil { continue } isd := st.IsDir() displayName := n if isd { displayName = n + "/" } href := path.Join(urlPath, n) if isd && !strings.HasSuffix(href, "/") { href = href + "/" } size := int64(-1) if !isd { size = st.Size() } list = append(list, entryInfo{name: displayName, isDir: isd, href: href, size: size, mod: st.ModTime()}) } sort.Slice(list, func(i, j int) bool { if list[i].isDir != list[j].isDir { return list[i].isDir } return strings.ToLower(list[i].name) < strings.ToLower(list[j].name) }) urlPath = basePath(strings.TrimPrefix(urlPath, "/archive")) var b strings.Builder b.WriteString("\n
")
if urlPath != "/archive" && urlPath != "/archive/" {
up := path.Dir(urlPath)
if up == "." {
up = "/archive"
}
if !strings.HasSuffix(up, "/") {
up = up + "/"
}
b.WriteString(`../` + "\n")
} else {
b.WriteString(`../` + "\n")
}
nameCol := 50
for _, ei := range list {
escapedName := template.HTMLEscapeString(ei.name)
dateStr := ei.mod.Local().Format("02-Jan-2006 15:04")
var sizeStr string
if ei.isDir {
sizeStr = "-"
} else {
sizeStr = fmt.Sprintf("%d", ei.size)
}
spaces := 1
if len(escapedName) < nameCol {
spaces = nameCol - len(escapedName)
}
line := `` + escapedName + `` + strings.Repeat(" ", spaces) + dateStr + strings.Repeat(" ", 19-len(sizeStr)) + sizeStr + "\n"
b.WriteString(line)
}
b.WriteString("