diff --git a/registry/templates/Recipe.marka b/registry/templates/Recipe.marka index 0bba9fc..0bf9abb 100644 --- a/registry/templates/Recipe.marka +++ b/registry/templates/Recipe.marka @@ -10,6 +10,7 @@ - path: "_type" codec: const value: Recipe + - path: link - path: image - path: author._type codec: const diff --git a/server/cmd/marka-server/main.go b/server/cmd/marka-server/main.go index b001bfd..9bce64c 100644 --- a/server/cmd/marka-server/main.go +++ b/server/cmd/marka-server/main.go @@ -6,31 +6,48 @@ import ( "net/http" "os" "path/filepath" + "strings" "git.max-richter.dev/max/marka/server-new/internal/adapters" "git.max-richter.dev/max/marka/server-new/internal/handler" ) +type multi []string + +func (m *multi) String() string { return strings.Join(*m, ",") } +func (m *multi) Set(v string) error { + *m = append(*m, v) + return nil +} + func main() { - root := flag.String("root", ".", "filesystem root to serve") + var roots multi + flag.Var(&roots, "root", "repeatable; specify multiple -root flags") 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") + if len(roots) == 0 { + log.Fatal("at least one -root flag must be specified") } - fsAdapter, err := adapters.NewLocalFsAdapter(absRoot) + absRoots := make([]string, len(roots)) + for i, r := range roots { + abs, err := filepath.Abs(r) + must(err) + info, err := os.Stat(abs) + must(err) + if !info.IsDir() { + log.Fatalf("root %s is not a directory", r) + } + absRoots[i] = abs + } + + fsAdapter, err := adapters.NewLocalFsAdapter(absRoots) must(err) http.Handle("/", handler.NewHandler(fsAdapter)) - log.Printf("listening on %s, root=%s", *addr, absRoot) + log.Printf("listening on %s, roots=%s", *addr, strings.Join(absRoots, ", ")) log.Fatal(http.ListenAndServe(*addr, nil)) } diff --git a/server/internal/adapters/fs.go b/server/internal/adapters/fs.go index cfe95dd..c3b80b9 100644 --- a/server/internal/adapters/fs.go +++ b/server/internal/adapters/fs.go @@ -1,16 +1,22 @@ package adapters import ( + "errors" "os" "path/filepath" + "strings" + "time" ) type LocalFsAdapter struct { - root string + roots []string } -func (l LocalFsAdapter) readDir(path string) (FsResponse, error) { - dirInfo, _ := os.Stat(path) +func (l LocalFsAdapter) readDir(path string, root string) (FsResponse, error) { + dirInfo, err := os.Stat(path) + if err != nil { + return FsResponse{}, err + } entries, err := os.ReadDir(path) if err != nil { @@ -29,20 +35,31 @@ func (l LocalFsAdapter) readDir(path string) (FsResponse, error) { out = append(out, FsDirEntry{ Name: e.Name(), Type: entryType, + IsDir: e.IsDir(), ModTime: info.ModTime(), }) } + relPath, err := filepath.Rel(root, path) + if err != nil { + return FsResponse{}, err + } + if relPath == "." { + relPath = "" + } + + responsePath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), relPath)) + return FsResponse{ Dir: &FsDir{ Files: out, - Name: ResponsePath(l.root, path), + Name: responsePath, ModTime: dirInfo.ModTime(), }, }, nil } -func (l LocalFsAdapter) readFile(path string) (FsResponse, error) { +func (l LocalFsAdapter) readFile(path string, root string) (FsResponse, error) { fi, err := os.Stat(path) if err != nil { return FsResponse{}, err @@ -53,9 +70,16 @@ func (l LocalFsAdapter) readFile(path string) (FsResponse, error) { return FsResponse{}, err } + relPath, err := filepath.Rel(root, path) + if err != nil { + return FsResponse{}, err + } + + responsePath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), relPath)) + return FsResponse{ File: &FsFile{ - Name: ResponsePath(l.root, path), + Name: responsePath, Type: contentTypeFor(path), ModTime: fi.ModTime(), Content: data, @@ -64,11 +88,66 @@ func (l LocalFsAdapter) readFile(path string) (FsResponse, error) { } func (l LocalFsAdapter) Read(path string) (FsResponse, error) { - cleanRel, err := SafeRel(l.root, path) + if path == "/" { + entries := make([]FsDirEntry, 0, len(l.roots)) + var latestModTime time.Time + for _, r := range l.roots { + info, err := os.Stat(r) + if err != nil { + continue + } + entries = append(entries, FsDirEntry{ + Name: filepath.Base(r), + Type: "dir", + IsDir: true, + ModTime: info.ModTime(), + }) + if info.ModTime().After(latestModTime) { + latestModTime = info.ModTime() + } + } + + return FsResponse{ + Dir: &FsDir{ + Files: entries, + Name: "/", + ModTime: latestModTime, + }, + }, nil + } + + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) == 0 || pathParts[0] == "" { + return FsResponse{}, ErrNotFound + } + + rootIdentifier := pathParts[0] + var targetRoot string + for _, r := range l.roots { + if filepath.Base(r) == rootIdentifier { + targetRoot = r + break + } + } + + if targetRoot == "" { + return FsResponse{}, ErrNotFound + } + + subPath := filepath.Join(pathParts[1:]...) + target := filepath.Join(targetRoot, subPath) + + absTarget, err := filepath.Abs(target) if err != nil { return FsResponse{}, err } - target := filepath.Join(l.root, filepath.FromSlash(cleanRel)) + absRoot, err := filepath.Abs(targetRoot) + if err != nil { + return FsResponse{}, err + } + if !strings.HasPrefix(absTarget, absRoot) { + return FsResponse{}, errors.New("path escapes root") + } fi, err := os.Stat(target) if err != nil { @@ -80,18 +159,19 @@ func (l LocalFsAdapter) Read(path string) (FsResponse, error) { } if fi.IsDir() { - return l.readDir(target) + return l.readDir(target, targetRoot) } - return l.readFile(target) + return l.readFile(target, targetRoot) } func (LocalFsAdapter) Write(path string, content []byte) error { return nil } -func NewLocalFsAdapter(root string) (FileAdapter, error) { +func NewLocalFsAdapter(roots []string) (FileAdapter, error) { return LocalFsAdapter{ - root: root, + roots: roots, }, nil } +