2022-07-17 18:16:00 +00:00
|
|
|
// 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/>.
|
|
|
|
|
2020-01-19 06:41:05 +00:00
|
|
|
package vfs
|
|
|
|
|
|
|
|
import (
|
2022-05-09 17:09:43 +00:00
|
|
|
"errors"
|
2020-01-19 06:41:05 +00:00
|
|
|
"fmt"
|
2020-11-04 18:11:40 +00:00
|
|
|
"io"
|
2022-05-09 17:09:43 +00:00
|
|
|
"io/fs"
|
2020-11-04 18:11:40 +00:00
|
|
|
"net/http"
|
2020-01-19 06:41:05 +00:00
|
|
|
"os"
|
2020-02-23 10:30:26 +00:00
|
|
|
"path"
|
2020-01-19 06:41:05 +00:00
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/eikenb/pipeat"
|
2021-05-27 18:14:12 +00:00
|
|
|
fscopy "github.com/otiai10/copy"
|
2021-02-11 18:45:52 +00:00
|
|
|
"github.com/pkg/sftp"
|
2020-01-19 06:41:05 +00:00
|
|
|
"github.com/rs/xid"
|
2020-05-06 17:36:34 +00:00
|
|
|
|
2022-07-24 14:18:54 +00:00
|
|
|
"github.com/drakkan/sftpgo/v2/internal/logger"
|
2020-01-19 06:41:05 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// osFsName is the name for the local Fs implementation
|
|
|
|
osFsName = "osfs"
|
|
|
|
)
|
|
|
|
|
2021-05-21 11:04:22 +00:00
|
|
|
type pathResolutionError struct {
|
|
|
|
err string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *pathResolutionError) Error() string {
|
|
|
|
return fmt.Sprintf("Path resolution error: %s", e.err)
|
|
|
|
}
|
|
|
|
|
2020-01-19 06:41:05 +00:00
|
|
|
// OsFs is a Fs implementation that uses functions provided by the os package.
|
|
|
|
type OsFs struct {
|
2021-03-21 18:15:47 +00:00
|
|
|
name string
|
|
|
|
connectionID string
|
|
|
|
rootDir string
|
|
|
|
// if not empty this fs is mouted as virtual folder in the specified path
|
|
|
|
mountPath string
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewOsFs returns an OsFs object that allows to interact with local Os filesystem
|
2021-03-21 18:15:47 +00:00
|
|
|
func NewOsFs(connectionID, rootDir, mountPath string) Fs {
|
2020-11-02 18:16:12 +00:00
|
|
|
return &OsFs{
|
2021-03-21 18:15:47 +00:00
|
|
|
name: osFsName,
|
|
|
|
connectionID: connectionID,
|
|
|
|
rootDir: rootDir,
|
2022-04-02 16:32:46 +00:00
|
|
|
mountPath: getMountPath(mountPath),
|
2020-01-19 12:58:55 +00:00
|
|
|
}
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Name returns the name for the Fs implementation
|
2020-11-02 18:16:12 +00:00
|
|
|
func (fs *OsFs) Name() string {
|
2020-01-19 06:41:05 +00:00
|
|
|
return fs.name
|
|
|
|
}
|
|
|
|
|
|
|
|
// ConnectionID returns the SSH connection ID associated to this Fs implementation
|
2020-11-02 18:16:12 +00:00
|
|
|
func (fs *OsFs) ConnectionID() string {
|
2020-01-19 06:41:05 +00:00
|
|
|
return fs.connectionID
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stat returns a FileInfo describing the named file
|
2020-11-02 18:16:12 +00:00
|
|
|
func (fs *OsFs) Stat(name string) (os.FileInfo, error) {
|
2021-03-21 18:15:47 +00:00
|
|
|
return os.Stat(name)
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Lstat returns a FileInfo describing the named file
|
2020-11-02 18:16:12 +00:00
|
|
|
func (fs *OsFs) Lstat(name string) (os.FileInfo, error) {
|
2021-03-21 18:15:47 +00:00
|
|
|
return os.Lstat(name)
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Open opens the named file for reading
|
2020-11-17 18:36:39 +00:00
|
|
|
func (*OsFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, func(), error) {
|
2020-01-19 06:41:05 +00:00
|
|
|
f, err := os.Open(name)
|
2021-05-06 19:35:43 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2020-01-19 06:41:05 +00:00
|
|
|
return f, nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create creates or opens the named file for writing
|
2020-11-17 18:36:39 +00:00
|
|
|
func (*OsFs) Create(name string, flag int) (File, *PipeWriter, func(), error) {
|
2020-01-19 06:41:05 +00:00
|
|
|
var err error
|
|
|
|
var f *os.File
|
|
|
|
if flag == 0 {
|
|
|
|
f, err = os.Create(name)
|
|
|
|
} else {
|
2022-02-16 15:05:56 +00:00
|
|
|
f, err = os.OpenFile(name, flag, 0666)
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
return f, nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rename renames (moves) source to target
|
2021-05-27 18:14:12 +00:00
|
|
|
func (fs *OsFs) Rename(source, target string) error {
|
|
|
|
err := os.Rename(source, target)
|
|
|
|
if err != nil && isCrossDeviceError(err) {
|
2021-12-16 18:53:00 +00:00
|
|
|
fsLog(fs, logger.LevelError, "cross device error detected while renaming %#v -> %#v. Trying a copy and remove, this could take a long time",
|
2021-05-27 18:14:12 +00:00
|
|
|
source, target)
|
2021-05-30 21:07:46 +00:00
|
|
|
err = fscopy.Copy(source, target, fscopy.Options{
|
|
|
|
OnSymlink: func(src string) fscopy.SymlinkAction {
|
|
|
|
return fscopy.Skip
|
|
|
|
},
|
|
|
|
})
|
2021-05-27 20:23:14 +00:00
|
|
|
if err != nil {
|
2021-12-16 18:53:00 +00:00
|
|
|
fsLog(fs, logger.LevelError, "cross device copy error: %v", err)
|
2021-05-27 20:23:14 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
return os.RemoveAll(source)
|
2021-05-27 18:14:12 +00:00
|
|
|
}
|
|
|
|
return err
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Remove removes the named file or (empty) directory.
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) Remove(name string, isDir bool) error {
|
2020-01-19 06:41:05 +00:00
|
|
|
return os.Remove(name)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mkdir creates a new directory with the specified name and default permissions
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) Mkdir(name string) error {
|
2020-06-08 17:40:17 +00:00
|
|
|
return os.Mkdir(name, os.ModePerm)
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Symlink creates source as a symbolic link to target.
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) Symlink(source, target string) error {
|
2020-01-19 06:41:05 +00:00
|
|
|
return os.Symlink(source, target)
|
|
|
|
}
|
|
|
|
|
2020-08-22 12:52:17 +00:00
|
|
|
// Readlink returns the destination of the named symbolic link
|
|
|
|
// as absolute virtual path
|
2020-11-02 18:16:12 +00:00
|
|
|
func (fs *OsFs) Readlink(name string) (string, error) {
|
2022-04-27 16:38:46 +00:00
|
|
|
// 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)
|
2020-08-22 12:52:17 +00:00
|
|
|
if err != nil {
|
2022-04-27 16:38:46 +00:00
|
|
|
return "", err
|
2020-08-22 12:52:17 +00:00
|
|
|
}
|
2022-04-27 16:38:46 +00:00
|
|
|
resolved = filepath.Clean(resolved)
|
|
|
|
if !filepath.IsAbs(resolved) {
|
|
|
|
resolved = filepath.Join(filepath.Dir(name), resolved)
|
|
|
|
}
|
|
|
|
return fs.GetRelativePath(resolved), nil
|
2020-08-22 12:52:17 +00:00
|
|
|
}
|
|
|
|
|
2020-01-19 06:41:05 +00:00
|
|
|
// Chown changes the numeric uid and gid of the named file.
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) Chown(name string, uid int, gid int) error {
|
2020-01-19 06:41:05 +00:00
|
|
|
return os.Chown(name, uid, gid)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Chmod changes the mode of the named file to mode
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) Chmod(name string, mode os.FileMode) error {
|
2020-01-19 06:41:05 +00:00
|
|
|
return os.Chmod(name, mode)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Chtimes changes the access and modification times of the named file
|
2021-12-16 17:18:36 +00:00
|
|
|
func (*OsFs) Chtimes(name string, atime, mtime time.Time, isUploading bool) error {
|
2020-01-19 06:41:05 +00:00
|
|
|
return os.Chtimes(name, atime, mtime)
|
|
|
|
}
|
|
|
|
|
2020-08-20 11:54:36 +00:00
|
|
|
// Truncate changes the size of the named file
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) Truncate(name string, size int64) error {
|
2020-08-20 11:54:36 +00:00
|
|
|
return os.Truncate(name, size)
|
|
|
|
}
|
|
|
|
|
2020-01-19 06:41:05 +00:00
|
|
|
// ReadDir reads the directory named by dirname and returns
|
|
|
|
// a list of directory entries.
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) ReadDir(dirname string) ([]os.FileInfo, error) {
|
2020-02-23 10:30:26 +00:00
|
|
|
f, err := os.Open(dirname)
|
|
|
|
if err != nil {
|
2022-05-05 09:27:19 +00:00
|
|
|
if isInvalidNameError(err) {
|
|
|
|
err = os.ErrNotExist
|
|
|
|
}
|
2020-02-23 10:30:26 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2022-04-14 17:07:41 +00:00
|
|
|
list, err := f.Readdir(-1)
|
2020-02-23 10:30:26 +00:00
|
|
|
f.Close()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-04-14 17:07:41 +00:00
|
|
|
return list, nil
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
|
2021-04-03 14:00:55 +00:00
|
|
|
// IsUploadResumeSupported returns true if resuming uploads is supported
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) IsUploadResumeSupported() bool {
|
2020-01-19 06:41:05 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsAtomicUploadSupported returns true if atomic upload is supported
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) IsAtomicUploadSupported() bool {
|
2020-01-19 06:41:05 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsNotExist returns a boolean indicating whether the error is known to
|
|
|
|
// report that a file or directory does not exist
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) IsNotExist(err error) bool {
|
2022-05-09 17:09:43 +00:00
|
|
|
return errors.Is(err, fs.ErrNotExist)
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// IsPermission returns a boolean indicating whether the error is known to
|
|
|
|
// report that permission is denied.
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) IsPermission(err error) bool {
|
2021-05-21 11:04:22 +00:00
|
|
|
if _, ok := err.(*pathResolutionError); ok {
|
|
|
|
return true
|
|
|
|
}
|
2022-05-09 17:09:43 +00:00
|
|
|
return errors.Is(err, fs.ErrPermission)
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
|
2020-11-12 09:39:46 +00:00
|
|
|
// IsNotSupported returns true if the error indicate an unsupported operation
|
|
|
|
func (*OsFs) IsNotSupported(err error) bool {
|
|
|
|
if err == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return err == ErrVfsUnsupported
|
|
|
|
}
|
|
|
|
|
2020-01-19 12:58:55 +00:00
|
|
|
// CheckRootPath creates the root directory if it does not exists
|
2020-11-02 18:16:12 +00:00
|
|
|
func (fs *OsFs) CheckRootPath(username string, uid int, gid int) bool {
|
2020-01-19 06:41:05 +00:00
|
|
|
var err error
|
2020-01-19 12:58:55 +00:00
|
|
|
if _, err = fs.Stat(fs.rootDir); fs.IsNotExist(err) {
|
2020-06-08 17:40:17 +00:00
|
|
|
err = os.MkdirAll(fs.rootDir, os.ModePerm)
|
2020-01-19 06:41:05 +00:00
|
|
|
if err == nil {
|
2020-01-19 12:58:55 +00:00
|
|
|
SetPathPermissions(fs, fs.rootDir, uid, gid)
|
2022-07-12 06:32:31 +00:00
|
|
|
} else {
|
|
|
|
fsLog(fs, logger.LevelError, "error creating root directory %q for user %q: %v", fs.rootDir, username, err)
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
}
|
2021-03-21 18:15:47 +00:00
|
|
|
return err == nil
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
|
2020-12-05 12:48:13 +00:00
|
|
|
// ScanRootDirContents returns the number of files contained in the root
|
|
|
|
// directory and their size
|
2020-11-02 18:16:12 +00:00
|
|
|
func (fs *OsFs) ScanRootDirContents() (int, int64, error) {
|
2021-03-21 18:15:47 +00:00
|
|
|
return fs.GetDirSize(fs.rootDir)
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
|
2021-12-16 17:18:36 +00:00
|
|
|
// CheckMetadata checks the metadata consistency
|
|
|
|
func (*OsFs) CheckMetadata() error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-01-19 06:41:05 +00:00
|
|
|
// GetAtomicUploadPath returns the path to use for an atomic upload
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) GetAtomicUploadPath(name string) string {
|
2020-01-19 06:41:05 +00:00
|
|
|
dir := filepath.Dir(name)
|
2021-05-27 13:38:27 +00:00
|
|
|
if tempPath != "" {
|
|
|
|
dir = tempPath
|
|
|
|
}
|
2020-01-19 06:41:05 +00:00
|
|
|
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.
|
2021-03-21 18:15:47 +00:00
|
|
|
// This is the path as seen by SFTPGo users
|
2020-11-02 18:16:12 +00:00
|
|
|
func (fs *OsFs) GetRelativePath(name string) string {
|
2020-02-23 10:30:26 +00:00
|
|
|
virtualPath := "/"
|
2021-03-21 18:15:47 +00:00
|
|
|
if fs.mountPath != "" {
|
|
|
|
virtualPath = fs.mountPath
|
2020-02-23 10:30:26 +00:00
|
|
|
}
|
2021-03-21 18:15:47 +00:00
|
|
|
rel, err := filepath.Rel(fs.rootDir, filepath.Clean(name))
|
2020-01-19 06:41:05 +00:00
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
if rel == "." || strings.HasPrefix(rel, "..") {
|
|
|
|
rel = ""
|
|
|
|
}
|
2020-02-23 10:30:26 +00:00
|
|
|
return path.Join(virtualPath, filepath.ToSlash(rel))
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
|
2020-06-26 21:38:29 +00:00
|
|
|
// Walk walks the file tree rooted at root, calling walkFn for each file or
|
|
|
|
// directory in the tree, including root
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) Walk(root string, walkFn filepath.WalkFunc) error {
|
2020-06-26 21:38:29 +00:00
|
|
|
return filepath.Walk(root, walkFn)
|
|
|
|
}
|
|
|
|
|
2020-01-19 06:41:05 +00:00
|
|
|
// Join joins any number of path elements into a single path
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) Join(elem ...string) string {
|
2020-01-19 06:41:05 +00:00
|
|
|
return filepath.Join(elem...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ResolvePath returns the matching filesystem path for the specified sftp path
|
2021-03-21 18:15:47 +00:00
|
|
|
func (fs *OsFs) ResolvePath(virtualPath string) (string, error) {
|
2020-01-19 12:58:55 +00:00
|
|
|
if !filepath.IsAbs(fs.rootDir) {
|
2022-06-11 08:41:34 +00:00
|
|
|
return "", fmt.Errorf("invalid root path %q", fs.rootDir)
|
2021-03-21 18:15:47 +00:00
|
|
|
}
|
|
|
|
if fs.mountPath != "" {
|
|
|
|
virtualPath = strings.TrimPrefix(virtualPath, fs.mountPath)
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
2021-03-21 18:15:47 +00:00
|
|
|
r := filepath.Clean(filepath.Join(fs.rootDir, virtualPath))
|
2020-01-19 06:41:05 +00:00
|
|
|
p, err := filepath.EvalSymlinks(r)
|
2022-05-05 09:27:19 +00:00
|
|
|
if isInvalidNameError(err) {
|
|
|
|
err = os.ErrNotExist
|
|
|
|
}
|
2022-05-09 17:09:43 +00:00
|
|
|
isNotExist := fs.IsNotExist(err)
|
|
|
|
if err != nil && !isNotExist {
|
2020-01-19 06:41:05 +00:00
|
|
|
return "", err
|
2022-05-09 17:09:43 +00:00
|
|
|
} else if isNotExist {
|
2020-01-19 06:41:05 +00:00
|
|
|
// 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.
|
2021-03-21 18:15:47 +00:00
|
|
|
_, err = fs.findFirstExistingDir(r)
|
2020-01-19 06:41:05 +00:00
|
|
|
if err != nil {
|
2021-12-16 18:53:00 +00:00
|
|
|
fsLog(fs, logger.LevelError, "error resolving non-existent path %#v", err)
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
return r, err
|
|
|
|
}
|
|
|
|
|
2021-03-21 18:15:47 +00:00
|
|
|
err = fs.isSubDir(p)
|
2020-01-19 06:41:05 +00:00
|
|
|
if err != nil {
|
2022-07-17 14:02:45 +00:00
|
|
|
fsLog(fs, logger.LevelError, "Invalid path resolution, path %q original path %q resolved %q err: %v",
|
2021-03-21 18:15:47 +00:00
|
|
|
p, virtualPath, r, err)
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
return r, err
|
|
|
|
}
|
|
|
|
|
2022-07-17 14:02:45 +00:00
|
|
|
// 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"}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-07 21:30:18 +00:00
|
|
|
// GetDirSize returns the number of files and the size for a folder
|
|
|
|
// including any subfolders
|
2020-11-02 18:16:12 +00:00
|
|
|
func (fs *OsFs) GetDirSize(dirname string) (int, int64, error) {
|
2020-06-07 21:30:18 +00:00
|
|
|
numFiles := 0
|
|
|
|
size := int64(0)
|
|
|
|
isDir, err := IsDirectory(fs, dirname)
|
|
|
|
if err == nil && isDir {
|
2022-07-19 21:28:33 +00:00
|
|
|
err = filepath.Walk(dirname, func(_ string, info os.FileInfo, err error) error {
|
2020-06-07 21:30:18 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if info != nil && info.Mode().IsRegular() {
|
|
|
|
size += info.Size()
|
|
|
|
numFiles++
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return numFiles, size, err
|
|
|
|
}
|
|
|
|
|
2020-07-31 17:24:57 +00:00
|
|
|
// HasVirtualFolders returns true if folders are emulated
|
2020-11-02 18:16:12 +00:00
|
|
|
func (*OsFs) HasVirtualFolders() bool {
|
2020-07-31 17:24:57 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-03-21 18:15:47 +00:00
|
|
|
func (fs *OsFs) findNonexistentDirs(filePath string) ([]string, error) {
|
2020-01-19 06:41:05 +00:00
|
|
|
results := []string{}
|
2021-03-21 18:15:47 +00:00
|
|
|
cleanPath := filepath.Clean(filePath)
|
2020-01-19 06:41:05 +00:00
|
|
|
parent := filepath.Dir(cleanPath)
|
|
|
|
_, err := os.Stat(parent)
|
|
|
|
|
2022-05-09 17:09:43 +00:00
|
|
|
for fs.IsNotExist(err) {
|
2020-01-19 06:41:05 +00:00
|
|
|
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
|
|
|
|
}
|
2021-03-21 18:15:47 +00:00
|
|
|
err = fs.isSubDir(p)
|
2020-01-19 06:41:05 +00:00
|
|
|
if err != nil {
|
2021-12-16 18:53:00 +00:00
|
|
|
fsLog(fs, logger.LevelError, "error finding non existing dir: %v", err)
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
return results, err
|
|
|
|
}
|
|
|
|
|
2021-03-21 18:15:47 +00:00
|
|
|
func (fs *OsFs) findFirstExistingDir(path string) (string, error) {
|
|
|
|
results, err := fs.findNonexistentDirs(path)
|
2020-01-19 06:41:05 +00:00
|
|
|
if err != nil {
|
2021-12-16 18:53:00 +00:00
|
|
|
fsLog(fs, logger.LevelError, "unable to find non existent dirs: %v", err)
|
2020-01-19 06:41:05 +00:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
var parent string
|
|
|
|
if len(results) > 0 {
|
|
|
|
lastMissingDir := results[len(results)-1]
|
|
|
|
parent = filepath.Dir(lastMissingDir)
|
|
|
|
} else {
|
2021-03-21 18:15:47 +00:00
|
|
|
parent = fs.rootDir
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
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)
|
|
|
|
}
|
2021-03-21 18:15:47 +00:00
|
|
|
err = fs.isSubDir(p)
|
2020-01-19 06:41:05 +00:00
|
|
|
return p, err
|
|
|
|
}
|
|
|
|
|
2021-03-21 18:15:47 +00:00
|
|
|
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)
|
2020-01-19 06:41:05 +00:00
|
|
|
if err != nil {
|
2022-07-17 14:02:45 +00:00
|
|
|
fsLog(fs, logger.LevelError, "invalid root path %q: %v", fs.rootDir, err)
|
2020-01-19 06:41:05 +00:00
|
|
|
return err
|
|
|
|
}
|
2020-09-21 17:32:33 +00:00
|
|
|
if parent == sub {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if len(sub) < len(parent) {
|
2022-07-17 14:02:45 +00:00
|
|
|
err = fmt.Errorf("path %q is not inside %q", sub, parent)
|
2021-05-21 11:04:22 +00:00
|
|
|
return &pathResolutionError{err: err.Error()}
|
2020-09-21 17:32:33 +00:00
|
|
|
}
|
2021-04-25 12:36:29 +00:00
|
|
|
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) {
|
2022-07-17 14:02:45 +00:00
|
|
|
err = fmt.Errorf("path %q is not inside %q", sub, parent)
|
2021-05-21 11:04:22 +00:00
|
|
|
return &pathResolutionError{err: err.Error()}
|
2020-01-19 06:41:05 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2020-02-23 10:30:26 +00:00
|
|
|
|
2020-11-04 18:11:40 +00:00
|
|
|
// 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
|
|
|
|
}
|
2020-12-12 09:31:09 +00:00
|
|
|
|
|
|
|
// Close closes the fs
|
|
|
|
func (*OsFs) Close() error {
|
|
|
|
return nil
|
|
|
|
}
|
2020-12-25 10:14:08 +00:00
|
|
|
|
2022-05-15 05:30:36 +00:00
|
|
|
// GetAvailableDiskSize returns the available size for the specified path
|
2021-02-11 18:45:52 +00:00
|
|
|
func (*OsFs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {
|
|
|
|
return getStatFS(dirName)
|
2020-12-25 10:14:08 +00:00
|
|
|
}
|