sftpd: add support for excluding virtual folders from user quota limit

Fixes #110
This commit is contained in:
Nicola Murino 2020-05-01 15:27:53 +02:00
parent 14c2a244b7
commit 3f75d46a16
16 changed files with 340 additions and 139 deletions

View file

@ -652,8 +652,9 @@ func validateVirtualFolders(user *User) error {
v.MappedPath, user.GetHomeDir())}
}
virtualFolders = append(virtualFolders, vfs.VirtualFolder{
VirtualPath: cleanedVPath,
MappedPath: cleanedMPath,
VirtualPath: cleanedVPath,
MappedPath: cleanedMPath,
ExcludeFromQuota: v.ExcludeFromQuota,
})
for k, virtual := range mappedPaths {
if isMappedDirOverlapped(k, cleanedMPath) {

View file

@ -191,6 +191,22 @@ func (u *User) GetPermissionsForPath(p string) []string {
return permissions
}
// IsFileExcludedFromQuota returns true if the file must be excluded from quota usage
func (u *User) IsFileExcludedFromQuota(sftpPath string) bool {
if len(u.VirtualFolders) == 0 || u.FsConfig.Provider != 0 {
return false
}
dirsForPath := utils.GetDirsForSFTPPath(path.Dir(sftpPath))
for _, val := range dirsForPath {
for _, v := range u.VirtualFolders {
if v.VirtualPath == val {
return v.ExcludeFromQuota
}
}
}
return false
}
// AddVirtualDirs adds virtual folders, if defined, to the given files list
func (u *User) AddVirtualDirs(list []os.FileInfo, sftpPath string) []os.FileInfo {
if len(u.VirtualFolders) == 0 {

View file

@ -8,7 +8,7 @@ For each account, the following properties can be configured:
- `status` 1 means "active", 0 "inactive". An inactive account cannot login.
- `expiration_date` expiration date as unix timestamp in milliseconds. An expired account cannot login. 0 means no expiration.
- `home_dir` the user cannot upload or download files outside this directory. Must be an absolute path. A local home directory is required for Cloud Storage Backends too: in this case it will store temporary files.
- `virtual_folders` list of mappings between virtual SFTP/SCP paths and local filesystem paths outside the user home directory. The specified paths must be absolute and the virtual path cannot be "/", it must be a sub directory. The parent directory for the specified virtual path must exist. SFTPGo will try to automatically create any missing parent directory for the configured virtual folders at user login
- `virtual_folders` list of mappings between virtual SFTP/SCP paths and local filesystem paths outside the user home directory. The specified paths must be absolute and the virtual path cannot be "/", it must be a sub directory. The parent directory for the specified virtual path must exist. SFTPGo will try to automatically create any missing parent directory for the configured virtual folders at user login. For each mapping you can configure if the folder will be included or not in user quota limit.
- `uid`, `gid`. If SFTPGo runs as root system user then the created files and directories will be assigned to this system uid/gid. Ignored on windows or if SFTPGo runs as non root user: in this case files and directories for all SFTP users will be owned by the system user that runs SFTPGo.
- `max_sessions` maximum concurrent sessions. 0 means unlimited.
- `quota_size` maximum size allowed as bytes. 0 means unlimited.

View file

@ -183,7 +183,7 @@ func TestInitialization(t *testing.T) {
}
err = httpd.ReloadTLSCertificate()
if err != nil {
t.Error("realoding TLS Certificate must return nil error if no certificate is configured")
t.Error("reloading TLS Certificate must return nil error if no certificate is configured")
}
}
@ -628,8 +628,9 @@ func TestUpdateUser(t *testing.T) {
MappedPath: filepath.Join(os.TempDir(), "mapped_dir1"),
})
user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
VirtualPath: "/vdir12/subdir",
MappedPath: filepath.Join(os.TempDir(), "mapped_dir2"),
VirtualPath: "/vdir12/subdir",
MappedPath: filepath.Join(os.TempDir(), "mapped_dir2"),
ExcludeFromQuota: true,
})
user, _, err = httpd.UpdateUser(user, http.StatusOK)
if err != nil {
@ -1793,7 +1794,7 @@ func TestWebUserAddMock(t *testing.T) {
form.Set("expiration_date", "")
form.Set("permissions", "*")
form.Set("sub_dirs_permissions", " /subdir::list ,download ")
form.Set("virtual_folders", fmt.Sprintf(" /vdir:: %v ", mappedDir))
form.Set("virtual_folders", fmt.Sprintf(" /vdir:: %v ::1", mappedDir))
form.Set("allowed_extensions", "/dir1::.jpg,.png")
form.Set("denied_extensions", "/dir1::.zip")
b, contentType, _ := getMultipartFormData(form, "", "")
@ -1899,10 +1900,7 @@ func TestWebUserAddMock(t *testing.T) {
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
var users []dataprovider.User
err := render.DecodeJSON(rr.Body, &users)
if err != nil {
t.Errorf("Error decoding users: %v", err)
}
render.DecodeJSON(rr.Body, &users)
if len(users) != 1 {
t.Errorf("1 user is expected, actual: %v", len(users))
}
@ -1928,7 +1926,7 @@ func TestWebUserAddMock(t *testing.T) {
}
vfolderFoumd := false
for _, v := range newUser.VirtualFolders {
if v.VirtualPath == "/vdir" && v.MappedPath == mappedDir {
if v.VirtualPath == "/vdir" && v.MappedPath == mappedDir && v.ExcludeFromQuota == true {
vfolderFoumd = true
}
}

View file

@ -2,7 +2,7 @@ openapi: 3.0.1
info:
title: SFTPGo
description: 'SFTPGo REST API'
version: 1.8.4
version: 1.8.5
servers:
- url: /api/v1
@ -1077,6 +1077,10 @@ components:
type: string
mapped_path:
type: string
exclude_from_quota:
type: boolean
nullable: true
description: This folder will be excluded from user quota
required:
- virtual_path
- mapped_path

View file

@ -196,10 +196,17 @@ func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder {
if strings.Contains(cleaned, "::") {
mapping := strings.Split(cleaned, "::")
if len(mapping) > 1 {
virtualFolders = append(virtualFolders, vfs.VirtualFolder{
vfolder := vfs.VirtualFolder{
VirtualPath: strings.TrimSpace(mapping[0]),
MappedPath: strings.TrimSpace(mapping[1]),
})
}
if len(mapping) > 2 {
excludeFromQuota, err := strconv.Atoi(strings.TrimSpace(mapping[2]))
if err == nil {
vfolder.ExcludeFromQuota = (excludeFromQuota > 0)
}
}
virtualFolders = append(virtualFolders, vfolder)
}
}
}

View file

@ -140,7 +140,7 @@ Output:
Command:
```
python sftpgo_api_cli.py update-user 9576 test_username --password "test_pwd" --home-dir="/tmp/test_home_dir" --uid 0 --gid 33 --max-sessions 3 --quota-size 0 --quota-files 4 --permissions "*" --subdirs-permissions "/dir1::list,download,create_symlinks" --upload-bandwidth 90 --download-bandwidth 80 --status 1 --expiration-date "" --allowed-ip "" --denied-ip "192.168.1.0/24" --denied-login-methods "" --fs local --virtual-folders "/vdir1::/tmp/mapped1" "/vdir2::/tmp/mapped2" --allowed-extensions "" --denied-extensions ""
python sftpgo_api_cli.py update-user 9576 test_username --password "test_pwd" --home-dir="/tmp/test_home_dir" --uid 0 --gid 33 --max-sessions 3 --quota-size 0 --quota-files 4 --permissions "*" --subdirs-permissions "/dir1::list,download,create_symlinks" --upload-bandwidth 90 --download-bandwidth 80 --status 1 --expiration-date "" --allowed-ip "" --denied-ip "192.168.1.0/24" --denied-login-methods "" --fs local --virtual-folders "/vdir1::/tmp/mapped1" "/vdir2::/tmp/mapped2::1" --allowed-extensions "" --denied-extensions ""
```
Output:
@ -203,10 +203,12 @@ Output:
"username": "test_username",
"virtual_folders": [
{
"exclude_from_quota": false,
"mapped_path": "/tmp/mapped1",
"virtual_path": "/vdir1"
},
{
"exclude_from_quota": true,
"mapped_path": "/tmp/mapped2",
"virtual_path": "/vdir2"
}
@ -265,10 +267,12 @@ Output:
"username": "test_username",
"virtual_folders": [
{
"exclude_from_quota": false,
"mapped_path": "/tmp/mapped1",
"virtual_path": "/vdir1"
},
{
"exclude_from_quota": true,
"mapped_path": "/tmp/mapped2",
"virtual_path": "/vdir2"
}

View file

@ -110,12 +110,19 @@ class SFTPGoApiRequests:
if '::' in f:
vpath = ''
mapped_path = ''
exclude_from_quota = False
values = f.split('::')
if len(values) > 1:
vpath = values[0]
mapped_path = values[1]
if len(values) > 2:
try:
exclude_from_quota = int(values[2]) > 0
except:
pass
if vpath and mapped_path:
result.append({"virtual_path":vpath, "mapped_path":mapped_path})
result.append({"virtual_path":vpath, "mapped_path":mapped_path,
"exclude_from_quota":exclude_from_quota})
return result
def buildPermissions(self, root_perms, subdirs_perms):
@ -508,7 +515,8 @@ def addCommonUserArguments(parser):
parser.add_argument('--subdirs-permissions', type=str, nargs='*', default=[], help='Permissions for subdirs. '
+'For example: "/somedir::list,download" "/otherdir/subdir::*" Default: %(default)s')
parser.add_argument('--virtual-folders', type=str, nargs='*', default=[], help='Virtual folder mapping. For example: '
+'"/vpath::/home/adir" "/vpath::C:\adir", ignored for non local filesystems. Default: %(default)s')
+'"/vpath::/home/adir" "/vpath::C:\adir::1". If the optional third argument is > 0 the virtual '
+'folder will be excluded from user quota. Ignored for non local filesystems. Default: %(default)s')
parser.add_argument('-U', '--upload-bandwidth', type=int, default=0,
help='Maximum upload bandwidth as KB/s, 0 means unlimited. Default: %(default)s')
parser.add_argument('-D', '--download-bandwidth', type=int, default=0,

View file

@ -75,25 +75,26 @@ func (c Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) {
c.Log(logger.LevelDebug, logSender, "fileread requested for path: %#v", p)
transfer := Transfer{
file: file,
readerAt: r,
writerAt: nil,
cancelFn: cancelFn,
path: p,
start: time.Now(),
bytesSent: 0,
bytesReceived: 0,
user: c.User,
connectionID: c.ID,
transferType: transferDownload,
lastActivity: time.Now(),
isNewFile: false,
protocol: c.protocol,
transferError: nil,
isFinished: false,
minWriteOffset: 0,
expectedSize: fi.Size(),
lock: new(sync.Mutex),
file: file,
readerAt: r,
writerAt: nil,
cancelFn: cancelFn,
path: p,
start: time.Now(),
bytesSent: 0,
bytesReceived: 0,
user: c.User,
connectionID: c.ID,
transferType: transferDownload,
lastActivity: time.Now(),
isNewFile: false,
protocol: c.protocol,
transferError: nil,
isFinished: false,
minWriteOffset: 0,
expectedSize: fi.Size(),
isExcludedFromQuota: c.User.IsFileExcludedFromQuota(request.Filepath),
lock: new(sync.Mutex),
}
addTransfer(&transfer)
return &transfer, nil
@ -123,7 +124,7 @@ func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
if !c.User.HasPerm(dataprovider.PermUpload, path.Dir(request.Filepath)) {
return nil, sftp.ErrSSHFxPermissionDenied
}
return c.handleSFTPUploadToNewFile(p, filePath)
return c.handleSFTPUploadToNewFile(p, filePath, c.User.IsFileExcludedFromQuota(request.Filepath))
}
if statErr != nil {
@ -141,7 +142,8 @@ func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
return nil, sftp.ErrSSHFxPermissionDenied
}
return c.handleSFTPUploadToExistingFile(request.Pflags(), p, filePath, stat.Size())
return c.handleSFTPUploadToExistingFile(request.Pflags(), p, filePath, stat.Size(),
c.User.IsFileExcludedFromQuota(request.Filepath))
}
// Filecmd hander for basic SFTP system calls related to files, but not anything to do with reading
@ -437,14 +439,16 @@ func (c Connection) handleSFTPRemove(filePath string, request *sftp.Request) err
logger.CommandLog(removeLogSender, filePath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "")
if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
dataprovider.UpdateUserQuota(dataProvider, c.User, -1, -size, false) //nolint:errcheck
if !c.User.IsFileExcludedFromQuota(request.Filepath) {
dataprovider.UpdateUserQuota(dataProvider, c.User, -1, -size, false) //nolint:errcheck
}
}
go executeAction(newActionNotification(c.User, operationDelete, filePath, "", "", fi.Size(), nil)) //nolint:errcheck
return sftp.ErrSSHFxOk
}
func (c Connection) handleSFTPUploadToNewFile(requestPath, filePath string) (io.WriterAt, error) {
func (c Connection) handleSFTPUploadToNewFile(requestPath, filePath string, isExcludedFromQuota bool) (io.WriterAt, error) {
if !c.hasSpace(true) {
c.Log(logger.LevelInfo, logSender, "denying file write due to space limit")
return nil, sftp.ErrSSHFxFailure
@ -459,31 +463,32 @@ func (c Connection) handleSFTPUploadToNewFile(requestPath, filePath string) (io.
vfs.SetPathPermissions(c.fs, filePath, c.User.GetUID(), c.User.GetGID())
transfer := Transfer{
file: file,
writerAt: w,
readerAt: nil,
cancelFn: cancelFn,
path: requestPath,
start: time.Now(),
bytesSent: 0,
bytesReceived: 0,
user: c.User,
connectionID: c.ID,
transferType: transferUpload,
lastActivity: time.Now(),
isNewFile: true,
protocol: c.protocol,
transferError: nil,
isFinished: false,
minWriteOffset: 0,
lock: new(sync.Mutex),
file: file,
writerAt: w,
readerAt: nil,
cancelFn: cancelFn,
path: requestPath,
start: time.Now(),
bytesSent: 0,
bytesReceived: 0,
user: c.User,
connectionID: c.ID,
transferType: transferUpload,
lastActivity: time.Now(),
isNewFile: true,
protocol: c.protocol,
transferError: nil,
isFinished: false,
minWriteOffset: 0,
isExcludedFromQuota: isExcludedFromQuota,
lock: new(sync.Mutex),
}
addTransfer(&transfer)
return &transfer, nil
}
func (c Connection) handleSFTPUploadToExistingFile(pflags sftp.FileOpenFlags, requestPath, filePath string,
fileSize int64) (io.WriterAt, error) {
fileSize int64, isExcludedFromQuota bool) (io.WriterAt, error) {
var err error
if !c.hasSpace(false) {
c.Log(logger.LevelInfo, logSender, "denying file write due to space limit")
@ -520,7 +525,9 @@ func (c Connection) handleSFTPUploadToExistingFile(pflags sftp.FileOpenFlags, re
minWriteOffset = fileSize
} else {
if vfs.IsLocalOsFs(c.fs) {
dataprovider.UpdateUserQuota(dataProvider, c.User, 0, -fileSize, false) //nolint:errcheck
if !isExcludedFromQuota {
dataprovider.UpdateUserQuota(dataProvider, c.User, 0, -fileSize, false) //nolint:errcheck
}
} else {
initialSize = fileSize
}
@ -529,25 +536,26 @@ func (c Connection) handleSFTPUploadToExistingFile(pflags sftp.FileOpenFlags, re
vfs.SetPathPermissions(c.fs, filePath, c.User.GetUID(), c.User.GetGID())
transfer := Transfer{
file: file,
writerAt: w,
readerAt: nil,
cancelFn: cancelFn,
path: requestPath,
start: time.Now(),
bytesSent: 0,
bytesReceived: 0,
user: c.User,
connectionID: c.ID,
transferType: transferUpload,
lastActivity: time.Now(),
isNewFile: false,
protocol: c.protocol,
transferError: nil,
isFinished: false,
minWriteOffset: minWriteOffset,
initialSize: initialSize,
lock: new(sync.Mutex),
file: file,
writerAt: w,
readerAt: nil,
cancelFn: cancelFn,
path: requestPath,
start: time.Now(),
bytesSent: 0,
bytesReceived: 0,
user: c.User,
connectionID: c.ID,
transferType: transferUpload,
lastActivity: time.Now(),
isNewFile: false,
protocol: c.protocol,
transferError: nil,
isFinished: false,
minWriteOffset: minWriteOffset,
initialSize: initialSize,
isExcludedFromQuota: isExcludedFromQuota,
lock: new(sync.Mutex),
}
addTransfer(&transfer)
return &transfer, nil

View file

@ -423,7 +423,7 @@ func TestMockFsErrors(t *testing.T) {
flags.Write = true
flags.Trunc = false
flags.Append = true
_, err = c.handleSFTPUploadToExistingFile(flags, testfile, testfile, 0)
_, err = c.handleSFTPUploadToExistingFile(flags, testfile, testfile, 0, false)
if err != sftp.ErrSSHFxOpUnsupported {
t.Errorf("unexpected error: %v", err)
}
@ -439,12 +439,12 @@ func TestUploadFiles(t *testing.T) {
var flags sftp.FileOpenFlags
flags.Write = true
flags.Trunc = true
_, err := c.handleSFTPUploadToExistingFile(flags, "missing_path", "other_missing_path", 0)
_, err := c.handleSFTPUploadToExistingFile(flags, "missing_path", "other_missing_path", 0, false)
if err == nil {
t.Errorf("upload to existing file must fail if one or both paths are invalid")
}
uploadMode = uploadModeStandard
_, err = c.handleSFTPUploadToExistingFile(flags, "missing_path", "other_missing_path", 0)
_, err = c.handleSFTPUploadToExistingFile(flags, "missing_path", "other_missing_path", 0, false)
if err == nil {
t.Errorf("upload to existing file must fail if one or both paths are invalid")
}
@ -452,14 +452,14 @@ func TestUploadFiles(t *testing.T) {
if runtime.GOOS == "windows" {
missingFile = "missing\\relative\\file.txt"
}
_, err = c.handleSFTPUploadToNewFile(".", missingFile)
_, err = c.handleSFTPUploadToNewFile(".", missingFile, false)
if err == nil {
t.Errorf("upload new file in missing path must fail")
}
c.fs = newMockOsFs(nil, nil, false, "123", os.TempDir())
f, _ := ioutil.TempFile("", "temp")
f.Close()
_, err = c.handleSFTPUploadToExistingFile(flags, f.Name(), f.Name(), 123)
_, err = c.handleSFTPUploadToExistingFile(flags, f.Name(), f.Name(), 123, false)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
@ -1437,7 +1437,7 @@ func TestSCPErrorsMockFs(t *testing.T) {
if err != errFake {
t.Errorf("unexpected error: %v", err)
}
err = scpCommand.handleUploadFile(testfile, testfile, 0, false, 4)
err = scpCommand.handleUploadFile(testfile, testfile, 0, false, 4, false)
if err != nil {
t.Errorf("unexpected error: %v", err)
}

View file

@ -187,7 +187,8 @@ func (c *scpCommand) getUploadFileData(sizeToRead int64, transfer *Transfer) err
return c.sendConfirmationMessage()
}
func (c *scpCommand) handleUploadFile(requestPath, filePath string, sizeToRead int64, isNewFile bool, fileSize int64) error {
func (c *scpCommand) handleUploadFile(requestPath, filePath string, sizeToRead int64, isNewFile bool, fileSize int64,
isExcludedFromQuota bool) error {
if !c.connection.hasSpace(true) {
err := fmt.Errorf("denying file write due to space limit")
c.connection.Log(logger.LevelWarn, logSenderSCP, "error uploading file: %#v, err: %v", filePath, err)
@ -198,7 +199,9 @@ func (c *scpCommand) handleUploadFile(requestPath, filePath string, sizeToRead i
initialSize := int64(0)
if !isNewFile {
if vfs.IsLocalOsFs(c.connection.fs) {
dataprovider.UpdateUserQuota(dataProvider, c.connection.User, 0, -fileSize, false) //nolint:errcheck
if !isExcludedFromQuota {
dataprovider.UpdateUserQuota(dataProvider, c.connection.User, 0, -fileSize, false) //nolint:errcheck
}
} else {
initialSize = fileSize
}
@ -213,25 +216,26 @@ func (c *scpCommand) handleUploadFile(requestPath, filePath string, sizeToRead i
vfs.SetPathPermissions(c.connection.fs, filePath, c.connection.User.GetUID(), c.connection.User.GetGID())
transfer := Transfer{
file: file,
readerAt: nil,
writerAt: w,
cancelFn: cancelFn,
path: requestPath,
start: time.Now(),
bytesSent: 0,
bytesReceived: 0,
user: c.connection.User,
connectionID: c.connection.ID,
transferType: transferUpload,
lastActivity: time.Now(),
isNewFile: isNewFile,
protocol: c.connection.protocol,
transferError: nil,
isFinished: false,
minWriteOffset: 0,
initialSize: initialSize,
lock: new(sync.Mutex),
file: file,
readerAt: nil,
writerAt: w,
cancelFn: cancelFn,
path: requestPath,
start: time.Now(),
bytesSent: 0,
bytesReceived: 0,
user: c.connection.User,
connectionID: c.connection.ID,
transferType: transferUpload,
lastActivity: time.Now(),
isNewFile: isNewFile,
protocol: c.connection.protocol,
transferError: nil,
isFinished: false,
minWriteOffset: 0,
initialSize: initialSize,
isExcludedFromQuota: isExcludedFromQuota,
lock: new(sync.Mutex),
}
addTransfer(&transfer)
@ -265,7 +269,7 @@ func (c *scpCommand) handleUpload(uploadFilePath string, sizeToRead int64) error
c.sendErrorMessage(errPermission)
return errPermission
}
return c.handleUploadFile(p, filePath, sizeToRead, true, 0)
return c.handleUploadFile(p, filePath, sizeToRead, true, 0, c.connection.User.IsFileExcludedFromQuota(uploadFilePath))
}
if statErr != nil {
@ -297,7 +301,7 @@ func (c *scpCommand) handleUpload(uploadFilePath string, sizeToRead int64) error
}
}
return c.handleUploadFile(p, filePath, sizeToRead, false, stat.Size())
return c.handleUploadFile(p, filePath, sizeToRead, false, stat.Size(), c.connection.User.IsFileExcludedFromQuota(uploadFilePath))
}
func (c *scpCommand) sendDownloadProtocolMessages(dirPath string, stat os.FileInfo) error {

View file

@ -2449,15 +2449,13 @@ func TestVirtualFoldersQuota(t *testing.T) {
MappedPath: mappedPath1,
})
u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
VirtualPath: vdirPath2,
MappedPath: mappedPath2,
VirtualPath: vdirPath2,
MappedPath: mappedPath2,
ExcludeFromQuota: true,
})
os.MkdirAll(mappedPath1, 0777)
os.MkdirAll(mappedPath2, 0777)
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
}
user, _, _ := httpd.AddUser(u, http.StatusOK)
client, err := getSftpClient(user, usePubKey)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
@ -2482,8 +2480,26 @@ func TestVirtualFoldersQuota(t *testing.T) {
if err != nil {
t.Errorf("file upload error: %v", err)
}
expectedQuotaFiles := 3
expectedQuotaSize := testFileSize * 3
err = sftpUploadFile(testFilePath, path.Join(vdirPath2, testFileName), testFileSize, client)
if err != nil {
t.Errorf("file upload error: %v", err)
}
expectedQuotaFiles := 2
expectedQuotaSize := testFileSize * 2
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
if err != nil {
t.Errorf("error getting user: %v", err)
}
if expectedQuotaFiles != user.UsedQuotaFiles {
t.Errorf("quota files does not match, expected: %v, actual: %v", expectedQuotaFiles, user.UsedQuotaFiles)
}
if expectedQuotaSize != user.UsedQuotaSize {
t.Errorf("quota size does not match, expected: %v, actual: %v", expectedQuotaSize, user.UsedQuotaSize)
}
err = client.Remove(path.Join(vdirPath2, testFileName))
if err != nil {
t.Errorf("unexpected error removing uploaded file: %v", err)
}
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
if err != nil {
t.Errorf("error getting user: %v", err)
@ -3878,6 +3894,52 @@ func TestResolveVirtualPaths(t *testing.T) {
}
}
func TestVirtualFoldersExcludeQuota(t *testing.T) {
user := getTestUser(true)
mappedPath := filepath.Join(os.TempDir(), "vdir")
vdirPath := "/vdir/sub"
vSubDirPath := path.Join(vdirPath, "subdir", "subdir")
vSubDir1Path := path.Join(vSubDirPath, "subdir", "subdir")
user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
VirtualPath: vdirPath,
MappedPath: mappedPath,
ExcludeFromQuota: false,
})
user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
VirtualPath: vSubDir1Path,
MappedPath: mappedPath,
ExcludeFromQuota: false,
})
user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
VirtualPath: vSubDirPath,
MappedPath: mappedPath,
ExcludeFromQuota: true,
})
if user.IsFileExcludedFromQuota("/file") {
t.Errorf("unexpected file excluded from quota")
}
if user.IsFileExcludedFromQuota(path.Join(vdirPath, "file")) {
t.Errorf("unexpected file excluded from quota")
}
if !user.IsFileExcludedFromQuota(path.Join(vSubDirPath, "file")) {
t.Errorf("unexpected file included in quota")
}
if !user.IsFileExcludedFromQuota(path.Join(vSubDir1Path, "..", "file")) {
t.Errorf("unexpected file included in quota")
}
if user.IsFileExcludedFromQuota(path.Join(vSubDir1Path, "file")) {
t.Errorf("unexpected file excluded from quota")
}
if user.IsFileExcludedFromQuota(path.Join(vSubDirPath, "..", "file")) {
t.Errorf("unexpected file excluded from quota")
}
// we check the parent dir for a file
if user.IsFileExcludedFromQuota(vSubDirPath) {
t.Errorf("unexpected file excluded from quota")
}
}
func TestUserPerms(t *testing.T) {
user := getTestUser(true)
user.Permissions = make(map[string][]string)
@ -4719,6 +4781,86 @@ func TestSCPVirtualFolders(t *testing.T) {
os.RemoveAll(mappedPath)
}
func TestSCPVirtualFoldersQuota(t *testing.T) {
if len(scpPath) == 0 {
t.Skip("scp command not found, unable to execute this test")
}
usePubKey := true
u := getTestUser(usePubKey)
u.QuotaFiles = 100
mappedPath1 := filepath.Join(os.TempDir(), "vdir1")
vdirPath1 := "/vdir1"
mappedPath2 := filepath.Join(os.TempDir(), "vdir2")
vdirPath2 := "/vdir2"
u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
VirtualPath: vdirPath1,
MappedPath: mappedPath1,
})
u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
VirtualPath: vdirPath2,
MappedPath: mappedPath2,
ExcludeFromQuota: true,
})
os.MkdirAll(mappedPath1, 0777)
os.MkdirAll(mappedPath2, 0777)
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
}
testFileName := "test_file.dat"
testBaseDirName := "test_dir"
testBaseDirPath := filepath.Join(homeBasePath, testBaseDirName)
testBaseDirDownName := "test_dir_down"
testBaseDirDownPath := filepath.Join(homeBasePath, testBaseDirDownName)
testFilePath := filepath.Join(homeBasePath, testBaseDirName, testFileName)
testFilePath1 := filepath.Join(homeBasePath, testBaseDirName, testBaseDirName, testFileName)
testFileSize := int64(131074)
createTestFile(testFilePath, testFileSize)
createTestFile(testFilePath1, testFileSize)
remoteDownPath1 := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, path.Join("/", vdirPath1))
remoteUpPath1 := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, vdirPath1)
remoteDownPath2 := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, path.Join("/", vdirPath2))
remoteUpPath2 := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, vdirPath2)
err = scpUpload(testBaseDirPath, remoteUpPath1, true, false)
if err != nil {
t.Errorf("error uploading dir via scp: %v", err)
}
err = scpDownload(testBaseDirDownPath, remoteDownPath1, true, true)
if err != nil {
t.Errorf("error downloading dir via scp: %v", err)
}
err = scpUpload(testBaseDirPath, remoteUpPath2, true, false)
if err != nil {
t.Errorf("error uploading dir via scp: %v", err)
}
err = scpDownload(testBaseDirDownPath, remoteDownPath2, true, true)
if err != nil {
t.Errorf("error downloading dir via scp: %v", err)
}
expectedQuotaFiles := 2
expectedQuotaSize := testFileSize * 2
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
if err != nil {
t.Errorf("error getting user: %v", err)
}
if expectedQuotaFiles != user.UsedQuotaFiles {
t.Errorf("quota files does not match, expected: %v, actual: %v", expectedQuotaFiles, user.UsedQuotaFiles)
}
if expectedQuotaSize != user.UsedQuotaSize {
t.Errorf("quota size does not match, expected: %v, actual: %v", expectedQuotaSize, user.UsedQuotaSize)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to remove user: %v", err)
}
os.RemoveAll(testBaseDirPath)
os.RemoveAll(testBaseDirDownPath)
os.RemoveAll(user.GetHomeDir())
os.RemoveAll(mappedPath1)
os.RemoveAll(mappedPath1)
}
func TestSCPPermsSubDirs(t *testing.T) {
if len(scpPath) == 0 {
t.Skip("scp command not found, unable to execute this test")

View file

@ -26,26 +26,27 @@ var (
// Transfer contains the transfer details for an upload or a download.
// It implements the io Reader and Writer interface to handle files downloads and uploads
type Transfer struct {
file *os.File
writerAt *pipeat.PipeWriterAt
readerAt *pipeat.PipeReaderAt
cancelFn func()
path string
start time.Time
bytesSent int64
bytesReceived int64
user dataprovider.User
connectionID string
transferType int
lastActivity time.Time
protocol string
transferError error
minWriteOffset int64
expectedSize int64
initialSize int64
lock *sync.Mutex
isNewFile bool
isFinished bool
file *os.File
writerAt *pipeat.PipeWriterAt
readerAt *pipeat.PipeReaderAt
cancelFn func()
path string
start time.Time
bytesSent int64
bytesReceived int64
user dataprovider.User
connectionID string
transferType int
lastActivity time.Time
protocol string
transferError error
minWriteOffset int64
expectedSize int64
initialSize int64
lock *sync.Mutex
isNewFile bool
isFinished bool
isExcludedFromQuota bool
}
// TransferError is called if there is an unexpected error.
@ -184,6 +185,9 @@ func (t *Transfer) updateQuota(numFiles int) bool {
if t.file == nil && t.transferError != nil {
return false
}
if t.isExcludedFromQuota {
return false
}
if t.transferType == transferUpload && (numFiles != 0 || t.bytesReceived > 0) {
dataprovider.UpdateUserQuota(dataProvider, t.user, numFiles, t.bytesReceived-t.initialSize, false) //nolint:errcheck
return true

View file

@ -124,10 +124,10 @@
<div class="col-sm-10">
<textarea class="form-control" id="idVirtualFolders" name="virtual_folders" rows="3"
aria-describedby="vfHelpBlock">{{range $index, $mapping := .User.VirtualFolders -}}
{{$mapping.VirtualPath}}::{{$mapping.MappedPath}}&#10;
{{$mapping.VirtualPath}}::{{$mapping.MappedPath}}{{if $mapping.ExcludeFromQuota}}::1{{end}}&#10;
{{- end}}</textarea>
<small id="vfHelpBlock" class="form-text text-muted">
One mapping per line as vpath::path, for example /vdir::/home/adir or /vdir::C:\adir, ignored for non local filesystems
One mapping per line as vpath::path::[exclude_from_quota], for example /vdir::/home/adir or /vdir::C:\adir::1, ignored for non local filesystems
</small>
</div>
</div>

View file

@ -174,6 +174,9 @@ func (fs OsFs) CheckRootPath(username string, uid int, gid int) bool {
func (fs OsFs) ScanRootDirContents() (int, int64, error) {
numFiles, size, err := fs.getDirSize(fs.rootDir)
for _, v := range fs.virtualFolders {
if v.ExcludeFromQuota {
continue
}
num, s, err := fs.getDirSize(v.MappedPath)
if err != nil {
if fs.IsNotExist(err) {

View file

@ -52,6 +52,8 @@ type Fs interface {
type VirtualFolder struct {
VirtualPath string `json:"virtual_path"`
MappedPath string `json:"mapped_path"`
// This folder will be excluded from user quota
ExcludeFromQuota bool `json:"exclude_from_quota"`
}
// IsDirectory checks if a path exists and is a directory