From 7e3af14059f10915664b07507832ce089d3d6d75 Mon Sep 17 00:00:00 2001 From: vx-clutch Date: Fri, 16 Jan 2026 16:08:10 -0500 Subject: [PATCH] rewrite server module --- modules/server/archive.go | 120 +++++++++++++++++++++++++++ modules/server/dirs.go | 80 ++++++++++++++++++ modules/server/render.go | 164 ++++++++++++++++++++++++++++++++++++- modules/server/server.go | 65 ++++++++++++++- modules/server/util.go | 14 ++++ modules/version/version.go | 2 +- 6 files changed, 437 insertions(+), 8 deletions(-) create mode 100644 modules/server/archive.go create mode 100644 modules/server/dirs.go diff --git a/modules/server/archive.go b/modules/server/archive.go new file mode 100644 index 0000000..bf1ffb2 --- /dev/null +++ b/modules/server/archive.go @@ -0,0 +1,120 @@ +package server + +import ( + "fmt" + "net/http" + "os" + "path" + "path/filepath" + "sort" + "strings" + "text/template" + "time" +) + +/* this indexes and generate the page for viewing the archive directory */ +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 = strings.TrimPrefix(urlPath, "/archive") + + var b strings.Builder + + b.WriteString("\nIndex of ") + b.WriteString(template.HTMLEscapeString(urlPath)) + b.WriteString("\n\n

Index of ") + b.WriteString(template.HTMLEscapeString(urlPath)) + b.WriteString("


")
+
+	if urlPath != "/" {
+		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("

\n") + return b.String(), nil +} + +/* helper to read the archive files */ +func readArchive(w http.ResponseWriter, route string) error { + fsPath := "." + route + if info, err := os.Stat(fsPath); err == nil && info.IsDir() { + if page, err := generateArchiveIndex(fsPath, route); err == nil { + w.Write([]byte(page)) + return nil + } else { + return err + } + } + return nil +} diff --git a/modules/server/dirs.go b/modules/server/dirs.go new file mode 100644 index 0000000..aefa13e --- /dev/null +++ b/modules/server/dirs.go @@ -0,0 +1,80 @@ +package server + +import ( + "fes/modules/ui" + "fmt" + "os" + "path/filepath" + "strings" +) + +/* performs relavent handling based on the directory passaed + * + * Special directories + * - www/ <= contains lua routes. + * - static/ <= static content accessable at /static/path or /static/dir/path. + * - include/ <= globally accessable lua functions, cannot directly access "fes" right now. + * - archive/ <= contains user facing files such as archives or dists. + * + */ +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 +} + +/* helper to load all special directories */ +func loadDirs() map[string]string { + routes := make(map[string]string) + + if entries, err := os.ReadDir("www"); err == nil { + if err := handleDir(entries, "www", routes, "", false); err != nil { + ui.Warning("failed to handle www directory", err) + } + } + + if entries, err := os.ReadDir("static"); err == nil { + if err := handleDir(entries, "static", routes, "/static", true); err != nil { + ui.Warning("failed to handle static directory", err) + } + } + + if entries, err := os.ReadDir("archive"); err == nil { + if err := handleDir(entries, "archive", routes, "/archive", true); err != nil { + ui.Warning("failed to handle archive directory", err) + } + } + + return routes +} diff --git a/modules/server/render.go b/modules/server/render.go index 1cf66d7..c99162f 100644 --- a/modules/server/render.go +++ b/modules/server/render.go @@ -1,6 +1,164 @@ package server -/* returns a string of rendered html */ -func render(luapath string) (string, error) { - return "", nil +import ( + "fes/modules/config" + "io/fs" + "os" + "path/filepath" + "strings" + + lua "github.com/yuin/gopher-lua" +) + +/* this is the request data we pass over the bus to the application, via the fes.bus interface */ +type reqData struct { + path string + params map[string]string +} + +/* returns a string of rendered html */ +func render(luapath string, requestData reqData) ([]byte, error) { + L := lua.NewState() + defer L.Close() + + if lib, err := fs.ReadDir(config.Lib, "lib"); err == nil { + for _, de := range lib { + if de.IsDir() || !strings.HasSuffix(de.Name(), ".lua") { + continue + } + path := filepath.Join("lib", de.Name()) + if data, err := config.Lib.ReadFile(path); err != nil { + continue + } else { + L.DoString(string(data)) + } + } + } + + 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", func() *lua.LTable { + app := L.NewTable() + includeDir := "include" + + includes, err := os.ReadDir(includeDir) + if err != nil { + return app // load no includes + } + + for _, de := range includes { + if de.IsDir() || !strings.HasSuffix(de.Name(), ".lua") { + continue + } + + base := strings.TrimSuffix(de.Name(), ".lua") + path := filepath.Join(includeDir, de.Name()) + + if _, err := os.Stat(path); err != nil { + continue + } else if err := L.DoFile(path); err != nil { + 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 + }()) + + 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(luapath); 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 } diff --git a/modules/server/server.go b/modules/server/server.go index 3cf31e4..31fc9a8 100644 --- a/modules/server/server.go +++ b/modules/server/server.go @@ -2,17 +2,74 @@ package server import ( "fes/modules/config" + "fes/modules/ui" "fmt" "log" "net/http" + "os" + "path/filepath" + "strings" ) var routes map[string]string func Start(dir string) { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("

Sup bitch

")) - }) + if err := os.Chdir(dir); err != nil { + ui.Error(fmt.Sprintf("failed to change directory to %s", dir), err) + } - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *config.Port), nil)) + ui.Log("running root=%s, port=%d.", filepath.Clean(dir), *config.Port) + + routes := loadDirs() + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + route, ok := routes[r.URL.Path] + + var err error = nil + + /* defer won't update paramaters unless we do this. */ + defer func() { + ui.Path(route, err) + }() + + if !ok { + err = config.ErrRouteMiss + route = r.URL.Path + + if strings.HasPrefix(route, "/archive") { + err = readArchive(w, route) + } else { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("not found :(")) + } + return + } + + params := make(map[string]string) + for k, v := range r.URL.Query() { + if len(v) > 0 { + params[k] = v[0] + } + } + + var data []byte + if strings.HasSuffix(route, ".lua") { + data, err = render(route, reqData{path: r.URL.Path, params: params}) + } else if strings.HasSuffix(route, ".md") { + data, err = os.ReadFile(route) + data = []byte(markdownToHTML(string(data))) + data = []byte("\n" + string(data)) + } else { + data, err = os.ReadFile(route) + } + + if err != nil { + http.Error(w, fmt.Sprintf("Error loading page: %v", err), http.StatusInternalServerError) + } + + w.Write(data) + }) + ui.Log("Server initialized") + + log.Fatal(http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", *config.Port), nil)) } diff --git a/modules/server/util.go b/modules/server/util.go index 8e54629..57bb01e 100644 --- a/modules/server/util.go +++ b/modules/server/util.go @@ -15,3 +15,17 @@ func markdownToHTML(mdText string) string { renderer := html.NewRenderer(opts) return string(markdown.Render(doc, renderer)) } + +func basePath(base string) string { + if base == "" || base == "." { + return "/" + } + return base +} + +func joinBase(base, name string) string { + if base == "" { + return "/" + name + } + return base + "/" + name +} diff --git a/modules/version/version.go b/modules/version/version.go index 503a018..1e50ad7 100644 --- a/modules/version/version.go +++ b/modules/version/version.go @@ -9,7 +9,7 @@ var gitCommit string = "devel" const PROGRAM_NAME string = "fes" const PROGRAM_NAME_LONG string = "fes/fSD" -const VERSION string = "0.3.1" +const VERSION string = "1.0.0" func Version() { fmt.Printf("%s version %s\n", PROGRAM_NAME_LONG, VERSION)