filesystem.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. package filesystem
  2. /*
  3. ArOZ Online File System Handler Wrappers
  4. author: tobychui
  5. This is a module design to do the followings
  6. 1. Mount / Create a fs when open
  7. 2. Provide the basic function and operations of a file system type
  8. 3. THIS MODULE **SHOULD NOT CONTAIN** CROSS FILE SYSTEM TYPE OPERATIONS
  9. */
  10. import (
  11. "crypto/md5"
  12. "encoding/hex"
  13. "errors"
  14. "fmt"
  15. "io"
  16. "io/fs"
  17. "log"
  18. "os"
  19. "path/filepath"
  20. "strconv"
  21. "strings"
  22. "time"
  23. uuid "github.com/satori/go.uuid"
  24. "imuslab.com/arozos/mod/filesystem/abstractions/ftpfs"
  25. "imuslab.com/arozos/mod/filesystem/abstractions/localfs"
  26. sftpfs "imuslab.com/arozos/mod/filesystem/abstractions/sftpfs"
  27. "imuslab.com/arozos/mod/filesystem/abstractions/smbfs"
  28. "imuslab.com/arozos/mod/filesystem/abstractions/webdavfs"
  29. "imuslab.com/arozos/mod/filesystem/arozfs"
  30. "imuslab.com/arozos/mod/utils"
  31. )
  32. //Options for creating new file system handler
  33. /*
  34. type FileSystemOpeningOptions struct{
  35. Name string `json:"name"` //Display name of this device
  36. Uuid string `json:"uuid"` //UUID of this device, e.g. S1
  37. Path string `json:"path"` //Path for the storage root
  38. Access string `json:"access,omitempty"` //Access right, allow {readonly, readwrite}
  39. Hierarchy string `json:"hierarchy"` //Folder hierarchy, allow {public, user}
  40. Automount bool `json:"automount"` //Automount this device if exists
  41. Filesystem string `json:"filesystem,omitempty"` //Support {"ext4","ext2", "ext3", "fat", "vfat", "ntfs"}
  42. Mountdev string `json:"mountdev,omitempty"` //Device file (e.g. /dev/sda1)
  43. Mountpt string `json:"mountpt,omitempty"` //Device mount point (e.g. /media/storage1)
  44. }
  45. */
  46. /*
  47. An interface for storing data related to a specific hierarchy settings.
  48. Example like the account information of network drive,
  49. backup mode of backup drive etc
  50. */
  51. type HierarchySpecificConfig interface{}
  52. type FileSystemAbstraction interface {
  53. //Fundamental Functions
  54. Chmod(string, os.FileMode) error
  55. Chown(string, int, int) error
  56. Chtimes(string, time.Time, time.Time) error
  57. Create(string) (arozfs.File, error)
  58. Mkdir(string, os.FileMode) error
  59. MkdirAll(string, os.FileMode) error
  60. Name() string
  61. Open(string) (arozfs.File, error)
  62. OpenFile(string, int, os.FileMode) (arozfs.File, error)
  63. Remove(string) error
  64. RemoveAll(string) error
  65. Rename(string, string) error
  66. Stat(string) (os.FileInfo, error)
  67. Close() error
  68. //Utils Functions
  69. VirtualPathToRealPath(string, string) (string, error)
  70. RealPathToVirtualPath(string, string) (string, error)
  71. FileExists(string) bool
  72. IsDir(string) bool
  73. Glob(string) ([]string, error)
  74. GetFileSize(string) int64
  75. GetModTime(string) (int64, error)
  76. WriteFile(string, []byte, os.FileMode) error
  77. ReadFile(string) ([]byte, error)
  78. ReadDir(string) ([]fs.DirEntry, error)
  79. WriteStream(string, io.Reader, os.FileMode) error
  80. ReadStream(string) (io.ReadCloser, error)
  81. Walk(string, filepath.WalkFunc) error
  82. Heartbeat() error
  83. }
  84. // Runtime persistence config, use to pass through startup paramters related to file system handlers
  85. type RuntimePersistenceConfig struct {
  86. LocalBufferPath string
  87. }
  88. // System Handler for returing
  89. type FileSystemHandler struct {
  90. Name string
  91. UUID string
  92. Path string
  93. Hierarchy string
  94. HierarchyConfig HierarchySpecificConfig
  95. ReadOnly bool
  96. RequireBuffer bool //Set this to true if the fsh do not provide file header functions like Open() or Create(), require WriteStream() and ReadStream()
  97. Parentuid string
  98. InitiationTime int64
  99. FileSystemAbstraction FileSystemAbstraction
  100. Filesystem string
  101. StartOptions FileSystemOption
  102. RuntimePersistenceConfig RuntimePersistenceConfig
  103. Closed bool
  104. }
  105. // Create a list of file system handler from the given json content
  106. func NewFileSystemHandlersFromJSON(jsonContent []byte, runtimePersistenceConfig RuntimePersistenceConfig) ([]*FileSystemHandler, error) {
  107. //Generate a list of handler option from json file
  108. options, err := loadConfigFromJSON(jsonContent)
  109. if err != nil {
  110. return []*FileSystemHandler{}, err
  111. }
  112. resultingHandlers := []*FileSystemHandler{}
  113. for _, option := range options {
  114. thisHandler, err := NewFileSystemHandler(option, runtimePersistenceConfig)
  115. if err != nil {
  116. log.Println("[File System] Failed to create system handler for " + option.Name)
  117. //log.Println(err.Error())
  118. continue
  119. }
  120. resultingHandlers = append(resultingHandlers, thisHandler)
  121. }
  122. return resultingHandlers, nil
  123. }
  124. // Create a new file system handler with the given config
  125. func NewFileSystemHandler(option FileSystemOption, RuntimePersistenceConfig RuntimePersistenceConfig) (*FileSystemHandler, error) {
  126. fstype := strings.ToLower(option.Filesystem)
  127. if inSlice([]string{"ext4", "ext2", "ext3", "fat", "vfat", "ntfs"}, fstype) || fstype == "" {
  128. //Check if the target fs require mounting
  129. if option.Automount == true {
  130. err := MountDevice(option.Mountpt, option.Mountdev, option.Filesystem)
  131. if err != nil {
  132. return &FileSystemHandler{}, err
  133. }
  134. }
  135. //Check if the path exists
  136. if !FileExists(option.Path) {
  137. return &FileSystemHandler{}, errors.New("Mount point not exists!")
  138. }
  139. //Handle Hierarchy branching
  140. if option.Hierarchy == "user" {
  141. //Create user hierarchy for this virtual device
  142. os.MkdirAll(filepath.ToSlash(filepath.Clean(option.Path))+"/users", 0755)
  143. }
  144. rootpath := filepath.ToSlash(filepath.Clean(option.Path)) + "/"
  145. return &FileSystemHandler{
  146. Name: option.Name,
  147. UUID: option.Uuid,
  148. Path: filepath.ToSlash(filepath.Clean(option.Path)) + "/",
  149. ReadOnly: option.Access == arozfs.FsReadOnly,
  150. RequireBuffer: false,
  151. Hierarchy: option.Hierarchy,
  152. HierarchyConfig: DefaultEmptyHierarchySpecificConfig,
  153. InitiationTime: time.Now().Unix(),
  154. FileSystemAbstraction: localfs.NewLocalFileSystemAbstraction(option.Uuid, rootpath, option.Hierarchy, option.Access == arozfs.FsReadOnly),
  155. Filesystem: fstype,
  156. StartOptions: option,
  157. RuntimePersistenceConfig: RuntimePersistenceConfig,
  158. Closed: false,
  159. }, nil
  160. } else if fstype == "webdav" {
  161. //WebDAV. Create an object and mount it
  162. root := option.Path
  163. user := option.Username
  164. password := option.Password
  165. webdavfs, err := webdavfs.NewWebDAVMount(option.Uuid, option.Hierarchy, root, user, password)
  166. if err != nil {
  167. return nil, err
  168. }
  169. return &FileSystemHandler{
  170. Name: option.Name,
  171. UUID: option.Uuid,
  172. Path: option.Path,
  173. ReadOnly: option.Access == arozfs.FsReadOnly,
  174. RequireBuffer: true,
  175. Hierarchy: option.Hierarchy,
  176. HierarchyConfig: nil,
  177. InitiationTime: time.Now().Unix(),
  178. FileSystemAbstraction: webdavfs,
  179. Filesystem: fstype,
  180. StartOptions: option,
  181. Closed: false,
  182. }, nil
  183. } else if fstype == "smb" {
  184. //SMB. Create an object and mount it
  185. pathChunks := strings.Split(strings.ReplaceAll(option.Path, "\\", "/"), "/")
  186. if len(pathChunks) < 2 {
  187. log.Println("[File System] Invalid configured smb filepath: Path format not matching [ip_addr]:[port]/[root_share path]")
  188. return nil, errors.New("Invalid configured smb filepath: Path format not matching [ip_addr]:[port]/[root_share path]")
  189. }
  190. ipAddr := pathChunks[0]
  191. rootShare := strings.Join(pathChunks[1:], "/")
  192. user := option.Username
  193. password := option.Password
  194. smbfs, err := smbfs.NewServerMessageBlockFileSystemAbstraction(
  195. option.Uuid,
  196. option.Hierarchy,
  197. ipAddr,
  198. rootShare,
  199. user,
  200. password,
  201. )
  202. if err != nil {
  203. return nil, err
  204. }
  205. thisFsh := FileSystemHandler{
  206. Name: option.Name,
  207. UUID: option.Uuid,
  208. Path: option.Path,
  209. ReadOnly: option.Access == arozfs.FsReadOnly,
  210. RequireBuffer: false,
  211. Hierarchy: option.Hierarchy,
  212. HierarchyConfig: nil,
  213. InitiationTime: time.Now().Unix(),
  214. FileSystemAbstraction: smbfs,
  215. Filesystem: fstype,
  216. StartOptions: option,
  217. Closed: false,
  218. }
  219. return &thisFsh, nil
  220. } else if fstype == "sftp" {
  221. //SFTP
  222. pathChunks := strings.Split(strings.ReplaceAll(option.Path, "\\", "/"), "/")
  223. ipAddr := pathChunks[0]
  224. port := 22
  225. if strings.Contains(ipAddr, ":") {
  226. //Custom port defined
  227. ipChunks := strings.Split(ipAddr, ":")
  228. ipAddr = ipChunks[0]
  229. p, err := strconv.Atoi(ipChunks[1])
  230. if err == nil {
  231. port = p
  232. }
  233. }
  234. rootShare := pathChunks[1:]
  235. user := option.Username
  236. password := option.Password
  237. sftpfs, err := sftpfs.NewSFTPFileSystemAbstraction(
  238. option.Uuid,
  239. option.Hierarchy,
  240. ipAddr,
  241. port,
  242. "/"+strings.Join(rootShare, "/"),
  243. user,
  244. password,
  245. )
  246. if err != nil {
  247. fmt.Println(err.Error())
  248. return nil, err
  249. }
  250. thisFsh := FileSystemHandler{
  251. Name: option.Name,
  252. UUID: option.Uuid,
  253. Path: option.Path,
  254. ReadOnly: option.Access == arozfs.FsReadOnly,
  255. RequireBuffer: false,
  256. Hierarchy: option.Hierarchy,
  257. HierarchyConfig: nil,
  258. InitiationTime: time.Now().Unix(),
  259. FileSystemAbstraction: sftpfs,
  260. Filesystem: fstype,
  261. StartOptions: option,
  262. Closed: false,
  263. }
  264. return &thisFsh, nil
  265. } else if fstype == "ftp" {
  266. ftpfs, err := ftpfs.NewFTPFSAbstraction(option.Uuid, option.Hierarchy, option.Path, option.Username, option.Password)
  267. if err != nil {
  268. return nil, err
  269. }
  270. return &FileSystemHandler{
  271. Name: option.Name,
  272. UUID: option.Uuid,
  273. Path: option.Path,
  274. ReadOnly: option.Access == arozfs.FsReadOnly,
  275. RequireBuffer: true,
  276. Hierarchy: option.Hierarchy,
  277. HierarchyConfig: nil,
  278. InitiationTime: time.Now().Unix(),
  279. FileSystemAbstraction: ftpfs,
  280. Filesystem: fstype,
  281. StartOptions: option,
  282. Closed: false,
  283. }, nil
  284. } else if option.Filesystem == "virtual" {
  285. //Virtual filesystem, deprecated
  286. log.Println("[File System] Deprecated file system type: Virtual")
  287. }
  288. return nil, errors.New("Not supported file system: " + fstype)
  289. }
  290. // Check if a fsh is a network drive
  291. func (fsh *FileSystemHandler) IsNetworkDrive() bool {
  292. return arozfs.IsNetworkDrive(fsh.Filesystem)
  293. }
  294. // Check if a fsh is a local disk drive
  295. func (fsh *FileSystemHandler) IsLocalDrive() bool {
  296. //Check if network drive
  297. if arozfs.IsNetworkDrive(fsh.Filesystem) {
  298. return false
  299. }
  300. //Check if mounted locally
  301. if _, err := os.Stat(fsh.Path); os.IsNotExist(err) {
  302. return false
  303. }
  304. return true
  305. }
  306. // Buffer a file to local tmp folder and return the tmp location for further processing
  307. func (fsh *FileSystemHandler) BufferRemoteToLocal(rpath string) (string, error) {
  308. //Check if the remote file exists
  309. fsa := fsh.FileSystemAbstraction
  310. if !fsa.FileExists(rpath) {
  311. //Target file not exists
  312. return "", errors.New("target file not exists on remote")
  313. }
  314. //Check if the remote is a file (not support dir)
  315. if fsa.IsDir(rpath) {
  316. return "", errors.New("directory cannot be buffered to local")
  317. }
  318. //Get the tmp folder directory
  319. tmpdir := fsh.RuntimePersistenceConfig.LocalBufferPath
  320. if !utils.FileExists(tmpdir) {
  321. //Create the tmp dir if not exists
  322. os.MkdirAll(tmpdir, 0775)
  323. }
  324. //Generate a filename for the buffer file
  325. tmpFilename := uuid.NewV4().String()
  326. tmpFilepath := arozfs.ToSlash(filepath.Join(tmpdir, tmpFilename+filepath.Ext(rpath)))
  327. //Copy the file from remote location to local
  328. src, err := fsh.FileSystemAbstraction.ReadStream(rpath)
  329. if err != nil {
  330. return "", err
  331. }
  332. defer src.Close()
  333. dest, err := os.OpenFile(tmpFilepath, os.O_CREATE|os.O_WRONLY, 0777)
  334. if err != nil {
  335. return "", errors.New("unable to write to buffer location: " + err.Error())
  336. }
  337. defer dest.Close()
  338. _, err = io.Copy(dest, src)
  339. if err != nil {
  340. return "", errors.New("file buffer failed: " + err.Error())
  341. }
  342. //Return the buffered filepath on local disk
  343. return tmpFilepath, nil
  344. }
  345. func (fsh *FileSystemHandler) IsRootOf(vpath string) bool {
  346. return strings.HasPrefix(vpath, fsh.UUID+":")
  347. }
  348. func (fsh *FileSystemHandler) GetUniquePathHash(vpath string, username string) (string, error) {
  349. fshAbs := fsh.FileSystemAbstraction
  350. rpath := ""
  351. if strings.Contains(vpath, ":/") {
  352. r, err := fshAbs.VirtualPathToRealPath(vpath, username)
  353. if err != nil {
  354. return "", err
  355. }
  356. rpath = filepath.ToSlash(r)
  357. } else {
  358. //Passed in realpath as vpath.
  359. rpath = vpath
  360. }
  361. hash := md5.Sum([]byte(fsh.UUID + "_" + rpath))
  362. return hex.EncodeToString(hash[:]), nil
  363. }
  364. func (fsh *FileSystemHandler) GetDirctorySizeFromRealPath(rpath string, includeHidden bool) (int64, int) {
  365. var size int64 = 0
  366. var fileCount int = 0
  367. err := fsh.FileSystemAbstraction.Walk(rpath, func(thisFilename string, info os.FileInfo, err error) error {
  368. if err != nil {
  369. return err
  370. }
  371. if !info.IsDir() {
  372. if includeHidden {
  373. //append all into the file count and size
  374. size += info.Size()
  375. fileCount++
  376. } else {
  377. //Check if this is hidden
  378. if !IsInsideHiddenFolder(thisFilename) {
  379. size += info.Size()
  380. fileCount++
  381. }
  382. }
  383. }
  384. return nil
  385. })
  386. if err != nil {
  387. return 0, fileCount
  388. }
  389. return size, fileCount
  390. }
  391. func (fsh *FileSystemHandler) GetDirctorySizeFromVpath(vpath string, username string, includeHidden bool) (int64, int) {
  392. realpath, _ := fsh.FileSystemAbstraction.VirtualPathToRealPath(vpath, username)
  393. return fsh.GetDirctorySizeFromRealPath(realpath, includeHidden)
  394. }
  395. // Reload the target file system abstraction
  396. func (fsh *FileSystemHandler) ReloadFileSystelAbstraction() error {
  397. log.Println("[File System] Reloading File System Abstraction for " + fsh.Name)
  398. //Load the start option for this fsh
  399. originalStartOption := fsh.StartOptions
  400. runtimePersistenceConfig := fsh.RuntimePersistenceConfig
  401. //Close the file system handler
  402. fsh.Close()
  403. //Give it a few ms to do physical disk stuffs
  404. time.Sleep(800 * time.Millisecond)
  405. //Generate a new fsh from original start option
  406. reloadedFsh, err := NewFileSystemHandler(originalStartOption, runtimePersistenceConfig)
  407. if err != nil {
  408. return err
  409. }
  410. //Overwrite the pointers to target fsa
  411. fsh.FileSystemAbstraction = reloadedFsh.FileSystemAbstraction
  412. fsh.Closed = false
  413. return nil
  414. }
  415. // Check if this file system require user isolation (aka Hierarchy == user)
  416. func (fsh *FileSystemHandler) RequierUserIsolation() bool {
  417. return fsh.Hierarchy == "user"
  418. }
  419. // Close an openeded File System
  420. func (fsh *FileSystemHandler) Close() {
  421. //Set the close flag to true so others function wont access it
  422. fsh.Closed = true
  423. //Close the file system object
  424. err := fsh.FileSystemAbstraction.Close()
  425. if err != nil {
  426. log.Println("[File System] Unable to close File System Abstraction for Handler: " + fsh.UUID + ". Skipping.")
  427. }
  428. }
  429. // Helper function
  430. func inSlice(slice []string, val string) bool {
  431. for _, item := range slice {
  432. if item == val {
  433. return true
  434. }
  435. }
  436. return false
  437. }
  438. func FileExists(filename string) bool {
  439. _, err := os.Stat(filename)
  440. if os.IsNotExist(err) {
  441. return false
  442. }
  443. return true
  444. }