vfs.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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. // HideConfidentialData hides confidential data
  130. func (c *S3FsConfig) HideConfidentialData() {
  131. if c.AccessSecret != nil {
  132. c.AccessSecret.Hide()
  133. }
  134. }
  135. func (c *S3FsConfig) isEqual(other *S3FsConfig) bool {
  136. if c.Bucket != other.Bucket {
  137. return false
  138. }
  139. if c.KeyPrefix != other.KeyPrefix {
  140. return false
  141. }
  142. if c.Region != other.Region {
  143. return false
  144. }
  145. if c.AccessKey != other.AccessKey {
  146. return false
  147. }
  148. if c.Endpoint != other.Endpoint {
  149. return false
  150. }
  151. if c.StorageClass != other.StorageClass {
  152. return false
  153. }
  154. if c.ACL != other.ACL {
  155. return false
  156. }
  157. if c.UploadPartSize != other.UploadPartSize {
  158. return false
  159. }
  160. if c.UploadConcurrency != other.UploadConcurrency {
  161. return false
  162. }
  163. if c.DownloadConcurrency != other.DownloadConcurrency {
  164. return false
  165. }
  166. if c.DownloadPartSize != other.DownloadPartSize {
  167. return false
  168. }
  169. if c.DownloadPartMaxTime != other.DownloadPartMaxTime {
  170. return false
  171. }
  172. if c.ForcePathStyle != other.ForcePathStyle {
  173. return false
  174. }
  175. return c.isSecretEqual(other)
  176. }
  177. func (c *S3FsConfig) isSecretEqual(other *S3FsConfig) bool {
  178. if c.AccessSecret == nil {
  179. c.AccessSecret = kms.NewEmptySecret()
  180. }
  181. if other.AccessSecret == nil {
  182. other.AccessSecret = kms.NewEmptySecret()
  183. }
  184. return c.AccessSecret.IsEqual(other.AccessSecret)
  185. }
  186. func (c *S3FsConfig) checkCredentials() error {
  187. if c.AccessKey == "" && !c.AccessSecret.IsEmpty() {
  188. return errors.New("access_key cannot be empty with access_secret not empty")
  189. }
  190. if c.AccessSecret.IsEmpty() && c.AccessKey != "" {
  191. return errors.New("access_secret cannot be empty with access_key not empty")
  192. }
  193. if c.AccessSecret.IsEncrypted() && !c.AccessSecret.IsValid() {
  194. return errors.New("invalid encrypted access_secret")
  195. }
  196. if !c.AccessSecret.IsEmpty() && !c.AccessSecret.IsValidInput() {
  197. return errors.New("invalid access_secret")
  198. }
  199. return nil
  200. }
  201. // EncryptCredentials encrypts access secret if it is in plain text
  202. func (c *S3FsConfig) EncryptCredentials(additionalData string) error {
  203. if c.AccessSecret.IsPlain() {
  204. c.AccessSecret.SetAdditionalData(additionalData)
  205. err := c.AccessSecret.Encrypt()
  206. if err != nil {
  207. return err
  208. }
  209. }
  210. return nil
  211. }
  212. func (c *S3FsConfig) checkPartSizeAndConcurrency() error {
  213. if c.UploadPartSize != 0 && (c.UploadPartSize < 5 || c.UploadPartSize > 5000) {
  214. return errors.New("upload_part_size cannot be != 0, lower than 5 (MB) or greater than 5000 (MB)")
  215. }
  216. if c.UploadConcurrency < 0 || c.UploadConcurrency > 64 {
  217. return fmt.Errorf("invalid upload concurrency: %v", c.UploadConcurrency)
  218. }
  219. if c.DownloadPartSize != 0 && (c.DownloadPartSize < 5 || c.DownloadPartSize > 5000) {
  220. return errors.New("download_part_size cannot be != 0, lower than 5 (MB) or greater than 5000 (MB)")
  221. }
  222. if c.DownloadConcurrency < 0 || c.DownloadConcurrency > 64 {
  223. return fmt.Errorf("invalid download concurrency: %v", c.DownloadConcurrency)
  224. }
  225. return nil
  226. }
  227. // Validate returns an error if the configuration is not valid
  228. func (c *S3FsConfig) Validate() error {
  229. if c.AccessSecret == nil {
  230. c.AccessSecret = kms.NewEmptySecret()
  231. }
  232. if c.Bucket == "" {
  233. return errors.New("bucket cannot be empty")
  234. }
  235. if c.Region == "" {
  236. return errors.New("region cannot be empty")
  237. }
  238. if err := c.checkCredentials(); err != nil {
  239. return err
  240. }
  241. if c.KeyPrefix != "" {
  242. if strings.HasPrefix(c.KeyPrefix, "/") {
  243. return errors.New("key_prefix cannot start with /")
  244. }
  245. c.KeyPrefix = path.Clean(c.KeyPrefix)
  246. if !strings.HasSuffix(c.KeyPrefix, "/") {
  247. c.KeyPrefix += "/"
  248. }
  249. }
  250. c.StorageClass = strings.TrimSpace(c.StorageClass)
  251. c.ACL = strings.TrimSpace(c.ACL)
  252. return c.checkPartSizeAndConcurrency()
  253. }
  254. // GCSFsConfig defines the configuration for Google Cloud Storage based filesystem
  255. type GCSFsConfig struct {
  256. sdk.GCSFsConfig
  257. }
  258. // HideConfidentialData hides confidential data
  259. func (c *GCSFsConfig) HideConfidentialData() {
  260. if c.Credentials != nil {
  261. c.Credentials.Hide()
  262. }
  263. }
  264. func (c *GCSFsConfig) isEqual(other *GCSFsConfig) bool {
  265. if c.Bucket != other.Bucket {
  266. return false
  267. }
  268. if c.KeyPrefix != other.KeyPrefix {
  269. return false
  270. }
  271. if c.AutomaticCredentials != other.AutomaticCredentials {
  272. return false
  273. }
  274. if c.StorageClass != other.StorageClass {
  275. return false
  276. }
  277. if c.Credentials == nil {
  278. c.Credentials = kms.NewEmptySecret()
  279. }
  280. if other.Credentials == nil {
  281. other.Credentials = kms.NewEmptySecret()
  282. }
  283. return c.Credentials.IsEqual(other.Credentials)
  284. }
  285. // Validate returns an error if the configuration is not valid
  286. func (c *GCSFsConfig) Validate(credentialsFilePath string) error {
  287. if c.Credentials == nil {
  288. c.Credentials = kms.NewEmptySecret()
  289. }
  290. if c.Bucket == "" {
  291. return errors.New("bucket cannot be empty")
  292. }
  293. if c.KeyPrefix != "" {
  294. if strings.HasPrefix(c.KeyPrefix, "/") {
  295. return errors.New("key_prefix cannot start with /")
  296. }
  297. c.KeyPrefix = path.Clean(c.KeyPrefix)
  298. if !strings.HasSuffix(c.KeyPrefix, "/") {
  299. c.KeyPrefix += "/"
  300. }
  301. }
  302. if c.Credentials.IsEncrypted() && !c.Credentials.IsValid() {
  303. return errors.New("invalid encrypted credentials")
  304. }
  305. if !c.Credentials.IsValidInput() && c.AutomaticCredentials == 0 {
  306. fi, err := os.Stat(credentialsFilePath)
  307. if err != nil {
  308. return fmt.Errorf("invalid credentials %v", err)
  309. }
  310. if fi.Size() == 0 {
  311. return errors.New("credentials cannot be empty")
  312. }
  313. }
  314. c.StorageClass = strings.TrimSpace(c.StorageClass)
  315. return nil
  316. }
  317. // AzBlobFsConfig defines the configuration for Azure Blob Storage based filesystem
  318. type AzBlobFsConfig struct {
  319. sdk.AzBlobFsConfig
  320. }
  321. // HideConfidentialData hides confidential data
  322. func (c *AzBlobFsConfig) HideConfidentialData() {
  323. if c.AccountKey != nil {
  324. c.AccountKey.Hide()
  325. }
  326. if c.SASURL != nil {
  327. c.SASURL.Hide()
  328. }
  329. }
  330. func (c *AzBlobFsConfig) isEqual(other *AzBlobFsConfig) bool {
  331. if c.Container != other.Container {
  332. return false
  333. }
  334. if c.AccountName != other.AccountName {
  335. return false
  336. }
  337. if c.Endpoint != other.Endpoint {
  338. return false
  339. }
  340. if c.SASURL.IsEmpty() {
  341. c.SASURL = kms.NewEmptySecret()
  342. }
  343. if other.SASURL.IsEmpty() {
  344. other.SASURL = kms.NewEmptySecret()
  345. }
  346. if !c.SASURL.IsEqual(other.SASURL) {
  347. return false
  348. }
  349. if c.KeyPrefix != other.KeyPrefix {
  350. return false
  351. }
  352. if c.UploadPartSize != other.UploadPartSize {
  353. return false
  354. }
  355. if c.UploadConcurrency != other.UploadConcurrency {
  356. return false
  357. }
  358. if c.UseEmulator != other.UseEmulator {
  359. return false
  360. }
  361. if c.AccessTier != other.AccessTier {
  362. return false
  363. }
  364. if c.AccountKey == nil {
  365. c.AccountKey = kms.NewEmptySecret()
  366. }
  367. if other.AccountKey == nil {
  368. other.AccountKey = kms.NewEmptySecret()
  369. }
  370. return c.AccountKey.IsEqual(other.AccountKey)
  371. }
  372. // EncryptCredentials encrypts access secret if it is in plain text
  373. func (c *AzBlobFsConfig) EncryptCredentials(additionalData string) error {
  374. if c.AccountKey.IsPlain() {
  375. c.AccountKey.SetAdditionalData(additionalData)
  376. if err := c.AccountKey.Encrypt(); err != nil {
  377. return err
  378. }
  379. }
  380. if c.SASURL.IsPlain() {
  381. c.SASURL.SetAdditionalData(additionalData)
  382. if err := c.SASURL.Encrypt(); err != nil {
  383. return err
  384. }
  385. }
  386. return nil
  387. }
  388. func (c *AzBlobFsConfig) checkCredentials() error {
  389. if c.SASURL.IsPlain() {
  390. _, err := url.Parse(c.SASURL.GetPayload())
  391. return err
  392. }
  393. if c.SASURL.IsEncrypted() && !c.SASURL.IsValid() {
  394. return errors.New("invalid encrypted sas_url")
  395. }
  396. if !c.SASURL.IsEmpty() {
  397. return nil
  398. }
  399. if c.AccountName == "" || !c.AccountKey.IsValidInput() {
  400. return errors.New("credentials cannot be empty or invalid")
  401. }
  402. if c.AccountKey.IsEncrypted() && !c.AccountKey.IsValid() {
  403. return errors.New("invalid encrypted account_key")
  404. }
  405. return nil
  406. }
  407. // Validate returns an error if the configuration is not valid
  408. func (c *AzBlobFsConfig) Validate() error {
  409. if c.AccountKey == nil {
  410. c.AccountKey = kms.NewEmptySecret()
  411. }
  412. if c.SASURL == nil {
  413. c.SASURL = kms.NewEmptySecret()
  414. }
  415. // container could be embedded within SAS URL we check this at runtime
  416. if c.SASURL.IsEmpty() && c.Container == "" {
  417. return errors.New("container cannot be empty")
  418. }
  419. if err := c.checkCredentials(); err != nil {
  420. return err
  421. }
  422. if c.KeyPrefix != "" {
  423. if strings.HasPrefix(c.KeyPrefix, "/") {
  424. return errors.New("key_prefix cannot start with /")
  425. }
  426. c.KeyPrefix = path.Clean(c.KeyPrefix)
  427. if !strings.HasSuffix(c.KeyPrefix, "/") {
  428. c.KeyPrefix += "/"
  429. }
  430. }
  431. if c.UploadPartSize < 0 || c.UploadPartSize > 100 {
  432. return fmt.Errorf("invalid upload part size: %v", c.UploadPartSize)
  433. }
  434. if c.UploadConcurrency < 0 || c.UploadConcurrency > 64 {
  435. return fmt.Errorf("invalid upload concurrency: %v", c.UploadConcurrency)
  436. }
  437. if !util.IsStringInSlice(c.AccessTier, validAzAccessTier) {
  438. return fmt.Errorf("invalid access tier %#v, valid values: \"''%v\"", c.AccessTier, strings.Join(validAzAccessTier, ", "))
  439. }
  440. return nil
  441. }
  442. // CryptFsConfig defines the configuration to store local files as encrypted
  443. type CryptFsConfig struct {
  444. sdk.CryptFsConfig
  445. }
  446. // HideConfidentialData hides confidential data
  447. func (c *CryptFsConfig) HideConfidentialData() {
  448. if c.Passphrase != nil {
  449. c.Passphrase.Hide()
  450. }
  451. }
  452. func (c *CryptFsConfig) isEqual(other *CryptFsConfig) bool {
  453. if c.Passphrase == nil {
  454. c.Passphrase = kms.NewEmptySecret()
  455. }
  456. if other.Passphrase == nil {
  457. other.Passphrase = kms.NewEmptySecret()
  458. }
  459. return c.Passphrase.IsEqual(other.Passphrase)
  460. }
  461. // EncryptCredentials encrypts access secret if it is in plain text
  462. func (c *CryptFsConfig) EncryptCredentials(additionalData string) error {
  463. if c.Passphrase.IsPlain() {
  464. c.Passphrase.SetAdditionalData(additionalData)
  465. if err := c.Passphrase.Encrypt(); err != nil {
  466. return err
  467. }
  468. }
  469. return nil
  470. }
  471. // Validate returns an error if the configuration is not valid
  472. func (c *CryptFsConfig) Validate() error {
  473. if c.Passphrase == nil || c.Passphrase.IsEmpty() {
  474. return errors.New("invalid passphrase")
  475. }
  476. if !c.Passphrase.IsValidInput() {
  477. return errors.New("passphrase cannot be empty or invalid")
  478. }
  479. if c.Passphrase.IsEncrypted() && !c.Passphrase.IsValid() {
  480. return errors.New("invalid encrypted passphrase")
  481. }
  482. return nil
  483. }
  484. // PipeWriter defines a wrapper for pipeat.PipeWriterAt.
  485. type PipeWriter struct {
  486. writer *pipeat.PipeWriterAt
  487. err error
  488. done chan bool
  489. }
  490. // NewPipeWriter initializes a new PipeWriter
  491. func NewPipeWriter(w *pipeat.PipeWriterAt) *PipeWriter {
  492. return &PipeWriter{
  493. writer: w,
  494. err: nil,
  495. done: make(chan bool),
  496. }
  497. }
  498. // Close waits for the upload to end, closes the pipeat.PipeWriterAt and returns an error if any.
  499. func (p *PipeWriter) Close() error {
  500. p.writer.Close() //nolint:errcheck // the returned error is always null
  501. <-p.done
  502. return p.err
  503. }
  504. // Done unlocks other goroutines waiting on Close().
  505. // It must be called when the upload ends
  506. func (p *PipeWriter) Done(err error) {
  507. p.err = err
  508. p.done <- true
  509. }
  510. // WriteAt is a wrapper for pipeat WriteAt
  511. func (p *PipeWriter) WriteAt(data []byte, off int64) (int, error) {
  512. return p.writer.WriteAt(data, off)
  513. }
  514. // Write is a wrapper for pipeat Write
  515. func (p *PipeWriter) Write(data []byte) (int, error) {
  516. return p.writer.Write(data)
  517. }
  518. // IsDirectory checks if a path exists and is a directory
  519. func IsDirectory(fs Fs, path string) (bool, error) {
  520. fileInfo, err := fs.Stat(path)
  521. if err != nil {
  522. return false, err
  523. }
  524. return fileInfo.IsDir(), err
  525. }
  526. // IsLocalOsFs returns true if fs is a local filesystem implementation
  527. func IsLocalOsFs(fs Fs) bool {
  528. return fs.Name() == osFsName
  529. }
  530. // IsCryptOsFs returns true if fs is an encrypted local filesystem implementation
  531. func IsCryptOsFs(fs Fs) bool {
  532. return fs.Name() == cryptFsName
  533. }
  534. // IsSFTPFs returns true if fs is an SFTP filesystem
  535. func IsSFTPFs(fs Fs) bool {
  536. return strings.HasPrefix(fs.Name(), sftpFsName)
  537. }
  538. // IsBufferedSFTPFs returns true if this is a buffered SFTP filesystem
  539. func IsBufferedSFTPFs(fs Fs) bool {
  540. if !IsSFTPFs(fs) {
  541. return false
  542. }
  543. return !fs.IsUploadResumeSupported()
  544. }
  545. // IsLocalOrUnbufferedSFTPFs returns true if fs is local or SFTP with no buffer
  546. func IsLocalOrUnbufferedSFTPFs(fs Fs) bool {
  547. if IsLocalOsFs(fs) {
  548. return true
  549. }
  550. if IsSFTPFs(fs) {
  551. return fs.IsUploadResumeSupported()
  552. }
  553. return false
  554. }
  555. // IsLocalOrSFTPFs returns true if fs is local or SFTP
  556. func IsLocalOrSFTPFs(fs Fs) bool {
  557. return IsLocalOsFs(fs) || IsSFTPFs(fs)
  558. }
  559. // HasOpenRWSupport returns true if the fs can open a file
  560. // for reading and writing at the same time
  561. func HasOpenRWSupport(fs Fs) bool {
  562. if IsLocalOsFs(fs) {
  563. return true
  564. }
  565. if IsSFTPFs(fs) && fs.IsUploadResumeSupported() {
  566. return true
  567. }
  568. return false
  569. }
  570. // IsLocalOrCryptoFs returns true if fs is local or local encrypted
  571. func IsLocalOrCryptoFs(fs Fs) bool {
  572. return IsLocalOsFs(fs) || IsCryptOsFs(fs)
  573. }
  574. // SetPathPermissions calls fs.Chown.
  575. // It does nothing for local filesystem on windows
  576. func SetPathPermissions(fs Fs, path string, uid int, gid int) {
  577. if uid == -1 && gid == -1 {
  578. return
  579. }
  580. if IsLocalOsFs(fs) {
  581. if runtime.GOOS == "windows" {
  582. return
  583. }
  584. }
  585. if err := fs.Chown(path, uid, gid); err != nil {
  586. fsLog(fs, logger.LevelWarn, "error chowning path %v: %v", path, err)
  587. }
  588. }
  589. func fsLog(fs Fs, level logger.LogLevel, format string, v ...interface{}) {
  590. logger.Log(level, fs.Name(), fs.ConnectionID(), format, v...)
  591. }