feat: implement multi-root virtual filesystem
This commit is contained in:
@@ -53,17 +53,21 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
root := cfg.Path[0]
|
root := cfg.Path[0]
|
||||||
if len(cfg.Path) > 1 {
|
for _, p := range cfg.Path {
|
||||||
log.Printf("warning: %d shared roots configured; v1 compatibility currently serves the first root %q", len(cfg.Path), root)
|
if err := os.MkdirAll(p, 0755); err != nil {
|
||||||
}
|
log.Fatal(err)
|
||||||
if err := os.MkdirAll(root, 0755); err != nil {
|
}
|
||||||
log.Fatal(err)
|
|
||||||
}
|
}
|
||||||
mgr, err := auth.ParseRules(cfg.Rules)
|
mgr, err := auth.ParseRules(cfg.Rules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
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)
|
addr := ":" + itoa(cfg.Port)
|
||||||
log.Printf("chfs-ng listening on %s root=%s", addr, filepath.Clean(root))
|
log.Printf("chfs-ng listening on %s root=%s", addr, filepath.Clean(root))
|
||||||
if cfg.SSLCert != "" && cfg.SSLKey != "" {
|
if cfg.SSLCert != "" && cfg.SSLKey != "" {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Server struct {
|
|||||||
Sessions map[string]*auth.Account
|
Sessions map[string]*auth.Account
|
||||||
WebDAVRoot string
|
WebDAVRoot string
|
||||||
Allow string
|
Allow string
|
||||||
|
MultiRoot bool
|
||||||
}
|
}
|
||||||
type fileInfo struct {
|
type fileInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -74,13 +75,13 @@ func qpath(r *http.Request) string {
|
|||||||
}
|
}
|
||||||
func permString(p auth.PermMask) string {
|
func permString(p auth.PermMask) string {
|
||||||
if p.HasDelete() {
|
if p.HasDelete() {
|
||||||
return "d"
|
return "RWD"
|
||||||
}
|
}
|
||||||
if p.HasWrite() {
|
if p.HasWrite() {
|
||||||
return "w"
|
return "RW"
|
||||||
}
|
}
|
||||||
if p.HasRead() {
|
if p.HasRead() {
|
||||||
return "r"
|
return "R"
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -106,9 +107,10 @@ func (s *Server) files(w http.ResponseWriter, r *http.Request) {
|
|||||||
sub = len(xs) > 0
|
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) {
|
func (s *Server) newdir(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = r.ParseMultipartForm(16 << 20)
|
_ = r.ParseMultipartForm(16 << 20)
|
||||||
|
|||||||
@@ -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 }
|
||||||
Reference in New Issue
Block a user