// Package handlers provides HTTP handlers for the file system. package handlers import ( "errors" "fmt" "io" "net/http" "os" "path/filepath" "time" "git.max-richter.dev/max/marka/parser" "git.max-richter.dev/max/marka/server/internal/fsx" "git.max-richter.dev/max/marka/server/internal/httpx" ) type File struct{ root string } func NewFile(root string) http.Handler { return &File{root: root} } func (h *File) ServeHTTP(w http.ResponseWriter, r *http.Request) { reqPath := r.URL.Path if reqPath == "" { reqPath = "/" } cleanRel, err := fsx.SafeRel(h.root, reqPath) if err != nil { httpx.WriteError(w, http.StatusBadRequest, err) return } target := filepath.Join(h.root, filepath.FromSlash(cleanRel)) switch r.Method { case http.MethodGet: h.get(w, r, target) case http.MethodPost: h.post(w, r, target) default: httpx.WriteError(w, http.StatusMethodNotAllowed, errors.New("method not allowed")) } } func read(path string) any { data, err := os.ReadFile(path) if err != nil { return nil } res, err := parser.ParseFile(string(data)) if err != nil { return nil } return res } 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() { entries, err := os.ReadDir(target) 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"` 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() out = append(out, item{ Name: e.Name(), Path: fsx.ResponsePath(h.root, filepath.Join(target, e.Name())), IsDir: e.IsDir(), Content: read(filepath.Join(target, e.Name())), Size: sizeOrZero(info), ModTime: modTimeOrZero(info), }) } httpx.WriteJSON(w, http.StatusOK, out) return } f, err := os.Open(target) if err != nil { httpx.WriteError(w, http.StatusInternalServerError, err) return } defer f.Close() contentType := fsx.ContentTypeFor(target) if contentType == "application/markdown" { data, err := io.ReadAll(f) if err != nil { httpx.WriteError(w, http.StatusInternalServerError, err) return } res, err := parser.ParseFile(string(data)) if err != nil { fmt.Println(err) } w.Header().Set("Content-Type", "application/json") httpx.WriteJSON(w, http.StatusOK, res) return } w.Header().Set("Content-Type", contentType) http.ServeContent(w, r, filepath.Base(target), fi.ModTime(), f) } 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 } tmp := target + ".tmp~" f, err := os.Create(tmp) if err != nil { httpx.WriteError(w, http.StatusInternalServerError, err) return } if _, err := io.Copy(f, r.Body); err != nil { _ = f.Close() _ = os.Remove(tmp) httpx.WriteError(w, http.StatusInternalServerError, err) return } if err := f.Close(); err != nil { _ = os.Remove(tmp) httpx.WriteError(w, http.StatusInternalServerError, err) return } if err := os.Rename(tmp, target); err != nil { _ = os.Remove(tmp) httpx.WriteError(w, http.StatusInternalServerError, err) return } w.Header().Set("Location", r.URL.Path) w.WriteHeader(http.StatusCreated) } 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() }