| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 | 
							- // Copyright (C) 2019-2022  Nicola Murino
 
- //
 
- // This program is free software: you can redistribute it and/or modify
 
- // it under the terms of the GNU Affero General Public License as published
 
- // by the Free Software Foundation, version 3.
 
- //
 
- // This program is distributed in the hope that it will be useful,
 
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
 
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 
- // GNU Affero General Public License for more details.
 
- //
 
- // You should have received a copy of the GNU Affero General Public License
 
- // along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
- package vfs
 
- import (
 
- 	"errors"
 
- 	"fmt"
 
- 	"io"
 
- 	"io/fs"
 
- 	"net/http"
 
- 	"os"
 
- 	"path"
 
- 	"path/filepath"
 
- 	"strings"
 
- 	"time"
 
- 	"github.com/eikenb/pipeat"
 
- 	fscopy "github.com/otiai10/copy"
 
- 	"github.com/pkg/sftp"
 
- 	"github.com/rs/xid"
 
- 	"github.com/drakkan/sftpgo/v2/internal/logger"
 
- )
 
- const (
 
- 	// osFsName is the name for the local Fs implementation
 
- 	osFsName = "osfs"
 
- )
 
- type pathResolutionError struct {
 
- 	err string
 
- }
 
- func (e *pathResolutionError) Error() string {
 
- 	return fmt.Sprintf("Path resolution error: %s", e.err)
 
- }
 
- // OsFs is a Fs implementation that uses functions provided by the os package.
 
- type OsFs struct {
 
- 	name         string
 
- 	connectionID string
 
- 	rootDir      string
 
- 	// if not empty this fs is mouted as virtual folder in the specified path
 
- 	mountPath string
 
- }
 
- // NewOsFs returns an OsFs object that allows to interact with local Os filesystem
 
- func NewOsFs(connectionID, rootDir, mountPath string) Fs {
 
- 	return &OsFs{
 
- 		name:         osFsName,
 
- 		connectionID: connectionID,
 
- 		rootDir:      rootDir,
 
- 		mountPath:    getMountPath(mountPath),
 
- 	}
 
- }
 
- // Name returns the name for the Fs implementation
 
- func (fs *OsFs) Name() string {
 
- 	return fs.name
 
- }
 
- // ConnectionID returns the SSH connection ID associated to this Fs implementation
 
- func (fs *OsFs) ConnectionID() string {
 
- 	return fs.connectionID
 
- }
 
- // Stat returns a FileInfo describing the named file
 
- func (fs *OsFs) Stat(name string) (os.FileInfo, error) {
 
- 	return os.Stat(name)
 
- }
 
- // Lstat returns a FileInfo describing the named file
 
- func (fs *OsFs) Lstat(name string) (os.FileInfo, error) {
 
- 	return os.Lstat(name)
 
- }
 
- // Open opens the named file for reading
 
- func (*OsFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, func(), error) {
 
- 	f, err := os.Open(name)
 
- 	if err != nil {
 
- 		return nil, nil, nil, err
 
- 	}
 
- 	if offset > 0 {
 
- 		_, err = f.Seek(offset, io.SeekStart)
 
- 		if err != nil {
 
- 			f.Close()
 
- 			return nil, nil, nil, err
 
- 		}
 
- 	}
 
- 	return f, nil, nil, err
 
- }
 
- // Create creates or opens the named file for writing
 
- func (*OsFs) Create(name string, flag int) (File, *PipeWriter, func(), error) {
 
- 	var err error
 
- 	var f *os.File
 
- 	if flag == 0 {
 
- 		f, err = os.Create(name)
 
- 	} else {
 
- 		f, err = os.OpenFile(name, flag, 0666)
 
- 	}
 
- 	return f, nil, nil, err
 
- }
 
- // Rename renames (moves) source to target
 
- func (fs *OsFs) Rename(source, target string) error {
 
- 	if source == target {
 
- 		return nil
 
- 	}
 
- 	err := os.Rename(source, target)
 
- 	if err != nil && isCrossDeviceError(err) {
 
- 		fsLog(fs, logger.LevelError, "cross device error detected while renaming %#v -> %#v. Trying a copy and remove, this could take a long time",
 
- 			source, target)
 
- 		err = fscopy.Copy(source, target, fscopy.Options{
 
- 			OnSymlink: func(src string) fscopy.SymlinkAction {
 
- 				return fscopy.Skip
 
- 			},
 
- 		})
 
- 		if err != nil {
 
- 			fsLog(fs, logger.LevelError, "cross device copy error: %v", err)
 
- 			return err
 
- 		}
 
- 		return os.RemoveAll(source)
 
- 	}
 
- 	return err
 
- }
 
- // Remove removes the named file or (empty) directory.
 
- func (*OsFs) Remove(name string, isDir bool) error {
 
- 	return os.Remove(name)
 
- }
 
- // Mkdir creates a new directory with the specified name and default permissions
 
- func (*OsFs) Mkdir(name string) error {
 
- 	return os.Mkdir(name, os.ModePerm)
 
- }
 
- // Symlink creates source as a symbolic link to target.
 
- func (*OsFs) Symlink(source, target string) error {
 
- 	return os.Symlink(source, target)
 
- }
 
- // Readlink returns the destination of the named symbolic link
 
- // as absolute virtual path
 
- func (fs *OsFs) Readlink(name string) (string, error) {
 
- 	// we don't have to follow multiple links:
 
- 	// https://github.com/openssh/openssh-portable/blob/7bf2eb958fbb551e7d61e75c176bb3200383285d/sftp-server.c#L1329
 
- 	resolved, err := os.Readlink(name)
 
- 	if err != nil {
 
- 		return "", err
 
- 	}
 
- 	resolved = filepath.Clean(resolved)
 
- 	if !filepath.IsAbs(resolved) {
 
- 		resolved = filepath.Join(filepath.Dir(name), resolved)
 
- 	}
 
- 	return fs.GetRelativePath(resolved), nil
 
- }
 
- // Chown changes the numeric uid and gid of the named file.
 
- func (*OsFs) Chown(name string, uid int, gid int) error {
 
- 	return os.Chown(name, uid, gid)
 
- }
 
- // Chmod changes the mode of the named file to mode
 
- func (*OsFs) Chmod(name string, mode os.FileMode) error {
 
- 	return os.Chmod(name, mode)
 
- }
 
- // Chtimes changes the access and modification times of the named file
 
- func (*OsFs) Chtimes(name string, atime, mtime time.Time, isUploading bool) error {
 
- 	return os.Chtimes(name, atime, mtime)
 
- }
 
- // Truncate changes the size of the named file
 
- func (*OsFs) Truncate(name string, size int64) error {
 
- 	return os.Truncate(name, size)
 
- }
 
- // ReadDir reads the directory named by dirname and returns
 
- // a list of directory entries.
 
- func (*OsFs) ReadDir(dirname string) ([]os.FileInfo, error) {
 
- 	f, err := os.Open(dirname)
 
- 	if err != nil {
 
- 		if isInvalidNameError(err) {
 
- 			err = os.ErrNotExist
 
- 		}
 
- 		return nil, err
 
- 	}
 
- 	list, err := f.Readdir(-1)
 
- 	f.Close()
 
- 	if err != nil {
 
- 		return nil, err
 
- 	}
 
- 	return list, nil
 
- }
 
- // IsUploadResumeSupported returns true if resuming uploads is supported
 
- func (*OsFs) IsUploadResumeSupported() bool {
 
- 	return true
 
- }
 
- // IsAtomicUploadSupported returns true if atomic upload is supported
 
- func (*OsFs) IsAtomicUploadSupported() bool {
 
- 	return true
 
- }
 
- // IsNotExist returns a boolean indicating whether the error is known to
 
- // report that a file or directory does not exist
 
- func (*OsFs) IsNotExist(err error) bool {
 
- 	return errors.Is(err, fs.ErrNotExist)
 
- }
 
- // IsPermission returns a boolean indicating whether the error is known to
 
- // report that permission is denied.
 
- func (*OsFs) IsPermission(err error) bool {
 
- 	if _, ok := err.(*pathResolutionError); ok {
 
- 		return true
 
- 	}
 
- 	return errors.Is(err, fs.ErrPermission)
 
- }
 
- // IsNotSupported returns true if the error indicate an unsupported operation
 
- func (*OsFs) IsNotSupported(err error) bool {
 
- 	if err == nil {
 
- 		return false
 
- 	}
 
- 	return err == ErrVfsUnsupported
 
- }
 
- // CheckRootPath creates the root directory if it does not exists
 
- func (fs *OsFs) CheckRootPath(username string, uid int, gid int) bool {
 
- 	var err error
 
- 	if _, err = fs.Stat(fs.rootDir); fs.IsNotExist(err) {
 
- 		err = os.MkdirAll(fs.rootDir, os.ModePerm)
 
- 		if err == nil {
 
- 			SetPathPermissions(fs, fs.rootDir, uid, gid)
 
- 		} else {
 
- 			fsLog(fs, logger.LevelError, "error creating root directory %q for user %q: %v", fs.rootDir, username, err)
 
- 		}
 
- 	}
 
- 	return err == nil
 
- }
 
- // ScanRootDirContents returns the number of files contained in the root
 
- // directory and their size
 
- func (fs *OsFs) ScanRootDirContents() (int, int64, error) {
 
- 	return fs.GetDirSize(fs.rootDir)
 
- }
 
- // CheckMetadata checks the metadata consistency
 
- func (*OsFs) CheckMetadata() error {
 
- 	return nil
 
- }
 
- // GetAtomicUploadPath returns the path to use for an atomic upload
 
- func (*OsFs) GetAtomicUploadPath(name string) string {
 
- 	dir := filepath.Dir(name)
 
- 	if tempPath != "" {
 
- 		dir = tempPath
 
- 	}
 
- 	guid := xid.New().String()
 
- 	return filepath.Join(dir, ".sftpgo-upload."+guid+"."+filepath.Base(name))
 
- }
 
- // GetRelativePath returns the path for a file relative to the user's home dir.
 
- // This is the path as seen by SFTPGo users
 
- func (fs *OsFs) GetRelativePath(name string) string {
 
- 	virtualPath := "/"
 
- 	if fs.mountPath != "" {
 
- 		virtualPath = fs.mountPath
 
- 	}
 
- 	rel, err := filepath.Rel(fs.rootDir, filepath.Clean(name))
 
- 	if err != nil {
 
- 		return ""
 
- 	}
 
- 	if rel == "." || strings.HasPrefix(rel, "..") {
 
- 		rel = ""
 
- 	}
 
- 	return path.Join(virtualPath, filepath.ToSlash(rel))
 
- }
 
- // Walk walks the file tree rooted at root, calling walkFn for each file or
 
- // directory in the tree, including root
 
- func (*OsFs) Walk(root string, walkFn filepath.WalkFunc) error {
 
- 	return filepath.Walk(root, walkFn)
 
- }
 
- // Join joins any number of path elements into a single path
 
- func (*OsFs) Join(elem ...string) string {
 
- 	return filepath.Join(elem...)
 
- }
 
- // ResolvePath returns the matching filesystem path for the specified sftp path
 
- func (fs *OsFs) ResolvePath(virtualPath string) (string, error) {
 
- 	if !filepath.IsAbs(fs.rootDir) {
 
- 		return "", fmt.Errorf("invalid root path %q", fs.rootDir)
 
- 	}
 
- 	if fs.mountPath != "" {
 
- 		virtualPath = strings.TrimPrefix(virtualPath, fs.mountPath)
 
- 	}
 
- 	r := filepath.Clean(filepath.Join(fs.rootDir, virtualPath))
 
- 	p, err := filepath.EvalSymlinks(r)
 
- 	if isInvalidNameError(err) {
 
- 		err = os.ErrNotExist
 
- 	}
 
- 	isNotExist := fs.IsNotExist(err)
 
- 	if err != nil && !isNotExist {
 
- 		return "", err
 
- 	} else if isNotExist {
 
- 		// The requested path doesn't exist, so at this point we need to iterate up the
 
- 		// path chain until we hit a directory that _does_ exist and can be validated.
 
- 		_, err = fs.findFirstExistingDir(r)
 
- 		if err != nil {
 
- 			fsLog(fs, logger.LevelError, "error resolving non-existent path %#v", err)
 
- 		}
 
- 		return r, err
 
- 	}
 
- 	err = fs.isSubDir(p)
 
- 	if err != nil {
 
- 		fsLog(fs, logger.LevelError, "Invalid path resolution, path %q original path %q resolved %q err: %v",
 
- 			p, virtualPath, r, err)
 
- 	}
 
- 	return r, err
 
- }
 
- // RealPath implements the FsRealPather interface
 
- func (fs *OsFs) RealPath(p string) (string, error) {
 
- 	linksWalked := 0
 
- 	for {
 
- 		info, err := os.Lstat(p)
 
- 		if err != nil {
 
- 			if errors.Is(err, os.ErrNotExist) {
 
- 				return fs.GetRelativePath(p), nil
 
- 			}
 
- 			return "", err
 
- 		}
 
- 		if info.Mode()&os.ModeSymlink == 0 {
 
- 			return fs.GetRelativePath(p), nil
 
- 		}
 
- 		resolvedLink, err := os.Readlink(p)
 
- 		if err != nil {
 
- 			return "", err
 
- 		}
 
- 		resolvedLink = filepath.Clean(resolvedLink)
 
- 		if filepath.IsAbs(resolvedLink) {
 
- 			p = resolvedLink
 
- 		} else {
 
- 			p = filepath.Join(filepath.Dir(p), resolvedLink)
 
- 		}
 
- 		linksWalked++
 
- 		if linksWalked > 10 {
 
- 			fsLog(fs, logger.LevelError, "unable to get real path, too many links: %d", linksWalked)
 
- 			return "", &pathResolutionError{err: "too many links"}
 
- 		}
 
- 	}
 
- }
 
- // GetDirSize returns the number of files and the size for a folder
 
- // including any subfolders
 
- func (fs *OsFs) GetDirSize(dirname string) (int, int64, error) {
 
- 	numFiles := 0
 
- 	size := int64(0)
 
- 	isDir, err := isDirectory(fs, dirname)
 
- 	if err == nil && isDir {
 
- 		err = filepath.Walk(dirname, func(_ string, info os.FileInfo, err error) error {
 
- 			if err != nil {
 
- 				return err
 
- 			}
 
- 			if info != nil && info.Mode().IsRegular() {
 
- 				size += info.Size()
 
- 				numFiles++
 
- 				if numFiles%1000 == 0 {
 
- 					fsLog(fs, logger.LevelDebug, "dirname %q scan in progress, files: %d, size: %d", dirname, numFiles, size)
 
- 				}
 
- 			}
 
- 			return err
 
- 		})
 
- 	}
 
- 	return numFiles, size, err
 
- }
 
- // HasVirtualFolders returns true if folders are emulated
 
- func (*OsFs) HasVirtualFolders() bool {
 
- 	return false
 
- }
 
- func (fs *OsFs) findNonexistentDirs(filePath string) ([]string, error) {
 
- 	results := []string{}
 
- 	cleanPath := filepath.Clean(filePath)
 
- 	parent := filepath.Dir(cleanPath)
 
- 	_, err := os.Stat(parent)
 
- 	for fs.IsNotExist(err) {
 
- 		results = append(results, parent)
 
- 		parent = filepath.Dir(parent)
 
- 		_, err = os.Stat(parent)
 
- 	}
 
- 	if err != nil {
 
- 		return results, err
 
- 	}
 
- 	p, err := filepath.EvalSymlinks(parent)
 
- 	if err != nil {
 
- 		return results, err
 
- 	}
 
- 	err = fs.isSubDir(p)
 
- 	if err != nil {
 
- 		fsLog(fs, logger.LevelError, "error finding non existing dir: %v", err)
 
- 	}
 
- 	return results, err
 
- }
 
- func (fs *OsFs) findFirstExistingDir(path string) (string, error) {
 
- 	results, err := fs.findNonexistentDirs(path)
 
- 	if err != nil {
 
- 		fsLog(fs, logger.LevelError, "unable to find non existent dirs: %v", err)
 
- 		return "", err
 
- 	}
 
- 	var parent string
 
- 	if len(results) > 0 {
 
- 		lastMissingDir := results[len(results)-1]
 
- 		parent = filepath.Dir(lastMissingDir)
 
- 	} else {
 
- 		parent = fs.rootDir
 
- 	}
 
- 	p, err := filepath.EvalSymlinks(parent)
 
- 	if err != nil {
 
- 		return "", err
 
- 	}
 
- 	fileInfo, err := os.Stat(p)
 
- 	if err != nil {
 
- 		return "", err
 
- 	}
 
- 	if !fileInfo.IsDir() {
 
- 		return "", fmt.Errorf("resolved path is not a dir: %#v", p)
 
- 	}
 
- 	err = fs.isSubDir(p)
 
- 	return p, err
 
- }
 
- func (fs *OsFs) isSubDir(sub string) error {
 
- 	// fs.rootDir must exist and it is already a validated absolute path
 
- 	parent, err := filepath.EvalSymlinks(fs.rootDir)
 
- 	if err != nil {
 
- 		fsLog(fs, logger.LevelError, "invalid root path %q: %v", fs.rootDir, err)
 
- 		return err
 
- 	}
 
- 	if parent == sub {
 
- 		return nil
 
- 	}
 
- 	if len(sub) < len(parent) {
 
- 		err = fmt.Errorf("path %q is not inside %q", sub, parent)
 
- 		return &pathResolutionError{err: err.Error()}
 
- 	}
 
- 	separator := string(os.PathSeparator)
 
- 	if parent == filepath.Dir(parent) {
 
- 		// parent is the root dir, on Windows we can have C:\, D:\ and so on here
 
- 		// so we still need the prefix check
 
- 		separator = ""
 
- 	}
 
- 	if !strings.HasPrefix(sub, parent+separator) {
 
- 		err = fmt.Errorf("path %q is not inside %q", sub, parent)
 
- 		return &pathResolutionError{err: err.Error()}
 
- 	}
 
- 	return nil
 
- }
 
- // GetMimeType returns the content type
 
- func (fs *OsFs) GetMimeType(name string) (string, error) {
 
- 	f, err := os.OpenFile(name, os.O_RDONLY, 0)
 
- 	if err != nil {
 
- 		return "", err
 
- 	}
 
- 	defer f.Close()
 
- 	var buf [512]byte
 
- 	n, err := io.ReadFull(f, buf[:])
 
- 	if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
 
- 		return "", err
 
- 	}
 
- 	ctype := http.DetectContentType(buf[:n])
 
- 	// Rewind file.
 
- 	_, err = f.Seek(0, io.SeekStart)
 
- 	return ctype, err
 
- }
 
- // Close closes the fs
 
- func (*OsFs) Close() error {
 
- 	return nil
 
- }
 
- // GetAvailableDiskSize returns the available size for the specified path
 
- func (*OsFs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {
 
- 	return getStatFS(dirName)
 
- }
 
 
  |