Browse Source

add a specific permission to manage folders

creating/updating folders embedded in users is no longer supported.

Fixes #1349

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 2 years ago
parent
commit
0413c0471c

+ 9 - 10
go.mod

@@ -23,9 +23,9 @@ require (
 	github.com/coreos/go-oidc/v3 v3.6.0
 	github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8
 	github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
-	github.com/fclairamb/ftpserverlib v0.21.0
+	github.com/fclairamb/ftpserverlib v0.21.1-0.20230719102702-76e3b6785cda
 	github.com/fclairamb/go-log v0.4.1
-	github.com/go-acme/lego/v4 v4.12.3
+	github.com/go-acme/lego/v4 v4.13.2
 	github.com/go-chi/chi/v5 v5.0.10
 	github.com/go-chi/jwtauth/v5 v5.1.1
 	github.com/go-chi/render v1.0.3
@@ -66,21 +66,21 @@ require (
 	github.com/wneessen/go-mail v0.4.0
 	github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
 	go.etcd.io/bbolt v1.3.7
-	go.uber.org/automaxprocs v1.5.2
-	gocloud.dev v0.30.0
+	go.uber.org/automaxprocs v1.5.3
+	gocloud.dev v0.32.0
 	golang.org/x/crypto v0.11.0
 	golang.org/x/net v0.12.0
 	golang.org/x/oauth2 v0.10.0
 	golang.org/x/sys v0.10.0
 	golang.org/x/term v0.10.0
 	golang.org/x/time v0.3.0
-	google.golang.org/api v0.131.0
+	google.golang.org/api v0.132.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 )
 
 require (
 	cloud.google.com/go v0.110.6 // indirect
-	cloud.google.com/go/compute v1.21.0 // indirect
+	cloud.google.com/go/compute v1.22.0 // indirect
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
 	cloud.google.com/go/iam v1.1.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
@@ -161,9 +161,9 @@ require (
 	golang.org/x/tools v0.11.0 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
+	google.golang.org/genproto v0.0.0-20230720185612-659f7aaaa771 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20230720185612-659f7aaaa771 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20230720185612-659f7aaaa771 // indirect
 	google.golang.org/grpc v1.56.2 // indirect
 	google.golang.org/protobuf v1.31.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
@@ -171,7 +171,6 @@ require (
 )
 
 replace (
-	github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20230714144823-d8aff325a796
 	github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
 	github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
 	golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20230614155948-29e7be6c0fab

File diff suppressed because it is too large
+ 9 - 1173
go.sum


+ 405 - 210
internal/common/protocol_test.go

@@ -1062,33 +1062,42 @@ func TestFileNotAllowedErrors(t *testing.T) {
 }
 
 func TestRootDirVirtualFolder(t *testing.T) {
+	mappedPath1 := filepath.Join(os.TempDir(), "mapped1")
+	f1 := vfs.BaseVirtualFolder{
+		Name:       filepath.Base(mappedPath1),
+		MappedPath: mappedPath1,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret("cryptsecret"),
+			},
+		},
+	}
+	mappedPath2 := filepath.Join(os.TempDir(), "mapped2")
+	f2 := vfs.BaseVirtualFolder{
+		Name:       filepath.Base(mappedPath2),
+		MappedPath: mappedPath2,
+	}
+	folder1, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	folder2, _, err := httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+
 	u := getTestUser()
 	u.QuotaFiles = 1000
 	u.UploadDataTransfer = 1000
 	u.DownloadDataTransfer = 5000
-	mappedPath1 := filepath.Join(os.TempDir(), "mapped1")
-	folderName1 := filepath.Base(mappedPath1)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					Passphrase: kms.NewPlainSecret("cryptsecret"),
-				},
-			},
+			Name: folder1.Name,
 		},
 		VirtualPath: "/",
 		QuotaFiles:  1000,
 	})
-	mappedPath2 := filepath.Join(os.TempDir(), "mapped2")
-	folderName2 := filepath.Base(mappedPath2)
 	vdirPath2 := "/vmapped"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folder2.Name,
 		},
 		VirtualPath: vdirPath2,
 		QuotaFiles:  -1,
@@ -1123,7 +1132,7 @@ func TestRootDirVirtualFolder(t *testing.T) {
 		user, _, err := httpdtest.GetUserByUsername(user.Username, http.StatusOK)
 		assert.NoError(t, err)
 		assert.Equal(t, 0, user.UsedQuotaFiles)
-		folder, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
+		folder, _, err := httpdtest.GetFolderByName(folder1.Name, http.StatusOK)
 		assert.NoError(t, err)
 		assert.Equal(t, 1, folder.UsedQuotaFiles)
 
@@ -1137,7 +1146,7 @@ func TestRootDirVirtualFolder(t *testing.T) {
 		user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
 		assert.NoError(t, err)
 		assert.Equal(t, 1, user.UsedQuotaFiles)
-		folder, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
+		folder, _, err = httpdtest.GetFolderByName(folder1.Name, http.StatusOK)
 		assert.NoError(t, err)
 		assert.Equal(t, 1, folder.UsedQuotaFiles)
 
@@ -1155,39 +1164,47 @@ func TestRootDirVirtualFolder(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.RemoveAll(user.GetHomeDir())
 	assert.NoError(t, err)
-	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)
+	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folder1.Name}, http.StatusOK)
 	assert.NoError(t, err)
 	err = os.RemoveAll(mappedPath1)
 	assert.NoError(t, err)
-	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)
+	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folder2.Name}, http.StatusOK)
 	assert.NoError(t, err)
 	err = os.RemoveAll(mappedPath2)
 	assert.NoError(t, err)
 }
 
 func TestTruncateQuotaLimits(t *testing.T) {
+	mappedPath1 := filepath.Join(os.TempDir(), "mapped1")
+	f1 := vfs.BaseVirtualFolder{
+		Name:       filepath.Base(mappedPath1),
+		MappedPath: mappedPath1,
+	}
+	folder1, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	mappedPath2 := filepath.Join(os.TempDir(), "mapped2")
+	f2 := vfs.BaseVirtualFolder{
+		Name:       filepath.Base(mappedPath2),
+		MappedPath: mappedPath2,
+	}
+	folder2, _, err := httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	u := getTestUser()
 	u.QuotaSize = 20
 	u.UploadDataTransfer = 1000
 	u.DownloadDataTransfer = 5000
-	mappedPath1 := filepath.Join(os.TempDir(), "mapped1")
-	folderName1 := filepath.Base(mappedPath1)
 	vdirPath1 := "/vmapped1"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folder1.Name,
 		},
 		VirtualPath: vdirPath1,
 		QuotaFiles:  10,
 	})
-	mappedPath2 := filepath.Join(os.TempDir(), "mapped2")
-	folderName2 := filepath.Base(mappedPath2)
 	vdirPath2 := "/vmapped2"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folder2.Name,
 		},
 		VirtualPath: vdirPath2,
 		QuotaFiles:  -1,
@@ -1331,21 +1348,21 @@ func TestTruncateQuotaLimits(t *testing.T) {
 					assert.NoError(t, err)
 					expectedQuotaFiles := 0
 					expectedQuotaSize := int64(2)
-					fold, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
+					fold, _, err := httpdtest.GetFolderByName(folder1.Name, http.StatusOK)
 					assert.NoError(t, err)
 					assert.Equal(t, expectedQuotaSize, fold.UsedQuotaSize)
 					assert.Equal(t, expectedQuotaFiles, fold.UsedQuotaFiles)
 					err = f.Close()
 					assert.NoError(t, err)
 					expectedQuotaFiles = 1
-					fold, _, err = httpdtest.GetFolderByName(folderName1, http.StatusOK)
+					fold, _, err = httpdtest.GetFolderByName(folder1.Name, http.StatusOK)
 					assert.NoError(t, err)
 					assert.Equal(t, expectedQuotaSize, fold.UsedQuotaSize)
 					assert.Equal(t, expectedQuotaFiles, fold.UsedQuotaFiles)
 				}
 				err = client.Truncate(vfileName1, 1)
 				assert.NoError(t, err)
-				fold, _, err := httpdtest.GetFolderByName(folderName1, http.StatusOK)
+				fold, _, err := httpdtest.GetFolderByName(folder1.Name, http.StatusOK)
 				assert.NoError(t, err)
 				assert.Equal(t, int64(1), fold.UsedQuotaSize)
 				assert.Equal(t, 1, fold.UsedQuotaFiles)
@@ -1360,14 +1377,14 @@ func TestTruncateQuotaLimits(t *testing.T) {
 					assert.NoError(t, err)
 					expectedQuotaFiles := 0
 					expectedQuotaSize := int64(3)
-					fold, _, err := httpdtest.GetFolderByName(folderName2, http.StatusOK)
+					fold, _, err := httpdtest.GetFolderByName(folder2.Name, http.StatusOK)
 					assert.NoError(t, err)
 					assert.Equal(t, expectedQuotaSize, fold.UsedQuotaSize)
 					assert.Equal(t, expectedQuotaFiles, fold.UsedQuotaFiles)
 					err = f.Close()
 					assert.NoError(t, err)
 					expectedQuotaFiles = 1
-					fold, _, err = httpdtest.GetFolderByName(folderName2, http.StatusOK)
+					fold, _, err = httpdtest.GetFolderByName(folder2.Name, http.StatusOK)
 					assert.NoError(t, err)
 					assert.Equal(t, expectedQuotaSize, fold.UsedQuotaSize)
 					assert.Equal(t, expectedQuotaFiles, fold.UsedQuotaFiles)
@@ -1399,11 +1416,11 @@ func TestTruncateQuotaLimits(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.RemoveAll(localUser.GetHomeDir())
 	assert.NoError(t, err)
-	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName1}, http.StatusOK)
+	_, err = httpdtest.RemoveFolder(folder1, http.StatusOK)
 	assert.NoError(t, err)
 	err = os.RemoveAll(mappedPath1)
 	assert.NoError(t, err)
-	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName2}, http.StatusOK)
+	_, err = httpdtest.RemoveFolder(folder2, http.StatusOK)
 	assert.NoError(t, err)
 	err = os.RemoveAll(mappedPath2)
 	assert.NoError(t, err)
@@ -1425,10 +1442,27 @@ func TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) {
 	mappedPath3 := filepath.Join(os.TempDir(), "vdir3")
 	folderName3 := filepath.Base(mappedPath3)
 	vdirPath3 := "/vdir3"
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+	f3 := vfs.BaseVirtualFolder{
+		Name:       folderName3,
+		MappedPath: mappedPath3,
+	}
+	_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		QuotaFiles:  2,
@@ -1436,8 +1470,7 @@ func TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			MappedPath: mappedPath2,
-			Name:       folderName2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		QuotaFiles:  0,
@@ -1445,8 +1478,7 @@ func TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName3,
-			MappedPath: mappedPath3,
+			Name: folderName3,
 		},
 		VirtualPath: vdirPath3,
 		QuotaFiles:  2,
@@ -1611,10 +1643,21 @@ func TestVirtualFoldersQuotaValues(t *testing.T) {
 	mappedPath2 := filepath.Join(os.TempDir(), "vdir2")
 	vdirPath2 := "/vdir2"
 	folderName2 := filepath.Base(mappedPath2)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -1623,8 +1666,7 @@ func TestVirtualFoldersQuotaValues(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		// quota is unlimited and excluded from user's one
@@ -1701,10 +1743,21 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
 	mappedPath2 := filepath.Join(os.TempDir(), "vdir2")
 	vdirPath2 := "/vdir2"
 	folderName2 := filepath.Base(mappedPath2)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -1713,8 +1766,7 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		// quota is unlimited and excluded from user's one
@@ -1881,10 +1933,21 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
 	mappedPath2 := filepath.Join(os.TempDir(), "vdir2")
 	folderName2 := filepath.Base(mappedPath2)
 	vdirPath2 := "/vdir2"
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -1893,8 +1956,7 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		// quota is unlimited and excluded from user's one
@@ -2077,10 +2139,21 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) {
 	mappedPath2 := filepath.Join(os.TempDir(), "vdir2")
 	folderName2 := filepath.Base(mappedPath2)
 	vdirPath2 := "/vdir2"
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -2089,8 +2162,7 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		// quota is unlimited and excluded from user's one
@@ -2276,10 +2348,21 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
 	mappedPath2 := filepath.Join(os.TempDir(), "vdir2")
 	folderName2 := filepath.Base(mappedPath2)
 	vdirPath2 := "/vdir2"
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -2288,8 +2371,7 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		// quota is unlimited and excluded from user's one
@@ -2568,10 +2650,21 @@ func TestVirtualFoldersLink(t *testing.T) {
 	mappedPath2 := filepath.Join(os.TempDir(), "vdir2")
 	folderName2 := filepath.Base(mappedPath2)
 	vdirPath2 := "/vdir2"
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -2580,8 +2673,7 @@ func TestVirtualFoldersLink(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		// quota is unlimited and excluded from user's one
@@ -2687,18 +2779,116 @@ func TestCrossFolderRename(t *testing.T) {
 	baseUser, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
 	assert.NoError(t, err, string(resp))
 
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folder1,
+		MappedPath: filepath.Join(os.TempDir(), folder1),
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folder2,
+		MappedPath: filepath.Join(os.TempDir(), folder2),
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+	f3 := vfs.BaseVirtualFolder{
+		Name:       folder3,
+		MappedPath: filepath.Join(os.TempDir(), folder3),
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret(defaultPassword + "mod"),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)
+	assert.NoError(t, err)
+	f4 := vfs.BaseVirtualFolder{
+		Name:       folder4,
+		MappedPath: filepath.Join(os.TempDir(), folder4),
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.SFTPFilesystemProvider,
+			SFTPConfig: vfs.SFTPFsConfig{
+				BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+					Endpoint: sftpServerAddr,
+					Username: baseUser.Username,
+					Prefix:   path.Join("/", folder4),
+				},
+				Password: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f4, http.StatusCreated)
+	assert.NoError(t, err)
+	f5 := vfs.BaseVirtualFolder{
+		Name:       folder5,
+		MappedPath: filepath.Join(os.TempDir(), folder5),
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.SFTPFilesystemProvider,
+			SFTPConfig: vfs.SFTPFsConfig{
+				BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+					Endpoint: sftpServerAddr,
+					Username: baseUser.Username,
+					Prefix:   path.Join("/", folder5),
+				},
+				Password: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f5, http.StatusCreated)
+	assert.NoError(t, err)
+	f6 := vfs.BaseVirtualFolder{
+		Name:       folder6,
+		MappedPath: filepath.Join(os.TempDir(), folder6),
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.SFTPFilesystemProvider,
+			SFTPConfig: vfs.SFTPFsConfig{
+				BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+					Endpoint: "127.0.0.1:4024",
+					Username: baseUser.Username,
+					Prefix:   path.Join("/", folder6),
+				},
+				Password: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f6, http.StatusCreated)
+	assert.NoError(t, err)
+	f7 := vfs.BaseVirtualFolder{
+		Name:       folder7,
+		MappedPath: filepath.Join(os.TempDir(), folder7),
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.SFTPFilesystemProvider,
+			SFTPConfig: vfs.SFTPFsConfig{
+				BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+					Endpoint: sftpServerAddr,
+					Username: baseUser.Username,
+					Prefix:   path.Join("/", folder4),
+				},
+				Password: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f7, http.StatusCreated)
+	assert.NoError(t, err)
+
 	u := getCryptFsUser()
 	u.VirtualFolders = []vfs.VirtualFolder{
 		{
 			BaseVirtualFolder: vfs.BaseVirtualFolder{
-				Name:       folder1,
-				MappedPath: filepath.Join(os.TempDir(), folder1),
-				FsConfig: vfs.Filesystem{
-					Provider: sdk.CryptedFilesystemProvider,
-					CryptConfig: vfs.CryptFsConfig{
-						Passphrase: kms.NewPlainSecret(defaultPassword),
-					},
-				},
+				Name: folder1,
 			},
 			VirtualPath: path.Join("/", folder1),
 			QuotaSize:   -1,
@@ -2706,14 +2896,7 @@ func TestCrossFolderRename(t *testing.T) {
 		},
 		{
 			BaseVirtualFolder: vfs.BaseVirtualFolder{
-				Name:       folder2,
-				MappedPath: filepath.Join(os.TempDir(), folder2),
-				FsConfig: vfs.Filesystem{
-					Provider: sdk.CryptedFilesystemProvider,
-					CryptConfig: vfs.CryptFsConfig{
-						Passphrase: kms.NewPlainSecret(defaultPassword),
-					},
-				},
+				Name: folder2,
 			},
 			VirtualPath: path.Join("/", folder2),
 			QuotaSize:   -1,
@@ -2721,14 +2904,7 @@ func TestCrossFolderRename(t *testing.T) {
 		},
 		{
 			BaseVirtualFolder: vfs.BaseVirtualFolder{
-				Name:       folder3,
-				MappedPath: filepath.Join(os.TempDir(), folder3),
-				FsConfig: vfs.Filesystem{
-					Provider: sdk.CryptedFilesystemProvider,
-					CryptConfig: vfs.CryptFsConfig{
-						Passphrase: kms.NewPlainSecret(defaultPassword + "mod"),
-					},
-				},
+				Name: folder3,
 			},
 			VirtualPath: path.Join("/", folder3),
 			QuotaSize:   -1,
@@ -2736,19 +2912,7 @@ func TestCrossFolderRename(t *testing.T) {
 		},
 		{
 			BaseVirtualFolder: vfs.BaseVirtualFolder{
-				Name:       folder4,
-				MappedPath: filepath.Join(os.TempDir(), folder4),
-				FsConfig: vfs.Filesystem{
-					Provider: sdk.SFTPFilesystemProvider,
-					SFTPConfig: vfs.SFTPFsConfig{
-						BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
-							Endpoint: sftpServerAddr,
-							Username: baseUser.Username,
-							Prefix:   path.Join("/", folder4),
-						},
-						Password: kms.NewPlainSecret(defaultPassword),
-					},
-				},
+				Name: folder4,
 			},
 			VirtualPath: path.Join("/", folder4),
 			QuotaSize:   -1,
@@ -2756,19 +2920,7 @@ func TestCrossFolderRename(t *testing.T) {
 		},
 		{
 			BaseVirtualFolder: vfs.BaseVirtualFolder{
-				Name:       folder5,
-				MappedPath: filepath.Join(os.TempDir(), folder5),
-				FsConfig: vfs.Filesystem{
-					Provider: sdk.SFTPFilesystemProvider,
-					SFTPConfig: vfs.SFTPFsConfig{
-						BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
-							Endpoint: sftpServerAddr,
-							Username: baseUser.Username,
-							Prefix:   path.Join("/", folder5),
-						},
-						Password: kms.NewPlainSecret(defaultPassword),
-					},
-				},
+				Name: folder5,
 			},
 			VirtualPath: path.Join("/", folder5),
 			QuotaSize:   -1,
@@ -2776,19 +2928,7 @@ func TestCrossFolderRename(t *testing.T) {
 		},
 		{
 			BaseVirtualFolder: vfs.BaseVirtualFolder{
-				Name:       folder6,
-				MappedPath: filepath.Join(os.TempDir(), folder6),
-				FsConfig: vfs.Filesystem{
-					Provider: sdk.SFTPFilesystemProvider,
-					SFTPConfig: vfs.SFTPFsConfig{
-						BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
-							Endpoint: "127.0.0.1:4024",
-							Username: baseUser.Username,
-							Prefix:   path.Join("/", folder6),
-						},
-						Password: kms.NewPlainSecret(defaultPassword),
-					},
-				},
+				Name: folder6,
 			},
 			VirtualPath: path.Join("/", folder6),
 			QuotaSize:   -1,
@@ -2796,19 +2936,7 @@ func TestCrossFolderRename(t *testing.T) {
 		},
 		{
 			BaseVirtualFolder: vfs.BaseVirtualFolder{
-				Name:       folder7,
-				MappedPath: filepath.Join(os.TempDir(), folder7),
-				FsConfig: vfs.Filesystem{
-					Provider: sdk.SFTPFilesystemProvider,
-					SFTPConfig: vfs.SFTPFsConfig{
-						BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
-							Endpoint: sftpServerAddr,
-							Username: baseUser.Username,
-							Prefix:   path.Join("/", folder4),
-						},
-						Password: kms.NewPlainSecret(defaultPassword),
-					},
-				},
+				Name: folder7,
 			},
 			VirtualPath: path.Join("/", folder7),
 			QuotaSize:   -1,
@@ -2889,10 +3017,15 @@ func TestDirs(t *testing.T) {
 	mappedPath := filepath.Join(os.TempDir(), "vdir")
 	folderName := filepath.Base(mappedPath)
 	vdirPath := "/path/vdir"
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdirPath,
 	})
@@ -4398,13 +4531,18 @@ func TestEventRulePreDelete(t *testing.T) {
 	}
 	rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
 	assert.NoError(t, err)
+	f := vfs.BaseVirtualFolder{
+		Name:       movePath,
+		MappedPath: filepath.Join(os.TempDir(), movePath),
+	}
+	_, _, err = httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	u := getTestUser()
 	u.QuotaFiles = 1000
 	u.VirtualFolders = []vfs.VirtualFolder{
 		{
 			BaseVirtualFolder: vfs.BaseVirtualFolder{
-				Name:       movePath,
-				MappedPath: filepath.Join(os.TempDir(), movePath),
+				Name: movePath,
 			},
 			VirtualPath: "/" + movePath,
 			QuotaFiles:  1000,
@@ -5326,10 +5464,15 @@ func TestEventActionCompressQuotaFolder(t *testing.T) {
 	mappedPath := filepath.Join(os.TempDir(), "virtualpath")
 	folderName := filepath.Base(mappedPath)
 	vdirPath := "/virtualpath"
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err = httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdirPath,
 		QuotaSize:   -1,
@@ -7092,10 +7235,15 @@ func TestGetQuotaError(t *testing.T) {
 	mappedPath := filepath.Join(os.TempDir(), "vdir")
 	folderName := filepath.Base(mappedPath)
 	vdirPath := "/vpath"
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdirPath,
 		QuotaSize:   0,
@@ -7559,27 +7707,31 @@ func TestSFTPLoopError(t *testing.T) {
 	}
 	err := smtpCfg.Initialize(configDir, true)
 	require.NoError(t, err)
-
 	user1 := getTestUser()
 	user2 := getTestUser()
 	user1.Username += "1"
 	user2.Username += "2"
 	// user1 is a local account with a virtual SFTP folder to user2
 	// user2 has user1 as SFTP fs
-	user1.VirtualFolders = append(user1.VirtualFolders, vfs.VirtualFolder{
-		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name: "sftp",
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.SFTPFilesystemProvider,
-				SFTPConfig: vfs.SFTPFsConfig{
-					BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
-						Endpoint: sftpServerAddr,
-						Username: user2.Username,
-					},
-					Password: kms.NewPlainSecret(defaultPassword),
+	f := vfs.BaseVirtualFolder{
+		Name: "sftp",
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.SFTPFilesystemProvider,
+			SFTPConfig: vfs.SFTPFsConfig{
+				BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+					Endpoint: sftpServerAddr,
+					Username: user2.Username,
 				},
+				Password: kms.NewPlainSecret(defaultPassword),
 			},
 		},
+	}
+	folder, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
+	user1.VirtualFolders = append(user1.VirtualFolders, vfs.VirtualFolder{
+		BaseVirtualFolder: vfs.BaseVirtualFolder{
+			Name: folder.Name,
+		},
 		VirtualPath: "/vdir",
 	})
 
@@ -7683,7 +7835,7 @@ func TestSFTPLoopError(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.RemoveAll(user2.GetHomeDir())
 	assert.NoError(t, err)
-	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: "sftp"}, http.StatusOK)
+	_, err = httpdtest.RemoveFolder(folder, http.StatusOK)
 	assert.NoError(t, err)
 
 	smtpCfg = smtp.Config{}
@@ -7703,16 +7855,6 @@ func TestNonLocalCrossRename(t *testing.T) {
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
 			Name: folderNameSFTP,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.SFTPFilesystemProvider,
-				SFTPConfig: vfs.SFTPFsConfig{
-					BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
-						Endpoint: sftpServerAddr,
-						Username: baseUser.Username,
-					},
-					Password: kms.NewPlainSecret(defaultPassword),
-				},
-			},
 		},
 		VirtualPath: vdirSFTPPath,
 	})
@@ -7722,16 +7864,37 @@ func TestNonLocalCrossRename(t *testing.T) {
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
 			Name: folderNameCrypt,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					Passphrase: kms.NewPlainSecret(defaultPassword),
-				},
-			},
-			MappedPath: mappedPathCrypt,
 		},
 		VirtualPath: vdirCryptPath,
 	})
+	f1 := vfs.BaseVirtualFolder{
+		Name: folderNameSFTP,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.SFTPFilesystemProvider,
+			SFTPConfig: vfs.SFTPFsConfig{
+				BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+					Endpoint: sftpServerAddr,
+					Username: baseUser.Username,
+				},
+				Password: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name: folderNameCrypt,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+		MappedPath: mappedPathCrypt,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+
 	user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err, string(resp))
 	conn, client, err := getSftpClient(user)
@@ -7812,8 +7975,7 @@ func TestNonLocalCrossRenameNonLocalBaseUser(t *testing.T) {
 	vdirLocalPath := "/vdir/local"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderNameLocal,
-			MappedPath: mappedPathLocal,
+			Name: folderNameLocal,
 		},
 		VirtualPath: vdirLocalPath,
 	})
@@ -7823,16 +7985,28 @@ func TestNonLocalCrossRenameNonLocalBaseUser(t *testing.T) {
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
 			Name: folderNameCrypt,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					Passphrase: kms.NewPlainSecret(defaultPassword),
-				},
-			},
-			MappedPath: mappedPathCrypt,
 		},
 		VirtualPath: vdirCryptPath,
 	})
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderNameLocal,
+		MappedPath: mappedPathLocal,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name: folderNameCrypt,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+		MappedPath: mappedPathCrypt,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+
 	user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err, string(resp))
 	conn, client, err := getSftpClient(user)
@@ -8130,8 +8304,7 @@ func TestCrossFoldersCopy(t *testing.T) {
 	vpath1 := "/vdirs/vdir1"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vpath1,
 		QuotaSize:   -1,
@@ -8142,8 +8315,7 @@ func TestCrossFoldersCopy(t *testing.T) {
 	vpath2 := "/vdirs/vdir2"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vpath2,
 		QuotaSize:   -1,
@@ -8154,14 +8326,7 @@ func TestCrossFoldersCopy(t *testing.T) {
 	vpath3 := "/vdirs/vdir3"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName3,
-			MappedPath: mappedPath3,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					Passphrase: kms.NewPlainSecret(defaultPassword),
-				},
-			},
+			Name: folderName3,
 		},
 		VirtualPath: vpath3,
 		QuotaSize:   -1,
@@ -8172,23 +8337,53 @@ func TestCrossFoldersCopy(t *testing.T) {
 	vpath4 := "/vdirs/vdir4"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName4,
-			MappedPath: mappedPath4,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.SFTPFilesystemProvider,
-				SFTPConfig: vfs.SFTPFsConfig{
-					BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
-						Endpoint: sftpServerAddr,
-						Username: baseUser.Username,
-					},
-					Password: kms.NewPlainSecret(defaultPassword),
-				},
-			},
+			Name: folderName4,
 		},
 		VirtualPath: vpath4,
 		QuotaSize:   -1,
 		QuotaFiles:  -1,
 	})
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+	f3 := vfs.BaseVirtualFolder{
+		Name:       folderName3,
+		MappedPath: mappedPath3,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)
+	assert.NoError(t, err)
+	f4 := vfs.BaseVirtualFolder{
+		Name:       folderName4,
+		MappedPath: mappedPath4,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.SFTPFilesystemProvider,
+			SFTPConfig: vfs.SFTPFsConfig{
+				BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+					Endpoint: sftpServerAddr,
+					Username: baseUser.Username,
+				},
+				Password: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f4, http.StatusCreated)
+	assert.NoError(t, err)
+
 	user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err, string(resp))
 	conn, client, err := getSftpClient(user)

+ 14 - 4
internal/common/transferschecker_test.go

@@ -48,6 +48,10 @@ func TestTransfersCheckerDiskQuota(t *testing.T) {
 			},
 		},
 	}
+	folder := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: filepath.Join(os.TempDir(), folderName),
+	}
 	user := dataprovider.User{
 		BaseUser: sdk.BaseUser{
 			Username:  username,
@@ -62,8 +66,7 @@ func TestTransfersCheckerDiskQuota(t *testing.T) {
 		VirtualFolders: []vfs.VirtualFolder{
 			{
 				BaseVirtualFolder: vfs.BaseVirtualFolder{
-					Name:       folderName,
-					MappedPath: filepath.Join(os.TempDir(), folderName),
+					Name: folderName,
 				},
 				VirtualPath: vdirPath,
 				QuotaSize:   100,
@@ -80,6 +83,8 @@ func TestTransfersCheckerDiskQuota(t *testing.T) {
 	assert.NoError(t, err)
 	group, err = dataprovider.GroupExists(groupName)
 	assert.NoError(t, err)
+	err = dataprovider.AddFolder(&folder, "", "", "")
+	assert.NoError(t, err)
 	assert.Equal(t, int64(120), group.UserSettings.QuotaSize)
 	err = dataprovider.AddUser(&user, "", "", "")
 	assert.NoError(t, err)
@@ -601,6 +606,10 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
 	assert.Len(t, users, 0)
 
 	for i := 0; i < 40; i++ {
+		folder := vfs.BaseVirtualFolder{
+			Name:       fmt.Sprintf("f%v", i),
+			MappedPath: filepath.Join(os.TempDir(), fmt.Sprintf("f%v", i)),
+		}
 		user := dataprovider.User{
 			BaseUser: sdk.BaseUser{
 				Username:  fmt.Sprintf("user%v", i),
@@ -615,14 +624,15 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
 			VirtualFolders: []vfs.VirtualFolder{
 				{
 					BaseVirtualFolder: vfs.BaseVirtualFolder{
-						Name:       fmt.Sprintf("f%v", i),
-						MappedPath: filepath.Join(os.TempDir(), fmt.Sprintf("f%v", i)),
+						Name: folder.Name,
 					},
 					VirtualPath: "/vfolder",
 					QuotaSize:   100,
 				},
 			},
 		}
+		err = dataprovider.AddFolder(&folder, "", "", "")
+		assert.NoError(t, err)
 		err = dataprovider.AddUser(&user, "", "", "")
 		assert.NoError(t, err)
 		err = dataprovider.UpdateVirtualFolderQuota(&vfs.BaseVirtualFolder{Name: fmt.Sprintf("f%v", i)}, 1, 50, false)

+ 6 - 5
internal/dataprovider/admin.go

@@ -47,6 +47,7 @@ const (
 	PermAdminViewServerStatus = "view_status"
 	PermAdminManageAdmins     = "manage_admins"
 	PermAdminManageGroups     = "manage_groups"
+	PermAdminManageFolders    = "manage_folders"
 	PermAdminManageAPIKeys    = "manage_apikeys"
 	PermAdminQuotaScans       = "quota_scans"
 	PermAdminManageSystem     = "manage_system"
@@ -71,11 +72,11 @@ const (
 
 var (
 	validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
-		PermAdminViewUsers, PermAdminManageGroups, PermAdminViewConnections, PermAdminCloseConnections,
-		PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageRoles, PermAdminManageEventRules,
-		PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem, PermAdminManageDefender,
-		PermAdminViewDefender, PermAdminManageIPLists, PermAdminRetentionChecks, PermAdminMetadataChecks,
-		PermAdminViewEvents}
+		PermAdminViewUsers, PermAdminManageFolders, PermAdminManageGroups, PermAdminViewConnections,
+		PermAdminCloseConnections, PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageRoles,
+		PermAdminManageEventRules, PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem,
+		PermAdminManageDefender, PermAdminViewDefender, PermAdminManageIPLists, PermAdminRetentionChecks,
+		PermAdminMetadataChecks, PermAdminViewEvents}
 	forbiddenPermsForRoleAdmins = []string{PermAdminAny, PermAdminManageAdmins, PermAdminManageSystem,
 		PermAdminManageEventRules, PermAdminManageIPLists, PermAdminManageRoles}
 )

+ 22 - 32
internal/dataprovider/bolt.go

@@ -664,7 +664,7 @@ func (p *BoltProvider) addUser(user *User) error {
 			return err
 		}
 		for idx := range user.VirtualFolders {
-			err = p.addRelationToFolderMapping(&user.VirtualFolders[idx].BaseVirtualFolder, user, nil, foldersBucket)
+			err = p.addRelationToFolderMapping(user.VirtualFolders[idx].Name, user, nil, foldersBucket)
 			if err != nil {
 				return err
 			}
@@ -1434,7 +1434,7 @@ func (p *BoltProvider) addGroup(group *Group) error {
 		group.Users = nil
 		group.Admins = nil
 		for idx := range group.VirtualFolders {
-			err = p.addRelationToFolderMapping(&group.VirtualFolders[idx].BaseVirtualFolder, nil, group, foldersBucket)
+			err = p.addRelationToFolderMapping(group.VirtualFolders[idx].Name, nil, group, foldersBucket)
 			if err != nil {
 				return err
 			}
@@ -1476,7 +1476,7 @@ func (p *BoltProvider) updateGroup(group *Group) error {
 			}
 		}
 		for idx := range group.VirtualFolders {
-			err = p.addRelationToFolderMapping(&group.VirtualFolders[idx].BaseVirtualFolder, nil, group, foldersBucket)
+			err = p.addRelationToFolderMapping(group.VirtualFolders[idx].Name, nil, group, foldersBucket)
 			if err != nil {
 				return err
 			}
@@ -3427,7 +3427,7 @@ func (p *BoltProvider) removeRuleFromActionMapping(ruleName, actionName string,
 func (p *BoltProvider) addUserToGroupMapping(username, groupname string, bucket *bolt.Bucket) error {
 	g := bucket.Get([]byte(groupname))
 	if g == nil {
-		return util.NewRecordNotFoundError(fmt.Sprintf("group %q does not exist", groupname))
+		return util.NewGenericError(fmt.Sprintf("group %q does not exist", groupname))
 	}
 	var group Group
 	err := json.Unmarshal(g, &group)
@@ -3539,43 +3539,33 @@ func (p *BoltProvider) removeGroupFromAdminMapping(groupName, adminName string,
 	return bucket.Put([]byte(adminName), buf)
 }
 
-func (p *BoltProvider) addRelationToFolderMapping(baseFolder *vfs.BaseVirtualFolder, user *User, group *Group, bucket *bolt.Bucket) error {
-	f := bucket.Get([]byte(baseFolder.Name))
+func (p *BoltProvider) addRelationToFolderMapping(folderName string, user *User, group *Group, bucket *bolt.Bucket) error {
+	f := bucket.Get([]byte(folderName))
 	if f == nil {
-		// folder does not exists, try to create
-		baseFolder.LastQuotaUpdate = 0
-		baseFolder.UsedQuotaFiles = 0
-		baseFolder.UsedQuotaSize = 0
-		if user != nil {
-			baseFolder.Users = []string{user.Username}
-		}
-		if group != nil {
-			baseFolder.Groups = []string{group.Name}
-		}
-		return p.addFolderInternal(*baseFolder, bucket)
+		return util.NewGenericError(fmt.Sprintf("folder %q does not exist", folderName))
 	}
-	var oldFolder vfs.BaseVirtualFolder
-	err := json.Unmarshal(f, &oldFolder)
+	var folder vfs.BaseVirtualFolder
+	err := json.Unmarshal(f, &folder)
 	if err != nil {
 		return err
 	}
-	baseFolder.ID = oldFolder.ID
-	baseFolder.LastQuotaUpdate = oldFolder.LastQuotaUpdate
-	baseFolder.UsedQuotaFiles = oldFolder.UsedQuotaFiles
-	baseFolder.UsedQuotaSize = oldFolder.UsedQuotaSize
-	baseFolder.Users = oldFolder.Users
-	baseFolder.Groups = oldFolder.Groups
-	if user != nil && !util.Contains(baseFolder.Users, user.Username) {
-		baseFolder.Users = append(baseFolder.Users, user.Username)
+	updated := false
+	if user != nil && !util.Contains(folder.Users, user.Username) {
+		folder.Users = append(folder.Users, user.Username)
+		updated = true
 	}
-	if group != nil && !util.Contains(baseFolder.Groups, group.Name) {
-		baseFolder.Groups = append(baseFolder.Groups, group.Name)
+	if group != nil && !util.Contains(folder.Groups, group.Name) {
+		folder.Groups = append(folder.Groups, group.Name)
+		updated = true
 	}
-	buf, err := json.Marshal(baseFolder)
+	if !updated {
+		return nil
+	}
+	buf, err := json.Marshal(folder)
 	if err != nil {
 		return err
 	}
-	return bucket.Put([]byte(baseFolder.Name), buf)
+	return bucket.Put([]byte(folder.Name), buf)
 }
 
 func (p *BoltProvider) removeRelationFromFolderMapping(folder vfs.VirtualFolder, username, groupname string,
@@ -3651,7 +3641,7 @@ func (p *BoltProvider) updateUserRelations(tx *bolt.Tx, user *User, oldUser User
 		return err
 	}
 	for idx := range user.VirtualFolders {
-		err = p.addRelationToFolderMapping(&user.VirtualFolders[idx].BaseVirtualFolder, user, nil, foldersBucket)
+		err = p.addRelationToFolderMapping(user.VirtualFolders[idx].Name, user, nil, foldersBucket)
 		if err != nil {
 			return err
 		}

+ 11 - 31
internal/dataprovider/dataprovider.go

@@ -2621,27 +2621,6 @@ func validateFolderQuotaLimits(folder vfs.VirtualFolder) error {
 	return nil
 }
 
-func getVirtualFolderIfInvalid(folder *vfs.BaseVirtualFolder) *vfs.BaseVirtualFolder {
-	if err := ValidateFolder(folder); err == nil {
-		return folder
-	}
-	if folder.Name == "" {
-		return folder
-	}
-	// we try to get the folder from the data provider if only the Name is populated
-	// so if MappedPath or Provider are set just return
-	if folder.MappedPath != "" {
-		return folder
-	}
-	if folder.FsConfig.Provider != sdk.LocalFilesystemProvider {
-		return folder
-	}
-	if f, err := GetFolderByName(folder.Name); err == nil {
-		return &f
-	}
-	return folder
-}
-
 func validateUserGroups(user *User) error {
 	if len(user.Groups) == 0 {
 		return nil
@@ -2682,12 +2661,11 @@ func validateAssociatedVirtualFolders(vfolders []vfs.VirtualFolder) ([]vfs.Virtu
 		if err := validateFolderQuotaLimits(v); err != nil {
 			return nil, err
 		}
-		folder := getVirtualFolderIfInvalid(&v.BaseVirtualFolder)
-		if err := ValidateFolder(folder); err != nil {
-			return nil, err
+		if v.Name == "" {
+			return nil, util.NewValidationError("folder name is mandatory")
 		}
-		if folderNames[folder.Name] {
-			return nil, util.NewValidationError(fmt.Sprintf("the folder %q is duplicated", folder.Name))
+		if folderNames[v.Name] {
+			return nil, util.NewValidationError(fmt.Sprintf("the folder %q is duplicated", v.Name))
 		}
 		for _, vFolder := range virtualFolders {
 			if util.IsDirOverlapped(vFolder.VirtualPath, cleanedVPath, false, "/") {
@@ -2696,12 +2674,14 @@ func validateAssociatedVirtualFolders(vfolders []vfs.VirtualFolder) ([]vfs.Virtu
 			}
 		}
 		virtualFolders = append(virtualFolders, vfs.VirtualFolder{
-			BaseVirtualFolder: *folder,
-			VirtualPath:       cleanedVPath,
-			QuotaSize:         v.QuotaSize,
-			QuotaFiles:        v.QuotaFiles,
+			BaseVirtualFolder: vfs.BaseVirtualFolder{
+				Name: v.Name,
+			},
+			VirtualPath: cleanedVPath,
+			QuotaSize:   v.QuotaSize,
+			QuotaFiles:  v.QuotaFiles,
 		})
-		folderNames[folder.Name] = true
+		folderNames[v.Name] = true
 	}
 	return virtualFolders, nil
 }

+ 84 - 91
internal/dataprovider/memory.go

@@ -319,8 +319,6 @@ func (p *MemoryProvider) getUsedQuota(username string) (int, int64, int64, int64
 }
 
 func (p *MemoryProvider) addUser(user *User) error {
-	// we can query virtual folder while validating a user
-	// so we have to check without holding the lock
 	err := ValidateUser(user)
 	if err != nil {
 		return err
@@ -361,16 +359,24 @@ func (p *MemoryProvider) addUser(user *User) error {
 		}
 		mappedGroups = append(mappedGroups, user.Groups[idx].Name)
 	}
-	user.VirtualFolders = p.joinUserVirtualFoldersFields(user)
+	var mappedFolders []string
+	for idx := range user.VirtualFolders {
+		if err = p.addUserToFolderMapping(user.Username, user.VirtualFolders[idx].Name); err != nil {
+			// try to remove folder mapping
+			for _, f := range mappedFolders {
+				p.removeRelationFromFolderMapping(f, user.Username, "")
+			}
+			return err
+		}
+		mappedFolders = append(mappedFolders, user.VirtualFolders[idx].Name)
+	}
 	p.dbHandle.users[user.Username] = user.getACopy()
 	p.dbHandle.usernames = append(p.dbHandle.usernames, user.Username)
 	sort.Strings(p.dbHandle.usernames)
 	return nil
 }
 
-func (p *MemoryProvider) updateUser(user *User) error {
-	// we can query virtual folder while validating a user
-	// so we have to check without holding the lock
+func (p *MemoryProvider) updateUser(user *User) error { //nolint:gocyclo
 	err := ValidateUser(user)
 	if err != nil {
 		return err
@@ -413,7 +419,18 @@ func (p *MemoryProvider) updateUser(user *User) error {
 	for _, oldFolder := range u.VirtualFolders {
 		p.removeRelationFromFolderMapping(oldFolder.Name, u.Username, "")
 	}
-	user.VirtualFolders = p.joinUserVirtualFoldersFields(user)
+	for idx := range user.VirtualFolders {
+		if err = p.addUserToFolderMapping(user.Username, user.VirtualFolders[idx].Name); err != nil {
+			// try to add old mapping
+			for _, f := range u.VirtualFolders {
+				if errRollback := p.addUserToFolderMapping(user.Username, f.Name); errRollback != nil {
+					providerLog(logger.LevelError, "unable to rollback old folder mapping %q for user %q, error: %v",
+						f.Name, user.Username, errRollback)
+				}
+			}
+			return err
+		}
+	}
 	user.LastQuotaUpdate = u.LastQuotaUpdate
 	user.UsedQuotaSize = u.UsedQuotaSize
 	user.UsedQuotaFiles = u.UsedQuotaFiles
@@ -1012,7 +1029,17 @@ func (p *MemoryProvider) addGroup(group *Group) error {
 	group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
 	group.Users = nil
 	group.Admins = nil
-	group.VirtualFolders = p.joinGroupVirtualFoldersFields(group)
+	var mappedFolders []string
+	for idx := range group.VirtualFolders {
+		if err = p.addGroupToFolderMapping(group.Name, group.VirtualFolders[idx].Name); err != nil {
+			// try to remove folder mapping
+			for _, f := range mappedFolders {
+				p.removeRelationFromFolderMapping(f, "", group.Name)
+			}
+			return err
+		}
+		mappedFolders = append(mappedFolders, group.VirtualFolders[idx].Name)
+	}
 	p.dbHandle.groups[group.Name] = group.getACopy()
 	p.dbHandle.groupnames = append(p.dbHandle.groupnames, group.Name)
 	sort.Strings(p.dbHandle.groupnames)
@@ -1035,7 +1062,18 @@ func (p *MemoryProvider) updateGroup(group *Group) error {
 	for _, oldFolder := range g.VirtualFolders {
 		p.removeRelationFromFolderMapping(oldFolder.Name, "", g.Name)
 	}
-	group.VirtualFolders = p.joinGroupVirtualFoldersFields(group)
+	for idx := range group.VirtualFolders {
+		if err = p.addGroupToFolderMapping(group.Name, group.VirtualFolders[idx].Name); err != nil {
+			// try to add old mapping
+			for _, f := range g.VirtualFolders {
+				if errRollback := p.addGroupToFolderMapping(group.Name, f.Name); errRollback != nil {
+					providerLog(logger.LevelError, "unable to rollback old folder mapping %q for group %q, error: %v",
+						f.Name, group.Name, errRollback)
+				}
+			}
+			return err
+		}
+	}
 	group.CreatedAt = g.CreatedAt
 	group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
 	group.ID = g.ID
@@ -1105,19 +1143,6 @@ func (p *MemoryProvider) getUsedFolderQuota(name string) (int, int64, error) {
 	return folder.UsedQuotaFiles, folder.UsedQuotaSize, err
 }
 
-func (p *MemoryProvider) joinGroupVirtualFoldersFields(group *Group) []vfs.VirtualFolder {
-	var folders []vfs.VirtualFolder
-	for idx := range group.VirtualFolders {
-		folder := &group.VirtualFolders[idx]
-		f, err := p.addOrUpdateFolderInternal(&folder.BaseVirtualFolder, "", group.Name, 0, 0, 0)
-		if err == nil {
-			folder.BaseVirtualFolder = f
-			folders = append(folders, *folder)
-		}
-	}
-	return folders
-}
-
 func (p *MemoryProvider) addVirtualFoldersToGroup(group *Group) {
 	if len(group.VirtualFolders) > 0 {
 		var folders []vfs.VirtualFolder
@@ -1317,17 +1342,28 @@ func (p *MemoryProvider) removeUserFromRole(username, role string) {
 	p.dbHandle.roles[role] = r
 }
 
-func (p *MemoryProvider) joinUserVirtualFoldersFields(user *User) []vfs.VirtualFolder {
-	var folders []vfs.VirtualFolder
-	for idx := range user.VirtualFolders {
-		folder := &user.VirtualFolders[idx]
-		f, err := p.addOrUpdateFolderInternal(&folder.BaseVirtualFolder, user.Username, "", 0, 0, 0)
-		if err == nil {
-			folder.BaseVirtualFolder = f
-			folders = append(folders, *folder)
-		}
+func (p *MemoryProvider) addUserToFolderMapping(username, foldername string) error {
+	f, err := p.folderExistsInternal(foldername)
+	if err != nil {
+		return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
+	}
+	if !util.Contains(f.Users, username) {
+		f.Users = append(f.Users, username)
+		p.dbHandle.vfolders[foldername] = f
+	}
+	return nil
+}
+
+func (p *MemoryProvider) addGroupToFolderMapping(name, foldername string) error {
+	f, err := p.folderExistsInternal(foldername)
+	if err != nil {
+		return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
+	}
+	if !util.Contains(f.Groups, name) {
+		f.Groups = append(f.Groups, name)
+		p.dbHandle.vfolders[foldername] = f
 	}
-	return folders
+	return nil
 }
 
 func (p *MemoryProvider) addVirtualFoldersToUser(user *User) {
@@ -1348,71 +1384,28 @@ func (p *MemoryProvider) addVirtualFoldersToUser(user *User) {
 
 func (p *MemoryProvider) removeRelationFromFolderMapping(folderName, username, groupname string) {
 	folder, err := p.folderExistsInternal(folderName)
-	if err == nil {
-		if username != "" {
-			var usernames []string
-			for _, user := range folder.Users {
-				if user != username {
-					usernames = append(usernames, user)
-				}
+	if err != nil {
+		return
+	}
+	if username != "" {
+		var usernames []string
+		for _, user := range folder.Users {
+			if user != username {
+				usernames = append(usernames, user)
 			}
-			folder.Users = usernames
 		}
-		if groupname != "" {
-			var groups []string
-			for _, group := range folder.Groups {
-				if group != groupname {
-					groups = append(groups, group)
-				}
+		folder.Users = usernames
+	}
+	if groupname != "" {
+		var groups []string
+		for _, group := range folder.Groups {
+			if group != groupname {
+				groups = append(groups, group)
 			}
-			folder.Groups = groups
 		}
-		p.dbHandle.vfolders[folder.Name] = folder
+		folder.Groups = groups
 	}
-}
-
-func (p *MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolder) {
 	p.dbHandle.vfolders[folder.Name] = folder
-	if !util.Contains(p.dbHandle.vfoldersNames, folder.Name) {
-		p.dbHandle.vfoldersNames = append(p.dbHandle.vfoldersNames, folder.Name)
-		sort.Strings(p.dbHandle.vfoldersNames)
-	}
-}
-
-func (p *MemoryProvider) addOrUpdateFolderInternal(baseFolder *vfs.BaseVirtualFolder, username, groupname string,
-	usedQuotaSize int64, usedQuotaFiles int, lastQuotaUpdate int64,
-) (vfs.BaseVirtualFolder, error) {
-	folder, err := p.folderExistsInternal(baseFolder.Name)
-	if err == nil {
-		// exists
-		folder.MappedPath = baseFolder.MappedPath
-		folder.Description = baseFolder.Description
-		folder.FsConfig = baseFolder.FsConfig.GetACopy()
-		if username != "" && !util.Contains(folder.Users, username) {
-			folder.Users = append(folder.Users, username)
-		}
-		if groupname != "" && !util.Contains(folder.Groups, groupname) {
-			folder.Groups = append(folder.Groups, groupname)
-		}
-		p.updateFoldersMappingInternal(folder)
-		return folder, nil
-	}
-	if errors.Is(err, util.ErrNotFound) {
-		folder = baseFolder.GetACopy()
-		folder.ID = p.getNextFolderID()
-		folder.UsedQuotaSize = usedQuotaSize
-		folder.UsedQuotaFiles = usedQuotaFiles
-		folder.LastQuotaUpdate = lastQuotaUpdate
-		if username != "" {
-			folder.Users = []string{username}
-		}
-		if groupname != "" {
-			folder.Groups = []string{groupname}
-		}
-		p.updateFoldersMappingInternal(folder)
-		return folder, nil
-	}
-	return folder, err
 }
 
 func (p *MemoryProvider) folderExistsInternal(name string) (vfs.BaseVirtualFolder, error) {

+ 0 - 21
internal/dataprovider/sqlcommon.go

@@ -2302,19 +2302,6 @@ func sqlCommonGetFolderByName(ctx context.Context, name string, dbHandle sqlQuer
 	return folders[0], nil
 }
 
-func sqlCommonAddOrUpdateFolder(ctx context.Context, baseFolder *vfs.BaseVirtualFolder, usedQuotaSize int64,
-	usedQuotaFiles int, lastQuotaUpdate int64, dbHandle sqlQuerier,
-) error {
-	fsConfig, err := json.Marshal(baseFolder.FsConfig)
-	if err != nil {
-		return err
-	}
-	q := getUpsertFolderQuery()
-	_, err = dbHandle.ExecContext(ctx, q, baseFolder.MappedPath, usedQuotaSize, usedQuotaFiles,
-		lastQuotaUpdate, baseFolder.Name, baseFolder.Description, fsConfig)
-	return err
-}
-
 func sqlCommonAddFolder(folder *vfs.BaseVirtualFolder, dbHandle sqlQuerier) error {
 	err := ValidateFolder(folder)
 	if err != nil {
@@ -2518,10 +2505,6 @@ func generateGroupVirtualFoldersMapping(ctx context.Context, group *Group, dbHan
 	}
 	for idx := range group.VirtualFolders {
 		vfolder := &group.VirtualFolders[idx]
-		err = sqlCommonAddOrUpdateFolder(ctx, &vfolder.BaseVirtualFolder, 0, 0, 0, dbHandle)
-		if err != nil {
-			return err
-		}
 		err = sqlCommonAddGroupFolderMapping(ctx, group, vfolder, dbHandle)
 		if err != nil {
 			return err
@@ -2537,10 +2520,6 @@ func generateUserVirtualFoldersMapping(ctx context.Context, user *User, dbHandle
 	}
 	for idx := range user.VirtualFolders {
 		vfolder := &user.VirtualFolders[idx]
-		err := sqlCommonAddOrUpdateFolder(ctx, &vfolder.BaseVirtualFolder, 0, 0, 0, dbHandle)
-		if err != nil {
-			return err
-		}
 		err = sqlCommonAddUserFolderMapping(ctx, user, vfolder, dbHandle)
 		if err != nil {
 			return err

+ 0 - 14
internal/dataprovider/sqlqueries.go

@@ -751,20 +751,6 @@ func getDeleteFolderQuery() string {
 	return fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableFolders, sqlPlaceholders[0])
 }
 
-func getUpsertFolderQuery() string {
-	if config.Driver == MySQLDataProviderName {
-		return fmt.Sprintf("INSERT INTO %s (`path`,`used_quota_size`,`used_quota_files`,`last_quota_update`,`name`,"+
-			"`description`,`filesystem`) VALUES (%s,%s,%s,%s,%s,%s,%s) ON DUPLICATE KEY UPDATE "+
-			"`path`=VALUES(`path`),`description`=VALUES(`description`),`filesystem`=VALUES(`filesystem`)",
-			sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],
-			sqlPlaceholders[5], sqlPlaceholders[6])
-	}
-	return fmt.Sprintf(`INSERT INTO %s (path,used_quota_size,used_quota_files,last_quota_update,name,description,filesystem)
-		VALUES (%s,%s,%s,%s,%s,%s,%s) ON CONFLICT (name) DO UPDATE SET path = EXCLUDED.path,description=EXCLUDED.description,
-		filesystem=EXCLUDED.filesystem`, sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],
-		sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6])
-}
-
 func getClearUserGroupMappingQuery() string {
 	return fmt.Sprintf(`DELETE FROM %s WHERE user_id = (SELECT id FROM %s WHERE username = %s)`, sqlTableUsersGroupsMapping,
 		sqlTableUsers, sqlPlaceholders[0])

+ 43 - 17
internal/ftpd/ftpd_test.go

@@ -2684,16 +2684,21 @@ func TestUploadOverwriteVfolder(t *testing.T) {
 	vdir := "/vdir"
 	mappedPath := filepath.Join(os.TempDir(), "vdir")
 	folderName := filepath.Base(mappedPath)
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdir,
 		QuotaSize:   -1,
 		QuotaFiles:  -1,
 	})
-	err := os.MkdirAll(mappedPath, os.ModePerm)
+	err = os.MkdirAll(mappedPath, os.ModePerm)
 	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
@@ -2799,15 +2804,20 @@ func TestAllocateAvailable(t *testing.T) {
 	u := getTestUser()
 	mappedPath := filepath.Join(os.TempDir(), "vdir")
 	folderName := filepath.Base(mappedPath)
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: "/vdir",
 		QuotaSize:   110,
 	})
-	err := os.MkdirAll(mappedPath, os.ModePerm)
+	err = os.MkdirAll(mappedPath, os.ModePerm)
 	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
@@ -3654,13 +3664,6 @@ func TestNestedVirtualFolders(t *testing.T) {
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
 			Name: folderNameCrypt,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					Passphrase: kms.NewPlainSecret(defaultPassword),
-				},
-			},
-			MappedPath: mappedPathCrypt,
 		},
 		VirtualPath: vdirCryptPath,
 	})
@@ -3669,8 +3672,7 @@ func TestNestedVirtualFolders(t *testing.T) {
 	vdirPath := "/vdir/local"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdirPath,
 	})
@@ -3679,13 +3681,37 @@ func TestNestedVirtualFolders(t *testing.T) {
 	vdirNestedPath := "/vdir/crypt/nested"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderNameNested,
-			MappedPath: mappedPathNested,
+			Name: folderNameNested,
 		},
 		VirtualPath: vdirNestedPath,
 		QuotaFiles:  -1,
 		QuotaSize:   -1,
 	})
+	f1 := vfs.BaseVirtualFolder{
+		Name: folderNameCrypt,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+		MappedPath: mappedPathCrypt,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+	f3 := vfs.BaseVirtualFolder{
+		Name:       folderNameNested,
+		MappedPath: mappedPathNested,
+	}
+	_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)
+	assert.NoError(t, err)
+
 	sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	client, err := getFTPClient(sftpUser, false, nil)

+ 251 - 312
internal/httpd/httpd_test.go

@@ -904,8 +904,7 @@ func TestGroupRelations(t *testing.T) {
 	g1.Name += "_1"
 	g1.VirtualFolders = append(g1.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: "/vdir1",
 	})
@@ -913,8 +912,7 @@ func TestGroupRelations(t *testing.T) {
 	g2.Name += "_2"
 	g2.VirtualFolders = append(g2.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: "/vdir2",
 	})
@@ -922,11 +920,18 @@ func TestGroupRelations(t *testing.T) {
 	g3.Name += "_3"
 	g3.VirtualFolders = append(g3.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: "/vdir3",
 	})
+	_, _, err = httpdtest.AddGroup(g1, http.StatusCreated)
+	assert.Error(t, err, "adding a group with a missing folder must fail")
+	_, _, err = httpdtest.AddFolder(vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}, http.StatusCreated)
+	assert.NoError(t, err)
+
 	group1, resp, err := httpdtest.AddGroup(g1, http.StatusCreated)
 	assert.NoError(t, err, string(resp))
 	assert.Len(t, group1.VirtualFolders, 1)
@@ -1162,14 +1167,7 @@ func TestGroupSettingsOverride(t *testing.T) {
 	g1.Name += "_1"
 	g1.VirtualFolders = append(g1.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
-			FsConfig: vfs.Filesystem{
-				OSConfig: sdk.OSFsConfig{
-					ReadBufferSize:  3,
-					WriteBufferSize: 5,
-				},
-			},
+			Name: folderName1,
 		},
 		VirtualPath: "/vdir1",
 	})
@@ -1189,37 +1187,53 @@ func TestGroupSettingsOverride(t *testing.T) {
 	}
 	g2.VirtualFolders = append(g2.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
-			FsConfig: vfs.Filesystem{
-				OSConfig: sdk.OSFsConfig{
-					ReadBufferSize:  3,
-					WriteBufferSize: 5,
-				},
-			},
+			Name: folderName1,
 		},
 		VirtualPath: "/vdir2",
 	})
 	g2.VirtualFolders = append(g2.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: "/vdir3",
 	})
 	g2.VirtualFolders = append(g2.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName3,
-			MappedPath: mappedPath3,
-			FsConfig: vfs.Filesystem{
-				OSConfig: sdk.OSFsConfig{
-					ReadBufferSize:  1,
-					WriteBufferSize: 2,
-				},
-			},
+			Name: folderName3,
 		},
 		VirtualPath: "/vdir4",
 	})
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+		FsConfig: vfs.Filesystem{
+			OSConfig: sdk.OSFsConfig{
+				ReadBufferSize:  3,
+				WriteBufferSize: 5,
+			},
+		},
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+	f3 := vfs.BaseVirtualFolder{
+		Name:       folderName3,
+		MappedPath: mappedPath3,
+		FsConfig: vfs.Filesystem{
+			OSConfig: sdk.OSFsConfig{
+				ReadBufferSize:  1,
+				WriteBufferSize: 2,
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)
+	assert.NoError(t, err)
+
 	group1, resp, err := httpdtest.AddGroup(g1, http.StatusCreated)
 	assert.NoError(t, err, string(resp))
 	group2, resp, err := httpdtest.AddGroup(g2, http.StatusCreated)
@@ -1235,8 +1249,8 @@ func TestGroupSettingsOverride(t *testing.T) {
 			Type: sdk.GroupTypeSecondary,
 		},
 	}
-	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
-	assert.NoError(t, err)
+	user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
+	assert.NoError(t, err, string(resp))
 	assert.Len(t, user.VirtualFolders, 0)
 	assert.Len(t, user.Permissions, 1)
 
@@ -2749,6 +2763,12 @@ func TestUserTimestamps(t *testing.T) {
 		},
 		VirtualPath: "/vdir",
 	})
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err = httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	time.Sleep(10 * time.Millisecond)
 	user, resp, err = httpdtest.UpdateUser(user, http.StatusOK, "")
 	assert.NoError(t, err, string(resp))
@@ -4847,20 +4867,31 @@ func TestUpdateUser(t *testing.T) {
 	folderName2 := filepath.Base(mappedPath2)
 	user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: "/vdir1",
 	})
 	user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: "/vdir12/subdir",
 		QuotaSize:   123,
 		QuotaFiles:  2,
 	})
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+
 	user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
 	assert.NoError(t, err)
 
@@ -5031,16 +5062,27 @@ func TestUserFolderMapping(t *testing.T) {
 	u1 := getTestUser()
 	u1.VirtualFolders = append(u1.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:            folderName1,
-			MappedPath:      mappedPath1,
-			UsedQuotaFiles:  2,
-			UsedQuotaSize:   123,
-			LastQuotaUpdate: 456,
+			Name: folderName1,
 		},
 		VirtualPath: "/vdir",
 		QuotaSize:   -1,
 		QuotaFiles:  -1,
 	})
+	f1 := vfs.BaseVirtualFolder{
+		Name:            folderName1,
+		MappedPath:      mappedPath1,
+		UsedQuotaFiles:  2,
+		UsedQuotaSize:   123,
+		LastQuotaUpdate: 456,
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	user1, _, err := httpdtest.AddUser(u1, http.StatusCreated)
 	assert.NoError(t, err)
 	// virtual folder must be auto created
@@ -5048,12 +5090,12 @@ func TestUserFolderMapping(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, folder.Users, 1)
 	assert.Contains(t, folder.Users, user1.Username)
-	assert.Equal(t, 0, folder.UsedQuotaFiles)
-	assert.Equal(t, int64(0), folder.UsedQuotaSize)
-	assert.Equal(t, int64(0), folder.LastQuotaUpdate)
-	assert.Equal(t, 0, user1.VirtualFolders[0].UsedQuotaFiles)
-	assert.Equal(t, int64(0), user1.VirtualFolders[0].UsedQuotaSize)
-	assert.Equal(t, int64(0), user1.VirtualFolders[0].LastQuotaUpdate)
+	assert.Equal(t, 2, folder.UsedQuotaFiles)
+	assert.Equal(t, int64(123), folder.UsedQuotaSize)
+	assert.Equal(t, int64(456), folder.LastQuotaUpdate)
+	assert.Equal(t, 2, user1.VirtualFolders[0].UsedQuotaFiles)
+	assert.Equal(t, int64(123), user1.VirtualFolders[0].UsedQuotaSize)
+	assert.Equal(t, int64(456), user1.VirtualFolders[0].LastQuotaUpdate)
 
 	u2 := getTestUser()
 	u2.Username = defaultUsername + "2"
@@ -5179,17 +5221,22 @@ func TestUserS3Config(t *testing.T) {
 	folderName := "vfolderName"
 	user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: filepath.Join(os.TempDir(), "folderName"),
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					Passphrase: kms.NewPlainSecret("Crypted-Secret"),
-				},
-			},
+			Name: folderName,
 		},
 		VirtualPath: "/folderPath",
 	})
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: filepath.Join(os.TempDir(), "folderName"),
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret("Crypted-Secret"),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	user, body, err := httpdtest.UpdateUser(user, http.StatusOK, "")
 	assert.NoError(t, err, string(body))
 	assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus())
@@ -6097,227 +6144,6 @@ func TestStartQuotaScan(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-func TestEmbeddedFolders(t *testing.T) {
-	u := getTestUser()
-	mappedPath := filepath.Join(os.TempDir(), "mapped_path")
-	name := filepath.Base(mappedPath)
-	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
-		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:            name,
-			UsedQuotaFiles:  1000,
-			UsedQuotaSize:   8192,
-			LastQuotaUpdate: 123,
-		},
-		VirtualPath: "/vdir",
-		QuotaSize:   4096,
-		QuotaFiles:  1,
-	})
-	_, _, err := httpdtest.AddUser(u, http.StatusBadRequest)
-	assert.NoError(t, err)
-	u.VirtualFolders[0].MappedPath = mappedPath
-	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
-	assert.NoError(t, err)
-	// check that the folder was created
-	folder, _, err := httpdtest.GetFolderByName(name, http.StatusOK)
-	assert.NoError(t, err)
-	assert.Equal(t, mappedPath, folder.MappedPath)
-	assert.Equal(t, 0, folder.UsedQuotaFiles)
-	assert.Equal(t, int64(0), folder.UsedQuotaSize)
-	assert.Equal(t, int64(0), folder.LastQuotaUpdate)
-	if assert.Len(t, user.VirtualFolders, 1) {
-		assert.Equal(t, mappedPath, user.VirtualFolders[0].MappedPath)
-		assert.Equal(t, u.VirtualFolders[0].VirtualPath, user.VirtualFolders[0].VirtualPath)
-		assert.Equal(t, u.VirtualFolders[0].QuotaFiles, user.VirtualFolders[0].QuotaFiles)
-		assert.Equal(t, u.VirtualFolders[0].QuotaSize, user.VirtualFolders[0].QuotaSize)
-	}
-	// if the folder already exists we can just reference it by name while adding/updating a user
-	u.Username = u.Username + "1"
-	u.VirtualFolders[0].MappedPath = ""
-	user1, _, err := httpdtest.AddUser(u, http.StatusCreated)
-	assert.EqualError(t, err, "mapped path mismatch")
-	if assert.Len(t, user1.VirtualFolders, 1) {
-		assert.Equal(t, mappedPath, user1.VirtualFolders[0].MappedPath)
-		assert.Equal(t, u.VirtualFolders[0].VirtualPath, user1.VirtualFolders[0].VirtualPath)
-		assert.Equal(t, u.VirtualFolders[0].QuotaFiles, user1.VirtualFolders[0].QuotaFiles)
-		assert.Equal(t, u.VirtualFolders[0].QuotaSize, user1.VirtualFolders[0].QuotaSize)
-	}
-	user1.VirtualFolders = u.VirtualFolders
-	user1, _, err = httpdtest.UpdateUser(user1, http.StatusOK, "")
-	assert.EqualError(t, err, "mapped path mismatch")
-	if assert.Len(t, user1.VirtualFolders, 1) {
-		assert.Equal(t, mappedPath, user1.VirtualFolders[0].MappedPath)
-		assert.Equal(t, u.VirtualFolders[0].VirtualPath, user1.VirtualFolders[0].VirtualPath)
-		assert.Equal(t, u.VirtualFolders[0].QuotaFiles, user1.VirtualFolders[0].QuotaFiles)
-		assert.Equal(t, u.VirtualFolders[0].QuotaSize, user1.VirtualFolders[0].QuotaSize)
-	}
-	// now the virtual folder contains all the required paths
-	user1, _, err = httpdtest.UpdateUser(user1, http.StatusOK, "")
-	assert.NoError(t, err)
-	if assert.Len(t, user1.VirtualFolders, 1) {
-		assert.Equal(t, mappedPath, user1.VirtualFolders[0].MappedPath)
-		assert.Equal(t, u.VirtualFolders[0].VirtualPath, user1.VirtualFolders[0].VirtualPath)
-		assert.Equal(t, u.VirtualFolders[0].QuotaFiles, user1.VirtualFolders[0].QuotaFiles)
-		assert.Equal(t, u.VirtualFolders[0].QuotaSize, user1.VirtualFolders[0].QuotaSize)
-	}
-
-	_, err = httpdtest.RemoveUser(user, http.StatusOK)
-	assert.NoError(t, err)
-	_, err = httpdtest.RemoveUser(user1, http.StatusOK)
-	assert.NoError(t, err)
-
-	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: name}, http.StatusOK)
-	assert.NoError(t, err)
-}
-
-func TestEmbeddedFoldersUpdate(t *testing.T) {
-	u := getTestUser()
-	mappedPath := filepath.Join(os.TempDir(), "mapped_path")
-	name := filepath.Base(mappedPath)
-	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
-		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:            name,
-			MappedPath:      mappedPath,
-			UsedQuotaFiles:  1000,
-			UsedQuotaSize:   8192,
-			LastQuotaUpdate: 123,
-		},
-		VirtualPath: "/vdir",
-		QuotaSize:   4096,
-		QuotaFiles:  1,
-	})
-	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
-	assert.NoError(t, err)
-	folder, _, err := httpdtest.GetFolderByName(name, http.StatusOK)
-	assert.NoError(t, err)
-	assert.Equal(t, mappedPath, folder.MappedPath)
-	assert.Equal(t, 0, folder.UsedQuotaFiles)
-	assert.Equal(t, int64(0), folder.UsedQuotaSize)
-	assert.Equal(t, int64(0), folder.LastQuotaUpdate)
-	assert.Empty(t, folder.Description)
-	assert.Equal(t, sdk.LocalFilesystemProvider, folder.FsConfig.Provider)
-	assert.Len(t, folder.Users, 1)
-	assert.Contains(t, folder.Users, user.Username)
-	// update a field on the folder
-	description := "updatedDesc"
-	folder.MappedPath = mappedPath + "_update"
-	folder.Description = description
-	folder, _, err = httpdtest.UpdateFolder(folder, http.StatusOK)
-	assert.NoError(t, err)
-	assert.Equal(t, mappedPath+"_update", folder.MappedPath)
-	assert.Equal(t, 0, folder.UsedQuotaFiles)
-	assert.Equal(t, int64(0), folder.UsedQuotaSize)
-	assert.Equal(t, int64(0), folder.LastQuotaUpdate)
-	assert.Equal(t, description, folder.Description)
-	assert.Equal(t, sdk.LocalFilesystemProvider, folder.FsConfig.Provider)
-	// check that the user gets the changes
-	user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
-	assert.NoError(t, err)
-	userFolder := user.VirtualFolders[0].BaseVirtualFolder
-	assert.Equal(t, mappedPath+"_update", folder.MappedPath)
-	assert.Equal(t, 0, userFolder.UsedQuotaFiles)
-	assert.Equal(t, int64(0), userFolder.UsedQuotaSize)
-	assert.Equal(t, int64(0), userFolder.LastQuotaUpdate)
-	assert.Equal(t, description, userFolder.Description)
-	assert.Equal(t, sdk.LocalFilesystemProvider, userFolder.FsConfig.Provider)
-	// now update the folder embedding it inside the user
-	user.VirtualFolders = []vfs.VirtualFolder{
-		{
-			BaseVirtualFolder: vfs.BaseVirtualFolder{
-				Name:            name,
-				MappedPath:      "",
-				UsedQuotaFiles:  1000,
-				UsedQuotaSize:   8192,
-				LastQuotaUpdate: 123,
-				FsConfig: vfs.Filesystem{
-					Provider: sdk.S3FilesystemProvider,
-					S3Config: vfs.S3FsConfig{
-						BaseS3FsConfig: sdk.BaseS3FsConfig{
-							Bucket:    "test",
-							Region:    "us-east-1",
-							AccessKey: "akey",
-							Endpoint:  "http://127.0.1.1:9090",
-						},
-						AccessSecret: kms.NewPlainSecret("asecret"),
-					},
-				},
-			},
-			VirtualPath: "/vdir1",
-			QuotaSize:   4096,
-			QuotaFiles:  1,
-		},
-	}
-	user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
-	assert.NoError(t, err)
-	userFolder = user.VirtualFolders[0].BaseVirtualFolder
-	assert.Equal(t, 0, userFolder.UsedQuotaFiles)
-	assert.Equal(t, int64(0), userFolder.UsedQuotaSize)
-	assert.Equal(t, int64(0), userFolder.LastQuotaUpdate)
-	assert.Empty(t, userFolder.Description)
-	assert.Equal(t, sdk.S3FilesystemProvider, userFolder.FsConfig.Provider)
-	assert.Equal(t, "test", userFolder.FsConfig.S3Config.Bucket)
-	assert.Equal(t, "us-east-1", userFolder.FsConfig.S3Config.Region)
-	assert.Equal(t, "http://127.0.1.1:9090", userFolder.FsConfig.S3Config.Endpoint)
-	assert.Equal(t, sdkkms.SecretStatusSecretBox, userFolder.FsConfig.S3Config.AccessSecret.GetStatus())
-	assert.NotEmpty(t, userFolder.FsConfig.S3Config.AccessSecret.GetPayload())
-	assert.Empty(t, userFolder.FsConfig.S3Config.AccessSecret.GetKey())
-	assert.Empty(t, userFolder.FsConfig.S3Config.AccessSecret.GetAdditionalData())
-	// confirm the changes
-	folder, _, err = httpdtest.GetFolderByName(name, http.StatusOK)
-	assert.NoError(t, err)
-	assert.Equal(t, 0, folder.UsedQuotaFiles)
-	assert.Equal(t, int64(0), folder.UsedQuotaSize)
-	assert.Equal(t, int64(0), folder.LastQuotaUpdate)
-	assert.Empty(t, folder.Description)
-	assert.Equal(t, sdk.S3FilesystemProvider, folder.FsConfig.Provider)
-	assert.Equal(t, "test", folder.FsConfig.S3Config.Bucket)
-	assert.Equal(t, "us-east-1", folder.FsConfig.S3Config.Region)
-	assert.Equal(t, "http://127.0.1.1:9090", folder.FsConfig.S3Config.Endpoint)
-	assert.Equal(t, sdkkms.SecretStatusSecretBox, folder.FsConfig.S3Config.AccessSecret.GetStatus())
-	assert.NotEmpty(t, folder.FsConfig.S3Config.AccessSecret.GetPayload())
-	assert.Empty(t, folder.FsConfig.S3Config.AccessSecret.GetKey())
-	assert.Empty(t, folder.FsConfig.S3Config.AccessSecret.GetAdditionalData())
-	// now update folder usage limits and check that a folder update will not change them
-	folder.UsedQuotaFiles = 100
-	folder.UsedQuotaSize = 32768
-	_, err = httpdtest.UpdateFolderQuotaUsage(folder, "reset", http.StatusOK)
-	assert.NoError(t, err)
-	folder, _, err = httpdtest.GetFolderByName(name, http.StatusOK)
-	assert.NoError(t, err)
-	assert.Equal(t, 100, folder.UsedQuotaFiles)
-	assert.Equal(t, int64(32768), folder.UsedQuotaSize)
-	assert.Greater(t, folder.LastQuotaUpdate, int64(0))
-	assert.Equal(t, sdk.S3FilesystemProvider, folder.FsConfig.Provider)
-	assert.Equal(t, "test", folder.FsConfig.S3Config.Bucket)
-	assert.Equal(t, "us-east-1", folder.FsConfig.S3Config.Region)
-	assert.Equal(t, "http://127.0.1.1:9090", folder.FsConfig.S3Config.Endpoint)
-	assert.Equal(t, sdkkms.SecretStatusSecretBox, folder.FsConfig.S3Config.AccessSecret.GetStatus())
-	assert.NotEmpty(t, folder.FsConfig.S3Config.AccessSecret.GetPayload())
-	assert.Empty(t, folder.FsConfig.S3Config.AccessSecret.GetKey())
-	assert.Empty(t, folder.FsConfig.S3Config.AccessSecret.GetAdditionalData())
-
-	user.VirtualFolders[0].FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("updated secret")
-	user, resp, err := httpdtest.UpdateUser(user, http.StatusOK, "")
-	assert.NoError(t, err, string(resp))
-	userFolder = user.VirtualFolders[0].BaseVirtualFolder
-	assert.Equal(t, 100, userFolder.UsedQuotaFiles)
-	assert.Equal(t, int64(32768), userFolder.UsedQuotaSize)
-	assert.Greater(t, userFolder.LastQuotaUpdate, int64(0))
-	assert.Empty(t, userFolder.Description)
-	assert.Equal(t, sdk.S3FilesystemProvider, userFolder.FsConfig.Provider)
-	assert.Equal(t, "test", userFolder.FsConfig.S3Config.Bucket)
-	assert.Equal(t, "us-east-1", userFolder.FsConfig.S3Config.Region)
-	assert.Equal(t, "http://127.0.1.1:9090", userFolder.FsConfig.S3Config.Endpoint)
-	assert.Equal(t, sdkkms.SecretStatusSecretBox, userFolder.FsConfig.S3Config.AccessSecret.GetStatus())
-	assert.NotEmpty(t, userFolder.FsConfig.S3Config.AccessSecret.GetPayload())
-	assert.Empty(t, userFolder.FsConfig.S3Config.AccessSecret.GetKey())
-	assert.Empty(t, userFolder.FsConfig.S3Config.AccessSecret.GetAdditionalData())
-
-	_, err = httpdtest.RemoveUser(user, http.StatusOK)
-	assert.NoError(t, err)
-	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: name}, http.StatusOK)
-	assert.NoError(t, err)
-}
-
 func TestUpdateFolderQuotaUsage(t *testing.T) {
 	f := vfs.BaseVirtualFolder{
 		Name:       "vdir",
@@ -7430,6 +7256,112 @@ func TestFolders(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+func TestFolderRelations(t *testing.T) {
+	mappedPath := filepath.Join(os.TempDir(), "mapped_path")
+	name := filepath.Base(mappedPath)
+	u := getTestUser()
+	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
+		BaseVirtualFolder: vfs.BaseVirtualFolder{
+			Name: name,
+		},
+		VirtualPath: "/mountu",
+	})
+	_, resp, err := httpdtest.AddUser(u, http.StatusInternalServerError)
+	assert.NoError(t, err, string(resp))
+	g := getTestGroup()
+	g.VirtualFolders = append(g.VirtualFolders, vfs.VirtualFolder{
+		BaseVirtualFolder: vfs.BaseVirtualFolder{
+			Name: name,
+		},
+		VirtualPath: "/mountg",
+	})
+	_, resp, err = httpdtest.AddGroup(g, http.StatusInternalServerError)
+	assert.NoError(t, err, string(resp))
+	f := vfs.BaseVirtualFolder{
+		Name:       name,
+		MappedPath: mappedPath,
+	}
+	folder, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
+	assert.Len(t, folder.Users, 0)
+	assert.Len(t, folder.Groups, 0)
+
+	user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
+	assert.NoError(t, err, string(resp))
+	group, resp, err := httpdtest.AddGroup(g, http.StatusCreated)
+	assert.NoError(t, err, string(resp))
+
+	folder, _, err = httpdtest.GetFolderByName(folder.Name, http.StatusOK)
+	assert.NoError(t, err)
+	assert.Len(t, folder.Users, 1)
+	assert.Len(t, folder.Groups, 1)
+
+	user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
+	assert.NoError(t, err)
+	if assert.Len(t, user.VirtualFolders, 1) {
+		assert.Equal(t, mappedPath, user.VirtualFolders[0].MappedPath)
+	}
+
+	group, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)
+	assert.NoError(t, err)
+	if assert.Len(t, group.VirtualFolders, 1) {
+		assert.Equal(t, mappedPath, group.VirtualFolders[0].MappedPath)
+	}
+	// update the folder and check the modified field on user and group
+	mappedPath = filepath.Join(os.TempDir(), "mapped_path")
+	folder.MappedPath = mappedPath
+	_, _, err = httpdtest.UpdateFolder(folder, http.StatusOK)
+	assert.NoError(t, err)
+	user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
+	assert.NoError(t, err)
+	if assert.Len(t, user.VirtualFolders, 1) {
+		assert.Equal(t, mappedPath, user.VirtualFolders[0].MappedPath)
+	}
+
+	group, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)
+	assert.NoError(t, err)
+	if assert.Len(t, group.VirtualFolders, 1) {
+		assert.Equal(t, mappedPath, group.VirtualFolders[0].MappedPath)
+	}
+
+	_, err = httpdtest.RemoveUser(user, http.StatusOK)
+	assert.NoError(t, err)
+	_, err = httpdtest.RemoveGroup(group, http.StatusOK)
+	assert.NoError(t, err)
+	folder, _, err = httpdtest.GetFolderByName(folder.Name, http.StatusOK)
+	assert.NoError(t, err)
+	assert.Len(t, folder.Users, 0)
+	assert.Len(t, folder.Groups, 0)
+
+	user, resp, err = httpdtest.AddUser(u, http.StatusCreated)
+	assert.NoError(t, err, string(resp))
+	assert.Len(t, user.VirtualFolders, 1)
+	group, resp, err = httpdtest.AddGroup(g, http.StatusCreated)
+	assert.NoError(t, err, string(resp))
+	assert.Len(t, group.VirtualFolders, 1)
+
+	folder, _, err = httpdtest.GetFolderByName(folder.Name, http.StatusOK)
+	assert.NoError(t, err)
+	assert.Len(t, folder.Users, 1)
+	assert.Len(t, folder.Groups, 1)
+
+	_, err = httpdtest.RemoveFolder(folder, http.StatusOK)
+	assert.NoError(t, err)
+
+	user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
+	assert.NoError(t, err)
+	assert.Len(t, user.VirtualFolders, 0)
+
+	group, _, err = httpdtest.GetGroupByName(group.Name, http.StatusOK)
+	assert.NoError(t, err)
+	assert.Len(t, group.VirtualFolders, 0)
+
+	_, err = httpdtest.RemoveUser(user, http.StatusOK)
+	assert.NoError(t, err)
+	_, err = httpdtest.RemoveGroup(group, http.StatusOK)
+	assert.NoError(t, err)
+}
+
 func TestDumpdata(t *testing.T) {
 	err := dataprovider.Close()
 	assert.NoError(t, err)
@@ -15550,17 +15482,22 @@ func TestWebGetFiles(t *testing.T) {
 
 func TestRenameDifferentResource(t *testing.T) {
 	folderName := "foldercryptfs"
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: filepath.Join(os.TempDir(), "folderName"),
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret("super secret"),
+			},
+		},
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	u := getTestUser()
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: filepath.Join(os.TempDir(), "folderName"),
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					Passphrase: kms.NewPlainSecret("super secret"),
-				},
-			},
+			Name: folderName,
 		},
 		VirtualPath: "/folderPath",
 	})
@@ -16141,23 +16078,29 @@ func TestBufferedWebFilesAPI(t *testing.T) {
 	folderName := filepath.Base(mappedPath)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					OSFsConfig: sdk.OSFsConfig{
-						WriteBufferSize: 3,
-						ReadBufferSize:  2,
-					},
-					Passphrase: kms.NewPlainSecret(defaultPassword),
-				},
-			},
+			Name: folderName,
 		},
 		VirtualPath: vdirPath,
 		QuotaFiles:  -1,
 		QuotaSize:   -1,
 	})
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				OSFsConfig: sdk.OSFsConfig{
+					WriteBufferSize: 3,
+					ReadBufferSize:  2,
+				},
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
+
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	webAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
@@ -16696,13 +16639,19 @@ func TestWebAPIVFolder(t *testing.T) {
 	folderName := filepath.Base(mappedPath)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdir,
 		QuotaSize:   -1,
 		QuotaFiles:  -1,
 	})
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
+
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 
@@ -19102,7 +19051,7 @@ func TestWebAdminPermissions(t *testing.T) {
 	resp, err = httpclient.GetHTTPClient().Do(req)
 	require.NoError(t, err)
 	defer resp.Body.Close()
-	assert.Equal(t, http.StatusOK, resp.StatusCode)
+	assert.Equal(t, http.StatusForbidden, resp.StatusCode)
 
 	req, err = http.NewRequest(http.MethodGet, httpBaseURL+webStatusPath, nil)
 	assert.NoError(t, err)
@@ -20072,18 +20021,10 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
 	checkResponseCode(t, http.StatusForbidden, rr)
 	require.Contains(t, rr.Body.String(), "unable to verify form token")
 
-	form.Set(csrfFormToken, csrfToken)
-	b, contentType, _ = getMultipartFormData(form, "", "")
-	req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
-	setJWTCookieForReq(req, token)
-	req.Header.Set("Content-Type", contentType)
-	rr = executeRequest(req)
-	checkResponseCode(t, http.StatusBadRequest, rr)
-	require.Contains(t, rr.Body.String(), "invalid folder mapped path")
-
 	folder, resp, err := httpdtest.AddFolder(folder, http.StatusCreated)
 	assert.NoError(t, err, string(resp))
 
+	form.Set(csrfFormToken, csrfToken)
 	b, contentType, _ = getMultipartFormData(form, "", "")
 	req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
 	setJWTCookieForReq(req, token)
@@ -20109,8 +20050,6 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
 	assert.Equal(t, path.Join("/base", user2.Username), user2.Filters.StartDirectory)
 	assert.Equal(t, 0, user2.Filters.DefaultSharesExpiration)
 	assert.Equal(t, folder.Name, folder1.Name)
-	assert.Equal(t, folder.MappedPath, folder1.MappedPath)
-	assert.Equal(t, folder.Description, folder1.Description)
 	assert.Len(t, user1.PublicKeys, 0)
 	assert.Len(t, user2.PublicKeys, 1)
 	assert.Len(t, user1.VirtualFolders, 1)

+ 11 - 11
internal/httpd/server.go

@@ -1297,11 +1297,11 @@ func (s *httpdServer) initializeRouter() {
 			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser)
 			router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser)
 			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}/2fa/disable", disableUser2FA)
-			router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(folderPath, getFolders)
-			router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(folderPath+"/{name}", getFolderByName)
-			router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(folderPath, addFolder)
-			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(folderPath+"/{name}", updateFolder)
-			router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(folderPath+"/{name}", deleteFolder)
+			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath, getFolders)
+			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath+"/{name}", getFolderByName)
+			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(folderPath, addFolder)
+			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Put(folderPath+"/{name}", updateFolder)
+			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Delete(folderPath+"/{name}", deleteFolder)
 			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath, getGroups)
 			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath+"/{name}", getGroupByName)
 			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(groupPath, addGroup)
@@ -1652,11 +1652,11 @@ func (s *httpdServer) setupWebAdminRoutes() {
 				Delete(webGroupPath+"/{name}", deleteGroup)
 			router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
 				Get(webConnectionsPath, s.handleWebGetConnections)
-			router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
+			router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
 				Get(webFoldersPath, s.handleWebGetFolders)
-			router.With(s.checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie).
+			router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
 				Get(webFolderPath, s.handleWebAddFolderGet)
-			router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(webFolderPath, s.handleWebAddFolderPost)
+			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath, s.handleWebAddFolderPost)
 			router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus), s.refreshCookie).
 				Get(webStatusPath, s.handleWebGetStatus)
 			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
@@ -1672,11 +1672,11 @@ func (s *httpdServer) setupWebAdminRoutes() {
 				Delete(webAdminPath+"/{username}", deleteAdmin)
 			router.With(s.checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader).
 				Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection)
-			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie).
+			router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
 				Get(webFolderPath+"/{name}", s.handleWebUpdateFolderGet)
-			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Post(webFolderPath+"/{name}",
+			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath+"/{name}",
 				s.handleWebUpdateFolderPost)
-			router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
+			router.With(s.checkPerm(dataprovider.PermAdminManageFolders), verifyCSRFHeader).
 				Delete(webFolderPath+"/{name}", deleteFolder)
 			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
 				Post(webScanVFolderPath+"/{name}", startFolderQuotaScan)

+ 2 - 2
internal/httpdtest/httpdtest.go

@@ -2105,8 +2105,8 @@ func compareVirtualFolders(expected []vfs.VirtualFolder, actual []vfs.VirtualFol
 		found := false
 		for _, v1 := range expected {
 			if path.Clean(v.VirtualPath) == path.Clean(v1.VirtualPath) {
-				if err := checkFolder(&v1.BaseVirtualFolder, &v.BaseVirtualFolder); err != nil {
-					return err
+				if dataprovider.ConvertName(v1.Name) != v.Name {
+					return errors.New("virtual folder name mismatch")
 				}
 				if v.QuotaSize != v1.QuotaSize {
 					return errors.New("vfolder quota size mismatch")

+ 15 - 10
internal/sftpd/httpfs_test.go

@@ -194,19 +194,24 @@ func TestHTTPFsVirtualFolder(t *testing.T) {
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
 			Name: folderName,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.HTTPFilesystemProvider,
-				HTTPConfig: vfs.HTTPFsConfig{
-					BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
-						Endpoint:          fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
-						Username:          defaultHTTPFsUsername,
-						EqualityCheckMode: 1,
-					},
-				},
-			},
 		},
 		VirtualPath: vdirPath,
 	})
+	f := vfs.BaseVirtualFolder{
+		Name: folderName,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.HTTPFilesystemProvider,
+			HTTPConfig: vfs.HTTPFsConfig{
+				BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
+					Endpoint:          fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
+					Username:          defaultHTTPFsUsername,
+					EqualityCheckMode: 1,
+				},
+			},
+		},
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)

+ 350 - 136
internal/sftpd/sftpd_test.go

@@ -3888,13 +3888,13 @@ func TestLoginExternalAuth(t *testing.T) {
 		u := getTestUser(usePubKey)
 		u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 			BaseVirtualFolder: vfs.BaseVirtualFolder{
-				Name:       folderName,
-				MappedPath: mappedPath,
+				Name: folderName,
 			},
 			VirtualPath: "/vpath",
 			QuotaFiles:  1 + authScope,
 			QuotaSize:   10 + int64(authScope),
 		})
+
 		err := dataprovider.Close()
 		assert.NoError(t, err)
 		err = config.LoadConfig(configDir, "")
@@ -3907,6 +3907,13 @@ func TestLoginExternalAuth(t *testing.T) {
 		err = dataprovider.Initialize(providerConf, configDir, true)
 		assert.NoError(t, err)
 
+		f := vfs.BaseVirtualFolder{
+			Name:       folderName,
+			MappedPath: mappedPath,
+		}
+		_, _, err = httpdtest.AddFolder(f, http.StatusCreated)
+		assert.NoError(t, err)
+
 		conn, client, err := getSftpClient(u, usePubKey)
 		if assert.NoError(t, err) {
 			defer conn.Close()
@@ -5013,8 +5020,7 @@ func TestVirtualFolders(t *testing.T) {
 	testDir1 := "/userDir1"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdirPath,
 	})
@@ -5022,6 +5028,12 @@ func TestVirtualFolders(t *testing.T) {
 	u.Permissions[testDir1] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermRename}
 	u.Permissions[path.Join(testDir1, "subdir")] = []string{dataprovider.PermRename}
 
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)
@@ -5121,8 +5133,7 @@ func TestVirtualFoldersQuotaLimit(t *testing.T) {
 	vdirPath2 := "/vdir2" //nolint:goconst
 	u1.VirtualFolders = append(u1.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		QuotaFiles:  -1,
@@ -5130,8 +5141,7 @@ func TestVirtualFoldersQuotaLimit(t *testing.T) {
 	})
 	u1.VirtualFolders = append(u1.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		QuotaFiles:  1,
@@ -5145,8 +5155,7 @@ func TestVirtualFoldersQuotaLimit(t *testing.T) {
 	u2.QuotaSize = testFileSize + 1
 	u2.VirtualFolders = append(u2.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		QuotaFiles:  -1,
@@ -5154,8 +5163,7 @@ func TestVirtualFoldersQuotaLimit(t *testing.T) {
 	})
 	u2.VirtualFolders = append(u2.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		QuotaFiles:  0,
@@ -5167,6 +5175,18 @@ func TestVirtualFoldersQuotaLimit(t *testing.T) {
 		assert.NoError(t, err)
 		err = os.MkdirAll(mappedPath2, os.ModePerm)
 		assert.NoError(t, err)
+		f1 := vfs.BaseVirtualFolder{
+			Name:       folderName1,
+			MappedPath: mappedPath1,
+		}
+		_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+		assert.NoError(t, err)
+		f2 := vfs.BaseVirtualFolder{
+			Name:       folderName2,
+			MappedPath: mappedPath2,
+		}
+		_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+		assert.NoError(t, err)
 		user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 		assert.NoError(t, err)
 		conn, client, err := getSftpClient(user, usePubKey)
@@ -5293,17 +5313,6 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
 	user1.VirtualFolders = append(user1.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
 			Name: sftpFloderName,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.SFTPFilesystemProvider,
-				SFTPConfig: vfs.SFTPFsConfig{
-					BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
-						Endpoint:          sftpServerAddr,
-						Username:          user2.Username,
-						EqualityCheckMode: 1,
-					},
-					Password: kms.NewPlainSecret(defaultPassword),
-				},
-			},
 		},
 		VirtualPath: "/vdir",
 	})
@@ -5324,6 +5333,22 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
 		},
 		Password: kms.NewPlainSecret(defaultPassword),
 	}
+	f := vfs.BaseVirtualFolder{
+		Name: sftpFloderName,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.SFTPFilesystemProvider,
+			SFTPConfig: vfs.SFTPFsConfig{
+				BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+					Endpoint:          sftpServerAddr,
+					Username:          user2.Username,
+					EqualityCheckMode: 1,
+				},
+				Password: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 
 	user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)
 	assert.NoError(t, err, string(resp))
@@ -5405,13 +5430,6 @@ func TestNestedVirtualFolders(t *testing.T) {
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
 			Name: folderNameCrypt,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					Passphrase: kms.NewPlainSecret(defaultPassword),
-				},
-			},
-			MappedPath: mappedPathCrypt,
 		},
 		VirtualPath: vdirCryptPath,
 		QuotaFiles:  100,
@@ -5421,8 +5439,7 @@ func TestNestedVirtualFolders(t *testing.T) {
 	vdirPath := "/vdir/local"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdirPath,
 		QuotaFiles:  -1,
@@ -5433,13 +5450,36 @@ func TestNestedVirtualFolders(t *testing.T) {
 	vdirNestedPath := "/vdir/crypt/nested"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderNameNested,
-			MappedPath: mappedPathNested,
+			Name: folderNameNested,
 		},
 		VirtualPath: vdirNestedPath,
 		QuotaFiles:  -1,
 		QuotaSize:   -1,
 	})
+	f1 := vfs.BaseVirtualFolder{
+		Name: folderNameCrypt,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+		MappedPath: mappedPathCrypt,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+	f3 := vfs.BaseVirtualFolder{
+		Name:       folderNameNested,
+		MappedPath: mappedPathNested,
+	}
+	_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)
+	assert.NoError(t, err)
 	user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err, string(resp))
 	conn, client, err := getSftpClient(user, usePubKey)
@@ -5597,23 +5637,28 @@ func TestBufferedUser(t *testing.T) {
 	folderName := filepath.Base(mappedPath)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					OSFsConfig: sdk.OSFsConfig{
-						WriteBufferSize: 3,
-						ReadBufferSize:  2,
-					},
-					Passphrase: kms.NewPlainSecret(defaultPassword),
-				},
-			},
+			Name: folderName,
 		},
 		VirtualPath: vdirPath,
 		QuotaFiles:  -1,
 		QuotaSize:   -1,
 	})
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				OSFsConfig: sdk.OSFsConfig{
+					WriteBufferSize: 3,
+					ReadBufferSize:  2,
+				},
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)
@@ -5718,12 +5763,17 @@ func TestTruncateQuotaLimits(t *testing.T) {
 	vdirPath := "/vmapped"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdirPath,
 		QuotaFiles:  10,
 	})
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err = httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	u = getTestSFTPUser(usePubKey)
@@ -5932,8 +5982,7 @@ func TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) {
 	vdirPath3 := "/vdir3"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		QuotaFiles:  2,
@@ -5941,8 +5990,7 @@ func TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			MappedPath: mappedPath2,
-			Name:       folderName2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		QuotaFiles:  0,
@@ -5950,8 +5998,7 @@ func TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName3,
-			MappedPath: mappedPath3,
+			Name: folderName3,
 		},
 		VirtualPath: vdirPath3,
 		QuotaFiles:  2,
@@ -5963,6 +6010,25 @@ func TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.MkdirAll(mappedPath3, os.ModePerm)
 	assert.NoError(t, err)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+	f3 := vfs.BaseVirtualFolder{
+		Name:       folderName3,
+		MappedPath: mappedPath3,
+	}
+	_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)
+	assert.NoError(t, err)
+
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)
@@ -6070,8 +6136,7 @@ func TestVirtualFoldersQuotaValues(t *testing.T) {
 	folderName2 := filepath.Base(mappedPath2)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -6080,8 +6145,7 @@ func TestVirtualFoldersQuotaValues(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		// quota is unlimited and excluded from user's one
@@ -6092,6 +6156,18 @@ func TestVirtualFoldersQuotaValues(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.MkdirAll(mappedPath2, os.ModePerm)
 	assert.NoError(t, err)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)
@@ -6170,8 +6246,7 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
 	folderName2 := filepath.Base(mappedPath2)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -6180,15 +6255,26 @@ func TestQuotaRenameInsideSameVirtualFolder(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		// quota is unlimited and excluded from user's one
 		QuotaFiles: 0,
 		QuotaSize:  0,
 	})
-	err := os.MkdirAll(mappedPath1, os.ModePerm)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+	err = os.MkdirAll(mappedPath1, os.ModePerm)
 	assert.NoError(t, err)
 	err = os.MkdirAll(mappedPath2, os.ModePerm)
 	assert.NoError(t, err)
@@ -6365,8 +6451,7 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
 	vdirPath2 := "/vdir2"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -6375,8 +6460,7 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		// quota is unlimited and excluded from user's one
@@ -6387,6 +6471,18 @@ func TestQuotaRenameBetweenVirtualFolder(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.MkdirAll(mappedPath2, os.ModePerm)
 	assert.NoError(t, err)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)
@@ -6577,8 +6673,7 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) {
 	vdirPath2 := "/vdir2"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -6587,8 +6682,7 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		// quota is unlimited and excluded from user's one
@@ -6599,6 +6693,18 @@ func TestQuotaRenameFromVirtualFolder(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.MkdirAll(mappedPath2, os.ModePerm)
 	assert.NoError(t, err)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)
@@ -6792,8 +6898,7 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
 	vdirPath2 := "/vdir2"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -6802,8 +6907,7 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		// quota is unlimited and excluded from user's one
@@ -6814,6 +6918,18 @@ func TestQuotaRenameToVirtualFolder(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.MkdirAll(mappedPath2, os.ModePerm)
 	assert.NoError(t, err)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)
@@ -7019,8 +7135,7 @@ func TestVirtualFoldersLink(t *testing.T) {
 	vdirPath2 := "/vdir2"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -7029,8 +7144,7 @@ func TestVirtualFoldersLink(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		// quota is unlimited and excluded from user's one
@@ -7041,6 +7155,18 @@ func TestVirtualFoldersLink(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.MkdirAll(mappedPath2, os.ModePerm)
 	assert.NoError(t, err)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)
@@ -7168,8 +7294,7 @@ func TestVFolderQuotaSize(t *testing.T) {
 	vdirPath2 := "/vpath2"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		// quota is included in the user's one
@@ -7178,8 +7303,7 @@ func TestVFolderQuotaSize(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		QuotaFiles:  1,
@@ -7192,6 +7316,19 @@ func TestVFolderQuotaSize(t *testing.T) {
 	testFilePath := filepath.Join(homeBasePath, testFileName)
 	err = createTestFile(testFilePath, testFileSize)
 	assert.NoError(t, err)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)
@@ -8894,8 +9031,7 @@ func TestSSHCopy(t *testing.T) {
 	vdirPath2 := "/vdir2/subdir"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		QuotaFiles:  -1,
@@ -8903,8 +9039,7 @@ func TestSSHCopy(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		QuotaFiles:  100,
@@ -8920,6 +9055,18 @@ func TestSSHCopy(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.MkdirAll(mappedPath2, os.ModePerm)
 	assert.NoError(t, err)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	testDir := "adir"
@@ -9176,8 +9323,7 @@ func TestSSHCopyQuotaLimits(t *testing.T) {
 	vdirPath2 := "/vdir2"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		QuotaFiles:  -1,
@@ -9185,8 +9331,7 @@ func TestSSHCopyQuotaLimits(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		QuotaFiles:  3,
@@ -9202,6 +9347,18 @@ func TestSSHCopyQuotaLimits(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.MkdirAll(mappedPath2, os.ModePerm)
 	assert.NoError(t, err)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)
@@ -9345,8 +9502,7 @@ func TestSSHRemove(t *testing.T) {
 	vdirPath2 := "/vdir2/sub"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		QuotaFiles:  -1,
@@ -9354,14 +9510,25 @@ func TestSSHRemove(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		QuotaFiles:  100,
 		QuotaSize:   0,
 	})
-	err := os.MkdirAll(mappedPath1, os.ModePerm)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+	err = os.MkdirAll(mappedPath1, os.ModePerm)
 	assert.NoError(t, err)
 	err = os.MkdirAll(mappedPath2, os.ModePerm)
 	assert.NoError(t, err)
@@ -9485,8 +9652,7 @@ func TestSSHRemoveCryptFs(t *testing.T) {
 	vdirPath2 := "/vdir2/sub"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		QuotaFiles:  -1,
@@ -9494,19 +9660,31 @@ func TestSSHRemoveCryptFs(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					Passphrase: kms.NewPlainSecret(defaultPassword),
-				},
-			},
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		QuotaFiles:  100,
 		QuotaSize:   0,
 	})
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	conn, client, err := getSftpClient(user, usePubKey)
@@ -9658,13 +9836,18 @@ func TestGitIncludedVirtualFolders(t *testing.T) {
 	folderName := filepath.Base(mappedPath)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: "/" + repoName,
 		QuotaFiles:  -1,
 		QuotaSize:   -1,
 	})
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 
@@ -9731,14 +9914,19 @@ func TestGitQuotaVirtualFolders(t *testing.T) {
 	folderName := filepath.Base(mappedPath)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: "/" + repoName,
 		QuotaFiles:  0,
 		QuotaSize:   0,
 	})
-	err := os.MkdirAll(mappedPath, os.ModePerm)
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
+	err = os.MkdirAll(mappedPath, os.ModePerm)
 	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
@@ -10209,12 +10397,17 @@ func TestSCPVirtualFolders(t *testing.T) {
 	vdirPath := "/vdir"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdirPath,
 	})
-	err := os.MkdirAll(mappedPath, os.ModePerm)
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
+	err = os.MkdirAll(mappedPath, os.ModePerm)
 	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
@@ -10266,16 +10459,6 @@ func TestSCPNestedFolders(t *testing.T) {
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
 			Name: folderNameSFTP,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.SFTPFilesystemProvider,
-				SFTPConfig: vfs.SFTPFsConfig{
-					BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
-						Endpoint: sftpServerAddr,
-						Username: baseUser.Username,
-					},
-					Password: kms.NewPlainSecret(defaultPassword),
-				},
-			},
 		},
 		VirtualPath: vdirSFTPPath,
 	})
@@ -10285,17 +10468,38 @@ func TestSCPNestedFolders(t *testing.T) {
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
 			Name: folderNameCrypt,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					Passphrase: kms.NewPlainSecret(defaultPassword),
-				},
-			},
-			MappedPath: mappedPathCrypt,
 		},
 		VirtualPath: vdirCryptPath,
 	})
 
+	f1 := vfs.BaseVirtualFolder{
+		Name: folderNameSFTP,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.SFTPFilesystemProvider,
+			SFTPConfig: vfs.SFTPFsConfig{
+				BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+					Endpoint: sftpServerAddr,
+					Username: baseUser.Username,
+				},
+				Password: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name: folderNameCrypt,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+		MappedPath: mappedPathCrypt,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+
 	user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err, string(resp))
 	baseDirDownPath := filepath.Join(os.TempDir(), "basedir-down")
@@ -10398,8 +10602,7 @@ func TestSCPVirtualFoldersQuota(t *testing.T) {
 	vdirPath2 := "/vdir2"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName1,
-			MappedPath: mappedPath1,
+			Name: folderName1,
 		},
 		VirtualPath: vdirPath1,
 		QuotaFiles:  -1,
@@ -10407,14 +10610,25 @@ func TestSCPVirtualFoldersQuota(t *testing.T) {
 	})
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName2,
-			MappedPath: mappedPath2,
+			Name: folderName2,
 		},
 		VirtualPath: vdirPath2,
 		QuotaFiles:  0,
 		QuotaSize:   0,
 	})
-	err := os.MkdirAll(mappedPath1, os.ModePerm)
+	f1 := vfs.BaseVirtualFolder{
+		Name:       folderName1,
+		MappedPath: mappedPath1,
+	}
+	_, _, err := httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName2,
+		MappedPath: mappedPath2,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+	err = os.MkdirAll(mappedPath1, os.ModePerm)
 	assert.NoError(t, err)
 	err = os.MkdirAll(mappedPath2, os.ModePerm)
 	assert.NoError(t, err)

+ 1 - 1
internal/version/version.go

@@ -17,7 +17,7 @@ package version
 
 import "strings"
 
-const version = "2.5.4-dev"
+const version = "2.5.99-dev"
 
 var (
 	commit = ""

+ 15 - 5
internal/webdavd/internal_test.go

@@ -1074,10 +1074,15 @@ func TestBasicUsersCache(t *testing.T) {
 	_, ok = dataprovider.GetCachedWebDAVUser(username)
 	assert.True(t, ok)
 	folderName := "testFolder"
+	f := &vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: filepath.Join(os.TempDir(), "mapped"),
+	}
+	err = dataprovider.AddFolder(f, "", "", "")
+	assert.NoError(t, err)
 	user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: filepath.Join(os.TempDir(), "mapped"),
+			Name: folderName,
 		},
 		VirtualPath: "/vdir",
 	})
@@ -1123,12 +1128,17 @@ func TestCachedUserWithFolders(t *testing.T) {
 	u.Permissions["/"] = []string{dataprovider.PermAny}
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: filepath.Join(os.TempDir(), folderName),
+			Name: folderName,
 		},
 		VirtualPath: "/vpath",
 	})
-	err := dataprovider.AddUser(&u, "", "", "")
+	f := &vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: filepath.Join(os.TempDir(), folderName),
+	}
+	err := dataprovider.AddFolder(f, "", "", "")
+	assert.NoError(t, err)
+	err = dataprovider.AddUser(&u, "", "", "")
 	assert.NoError(t, err)
 	user, err := dataprovider.UserExists(u.Username, "")
 	assert.NoError(t, err)

+ 76 - 40
internal/webdavd/webdavd_test.go

@@ -732,23 +732,28 @@ func TestBufferedUser(t *testing.T) {
 	folderName := filepath.Base(mappedPath)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					OSFsConfig: sdk.OSFsConfig{
-						WriteBufferSize: 3,
-						ReadBufferSize:  2,
-					},
-					Passphrase: kms.NewPlainSecret(defaultPassword),
-				},
-			},
+			Name: folderName,
 		},
 		VirtualPath: vdirPath,
 		QuotaFiles:  -1,
 		QuotaSize:   -1,
 	})
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				OSFsConfig: sdk.OSFsConfig{
+					WriteBufferSize: 3,
+					ReadBufferSize:  2,
+				},
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 
@@ -2535,16 +2540,21 @@ func TestUploadOverwriteVfolder(t *testing.T) {
 	vdir := "/vdir"
 	mappedPath := filepath.Join(os.TempDir(), "mappedDir")
 	folderName := filepath.Base(mappedPath)
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdir,
 		QuotaSize:   -1,
 		QuotaFiles:  -1,
 	})
-	err := os.MkdirAll(mappedPath, os.ModePerm)
+	err = os.MkdirAll(mappedPath, os.ModePerm)
 	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
@@ -2601,13 +2611,18 @@ func TestOsErrors(t *testing.T) {
 	folderName := filepath.Base(mappedPath)
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdir,
 		QuotaSize:   -1,
 		QuotaFiles:  -1,
 	})
+	f := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	client := getWebDavClient(user, false, nil)
@@ -3052,19 +3067,10 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
 	user2.Username += "2"
 	// user1 is a local account with a virtual SFTP folder to user2
 	// user2 has user1 as SFTP fs
+	folderName := "sftp"
 	user1.VirtualFolders = append(user1.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name: "sftp",
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.SFTPFilesystemProvider,
-				SFTPConfig: vfs.SFTPFsConfig{
-					BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
-						Endpoint: sftpServerAddr,
-						Username: user2.Username,
-					},
-					Password: kms.NewPlainSecret(defaultPassword),
-				},
-			},
+			Name: folderName,
 		},
 		VirtualPath: "/vdir",
 	})
@@ -3076,6 +3082,21 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
 		},
 		Password: kms.NewPlainSecret(defaultPassword),
 	}
+	f := vfs.BaseVirtualFolder{
+		Name: folderName,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.SFTPFilesystemProvider,
+			SFTPConfig: vfs.SFTPFsConfig{
+				BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+					Endpoint: sftpServerAddr,
+					Username: user2.Username,
+				},
+				Password: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+	}
+	_, _, err := httpdtest.AddFolder(f, http.StatusCreated)
+	assert.NoError(t, err)
 
 	user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)
 	assert.NoError(t, err, string(resp))
@@ -3112,7 +3133,7 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.RemoveAll(user2.GetHomeDir())
 	assert.NoError(t, err)
-	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: "sftp"}, http.StatusOK)
+	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)
 	assert.NoError(t, err)
 }
 
@@ -3127,13 +3148,6 @@ func TestNestedVirtualFolders(t *testing.T) {
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
 			Name: folderNameCrypt,
-			FsConfig: vfs.Filesystem{
-				Provider: sdk.CryptedFilesystemProvider,
-				CryptConfig: vfs.CryptFsConfig{
-					Passphrase: kms.NewPlainSecret(defaultPassword),
-				},
-			},
-			MappedPath: mappedPathCrypt,
 		},
 		VirtualPath: vdirCryptPath,
 	})
@@ -3142,8 +3156,7 @@ func TestNestedVirtualFolders(t *testing.T) {
 	vdirPath := "/vdir/local"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderName,
-			MappedPath: mappedPath,
+			Name: folderName,
 		},
 		VirtualPath: vdirPath,
 	})
@@ -3152,13 +3165,36 @@ func TestNestedVirtualFolders(t *testing.T) {
 	vdirNestedPath := "/vdir/crypt/nested"
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
 		BaseVirtualFolder: vfs.BaseVirtualFolder{
-			Name:       folderNameNested,
-			MappedPath: mappedPathNested,
+			Name: folderNameNested,
 		},
 		VirtualPath: vdirNestedPath,
 		QuotaFiles:  -1,
 		QuotaSize:   -1,
 	})
+	f1 := vfs.BaseVirtualFolder{
+		Name: folderNameCrypt,
+		FsConfig: vfs.Filesystem{
+			Provider: sdk.CryptedFilesystemProvider,
+			CryptConfig: vfs.CryptFsConfig{
+				Passphrase: kms.NewPlainSecret(defaultPassword),
+			},
+		},
+		MappedPath: mappedPathCrypt,
+	}
+	_, _, err = httpdtest.AddFolder(f1, http.StatusCreated)
+	assert.NoError(t, err)
+	f2 := vfs.BaseVirtualFolder{
+		Name:       folderName,
+		MappedPath: mappedPath,
+	}
+	_, _, err = httpdtest.AddFolder(f2, http.StatusCreated)
+	assert.NoError(t, err)
+	f3 := vfs.BaseVirtualFolder{
+		Name:       folderNameNested,
+		MappedPath: mappedPathNested,
+	}
+	_, _, err = httpdtest.AddFolder(f3, http.StatusCreated)
+	assert.NoError(t, err)
 	sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 

+ 4 - 2
openapi/openapi.yaml

@@ -29,7 +29,7 @@ info:
     SFTPGo supports groups to simplify the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user.
     The SFTPGo WebClient allows end users to change their credentials, browse and manage their files in the browser and setup two-factor authentication which works with Authy, Google Authenticator and other compatible apps.
     From the WebClient each authorized user can also create HTTP/S links to externally share files and folders securely, by setting limits to the number of downloads/uploads, protecting the share with a password, limiting access by source IP address, setting an automatic expiration date.
-  version: 2.5.4-dev
+  version: 2.5.99-dev
   contact:
     name: API support
     url: 'https://github.com/drakkan/sftpgo'
@@ -4993,6 +4993,7 @@ components:
         - close_conns
         - view_status
         - manage_admins
+        - manage_folders
         - manage_groups
         - manage_apikeys
         - quota_scans
@@ -5016,6 +5017,7 @@ components:
           * `close_conns` - close active connections is allowed
           * `view_status` - view the server status is allowed
           * `manage_admins` - manage other admins is allowed
+          * `manage_folders` - manage folders is allowed
           * `manage_groups` - manage groups is allowed
           * `manage_apikeys` - manage API keys is allowed
           * `quota_scans` - view and start quota scans is allowed
@@ -5919,7 +5921,7 @@ components:
           type: array
           items:
             $ref: '#/components/schemas/VirtualFolder'
-          description: mapping between virtual SFTPGo paths and virtual folders. If one or more of the specified folders are not inside the dataprovider they will be automatically created. You have to create the folder on the filesystem yourself
+          description: mapping between virtual SFTPGo paths and virtual folders
         uid:
           type: integer
           format: int32

+ 1 - 1
templates/webadmin/base.html

@@ -83,7 +83,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
             </li>
             {{end}}
 
-            {{ if .LoggedAdmin.HasPermission "view_users"}}
+            {{ if .LoggedAdmin.HasPermission "manage_folders"}}
             <li class="nav-item {{if eq .CurrentURL .FoldersURL}}active{{end}}">
                 <a class="nav-link" href="{{.FoldersURL}}">
                     <i class="fas fa-folder"></i>

Some files were not shown because too many files changed in this diff