package server import ( "fes/modules/config" "fes/modules/ui" "fmt" "github.com/pelletier/go-toml/v2" lua "github.com/yuin/gopher-lua" "html/template" "io/fs" "net/http" "os" "path" "path/filepath" "sort" "strings" "time" ) 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) } else if !isStatic && strings.HasSuffix(entry.Name(), ".md") { name := strings.TrimSuffix(entry.Name(), ".md") if name == "index" { routes[basePath(base)] = path routes[route] = path continue } route = joinBase(base, name) } routes[route] = path } return nil } 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(entry string, cfg *config.AppConfig, requestData reqData) ([]byte, error) { L := lua.NewState() defer L.Close() libFiles, err := fs.ReadDir(config.Lib, "lib") if err == nil { for _, de := range libFiles { if de.IsDir() || !strings.HasSuffix(de.Name(), ".lua") { continue } path := filepath.Join("lib", de.Name()) fileData, err := config.Lib.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.Lib.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("lib.std", "lib/std.lua") preloadLuaModule("lib.symbol", "lib/symbol.lua") preloadLuaModule("lib.util", "lib/util.lua") L.PreloadModule("fes", func(L *lua.LState) int { mod := L.NewTable() libModules := []string{} if ents, err := fs.ReadDir(config.Lib, "lib"); err == nil { for _, e := range ents { if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") { continue } libModules = append(libModules, strings.TrimSuffix(e.Name(), ".lua")) } } for _, modName := range libModules { path := filepath.Join("lib", modName+".lua") fileData, err := config.Lib.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 == "fes" { 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 []byte(""), err } if L.GetTop() == 0 { return []byte(""), 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 []byte(s), nil } return []byte(""), nil } if s := L.ToString(-1); s != "" { return []byte(s), nil } return []byte(""), 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 = "/archive" + filepath.Dir(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("