webdavfs.go 11 KB

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