httpfs.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. package vfs
  2. import (
  3. "context"
  4. "crypto/tls"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "io/fs"
  10. "mime"
  11. "net/http"
  12. "net/url"
  13. "os"
  14. "path"
  15. "path/filepath"
  16. "strings"
  17. "time"
  18. "github.com/eikenb/pipeat"
  19. "github.com/pkg/sftp"
  20. "github.com/sftpgo/sdk"
  21. "github.com/drakkan/sftpgo/v2/kms"
  22. "github.com/drakkan/sftpgo/v2/logger"
  23. "github.com/drakkan/sftpgo/v2/util"
  24. )
  25. const (
  26. // httpFsName is the name for the HTTP Fs implementation
  27. httpFsName = "httpfs"
  28. )
  29. // HTTPFsConfig defines the configuration for HTTP based filesystem
  30. type HTTPFsConfig struct {
  31. sdk.BaseHTTPFsConfig
  32. Password *kms.Secret `json:"password,omitempty"`
  33. APIKey *kms.Secret `json:"api_key,omitempty"`
  34. }
  35. // HideConfidentialData hides confidential data
  36. func (c *HTTPFsConfig) HideConfidentialData() {
  37. if c.Password != nil {
  38. c.Password.Hide()
  39. }
  40. if c.APIKey != nil {
  41. c.APIKey.Hide()
  42. }
  43. }
  44. func (c *HTTPFsConfig) setNilSecretsIfEmpty() {
  45. if c.Password != nil && c.Password.IsEmpty() {
  46. c.Password = nil
  47. }
  48. if c.APIKey != nil && c.APIKey.IsEmpty() {
  49. c.APIKey = nil
  50. }
  51. }
  52. func (c *HTTPFsConfig) setEmptyCredentialsIfNil() {
  53. if c.Password == nil {
  54. c.Password = kms.NewEmptySecret()
  55. }
  56. if c.APIKey == nil {
  57. c.APIKey = kms.NewEmptySecret()
  58. }
  59. }
  60. func (c *HTTPFsConfig) isEqual(other *HTTPFsConfig) bool {
  61. if c.Endpoint != other.Endpoint {
  62. return false
  63. }
  64. if c.Username != other.Username {
  65. return false
  66. }
  67. if c.SkipTLSVerify != other.SkipTLSVerify {
  68. return false
  69. }
  70. c.setEmptyCredentialsIfNil()
  71. other.setEmptyCredentialsIfNil()
  72. if !c.Password.IsEqual(other.Password) {
  73. return false
  74. }
  75. return c.APIKey.IsEqual(other.APIKey)
  76. }
  77. // validate returns an error if the configuration is not valid
  78. func (c *HTTPFsConfig) validate() error {
  79. c.setEmptyCredentialsIfNil()
  80. if c.Endpoint == "" {
  81. return errors.New("httpfs: endpoint cannot be empty")
  82. }
  83. c.Endpoint = strings.TrimRight(c.Endpoint, "/")
  84. _, err := url.Parse(c.Endpoint)
  85. if err != nil {
  86. return fmt.Errorf("httpfs: invalid endpoint: %w", err)
  87. }
  88. if c.Password.IsEncrypted() && !c.Password.IsValid() {
  89. return errors.New("httpfs: invalid encrypted password")
  90. }
  91. if !c.Password.IsEmpty() && !c.Password.IsValidInput() {
  92. return errors.New("httpfs: invalid password")
  93. }
  94. if c.APIKey.IsEncrypted() && !c.APIKey.IsValid() {
  95. return errors.New("httpfs: invalid encrypted API key")
  96. }
  97. if !c.APIKey.IsEmpty() && !c.APIKey.IsValidInput() {
  98. return errors.New("httpfs: invalid API key")
  99. }
  100. return nil
  101. }
  102. // ValidateAndEncryptCredentials validates the config and encrypts credentials if they are in plain text
  103. func (c *HTTPFsConfig) ValidateAndEncryptCredentials(additionalData string) error {
  104. if err := c.validate(); err != nil {
  105. return util.NewValidationError(fmt.Sprintf("could not validate HTTP fs config: %v", err))
  106. }
  107. if c.Password.IsPlain() {
  108. c.Password.SetAdditionalData(additionalData)
  109. if err := c.Password.Encrypt(); err != nil {
  110. return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP fs password: %v", err))
  111. }
  112. }
  113. if c.APIKey.IsPlain() {
  114. c.APIKey.SetAdditionalData(additionalData)
  115. if err := c.APIKey.Encrypt(); err != nil {
  116. return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP fs API key: %v", err))
  117. }
  118. }
  119. return nil
  120. }
  121. // HTTPFs is a Fs implementation for the SFTPGo HTTP filesystem backend
  122. type HTTPFs struct {
  123. connectionID string
  124. localTempDir string
  125. // if not empty this fs is mouted as virtual folder in the specified path
  126. mountPath string
  127. config *HTTPFsConfig
  128. client *http.Client
  129. ctxTimeout time.Duration
  130. }
  131. // NewHTTPFs returns an HTTPFs object that allows to interact with SFTPGo HTTP filesystem backends
  132. func NewHTTPFs(connectionID, localTempDir, mountPath string, config HTTPFsConfig) (Fs, error) {
  133. if localTempDir == "" {
  134. if tempPath != "" {
  135. localTempDir = tempPath
  136. } else {
  137. localTempDir = filepath.Clean(os.TempDir())
  138. }
  139. }
  140. if err := config.validate(); err != nil {
  141. return nil, err
  142. }
  143. if !config.Password.IsEmpty() {
  144. if err := config.Password.TryDecrypt(); err != nil {
  145. return nil, err
  146. }
  147. }
  148. if !config.APIKey.IsEmpty() {
  149. if err := config.APIKey.TryDecrypt(); err != nil {
  150. return nil, err
  151. }
  152. }
  153. fs := &HTTPFs{
  154. connectionID: connectionID,
  155. localTempDir: localTempDir,
  156. mountPath: mountPath,
  157. config: &config,
  158. ctxTimeout: 30 * time.Second,
  159. }
  160. transport := http.DefaultTransport.(*http.Transport).Clone()
  161. transport.MaxResponseHeaderBytes = 1 << 16
  162. transport.WriteBufferSize = 1 << 16
  163. transport.ReadBufferSize = 1 << 16
  164. if config.SkipTLSVerify {
  165. if transport.TLSClientConfig != nil {
  166. transport.TLSClientConfig.InsecureSkipVerify = true
  167. } else {
  168. transport.TLSClientConfig = &tls.Config{
  169. NextProtos: []string{"h2", "http/1.1"},
  170. InsecureSkipVerify: true,
  171. }
  172. }
  173. }
  174. fs.client = &http.Client{
  175. Transport: transport,
  176. }
  177. return fs, nil
  178. }
  179. // Name returns the name for the Fs implementation
  180. func (fs *HTTPFs) Name() string {
  181. return fmt.Sprintf("%v %#v", httpFsName, fs.config.Endpoint)
  182. }
  183. // ConnectionID returns the connection ID associated to this Fs implementation
  184. func (fs *HTTPFs) ConnectionID() string {
  185. return fs.connectionID
  186. }
  187. // Stat returns a FileInfo describing the named file
  188. func (fs *HTTPFs) Stat(name string) (os.FileInfo, error) {
  189. ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
  190. defer cancelFn()
  191. resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "stat", name, "", "", nil)
  192. if err != nil {
  193. return nil, err
  194. }
  195. defer resp.Body.Close()
  196. var response statResponse
  197. err = json.NewDecoder(resp.Body).Decode(&response)
  198. if err != nil {
  199. return nil, err
  200. }
  201. return response.getFileInfo(), nil
  202. }
  203. // Lstat returns a FileInfo describing the named file
  204. func (fs *HTTPFs) Lstat(name string) (os.FileInfo, error) {
  205. return fs.Stat(name)
  206. }
  207. // Open opens the named file for reading
  208. func (fs *HTTPFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, func(), error) {
  209. r, w, err := pipeat.PipeInDir(fs.localTempDir)
  210. if err != nil {
  211. return nil, nil, nil, err
  212. }
  213. ctx, cancelFn := context.WithCancel(context.Background())
  214. var queryString string
  215. if offset > 0 {
  216. queryString = fmt.Sprintf("?offset=%d", offset)
  217. }
  218. go func() {
  219. defer cancelFn()
  220. resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "open", name, queryString, "", nil)
  221. if err != nil {
  222. fsLog(fs, logger.LevelError, "download error, path %q, err: %v", name, err)
  223. w.CloseWithError(err) //nolint:errcheck
  224. return
  225. }
  226. defer resp.Body.Close()
  227. n, err := io.Copy(w, resp.Body)
  228. w.CloseWithError(err) //nolint:errcheck
  229. fsLog(fs, logger.LevelDebug, "download completed, path %q size: %v, err: %+v", name, n, err)
  230. }()
  231. return nil, r, cancelFn, nil
  232. }
  233. // Create creates or opens the named file for writing
  234. func (fs *HTTPFs) Create(name string, flag int) (File, *PipeWriter, func(), error) {
  235. r, w, err := pipeat.PipeInDir(fs.localTempDir)
  236. if err != nil {
  237. return nil, nil, nil, err
  238. }
  239. p := NewPipeWriter(w)
  240. ctx, cancelFn := context.WithCancel(context.Background())
  241. var queryString string
  242. if flag > 0 {
  243. queryString = fmt.Sprintf("?flags=%d", flag)
  244. }
  245. go func() {
  246. defer cancelFn()
  247. contentType := mime.TypeByExtension(path.Ext(name))
  248. resp, err := fs.sendHTTPRequest(ctx, http.MethodPost, "create", name, queryString, contentType,
  249. &wrapReader{reader: r})
  250. if err != nil {
  251. fsLog(fs, logger.LevelError, "upload error, path %q, err: %v", name, err)
  252. r.CloseWithError(err) //nolint:errcheck
  253. p.Done(err)
  254. return
  255. }
  256. defer resp.Body.Close()
  257. r.CloseWithError(err) //nolint:errcheck
  258. p.Done(err)
  259. fsLog(fs, logger.LevelDebug, "upload completed, path: %q, readed bytes: %d", name, r.GetReadedBytes())
  260. }()
  261. return nil, p, cancelFn, nil
  262. }
  263. // Rename renames (moves) source to target.
  264. func (fs *HTTPFs) Rename(source, target string) error {
  265. ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
  266. defer cancelFn()
  267. queryString := fmt.Sprintf("?target=%s", url.QueryEscape(target))
  268. resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "rename", source, queryString, "", nil)
  269. if err != nil {
  270. return err
  271. }
  272. defer resp.Body.Close()
  273. return nil
  274. }
  275. // Remove removes the named file or (empty) directory.
  276. func (fs *HTTPFs) Remove(name string, isDir bool) error {
  277. ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
  278. defer cancelFn()
  279. resp, err := fs.sendHTTPRequest(ctx, http.MethodDelete, "remove", name, "", "", nil)
  280. if err != nil {
  281. return err
  282. }
  283. defer resp.Body.Close()
  284. return nil
  285. }
  286. // Mkdir creates a new directory with the specified name and default permissions
  287. func (fs *HTTPFs) Mkdir(name string) error {
  288. ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
  289. defer cancelFn()
  290. resp, err := fs.sendHTTPRequest(ctx, http.MethodPost, "mkdir", name, "", "", nil)
  291. if err != nil {
  292. return err
  293. }
  294. defer resp.Body.Close()
  295. return nil
  296. }
  297. // Symlink creates source as a symbolic link to target.
  298. func (*HTTPFs) Symlink(source, target string) error {
  299. return ErrVfsUnsupported
  300. }
  301. // Readlink returns the destination of the named symbolic link
  302. func (*HTTPFs) Readlink(name string) (string, error) {
  303. return "", ErrVfsUnsupported
  304. }
  305. // Chown changes the numeric uid and gid of the named file.
  306. func (fs *HTTPFs) Chown(name string, uid int, gid int) error {
  307. /*ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
  308. defer cancelFn()
  309. queryString := fmt.Sprintf("?uid=%d&gid=%d", uid, gid)
  310. resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "chown", name, queryString, "", nil)
  311. if err != nil {
  312. return err
  313. }
  314. defer resp.Body.Close()
  315. return nil*/
  316. return ErrVfsUnsupported
  317. }
  318. // Chmod changes the mode of the named file to mode.
  319. func (fs *HTTPFs) Chmod(name string, mode os.FileMode) error {
  320. ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
  321. defer cancelFn()
  322. queryString := fmt.Sprintf("?mode=%d", mode)
  323. resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "chmod", name, queryString, "", nil)
  324. if err != nil {
  325. return err
  326. }
  327. defer resp.Body.Close()
  328. return nil
  329. }
  330. // Chtimes changes the access and modification times of the named file.
  331. func (fs *HTTPFs) Chtimes(name string, atime, mtime time.Time, isUploading bool) error {
  332. ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
  333. defer cancelFn()
  334. queryString := fmt.Sprintf("?access_time=%s&modification_time=%s", atime.UTC().Format(time.RFC3339),
  335. mtime.UTC().Format(time.RFC3339))
  336. resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "chtimes", name, queryString, "", nil)
  337. if err != nil {
  338. return err
  339. }
  340. defer resp.Body.Close()
  341. return nil
  342. }
  343. // Truncate changes the size of the named file.
  344. // Truncate by path is not supported, while truncating an opened
  345. // file is handled inside base transfer
  346. func (fs *HTTPFs) Truncate(name string, size int64) error {
  347. ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
  348. defer cancelFn()
  349. queryString := fmt.Sprintf("?size=%d", size)
  350. resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "truncate", name, queryString, "", nil)
  351. if err != nil {
  352. return err
  353. }
  354. defer resp.Body.Close()
  355. return nil
  356. }
  357. // ReadDir reads the directory named by dirname and returns
  358. // a list of directory entries.
  359. func (fs *HTTPFs) ReadDir(dirname string) ([]os.FileInfo, error) {
  360. ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
  361. defer cancelFn()
  362. resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "readdir", dirname, "", "", nil)
  363. if err != nil {
  364. return nil, err
  365. }
  366. defer resp.Body.Close()
  367. var response []statResponse
  368. err = json.NewDecoder(resp.Body).Decode(&response)
  369. if err != nil {
  370. return nil, err
  371. }
  372. result := make([]os.FileInfo, 0, len(response))
  373. for _, stat := range response {
  374. result = append(result, stat.getFileInfo())
  375. }
  376. return result, nil
  377. }
  378. // IsUploadResumeSupported returns true if resuming uploads is supported.
  379. func (*HTTPFs) IsUploadResumeSupported() bool {
  380. return false
  381. }
  382. // IsAtomicUploadSupported returns true if atomic upload is supported.
  383. func (*HTTPFs) IsAtomicUploadSupported() bool {
  384. return false
  385. }
  386. // IsNotExist returns a boolean indicating whether the error is known to
  387. // report that a file or directory does not exist
  388. func (*HTTPFs) IsNotExist(err error) bool {
  389. return errors.Is(err, fs.ErrNotExist)
  390. }
  391. // IsPermission returns a boolean indicating whether the error is known to
  392. // report that permission is denied.
  393. func (*HTTPFs) IsPermission(err error) bool {
  394. return errors.Is(err, fs.ErrPermission)
  395. }
  396. // IsNotSupported returns true if the error indicate an unsupported operation
  397. func (*HTTPFs) IsNotSupported(err error) bool {
  398. if err == nil {
  399. return false
  400. }
  401. return err == ErrVfsUnsupported
  402. }
  403. // CheckRootPath creates the specified local root directory if it does not exists
  404. func (fs *HTTPFs) CheckRootPath(username string, uid int, gid int) bool {
  405. // we need a local directory for temporary files
  406. osFs := NewOsFs(fs.ConnectionID(), fs.localTempDir, "")
  407. return osFs.CheckRootPath(username, uid, gid)
  408. }
  409. // ScanRootDirContents returns the number of files and their size
  410. func (fs *HTTPFs) ScanRootDirContents() (int, int64, error) {
  411. return fs.GetDirSize("/")
  412. }
  413. // CheckMetadata checks the metadata consistency
  414. func (*HTTPFs) CheckMetadata() error {
  415. return nil
  416. }
  417. // GetDirSize returns the number of files and the size for a folder
  418. // including any subfolders
  419. func (fs *HTTPFs) GetDirSize(dirname string) (int, int64, error) {
  420. ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
  421. defer cancelFn()
  422. resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "dirsize", dirname, "", "", nil)
  423. if err != nil {
  424. return 0, 0, err
  425. }
  426. defer resp.Body.Close()
  427. var response dirSizeResponse
  428. err = json.NewDecoder(resp.Body).Decode(&response)
  429. if err != nil {
  430. return 0, 0, err
  431. }
  432. return response.Files, response.Size, nil
  433. }
  434. // GetAtomicUploadPath returns the path to use for an atomic upload.
  435. func (*HTTPFs) GetAtomicUploadPath(name string) string {
  436. return ""
  437. }
  438. // GetRelativePath returns the path for a file relative to the user's home dir.
  439. // This is the path as seen by SFTPGo users
  440. func (fs *HTTPFs) GetRelativePath(name string) string {
  441. rel := path.Clean(name)
  442. if rel == "." {
  443. rel = ""
  444. }
  445. if !path.IsAbs(rel) {
  446. rel = "/" + rel
  447. }
  448. if fs.mountPath != "" {
  449. rel = path.Join(fs.mountPath, rel)
  450. }
  451. return rel
  452. }
  453. // Walk walks the file tree rooted at root, calling walkFn for each file or
  454. // directory in the tree, including root. The result are unordered
  455. func (fs *HTTPFs) Walk(root string, walkFn filepath.WalkFunc) error {
  456. info, err := fs.Lstat(root)
  457. if err != nil {
  458. return walkFn(root, nil, err)
  459. }
  460. return fs.walk(root, info, walkFn)
  461. }
  462. // Join joins any number of path elements into a single path
  463. func (*HTTPFs) Join(elem ...string) string {
  464. return strings.TrimPrefix(path.Join(elem...), "/")
  465. }
  466. // HasVirtualFolders returns true if folders are emulated
  467. func (*HTTPFs) HasVirtualFolders() bool {
  468. return false
  469. }
  470. // ResolvePath returns the matching filesystem path for the specified virtual path
  471. func (fs *HTTPFs) ResolvePath(virtualPath string) (string, error) {
  472. if fs.mountPath != "" {
  473. virtualPath = strings.TrimPrefix(virtualPath, fs.mountPath)
  474. }
  475. if !path.IsAbs(virtualPath) {
  476. virtualPath = path.Clean("/" + virtualPath)
  477. }
  478. return virtualPath, nil
  479. }
  480. // GetMimeType returns the content type
  481. func (fs *HTTPFs) GetMimeType(name string) (string, error) {
  482. ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
  483. defer cancelFn()
  484. resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "stat", name, "", "", nil)
  485. if err != nil {
  486. return "", err
  487. }
  488. defer resp.Body.Close()
  489. var response mimeTypeResponse
  490. err = json.NewDecoder(resp.Body).Decode(&response)
  491. if err != nil {
  492. return "", err
  493. }
  494. return response.Mime, nil
  495. }
  496. // Close closes the fs
  497. func (fs *HTTPFs) Close() error {
  498. fs.client.CloseIdleConnections()
  499. return nil
  500. }
  501. // GetAvailableDiskSize returns the available size for the specified path
  502. func (fs *HTTPFs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {
  503. ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
  504. defer cancelFn()
  505. resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "statvfs", dirName, "", "", nil)
  506. if err != nil {
  507. return nil, err
  508. }
  509. defer resp.Body.Close()
  510. var response statVFSResponse
  511. err = json.NewDecoder(resp.Body).Decode(&response)
  512. if err != nil {
  513. return nil, err
  514. }
  515. return response.toSFTPStatVFS(), nil
  516. }
  517. func (fs *HTTPFs) sendHTTPRequest(ctx context.Context, method, base, name, queryString, contentType string,
  518. body io.Reader,
  519. ) (*http.Response, error) {
  520. url := fmt.Sprintf("%s/%s/%s%s", fs.config.Endpoint, base, url.PathEscape(name), queryString)
  521. req, err := http.NewRequest(method, url, body)
  522. if err != nil {
  523. return nil, err
  524. }
  525. if contentType != "" {
  526. req.Header.Set("Content-Type", contentType)
  527. }
  528. if fs.config.APIKey.GetPayload() != "" {
  529. req.Header.Set("X-API-KEY", fs.config.APIKey.GetPayload())
  530. }
  531. if fs.config.Username != "" || fs.config.Password.GetPayload() != "" {
  532. req.SetBasicAuth(fs.config.Username, fs.config.Password.GetPayload())
  533. }
  534. resp, err := fs.client.Do(req.WithContext(ctx))
  535. if err != nil {
  536. return nil, fmt.Errorf("unable to send HTTP request to URL %v: %w", url, err)
  537. }
  538. if err = getErrorFromResponseCode(resp.StatusCode); err != nil {
  539. resp.Body.Close()
  540. return nil, err
  541. }
  542. return resp, nil
  543. }
  544. // walk recursively descends path, calling walkFn.
  545. func (fs *HTTPFs) walk(filePath string, info fs.FileInfo, walkFn filepath.WalkFunc) error {
  546. if !info.IsDir() {
  547. return walkFn(filePath, info, nil)
  548. }
  549. files, err := fs.ReadDir(filePath)
  550. err1 := walkFn(filePath, info, err)
  551. if err != nil || err1 != nil {
  552. return err1
  553. }
  554. for _, fi := range files {
  555. objName := path.Join(filePath, fi.Name())
  556. err = fs.walk(objName, fi, walkFn)
  557. if err != nil {
  558. return err
  559. }
  560. }
  561. return nil
  562. }
  563. func getErrorFromResponseCode(code int) error {
  564. switch code {
  565. case 401, 403:
  566. return os.ErrPermission
  567. case 404:
  568. return os.ErrNotExist
  569. case 501:
  570. return ErrVfsUnsupported
  571. case 200, 201:
  572. return nil
  573. default:
  574. return fmt.Errorf("unexpected response code: %v", code)
  575. }
  576. }
  577. type wrapReader struct {
  578. reader io.Reader
  579. }
  580. func (r *wrapReader) Read(p []byte) (n int, err error) {
  581. return r.reader.Read(p)
  582. }
  583. type statResponse struct {
  584. Name string `json:"name"`
  585. Size int64 `json:"size"`
  586. Mode uint32 `json:"mode"`
  587. LastModified time.Time `json:"last_modified"`
  588. }
  589. func (s *statResponse) getFileInfo() os.FileInfo {
  590. info := NewFileInfo(s.Name, false, s.Size, s.LastModified, false)
  591. info.SetMode(fs.FileMode(s.Mode))
  592. return info
  593. }
  594. type dirSizeResponse struct {
  595. Files int `json:"files"`
  596. Size int64 `json:"size"`
  597. }
  598. type mimeTypeResponse struct {
  599. Mime string `json:"mime"`
  600. }
  601. type statVFSResponse struct {
  602. ID uint32 `json:"-"`
  603. Bsize uint64 `json:"bsize"`
  604. Frsize uint64 `json:"frsize"`
  605. Blocks uint64 `json:"blocks"`
  606. Bfree uint64 `json:"bfree"`
  607. Bavail uint64 `json:"bavail"`
  608. Files uint64 `json:"files"`
  609. Ffree uint64 `json:"ffree"`
  610. Favail uint64 `json:"favail"`
  611. Fsid uint64 `json:"fsid"`
  612. Flag uint64 `json:"flag"`
  613. Namemax uint64 `json:"namemax"`
  614. }
  615. func (s *statVFSResponse) toSFTPStatVFS() *sftp.StatVFS {
  616. return &sftp.StatVFS{
  617. Bsize: s.Bsize,
  618. Frsize: s.Frsize,
  619. Blocks: s.Blocks,
  620. Bfree: s.Bfree,
  621. Bavail: s.Bavail,
  622. Files: s.Files,
  623. Ffree: s.Ffree,
  624. Favail: s.Ffree,
  625. Flag: s.Flag,
  626. Namemax: s.Namemax,
  627. }
  628. }