mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 09:00:27 +00:00
REST API: add an option to create missing dirs
This commit is contained in:
parent
cc73bb811b
commit
ced73ed04e
13 changed files with 290 additions and 24 deletions
|
@ -245,6 +245,35 @@ func (c *BaseConnection) ListDir(virtualPath string) ([]os.FileInfo, error) {
|
|||
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
|
||||
func (c *BaseConnection) CreateDir(virtualPath string) error {
|
||||
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))
|
||||
}
|
||||
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)
|
||||
}
|
||||
if vfs.IsCryptOsFs(fs) {
|
||||
|
@ -516,6 +545,14 @@ func (c *BaseConnection) DoStat(virtualPath string, mode int) (os.FileInfo, erro
|
|||
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 {
|
||||
if Config.SetstatMode == 1 {
|
||||
return true
|
||||
|
@ -1089,6 +1126,18 @@ func (c *BaseConnection) updateQuotaAfterRename(fs vfs.Fs, virtualSourcePath, vi
|
|||
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
|
||||
func (c *BaseConnection) GetPermissionDeniedError() error {
|
||||
switch c.protocol {
|
||||
|
|
|
@ -9,9 +9,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/sftp"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/kms"
|
||||
"github.com/drakkan/sftpgo/v2/sdk"
|
||||
"github.com/drakkan/sftpgo/v2/vfs"
|
||||
)
|
||||
|
@ -306,6 +308,8 @@ func TestErrorsMapping(t *testing.T) {
|
|||
}
|
||||
err = conn.GetQuotaExceededError()
|
||||
assert.True(t, conn.IsQuotaExceededError(err))
|
||||
err = conn.GetNotExistError()
|
||||
assert.True(t, conn.IsNotExistError(err))
|
||||
err = conn.GetFsError(fs, nil)
|
||||
assert.NoError(t, err)
|
||||
err = conn.GetOpUnsupportedError()
|
||||
|
@ -368,3 +372,76 @@ func TestMaxWriteSize(t *testing.T) {
|
|||
assert.EqualError(t, err, ErrOpUnsupported.Error())
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/pkg/sftp"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/rs/xid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -373,6 +374,51 @@ func TestChtimesOpenHandle(t *testing.T) {
|
|||
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) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -147,7 +147,6 @@ func (c *Connection) Stat(name string) (os.FileInfo, error) {
|
|||
|
||||
fi, err := c.DoStat(name, 0)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelDebug, "error running stat on path %#v: %+v", name, err)
|
||||
return nil, err
|
||||
}
|
||||
return fi, nil
|
||||
|
|
6
go.mod
6
go.mod
|
@ -33,12 +33,12 @@ require (
|
|||
github.com/minio/sio v0.3.0
|
||||
github.com/otiai10/copy v1.7.0
|
||||
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/prometheus/client_golang v1.11.0
|
||||
github.com/rs/cors v1.8.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/spf13/afero v1.6.0
|
||||
github.com/spf13/cobra v1.3.0
|
||||
|
@ -51,7 +51,7 @@ require (
|
|||
go.etcd.io/bbolt v1.3.6
|
||||
go.uber.org/automaxprocs v1.4.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/sys v0.0.0-20211216021012-1d35b9e2eb4e
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
|
|
12
go.sum
12
go.sum
|
@ -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/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.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg=
|
||||
github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8=
|
||||
github.com/pkg/sftp v1.13.5-0.20211217081921-1849af66afae h1:J8MHmz3LSjRtoR4SKiPq8BNo3DacJl5kQRjJeWilkUI=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/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.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
|
||||
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
|
||||
github.com/rs/zerolog v1.26.2-0.20211217020337-0c8d3c0b10c3 h1:TwTFgwG2b3h0GNPv3o9+Thu9n0lrIj2t+IsWHY/DbOE=
|
||||
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/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
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/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
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-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-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-20210510120138-977fb7262007/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-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-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-20210816183151-1e6c022a8912/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.4/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-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
@ -86,6 +86,12 @@ func createUserDir(w http.ResponseWriter, r *http.Request) {
|
|||
defer common.Connections.Remove(connection.GetID())
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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())
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -278,6 +290,12 @@ func uploadUserFiles(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, nil, "No files uploaded!", http.StatusBadRequest)
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -91,14 +91,15 @@ func getRespStatus(err error) int {
|
|||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// mappig between fs errors for HTTP protocol and HTTP response status codes
|
||||
func getMappedStatusCode(err error) int {
|
||||
var statusCode int
|
||||
switch err {
|
||||
case os.ErrPermission:
|
||||
switch {
|
||||
case errors.Is(err, os.ErrPermission):
|
||||
statusCode = http.StatusForbidden
|
||||
case os.ErrNotExist:
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
statusCode = http.StatusNotFound
|
||||
case common.ErrQuotaExceeded:
|
||||
case errors.Is(err, common.ErrQuotaExceeded):
|
||||
statusCode = http.StatusRequestEntityTooLarge
|
||||
default:
|
||||
statusCode = http.StatusInternalServerError
|
||||
|
@ -128,6 +129,10 @@ func getCommaSeparatedQueryParam(r *http.Request, key string) []string {
|
|||
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) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
connectionID := getURLParam(r, "connectionID")
|
||||
|
|
|
@ -68,7 +68,6 @@ func (c *Connection) Stat(name string, mode int) (os.FileInfo, error) {
|
|||
|
||||
fi, err := c.DoStat(name, mode)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelDebug, "error running stat on path %#v: %+v", name, err)
|
||||
return nil, err
|
||||
}
|
||||
return fi, err
|
||||
|
|
|
@ -10179,6 +10179,18 @@ func TestWebDirsAPI(t *testing.T) {
|
|||
if assert.Len(t, contents, 1) {
|
||||
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
|
||||
req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path="+testDir+"&target="+testDir+"new", nil)
|
||||
assert.NoError(t, err)
|
||||
|
@ -10271,6 +10283,17 @@ func TestWebUploadSingleFile(t *testing.T) {
|
|||
if assert.NoError(t, err) {
|
||||
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["modification_time"] = util.GetTimeAsMsSinceEpoch(modTime)
|
||||
|
@ -10424,6 +10447,24 @@ func TestWebFilesAPI(t *testing.T) {
|
|||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
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)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
|
@ -10432,7 +10473,7 @@ func TestWebFilesAPI(t *testing.T) {
|
|||
contents = nil
|
||||
err = json.NewDecoder(rr.Body).Decode(&contents)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, contents, 3)
|
||||
assert.Len(t, contents, 4)
|
||||
req, err = http.NewRequest(http.MethodGet, userDirsPath+"?path="+testDir, nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
|
@ -10579,7 +10620,28 @@ func TestWebUploadErrors(t *testing.T) {
|
|||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
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
|
||||
req, err = http.NewRequest(http.MethodPost, userDirsPath+"?path=file.zip", nil)
|
||||
assert.NoError(t, err)
|
||||
|
@ -11026,7 +11088,7 @@ func TestWebUploadMultipartFormReadError(t *testing.T) {
|
|||
req.Header.Add("Content-Type", "multipart/form-data")
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to read uploaded file")
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
|
|
|
@ -3543,6 +3543,12 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- in: query
|
||||
name: mkdir_parents
|
||||
description: Create parent directories if they do not exist?
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
responses:
|
||||
'201':
|
||||
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
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: mkdir_parents
|
||||
description: Create parent directories if they do not exist?
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
|
@ -3848,6 +3860,12 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- in: query
|
||||
name: mkdir_parents
|
||||
description: Create parent directories if they do not exist?
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
- in: header
|
||||
name: X-SFTPGO-MTIME
|
||||
schema:
|
||||
|
|
|
@ -216,7 +216,6 @@ func (c *Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
|||
|
||||
s, err := c.DoStat(request.Filepath, 0)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelDebug, "error running stat on path %#v: %+v", request.Filepath, 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)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelDebug, "error running lstat on path %#v: %+v", request.Filepath, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -87,7 +87,6 @@ func (c *Connection) Stat(ctx context.Context, name string) (os.FileInfo, error)
|
|||
|
||||
fi, err := c.DoStat(name, 0)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelDebug, "error running stat on path %#v: %+v", name, err)
|
||||
return nil, err
|
||||
}
|
||||
return fi, err
|
||||
|
|
Loading…
Reference in a new issue