diff --git a/chfs-ng/cmd/chfs/main.go b/chfs-ng/cmd/chfs/main.go index cef5734..a02df5d 100644 --- a/chfs-ng/cmd/chfs/main.go +++ b/chfs-ng/cmd/chfs/main.go @@ -53,17 +53,21 @@ func main() { } root := cfg.Path[0] - if len(cfg.Path) > 1 { - log.Printf("warning: %d shared roots configured; v1 compatibility currently serves the first root %q", len(cfg.Path), root) - } - if err := os.MkdirAll(root, 0755); err != nil { - log.Fatal(err) + for _, p := range cfg.Path { + if err := os.MkdirAll(p, 0755); err != nil { + log.Fatal(err) + } } mgr, err := auth.ParseRules(cfg.Rules) if err != nil { log.Fatal(err) } - srv := &api.Server{Store: storage.Local{Root: root}, Auth: mgr, Sessions: map[string]*auth.Account{}, WebDAVRoot: root, Allow: cfg.Allow} + multi := storage.NewMulti(cfg.Path) + store := storage.Storage(multi) + if len(cfg.Path) == 1 { + store = storage.Local{Root: root} + } + srv := &api.Server{Store: store, Auth: mgr, Sessions: map[string]*auth.Account{}, WebDAVRoot: root, Allow: cfg.Allow, MultiRoot: len(cfg.Path) > 1} addr := ":" + itoa(cfg.Port) log.Printf("chfs-ng listening on %s root=%s", addr, filepath.Clean(root)) if cfg.SSLCert != "" && cfg.SSLKey != "" { diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index b58c0fa..61d186f 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -24,6 +24,7 @@ type Server struct { Sessions map[string]*auth.Account WebDAVRoot string Allow string + MultiRoot bool } type fileInfo struct { Name string `json:"name"` @@ -74,13 +75,13 @@ func qpath(r *http.Request) string { } func permString(p auth.PermMask) string { if p.HasDelete() { - return "d" + return "RWD" } if p.HasWrite() { - return "w" + return "RW" } if p.HasRead() { - return "r" + return "R" } return "" } @@ -106,9 +107,10 @@ func (s *Server) files(w http.ResponseWriter, r *http.Request) { sub = len(xs) > 0 } } - out = append(out, fileInfo{Name: fi.Name(), Path: fp, Size: fi.Size(), Modified: fi.ModTime().Format(time.RFC3339), Dir: fi.IsDir(), Mask: permString(a.MaskFor(fp)), HasSubDir: sub}) + out = append(out, fileInfo{Name: fi.Name(), Path: fp, Size: fi.Size(), Modified: fi.ModTime().Format(time.RFC3339), Dir: fi.IsDir(), Mask: permString(a.MaskFor(fp)), GuestMask: permString(s.Auth.Guest.MaskFor(fp)), HasSubDir: sub}) } - writeJSON(w, map[string]any{"files": out, "currentMask": permString(m), "virtualRoot": false}) + virtual := p == "" && s.MultiRoot + writeJSON(w, map[string]any{"files": out, "currentMask": permString(m), "virtualRoot": virtual}) } func (s *Server) newdir(w http.ResponseWriter, r *http.Request) { _ = r.ParseMultipartForm(16 << 20) diff --git a/chfs-ng/internal/storage/multipath.go b/chfs-ng/internal/storage/multipath.go new file mode 100644 index 0000000..6748232 --- /dev/null +++ b/chfs-ng/internal/storage/multipath.go @@ -0,0 +1,132 @@ +package storage + +import ( + "os" + "path/filepath" + "strings" + "time" +) + +type Multi struct { + Roots []Local + Names []string +} + +func NewMulti(paths []string) Multi { + m := Multi{} + for _, p := range paths { + if strings.TrimSpace(p) == "" { + continue + } + abs, _ := filepath.Abs(p) + name := filepath.Base(filepath.Clean(abs)) + if name == "." || name == string(filepath.Separator) || name == "" { + name = "root" + } + duplicate := false + for _, existing := range m.Names { + if existing == name { + duplicate = true + break + } + } + if duplicate { + continue + } + m.Roots = append(m.Roots, Local{Root: abs}) + m.Names = append(m.Names, name) + } + return m +} + +func (m Multi) isVirtualRoot(p string) bool { return strings.Trim(p, "/") == "" } + +func (m Multi) find(p string) (Local, string, bool) { + p = strings.Trim(strings.ReplaceAll(p, "\\", "/"), "/") + if p == "" { + return Local{}, "", false + } + parts := strings.SplitN(p, "/", 2) + for i, n := range m.Names { + if parts[0] != n { + continue + } + rel := "" + if len(parts) == 2 { + rel = parts[1] + } + return m.Roots[i], rel, true + } + return Local{}, "", false +} + +func (m Multi) Stat(p string) (os.FileInfo, error) { + if m.isVirtualRoot(p) { + return virtualDirInfo{name: "."}, nil + } + r, rel, ok := m.find(p) + if !ok { + return nil, os.ErrNotExist + } + return r.Stat(rel) +} +func (m Multi) Open(p string) (*os.File, error) { + r, rel, ok := m.find(p) + if !ok { + return nil, os.ErrNotExist + } + return r.Open(rel) +} +func (m Multi) Create(p string) (*os.File, error) { + r, rel, ok := m.find(p) + if !ok { + return nil, os.ErrNotExist + } + return r.Create(rel) +} +func (m Multi) Remove(p string) error { + r, rel, ok := m.find(p) + if !ok { + return os.ErrNotExist + } + return r.Remove(rel) +} +func (m Multi) Rename(a, b string) error { + ra, aa, oka := m.find(a) + rb, bb, okb := m.find(b) + if !oka || !okb || ra.Root != rb.Root { + return os.ErrInvalid + } + return ra.Rename(aa, bb) +} +func (m Multi) MkdirAll(p string) error { + r, rel, ok := m.find(p) + if !ok { + return os.ErrNotExist + } + return r.MkdirAll(rel) +} +func (m Multi) List(p string) ([]os.FileInfo, error) { + p = strings.Trim(strings.ReplaceAll(p, "\\", "/"), "/") + if p == "" { + out := make([]os.FileInfo, 0, len(m.Names)) + for _, n := range m.Names { + out = append(out, virtualDirInfo{name: n}) + } + return out, nil + } + r, rel, ok := m.find(p) + if !ok { + return nil, os.ErrNotExist + } + return r.List(rel) +} + +type virtualDirInfo struct{ name string } + +func (v virtualDirInfo) Name() string { return v.name } +func (v virtualDirInfo) Size() int64 { return 0 } +func (v virtualDirInfo) Mode() os.FileMode { return os.ModeDir | 0555 } +func (v virtualDirInfo) ModTime() (t time.Time) { return time.Time{} } +func (v virtualDirInfo) IsDir() bool { return true } +func (v virtualDirInfo) Sys() any { return nil }