diff --git a/registry/templates/Article.marka b/registry/templates/Article.marka index bc1d532..a282835 100644 --- a/registry/templates/Article.marka +++ b/registry/templates/Article.marka @@ -29,11 +29,14 @@ value: 5 codec: const hidden: true + - path: keywords - path: reviewRating.worstRating value: 1 codec: const hidden: true } --- + # { headline } + { articleBody } diff --git a/registry/templates/Recipe.marka b/registry/templates/Recipe.marka index feeb85e..daf66b3 100644 --- a/registry/templates/Recipe.marka +++ b/registry/templates/Recipe.marka @@ -34,7 +34,9 @@ - path: suitableForDiet } --- + # { name | text } + { description | text } ## Ingredients diff --git a/registry/templates/Review.marka b/registry/templates/Review.marka index 6412db6..d34cfe2 100644 --- a/registry/templates/Review.marka +++ b/registry/templates/Review.marka @@ -35,5 +35,7 @@ - path: reviewBody } --- + # { itemReviewed.name } + { reviewBody } diff --git a/renderer/renderer.go b/renderer/renderer.go index de7556c..0b3f670 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -37,7 +37,7 @@ func RenderFile(rawJSON []byte) ([]byte, error) { } // 5) validate JSON against schema - if schemaName, ok := data["@schema"].(string); ok { + if schemaName, ok := data["_schema"].(string); ok { if validationErr := validator.ValidateSchema(data, schemaName); validationErr != nil { return nil, fmt.Errorf("failed to validate schema: %w", validationErr) } diff --git a/server/internal/adapters/fs.go b/server/internal/adapters/fs.go index 1cd48ce..18a0e9d 100644 --- a/server/internal/adapters/fs.go +++ b/server/internal/adapters/fs.go @@ -257,6 +257,66 @@ func (l *LocalFsAdapter) Read(path string) (*Entry, error) { } func (l *LocalFsAdapter) Write(path string, content []byte) error { + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) == 0 || pathParts[0] == "" { + return errors.New("invalid path") + } + + rootIdentifier := pathParts[0] + var targetRoot string + for _, r := range l.roots { + if filepath.Base(r) == rootIdentifier { + targetRoot = r + break + } + } + + if targetRoot == "" { + return ErrNotFound + } + + subPath := filepath.Join(pathParts[1:]...) + target := filepath.Join(targetRoot, subPath) + + absTarget, err := filepath.Abs(target) + if err != nil { + return err + } + absRoot, err := filepath.Abs(targetRoot) + if err != nil { + return err + } + if !strings.HasPrefix(absTarget, absRoot) { + return errors.New("path escapes root") + } + + dir := filepath.Dir(absTarget) + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + err = os.WriteFile(absTarget, content, 0o644) + if err != nil { + return err + } + + // Invalidate cache + l.mu.Lock() + defer l.mu.Unlock() + currentPath := path + for { + delete(l.cache, currentPath) + if currentPath == "/" { + break + } + lastSlash := strings.LastIndex(currentPath, "/") + if lastSlash <= 0 { + currentPath = "/" + } else { + currentPath = currentPath[:lastSlash] + } + } + return nil } diff --git a/server/internal/handler/error.go b/server/internal/handler/error.go index 37fcc3d..cdf875a 100644 --- a/server/internal/handler/error.go +++ b/server/internal/handler/error.go @@ -2,6 +2,7 @@ package handler import ( "encoding/json" + "log" "net/http" ) @@ -10,6 +11,7 @@ type ErrorResponse struct { } func writeError(w http.ResponseWriter, code int, err error) { + log.Printf("error: %s", err) writeJSON(w, code, ErrorResponse{Error: err.Error()}) } diff --git a/server/internal/handler/handler.go b/server/internal/handler/handler.go index 0b063fc..7cbca84 100644 --- a/server/internal/handler/handler.go +++ b/server/internal/handler/handler.go @@ -3,8 +3,12 @@ package handler import ( "errors" + "fmt" + "io" "net/http" + "strings" + "git.max-richter.dev/max/marka/renderer" "git.max-richter.dev/max/marka/server/internal/adapters" ) @@ -44,7 +48,35 @@ func (h *Handler) get(w http.ResponseWriter, target string) { writeError(w, http.StatusInternalServerError, errors.New("unknown entry type")) } -func (h *Handler) post(w http.ResponseWriter, target string) { +func (h *Handler) post(w http.ResponseWriter, r *http.Request, target string) { + body, err := io.ReadAll(r.Body) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + defer r.Body.Close() + + contentType := r.Header.Get("Content-Type") + isJSON := strings.HasPrefix(contentType, "application/json") + + var contentToWrite []byte + if strings.HasSuffix(target, ".md") && isJSON { + renderedContent, err := renderer.RenderFile(body) + if err != nil { + writeError(w, http.StatusInternalServerError, fmt.Errorf("failed to render file: %w", err)) + return + } + contentToWrite = renderedContent + } else { + contentToWrite = body + } + + if err := h.adapter.Write(target, contentToWrite); err != nil { + writeError(w, http.StatusInternalServerError, fmt.Errorf("failed to write file: %w", err)) + return + } + + w.WriteHeader(http.StatusNoContent) } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -59,7 +91,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { case http.MethodGet: h.get(w, target) case http.MethodPost: - h.post(w, target) + h.post(w, r, target) default: writeError(w, http.StatusMethodNotAllowed, errors.New("method not allowed")) }