diff --git a/common/connection.go b/common/connection.go index a1a4fbf8..92a432be 100644 --- a/common/connection.go +++ b/common/connection.go @@ -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 { diff --git a/common/connection_test.go b/common/connection_test.go index 1ab0e5f0..233bd868 100644 --- a/common/connection_test.go +++ b/common/connection_test.go @@ -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) +} diff --git a/common/protocol_test.go b/common/protocol_test.go index f28a9772..51b42485 100644 --- a/common/protocol_test.go +++ b/common/protocol_test.go @@ -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) diff --git a/ftpd/handler.go b/ftpd/handler.go index 0a8b1812..3ab03afb 100644 --- a/ftpd/handler.go +++ b/ftpd/handler.go @@ -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 diff --git a/go.mod b/go.mod index 657afbe9..aac50513 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index c973da02..19e6c28d 100644 --- a/go.sum +++ b/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= diff --git a/httpd/api_http_user.go b/httpd/api_http_user.go index e7ed4c47..378c1aba 100644 --- a/httpd/api_http_user.go +++ b/httpd/api_http_user.go @@ -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) } diff --git a/httpd/api_utils.go b/httpd/api_utils.go index 8c404b18..88c2326f 100644 --- a/httpd/api_utils.go +++ b/httpd/api_utils.go @@ -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") diff --git a/httpd/handler.go b/httpd/handler.go index c03eeee2..be6b3620 100644 --- a/httpd/handler.go +++ b/httpd/handler.go @@ -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 diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index e1408485..0ee83a2f 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -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) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 7942a9e1..18000e9a 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -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: diff --git a/sftpd/handler.go b/sftpd/handler.go index c92edfb4..7fb25f3c 100644 --- a/sftpd/handler.go +++ b/sftpd/handler.go @@ -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 } diff --git a/webdavd/handler.go b/webdavd/handler.go index 1fd6844b..a6dceb14 100644 --- a/webdavd/handler.go +++ b/webdavd/handler.go @@ -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