add setstat_mode 2
in this mode chmod/chtimes/chown can be silently ignored only for cloud based file systems Fixes #223
This commit is contained in:
parent
38e0cba675
commit
5720d40fee
13 changed files with 117 additions and 56 deletions
|
@ -223,6 +223,8 @@ type Configuration struct {
|
|||
Actions ProtocolActions `json:"actions" mapstructure:"actions"`
|
||||
// SetstatMode 0 means "normal mode": requests for changing permissions and owner/group are executed.
|
||||
// 1 means "ignore mode": requests for changing permissions and owner/group are silently ignored.
|
||||
// 2 means "ignore mode for cloud fs": requests for changing permissions and owner/group/time are
|
||||
// silently ignored for cloud based filesystem such as S3, GCS, Azure Blob
|
||||
SetstatMode int `json:"setstat_mode" mapstructure:"setstat_mode"`
|
||||
// Support for HAProxy PROXY protocol.
|
||||
// If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable
|
||||
|
|
|
@ -448,17 +448,28 @@ func (c *BaseConnection) DoStat(fsPath string, mode int) (os.FileInfo, error) {
|
|||
return c.Fs.Stat(c.getRealFsPath(fsPath))
|
||||
}
|
||||
|
||||
// SetStat set StatAttributes for the specified fsPath
|
||||
func (c *BaseConnection) SetStat(fsPath, virtualPath string, attributes *StatAttributes) error {
|
||||
func (c *BaseConnection) ignoreSetStat() bool {
|
||||
if Config.SetstatMode == 1 {
|
||||
return nil
|
||||
return true
|
||||
}
|
||||
if Config.SetstatMode == 2 && !vfs.IsLocalOsFs(c.Fs) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SetStat set StatAttributes for the specified fsPath
|
||||
// nolint:gocyclo
|
||||
func (c *BaseConnection) SetStat(fsPath, virtualPath string, attributes *StatAttributes) error {
|
||||
pathForPerms := c.getPathForSetStatPerms(fsPath, virtualPath)
|
||||
|
||||
if attributes.Flags&StatAttrPerms != 0 {
|
||||
if !c.User.HasPerm(dataprovider.PermChmod, pathForPerms) {
|
||||
return c.GetPermissionDeniedError()
|
||||
}
|
||||
if c.ignoreSetStat() {
|
||||
return nil
|
||||
}
|
||||
if err := c.Fs.Chmod(c.getRealFsPath(fsPath), attributes.Mode); err != nil {
|
||||
c.Log(logger.LevelWarn, "failed to chmod path %#v, mode: %v, err: %+v", fsPath, attributes.Mode.String(), err)
|
||||
return c.GetFsError(err)
|
||||
|
@ -471,6 +482,9 @@ func (c *BaseConnection) SetStat(fsPath, virtualPath string, attributes *StatAtt
|
|||
if !c.User.HasPerm(dataprovider.PermChown, pathForPerms) {
|
||||
return c.GetPermissionDeniedError()
|
||||
}
|
||||
if c.ignoreSetStat() {
|
||||
return nil
|
||||
}
|
||||
if err := c.Fs.Chown(c.getRealFsPath(fsPath), attributes.UID, attributes.GID); err != nil {
|
||||
c.Log(logger.LevelWarn, "failed to chown path %#v, uid: %v, gid: %v, err: %+v", fsPath, attributes.UID,
|
||||
attributes.GID, err)
|
||||
|
@ -484,7 +498,9 @@ func (c *BaseConnection) SetStat(fsPath, virtualPath string, attributes *StatAtt
|
|||
if !c.User.HasPerm(dataprovider.PermChtimes, pathForPerms) {
|
||||
return c.GetPermissionDeniedError()
|
||||
}
|
||||
|
||||
if c.ignoreSetStat() {
|
||||
return nil
|
||||
}
|
||||
if err := c.Fs.Chtimes(c.getRealFsPath(fsPath), attributes.Atime, attributes.Mtime); err != nil {
|
||||
c.Log(logger.LevelWarn, "failed to chtimes for path %#v, access time: %v, modification time: %v, err: %+v",
|
||||
fsPath, attributes.Atime, attributes.Mtime, err)
|
||||
|
@ -950,6 +966,8 @@ func (c *BaseConnection) GetFsError(err error) error {
|
|||
return c.GetNotExistError()
|
||||
} else if c.Fs.IsPermission(err) {
|
||||
return c.GetPermissionDeniedError()
|
||||
} else if c.Fs.IsNotSupported(err) {
|
||||
return c.GetOpUnsupportedError()
|
||||
} else if err != nil {
|
||||
return c.GetGenericError(err)
|
||||
}
|
||||
|
|
|
@ -496,6 +496,29 @@ func TestSetStat(t *testing.T) {
|
|||
err = c.SetStat(user.GetHomeDir(), "/", &StatAttributes{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = c.SetStat(dir2, "/dir1/file", &StatAttributes{
|
||||
Mode: os.ModePerm,
|
||||
Flags: StatAttrPerms,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
err = c.SetStat(dir1, "/dir2/file", &StatAttributes{
|
||||
UID: os.Getuid(),
|
||||
GID: os.Getgid(),
|
||||
Flags: StatAttrUIDGID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
err = c.SetStat(dir1, "/dir3/file", &StatAttributes{
|
||||
Atime: time.Now(),
|
||||
Mtime: time.Now(),
|
||||
Flags: StatAttrTimes,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
Config.SetstatMode = 2
|
||||
assert.False(t, c.ignoreSetStat())
|
||||
c1 := NewBaseConnection("", ProtocolSFTP, user, newMockOsFs(false, fs.ConnectionID(), user.GetHomeDir()))
|
||||
assert.True(t, c1.ignoreSetStat())
|
||||
|
||||
Config.SetstatMode = oldSetStatMode
|
||||
// chmod
|
||||
err = c.SetStat(dir1, "/dir1/file", &StatAttributes{
|
||||
|
@ -1146,6 +1169,12 @@ func TestErrorsMapping(t *testing.T) {
|
|||
} else {
|
||||
assert.EqualError(t, err, ErrPermissionDenied.Error())
|
||||
}
|
||||
err = conn.GetFsError(vfs.ErrVfsUnsupported)
|
||||
if protocol == ProtocolSFTP {
|
||||
assert.EqualError(t, err, sftp.ErrSSHFxOpUnsupported.Error())
|
||||
} else {
|
||||
assert.EqualError(t, err, ErrOpUnsupported.Error())
|
||||
}
|
||||
err = conn.GetFsError(nil)
|
||||
assert.NoError(t, err)
|
||||
err = conn.GetOpUnsupportedError()
|
||||
|
|
|
@ -10,7 +10,7 @@ if [ "$1" = 'sftpgo' ]; then
|
|||
DIR_UID=$(stat -c %u ${DIR})
|
||||
DIR_GID=$(stat -c %g ${DIR})
|
||||
if [ ${DIR_UID} != ${SFTPGO_PUID} ] || [ ${DIR_GID} != ${SFTPGO_PGID} ]; then
|
||||
echo `date +%Y-%m-%dT%H:%M:%S` - "entrypoint, change owner for ${DIR} uid: ${SFTPGO_PUID} gid: ${SFTPGO_PGID}"
|
||||
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.000`'","sender":"entrypoint","message":"change owner for \"'${DIR}'\" UID: '${SFTPGO_PUID}' GID: '${SFTPGO_PGID}'"}'
|
||||
if [ ${DIR} = "/etc/sftpgo" ]; then
|
||||
chown -R ${SFTPGO_PUID}:${SFTPGO_PGID} ${DIR}
|
||||
else
|
||||
|
@ -18,7 +18,7 @@ if [ "$1" = 'sftpgo' ]; then
|
|||
fi
|
||||
fi
|
||||
done
|
||||
echo `date +%Y-%m-%dT%H:%M:%S` - "entrypoint, run as uid: ${SFTPGO_PUID} gid: ${SFTPGO_PGID}"
|
||||
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.000`'","sender":"entrypoint","message":"run as UID: '${SFTPGO_PUID}' GID: '${SFTPGO_PGID}'"}'
|
||||
exec su-exec ${SFTPGO_PUID}:${SFTPGO_PGID} "$@"
|
||||
fi
|
||||
|
||||
|
|
|
@ -10,19 +10,19 @@ if [ "$1" = 'sftpgo' ]; then
|
|||
getent group ${SFTPGO_PGID} > /dev/null
|
||||
HAS_PGID=$?
|
||||
if [ ${HAS_PUID} -ne 0 ] || [ ${HAS_PGID} -ne 0 ]; then
|
||||
echo `date +%Y-%m-%dT%H:%M:%S.%3N` - "entrypoint, prepare to run as uid: ${SFTPGO_PUID} gid: ${SFTPGO_PGID}"
|
||||
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.%3N`'","sender":"entrypoint","message":"prepare to run as UID: '${SFTPGO_PUID}' GID: '${SFTPGO_PGID}'"}'
|
||||
if [ ${HAS_PGID} -ne 0 ]; then
|
||||
echo `date +%Y-%m-%dT%H:%M:%S.%3N` - "entrypoint, set GID to: ${SFTPGO_PGID}"
|
||||
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.%3N`'","sender":"entrypoint","message":"set GID to: '${SFTPGO_PGID}'"}'
|
||||
groupmod -g ${SFTPGO_PGID} sftpgo
|
||||
fi
|
||||
if [ ${HAS_PUID} -ne 0 ]; then
|
||||
echo `date +%Y-%m-%dT%H:%M:%S.%3N` - "entrypoint, set UID to: ${SFTPGO_PUID}"
|
||||
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.%3N`'","sender":"entrypoint","message":"set UID to: '${SFTPGO_PUID}'"}'
|
||||
usermod -u ${SFTPGO_PUID} sftpgo
|
||||
fi
|
||||
chown -R ${SFTPGO_PUID}:${SFTPGO_PGID} /etc/sftpgo
|
||||
chown ${SFTPGO_PUID}:${SFTPGO_PGID} /var/lib/sftpgo /srv/sftpgo
|
||||
fi
|
||||
echo `date +%Y-%m-%dT%H:%M:%S.%3N` - "entrypoint, run as uid: ${SFTPGO_PUID} gid: ${SFTPGO_PGID}"
|
||||
echo '{"level":"info","time":"'`date +%Y-%m-%dT%H:%M:%S.%3N`'","sender":"entrypoint","message":"run as UID: '${SFTPGO_PUID}' GID: '${SFTPGO_PGID}'"}'
|
||||
exec gosu ${SFTPGO_PUID}:${SFTPGO_PGID} "$@"
|
||||
fi
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ The configuration file contains the following sections:
|
|||
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions. See [Custom Actions](./custom-actions.md) for more details
|
||||
- `execute_on`, list of strings. Valid values are `download`, `upload`, `pre-delete`, `delete`, `rename`, `ssh_cmd`. Leave empty to disable actions.
|
||||
- `hook`, string. Absolute path to the command to execute or HTTP URL to notify.
|
||||
- `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions, owner/group and access/modification times are executed. 1 means "ignore mode": requests for changing permissions, owner/group and access/modification times are silently ignored.
|
||||
- `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions, owner/group and access/modification times are executed. 1 means "ignore mode": requests for changing permissions, owner/group and access/modification times are silently ignored. 2 means "ignore mode for cloud based filesystems": requests for changing permissions, owner/group and access/modification times are silently ignored for cloud filesystems and executed for local filesystem.
|
||||
- `proxy_protocol`, integer. Support for [HAProxy PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGNIX, you can enable the proxy protocol. It provides a convenient way to safely transport connection information such as a client's address across multiple layers of NAT or TCP proxies to get the real client IP address instead of the proxy IP. Both protocol versions 1 and 2 are supported. If the proxy protocol is enabled in SFTPGo then you have to enable the protocol in your proxy configuration too. For example, for HAProxy, add `send-proxy` or `send-proxy-v2` to each server configuration line. The following modes are supported:
|
||||
- 0, disabled
|
||||
- 1, enabled. Proxy header will be used and requests without proxy header will be accepted
|
||||
|
|
|
@ -20,9 +20,8 @@ The configured bucket must exist.
|
|||
|
||||
Some SFTP commands don't work over S3:
|
||||
|
||||
- `symlink` and `chtimes` will fail
|
||||
- `chown` and `chmod` are silently ignored
|
||||
- `truncate` is not supported
|
||||
- `chtimes`, `chown` and `chmod` will fail. If you want to silently ignore these method set `setstat_mode` to `1` or `2` in your configuration file
|
||||
- `truncate`, `symlink`, `readlink` are not supported
|
||||
- opening a file for both reading and writing at the same time is not supported
|
||||
- upload resume is not supported
|
||||
- upload mode `atomic` is ignored since S3 uploads are already atomic
|
||||
|
|
|
@ -397,17 +397,6 @@ func TestSFTPCmdTargetPath(t *testing.T) {
|
|||
assert.True(t, os.IsNotExist(err))
|
||||
}
|
||||
|
||||
func TestSetstatModeIgnore(t *testing.T) {
|
||||
originalMode := common.Config.SetstatMode
|
||||
common.Config.SetstatMode = 1
|
||||
connection := Connection{}
|
||||
request := sftp.NewRequest("Setstat", "invalid")
|
||||
request.Flags = 0
|
||||
err := connection.handleSFTPSetstat("invalid", request)
|
||||
assert.NoError(t, err)
|
||||
common.Config.SetstatMode = originalMode
|
||||
}
|
||||
|
||||
func TestSFTPGetUsedQuota(t *testing.T) {
|
||||
u := dataprovider.User{}
|
||||
u.HomeDir = "home_rel_path"
|
||||
|
|
|
@ -361,37 +361,34 @@ func (fs *AzureBlobFs) Mkdir(name string) error {
|
|||
|
||||
// Symlink creates source as a symbolic link to target.
|
||||
func (*AzureBlobFs) Symlink(source, target string) error {
|
||||
return errors.New("403 symlinks are not supported")
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Readlink returns the destination of the named symbolic link
|
||||
func (*AzureBlobFs) Readlink(name string) (string, error) {
|
||||
return "", errors.New("403 readlink is not supported")
|
||||
return "", ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Chown changes the numeric uid and gid of the named file.
|
||||
// Silently ignored.
|
||||
func (*AzureBlobFs) Chown(name string, uid int, gid int) error {
|
||||
return nil
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Chmod changes the mode of the named file to mode.
|
||||
// Silently ignored.
|
||||
func (*AzureBlobFs) Chmod(name string, mode os.FileMode) error {
|
||||
return nil
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Chtimes changes the access and modification times of the named file.
|
||||
// Silently ignored.
|
||||
func (*AzureBlobFs) Chtimes(name string, atime, mtime time.Time) error {
|
||||
return errors.New("403 chtimes is not supported")
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Truncate changes the size of the named file.
|
||||
// Truncate by path is not supported, while truncating an opened
|
||||
// file is handled inside base transfer
|
||||
func (*AzureBlobFs) Truncate(name string, size int64) error {
|
||||
return errors.New("403 truncate is not supported")
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// ReadDir reads the directory named by dirname and returns
|
||||
|
@ -519,6 +516,14 @@ func (*AzureBlobFs) IsPermission(err error) bool {
|
|||
return strings.Contains(err.Error(), "403")
|
||||
}
|
||||
|
||||
// IsNotSupported returns true if the error indicate an unsupported operation
|
||||
func (*AzureBlobFs) IsNotSupported(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return err == ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// CheckRootPath creates the specified local root directory if it does not exists
|
||||
func (fs *AzureBlobFs) CheckRootPath(username string, uid int, gid int) bool {
|
||||
// we need a local directory for temporary files
|
||||
|
@ -575,7 +580,7 @@ func (fs *AzureBlobFs) ScanRootDirContents() (int, int64, error) {
|
|||
// GetDirSize returns the number of files and the size for a folder
|
||||
// including any subfolders
|
||||
func (*AzureBlobFs) GetDirSize(dirname string) (int, int64, error) {
|
||||
return 0, 0, errUnsupported
|
||||
return 0, 0, ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// GetAtomicUploadPath returns the path to use for an atomic upload.
|
||||
|
|
25
vfs/gcsfs.go
25
vfs/gcsfs.go
|
@ -314,37 +314,34 @@ func (fs *GCSFs) Mkdir(name string) error {
|
|||
|
||||
// Symlink creates source as a symbolic link to target.
|
||||
func (*GCSFs) Symlink(source, target string) error {
|
||||
return errors.New("403 symlinks are not supported")
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Readlink returns the destination of the named symbolic link
|
||||
func (*GCSFs) Readlink(name string) (string, error) {
|
||||
return "", errors.New("403 readlink is not supported")
|
||||
return "", ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Chown changes the numeric uid and gid of the named file.
|
||||
// Silently ignored.
|
||||
func (*GCSFs) Chown(name string, uid int, gid int) error {
|
||||
return nil
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Chmod changes the mode of the named file to mode.
|
||||
// Silently ignored.
|
||||
func (*GCSFs) Chmod(name string, mode os.FileMode) error {
|
||||
return nil
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Chtimes changes the access and modification times of the named file.
|
||||
// Silently ignored.
|
||||
func (*GCSFs) Chtimes(name string, atime, mtime time.Time) error {
|
||||
return errors.New("403 chtimes is not supported")
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Truncate changes the size of the named file.
|
||||
// Truncate by path is not supported, while truncating an opened
|
||||
// file is handled inside base transfer
|
||||
func (*GCSFs) Truncate(name string, size int64) error {
|
||||
return errors.New("403 truncate is not supported")
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// ReadDir reads the directory named by dirname and returns
|
||||
|
@ -455,6 +452,14 @@ func (*GCSFs) IsPermission(err error) bool {
|
|||
return strings.Contains(err.Error(), "403")
|
||||
}
|
||||
|
||||
// IsNotSupported returns true if the error indicate an unsupported operation
|
||||
func (*GCSFs) IsNotSupported(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return err == ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// CheckRootPath creates the specified local root directory if it does not exists
|
||||
func (fs *GCSFs) CheckRootPath(username string, uid int, gid int) bool {
|
||||
// we need a local directory for temporary files
|
||||
|
@ -502,7 +507,7 @@ func (fs *GCSFs) ScanRootDirContents() (int, int64, error) {
|
|||
// GetDirSize returns the number of files and the size for a folder
|
||||
// including any subfolders
|
||||
func (*GCSFs) GetDirSize(dirname string) (int, int64, error) {
|
||||
return 0, 0, errUnsupported
|
||||
return 0, 0, ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// GetAtomicUploadPath returns the path to use for an atomic upload.
|
||||
|
|
|
@ -185,6 +185,14 @@ func (*OsFs) IsPermission(err error) bool {
|
|||
return os.IsPermission(err)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
25
vfs/s3fs.go
25
vfs/s3fs.go
|
@ -350,37 +350,34 @@ func (fs *S3Fs) Mkdir(name string) error {
|
|||
|
||||
// Symlink creates source as a symbolic link to target.
|
||||
func (*S3Fs) Symlink(source, target string) error {
|
||||
return errors.New("403 symlinks are not supported")
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Readlink returns the destination of the named symbolic link
|
||||
func (*S3Fs) Readlink(name string) (string, error) {
|
||||
return "", errors.New("403 readlink is not supported")
|
||||
return "", ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Chown changes the numeric uid and gid of the named file.
|
||||
// Silently ignored.
|
||||
func (*S3Fs) Chown(name string, uid int, gid int) error {
|
||||
return nil
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Chmod changes the mode of the named file to mode.
|
||||
// Silently ignored.
|
||||
func (*S3Fs) Chmod(name string, mode os.FileMode) error {
|
||||
return nil
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Chtimes changes the access and modification times of the named file.
|
||||
// Silently ignored.
|
||||
func (*S3Fs) Chtimes(name string, atime, mtime time.Time) error {
|
||||
return errors.New("403 chtimes is not supported")
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Truncate changes the size of the named file.
|
||||
// Truncate by path is not supported, while truncating an opened
|
||||
// file is handled inside base transfer
|
||||
func (*S3Fs) Truncate(name string, size int64) error {
|
||||
return errors.New("403 truncate is not supported")
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// ReadDir reads the directory named by dirname and returns
|
||||
|
@ -485,6 +482,14 @@ func (*S3Fs) IsPermission(err error) bool {
|
|||
return strings.Contains(err.Error(), "403")
|
||||
}
|
||||
|
||||
// IsNotSupported returns true if the error indicate an unsupported operation
|
||||
func (*S3Fs) IsNotSupported(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return err == ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// CheckRootPath creates the specified local root directory if it does not exists
|
||||
func (fs *S3Fs) CheckRootPath(username string, uid int, gid int) bool {
|
||||
// we need a local directory for temporary files
|
||||
|
@ -520,7 +525,7 @@ func (fs *S3Fs) ScanRootDirContents() (int, int64, error) {
|
|||
// GetDirSize returns the number of files and the size for a folder
|
||||
// including any subfolders
|
||||
func (*S3Fs) GetDirSize(dirname string) (int, int64, error) {
|
||||
return 0, 0, errUnsupported
|
||||
return 0, 0, ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// GetAtomicUploadPath returns the path to use for an atomic upload.
|
||||
|
|
|
@ -46,6 +46,7 @@ type Fs interface {
|
|||
ResolvePath(sftpPath string) (string, error)
|
||||
IsNotExist(err error) bool
|
||||
IsPermission(err error) bool
|
||||
IsNotSupported(err error) bool
|
||||
ScanRootDirContents() (int, int64, error)
|
||||
GetDirSize(dirname string) (int, int64, error)
|
||||
GetAtomicUploadPath(name string) string
|
||||
|
@ -56,7 +57,7 @@ type Fs interface {
|
|||
GetMimeType(name string) (string, error)
|
||||
}
|
||||
|
||||
var errUnsupported = errors.New("Not supported")
|
||||
var ErrVfsUnsupported = errors.New("Not supported")
|
||||
|
||||
// QuotaCheckResult defines the result for a quota check
|
||||
type QuotaCheckResult struct {
|
||||
|
|
Loading…
Reference in a new issue