filesystem.go 18 KB

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