REST API: add an option to create missing dirs

This commit is contained in:
Nicola Murino 2021-12-19 12:14:53 +01:00
parent cc73bb811b
commit ced73ed04e
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
13 changed files with 290 additions and 24 deletions

View file

@ -245,6 +245,35 @@ func (c *BaseConnection) ListDir(virtualPath string) ([]os.FileInfo, error) {
return c.User.AddVirtualDirs(files, virtualPath), nil return c.User.AddVirtualDirs(files, virtualPath), nil
} }
// CheckParentDirs tries to create the specified directory and any missing parent dirs
func (c *BaseConnection) CheckParentDirs(virtualPath string) error {
fs, err := c.User.GetFilesystemForPath(virtualPath, "")
if err != nil {
return err
}
if fs.HasVirtualFolders() {
return nil
}
if _, err := c.DoStat(virtualPath, 0); !c.IsNotExistError(err) {
return err
}
dirs := util.GetDirsForVirtualPath(virtualPath)
for idx := len(dirs) - 1; idx >= 0; idx-- {
fs, err = c.User.GetFilesystemForPath(dirs[idx], "")
if err != nil {
return err
}
if fs.HasVirtualFolders() {
continue
}
if err = c.createDirIfMissing(dirs[idx]); err != nil {
return fmt.Errorf("unable to check/create missing parent dir %#v for virtual path %#v: %w",
dirs[idx], virtualPath, err)
}
}
return nil
}
// CreateDir creates a new directory at the specified fsPath // CreateDir creates a new directory at the specified fsPath
func (c *BaseConnection) CreateDir(virtualPath string) error { func (c *BaseConnection) CreateDir(virtualPath string) error {
if !c.User.HasPerm(dataprovider.PermCreateDirs, path.Dir(virtualPath)) { if !c.User.HasPerm(dataprovider.PermCreateDirs, path.Dir(virtualPath)) {
@ -507,7 +536,7 @@ func (c *BaseConnection) DoStat(virtualPath string, mode int) (os.FileInfo, erro
info, err = fs.Stat(c.getRealFsPath(fsPath)) info, err = fs.Stat(c.getRealFsPath(fsPath))
} }
if err != nil { if err != nil {
c.Log(logger.LevelDebug, "stat error for path %#v: %+v", virtualPath, err) c.Log(logger.LevelError, "stat error for path %#v: %+v", virtualPath, err)
return info, c.GetFsError(fs, err) return info, c.GetFsError(fs, err)
} }
if vfs.IsCryptOsFs(fs) { if vfs.IsCryptOsFs(fs) {
@ -516,6 +545,14 @@ func (c *BaseConnection) DoStat(virtualPath string, mode int) (os.FileInfo, erro
return info, nil return info, nil
} }
func (c *BaseConnection) createDirIfMissing(name string) error {
_, err := c.DoStat(name, 0)
if c.IsNotExistError(err) {
return c.CreateDir(name)
}
return err
}
func (c *BaseConnection) ignoreSetStat(fs vfs.Fs) bool { func (c *BaseConnection) ignoreSetStat(fs vfs.Fs) bool {
if Config.SetstatMode == 1 { if Config.SetstatMode == 1 {
return true return true
@ -1089,6 +1126,18 @@ func (c *BaseConnection) updateQuotaAfterRename(fs vfs.Fs, virtualSourcePath, vi
return nil return nil
} }
// IsNotExistError returns true if the specified fs error is not exist for the connection protocol
func (c *BaseConnection) IsNotExistError(err error) bool {
switch c.protocol {
case ProtocolSFTP:
return errors.Is(err, sftp.ErrSSHFxNoSuchFile)
case ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolHTTPShare, ProtocolDataRetention:
return errors.Is(err, os.ErrNotExist)
default:
return errors.Is(err, ErrNotExist)
}
}
// GetPermissionDeniedError returns an appropriate permission denied error for the connection protocol // GetPermissionDeniedError returns an appropriate permission denied error for the connection protocol
func (c *BaseConnection) GetPermissionDeniedError() error { func (c *BaseConnection) GetPermissionDeniedError() error {
switch c.protocol { switch c.protocol {

View file

@ -9,9 +9,11 @@ import (
"time" "time"
"github.com/pkg/sftp" "github.com/pkg/sftp"
"github.com/rs/xid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/sdk" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -306,6 +308,8 @@ func TestErrorsMapping(t *testing.T) {
} }
err = conn.GetQuotaExceededError() err = conn.GetQuotaExceededError()
assert.True(t, conn.IsQuotaExceededError(err)) assert.True(t, conn.IsQuotaExceededError(err))
err = conn.GetNotExistError()
assert.True(t, conn.IsNotExistError(err))
err = conn.GetFsError(fs, nil) err = conn.GetFsError(fs, nil)
assert.NoError(t, err) assert.NoError(t, err)
err = conn.GetOpUnsupportedError() err = conn.GetOpUnsupportedError()
@ -368,3 +372,76 @@ func TestMaxWriteSize(t *testing.T) {
assert.EqualError(t, err, ErrOpUnsupported.Error()) assert.EqualError(t, err, ErrOpUnsupported.Error())
assert.Equal(t, int64(0), size) assert.Equal(t, int64(0), size)
} }
func TestCheckParentDirsErrors(t *testing.T) {
permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny}
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: userTestUsername,
Permissions: permissions,
HomeDir: filepath.Clean(os.TempDir()),
},
FsConfig: vfs.Filesystem{
Provider: sdk.CryptedFilesystemProvider,
},
}
c := NewBaseConnection(xid.New().String(), ProtocolSFTP, "", "", user)
err := c.CheckParentDirs("/a/dir")
assert.Error(t, err)
user.FsConfig.Provider = sdk.LocalFilesystemProvider
user.VirtualFolders = nil
user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{
FsConfig: vfs.Filesystem{
Provider: sdk.CryptedFilesystemProvider,
},
},
VirtualPath: "/vdir",
})
user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{
MappedPath: filepath.Clean(os.TempDir()),
},
VirtualPath: "/vdir/sub",
})
c = NewBaseConnection(xid.New().String(), ProtocolSFTP, "", "", user)
err = c.CheckParentDirs("/vdir/sub/dir")
assert.Error(t, err)
user = dataprovider.User{
BaseUser: sdk.BaseUser{
Username: userTestUsername,
Permissions: permissions,
HomeDir: filepath.Clean(os.TempDir()),
},
FsConfig: vfs.Filesystem{
Provider: sdk.S3FilesystemProvider,
S3Config: vfs.S3FsConfig{
S3FsConfig: sdk.S3FsConfig{
Bucket: "buck",
Region: "us-east-1",
AccessKey: "key",
AccessSecret: kms.NewPlainSecret("s3secret"),
},
},
},
}
c = NewBaseConnection(xid.New().String(), ProtocolSFTP, "", "", user)
err = c.CheckParentDirs("/a/dir")
assert.NoError(t, err)
user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{
MappedPath: filepath.Clean(os.TempDir()),
},
VirtualPath: "/local/dir",
})
c = NewBaseConnection(xid.New().String(), ProtocolSFTP, "", "", user)
err = c.CheckParentDirs("/local/dir/sub-dir")
assert.NoError(t, err)
err = os.RemoveAll(filepath.Join(os.TempDir(), "sub-dir"))
assert.NoError(t, err)
}

View file

@ -23,6 +23,7 @@ import (
"github.com/pkg/sftp" "github.com/pkg/sftp"
"github.com/pquerna/otp" "github.com/pquerna/otp"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
"github.com/rs/xid"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -373,6 +374,51 @@ func TestChtimesOpenHandle(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestCheckParentDirs(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
testDir := "/path/to/sub/dir"
conn, client, err := getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
_, err = client.Stat(testDir)
assert.ErrorIs(t, err, os.ErrNotExist)
c := common.NewBaseConnection(xid.New().String(), common.ProtocolSFTP, "", "", user)
err = c.CheckParentDirs(testDir)
assert.NoError(t, err)
_, err = client.Stat(testDir)
assert.NoError(t, err)
err = c.CheckParentDirs(testDir)
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
u := getTestUser()
u.Permissions["/"] = []string{dataprovider.PermUpload, dataprovider.PermListItems, dataprovider.PermDownload}
user, _, err = httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
conn, client, err = getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
c := common.NewBaseConnection(xid.New().String(), common.ProtocolSFTP, "", "", user)
err = c.CheckParentDirs(testDir)
assert.ErrorIs(t, err, sftp.ErrSSHFxPermissionDenied)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestPermissionErrors(t *testing.T) { func TestPermissionErrors(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -147,7 +147,6 @@ func (c *Connection) Stat(name string) (os.FileInfo, error) {
fi, err := c.DoStat(name, 0) fi, err := c.DoStat(name, 0)
if err != nil { if err != nil {
c.Log(logger.LevelDebug, "error running stat on path %#v: %+v", name, err)
return nil, err return nil, err
} }
return fi, nil return fi, nil

6
go.mod
View file

@ -33,12 +33,12 @@ require (
github.com/minio/sio v0.3.0 github.com/minio/sio v0.3.0
github.com/otiai10/copy v1.7.0 github.com/otiai10/copy v1.7.0
github.com/pires/go-proxyproto v0.6.1 github.com/pires/go-proxyproto v0.6.1
github.com/pkg/sftp v1.13.4 github.com/pkg/sftp v1.13.5-0.20211217081921-1849af66afae
github.com/pquerna/otp v1.3.0 github.com/pquerna/otp v1.3.0
github.com/prometheus/client_golang v1.11.0 github.com/prometheus/client_golang v1.11.0
github.com/rs/cors v1.8.0 github.com/rs/cors v1.8.0
github.com/rs/xid v1.3.0 github.com/rs/xid v1.3.0
github.com/rs/zerolog v1.26.1 github.com/rs/zerolog v1.26.2-0.20211217020337-0c8d3c0b10c3
github.com/shirou/gopsutil/v3 v3.21.11 github.com/shirou/gopsutil/v3 v3.21.11
github.com/spf13/afero v1.6.0 github.com/spf13/afero v1.6.0
github.com/spf13/cobra v1.3.0 github.com/spf13/cobra v1.3.0
@ -51,7 +51,7 @@ require (
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.6
go.uber.org/automaxprocs v1.4.0 go.uber.org/automaxprocs v1.4.0
gocloud.dev v0.24.0 gocloud.dev v0.24.0
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/net v0.0.0-20211209124913-491a49abca63 golang.org/x/net v0.0.0-20211209124913-491a49abca63
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11

12
go.sum
View file

@ -682,8 +682,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg= github.com/pkg/sftp v1.13.5-0.20211217081921-1849af66afae h1:J8MHmz3LSjRtoR4SKiPq8BNo3DacJl5kQRjJeWilkUI=
github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8= github.com/pkg/sftp v1.13.5-0.20211217081921-1849af66afae/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@ -726,8 +726,8 @@ github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= github.com/rs/zerolog v1.26.2-0.20211217020337-0c8d3c0b10c3 h1:TwTFgwG2b3h0GNPv3o9+Thu9n0lrIj2t+IsWHY/DbOE=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= github.com/rs/zerolog v1.26.2-0.20211217020337-0c8d3c0b10c3/go.mod h1:eHQZiDw9MQWpDKmOvvJAV5O5us43b8IMRafp/wTdspo=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@ -800,7 +800,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
@ -973,7 +972,6 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -984,7 +982,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1083,7 +1080,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -86,6 +86,12 @@ func createUserDir(w http.ResponseWriter, r *http.Request) {
defer common.Connections.Remove(connection.GetID()) defer common.Connections.Remove(connection.GetID())
name := util.CleanPath(r.URL.Query().Get("path")) name := util.CleanPath(r.URL.Query().Get("path"))
if getBoolQueryParam(r, "mkdir_parents") {
if err = connection.CheckParentDirs(path.Dir(name)); err != nil {
sendAPIResponse(w, r, err, "Error checking parent directories", getMappedStatusCode(err))
return
}
}
err = connection.CreateDir(name) err = connection.CreateDir(name)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to create directory %#v", name), getMappedStatusCode(err)) sendAPIResponse(w, r, err, fmt.Sprintf("Unable to create directory %#v", name), getMappedStatusCode(err))
@ -224,6 +230,12 @@ func uploadUserFile(w http.ResponseWriter, r *http.Request) {
defer common.Connections.Remove(connection.GetID()) defer common.Connections.Remove(connection.GetID())
filePath := util.CleanPath(r.URL.Query().Get("path")) filePath := util.CleanPath(r.URL.Query().Get("path"))
if getBoolQueryParam(r, "mkdir_parents") {
if err = connection.CheckParentDirs(path.Dir(filePath)); err != nil {
sendAPIResponse(w, r, err, "Error checking parent directories", getMappedStatusCode(err))
return
}
}
doUploadFile(w, r, connection, filePath) //nolint:errcheck doUploadFile(w, r, connection, filePath) //nolint:errcheck
} }
@ -278,6 +290,12 @@ func uploadUserFiles(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, nil, "No files uploaded!", http.StatusBadRequest) sendAPIResponse(w, r, nil, "No files uploaded!", http.StatusBadRequest)
return return
} }
if getBoolQueryParam(r, "mkdir_parents") {
if err = connection.CheckParentDirs(parentDir); err != nil {
sendAPIResponse(w, r, err, "Error checking parent directories", getMappedStatusCode(err))
return
}
}
doUploadFiles(w, r, connection, parentDir, files) doUploadFiles(w, r, connection, parentDir, files)
} }

View file

@ -91,14 +91,15 @@ func getRespStatus(err error) int {
return http.StatusInternalServerError return http.StatusInternalServerError
} }
// mappig between fs errors for HTTP protocol and HTTP response status codes
func getMappedStatusCode(err error) int { func getMappedStatusCode(err error) int {
var statusCode int var statusCode int
switch err { switch {
case os.ErrPermission: case errors.Is(err, os.ErrPermission):
statusCode = http.StatusForbidden statusCode = http.StatusForbidden
case os.ErrNotExist: case errors.Is(err, os.ErrNotExist):
statusCode = http.StatusNotFound statusCode = http.StatusNotFound
case common.ErrQuotaExceeded: case errors.Is(err, common.ErrQuotaExceeded):
statusCode = http.StatusRequestEntityTooLarge statusCode = http.StatusRequestEntityTooLarge
default: default:
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
@ -128,6 +129,10 @@ func getCommaSeparatedQueryParam(r *http.Request, key string) []string {
return util.RemoveDuplicates(result) return util.RemoveDuplicates(result)
} }
func getBoolQueryParam(r *http.Request, param string) bool {
return r.URL.Query().Get(param) == "true"
}
func handleCloseConnection(w http.ResponseWriter, r *http.Request) { func handleCloseConnection(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
connectionID := getURLParam(r, "connectionID") connectionID := getURLParam(r, "connectionID")

View file

@ -68,7 +68,6 @@ func (c *Connection) Stat(name string, mode int) (os.FileInfo, error) {
fi, err := c.DoStat(name, mode) fi, err := c.DoStat(name, mode)
if err != nil { if err != nil {
c.Log(logger.LevelDebug, "error running stat on path %#v: %+v", name, err)
return nil, err return nil, err
} }
return fi, err return fi, err

View file

@ -10179,6 +10179,18 @@ func TestWebDirsAPI(t *testing.T) {
if assert.Len(t, contents, 1) { if assert.Len(t, contents, 1) {
assert.Equal(t, testDir, contents[0]["name"]) assert.Equal(t, testDir, contents[0]["name"])
} }
// create a dir with missing parents
req, err = http.NewRequest(http.MethodPost, userDirsPath+"?path="+url.QueryEscape(path.Join("/sub/dir", testDir)), nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr)
// setting the mkdir_parents param will work
req, err = http.NewRequest(http.MethodPost, userDirsPath+"?mkdir_parents=true&path="+url.QueryEscape(path.Join("/sub/dir", testDir)), nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusCreated, rr)
// rename the dir // rename the dir
req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path="+testDir+"&target="+testDir+"new", nil) req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path="+testDir+"&target="+testDir+"new", nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -10271,6 +10283,17 @@ func TestWebUploadSingleFile(t *testing.T) {
if assert.NoError(t, err) { if assert.NoError(t, err) {
assert.InDelta(t, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(3000)) assert.InDelta(t, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(3000))
} }
// upload to a missing dir will fail without the mkdir_parents param
req, err = http.NewRequest(http.MethodPost, userUploadFilePath+"?path="+url.QueryEscape("/subdir/file.txt"), bytes.NewBuffer(content))
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr)
req, err = http.NewRequest(http.MethodPost, userUploadFilePath+"?mkdir_parents=true&path="+url.QueryEscape("/subdir/file.txt"), bytes.NewBuffer(content))
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusCreated, rr)
metadataReq := make(map[string]int64) metadataReq := make(map[string]int64)
metadataReq["modification_time"] = util.GetTimeAsMsSinceEpoch(modTime) metadataReq["modification_time"] = util.GetTimeAsMsSinceEpoch(modTime)
@ -10424,6 +10447,24 @@ func TestWebFilesAPI(t *testing.T) {
setBearerForReq(req, webAPIToken) setBearerForReq(req, webAPIToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusCreated, rr) checkResponseCode(t, http.StatusCreated, rr)
// upload to a missing subdir will fail without the mkdir_parents param
_, err = reader.Seek(0, io.SeekStart)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, userFilesPath+"?path="+url.QueryEscape("/sub/"+testDir), reader)
assert.NoError(t, err)
req.Header.Add("Content-Type", writer.FormDataContentType())
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr)
_, err = reader.Seek(0, io.SeekStart)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, userFilesPath+"?mkdir_parents=true&path="+url.QueryEscape("/sub/"+testDir), reader)
assert.NoError(t, err)
req.Header.Add("Content-Type", writer.FormDataContentType())
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusCreated, rr)
req, err = http.NewRequest(http.MethodGet, userDirsPath, nil) req, err = http.NewRequest(http.MethodGet, userDirsPath, nil)
assert.NoError(t, err) assert.NoError(t, err)
setBearerForReq(req, webAPIToken) setBearerForReq(req, webAPIToken)
@ -10432,7 +10473,7 @@ func TestWebFilesAPI(t *testing.T) {
contents = nil contents = nil
err = json.NewDecoder(rr.Body).Decode(&contents) err = json.NewDecoder(rr.Body).Decode(&contents)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, contents, 3) assert.Len(t, contents, 4)
req, err = http.NewRequest(http.MethodGet, userDirsPath+"?path="+testDir, nil) req, err = http.NewRequest(http.MethodGet, userDirsPath+"?path="+testDir, nil)
assert.NoError(t, err) assert.NoError(t, err)
setBearerForReq(req, webAPIToken) setBearerForReq(req, webAPIToken)
@ -10579,7 +10620,28 @@ func TestWebUploadErrors(t *testing.T) {
setBearerForReq(req, webAPIToken) setBearerForReq(req, webAPIToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
// we cannot create dirs in sub2
_, err = reader.Seek(0, io.SeekStart)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, userFilesPath+"?mkdir_parents=true&path="+url.QueryEscape("/sub2/dir"), reader)
assert.NoError(t, err)
req.Header.Add("Content-Type", writer.FormDataContentType())
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "unable to check/create missing parent dir")
req, err = http.NewRequest(http.MethodPost, userDirsPath+"?mkdir_parents=true&path="+url.QueryEscape("/sub2/dir/test"), nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Error checking parent directories")
req, err = http.NewRequest(http.MethodPost, userUploadFilePath+"?mkdir_parents=true&path="+url.QueryEscape("/sub2/dir1/file.txt"), bytes.NewBuffer([]byte("")))
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Error checking parent directories")
// create a dir and try to overwrite it with a file // create a dir and try to overwrite it with a file
req, err = http.NewRequest(http.MethodPost, userDirsPath+"?path=file.zip", nil) req, err = http.NewRequest(http.MethodPost, userDirsPath+"?path=file.zip", nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -11026,7 +11088,7 @@ func TestWebUploadMultipartFormReadError(t *testing.T) {
req.Header.Add("Content-Type", "multipart/form-data") req.Header.Add("Content-Type", "multipart/form-data")
setBearerForReq(req, webAPIToken) setBearerForReq(req, webAPIToken)
rr := executeRequest(req) rr := executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr) checkResponseCode(t, http.StatusNotFound, rr)
assert.Contains(t, rr.Body.String(), "Unable to read uploaded file") assert.Contains(t, rr.Body.String(), "Unable to read uploaded file")
_, err = httpdtest.RemoveUser(user, http.StatusOK) _, err = httpdtest.RemoveUser(user, http.StatusOK)

View file

@ -3543,6 +3543,12 @@ paths:
schema: schema:
type: string type: string
required: true required: true
- in: query
name: mkdir_parents
description: Create parent directories if they do not exist?
schema:
type: boolean
required: false
responses: responses:
'201': '201':
description: successful operation description: successful operation
@ -3729,6 +3735,12 @@ paths:
description: Parent directory for the uploaded files. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir". If empty or missing the root path is assumed. If a file with the same name already exists, it will be overwritten description: Parent directory for the uploaded files. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir". If empty or missing the root path is assumed. If a file with the same name already exists, it will be overwritten
schema: schema:
type: string type: string
- in: query
name: mkdir_parents
description: Create parent directories if they do not exist?
schema:
type: boolean
required: false
requestBody: requestBody:
content: content:
multipart/form-data: multipart/form-data:
@ -3848,6 +3860,12 @@ paths:
schema: schema:
type: string type: string
required: true required: true
- in: query
name: mkdir_parents
description: Create parent directories if they do not exist?
schema:
type: boolean
required: false
- in: header - in: header
name: X-SFTPGO-MTIME name: X-SFTPGO-MTIME
schema: schema:

View file

@ -216,7 +216,6 @@ func (c *Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
s, err := c.DoStat(request.Filepath, 0) s, err := c.DoStat(request.Filepath, 0)
if err != nil { if err != nil {
c.Log(logger.LevelDebug, "error running stat on path %#v: %+v", request.Filepath, err)
return nil, err return nil, err
} }
@ -258,7 +257,6 @@ func (c *Connection) Lstat(request *sftp.Request) (sftp.ListerAt, error) {
s, err := c.DoStat(request.Filepath, 1) s, err := c.DoStat(request.Filepath, 1)
if err != nil { if err != nil {
c.Log(logger.LevelDebug, "error running lstat on path %#v: %+v", request.Filepath, err)
return nil, err return nil, err
} }

View file

@ -87,7 +87,6 @@ func (c *Connection) Stat(ctx context.Context, name string) (os.FileInfo, error)
fi, err := c.DoStat(name, 0) fi, err := c.DoStat(name, 0)
if err != nil { if err != nil {
c.Log(logger.LevelDebug, "error running stat on path %#v: %+v", name, err)
return nil, err return nil, err
} }
return fi, err return fi, err