This commit is contained in:
Max Richter
2025-09-24 21:32:27 +02:00
parent 26c303d9cf
commit 733ae876b9
27 changed files with 374 additions and 201 deletions

View File

@@ -1,32 +0,0 @@
# Config file for Air - https://github.com/air-verse/air
# This file is for the marka-server application.
root = "."
tmp_dir = "tmp"
[build]
# Command to build the application.
cmd = "go build -o ./tmp/marka-server ./cmd/marka-server"
# The binary to run.
bin = "./tmp/marka-server"
# Command to run the application with arguments.
full_bin = "./tmp/marka-server -root=../examples -addr=:8080"
# Watch these file extensions.
include_ext = ["go", "http"]
# Ignore these directories.
exclude_dir = ["tmp"]
# Log file for build errors.
log = "air_errors.log"
[log]
# Show time in logs.
time = true
[misc]
# Delete tmp directory on exit.
clean_on_exit = true

1
server/.gitignore vendored
View File

@@ -1 +0,0 @@
tmp/

View File

@@ -1,37 +0,0 @@
package main
import (
"flag"
"log"
"net/http"
"os"
"path/filepath"
"git.max-richter.dev/max/marka/server/internal/handlers"
)
func main() {
root := flag.String("root", ".", "filesystem root to serve")
addr := flag.String("addr", ":8080", "listen address")
flag.Parse()
absRoot, err := filepath.Abs(*root)
must(err)
info, err := os.Stat(absRoot)
must(err)
if !info.IsDir() {
log.Fatal("root is not a directory")
}
http.Handle("/", handlers.NewFile(absRoot))
log.Printf("listening on %s, root=%s", *addr, absRoot)
log.Fatal(http.ListenAndServe(*addr, nil))
}
func must(err error) {
if err != nil {
log.Fatal(err)
}
}

View File

@@ -1,22 +0,0 @@
git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e h1:enZufetD3UoIVTnTNTQSFlr1Ir0jG7wObUAxb6+xwWg=
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-20250817132016-6db87db32567 h1:oe7Xb8dE43S8mRla5hfEqagMnvhvEVHsvRlzl2v540w=
git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567/go.mod h1:qGWl42P8mgEktfor/IjQp0aS9SqmpeIlhSuVTlUOXLQ=
git.max-richter.dev/max/marka/renderer v0.0.0-20250819170608-69c2550f448e h1:9Eg81l8YMTXWZC3xlZ5L/NJRuK26bksrVtEHyCTV4sM=
git.max-richter.dev/max/marka/renderer v0.0.0-20250819170608-69c2550f448e/go.mod h1:mjJqEqALg4YJoiebk3V21yJVUVEs3K2RiLO/IW6DGCM=
git.max-richter.dev/max/marka/template v0.0.0-20250817132016-6db87db32567 h1:XIx89KqTgd/h14oe5mLvT9E8+jGEAjWgudqiMtQdcec=
git.max-richter.dev/max/marka/template v0.0.0-20250817132016-6db87db32567/go.mod h1:Uxi5xcxtnjopsIZjjMlFaWJGuglB9JNL++FuaSbOf6U=
git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a h1:LAU2LlLZ96s8hcg1OEGD5HBshDspWVwWTa7YG5+A70w=
git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a/go.mod h1:88SkY5pTONkgfBy1FT10LoqRC8rt36iF1fk/rupjuJY=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
go.yaml.in/yaml/v4 v4.0.0-rc.1 h1:4J1+yLKUIPGexM/Si+9d3pij4hdc7aGO04NhrElqXbY=
go.yaml.in/yaml/v4 v4.0.0-rc.1/go.mod h1:CBdeces52/nUXndfQ5OY8GEQuNR9uEEOJPZj/Xq5IzU=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=

View File

@@ -1 +0,0 @@
SERVER_URL=http://localhost:8080

View File

@@ -1,5 +0,0 @@
POST {{SERVER_URL}}/Recipes/bolognaise.md
{
}

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -1,20 +0,0 @@
package httpx
import (
"encoding/json"
"net/http"
)
type ErrorResponse struct {
Error string `json:"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()})
}

View File

@@ -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)
}