osfs.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. // Copyright (C) 2019-2023 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. package vfs
  15. import (
  16. "errors"
  17. "fmt"
  18. "io"
  19. "io/fs"
  20. "net/http"
  21. "os"
  22. "path"
  23. "path/filepath"
  24. "strings"
  25. "time"
  26. "github.com/eikenb/pipeat"
  27. fscopy "github.com/otiai10/copy"
  28. "github.com/pkg/sftp"
  29. "github.com/rs/xid"
  30. "github.com/drakkan/sftpgo/v2/internal/logger"
  31. "github.com/drakkan/sftpgo/v2/internal/util"
  32. )
  33. const (
  34. // osFsName is the name for the local Fs implementation
  35. osFsName = "osfs"
  36. )
  37. type pathResolutionError struct {
  38. err string
  39. }
  40. func (e *pathResolutionError) Error() string {
  41. return fmt.Sprintf("Path resolution error: %s", e.err)
  42. }
  43. // OsFs is a Fs implementation that uses functions provided by the os package.
  44. type OsFs struct {
  45. name string
  46. connectionID string
  47. rootDir string
  48. // if not empty this fs is mouted as virtual folder in the specified path
  49. mountPath string
  50. }
  51. // NewOsFs returns an OsFs object that allows to interact with local Os filesystem
  52. func NewOsFs(connectionID, rootDir, mountPath string) Fs {
  53. return &OsFs{
  54. name: osFsName,
  55. connectionID: connectionID,
  56. rootDir: rootDir,
  57. mountPath: getMountPath(mountPath),
  58. }
  59. }
  60. // Name returns the name for the Fs implementation
  61. func (fs *OsFs) Name() string {
  62. return fs.name
  63. }
  64. // ConnectionID returns the SSH connection ID associated to this Fs implementation
  65. func (fs *OsFs) ConnectionID() string {
  66. return fs.connectionID
  67. }
  68. // Stat returns a FileInfo describing the named file
  69. func (fs *OsFs) Stat(name string) (os.FileInfo, error) {
  70. return os.Stat(name)
  71. }
  72. // Lstat returns a FileInfo describing the named file
  73. func (fs *OsFs) Lstat(name string) (os.FileInfo, error) {
  74. return os.Lstat(name)
  75. }
  76. // Open opens the named file for reading
  77. func (*OsFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, func(), error) {
  78. f, err := os.Open(name)
  79. if err != nil {
  80. return nil, nil, nil, err
  81. }
  82. if offset > 0 {
  83. _, err = f.Seek(offset, io.SeekStart)
  84. if err != nil {
  85. f.Close()
  86. return nil, nil, nil, err
  87. }
  88. }
  89. return f, nil, nil, err
  90. }
  91. // Create creates or opens the named file for writing
  92. func (*OsFs) Create(name string, flag int) (File, *PipeWriter, func(), error) {
  93. var err error
  94. var f *os.File
  95. if flag == 0 {
  96. f, err = os.Create(name)
  97. } else {
  98. f, err = os.OpenFile(name, flag, 0666)
  99. }
  100. return f, nil, nil, err
  101. }
  102. // Rename renames (moves) source to target
  103. func (fs *OsFs) Rename(source, target string) (int, int64, error) {
  104. if source == target {
  105. return -1, -1, nil
  106. }
  107. err := os.Rename(source, target)
  108. if err != nil && isCrossDeviceError(err) {
  109. fsLog(fs, logger.LevelError, "cross device error detected while renaming %q -> %q. Trying a copy and remove, this could take a long time",
  110. source, target)
  111. err = fscopy.Copy(source, target, fscopy.Options{
  112. OnSymlink: func(src string) fscopy.SymlinkAction {
  113. return fscopy.Skip
  114. },
  115. })
  116. if err != nil {
  117. fsLog(fs, logger.LevelError, "cross device copy error: %v", err)
  118. return -1, -1, err
  119. }
  120. err = os.RemoveAll(source)
  121. return -1, -1, err
  122. }
  123. return -1, -1, err
  124. }
  125. // Remove removes the named file or (empty) directory.
  126. func (*OsFs) Remove(name string, _ bool) error {
  127. return os.Remove(name)
  128. }
  129. // Mkdir creates a new directory with the specified name and default permissions
  130. func (*OsFs) Mkdir(name string) error {
  131. return os.Mkdir(name, os.ModePerm)
  132. }
  133. // Symlink creates source as a symbolic link to target.
  134. func (*OsFs) Symlink(source, target string) error {
  135. return os.Symlink(source, target)
  136. }
  137. // Readlink returns the destination of the named symbolic link
  138. // as absolute virtual path
  139. func (fs *OsFs) Readlink(name string) (string, error) {
  140. // we don't have to follow multiple links:
  141. // https://github.com/openssh/openssh-portable/blob/7bf2eb958fbb551e7d61e75c176bb3200383285d/sftp-server.c#L1329
  142. resolved, err := os.Readlink(name)
  143. if err != nil {
  144. return "", err
  145. }
  146. resolved = filepath.Clean(resolved)
  147. if !filepath.IsAbs(resolved) {
  148. resolved = filepath.Join(filepath.Dir(name), resolved)
  149. }
  150. return fs.GetRelativePath(resolved), nil
  151. }
  152. // Chown changes the numeric uid and gid of the named file.
  153. func (*OsFs) Chown(name string, uid int, gid int) error {
  154. return os.Chown(name, uid, gid)
  155. }
  156. // Chmod changes the mode of the named file to mode
  157. func (*OsFs) Chmod(name string, mode os.FileMode) error {
  158. return os.Chmod(name, mode)
  159. }
  160. // Chtimes changes the access and modification times of the named file
  161. func (*OsFs) Chtimes(name string, atime, mtime time.Time, _ bool) error {
  162. return os.Chtimes(name, atime, mtime)
  163. }
  164. // Truncate changes the size of the named file
  165. func (*OsFs) Truncate(name string, size int64) error {
  166. return os.Truncate(name, size)
  167. }
  168. // ReadDir reads the directory named by dirname and returns
  169. // a list of directory entries.
  170. func (*OsFs) ReadDir(dirname string) ([]os.FileInfo, error) {
  171. f, err := os.Open(dirname)
  172. if err != nil {
  173. if isInvalidNameError(err) {
  174. err = os.ErrNotExist
  175. }
  176. return nil, err
  177. }
  178. list, err := f.Readdir(-1)
  179. f.Close()
  180. if err != nil {
  181. return nil, err
  182. }
  183. return list, nil
  184. }
  185. // IsUploadResumeSupported returns true if resuming uploads is supported
  186. func (*OsFs) IsUploadResumeSupported() bool {
  187. return true
  188. }
  189. // IsAtomicUploadSupported returns true if atomic upload is supported
  190. func (*OsFs) IsAtomicUploadSupported() bool {
  191. return true
  192. }
  193. // IsNotExist returns a boolean indicating whether the error is known to
  194. // report that a file or directory does not exist
  195. func (*OsFs) IsNotExist(err error) bool {
  196. return errors.Is(err, fs.ErrNotExist)
  197. }
  198. // IsPermission returns a boolean indicating whether the error is known to
  199. // report that permission is denied.
  200. func (*OsFs) IsPermission(err error) bool {
  201. if _, ok := err.(*pathResolutionError); ok {
  202. return true
  203. }
  204. return errors.Is(err, fs.ErrPermission)
  205. }
  206. // IsNotSupported returns true if the error indicate an unsupported operation
  207. func (*OsFs) IsNotSupported(err error) bool {
  208. if err == nil {
  209. return false
  210. }
  211. return err == ErrVfsUnsupported
  212. }
  213. // CheckRootPath creates the root directory if it does not exists
  214. func (fs *OsFs) CheckRootPath(username string, uid int, gid int) bool {
  215. var err error
  216. if _, err = fs.Stat(fs.rootDir); fs.IsNotExist(err) {
  217. err = os.MkdirAll(fs.rootDir, os.ModePerm)
  218. if err == nil {
  219. SetPathPermissions(fs, fs.rootDir, uid, gid)
  220. } else {
  221. fsLog(fs, logger.LevelError, "error creating root directory %q for user %q: %v", fs.rootDir, username, err)
  222. }
  223. }
  224. return err == nil
  225. }
  226. // ScanRootDirContents returns the number of files contained in the root
  227. // directory and their size
  228. func (fs *OsFs) ScanRootDirContents() (int, int64, error) {
  229. return fs.GetDirSize(fs.rootDir)
  230. }
  231. // CheckMetadata checks the metadata consistency
  232. func (*OsFs) CheckMetadata() error {
  233. return nil
  234. }
  235. // GetAtomicUploadPath returns the path to use for an atomic upload
  236. func (*OsFs) GetAtomicUploadPath(name string) string {
  237. dir := filepath.Dir(name)
  238. if tempPath != "" {
  239. dir = tempPath
  240. }
  241. guid := xid.New().String()
  242. return filepath.Join(dir, ".sftpgo-upload."+guid+"."+filepath.Base(name))
  243. }
  244. // GetRelativePath returns the path for a file relative to the user's home dir.
  245. // This is the path as seen by SFTPGo users
  246. func (fs *OsFs) GetRelativePath(name string) string {
  247. virtualPath := "/"
  248. if fs.mountPath != "" {
  249. virtualPath = fs.mountPath
  250. }
  251. rel, err := filepath.Rel(fs.rootDir, filepath.Clean(name))
  252. if err != nil {
  253. return ""
  254. }
  255. if rel == "." || strings.HasPrefix(rel, "..") {
  256. rel = ""
  257. }
  258. return path.Join(virtualPath, filepath.ToSlash(rel))
  259. }
  260. // Walk walks the file tree rooted at root, calling walkFn for each file or
  261. // directory in the tree, including root
  262. func (*OsFs) Walk(root string, walkFn filepath.WalkFunc) error {
  263. return filepath.Walk(root, walkFn)
  264. }
  265. // Join joins any number of path elements into a single path
  266. func (*OsFs) Join(elem ...string) string {
  267. return filepath.Join(elem...)
  268. }
  269. // ResolvePath returns the matching filesystem path for the specified sftp path
  270. func (fs *OsFs) ResolvePath(virtualPath string) (string, error) {
  271. if !filepath.IsAbs(fs.rootDir) {
  272. return "", fmt.Errorf("invalid root path %q", fs.rootDir)
  273. }
  274. if fs.mountPath != "" {
  275. virtualPath = strings.TrimPrefix(virtualPath, fs.mountPath)
  276. }
  277. r := filepath.Clean(filepath.Join(fs.rootDir, virtualPath))
  278. p, err := filepath.EvalSymlinks(r)
  279. if isInvalidNameError(err) {
  280. err = os.ErrNotExist
  281. }
  282. isNotExist := fs.IsNotExist(err)
  283. if err != nil && !isNotExist {
  284. return "", err
  285. } else if isNotExist {
  286. // The requested path doesn't exist, so at this point we need to iterate up the
  287. // path chain until we hit a directory that _does_ exist and can be validated.
  288. _, err = fs.findFirstExistingDir(r)
  289. if err != nil {
  290. fsLog(fs, logger.LevelError, "error resolving non-existent path %q", err)
  291. }
  292. return r, err
  293. }
  294. err = fs.isSubDir(p)
  295. if err != nil {
  296. fsLog(fs, logger.LevelError, "Invalid path resolution, path %q original path %q resolved %q err: %v",
  297. p, virtualPath, r, err)
  298. }
  299. return r, err
  300. }
  301. // RealPath implements the FsRealPather interface
  302. func (fs *OsFs) RealPath(p string) (string, error) {
  303. linksWalked := 0
  304. for {
  305. info, err := os.Lstat(p)
  306. if err != nil {
  307. if errors.Is(err, os.ErrNotExist) {
  308. return fs.GetRelativePath(p), nil
  309. }
  310. return "", err
  311. }
  312. if info.Mode()&os.ModeSymlink == 0 {
  313. return fs.GetRelativePath(p), nil
  314. }
  315. resolvedLink, err := os.Readlink(p)
  316. if err != nil {
  317. return "", err
  318. }
  319. resolvedLink = filepath.Clean(resolvedLink)
  320. if filepath.IsAbs(resolvedLink) {
  321. p = resolvedLink
  322. } else {
  323. p = filepath.Join(filepath.Dir(p), resolvedLink)
  324. }
  325. linksWalked++
  326. if linksWalked > 10 {
  327. fsLog(fs, logger.LevelError, "unable to get real path, too many links: %d", linksWalked)
  328. return "", &pathResolutionError{err: "too many links"}
  329. }
  330. }
  331. }
  332. // GetDirSize returns the number of files and the size for a folder
  333. // including any subfolders
  334. func (fs *OsFs) GetDirSize(dirname string) (int, int64, error) {
  335. numFiles := 0
  336. size := int64(0)
  337. isDir, err := isDirectory(fs, dirname)
  338. if err == nil && isDir {
  339. err = filepath.Walk(dirname, func(_ string, info os.FileInfo, err error) error {
  340. if err != nil {
  341. return err
  342. }
  343. if info != nil && info.Mode().IsRegular() {
  344. size += info.Size()
  345. numFiles++
  346. if numFiles%1000 == 0 {
  347. fsLog(fs, logger.LevelDebug, "dirname %q scan in progress, files: %d, size: %d", dirname, numFiles, size)
  348. }
  349. }
  350. return err
  351. })
  352. }
  353. return numFiles, size, err
  354. }
  355. // HasVirtualFolders returns true if folders are emulated
  356. func (*OsFs) HasVirtualFolders() bool {
  357. return false
  358. }
  359. func (fs *OsFs) findNonexistentDirs(filePath string) ([]string, error) {
  360. results := []string{}
  361. cleanPath := filepath.Clean(filePath)
  362. parent := filepath.Dir(cleanPath)
  363. _, err := os.Stat(parent)
  364. for fs.IsNotExist(err) {
  365. results = append(results, parent)
  366. parent = filepath.Dir(parent)
  367. if util.Contains(results, parent) {
  368. break
  369. }
  370. _, err = os.Stat(parent)
  371. }
  372. if err != nil {
  373. return results, err
  374. }
  375. p, err := filepath.EvalSymlinks(parent)
  376. if err != nil {
  377. return results, err
  378. }
  379. err = fs.isSubDir(p)
  380. if err != nil {
  381. fsLog(fs, logger.LevelError, "error finding non existing dir: %v", err)
  382. }
  383. return results, err
  384. }
  385. func (fs *OsFs) findFirstExistingDir(path string) (string, error) {
  386. results, err := fs.findNonexistentDirs(path)
  387. if err != nil {
  388. fsLog(fs, logger.LevelError, "unable to find non existent dirs: %v", err)
  389. return "", err
  390. }
  391. var parent string
  392. if len(results) > 0 {
  393. lastMissingDir := results[len(results)-1]
  394. parent = filepath.Dir(lastMissingDir)
  395. } else {
  396. parent = fs.rootDir
  397. }
  398. p, err := filepath.EvalSymlinks(parent)
  399. if err != nil {
  400. return "", err
  401. }
  402. fileInfo, err := os.Stat(p)
  403. if err != nil {
  404. return "", err
  405. }
  406. if !fileInfo.IsDir() {
  407. return "", fmt.Errorf("resolved path is not a dir: %q", p)
  408. }
  409. err = fs.isSubDir(p)
  410. return p, err
  411. }
  412. func (fs *OsFs) isSubDir(sub string) error {
  413. // fs.rootDir must exist and it is already a validated absolute path
  414. parent, err := filepath.EvalSymlinks(fs.rootDir)
  415. if err != nil {
  416. fsLog(fs, logger.LevelError, "invalid root path %q: %v", fs.rootDir, err)
  417. return err
  418. }
  419. if parent == sub {
  420. return nil
  421. }
  422. if len(sub) < len(parent) {
  423. err = fmt.Errorf("path %q is not inside %q", sub, parent)
  424. return &pathResolutionError{err: err.Error()}
  425. }
  426. separator := string(os.PathSeparator)
  427. if parent == filepath.Dir(parent) {
  428. // parent is the root dir, on Windows we can have C:\, D:\ and so on here
  429. // so we still need the prefix check
  430. separator = ""
  431. }
  432. if !strings.HasPrefix(sub, parent+separator) {
  433. err = fmt.Errorf("path %q is not inside %q", sub, parent)
  434. return &pathResolutionError{err: err.Error()}
  435. }
  436. return nil
  437. }
  438. // GetMimeType returns the content type
  439. func (fs *OsFs) GetMimeType(name string) (string, error) {
  440. f, err := os.OpenFile(name, os.O_RDONLY, 0)
  441. if err != nil {
  442. return "", err
  443. }
  444. defer f.Close()
  445. var buf [512]byte
  446. n, err := io.ReadFull(f, buf[:])
  447. if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
  448. return "", err
  449. }
  450. ctype := http.DetectContentType(buf[:n])
  451. // Rewind file.
  452. _, err = f.Seek(0, io.SeekStart)
  453. return ctype, err
  454. }
  455. // Close closes the fs
  456. func (*OsFs) Close() error {
  457. return nil
  458. }
  459. // GetAvailableDiskSize returns the available size for the specified path
  460. func (*OsFs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {
  461. return getStatFS(dirName)
  462. }