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("\n
Index of ")
+ b.WriteString(template.HTMLEscapeString(urlPath))
+ b.WriteString("\n\nIndex 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)