osfs.go 12 KB

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