vfs.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. // Package vfs provides local and remote filesystems support
  2. package vfs
  3. import (
  4. "errors"
  5. "fmt"
  6. "io"
  7. "net/url"
  8. "os"
  9. "path"
  10. "path/filepath"
  11. "runtime"
  12. "strings"
  13. "time"
  14. "github.com/eikenb/pipeat"
  15. "github.com/pkg/sftp"
  16. "github.com/drakkan/sftpgo/v2/kms"
  17. "github.com/drakkan/sftpgo/v2/logger"
  18. "github.com/drakkan/sftpgo/v2/sdk"
  19. "github.com/drakkan/sftpgo/v2/util"
  20. )
  21. const dirMimeType = "inode/directory"
  22. var (
  23. validAzAccessTier = []string{"", "Archive", "Hot", "Cool"}
  24. // ErrStorageSizeUnavailable is returned if the storage backend does not support getting the size
  25. ErrStorageSizeUnavailable = errors.New("unable to get available size for this storage backend")
  26. // ErrVfsUnsupported defines the error for an unsupported VFS operation
  27. ErrVfsUnsupported = errors.New("not supported")
  28. credentialsDirPath string
  29. tempPath string
  30. sftpFingerprints []string
  31. )
  32. // SetCredentialsDirPath sets the credentials dir path
  33. func SetCredentialsDirPath(credentialsPath string) {
  34. credentialsDirPath = credentialsPath
  35. }
  36. // GetCredentialsDirPath returns the credentials dir path
  37. func GetCredentialsDirPath() string {
  38. return credentialsDirPath
  39. }
  40. // SetTempPath sets the path for temporary files
  41. func SetTempPath(fsPath string) {
  42. tempPath = fsPath
  43. }
  44. // GetTempPath returns the path for temporary files
  45. func GetTempPath() string {
  46. return tempPath
  47. }
  48. // SetSFTPFingerprints sets the SFTP host key fingerprints
  49. func SetSFTPFingerprints(fp []string) {
  50. sftpFingerprints = fp
  51. }
  52. // Fs defines the interface for filesystem backends
  53. type Fs interface {
  54. Name() string
  55. ConnectionID() string
  56. Stat(name string) (os.FileInfo, error)
  57. Lstat(name string) (os.FileInfo, error)
  58. Open(name string, offset int64) (File, *pipeat.PipeReaderAt, func(), error)
  59. Create(name string, flag int) (File, *PipeWriter, func(), error)
  60. Rename(source, target string) error
  61. Remove(name string, isDir bool) error
  62. Mkdir(name string) error
  63. MkdirAll(name string, uid int, gid int) error
  64. Symlink(source, target string) error
  65. Chown(name string, uid int, gid int) error
  66. Chmod(name string, mode os.FileMode) error
  67. Chtimes(name string, atime, mtime time.Time) error
  68. Truncate(name string, size int64) error
  69. ReadDir(dirname string) ([]os.FileInfo, error)
  70. Readlink(name string) (string, error)
  71. IsUploadResumeSupported() bool
  72. IsAtomicUploadSupported() bool
  73. CheckRootPath(username string, uid int, gid int) bool
  74. ResolvePath(sftpPath string) (string, error)
  75. IsNotExist(err error) bool
  76. IsPermission(err error) bool
  77. IsNotSupported(err error) bool
  78. ScanRootDirContents() (int, int64, error)
  79. GetDirSize(dirname string) (int, int64, error)
  80. GetAtomicUploadPath(name string) string
  81. GetRelativePath(name string) string
  82. Walk(root string, walkFn filepath.WalkFunc) error
  83. Join(elem ...string) string
  84. HasVirtualFolders() bool
  85. GetMimeType(name string) (string, error)
  86. GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error)
  87. Close() error
  88. }
  89. // File defines an interface representing a SFTPGo file
  90. type File interface {
  91. io.Reader
  92. io.Writer
  93. io.Closer
  94. io.ReaderAt
  95. io.WriterAt
  96. io.Seeker
  97. Stat() (os.FileInfo, error)
  98. Name() string
  99. Truncate(size int64) error
  100. }
  101. // QuotaCheckResult defines the result for a quota check
  102. type QuotaCheckResult struct {
  103. HasSpace bool
  104. AllowedSize int64
  105. AllowedFiles int
  106. UsedSize int64
  107. UsedFiles int
  108. QuotaSize int64
  109. QuotaFiles int
  110. }
  111. // GetRemainingSize returns the remaining allowed size
  112. func (q *QuotaCheckResult) GetRemainingSize() int64 {
  113. if q.QuotaSize > 0 {
  114. return q.QuotaSize - q.UsedSize
  115. }
  116. return 0
  117. }
  118. // GetRemainingFiles returns the remaining allowed files
  119. func (q *QuotaCheckResult) GetRemainingFiles() int {
  120. if q.QuotaFiles > 0 {
  121. return q.QuotaFiles - q.UsedFiles
  122. }
  123. return 0
  124. }
  125. // S3FsConfig defines the configuration for S3 based filesystem
  126. type S3FsConfig struct {
  127. sdk.S3FsConfig
  128. }
  129. func (c *S3FsConfig) isEqual(other *S3FsConfig) bool {
  130. if c.Bucket != other.Bucket {
  131. return false
  132. }
  133. if c.KeyPrefix != other.KeyPrefix {
  134. return false
  135. }
  136. if c.Region != other.Region {
  137. return false
  138. }
  139. if c.AccessKey != other.AccessKey {
  140. return false
  141. }
  142. if c.Endpoint != other.Endpoint {
  143. return false
  144. }
  145. if c.StorageClass != other.StorageClass {
  146. return false
  147. }
  148. if c.UploadPartSize != other.UploadPartSize {
  149. return false
  150. }
  151. if c.UploadConcurrency != other.UploadConcurrency {
  152. return false
  153. }
  154. if c.AccessSecret == nil {
  155. c.AccessSecret = kms.NewEmptySecret()
  156. }
  157. if other.AccessSecret == nil {
  158. other.AccessSecret = kms.NewEmptySecret()
  159. }
  160. return c.AccessSecret.IsEqual(other.AccessSecret)
  161. }
  162. func (c *S3FsConfig) checkCredentials() error {
  163. if c.AccessKey == "" && !c.AccessSecret.IsEmpty() {
  164. return errors.New("access_key cannot be empty with access_secret not empty")
  165. }
  166. if c.AccessSecret.IsEmpty() && c.AccessKey != "" {
  167. return errors.New("access_secret cannot be empty with access_key not empty")
  168. }
  169. if c.AccessSecret.IsEncrypted() && !c.AccessSecret.IsValid() {
  170. return errors.New("invalid encrypted access_secret")
  171. }
  172. if !c.AccessSecret.IsEmpty() && !c.AccessSecret.IsValidInput() {
  173. return errors.New("invalid access_secret")
  174. }
  175. return nil
  176. }
  177. // EncryptCredentials encrypts access secret if it is in plain text
  178. func (c *S3FsConfig) EncryptCredentials(additionalData string) error {
  179. if c.AccessSecret.IsPlain() {
  180. c.AccessSecret.SetAdditionalData(additionalData)
  181. err := c.AccessSecret.Encrypt()
  182. if err != nil {
  183. return err
  184. }
  185. }
  186. return nil
  187. }
  188. // Validate returns an error if the configuration is not valid
  189. func (c *S3FsConfig) Validate() error {
  190. if c.AccessSecret == nil {
  191. c.AccessSecret = kms.NewEmptySecret()
  192. }
  193. if c.Bucket == "" {
  194. return errors.New("bucket cannot be empty")
  195. }
  196. if c.Region == "" {
  197. return errors.New("region cannot be empty")
  198. }
  199. if err := c.checkCredentials(); err != nil {
  200. return err
  201. }
  202. if c.KeyPrefix != "" {
  203. if strings.HasPrefix(c.KeyPrefix, "/") {
  204. return errors.New("key_prefix cannot start with /")
  205. }
  206. c.KeyPrefix = path.Clean(c.KeyPrefix)
  207. if !strings.HasSuffix(c.KeyPrefix, "/") {
  208. c.KeyPrefix += "/"
  209. }
  210. }
  211. if c.UploadPartSize != 0 && (c.UploadPartSize < 5 || c.UploadPartSize > 5000) {
  212. return errors.New("upload_part_size cannot be != 0, lower than 5 (MB) or greater than 5000 (MB)")
  213. }
  214. if c.UploadConcurrency < 0 || c.UploadConcurrency > 64 {
  215. return fmt.Errorf("invalid upload concurrency: %v", c.UploadConcurrency)
  216. }
  217. return nil
  218. }
  219. // GCSFsConfig defines the configuration for Google Cloud Storage based filesystem
  220. type GCSFsConfig struct {
  221. sdk.GCSFsConfig
  222. }
  223. func (c *GCSFsConfig) isEqual(other *GCSFsConfig) bool {
  224. if c.Bucket != other.Bucket {
  225. return false
  226. }
  227. if c.KeyPrefix != other.KeyPrefix {
  228. return false
  229. }
  230. if c.AutomaticCredentials != other.AutomaticCredentials {
  231. return false
  232. }
  233. if c.StorageClass != other.StorageClass {
  234. return false
  235. }
  236. if c.Credentials == nil {
  237. c.Credentials = kms.NewEmptySecret()
  238. }
  239. if other.Credentials == nil {
  240. other.Credentials = kms.NewEmptySecret()
  241. }
  242. return c.Credentials.IsEqual(other.Credentials)
  243. }
  244. // Validate returns an error if the configuration is not valid
  245. func (c *GCSFsConfig) Validate(credentialsFilePath string) error {
  246. if c.Credentials == nil {
  247. c.Credentials = kms.NewEmptySecret()
  248. }
  249. if c.Bucket == "" {
  250. return errors.New("bucket cannot be empty")
  251. }
  252. if c.KeyPrefix != "" {
  253. if strings.HasPrefix(c.KeyPrefix, "/") {
  254. return errors.New("key_prefix cannot start with /")
  255. }
  256. c.KeyPrefix = path.Clean(c.KeyPrefix)
  257. if !strings.HasSuffix(c.KeyPrefix, "/") {
  258. c.KeyPrefix += "/"
  259. }
  260. }
  261. if c.Credentials.IsEncrypted() && !c.Credentials.IsValid() {
  262. return errors.New("invalid encrypted credentials")
  263. }
  264. if !c.Credentials.IsValidInput() && c.AutomaticCredentials == 0 {
  265. fi, err := os.Stat(credentialsFilePath)
  266. if err != nil {
  267. return fmt.Errorf("invalid credentials %v", err)
  268. }
  269. if fi.Size() == 0 {
  270. return errors.New("credentials cannot be empty")
  271. }
  272. }
  273. return nil
  274. }
  275. // AzBlobFsConfig defines the configuration for Azure Blob Storage based filesystem
  276. type AzBlobFsConfig struct {
  277. sdk.AzBlobFsConfig
  278. }
  279. func (c *AzBlobFsConfig) isEqual(other *AzBlobFsConfig) bool {
  280. if c.Container != other.Container {
  281. return false
  282. }
  283. if c.AccountName != other.AccountName {
  284. return false
  285. }
  286. if c.Endpoint != other.Endpoint {
  287. return false
  288. }
  289. if c.SASURL.IsEmpty() {
  290. c.SASURL = kms.NewEmptySecret()
  291. }
  292. if other.SASURL.IsEmpty() {
  293. other.SASURL = kms.NewEmptySecret()
  294. }
  295. if !c.SASURL.IsEqual(other.SASURL) {
  296. return false
  297. }
  298. if c.KeyPrefix != other.KeyPrefix {
  299. return false
  300. }
  301. if c.UploadPartSize != other.UploadPartSize {
  302. return false
  303. }
  304. if c.UploadConcurrency != other.UploadConcurrency {
  305. return false
  306. }
  307. if c.UseEmulator != other.UseEmulator {
  308. return false
  309. }
  310. if c.AccessTier != other.AccessTier {
  311. return false
  312. }
  313. if c.AccountKey == nil {
  314. c.AccountKey = kms.NewEmptySecret()
  315. }
  316. if other.AccountKey == nil {
  317. other.AccountKey = kms.NewEmptySecret()
  318. }
  319. return c.AccountKey.IsEqual(other.AccountKey)
  320. }
  321. // EncryptCredentials encrypts access secret if it is in plain text
  322. func (c *AzBlobFsConfig) EncryptCredentials(additionalData string) error {
  323. if c.AccountKey.IsPlain() {
  324. c.AccountKey.SetAdditionalData(additionalData)
  325. if err := c.AccountKey.Encrypt(); err != nil {
  326. return err
  327. }
  328. }
  329. if c.SASURL.IsPlain() {
  330. c.SASURL.SetAdditionalData(additionalData)
  331. if err := c.SASURL.Encrypt(); err != nil {
  332. return err
  333. }
  334. }
  335. return nil
  336. }
  337. func (c *AzBlobFsConfig) checkCredentials() error {
  338. if c.SASURL.IsPlain() {
  339. _, err := url.Parse(c.SASURL.GetPayload())
  340. return err
  341. }
  342. if c.SASURL.IsEncrypted() && !c.SASURL.IsValid() {
  343. return errors.New("invalid encrypted sas_url")
  344. }
  345. if !c.SASURL.IsEmpty() {
  346. return nil
  347. }
  348. if c.AccountName == "" || !c.AccountKey.IsValidInput() {
  349. return errors.New("credentials cannot be empty or invalid")
  350. }
  351. if c.AccountKey.IsEncrypted() && !c.AccountKey.IsValid() {
  352. return errors.New("invalid encrypted account_key")
  353. }
  354. return nil
  355. }
  356. // Validate returns an error if the configuration is not valid
  357. func (c *AzBlobFsConfig) Validate() error {
  358. if c.AccountKey == nil {
  359. c.AccountKey = kms.NewEmptySecret()
  360. }
  361. if c.SASURL == nil {
  362. c.SASURL = kms.NewEmptySecret()
  363. }
  364. // container could be embedded within SAS URL we check this at runtime
  365. if c.SASURL.IsEmpty() && c.Container == "" {
  366. return errors.New("container cannot be empty")
  367. }
  368. if err := c.checkCredentials(); err != nil {
  369. return err
  370. }
  371. if c.KeyPrefix != "" {
  372. if strings.HasPrefix(c.KeyPrefix, "/") {
  373. return errors.New("key_prefix cannot start with /")
  374. }
  375. c.KeyPrefix = path.Clean(c.KeyPrefix)
  376. if !strings.HasSuffix(c.KeyPrefix, "/") {
  377. c.KeyPrefix += "/"
  378. }
  379. }
  380. if c.UploadPartSize < 0 || c.UploadPartSize > 100 {
  381. return fmt.Errorf("invalid upload part size: %v", c.UploadPartSize)
  382. }
  383. if c.UploadConcurrency < 0 || c.UploadConcurrency > 64 {
  384. return fmt.Errorf("invalid upload concurrency: %v", c.UploadConcurrency)
  385. }
  386. if !util.IsStringInSlice(c.AccessTier, validAzAccessTier) {
  387. return fmt.Errorf("invalid access tier %#v, valid values: \"''%v\"", c.AccessTier, strings.Join(validAzAccessTier, ", "))
  388. }
  389. return nil
  390. }
  391. // CryptFsConfig defines the configuration to store local files as encrypted
  392. type CryptFsConfig struct {
  393. sdk.CryptFsConfig
  394. }
  395. func (c *CryptFsConfig) isEqual(other *CryptFsConfig) bool {
  396. if c.Passphrase == nil {
  397. c.Passphrase = kms.NewEmptySecret()
  398. }
  399. if other.Passphrase == nil {
  400. other.Passphrase = kms.NewEmptySecret()
  401. }
  402. return c.Passphrase.IsEqual(other.Passphrase)
  403. }
  404. // EncryptCredentials encrypts access secret if it is in plain text
  405. func (c *CryptFsConfig) EncryptCredentials(additionalData string) error {
  406. if c.Passphrase.IsPlain() {
  407. c.Passphrase.SetAdditionalData(additionalData)
  408. if err := c.Passphrase.Encrypt(); err != nil {
  409. return err
  410. }
  411. }
  412. return nil
  413. }
  414. // Validate returns an error if the configuration is not valid
  415. func (c *CryptFsConfig) Validate() error {
  416. if c.Passphrase == nil || c.Passphrase.IsEmpty() {
  417. return errors.New("invalid passphrase")
  418. }
  419. if !c.Passphrase.IsValidInput() {
  420. return errors.New("passphrase cannot be empty or invalid")
  421. }
  422. if c.Passphrase.IsEncrypted() && !c.Passphrase.IsValid() {
  423. return errors.New("invalid encrypted passphrase")
  424. }
  425. return nil
  426. }
  427. // PipeWriter defines a wrapper for pipeat.PipeWriterAt.
  428. type PipeWriter struct {
  429. writer *pipeat.PipeWriterAt
  430. err error
  431. done chan bool
  432. }
  433. // NewPipeWriter initializes a new PipeWriter
  434. func NewPipeWriter(w *pipeat.PipeWriterAt) *PipeWriter {
  435. return &PipeWriter{
  436. writer: w,
  437. err: nil,
  438. done: make(chan bool),
  439. }
  440. }
  441. // Close waits for the upload to end, closes the pipeat.PipeWriterAt and returns an error if any.
  442. func (p *PipeWriter) Close() error {
  443. p.writer.Close() //nolint:errcheck // the returned error is always null
  444. <-p.done
  445. return p.err
  446. }
  447. // Done unlocks other goroutines waiting on Close().
  448. // It must be called when the upload ends
  449. func (p *PipeWriter) Done(err error) {
  450. p.err = err
  451. p.done <- true
  452. }
  453. // WriteAt is a wrapper for pipeat WriteAt
  454. func (p *PipeWriter) WriteAt(data []byte, off int64) (int, error) {
  455. return p.writer.WriteAt(data, off)
  456. }
  457. // Write is a wrapper for pipeat Write
  458. func (p *PipeWriter) Write(data []byte) (int, error) {
  459. return p.writer.Write(data)
  460. }
  461. // IsDirectory checks if a path exists and is a directory
  462. func IsDirectory(fs Fs, path string) (bool, error) {
  463. fileInfo, err := fs.Stat(path)
  464. if err != nil {
  465. return false, err
  466. }
  467. return fileInfo.IsDir(), err
  468. }
  469. // IsLocalOsFs returns true if fs is a local filesystem implementation
  470. func IsLocalOsFs(fs Fs) bool {
  471. return fs.Name() == osFsName
  472. }
  473. // IsCryptOsFs returns true if fs is an encrypted local filesystem implementation
  474. func IsCryptOsFs(fs Fs) bool {
  475. return fs.Name() == cryptFsName
  476. }
  477. // IsSFTPFs returns true if fs is an SFTP filesystem
  478. func IsSFTPFs(fs Fs) bool {
  479. return strings.HasPrefix(fs.Name(), sftpFsName)
  480. }
  481. // IsBufferedSFTPFs returns true if this is a buffered SFTP filesystem
  482. func IsBufferedSFTPFs(fs Fs) bool {
  483. if !IsSFTPFs(fs) {
  484. return false
  485. }
  486. return !fs.IsUploadResumeSupported()
  487. }
  488. // IsLocalOrUnbufferedSFTPFs returns true if fs is local or SFTP with no buffer
  489. func IsLocalOrUnbufferedSFTPFs(fs Fs) bool {
  490. if IsLocalOsFs(fs) {
  491. return true
  492. }
  493. if IsSFTPFs(fs) {
  494. return fs.IsUploadResumeSupported()
  495. }
  496. return false
  497. }
  498. // IsLocalOrSFTPFs returns true if fs is local or SFTP
  499. func IsLocalOrSFTPFs(fs Fs) bool {
  500. return IsLocalOsFs(fs) || IsSFTPFs(fs)
  501. }
  502. // HasOpenRWSupport returns true if the fs can open a file
  503. // for reading and writing at the same time
  504. func HasOpenRWSupport(fs Fs) bool {
  505. if IsLocalOsFs(fs) {
  506. return true
  507. }
  508. if IsSFTPFs(fs) && fs.IsUploadResumeSupported() {
  509. return true
  510. }
  511. return false
  512. }
  513. // IsLocalOrCryptoFs returns true if fs is local or local encrypted
  514. func IsLocalOrCryptoFs(fs Fs) bool {
  515. return IsLocalOsFs(fs) || IsCryptOsFs(fs)
  516. }
  517. // SetPathPermissions calls fs.Chown.
  518. // It does nothing for local filesystem on windows
  519. func SetPathPermissions(fs Fs, path string, uid int, gid int) {
  520. if uid == -1 && gid == -1 {
  521. return
  522. }
  523. if IsLocalOsFs(fs) {
  524. if runtime.GOOS == "windows" {
  525. return
  526. }
  527. }
  528. if err := fs.Chown(path, uid, gid); err != nil {
  529. fsLog(fs, logger.LevelWarn, "error chowning path %v: %v", path, err)
  530. }
  531. }
  532. func fsLog(fs Fs, level logger.LogLevel, format string, v ...interface{}) {
  533. logger.Log(level, fs.Name(), fs.ConnectionID(), format, v...)
  534. }