osfs.go 13 KB

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