diff --git a/go.work b/go.work index 3fa22ab..bda2fb4 100644 --- a/go.work +++ b/go.work @@ -1,10 +1,10 @@ -go 1.24.5 +go 1.25.1 use ( ./parser ./registry ./renderer - ./server + ./server-new ./template ./testdata ./validator diff --git a/go.work.sum b/go.work.sum index 213f195..e2e1c0a 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,4 +1,8 @@ -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e/go.mod h1:xQK6tsgr9BOoeFw8JxjBwDkVENlOqapmcRkYyf/L+SQ= +git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e/go.mod h1:n793S7TENIfgHpZLz0lm0qorM7eCx3zBLby3Fb++hZA= +git.max-richter.dev/max/marka/template v0.0.0-20250819170608-69c2550f448e/go.mod h1:Uxi5xcxtnjopsIZjjMlFaWJGuglB9JNL++FuaSbOf6U= +git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a/go.mod h1:88SkY5pTONkgfBy1FT10LoqRC8rt36iF1fk/rupjuJY= +git.max-richter.dev/max/marka/validator v0.0.0-20250819170608-69c2550f448e/go.mod h1:qdGfCFRzsGedmnd77vb7pu/EMx0W0DcQBMEfvNxMYsw= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= diff --git a/parser/go.mod b/parser/go.mod index fe539c6..769354b 100644 --- a/parser/go.mod +++ b/parser/go.mod @@ -1,6 +1,6 @@ module git.max-richter.dev/max/marka/parser -go 1.24.5 +go 1.25.1 require ( git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567 diff --git a/parser/parser.go b/parser/parser.go index 74150a7..b80065b 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -23,6 +23,8 @@ func DetectType(markdownContent string) (string, error) { return "", fmt.Errorf("failed to compile template: %w", err) } + fmt.Println("Content:", markdownContent) + blocks := matcher.MatchBlocksFuzzy(markdownContent, defaultSchema, 0.3) result, err := decoders.Parse(blocks) @@ -30,6 +32,8 @@ func DetectType(markdownContent string) (string, error) { return "", fmt.Errorf("failed to parse blocks: %w", err) } + fmt.Println("Result: ", result) + if result, ok := result.(map[string]any); ok { if contentType, ok := result["@type"]; ok { return contentType.(string), nil diff --git a/registry/go.mod b/registry/go.mod index 7364278..94c016b 100644 --- a/registry/go.mod +++ b/registry/go.mod @@ -1,3 +1,3 @@ module git.max-richter.dev/max/marka/registry -go 1.24.5 +go 1.25.1 diff --git a/renderer/go.mod b/renderer/go.mod index 558a176..b14358a 100644 --- a/renderer/go.mod +++ b/renderer/go.mod @@ -1,6 +1,6 @@ module git.max-richter.dev/max/marka/renderer -go 1.24.5 +go 1.25.1 require ( git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e diff --git a/server/.air.toml b/server-new/.air.toml similarity index 83% rename from server/.air.toml rename to server-new/.air.toml index c8e2c18..b2eced4 100644 --- a/server/.air.toml +++ b/server-new/.air.toml @@ -1,6 +1,3 @@ -# Config file for Air - https://github.com/air-verse/air -# This file is for the marka-server application. - root = "." tmp_dir = "tmp" diff --git a/server/.gitignore b/server-new/.gitignore similarity index 100% rename from server/.gitignore rename to server-new/.gitignore diff --git a/server/cmd/marka-server/main.go b/server-new/cmd/marka-server/main.go similarity index 69% rename from server/cmd/marka-server/main.go rename to server-new/cmd/marka-server/main.go index ef8a90a..b001bfd 100644 --- a/server/cmd/marka-server/main.go +++ b/server-new/cmd/marka-server/main.go @@ -7,7 +7,8 @@ import ( "os" "path/filepath" - "git.max-richter.dev/max/marka/server/internal/handlers" + "git.max-richter.dev/max/marka/server-new/internal/adapters" + "git.max-richter.dev/max/marka/server-new/internal/handler" ) func main() { @@ -24,7 +25,10 @@ func main() { log.Fatal("root is not a directory") } - http.Handle("/", handlers.NewFile(absRoot)) + fsAdapter, err := adapters.NewLocalFsAdapter(absRoot) + must(err) + + http.Handle("/", handler.NewHandler(fsAdapter)) log.Printf("listening on %s, root=%s", *addr, absRoot) log.Fatal(http.ListenAndServe(*addr, nil)) diff --git a/server-new/go.mod b/server-new/go.mod new file mode 100644 index 0000000..341a24f --- /dev/null +++ b/server-new/go.mod @@ -0,0 +1,3 @@ +module git.max-richter.dev/max/marka/server-new + +go 1.25.1 diff --git a/server/go.sum b/server-new/go.sum similarity index 100% rename from server/go.sum rename to server-new/go.sum diff --git a/server/http/.env b/server-new/http/.env similarity index 100% rename from server/http/.env rename to server-new/http/.env diff --git a/server/http/post-recipe.http b/server-new/http/post-recipe.http similarity index 100% rename from server/http/post-recipe.http rename to server-new/http/post-recipe.http diff --git a/server-new/internal/adapters/errors.go b/server-new/internal/adapters/errors.go new file mode 100644 index 0000000..c121579 --- /dev/null +++ b/server-new/internal/adapters/errors.go @@ -0,0 +1,5 @@ +package adapters + +import "errors" + +var ErrNotFound = errors.New("not found") diff --git a/server-new/internal/adapters/fs.go b/server-new/internal/adapters/fs.go new file mode 100644 index 0000000..cfe95dd --- /dev/null +++ b/server-new/internal/adapters/fs.go @@ -0,0 +1,97 @@ +package adapters + +import ( + "os" + "path/filepath" +) + +type LocalFsAdapter struct { + root string +} + +func (l LocalFsAdapter) readDir(path string) (FsResponse, error) { + dirInfo, _ := os.Stat(path) + + entries, err := os.ReadDir(path) + if err != nil { + return FsResponse{}, err + } + + out := make([]FsDirEntry, 0, len(entries)) + for _, e := range entries { + info, _ := e.Info() + + entryType := "dir" + if !e.IsDir() { + entryType = contentTypeFor(e.Name()) + } + + out = append(out, FsDirEntry{ + Name: e.Name(), + Type: entryType, + ModTime: info.ModTime(), + }) + } + + return FsResponse{ + Dir: &FsDir{ + Files: out, + Name: ResponsePath(l.root, path), + ModTime: dirInfo.ModTime(), + }, + }, nil +} + +func (l LocalFsAdapter) readFile(path string) (FsResponse, error) { + fi, err := os.Stat(path) + if err != nil { + return FsResponse{}, err + } + + data, err := os.ReadFile(path) + if err != nil { + return FsResponse{}, err + } + + return FsResponse{ + File: &FsFile{ + Name: ResponsePath(l.root, path), + Type: contentTypeFor(path), + ModTime: fi.ModTime(), + Content: data, + }, + }, nil +} + +func (l LocalFsAdapter) Read(path string) (FsResponse, error) { + cleanRel, err := SafeRel(l.root, path) + if err != nil { + return FsResponse{}, err + } + target := filepath.Join(l.root, filepath.FromSlash(cleanRel)) + + fi, err := os.Stat(target) + if err != nil { + if os.IsNotExist(err) { + return FsResponse{}, ErrNotFound + } + + return FsResponse{}, err + } + + if fi.IsDir() { + return l.readDir(target) + } + + return l.readFile(target) +} + +func (LocalFsAdapter) Write(path string, content []byte) error { + return nil +} + +func NewLocalFsAdapter(root string) (FileAdapter, error) { + return LocalFsAdapter{ + root: root, + }, nil +} diff --git a/server-new/internal/adapters/fs_utils.go b/server-new/internal/adapters/fs_utils.go new file mode 100644 index 0000000..8ebdcde --- /dev/null +++ b/server-new/internal/adapters/fs_utils.go @@ -0,0 +1,78 @@ +package adapters + +import ( + "errors" + "mime" + "os" + "path/filepath" + "strings" + "time" +) + +func SafeRel(root, requested string) (string, error) { + s := requested + if after, ok := strings.CutPrefix(s, "/"); ok { + s = after + } + full := filepath.Join(root, filepath.FromSlash(s)) + rel, err := filepath.Rel(root, full) + if err != nil { + return "", err + } + if rel == "." { + return "/", nil + } + sep := string(filepath.Separator) + if strings.HasPrefix(rel, "..") || strings.Contains(rel, ".."+sep) { + return "", errors.New("path escapes root") + } + return "/" + filepath.ToSlash(rel), nil +} + +func ResponsePath(root, full string) string { + rel, err := filepath.Rel(root, full) + if err != nil || rel == "." { + return "/" + } + return "/" + filepath.ToSlash(rel) +} + +func sizeOrZero(fi os.FileInfo) int64 { + if fi == nil { + return 0 + } + return fi.Size() +} + +func modTimeOrZero(fi os.FileInfo) time.Time { + if fi == nil { + return time.Time{} + } + return fi.ModTime() +} + +var textPlainExtensions = map[string]bool{ + ".txt": true, + ".log": true, + ".json": true, + ".yaml": true, + ".yml": true, + ".toml": true, + ".xml": true, + ".csv": true, +} + +func contentTypeFor(name string) string { + ext := strings.ToLower(filepath.Ext(name)) + switch ext { + case ".md", ".markdown", ".mdown": + return "application/markdown" + } + if ct := mime.TypeByExtension(ext); ct != "" { + return ct + } + if textPlainExtensions[ext] { + return "text/plain; charset=utf-8" + } + return "application/octet-stream" +} diff --git a/server-new/internal/adapters/interface.go b/server-new/internal/adapters/interface.go new file mode 100644 index 0000000..a482676 --- /dev/null +++ b/server-new/internal/adapters/interface.go @@ -0,0 +1,34 @@ +// Package adapters are the backend to that connects the marka server to a storage +package adapters + +import "time" + +type FileAdapter interface { + Read(path string) (FsResponse, error) + Write(path string, content []byte) error +} + +type FsFile struct { + Name string `json:"name"` + Type string `json:"type"` + Content []byte `json:"content"` + ModTime time.Time `json:"modTime"` +} + +type FsDirEntry struct { + Name string `json:"name"` + Type string `json:"type"` + IsDir bool `json:"isDir,omitempty"` + ModTime time.Time `json:"modTime"` +} + +type FsDir struct { + Files []FsDirEntry `json:"files"` + Name string `json:"name"` + ModTime time.Time `json:"modTime"` +} + +type FsResponse struct { + Dir *FsDir `json:"dir,omitempty"` + File *FsFile `json:"file,omitempty"` +} diff --git a/server/internal/httpx/error.go b/server-new/internal/handler/error.go similarity index 52% rename from server/internal/httpx/error.go rename to server-new/internal/handler/error.go index 7c88320..37fcc3d 100644 --- a/server/internal/httpx/error.go +++ b/server-new/internal/handler/error.go @@ -1,4 +1,4 @@ -package httpx +package handler import ( "encoding/json" @@ -9,12 +9,12 @@ type ErrorResponse struct { Error string `json:"error"` } -func WriteJSON(w http.ResponseWriter, code int, v any) { +func writeError(w http.ResponseWriter, code int, err error) { + writeJSON(w, code, ErrorResponse{Error: err.Error()}) +} + +func writeJSON(w http.ResponseWriter, code int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(v) } - -func WriteError(w http.ResponseWriter, code int, err error) { - WriteJSON(w, code, ErrorResponse{Error: err.Error()}) -} diff --git a/server-new/internal/handler/handler.go b/server-new/internal/handler/handler.go new file mode 100644 index 0000000..9a1f13e --- /dev/null +++ b/server-new/internal/handler/handler.go @@ -0,0 +1,104 @@ +// Package handler provides the HTTP handler for the marka server +package handler + +import ( + "errors" + "net/http" + "time" + + "git.max-richter.dev/max/marka/parser" + "git.max-richter.dev/max/marka/server-new/internal/adapters" +) + +type ResponseItem struct { + Name string `json:"name"` + Content any `json:"content,omitempty"` + Type string `json:"type,omitempty"` + IsDir bool `json:"isDir"` + Size int64 `json:"size,omitempty"` + ModTime time.Time `json:"modTime"` +} + +type Handler struct { + adapter adapters.FileAdapter +} + +func (h *Handler) get(w http.ResponseWriter, target string) { + fsEntry, err := h.adapter.Read(target) + if err != nil { + writeError(w, 500, err) + return + } + + if fsEntry.File != nil { + + if fsEntry.File.Content != nil && fsEntry.File.Type == "application/markdown" { + data, err := parser.ParseFile(string(fsEntry.File.Content)) + if err != nil { + writeError(w, 500, err) + return + } + + res := ResponseItem{ + Name: fsEntry.File.Name, + Type: fsEntry.File.Type, + Content: data, + IsDir: false, + Size: int64(len(fsEntry.File.Content)), + ModTime: fsEntry.File.ModTime, + } + + writeJSON(w, 200, res) + return + } + + res := ResponseItem{ + Name: fsEntry.File.Name, + Content: fsEntry.File.Content, + Type: fsEntry.File.Type, + IsDir: false, + Size: int64(len(fsEntry.File.Content)), + ModTime: fsEntry.File.ModTime, + } + writeJSON(w, 200, res) + return + } + + if fsEntry.Dir != nil { + res := ResponseItem{ + Name: fsEntry.Dir.Name, + Content: fsEntry.Dir.Files, + IsDir: true, + ModTime: fsEntry.Dir.ModTime, + } + writeJSON(w, 200, res) + return + } +} + +func (h *Handler) post(w http.ResponseWriter, target string) { +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + reqPath := r.URL.Path + if reqPath == "" { + reqPath = "/" + } + + target := cleanURLLike(reqPath) + + switch r.Method { + case http.MethodGet: + h.get(w, target) + case http.MethodPost: + h.post(w, target) + default: + writeError(w, http.StatusMethodNotAllowed, errors.New("method not allowed")) + } +} + +func NewHandler(adapter adapters.FileAdapter) http.Handler { + return &Handler{ + adapter: adapter, + } +} diff --git a/server-new/internal/handler/utils.go b/server-new/internal/handler/utils.go new file mode 100644 index 0000000..1869ced --- /dev/null +++ b/server-new/internal/handler/utils.go @@ -0,0 +1,24 @@ +package handler + +import "strings" + +func cleanURLLike(p string) string { + p = strings.TrimSpace(p) + if p == "" || p == "/" { + return "/" + } + parts := []string{} + for seg := range strings.SplitSeq(p, "/") { + switch seg { + case "", ".": + continue + case "..": + if len(parts) > 0 { + parts = parts[:len(parts)-1] + } + default: + parts = append(parts, seg) + } + } + return "/" + strings.Join(parts, "/") +} diff --git a/server/internal/handlers/get.go b/server/internal/handlers/get.go deleted file mode 100644 index e1fc5cc..0000000 --- a/server/internal/handlers/get.go +++ /dev/null @@ -1,93 +0,0 @@ -package handlers - -import ( - "errors" - "fmt" - "net/http" - "os" - "path/filepath" - "time" - - "git.max-richter.dev/max/marka/server/internal/fsx" - "git.max-richter.dev/max/marka/server/internal/httpx" -) - -func (h *File) get(w http.ResponseWriter, r *http.Request, target string) { - fi, err := os.Stat(target) - if err != nil { - if os.IsNotExist(err) { - httpx.WriteError(w, http.StatusNotFound, errors.New("not found")) - } else { - httpx.WriteError(w, http.StatusInternalServerError, err) - } - return - } - - if fi.IsDir() { - h.handleDir(w, r, target) - } else { - h.handleFile(w, r, target, fi) - } -} - -func (h *File) handleDir(w http.ResponseWriter, r *http.Request, dirPath string) { - entries, err := os.ReadDir(dirPath) - if err != nil { - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - - type item struct { - Name string `json:"name"` - Path string `json:"path"` - Content any `json:"content,omitempty"` - IsDir bool `json:"isDir"` - Size int64 `json:"size"` - ModTime time.Time `json:"modTime"` - } - - out := make([]item, 0, len(entries)) - for _, e := range entries { - info, _ := e.Info() // As in original, ignore error to get partial info - entryPath := filepath.Join(dirPath, e.Name()) - - var content any - if !e.IsDir() && fsx.ContentTypeFor(e.Name()) == "application/markdown" { - content, _ = parseMarkdownFile(entryPath) // As in original, ignore error - } - - out = append(out, item{ - Name: e.Name(), - Path: fsx.ResponsePath(h.root, entryPath), - IsDir: e.IsDir(), - Content: content, - Size: sizeOrZero(info), - ModTime: modTimeOrZero(info), - }) - } - httpx.WriteJSON(w, http.StatusOK, out) -} - -func (h *File) handleFile(w http.ResponseWriter, r *http.Request, target string, fi os.FileInfo) { - contentType := fsx.ContentTypeFor(target) - - if contentType == "application/markdown" { - res, err := parseMarkdownFile(target) - if err != nil { - httpx.WriteError(w, http.StatusInternalServerError, fmt.Errorf("failed to parse markdown: %w", err)) - return - } - httpx.WriteJSON(w, http.StatusOK, res) - return - } - - f, err := os.Open(target) - if err != nil { - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - defer f.Close() - - w.Header().Set("Content-Type", contentType) - http.ServeContent(w, r, filepath.Base(target), fi.ModTime(), f) -} diff --git a/server/internal/handlers/helpers.go b/server/internal/handlers/helpers.go deleted file mode 100644 index 8cca076..0000000 --- a/server/internal/handlers/helpers.go +++ /dev/null @@ -1,30 +0,0 @@ -package handlers - -import ( - "os" - "time" - - "git.max-richter.dev/max/marka/parser" -) - -func parseMarkdownFile(path string) (any, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - return parser.ParseFile(string(data)) -} - -func sizeOrZero(fi os.FileInfo) int64 { - if fi == nil { - return 0 - } - return fi.Size() -} - -func modTimeOrZero(fi os.FileInfo) time.Time { - if fi == nil { - return time.Time{} - } - return fi.ModTime() -} diff --git a/server/internal/handlers/post.go b/server/internal/handlers/post.go deleted file mode 100644 index d7d23bc..0000000 --- a/server/internal/handlers/post.go +++ /dev/null @@ -1,47 +0,0 @@ -package handlers - -import ( - "io" - "net/http" - "os" - "path/filepath" - - "git.max-richter.dev/max/marka/server/internal/httpx" -) - -func (h *File) post(w http.ResponseWriter, r *http.Request, target string) { - if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - - tmpPath := target + ".tmp~" - f, err := os.Create(tmpPath) - if err != nil { - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - - _, copyErr := io.Copy(f, r.Body) - closeErr := f.Close() - - if copyErr != nil { - os.Remove(tmpPath) - httpx.WriteError(w, http.StatusInternalServerError, copyErr) - return - } - if closeErr != nil { - os.Remove(tmpPath) - httpx.WriteError(w, http.StatusInternalServerError, closeErr) - return - } - - if err := os.Rename(tmpPath, target); err != nil { - os.Remove(tmpPath) - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - - w.Header().Set("Location", r.URL.Path) - w.WriteHeader(http.StatusCreated) -} diff --git a/server/internal/httpx/router.go b/server/internal/httpx/router.go deleted file mode 100644 index dfdeec7..0000000 --- a/server/internal/httpx/router.go +++ /dev/null @@ -1,11 +0,0 @@ -package httpx - -import "net/http" - -type Router struct{ mux *http.ServeMux } - -func NewRouter() *Router { return &Router{mux: http.NewServeMux()} } -func (r *Router) Handle(pattern string, h http.Handler) { r.mux.Handle(pattern, h) } -func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { - r.mux.ServeHTTP(w, req) -} diff --git a/template/go.mod b/template/go.mod index 169d84c..2cd6cd1 100644 --- a/template/go.mod +++ b/template/go.mod @@ -1,6 +1,6 @@ module git.max-richter.dev/max/marka/template -go 1.24.5 +go 1.25.1 require ( git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e diff --git a/testdata/go.mod b/testdata/go.mod index dd01c9b..6515ee9 100644 --- a/testdata/go.mod +++ b/testdata/go.mod @@ -1,3 +1,3 @@ module git.max-richter.dev/max/marka/testdata -go 1.24.5 +go 1.25.1 diff --git a/validator/go.mod b/validator/go.mod index f278b87..358e582 100644 --- a/validator/go.mod +++ b/validator/go.mod @@ -1,6 +1,6 @@ module git.max-richter.dev/max/marka/validator -go 1.24.5 +go 1.25.1 require ( git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e