package webdavfs import ( "errors" "io" "io/fs" "log" "os" "path/filepath" "regexp" "strings" "time" "github.com/studio-b12/gowebdav" ) /* WebDAV Client This script is design as a wrapper of the studio-b12/gowebdav module that allow access to webdav network drive in ArozOS and allow arozos cross-mounting each others */ type WebDAVFileSystem struct { UUID string Hierarchy string root string user string c *gowebdav.Client } func NewWebDAVMount(UUID string, Hierarchy string, root string, user string, password string) (*WebDAVFileSystem, error) { //Connect to webdav server c := gowebdav.NewClient(root, user, password) err := c.Connect() if err != nil { log.Println("[WebDAV FS] Unable to connect to remote: ", err.Error()) return nil, err } else { log.Println("[WebDAV FS] Connected to remote: " + root) } return &WebDAVFileSystem{ UUID: UUID, Hierarchy: Hierarchy, c: c, root: root, user: user, }, nil } func (e WebDAVFileSystem) Chmod(filename string, mode os.FileMode) error { return errors.New("filesystem type not supported") } func (e WebDAVFileSystem) Chown(filename string, uid int, gid int) error { return errors.New("filesystem type not supported") } func (e WebDAVFileSystem) Chtimes(filename string, atime time.Time, mtime time.Time) error { return errors.New("filesystem type not supported") } func (e WebDAVFileSystem) Create(filename string) (*os.File, error) { return nil, errors.New("filesystem type not supported") } func (e WebDAVFileSystem) Mkdir(filename string, mode os.FileMode) error { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) return e.c.Mkdir(filename, mode) } func (e WebDAVFileSystem) MkdirAll(filename string, mode os.FileMode) error { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) return e.c.MkdirAll(filename, mode) } func (e WebDAVFileSystem) Name() string { return "" } func (e WebDAVFileSystem) Open(filename string) (*os.File, error) { return nil, errors.New("filesystem type not supported") } func (e WebDAVFileSystem) OpenFile(filename string, flag int, perm os.FileMode) (*os.File, error) { //Buffer the target file to memory //To be implement: Wait for Golang's fs.File.Write function to be released //f := bufffs.New(filename) //return f, nil return nil, errors.New("filesystem type not supported") } func (e WebDAVFileSystem) Remove(filename string) error { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) return e.c.Remove(filename) } func (e WebDAVFileSystem) RemoveAll(filename string) error { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) return e.c.RemoveAll(filename) } func (e WebDAVFileSystem) Rename(oldname, newname string) error { oldname = filterFilepath(filepath.ToSlash(filepath.Clean(oldname))) newname = filterFilepath(filepath.ToSlash(filepath.Clean(newname))) err := e.c.Rename(oldname, newname, true) if err != nil { //Unable to rename due to reverse proxy issue. Use Copy and Delete f, err := e.c.ReadStream(oldname) if err != nil { return err } err = e.c.WriteStream(newname, f, 0775) if err != nil { return err } f.Close() e.c.RemoveAll(oldname) } return nil } func (e WebDAVFileSystem) Stat(filename string) (os.FileInfo, error) { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) return e.c.Stat(filename) } func (e WebDAVFileSystem) VirtualPathToRealPath(subpath string, username string) (string, error) { subpath = filterFilepath(filepath.ToSlash(filepath.Clean(subpath))) if strings.HasPrefix(subpath, e.UUID+":") { //This is full virtual path. Trim the uuid and correct the subpath subpath = strings.TrimPrefix(subpath, e.UUID+":") } if e.Hierarchy == "user" { return filepath.ToSlash(filepath.Clean(filepath.Join("users", username, subpath))), nil } else if e.Hierarchy == "public" { return filepath.ToSlash(filepath.Clean(subpath)), nil } return "", errors.New("unsupported filesystem hierarchy") } func (e WebDAVFileSystem) RealPathToVirtualPath(rpath string, username string) (string, error) { rpath = filterFilepath(filepath.ToSlash(filepath.Clean(rpath))) if e.Hierarchy == "user" && strings.HasPrefix(rpath, "/users/"+username) { rpath = strings.TrimPrefix(rpath, "/users/"+username) } return e.UUID + ":" + filepath.ToSlash(rpath), nil } func (e WebDAVFileSystem) FileExists(filename string) bool { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) _, err := e.c.Stat(filename) if os.IsNotExist(err) || err != nil { return false } return true } func (e WebDAVFileSystem) IsDir(filename string) bool { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) s, err := e.c.Stat(filename) if err != nil { return false } return s.IsDir() } //Notes: This is not actual Glob function. This just emulate Glob using ReadDir with max depth 1 layer func (e WebDAVFileSystem) Glob(wildcard string) ([]string, error) { wildcard = filepath.ToSlash(filepath.Clean(wildcard)) if !strings.HasPrefix(wildcard, "/") { //Handle case for listing root, "*" wildcard = "/" + wildcard } //Get the longest path without * chunksWithoutStar := []string{} chunks := strings.Split(wildcard, "/") for _, chunk := range chunks { if !strings.Contains(chunk, "*") { chunksWithoutStar = append(chunksWithoutStar, chunk) } else { //Cut off break } } if strings.Count(wildcard, "*") <= 1 && strings.Contains(chunks[len(chunks)-1], "*") { //Fast Glob fileInfos, err := e.c.ReadDir(filterFilepath(filepath.ToSlash(filepath.Clean(filepath.Dir(wildcard))))) if err != nil { return []string{}, err } validFiles := []string{} matchingRule := wildCardToRegexp(wildcard) for _, thisFileInfo := range fileInfos { thisFileFullpath := filepath.ToSlash(filepath.Join(filepath.Dir(wildcard), thisFileInfo.Name())) match, _ := regexp.MatchString(matchingRule, thisFileFullpath) if match { validFiles = append(validFiles, thisFileFullpath) } } return validFiles, nil } else { //Slow Glob walkRoot := strings.Join(chunksWithoutStar, "/") if !strings.HasPrefix(walkRoot, "/") { walkRoot = "/" + walkRoot } allFiles := []string{} e.Walk(walkRoot, func(path string, info fs.FileInfo, err error) error { allFiles = append(allFiles, path) return nil }) validFiles := []string{} matchingRule := wildCardToRegexp(wildcard) + "$" for _, thisFilepath := range allFiles { match, _ := regexp.MatchString(matchingRule, thisFilepath) if match { validFiles = append(validFiles, thisFilepath) } } return validFiles, nil } } func (e WebDAVFileSystem) GetFileSize(filename string) int64 { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) s, err := e.Stat(filename) if err != nil { log.Println(err) return 0 } return s.Size() } func (e WebDAVFileSystem) GetModTime(filename string) (int64, error) { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) s, err := e.Stat(filename) if err != nil { return 0, err } return s.ModTime().Unix(), nil } func (e WebDAVFileSystem) WriteFile(filename string, content []byte, mode os.FileMode) error { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) return e.c.Write(filename, content, mode) } func (e WebDAVFileSystem) ReadFile(filename string) ([]byte, error) { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) bytes, err := e.c.Read(filename) if err != nil { return []byte(""), err } return bytes, nil } func (e WebDAVFileSystem) WriteStream(filename string, stream io.Reader, mode os.FileMode) error { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) return e.c.WriteStream(filename, stream, mode) } func (e WebDAVFileSystem) ReadStream(filename string) (io.ReadCloser, error) { filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename))) return e.c.ReadStream(filename) } func (e WebDAVFileSystem) Walk(rootpath string, walkFn filepath.WalkFunc) error { rootpath = filepath.ToSlash(filepath.Clean(rootpath)) rootStat, err := e.Stat(rootpath) err = walkFn(rootpath, rootStat, err) if err != nil { return err } return e.walk(rootpath, walkFn) } /* Helper Functions */ func (e WebDAVFileSystem) walk(thisPath string, walkFun filepath.WalkFunc) error { files, err := e.c.ReadDir(thisPath) if err != nil { return err } for _, file := range files { thisFileFullPath := filepath.ToSlash(filepath.Join(thisPath, file.Name())) if file.IsDir() { err = walkFun(thisFileFullPath, file, nil) if err != nil { return err } err = e.walk(thisFileFullPath, walkFun) if err != nil { return err } } else { err = walkFun(thisFileFullPath, file, nil) if err != nil { return err } } } return nil } /* func (e WebDAVFileSystem) globscan(currentPath string, wildcardChunks []string, layer int) ([]string, error) { if layer >= len(wildcardChunks) { return []string{}, nil } } */ func filterFilepath(rawpath string) string { rawpath = strings.TrimSpace(rawpath) if strings.HasPrefix(rawpath, "./") { return rawpath[1:] } else if rawpath == "." || rawpath == "" { return "/" } return rawpath } func wildCardToRegexp(pattern string) string { var result strings.Builder for i, literal := range strings.Split(pattern, "*") { // Replace * with .* if i > 0 { result.WriteString(".*") } // Quote any regular expression meta characters in the // literal text. result.WriteString(regexp.QuoteMeta(literal)) } return result.String() }