Compare commits

...

13 Commits

Author SHA1 Message Date
Max Richter
137e3da6d4 fix: some stuff
All checks were successful
Build and Push Server / build-and-push (push) Successful in 3m12s
2025-11-04 12:42:15 +01:00
Max Richter
148bc2c15c fix: some stuff
Some checks failed
Build and Push Server / build-and-push (push) Failing after 2m55s
2025-11-04 12:38:24 +01:00
Max Richter
cc19724277 chore: update
Some checks failed
Build and Push Server / build-and-push (push) Failing after 4m8s
2025-11-04 12:26:38 +01:00
Max Richter
851babe1e0 fix: dont writeJSON twice
All checks were successful
Build and Push Server / build-and-push (push) Successful in 2m51s
2025-10-31 20:08:33 +01:00
Max Richter
f14b685e43 fix: make sure we dont try to decode jpgs into json
All checks were successful
Build and Push Server / build-and-push (push) Successful in 3m17s
2025-10-31 19:39:44 +01:00
Max Richter
fd6aeb8a27 feat: add some more detailed errors
All checks were successful
Build and Push Server / build-and-push (push) Successful in 2m54s
2025-10-31 19:32:36 +01:00
Max Richter
4bfd2b9450 fix: return content of written article
All checks were successful
Build and Push Server / build-and-push (push) Successful in 2m51s
2025-10-31 19:18:45 +01:00
Max Richter
dfc38e2c86 fix: make templates work
All checks were successful
Build and Push Server / build-and-push (push) Successful in 2m57s
2025-10-31 18:16:33 +01:00
Max Richter
3fe7577250 fix: return json from write post
All checks were successful
Build and Push Server / build-and-push (push) Successful in 2m53s
2025-10-31 17:27:15 +01:00
Max Richter
3cd320637c fix: return status ok for write
All checks were successful
Build and Push Server / build-and-push (push) Successful in 6m31s
2025-10-31 15:24:48 +01:00
Max Richter
7c5f9f7829 feat: add some log
All checks were successful
Build and Push Server / build-and-push (push) Successful in 13m1s
2025-10-31 14:41:46 +01:00
Max Richter
6c47c8c3e9 feat: protect post route with MARKA_API_KEY
Some checks failed
Build and Push Server / build-and-push (push) Has been cancelled
2025-10-31 14:28:30 +01:00
Max Richter
814be43be8 feat: allow to write to notes 2025-10-31 14:16:32 +01:00
9 changed files with 132 additions and 6 deletions

View File

@@ -29,11 +29,13 @@
value: 5
codec: const
hidden: true
- path: keywords
- path: reviewRating.worstRating
value: 1
codec: const
hidden: true
}
---
# { headline }
{ articleBody }

View File

@@ -34,6 +34,7 @@
- path: suitableForDiet
}
---
# { name | text }
{ description | text }

View File

@@ -35,5 +35,6 @@
- path: reviewBody
}
---
# { itemReviewed.name }
{ reviewBody }

View File

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

View File

@@ -9,7 +9,7 @@ cmd = "go build -o ./tmp/marka-server ./cmd/marka-server"
bin = "./tmp/marka-server"
# Command to run the application with arguments.
full_bin = "./tmp/marka-server -root=/home/max/Notes/resources -addr=:8080 -playground-root=./playground"
full_bin = "MARKA_API_KEY='SECRET' ./tmp/marka-server -root=/home/max/Notes/resources -addr=:8080 -playground-root=./playground"
# Watch these file extensions.
include_ext = ["go", "http"]

View File

@@ -2,6 +2,7 @@ package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
@@ -61,7 +62,9 @@ func main() {
fsAdapter, err := adapters.NewLocalFsAdapter(absRoots)
must(err)
http.Handle("/", handler.NewHandler(fsAdapter))
apiKey := os.Getenv("MARKA_API_KEY")
fmt.Println(apiKey)
http.Handle("/", handler.NewHandler(fsAdapter, apiKey))
log.Printf("listening on %s, roots=%s", *addr, strings.Join(absRoots, ", "))
log.Fatal(http.ListenAndServe(*addr, nil))

View File

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

View File

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

View File

@@ -2,14 +2,20 @@
package handler
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"git.max-richter.dev/max/marka/renderer"
"git.max-richter.dev/max/marka/server/internal/adapters"
)
type Handler struct {
adapter adapters.FileAdapter
apiKey string
}
func (h *Handler) get(w http.ResponseWriter, target string) {
@@ -44,7 +50,55 @@ 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) {
if h.apiKey != "" {
if r.Header.Get("Authentication") != h.apiKey {
writeError(w, http.StatusUnauthorized, errors.New("invalid api key"))
return
}
} else {
writeError(w, http.StatusUnauthorized, errors.New("invalid api key"))
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, fmt.Errorf("failed to decode body: %w", err))
return
}
defer r.Body.Close()
contentType := r.Header.Get("Content-Type")
isResource := strings.HasPrefix(contentType, "application/json") && strings.HasSuffix(target, ".md")
var contentToWrite []byte
if isResource {
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
}
if isResource {
var data map[string]any
if err := json.Unmarshal(body, &data); err != nil && strings.HasSuffix(target, ".md") {
writeError(w, http.StatusInternalServerError, fmt.Errorf("failed to decode body: %w", err))
return
}
writeJSON(w, http.StatusOK, data)
return
}
writeJSON(w, http.StatusOK, map[string]any{"success": true})
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -55,18 +109,21 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
target := cleanURLLike(reqPath)
fmt.Printf("[serve] %s %s\n", r.Method, target)
switch r.Method {
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"))
}
}
func NewHandler(adapter adapters.FileAdapter) http.Handler {
func NewHandler(adapter adapters.FileAdapter, apiKey string) http.Handler {
return &Handler{
adapter: adapter,
apiKey: apiKey,
}
}