Browse Source

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 years ago
parent
commit
f5a0559be6
16 changed files with 355 additions and 156 deletions
  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)
 	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) {
 func BenchmarkBcryptHashing(b *testing.B) {
 	bcryptPassword := "bcryptpassword"
 	bcryptPassword := "bcryptpassword"
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {

+ 10 - 7
common/protocol_test.go

@@ -837,13 +837,16 @@ func TestTruncateQuotaLimits(t *testing.T) {
 				// cleanup
 				// cleanup
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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
 // UpdateLastLogin updates the last login field for the given SFTPGo user
 func UpdateLastLogin(user *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)
 		err := provider.updateLastLogin(user.Username)
 		if err == nil {
 		if err == nil {
 			webDAVUsersCache.updateLastLogin(user.Username)
 			webDAVUsersCache.updateLastLogin(user.Username)
@@ -1134,9 +1132,7 @@ func UpdateLastLogin(user *User) {
 
 
 // UpdateAdminLastLogin updates the last login field for the given SFTPGo admin
 // UpdateAdminLastLogin updates the last login field for the given SFTPGo admin
 func UpdateAdminLastLogin(admin *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
 		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{}) {
 func providerLog(level logger.LogLevel, format string, v ...interface{}) {
 	logger.Log(level, logSender, "", format, v...)
 	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.
 // CheckFsRoot check the root directory for the main fs and the virtual folders.
 // It returns an error if the main filesystem cannot be created
 // It returns an error if the main filesystem cannot be created
 func (u *User) CheckFsRoot(connectionID string) error {
 func (u *User) CheckFsRoot(connectionID string) error {
 	if u.Filters.DisableFsChecks {
 	if u.Filters.DisableFsChecks {
 		return nil
 		return nil
 	}
 	}
+	if isLastActivityRecent(u.LastLogin) {
+		return nil
+	}
 	fs, err := u.GetFilesystemForPath("/", connectionID)
 	fs, err := u.GetFilesystemForPath("/", connectionID)
 	if err != nil {
 	if err != nil {
 		logger.Warn(logSender, connectionID, "could not create main filesystem for user %#v err: %v", u.Username, err)
 		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())
 			fs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())
 		}
 		}
 		// now check intermediary folders
 		// 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
 	return nil
@@ -1071,6 +1104,11 @@ func (u *User) GetHomeDir() string {
 	return filepath.Clean(u.HomeDir)
 	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
 // HasQuotaRestrictions returns true if there are any disk quota restrictions
 func (u *User) HasQuotaRestrictions() bool {
 func (u *User) HasQuotaRestrictions() bool {
 	return u.QuotaFiles > 0 || u.QuotaSize > 0
 	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 {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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)
 			assert.NoError(t, err)
 			user.QuotaFiles = 0
 			user.QuotaFiles = 0
 			user.QuotaSize = 0
 			user.QuotaSize = 0
-			_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
 			assert.NoError(t, err)
 			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)
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -1631,9 +1644,14 @@ func TestUploadMaxSize(t *testing.T) {
 		if user.Username == defaultUsername {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
 			assert.NoError(t, err)
-			user.Filters.MaxUploadFileSize = 65536000
-			_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
 			assert.NoError(t, err)
 			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)
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -1823,12 +1841,17 @@ func TestRename(t *testing.T) {
 		err = os.Remove(testFilePath)
 		err = os.Remove(testFilePath)
 		assert.NoError(t, err)
 		assert.NoError(t, err)
 		if user.Username == defaultUsername {
 		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())
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
 			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)
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -1885,6 +1908,13 @@ func TestSymlink(t *testing.T) {
 			if user.Username == defaultUsername {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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)
 		err = os.Remove(testFilePath)
@@ -1935,6 +1965,13 @@ func TestStat(t *testing.T) {
 			if user.Username == defaultUsername {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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 {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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 {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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 {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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 {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0
 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
 	github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
 	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/cockroachdb/cockroach-go/v2 v2.2.8
 	github.com/coreos/go-oidc/v3 v3.1.0
 	github.com/coreos/go-oidc/v3 v3.1.0
 	github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
 	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-plugin v1.4.3
 	github.com/hashicorp/go-retryablehttp v0.7.0
 	github.com/hashicorp/go-retryablehttp v0.7.0
 	github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
 	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/lib/pq v1.10.4
 	github.com/lithammer/shortuuid/v3 v3.0.7
 	github.com/lithammer/shortuuid/v3 v3.0.7
 	github.com/mattn/go-sqlite3 v1.14.11
 	github.com/mattn/go-sqlite3 v1.14.11
@@ -57,16 +57,16 @@ require (
 	gocloud.dev v0.24.0
 	gocloud.dev v0.24.0
 	golang.org/x/crypto v0.0.0-20220214200702-86341886e292
 	golang.org/x/crypto v0.0.0-20220214200702-86341886e292
 	golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
 	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
 	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
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 )
 )
 
 
 require (
 require (
 	cloud.google.com/go v0.100.2 // indirect
 	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
 	cloud.google.com/go/iam v0.2.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
 	github.com/beorn7/perks v1.0.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/iter v1.0.1 // indirect
 	github.com/lestrrat-go/option v1.0.0 // indirect
 	github.com/lestrrat-go/option v1.0.0 // indirect
 	github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // 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-colorable v0.1.12 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // 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/tools v0.1.9 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	google.golang.org/appengine v1.6.7 // 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/grpc v1.44.0 // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
 	gopkg.in/ini.v1 v1.66.4 // 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/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 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.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.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.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/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 cloud.google.com/go/firestore v1.5.0/go.mod h1:c4nNYR1qdq7eaZ+jSc5fonrQN2k3M7sWATcYTiakjEo=
 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.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.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.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 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/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=
 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/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.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.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.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 h1:i2lw1Pm7Yi/4O6XCSyJWqEHI2MDw2FzUK6o/D21xn2A=
 github.com/klauspost/cpuid/v2 v2.0.11/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
 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 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
 github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
 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.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 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
 github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
 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=
 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 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
 github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
 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/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.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.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.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 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-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-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-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-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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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-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-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-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-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-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 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 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.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM=
 google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M=
 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.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.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.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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.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-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-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-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-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.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=

+ 5 - 10
httpd/middleware.go

@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
 	"strings"
 	"strings"
-	"time"
 
 
 	"github.com/go-chi/jwtauth/v5"
 	"github.com/go-chi/jwtauth/v5"
 	"github.com/lestrrat-go/jwx/jwt"
 	"github.com/lestrrat-go/jwx/jwt"
@@ -406,15 +405,11 @@ func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAu
 		updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
 		updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
 		return 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{
 	c := jwtTokenClaims{
 		Username:    user.Username,
 		Username:    user.Username,

+ 94 - 23
sftpd/sftpd_test.go

@@ -762,6 +762,13 @@ func TestOpenReadWritePerm(t *testing.T) {
 			if user.Username == defaultUsername {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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 {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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 {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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 {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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"}
 	sftpUser.FsConfig.SFTPConfig.Fingerprints = []string{"wrong"}
 	_, _, err = httpdtest.UpdateUser(sftpUser, http.StatusOK, "")
 	_, _, err = httpdtest.UpdateUser(sftpUser, http.StatusOK, "")
 	assert.NoError(t, err)
 	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)
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
@@ -1438,6 +1468,13 @@ func TestChtimes(t *testing.T) {
 			if user.Username == defaultUsername {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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 {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
 			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)
 			assert.NoError(t, err)
+			user.Password = defaultPassword
+			user.ID = 0
+			user.CreatedAt = 0
 			user.QuotaSize = 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)
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -3914,10 +3952,13 @@ func TestQuotaRename(t *testing.T) {
 			if user.Username == defaultUsername {
 			if user.Username == defaultUsername {
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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)
 				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 {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
 			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)
 			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)
 	err = os.Remove(testFilePath)
@@ -5078,13 +5122,14 @@ func TestTruncateQuotaLimits(t *testing.T) {
 				// cleanup
 				// cleanup
 				err = os.RemoveAll(user.GetHomeDir())
 				err = os.RemoveAll(user.GetHomeDir())
 				assert.NoError(t, err)
 				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)
 				assert.NoError(t, err)
+				user.Password = defaultPassword
+				user.ID = 0
+				user.CreatedAt = 0
 				user.QuotaSize = 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 {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
 			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 = make(map[string][]string)
 			user.Permissions["/"] = []string{dataprovider.PermAny}
 			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)
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -8900,6 +8950,13 @@ func TestSCPBasicHandling(t *testing.T) {
 		if user.Username == defaultUsername {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
 			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 {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
 			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)
 	err = os.Remove(testFilePath)
@@ -9043,6 +9107,13 @@ func TestSCPRecursive(t *testing.T) {
 		if user.Username == defaultUsername {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
 			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()
 	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.
 // Symlink creates source as a symbolic link to target.
 func (*AzureBlobFs) Symlink(source, target string) error {
 func (*AzureBlobFs) Symlink(source, target string) error {
 	return ErrVfsUnsupported
 	return ErrVfsUnsupported

+ 0 - 5
vfs/gcsfs.go

@@ -317,11 +317,6 @@ func (fs *GCSFs) Mkdir(name string) error {
 	return w.Close()
 	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.
 // Symlink creates source as a symbolic link to target.
 func (*GCSFs) Symlink(source, target string) error {
 func (*GCSFs) Symlink(source, target string) error {
 	return ErrVfsUnsupported
 	return ErrVfsUnsupported

+ 0 - 24
vfs/osfs.go

@@ -128,13 +128,6 @@ func (*OsFs) Mkdir(name string) error {
 	return os.Mkdir(name, os.ModePerm)
 	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.
 // Symlink creates source as a symbolic link to target.
 func (*OsFs) Symlink(source, target string) error {
 func (*OsFs) Symlink(source, target string) error {
 	return os.Symlink(source, target)
 	return os.Symlink(source, target)
@@ -418,23 +411,6 @@ func (fs *OsFs) isSubDir(sub string) error {
 	return nil
 	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
 // GetMimeType returns the content type
 func (fs *OsFs) GetMimeType(name string) (string, error) {
 func (fs *OsFs) GetMimeType(name string) (string, error) {
 	f, err := os.OpenFile(name, os.O_RDONLY, 0)
 	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 {
 	if fs.config.ForcePathStyle {
 		awsConfig.S3ForcePathStyle = aws.Bool(true)
 		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{
 	sessOpts := session.Options{
 		Config:            *awsConfig,
 		Config:            *awsConfig,
@@ -392,11 +377,6 @@ func (fs *S3Fs) Mkdir(name string) error {
 	return w.Close()
 	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.
 // Symlink creates source as a symbolic link to target.
 func (*S3Fs) Symlink(source, target string) error {
 func (*S3Fs) Symlink(source, target string) error {
 	return ErrVfsUnsupported
 	return ErrVfsUnsupported
@@ -742,6 +722,29 @@ func (fs *S3Fs) checkIfBucketExists() error {
 	return err
 	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) {
 func (fs *S3Fs) hasContents(name string) (bool, error) {
 	prefix := ""
 	prefix := ""
 	if name != "/" && name != "." {
 	if name != "/" && name != "." {

+ 4 - 11
vfs/sftpfs.go

@@ -349,16 +349,6 @@ func (fs *SFTPFs) Mkdir(name string) error {
 	return fs.sftpClient.Mkdir(name)
 	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.
 // Symlink creates source as a symbolic link to target.
 func (fs *SFTPFs) Symlink(source, target string) error {
 func (fs *SFTPFs) Symlink(source, target string) error {
 	if err := fs.checkConnection(); err != nil {
 	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 == "/" {
 	if fs.config.Prefix == "/" {
 		return true
 		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)
 		fsLog(fs, logger.LevelDebug, "error creating root directory %#v for user %#v: %v", fs.config.Prefix, username, err)
 		return false
 		return false
 	}
 	}

+ 0 - 1
vfs/vfs.go

@@ -73,7 +73,6 @@ type Fs interface {
 	Rename(source, target string) error
 	Rename(source, target string) error
 	Remove(name string, isDir bool) error
 	Remove(name string, isDir bool) error
 	Mkdir(name string) error
 	Mkdir(name string) error
-	MkdirAll(name string, uid int, gid int) error
 	Symlink(source, target string) error
 	Symlink(source, target string) error
 	Chown(name string, uid int, gid int) error
 	Chown(name string, uid int, gid int) error
 	Chmod(name string, mode os.FileMode) 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 {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
 			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)
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -1484,10 +1491,15 @@ func TestQuotaLimits(t *testing.T) {
 		if user.Username == defaultUsername {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
 			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.QuotaFiles = 0
 			user.QuotaSize = 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)
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -1581,9 +1593,14 @@ func TestUploadMaxSize(t *testing.T) {
 		if user.Username == defaultUsername {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
 			assert.NoError(t, err)
-			user.Filters.MaxUploadFileSize = 65536000
-			_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
 			assert.NoError(t, err)
 			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)
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
@@ -2195,9 +2212,14 @@ func TestMiscCommands(t *testing.T) {
 		if user.Username == defaultUsername {
 		if user.Username == defaultUsername {
 			err = os.RemoveAll(user.GetHomeDir())
 			err = os.RemoveAll(user.GetHomeDir())
 			assert.NoError(t, err)
 			assert.NoError(t, err)
-			user.QuotaFiles = 0
-			_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
+			_, err = httpdtest.RemoveUser(user, http.StatusOK)
 			assert.NoError(t, err)
 			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)
 	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)