Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions examples/ldapauthserver/httpd/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ type SFTPGoFilesystem struct {
}

type virtualFolder struct {
VirtualPath string `json:"virtual_path"`
MappedPath string `json:"mapped_path"`
VirtualPath string `json:"virtual_path"`
VirtualSubdirectory string `json:"virtual_subdirectory,omitempty"`
MappedPath string `json:"mapped_path"`
}

// SFTPGoUser defines an SFTPGo user
Expand Down
36 changes: 34 additions & 2 deletions examples/php-activedirectory-http-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,26 @@ $virtual_folders['example'] = [
"quota_files" => -1
]
];

// Alternative example using virtual_subdirectory for multi-tenant shared storage:
$virtual_folders['shared_example'] = [
[
"name" => "shared-documents",
"mapped_path" => 'F:\files\shared\documents',
"virtual_path" => "/documents",
"virtual_subdirectory" => "#USERNAME#", // Each user gets their own subdirectory
"quota_size" => 1073741824, // 1GB limit per user
"quota_files" => 1000
],
[
"name" => "shared-projects",
"mapped_path" => 'F:\files\shared\projects',
"virtual_path" => "/projects",
"virtual_subdirectory" => "#DEPARTMENT#", // Users in same department share space
"quota_size" => 0, // Unlimited
"quota_files" => 0
]
];
```

## Example Connection "Output Object" Allowing For No Files in the User's Home Directory ("Root Directory") but Allowing for Files in the Public/Private Virtual Folders
Expand Down Expand Up @@ -99,8 +119,20 @@ $auto_groups_mode_virtual_folder_template = [
//"used_quota_files" => 0,
//"last_quota_update" => 0,
"virtual_path" => "/groups/#GROUP#",
"quota_size" => 0,
"quota_files" => 100000
"quota_size" => -1,
"quota_files" => -1
]
];

// Alternative group template using virtual_subdirectory for shared group storage:
$auto_groups_mode_shared_template = [
[
"name" => "shared-group-storage",
"mapped_path" => 'F:\files\shared\groups',
"virtual_path" => "/group_shared",
"virtual_subdirectory" => "#GROUP#", // Each group gets isolated subdirectory
"quota_size" => 5368709120, // 5GB per group
"quota_files" => 0
]
];

Expand Down
2 changes: 1 addition & 1 deletion internal/common/actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ func TestPreDeleteAction(t *testing.T) {
}
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny}
fs := vfs.NewOsFs("id", homeDir, "", nil)
fs := vfs.NewOsFs("id", homeDir, "", "", nil)
c := NewBaseConnection("id", ProtocolSFTP, "", "", user)

testfile := filepath.Join(user.HomeDir, "testfile")
Expand Down
4 changes: 2 additions & 2 deletions internal/common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@ func TestConnectionStatus(t *testing.T) {
Username: username,
},
}
fs := vfs.NewOsFs("", os.TempDir(), "", nil)
fs := vfs.NewOsFs("", os.TempDir(), "", "", nil)
c1 := NewBaseConnection("id1", ProtocolSFTP, "", "", user)
fakeConn1 := &fakeConnection{
BaseConnection: c1,
Expand Down Expand Up @@ -1271,7 +1271,7 @@ func TestPostConnectHook(t *testing.T) {

func TestCryptoConvertFileInfo(t *testing.T) {
name := "name"
fs, err := vfs.NewCryptFs("connID1", os.TempDir(), "", vfs.CryptFsConfig{
fs, err := vfs.NewCryptFs("connID1", os.TempDir(), "", "", vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret("secret"),
})
require.NoError(t, err)
Expand Down
12 changes: 6 additions & 6 deletions internal/common/connection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (fs *MockOsFs) Walk(_ string, walkFn filepath.WalkFunc) error {

func newMockOsFs(hasVirtualFolders bool, connectionID, rootDir, name string, err error) vfs.Fs {
return &MockOsFs{
Fs: vfs.NewOsFs(connectionID, rootDir, "", nil),
Fs: vfs.NewOsFs(connectionID, rootDir, "", "", nil),
name: name,
hasVirtualFolders: hasVirtualFolders,
err: err,
Expand Down Expand Up @@ -119,7 +119,7 @@ func TestRemoveErrors(t *testing.T) {
}
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny}
fs := vfs.NewOsFs("", os.TempDir(), "", nil)
fs := vfs.NewOsFs("", os.TempDir(), "", "", nil)
conn := NewBaseConnection("", ProtocolFTP, "", "", user)
err := conn.IsRemoveDirAllowed(fs, mappedPath, "/virtualpath1")
if assert.Error(t, err) {
Expand Down Expand Up @@ -164,7 +164,7 @@ func TestSetStatMode(t *testing.T) {
}

func TestRecursiveRenameWalkError(t *testing.T) {
fs := vfs.NewOsFs("", filepath.Clean(os.TempDir()), "", nil)
fs := vfs.NewOsFs("", filepath.Clean(os.TempDir()), "", "", nil)
conn := NewBaseConnection("", ProtocolWebDAV, "", "", dataprovider.User{
BaseUser: sdk.BaseUser{
Permissions: map[string][]string{
Expand Down Expand Up @@ -201,7 +201,7 @@ func TestCrossRenameFsErrors(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
}
fs := vfs.NewOsFs("", os.TempDir(), "", nil)
fs := vfs.NewOsFs("", os.TempDir(), "", "", nil)
conn := NewBaseConnection("", ProtocolWebDAV, "", "", dataprovider.User{})
dirPath := filepath.Join(os.TempDir(), "d")
err := os.Mkdir(dirPath, os.ModePerm)
Expand All @@ -228,7 +228,7 @@ func TestRenameVirtualFolders(t *testing.T) {
},
VirtualPath: vdir,
})
fs := vfs.NewOsFs("", os.TempDir(), "", nil)
fs := vfs.NewOsFs("", os.TempDir(), "", "", nil)
conn := NewBaseConnection("", ProtocolFTP, "", "", u)
res := conn.isRenamePermitted(fs, fs, "source", "target", vdir, "vdirtarget", nil)
assert.False(t, res)
Expand Down Expand Up @@ -380,7 +380,7 @@ func TestUpdateQuotaAfterRename(t *testing.T) {
}

func TestErrorsMapping(t *testing.T) {
fs := vfs.NewOsFs("", os.TempDir(), "", nil)
fs := vfs.NewOsFs("", os.TempDir(), "", "", nil)
conn := NewBaseConnection("", ProtocolSFTP, "", "", dataprovider.User{BaseUser: sdk.BaseUser{HomeDir: os.TempDir()}})
osErrorsProtocols := []string{ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolHTTPShare,
ProtocolDataRetention, ProtocolOIDC, protocolEventAction}
Expand Down
18 changes: 9 additions & 9 deletions internal/common/transfer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestTransferUpdateQuota(t *testing.T) {
transfer := BaseTransfer{
Connection: conn,
transferType: TransferUpload,
Fs: vfs.NewOsFs("", os.TempDir(), "", nil),
Fs: vfs.NewOsFs("", os.TempDir(), "", "", nil),
}
transfer.BytesReceived.Store(123)
errFake := errors.New("fake error")
Expand Down Expand Up @@ -76,7 +76,7 @@ func TestTransferThrottling(t *testing.T) {
DownloadBandwidth: 40,
},
}
fs := vfs.NewOsFs("", os.TempDir(), "", nil)
fs := vfs.NewOsFs("", os.TempDir(), "", "", nil)
testFileSize := int64(131072)
wantedUploadElapsed := 1000 * (testFileSize / 1024) / u.UploadBandwidth
wantedDownloadElapsed := 1000 * (testFileSize / 1024) / u.DownloadBandwidth
Expand Down Expand Up @@ -108,7 +108,7 @@ func TestTransferThrottling(t *testing.T) {

func TestRealPath(t *testing.T) {
testFile := filepath.Join(os.TempDir(), "afile.txt")
fs := vfs.NewOsFs("123", os.TempDir(), "", nil)
fs := vfs.NewOsFs("123", os.TempDir(), "", "", nil)
u := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: "user",
Expand Down Expand Up @@ -142,7 +142,7 @@ func TestRealPath(t *testing.T) {

func TestTruncate(t *testing.T) {
testFile := filepath.Join(os.TempDir(), "transfer_test_file")
fs := vfs.NewOsFs("123", os.TempDir(), "", nil)
fs := vfs.NewOsFs("123", os.TempDir(), "", "", nil)
u := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: "user",
Expand Down Expand Up @@ -211,7 +211,7 @@ func TestTransferErrors(t *testing.T) {
isCancelled = true
}
testFile := filepath.Join(os.TempDir(), "transfer_test_file")
fs := vfs.NewOsFs("id", os.TempDir(), "", nil)
fs := vfs.NewOsFs("id", os.TempDir(), "", "", nil)
u := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: "test",
Expand Down Expand Up @@ -302,7 +302,7 @@ func TestTransferErrors(t *testing.T) {

func TestRemovePartialCryptoFile(t *testing.T) {
testFile := filepath.Join(os.TempDir(), "transfer_test_file")
fs, err := vfs.NewCryptFs("id", os.TempDir(), "", vfs.CryptFsConfig{Passphrase: kms.NewPlainSecret("secret")})
fs, err := vfs.NewCryptFs("id", os.TempDir(), "", "", vfs.CryptFsConfig{Passphrase: kms.NewPlainSecret("secret")})
require.NoError(t, err)
u := dataprovider.User{
BaseUser: sdk.BaseUser{
Expand Down Expand Up @@ -334,7 +334,7 @@ func TestFTPMode(t *testing.T) {
transfer := BaseTransfer{
Connection: conn,
transferType: TransferUpload,
Fs: vfs.NewOsFs("", os.TempDir(), "", nil),
Fs: vfs.NewOsFs("", os.TempDir(), "", "", nil),
}
transfer.BytesReceived.Store(123)
assert.Empty(t, transfer.ftpMode)
Expand Down Expand Up @@ -393,7 +393,7 @@ func TestTransferQuota(t *testing.T) {

conn := NewBaseConnection("", ProtocolSFTP, "", "", user)
transfer := NewBaseTransfer(nil, conn, nil, "file.txt", "file.txt", "/transfer_test_file", TransferUpload,
0, 0, 0, 0, true, vfs.NewOsFs("", os.TempDir(), "", nil), dataprovider.TransferQuota{})
0, 0, 0, 0, true, vfs.NewOsFs("", os.TempDir(), "", "", nil), dataprovider.TransferQuota{})
err := transfer.CheckRead()
assert.NoError(t, err)
err = transfer.CheckWrite()
Expand Down Expand Up @@ -452,7 +452,7 @@ func TestUploadOutsideHomeRenameError(t *testing.T) {
transfer := BaseTransfer{
Connection: conn,
transferType: TransferUpload,
Fs: vfs.NewOsFs("", filepath.Join(os.TempDir(), "home"), "", nil),
Fs: vfs.NewOsFs("", filepath.Join(os.TempDir(), "home"), "", "", nil),
}
transfer.BytesReceived.Store(123)

Expand Down
12 changes: 6 additions & 6 deletions internal/dataprovider/bolt.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import (
)

const (
boltDatabaseVersion = 33
boltDatabaseVersion = 34
)

var (
Expand Down Expand Up @@ -3167,15 +3167,15 @@ func (p *BoltProvider) migrateDatabase() error {
providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err)
return err
case version == 29, version == 30, version == 31, version == 32:
logger.InfoToConsole("updating database schema version: %d -> 33", version)
providerLog(logger.LevelInfo, "updating database schema version: %d -> 33", version)
case version == 29, version == 30, version == 31, version == 32, version == 33:
logger.InfoToConsole("updating database schema version: %d -> 34", version)
providerLog(logger.LevelInfo, "updating database schema version: %d -> 34", version)
if version <= 31 {
if err := updateEventActions(); err != nil {
return err
}
}
return updateBoltDatabaseVersion(p.dbHandle, 33)
return updateBoltDatabaseVersion(p.dbHandle, 34)
default:
if version > boltDatabaseVersion {
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
Expand All @@ -3197,7 +3197,7 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error { //nolint:gocycl
return errors.New("current version match target version, nothing to do")
}
switch dbVersion.Version {
case 30, 31, 32, 33:
case 30, 31, 32, 33, 34:
logger.InfoToConsole("downgrading database schema version: %d -> 29", dbVersion.Version)
providerLog(logger.LevelInfo, "downgrading database schema version: %d -> 29", dbVersion.Version)
if dbVersion.Version >= 32 {
Expand Down
61 changes: 45 additions & 16 deletions internal/dataprovider/dataprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2760,7 +2760,7 @@ func validateAssociatedVirtualFolders(vfolders []vfs.VirtualFolder) ([]vfs.Virtu
return []vfs.VirtualFolder{}, nil
}
var virtualFolders []vfs.VirtualFolder
folderNames := make(map[string]bool)
folderKeys := make(map[[2]string]bool)

for _, v := range vfolders {
if v.VirtualPath == "" {
Expand All @@ -2776,11 +2776,22 @@ func validateAssociatedVirtualFolders(vfolders []vfs.VirtualFolder) ([]vfs.Virtu
if v.Name == "" {
return nil, util.NewI18nError(util.NewValidationError("folder name is mandatory"), util.I18nErrorFolderNameRequired)
}
if folderNames[v.Name] {
return nil, util.NewI18nError(
util.NewValidationError(fmt.Sprintf("the folder %q is duplicated", v.Name)),
util.I18nErrorDuplicatedFolders,
)
if err := validateVirtualSubdirectory(v.VirtualSubdirectory); err != nil {
return nil, err
}
folderKey := [2]string{v.Name, v.VirtualSubdirectory}
if folderKeys[folderKey] {
if v.VirtualSubdirectory != "" {
return nil, util.NewI18nError(
util.NewValidationError(fmt.Sprintf("the folder %q with subdirectory %q is duplicated", v.Name, v.VirtualSubdirectory)),
util.I18nErrorDuplicatedFolders,
)
} else {
return nil, util.NewI18nError(
util.NewValidationError(fmt.Sprintf("the folder %q is duplicated", v.Name)),
util.I18nErrorDuplicatedFolders,
)
}
}
for _, vFolder := range virtualFolders {
if util.IsDirOverlapped(vFolder.VirtualPath, cleanedVPath, false, "/") {
Expand All @@ -2792,18 +2803,27 @@ func validateAssociatedVirtualFolders(vfolders []vfs.VirtualFolder) ([]vfs.Virtu
}
}
virtualFolders = append(virtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: v.Name,
},
VirtualPath: cleanedVPath,
QuotaSize: v.QuotaSize,
QuotaFiles: v.QuotaFiles,
BaseVirtualFolder: v.BaseVirtualFolder,
VirtualPath: cleanedVPath,
VirtualSubdirectory: v.VirtualSubdirectory,
QuotaSize: v.QuotaSize,
QuotaFiles: v.QuotaFiles,
})
folderNames[v.Name] = true
folderKeys[folderKey] = true
}
return virtualFolders, nil
}

func validateVirtualSubdirectory(subdirectory string) error {
if path.IsAbs(subdirectory) || strings.Contains(subdirectory, "..") {
return util.NewI18nError(
util.NewValidationError("invalid virtual subdirectory path"),
util.I18nErrorPathInvalid,
)
}
return nil
}

func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
if !c.Enabled {
c.ConfigName = ""
Expand Down Expand Up @@ -4844,10 +4864,19 @@ func downgradeSQLDatabaseFrom32To31(dbHandle *sql.DB) error {
if err := restoreEventActions(); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
defer cancel()

return sqlCommonUpdateDatabaseVersion(ctx, dbHandle, 31)
return sqlCommonUpdateDatabaseVersion(context.Background(), dbHandle, 31)
}

func downgradeSQLDatabaseFrom33To32(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 33 -> 32")
providerLog(logger.LevelInfo, "downgrading database schema version: 33 -> 32")

sql := fmt.Sprintf("ALTER TABLE %s DROP COLUMN subdirectory;"+
"ALTER TABLE %s DROP COLUMN subdirectory;",
sqlTableUsersFoldersMapping, sqlTableGroupsFoldersMapping)

return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 32, false)
}

func getConfigPath(name, configDir string) string {
Expand Down
Loading