Преглед на файлове

don't execute fs check if the user has recent activity

The check could be expensive with some backends and is generally
only required the first time that a user logs in

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino преди 3 години
родител
ревизия
f5a0559be6
променени са 16 файла, в които са добавени 355 реда и са изтрити 156 реда
  1. 33 0
      common/common_test.go
  2. 10 7
      common/protocol_test.go
  3. 11 6
      dataprovider/dataprovider.go
  4. 47 9
      dataprovider/user.go
  5. 72 7
      ftpd/ftpd_test.go
  6. 9 9
      go.mod
  7. 18 12
      go.sum
  8. 5 10
      httpd/middleware.go
  9. 94 23
      sftpd/sftpd_test.go
  10. 0 5
      vfs/azblobfs.go
  11. 0 5
      vfs/gcsfs.go
  12. 0 24
      vfs/osfs.go
  13. 24 21
      vfs/s3fs.go
  14. 4 11
      vfs/sftpfs.go
  15. 0 1
      vfs/vfs.go
  16. 28 6
      webdavd/webdavd_test.go

+ 33 - 0
common/common_test.go

@@ -881,6 +881,39 @@ func TestGetTLSVersion(t *testing.T) {
 	assert.Equal(t, uint16(tls.VersionTLS13), tlsVer)
 }
 
+func TestCleanPath(t *testing.T) {
+	assert.Equal(t, "/", util.CleanPath("/"))
+	assert.Equal(t, "/", util.CleanPath("."))
+	assert.Equal(t, "/", util.CleanPath("/."))
+	assert.Equal(t, "/", util.CleanPath("/a/.."))
+	assert.Equal(t, "/a", util.CleanPath("/a/"))
+	assert.Equal(t, "/a", util.CleanPath("a/"))
+	// filepath.ToSlash does not touch \ as char on unix systems
+	// so os.PathSeparator is used for windows compatible tests
+	bslash := string(os.PathSeparator)
+	assert.Equal(t, "/", util.CleanPath(bslash))
+	assert.Equal(t, "/", util.CleanPath(bslash+bslash))
+	assert.Equal(t, "/a", util.CleanPath(bslash+"a"+bslash))
+	assert.Equal(t, "/a", util.CleanPath("a"+bslash))
+	assert.Equal(t, "/a/b/c", util.CleanPath(bslash+"a"+bslash+bslash+"b"+bslash+bslash+"c"+bslash))
+	assert.Equal(t, "/C:/a", util.CleanPath("C:"+bslash+"a"))
+}
+
+func TestUserRecentActivity(t *testing.T) {
+	u := dataprovider.User{}
+	res := u.HasRecentActivity()
+	assert.False(t, res)
+	u.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())
+	res = u.HasRecentActivity()
+	assert.True(t, res)
+	u.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now().Add(1 * time.Minute))
+	res = u.HasRecentActivity()
+	assert.False(t, res)
+	u.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now().Add(1 * time.Second))
+	res = u.HasRecentActivity()
+	assert.True(t, res)
+}
+
 func BenchmarkBcryptHashing(b *testing.B) {
 	bcryptPassword := "bcryptpassword"
 	for i := 0; i < b.N; i++ {

+ 10 - 7
common/protocol_test.go

@@ -837,13 +837,16 @@ func TestTruncateQuotaLimits(t *testing.T) {
 				// cleanup
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
-				user.UsedQuotaFiles = 0
-				user.UsedQuotaSize = 0
-				_, err = httpdtest.UpdateQuotaUsage(user, "reset", http.StatusOK)
-				assert.NoError(t, err)
-				user.QuotaSize = 0
-				_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
-				assert.NoError(t, err)
+				if user.Username == defaultUsername {
+					_, err = httpdtest.RemoveUser(user, http.StatusOK)
+					assert.NoError(t, err)
+					user.Password = defaultPassword
+					user.QuotaSize = 0
+					user.ID = 0
+					user.CreatedAt = 0
+					_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+					assert.NoError(t, err, string(resp))
+				}
 			}
 		}
 	}

+ 11 - 6
dataprovider/dataprovider.go

@@ -1122,9 +1122,7 @@ func UpdateAPIKeyLastUse(apiKey *APIKey) error {
 
 // UpdateLastLogin updates the last login field for the given SFTPGo user
 func UpdateLastLogin(user *User) {
-	lastLogin := util.GetTimeFromMsecSinceEpoch(user.LastLogin)
-	diff := -time.Until(lastLogin)
-	if diff < 0 || diff > lastLoginMinDelay {
+	if !isLastActivityRecent(user.LastLogin) {
 		err := provider.updateLastLogin(user.Username)
 		if err == nil {
 			webDAVUsersCache.updateLastLogin(user.Username)
@@ -1134,9 +1132,7 @@ func UpdateLastLogin(user *User) {
 
 // UpdateAdminLastLogin updates the last login field for the given SFTPGo admin
 func UpdateAdminLastLogin(admin *Admin) {
-	lastLogin := util.GetTimeFromMsecSinceEpoch(admin.LastLogin)
-	diff := -time.Until(lastLogin)
-	if diff < 0 || diff > lastLoginMinDelay {
+	if !isLastActivityRecent(admin.LastLogin) {
 		provider.updateAdminLastLogin(admin.Username) //nolint:errcheck
 	}
 }
@@ -3375,3 +3371,12 @@ func getUserAndJSONForHook(username string) (User, []byte, error) {
 func providerLog(level logger.LogLevel, format string, v ...interface{}) {
 	logger.Log(level, logSender, "", format, v...)
 }
+
+func isLastActivityRecent(lastActivity int64) bool {
+	lastActivityTime := util.GetTimeFromMsecSinceEpoch(lastActivity)
+	diff := -time.Until(lastActivityTime)
+	if diff < -10*time.Second {
+		return false
+	}
+	return diff < lastLoginMinDelay
+}

+ 47 - 9
dataprovider/user.go

@@ -161,12 +161,51 @@ func (u *User) getRootFs(connectionID string) (fs vfs.Fs, err error) {
 	}
 }
 
+func (u *User) checkDirWithParents(virtualDirPath, connectionID string) error {
+	dirs := util.GetDirsForVirtualPath(virtualDirPath)
+	for idx := len(dirs) - 1; idx >= 0; idx-- {
+		vPath := dirs[idx]
+		if vPath == "/" {
+			continue
+		}
+		fs, err := u.GetFilesystemForPath(vPath, connectionID)
+		if err != nil {
+			return fmt.Errorf("unable to get fs for path %#v: %w", vPath, err)
+		}
+		if fs.HasVirtualFolders() {
+			continue
+		}
+		fsPath, err := fs.ResolvePath(vPath)
+		if err != nil {
+			return fmt.Errorf("unable to resolve path %#v: %w", vPath, err)
+		}
+		_, err = fs.Stat(fsPath)
+		if err == nil {
+			continue
+		}
+		if fs.IsNotExist(err) {
+			err = fs.Mkdir(fsPath)
+			if err != nil {
+				return err
+			}
+			vfs.SetPathPermissions(fs, fsPath, u.GetUID(), u.GetGID())
+		} else {
+			return fmt.Errorf("unable to stat path %#v: %w", vPath, err)
+		}
+	}
+
+	return nil
+}
+
 // CheckFsRoot check the root directory for the main fs and the virtual folders.
 // It returns an error if the main filesystem cannot be created
 func (u *User) CheckFsRoot(connectionID string) error {
 	if u.Filters.DisableFsChecks {
 		return nil
 	}
+	if isLastActivityRecent(u.LastLogin) {
+		return nil
+	}
 	fs, err := u.GetFilesystemForPath("/", connectionID)
 	if err != nil {
 		logger.Warn(logSender, connectionID, "could not create main filesystem for user %#v err: %v", u.Username, err)
@@ -180,15 +219,9 @@ func (u *User) CheckFsRoot(connectionID string) error {
 			fs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())
 		}
 		// now check intermediary folders
-		fs, err = u.GetFilesystemForPath(path.Dir(v.VirtualPath), connectionID)
-		if err == nil && !fs.HasVirtualFolders() {
-			fsPath, err := fs.ResolvePath(v.VirtualPath)
-			if err != nil {
-				continue
-			}
-			err = fs.MkdirAll(fsPath, u.GetUID(), u.GetGID())
-			logger.Debug(logSender, connectionID, "create intermediary dir to %#v, path %#v, err: %v",
-				v.VirtualPath, fsPath, err)
+		err = u.checkDirWithParents(path.Dir(v.VirtualPath), connectionID)
+		if err != nil {
+			logger.Warn(logSender, connectionID, "could not create intermediary dir to %#v, err: %v", v.VirtualPath, err)
 		}
 	}
 	return nil
@@ -1071,6 +1104,11 @@ func (u *User) GetHomeDir() string {
 	return filepath.Clean(u.HomeDir)
 }
 
+// HasRecentActivity returns true if the last user login is recent and so we can skip some expensive checks
+func (u *User) HasRecentActivity() bool {
+	return isLastActivityRecent(u.LastLogin)
+}
+
 // HasQuotaRestrictions returns true if there are any disk quota restrictions
 func (u *User) HasQuotaRestrictions() bool {
 	return u.QuotaFiles > 0 || u.QuotaSize > 0

+ 72 - 7
ftpd/ftpd_test.go

@@ -1433,6 +1433,13 @@ func TestResume(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
+				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}
@@ -1579,8 +1586,14 @@ func TestQuotaLimits(t *testing.T) {
 			assert.NoError(t, err)
 			user.QuotaFiles = 0
 			user.QuotaSize = 0
-			_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
 			assert.NoError(t, err)
+			user.Password = defaultPassword
+			user.QuotaSize = 0
+			user.ID = 0
+			user.CreatedAt = 0
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -1631,9 +1644,14 @@ func TestUploadMaxSize(t *testing.T) {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
-			user.Filters.MaxUploadFileSize = 65536000
-			_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
 			assert.NoError(t, err)
+			user.Password = defaultPassword
+			user.Filters.MaxUploadFileSize = 65536000
+			user.ID = 0
+			user.CreatedAt = 0
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -1823,12 +1841,17 @@ func TestRename(t *testing.T) {
 		err = os.Remove(testFilePath)
 		assert.NoError(t, err)
 		if user.Username == defaultUsername {
-			user.Permissions = make(map[string][]string)
-			user.Permissions["/"] = allPerms
-			user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
-			assert.NoError(t, err)
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
+			assert.NoError(t, err)
+			user.Permissions = make(map[string][]string)
+			user.Permissions["/"] = allPerms
+			user.Password = defaultPassword
+			user.ID = 0
+			user.CreatedAt = 0
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -1885,6 +1908,13 @@ func TestSymlink(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
+				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 		err = os.Remove(testFilePath)
@@ -1935,6 +1965,13 @@ func TestStat(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
+				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}
@@ -2284,6 +2321,13 @@ func TestChtimes(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
+				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}
@@ -2366,6 +2410,13 @@ func TestChmod(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
+				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}
@@ -2523,6 +2574,13 @@ func TestHASH(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
+				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}
@@ -2577,6 +2635,13 @@ func TestCombine(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
+				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}

+ 9 - 9
go.mod

@@ -8,7 +8,7 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0
 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
 	github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
-	github.com/aws/aws-sdk-go v1.43.2
+	github.com/aws/aws-sdk-go v1.43.5
 	github.com/cockroachdb/cockroach-go/v2 v2.2.8
 	github.com/coreos/go-oidc/v3 v3.1.0
 	github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
@@ -26,8 +26,8 @@ require (
 	github.com/hashicorp/go-plugin v1.4.3
 	github.com/hashicorp/go-retryablehttp v0.7.0
 	github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
-	github.com/klauspost/compress v1.14.3
-	github.com/lestrrat-go/jwx v1.2.18
+	github.com/klauspost/compress v1.14.4
+	github.com/lestrrat-go/jwx v1.2.19
 	github.com/lib/pq v1.10.4
 	github.com/lithammer/shortuuid/v3 v3.0.7
 	github.com/mattn/go-sqlite3 v1.14.11
@@ -57,16 +57,16 @@ require (
 	gocloud.dev v0.24.0
 	golang.org/x/crypto v0.0.0-20220214200702-86341886e292
 	golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
-	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
-	golang.org/x/sys v0.0.0-20220209214540-3681064d5158
+	golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
+	golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7
 	golang.org/x/time v0.0.0-20220210224613-90d013bbcef8
-	google.golang.org/api v0.69.0
+	google.golang.org/api v0.70.0
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 )
 
 require (
 	cloud.google.com/go v0.100.2 // indirect
-	cloud.google.com/go/compute v1.3.0 // indirect
+	cloud.google.com/go/compute v1.5.0 // indirect
 	cloud.google.com/go/iam v0.2.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
@@ -98,7 +98,7 @@ require (
 	github.com/lestrrat-go/iter v1.0.1 // indirect
 	github.com/lestrrat-go/option v1.0.0 // indirect
 	github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
-	github.com/magiconair/properties v1.8.5 // indirect
+	github.com/magiconair/properties v1.8.6 // indirect
 	github.com/mattn/go-colorable v0.1.12 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
@@ -128,7 +128,7 @@ require (
 	golang.org/x/tools v0.1.9 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c // indirect
+	google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect
 	google.golang.org/grpc v1.44.0 // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
 	gopkg.in/ini.v1 v1.66.4 // indirect

+ 18 - 12
go.sum

@@ -47,8 +47,9 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
 cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
 cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw=
-cloud.google.com/go/compute v1.3.0 h1:mPL/MzDDYHsh5tHRS9mhmhWlcgClCrCa6ApQCU6wnHI=
 cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
+cloud.google.com/go/compute v1.5.0 h1:b1zWmYuuHz7gO9kDcM/EpHGr06UgsYNRpNJzI2kFiLM=
+cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 cloud.google.com/go/firestore v1.5.0/go.mod h1:c4nNYR1qdq7eaZ+jSc5fonrQN2k3M7sWATcYTiakjEo=
@@ -143,8 +144,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
 github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
 github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
 github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
-github.com/aws/aws-sdk-go v1.43.2 h1:T6LuKCNu8CYXXDn3xJoldh8FbdvuVH7C9aSuLNrlht0=
-github.com/aws/aws-sdk-go v1.43.2/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
+github.com/aws/aws-sdk-go v1.43.5 h1:N7arnx54E4QyW69c45UW5o8j2DCSjzpoxzJW3yU6OSo=
+github.com/aws/aws-sdk-go v1.43.5/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
 github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
 github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY=
 github.com/aws/aws-sdk-go-v2/credentials v1.4.0/go.mod h1:dgGR+Qq7Wjcd4AOAW5Rf5Tnv3+x7ed6kETXyS9WCuAY=
@@ -513,8 +514,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/klauspost/compress v1.14.3 h1:DQv1WP+iS4srNjibdnHtqu8JNWCDMluj5NzPnFJsnvk=
-github.com/klauspost/compress v1.14.3/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4=
+github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.11 h1:i2lw1Pm7Yi/4O6XCSyJWqEHI2MDw2FzUK6o/D21xn2A=
 github.com/klauspost/cpuid/v2 v2.0.11/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
@@ -543,8 +544,8 @@ github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++
 github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
 github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
 github.com/lestrrat-go/jwx v1.2.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU=
-github.com/lestrrat-go/jwx v1.2.18 h1:RV4hcTRUlPVYUnGqATKXEojoOsLexoU8Na4KheVzxQ8=
-github.com/lestrrat-go/jwx v1.2.18/go.mod h1:bWTBO7IHHVMtNunM8so9MT8wD+euEY1PzGEyCnuI2qM=
+github.com/lestrrat-go/jwx v1.2.19 h1:qxxLmAXNwZpTTvjc4PH21nT7I4wPK6lVv3lVNcZPnUk=
+github.com/lestrrat-go/jwx v1.2.19/go.mod h1:bWTBO7IHHVMtNunM8so9MT8wD+euEY1PzGEyCnuI2qM=
 github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
 github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -560,8 +561,9 @@ github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaK
 github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
 github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
 github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
-github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
+github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -854,8 +856,9 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -953,8 +956,9 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 h1:BXxu8t6QN0G1uff4bzZzSkpsax8+ALqTGUtz08QrV00=
+golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1087,8 +1091,9 @@ google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tD
 google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM=
 google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M=
 google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
-google.golang.org/api v0.69.0 h1:yHW5s2SFyDapr/43kYtIQmoaaFVW4baLMLwqV4auj2A=
 google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7C80=
+google.golang.org/api v0.70.0 h1:67zQnAE0T2rB0A3CwLSas0K+SbVzSxP+zTLkQLexeiw=
+google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1178,8 +1183,9 @@ google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ6
 google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
 google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c h1:TU4rFa5APdKTq0s6B7WTsH6Xmx0Knj86s6Biz56mErE=
 google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf h1:SVYXkUz2yZS9FWb2Gm8ivSlbNQzL2Z/NpPKE3RG2jWk=
+google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
 google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=

+ 5 - 10
httpd/middleware.go

@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"net/http"
 	"strings"
-	"time"
 
 	"github.com/go-chi/jwtauth/v5"
 	"github.com/lestrrat-go/jwx/jwt"
@@ -406,15 +405,11 @@ func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAu
 		updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
 		return err
 	}
-	lastLogin := util.GetTimeFromMsecSinceEpoch(user.LastLogin)
-	diff := -time.Until(lastLogin)
-	if diff < 0 || diff > 10*time.Minute {
-		defer user.CloseFs() //nolint:errcheck
-		err = user.CheckFsRoot(connectionID)
-		if err != nil {
-			updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
-			return common.ErrInternalFailure
-		}
+	defer user.CloseFs() //nolint:errcheck
+	err = user.CheckFsRoot(connectionID)
+	if err != nil {
+		updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
+		return common.ErrInternalFailure
 	}
 	c := jwtTokenClaims{
 		Username:    user.Username,

+ 94 - 23
sftpd/sftpd_test.go

@@ -762,6 +762,13 @@ func TestOpenReadWritePerm(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
+				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}
@@ -1063,6 +1070,13 @@ func TestUploadResume(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
+				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}
@@ -1290,6 +1304,13 @@ func TestStat(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
+				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}
@@ -1340,6 +1361,13 @@ func TestStatChownChmod(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
+				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}
@@ -1384,11 +1412,13 @@ func TestSFTPFsLoginWrongFingerprint(t *testing.T) {
 	sftpUser.FsConfig.SFTPConfig.Fingerprints = []string{"wrong"}
 	_, _, err = httpdtest.UpdateUser(sftpUser, http.StatusOK, "")
 	assert.NoError(t, err)
-	_, _, err = getSftpClient(sftpUser, usePubKey)
-	assert.Error(t, err)
-
-	_, err = runSSHCommand("md5sum", sftpUser, usePubKey)
-	assert.Error(t, err)
+	conn, client, err = getSftpClient(sftpUser, usePubKey)
+	if assert.NoError(t, err) {
+		defer conn.Close()
+		defer client.Close()
+		err = checkBasicSFTP(client)
+		assert.Error(t, err)
+	}
 
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
 	assert.NoError(t, err)
@@ -1438,6 +1468,13 @@ func TestChtimes(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
+				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}
@@ -3816,13 +3853,14 @@ func TestQuotaFileReplace(t *testing.T) {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
-			user.UsedQuotaFiles = 0
-			user.UsedQuotaSize = 0
-			_, err = httpdtest.UpdateQuotaUsage(user, "reset", http.StatusOK)
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
 			assert.NoError(t, err)
+			user.Password = defaultPassword
+			user.ID = 0
+			user.CreatedAt = 0
 			user.QuotaSize = 0
-			_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
-			assert.NoError(t, err)
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -3914,10 +3952,13 @@ func TestQuotaRename(t *testing.T) {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
-				user.UsedQuotaFiles = 0
-				user.UsedQuotaSize = 0
-				_, err = httpdtest.UpdateQuotaUsage(user, "reset", http.StatusOK)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
 				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}
@@ -4078,10 +4119,13 @@ func TestQuotaLimits(t *testing.T) {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
-			user.UsedQuotaFiles = 0
-			user.UsedQuotaSize = 0
-			_, err = httpdtest.UpdateQuotaUsage(user, "reset", http.StatusOK)
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
 			assert.NoError(t, err)
+			user.Password = defaultPassword
+			user.ID = 0
+			user.CreatedAt = 0
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 	err = os.Remove(testFilePath)
@@ -5078,13 +5122,14 @@ func TestTruncateQuotaLimits(t *testing.T) {
 				// cleanup
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
-				user.UsedQuotaFiles = 0
-				user.UsedQuotaSize = 0
-				_, err = httpdtest.UpdateQuotaUsage(user, "reset", http.StatusOK)
+				_, err = httpdtest.RemoveUser(user, http.StatusOK)
 				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
 				user.QuotaSize = 0
-				_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
-				assert.NoError(t, err)
+				_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+				assert.NoError(t, err, string(resp))
 			}
 		}
 	}
@@ -7323,10 +7368,15 @@ func TestRootDirCommands(t *testing.T) {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
+			assert.NoError(t, err)
+			user.Password = defaultPassword
+			user.ID = 0
+			user.CreatedAt = 0
 			user.Permissions = make(map[string][]string)
 			user.Permissions["/"] = []string{dataprovider.PermAny}
-			_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
-			assert.NoError(t, err)
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -8900,6 +8950,13 @@ func TestSCPBasicHandling(t *testing.T) {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
+			assert.NoError(t, err)
+			user.Password = defaultPassword
+			user.ID = 0
+			user.CreatedAt = 0
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 
@@ -8975,6 +9032,13 @@ func TestSCPUploadFileOverwrite(t *testing.T) {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
+			assert.NoError(t, err)
+			user.Password = defaultPassword
+			user.ID = 0
+			user.CreatedAt = 0
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 	err = os.Remove(testFilePath)
@@ -9043,6 +9107,13 @@ func TestSCPRecursive(t *testing.T) {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
+			assert.NoError(t, err)
+			user.Password = defaultPassword
+			user.ID = 0
+			user.CreatedAt = 0
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 

+ 0 - 5
vfs/azblobfs.go

@@ -353,11 +353,6 @@ func (fs *AzureBlobFs) Mkdir(name string) error {
 	return w.Close()
 }
 
-// MkdirAll does nothing, we don't have folder
-func (*AzureBlobFs) MkdirAll(name string, uid int, gid int) error {
-	return nil
-}
-
 // Symlink creates source as a symbolic link to target.
 func (*AzureBlobFs) Symlink(source, target string) error {
 	return ErrVfsUnsupported

+ 0 - 5
vfs/gcsfs.go

@@ -317,11 +317,6 @@ func (fs *GCSFs) Mkdir(name string) error {
 	return w.Close()
 }
 
-// MkdirAll does nothing, we don't have folder
-func (*GCSFs) MkdirAll(name string, uid int, gid int) error {
-	return nil
-}
-
 // Symlink creates source as a symbolic link to target.
 func (*GCSFs) Symlink(source, target string) error {
 	return ErrVfsUnsupported

+ 0 - 24
vfs/osfs.go

@@ -128,13 +128,6 @@ func (*OsFs) Mkdir(name string) error {
 	return os.Mkdir(name, os.ModePerm)
 }
 
-// MkdirAll creates a directory named path, along with any necessary parents,
-// and returns nil, or else returns an error.
-// If path is already a directory, MkdirAll does nothing and returns nil.
-func (fs *OsFs) MkdirAll(name string, uid int, gid int) error {
-	return fs.createMissingDirs(name, uid, gid)
-}
-
 // Symlink creates source as a symbolic link to target.
 func (*OsFs) Symlink(source, target string) error {
 	return os.Symlink(source, target)
@@ -418,23 +411,6 @@ func (fs *OsFs) isSubDir(sub string) error {
 	return nil
 }
 
-func (fs *OsFs) createMissingDirs(filePath string, uid, gid int) error {
-	dirsToCreate, err := fs.findNonexistentDirs(filePath)
-	if err != nil {
-		return err
-	}
-	last := len(dirsToCreate) - 1
-	for i := range dirsToCreate {
-		d := dirsToCreate[last-i]
-		if err := os.Mkdir(d, os.ModePerm); err != nil {
-			fsLog(fs, logger.LevelError, "error creating missing dir: %#v", d)
-			return err
-		}
-		SetPathPermissions(fs, d, uid, gid)
-	}
-	return nil
-}
-
 // GetMimeType returns the content type
 func (fs *OsFs) GetMimeType(name string) (string, error) {
 	f, err := os.OpenFile(name, os.O_RDONLY, 0)

+ 24 - 21
vfs/s3fs.go

@@ -90,22 +90,7 @@ func NewS3Fs(connectionID, localTempDir, mountPath string, config S3FsConfig) (F
 	if fs.config.ForcePathStyle {
 		awsConfig.S3ForcePathStyle = aws.Bool(true)
 	}
-	if fs.config.UploadPartSize == 0 {
-		fs.config.UploadPartSize = s3manager.DefaultUploadPartSize
-	} else {
-		fs.config.UploadPartSize *= 1024 * 1024
-	}
-	if fs.config.UploadConcurrency == 0 {
-		fs.config.UploadConcurrency = s3manager.DefaultUploadConcurrency
-	}
-	if fs.config.DownloadPartSize == 0 {
-		fs.config.DownloadPartSize = s3manager.DefaultDownloadPartSize
-	} else {
-		fs.config.DownloadPartSize *= 1024 * 1024
-	}
-	if fs.config.DownloadConcurrency == 0 {
-		fs.config.DownloadConcurrency = s3manager.DefaultDownloadConcurrency
-	}
+	fs.setConfigDefaults()
 
 	sessOpts := session.Options{
 		Config:            *awsConfig,
@@ -392,11 +377,6 @@ func (fs *S3Fs) Mkdir(name string) error {
 	return w.Close()
 }
 
-// MkdirAll does nothing, we don't have folder
-func (*S3Fs) MkdirAll(name string, uid int, gid int) error {
-	return nil
-}
-
 // Symlink creates source as a symbolic link to target.
 func (*S3Fs) Symlink(source, target string) error {
 	return ErrVfsUnsupported
@@ -742,6 +722,29 @@ func (fs *S3Fs) checkIfBucketExists() error {
 	return err
 }
 
+func (fs *S3Fs) setConfigDefaults() {
+	if fs.config.UploadPartSize == 0 {
+		fs.config.UploadPartSize = s3manager.DefaultUploadPartSize
+	} else {
+		if fs.config.UploadPartSize < 1024*1024 {
+			fs.config.UploadPartSize *= 1024 * 1024
+		}
+	}
+	if fs.config.UploadConcurrency == 0 {
+		fs.config.UploadConcurrency = s3manager.DefaultUploadConcurrency
+	}
+	if fs.config.DownloadPartSize == 0 {
+		fs.config.DownloadPartSize = s3manager.DefaultDownloadPartSize
+	} else {
+		if fs.config.DownloadPartSize < 1024*1024 {
+			fs.config.DownloadPartSize *= 1024 * 1024
+		}
+	}
+	if fs.config.DownloadConcurrency == 0 {
+		fs.config.DownloadConcurrency = s3manager.DefaultDownloadConcurrency
+	}
+}
+
 func (fs *S3Fs) hasContents(name string) (bool, error) {
 	prefix := ""
 	if name != "/" && name != "." {

+ 4 - 11
vfs/sftpfs.go

@@ -349,16 +349,6 @@ func (fs *SFTPFs) Mkdir(name string) error {
 	return fs.sftpClient.Mkdir(name)
 }
 
-// MkdirAll creates a directory named path, along with any necessary parents,
-// and returns nil, or else returns an error.
-// If path is already a directory, MkdirAll does nothing and returns nil.
-func (fs *SFTPFs) MkdirAll(name string, uid int, gid int) error {
-	if err := fs.checkConnection(); err != nil {
-		return err
-	}
-	return fs.sftpClient.MkdirAll(name)
-}
-
 // Symlink creates source as a symbolic link to target.
 func (fs *SFTPFs) Symlink(source, target string) error {
 	if err := fs.checkConnection(); err != nil {
@@ -459,7 +449,10 @@ func (fs *SFTPFs) CheckRootPath(username string, uid int, gid int) bool {
 	if fs.config.Prefix == "/" {
 		return true
 	}
-	if err := fs.MkdirAll(fs.config.Prefix, uid, gid); err != nil {
+	if err := fs.checkConnection(); err != nil {
+		return false
+	}
+	if err := fs.sftpClient.MkdirAll(fs.config.Prefix); err != nil {
 		fsLog(fs, logger.LevelDebug, "error creating root directory %#v for user %#v: %v", fs.config.Prefix, username, err)
 		return false
 	}

+ 0 - 1
vfs/vfs.go

@@ -73,7 +73,6 @@ type Fs interface {
 	Rename(source, target string) error
 	Remove(name string, isDir bool) error
 	Mkdir(name string) error
-	MkdirAll(name string, uid int, gid int) error
 	Symlink(source, target string) error
 	Chown(name string, uid int, gid int) error
 	Chmod(name string, mode os.FileMode) error

+ 28 - 6
webdavd/webdavd_test.go

@@ -576,6 +576,13 @@ func TestBasicHandling(t *testing.T) {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
+			assert.NoError(t, err)
+			user.Password = defaultPassword
+			user.ID = 0
+			user.CreatedAt = 0
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -1484,10 +1491,15 @@ func TestQuotaLimits(t *testing.T) {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
+			assert.NoError(t, err)
+			user.Password = defaultPassword
+			user.ID = 0
+			user.CreatedAt = 0
 			user.QuotaFiles = 0
 			user.QuotaSize = 0
-			_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
-			assert.NoError(t, err)
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -1581,9 +1593,14 @@ func TestUploadMaxSize(t *testing.T) {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
-			user.Filters.MaxUploadFileSize = 65536000
-			_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
 			assert.NoError(t, err)
+			user.Filters.MaxUploadFileSize = 65536000
+			user.Password = defaultPassword
+			user.ID = 0
+			user.CreatedAt = 0
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -2195,9 +2212,14 @@ func TestMiscCommands(t *testing.T) {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
-			user.QuotaFiles = 0
-			_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
 			assert.NoError(t, err)
+			user.Password = defaultPassword
+			user.ID = 0
+			user.CreatedAt = 0
+			user.QuotaFiles = 0
+			_, resp, err := httpdtest.AddUser(user, http.StatusCreated)
+			assert.NoError(t, err, string(resp))
 		}
 	}
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)