webdavfs.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. package webdavfs
  2. import (
  3. "errors"
  4. "io"
  5. "io/fs"
  6. "log"
  7. "os"
  8. "path/filepath"
  9. "regexp"
  10. "strings"
  11. "time"
  12. "github.com/studio-b12/gowebdav"
  13. "imuslab.com/arozos/mod/filesystem/arozfs"
  14. )
  15. /*
  16. WebDAV Client
  17. This script is design as a wrapper of the studio-b12/gowebdav module
  18. that allow access to webdav network drive in ArozOS and allow arozos
  19. cross-mounting each others
  20. */
  21. type WebDAVFileSystem struct {
  22. UUID string
  23. Hierarchy string
  24. root string
  25. user string
  26. c *gowebdav.Client
  27. }
  28. func NewWebDAVMount(UUID string, Hierarchy string, root string, user string, password string) (*WebDAVFileSystem, error) {
  29. //Connect to webdav server
  30. c := gowebdav.NewClient(root, user, password)
  31. err := c.Connect()
  32. if err != nil {
  33. log.Println("[WebDAV FS] Unable to connect to remote: ", err.Error())
  34. return nil, err
  35. } else {
  36. log.Println("[WebDAV FS] Connected to remote: " + root)
  37. }
  38. return &WebDAVFileSystem{
  39. UUID: UUID,
  40. Hierarchy: Hierarchy,
  41. c: c,
  42. root: root,
  43. user: user,
  44. }, nil
  45. }
  46. func (e WebDAVFileSystem) Chmod(filename string, mode os.FileMode) error {
  47. return errors.New("filesystem type not supported")
  48. }
  49. func (e WebDAVFileSystem) Chown(filename string, uid int, gid int) error {
  50. return errors.New("filesystem type not supported")
  51. }
  52. func (e WebDAVFileSystem) Chtimes(filename string, atime time.Time, mtime time.Time) error {
  53. return errors.New("filesystem type not supported")
  54. }
  55. func (e WebDAVFileSystem) Create(filename string) (arozfs.File, error) {
  56. return nil, errors.New("filesystem type not supported")
  57. }
  58. func (e WebDAVFileSystem) Mkdir(filename string, mode os.FileMode) error {
  59. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  60. return e.c.Mkdir(filename, mode)
  61. }
  62. func (e WebDAVFileSystem) MkdirAll(filename string, mode os.FileMode) error {
  63. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  64. return e.c.MkdirAll(filename, mode)
  65. }
  66. func (e WebDAVFileSystem) Name() string {
  67. return ""
  68. }
  69. func (e WebDAVFileSystem) Open(filename string) (arozfs.File, error) {
  70. return nil, errors.New("filesystem type not supported")
  71. }
  72. func (e WebDAVFileSystem) OpenFile(filename string, flag int, perm os.FileMode) (arozfs.File, error) {
  73. //Buffer the target file to memory
  74. //To be implement: Wait for Golang's fs.File.Write function to be released
  75. //f := bufffs.New(filename)
  76. //return f, nil
  77. return nil, errors.New("filesystem type not supported")
  78. }
  79. func (e WebDAVFileSystem) Remove(filename string) error {
  80. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  81. return e.c.Remove(filename)
  82. }
  83. func (e WebDAVFileSystem) RemoveAll(filename string) error {
  84. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  85. return e.c.RemoveAll(filename)
  86. }
  87. func (e WebDAVFileSystem) Rename(oldname, newname string) error {
  88. oldname = filterFilepath(filepath.ToSlash(filepath.Clean(oldname)))
  89. newname = filterFilepath(filepath.ToSlash(filepath.Clean(newname)))
  90. err := e.c.Rename(oldname, newname, true)
  91. if err != nil {
  92. //Unable to rename due to reverse proxy issue. Use Copy and Delete
  93. f, err := e.c.ReadStream(oldname)
  94. if err != nil {
  95. return err
  96. }
  97. err = e.c.WriteStream(newname, f, 0775)
  98. if err != nil {
  99. return err
  100. }
  101. f.Close()
  102. e.c.RemoveAll(oldname)
  103. }
  104. return nil
  105. }
  106. func (e WebDAVFileSystem) Stat(filename string) (os.FileInfo, error) {
  107. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  108. return e.c.Stat(filename)
  109. }
  110. func (e WebDAVFileSystem) VirtualPathToRealPath(subpath string, username string) (string, error) {
  111. subpath = filterFilepath(filepath.ToSlash(filepath.Clean(subpath)))
  112. if strings.HasPrefix(subpath, e.UUID+":") {
  113. //This is full virtual path. Trim the uuid and correct the subpath
  114. subpath = strings.TrimPrefix(subpath, e.UUID+":")
  115. }
  116. if e.Hierarchy == "user" {
  117. return filepath.ToSlash(filepath.Clean(filepath.Join("users", username, subpath))), nil
  118. } else if e.Hierarchy == "public" {
  119. return filepath.ToSlash(filepath.Clean(subpath)), nil
  120. }
  121. return "", errors.New("unsupported filesystem hierarchy")
  122. }
  123. func (e WebDAVFileSystem) RealPathToVirtualPath(rpath string, username string) (string, error) {
  124. rpath = filterFilepath(filepath.ToSlash(filepath.Clean(rpath)))
  125. if e.Hierarchy == "user" && strings.HasPrefix(rpath, "/users/"+username) {
  126. rpath = strings.TrimPrefix(rpath, "/users/"+username)
  127. }
  128. return e.UUID + ":" + filepath.ToSlash(rpath), nil
  129. }
  130. func (e WebDAVFileSystem) FileExists(filename string) bool {
  131. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  132. _, err := e.c.Stat(filename)
  133. if os.IsNotExist(err) || err != nil {
  134. return false
  135. }
  136. return true
  137. }
  138. func (e WebDAVFileSystem) IsDir(filename string) bool {
  139. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  140. s, err := e.c.Stat(filename)
  141. if err != nil {
  142. return false
  143. }
  144. return s.IsDir()
  145. }
  146. //Notes: This is not actual Glob function. This just emulate Glob using ReadDir with max depth 1 layer
  147. func (e WebDAVFileSystem) Glob(wildcard string) ([]string, error) {
  148. wildcard = filepath.ToSlash(filepath.Clean(wildcard))
  149. if !strings.HasPrefix(wildcard, "/") {
  150. //Handle case for listing root, "*"
  151. wildcard = "/" + wildcard
  152. }
  153. chunks := strings.Split(strings.TrimPrefix(wildcard, "/"), "/")
  154. results, err := e.globpath("/", chunks, 0)
  155. return results, err
  156. /*
  157. //Get the longest path without *
  158. chunksWithoutStar := []string{}
  159. chunks := strings.Split(wildcard, "/")
  160. for _, chunk := range chunks {
  161. if !strings.Contains(chunk, "*") {
  162. chunksWithoutStar = append(chunksWithoutStar, chunk)
  163. } else {
  164. //Cut off
  165. break
  166. }
  167. }
  168. if strings.Count(wildcard, "*") <= 1 && strings.Contains(chunks[len(chunks)-1], "*") {
  169. //Fast Glob
  170. fileInfos, err := e.c.ReadDir(filterFilepath(filepath.ToSlash(filepath.Clean(filepath.Dir(wildcard)))))
  171. if err != nil {
  172. return []string{}, err
  173. }
  174. validFiles := []string{}
  175. matchingRule := wildCardToRegexp(wildcard)
  176. for _, thisFileInfo := range fileInfos {
  177. thisFileFullpath := filepath.ToSlash(filepath.Join(filepath.Dir(wildcard), thisFileInfo.Name()))
  178. match, _ := regexp.MatchString(matchingRule, thisFileFullpath)
  179. if match {
  180. validFiles = append(validFiles, thisFileFullpath)
  181. }
  182. }
  183. return validFiles, nil
  184. } else {
  185. //Slow Glob
  186. walkRoot := strings.Join(chunksWithoutStar, "/")
  187. if !strings.HasPrefix(walkRoot, "/") {
  188. walkRoot = "/" + walkRoot
  189. }
  190. allFiles := []string{}
  191. e.Walk(walkRoot, func(path string, info fs.FileInfo, err error) error {
  192. allFiles = append(allFiles, path)
  193. return nil
  194. })
  195. validFiles := []string{}
  196. matchingRule := wildCardToRegexp(wildcard) + "$"
  197. for _, thisFilepath := range allFiles {
  198. match, _ := regexp.MatchString(matchingRule, thisFilepath)
  199. if match {
  200. validFiles = append(validFiles, thisFilepath)
  201. }
  202. }
  203. return validFiles, nil
  204. }
  205. */
  206. }
  207. func (e WebDAVFileSystem) GetFileSize(filename string) int64 {
  208. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  209. s, err := e.Stat(filename)
  210. if err != nil {
  211. log.Println(err)
  212. return 0
  213. }
  214. return s.Size()
  215. }
  216. func (e WebDAVFileSystem) GetModTime(filename string) (int64, error) {
  217. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  218. s, err := e.Stat(filename)
  219. if err != nil {
  220. return 0, err
  221. }
  222. return s.ModTime().Unix(), nil
  223. }
  224. func (e WebDAVFileSystem) WriteFile(filename string, content []byte, mode os.FileMode) error {
  225. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  226. return e.c.Write(filename, content, mode)
  227. }
  228. func (e WebDAVFileSystem) ReadFile(filename string) ([]byte, error) {
  229. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  230. bytes, err := e.c.Read(filename)
  231. if err != nil {
  232. return []byte(""), err
  233. }
  234. return bytes, nil
  235. }
  236. func (e WebDAVFileSystem) ReadDir(filename string) ([]fs.DirEntry, error) {
  237. fis, err := e.c.ReadDir(filename)
  238. if err != nil {
  239. return []fs.DirEntry{}, err
  240. }
  241. dirEntires := []fs.DirEntry{}
  242. for _, fi := range fis {
  243. dirEntires = append(dirEntires, newDirEntryFromFileInfo(fi))
  244. }
  245. return dirEntires, nil
  246. }
  247. func (e WebDAVFileSystem) WriteStream(filename string, stream io.Reader, mode os.FileMode) error {
  248. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  249. return e.c.WriteStream(filename, stream, mode)
  250. }
  251. func (e WebDAVFileSystem) ReadStream(filename string) (io.ReadCloser, error) {
  252. filename = filterFilepath(filepath.ToSlash(filepath.Clean(filename)))
  253. return e.c.ReadStream(filename)
  254. }
  255. func (e WebDAVFileSystem) Walk(rootpath string, walkFn filepath.WalkFunc) error {
  256. rootpath = filepath.ToSlash(filepath.Clean(rootpath))
  257. rootStat, err := e.Stat(rootpath)
  258. err = walkFn(rootpath, rootStat, err)
  259. if err != nil {
  260. return err
  261. }
  262. return e.walk(rootpath, walkFn)
  263. }
  264. func (e WebDAVFileSystem) Close() error {
  265. return nil
  266. }
  267. /*
  268. Helper Functions
  269. */
  270. func (e WebDAVFileSystem) walk(thisPath string, walkFun filepath.WalkFunc) error {
  271. files, err := e.c.ReadDir(thisPath)
  272. if err != nil {
  273. return err
  274. }
  275. for _, file := range files {
  276. thisFileFullPath := filepath.ToSlash(filepath.Join(thisPath, file.Name()))
  277. if file.IsDir() {
  278. err = walkFun(thisFileFullPath, file, nil)
  279. if err != nil {
  280. return err
  281. }
  282. err = e.walk(thisFileFullPath, walkFun)
  283. if err != nil {
  284. return err
  285. }
  286. } else {
  287. err = walkFun(thisFileFullPath, file, nil)
  288. if err != nil {
  289. return err
  290. }
  291. }
  292. }
  293. return nil
  294. }
  295. func (e WebDAVFileSystem) globpath(currentPath string, pathSegments []string, depth int) ([]string, error) {
  296. const pathSeparatorsLimit = 1000
  297. if depth == pathSeparatorsLimit {
  298. return nil, errors.New("bad pattern")
  299. }
  300. // Check pattern is well-formed.
  301. if _, err := regexp.MatchString(wildCardToRegexp(strings.Join(pathSegments, "/")), ""); err != nil {
  302. return nil, err
  303. }
  304. if len(pathSegments) == 0 {
  305. //Reaching the bottom
  306. return []string{}, nil
  307. }
  308. thisSegment := pathSegments[0]
  309. if strings.Contains(thisSegment, "*") {
  310. //Search for matching
  311. matchPattern := currentPath + thisSegment
  312. files, err := e.c.ReadDir(currentPath)
  313. if err != nil {
  314. return []string{}, nil
  315. }
  316. //Check which file in the currentPath matches the wildcard
  317. matchedSubpaths := []string{}
  318. for _, file := range files {
  319. thisPath := currentPath + file.Name()
  320. match, _ := regexp.MatchString(wildCardToRegexp(matchPattern), thisPath)
  321. if match {
  322. if file.IsDir() {
  323. matchedSubpaths = append(matchedSubpaths, thisPath+"/")
  324. } else {
  325. matchedSubpaths = append(matchedSubpaths, thisPath)
  326. }
  327. }
  328. }
  329. if len(pathSegments[1:]) == 0 {
  330. return matchedSubpaths, nil
  331. }
  332. //For each of the subpaths, do a globpath
  333. matchingFilenames := []string{}
  334. for _, subpath := range matchedSubpaths {
  335. thisMatchedNames, _ := e.globpath(subpath, pathSegments[1:], depth+1)
  336. matchingFilenames = append(matchingFilenames, thisMatchedNames...)
  337. }
  338. return matchingFilenames, nil
  339. } else {
  340. //Check folder exists
  341. if e.FileExists(currentPath+thisSegment) && e.IsDir(currentPath+thisSegment) {
  342. return e.globpath(currentPath+thisSegment+"/", pathSegments[1:], depth+1)
  343. } else {
  344. //Not matching
  345. return []string{}, nil
  346. }
  347. }
  348. }
  349. func filterFilepath(rawpath string) string {
  350. rawpath = strings.TrimSpace(rawpath)
  351. if strings.HasPrefix(rawpath, "./") {
  352. return rawpath[1:]
  353. } else if rawpath == "." || rawpath == "" {
  354. return "/"
  355. }
  356. return rawpath
  357. }
  358. func wildCardToRegexp(pattern string) string {
  359. var result strings.Builder
  360. for i, literal := range strings.Split(pattern, "*") {
  361. // Replace * with .*
  362. if i > 0 {
  363. result.WriteString(".*")
  364. }
  365. // Quote any regular expression meta characters in the
  366. // literal text.
  367. result.WriteString(regexp.QuoteMeta(literal))
  368. }
  369. return result.String()
  370. }