mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
quota tracking: fix concurrent updates
added a test case to check quota size for upload that replace an existing file
This commit is contained in:
parent
b728a06984
commit
c547c5c9ee
6 changed files with 85 additions and 36 deletions
|
@ -96,39 +96,17 @@ func sqlCommonGetUserByID(ID int64) (User, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func sqlCommonUpdateQuota(username string, filesAdd int, sizeAdd int64, reset bool, p Provider) error {
|
func sqlCommonUpdateQuota(username string, filesAdd int, sizeAdd int64, reset bool, p Provider) error {
|
||||||
var usedFiles int
|
q := getUpdateQuotaQuery(reset)
|
||||||
var usedSize int64
|
|
||||||
var err error
|
|
||||||
if reset {
|
|
||||||
usedFiles = 0
|
|
||||||
usedSize = 0
|
|
||||||
} else {
|
|
||||||
usedFiles, usedSize, err = p.getUsedQuota(username)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
usedFiles += filesAdd
|
|
||||||
usedSize += sizeAdd
|
|
||||||
if usedFiles < 0 {
|
|
||||||
logger.Warn(logSender, "used files is negative, probably some files were added and not tracked, please rescan quota!")
|
|
||||||
usedFiles = 0
|
|
||||||
}
|
|
||||||
if usedSize < 0 {
|
|
||||||
logger.Warn(logSender, "used files is negative, probably some files were added and not tracked, please rescan quota!")
|
|
||||||
usedSize = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
q := getUpdateQuotaQuery()
|
|
||||||
stmt, err := dbHandle.Prepare(q)
|
stmt, err := dbHandle.Prepare(q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug(logSender, "error preparing database query %v: %v", q, err)
|
logger.Debug(logSender, "error preparing database query %v: %v", q, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer stmt.Close()
|
defer stmt.Close()
|
||||||
_, err = stmt.Exec(usedSize, usedFiles, utils.GetTimeAsMsSinceEpoch(time.Now()), username)
|
_, err = stmt.Exec(sizeAdd, filesAdd, utils.GetTimeAsMsSinceEpoch(time.Now()), username)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.Debug(logSender, "quota updated for user %v, new files: %v new size: %v", username, usedFiles, usedSize)
|
logger.Debug(logSender, "quota updated for user %v, files increment: %v size increment: %v is reset? %v",
|
||||||
|
username, filesAdd, sizeAdd, reset)
|
||||||
} else {
|
} else {
|
||||||
logger.Warn(logSender, "error updating quota for username %v: %v", username, err)
|
logger.Warn(logSender, "error updating quota for username %v: %v", username, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,8 +36,12 @@ func getUsersQuery(order string, username string) string {
|
||||||
order, sqlPlaceholders[0], sqlPlaceholders[1])
|
order, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUpdateQuotaQuery() string {
|
func getUpdateQuotaQuery(reset bool) string {
|
||||||
return fmt.Sprintf(`UPDATE %v SET used_quota_size = %v,used_quota_files = %v,last_quota_scan = %v
|
if reset {
|
||||||
|
return fmt.Sprintf(`UPDATE %v SET used_quota_size = %v,used_quota_files = %v,last_quota_scan = %v
|
||||||
|
WHERE username = %v`, config.UsersTable, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`UPDATE %v SET used_quota_size = used_quota_size + %v,used_quota_files = used_quota_files + %v,last_quota_scan = %v
|
||||||
WHERE username = %v`, config.UsersTable, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
|
WHERE username = %v`, config.UsersTable, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -168,8 +168,9 @@ func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if trunc {
|
if trunc {
|
||||||
|
// the file is truncated so we need to decrease quota size but not quota files
|
||||||
logger.Debug(logSender, "file truncation requested update quota for user %v", c.User.Username)
|
logger.Debug(logSender, "file truncation requested update quota for user %v", c.User.Username)
|
||||||
dataprovider.UpdateUserQuota(dataProvider, c.User.Username, -1, -stat.Size(), false)
|
dataprovider.UpdateUserQuota(dataProvider, c.User.Username, 0, -stat.Size(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.SetPathPermissions(p, c.User.GetUID(), c.User.GetGID())
|
utils.SetPathPermissions(p, c.User.GetUID(), c.User.GetGID())
|
||||||
|
@ -185,7 +186,7 @@ func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
||||||
user: c.User,
|
user: c.User,
|
||||||
connectionID: c.ID,
|
connectionID: c.ID,
|
||||||
transferType: transferUpload,
|
transferType: transferUpload,
|
||||||
isNewFile: trunc,
|
isNewFile: !trunc,
|
||||||
}
|
}
|
||||||
addTransfer(&transfer)
|
addTransfer(&transfer)
|
||||||
return &transfer, nil
|
return &transfer, nil
|
||||||
|
|
|
@ -298,8 +298,8 @@ func executeAction(operation string, username string, path string, target string
|
||||||
if _, err = os.Stat(actions.Command); err == nil {
|
if _, err = os.Stat(actions.Command); err == nil {
|
||||||
command := exec.Command(actions.Command, operation, username, path, target)
|
command := exec.Command(actions.Command, operation, username, path, target)
|
||||||
err = command.Start()
|
err = command.Start()
|
||||||
logger.Debug(logSender, "executed command \"%v\" with arguments: %v, %v, %v, error: %v",
|
logger.Debug(logSender, "executed command \"%v\" with arguments: %v, %v, %v, %v, error: %v",
|
||||||
actions.Command, operation, path, target, err)
|
actions.Command, operation, username, path, target, err)
|
||||||
} else {
|
} else {
|
||||||
logger.Warn(logSender, "Invalid action command \"%v\" : %v", actions.Command, err)
|
logger.Warn(logSender, "Invalid action command \"%v\" : %v", actions.Command, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -574,6 +574,66 @@ func TestMaxSessions(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQuotaFileReplace(t *testing.T) {
|
||||||
|
usePubKey := false
|
||||||
|
user, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
}
|
||||||
|
client, err := getSftpClient(user, usePubKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create sftp client: %v", err)
|
||||||
|
} else {
|
||||||
|
defer client.Close()
|
||||||
|
testFileSize := int64(65535)
|
||||||
|
expectedQuotaSize := user.UsedQuotaSize + testFileSize
|
||||||
|
expectedQuotaFiles := user.UsedQuotaFiles + 1
|
||||||
|
testFileName := "test_file.dat"
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to create test file: %v", err)
|
||||||
|
}
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("file upload error: %v", err)
|
||||||
|
}
|
||||||
|
user, err = api.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)
|
||||||
|
}
|
||||||
|
// now replace the same file, the quota must not change
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("file upload error: %v", err)
|
||||||
|
}
|
||||||
|
user, err = api.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(testFileName)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = api.RemoveUser(user, http.StatusOK)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to remove user: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestQuotaScan(t *testing.T) {
|
func TestQuotaScan(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
user, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
|
@ -670,14 +730,18 @@ func TestQuotaSize(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to create test file: %v", err)
|
t.Errorf("unable to create test file: %v", err)
|
||||||
}
|
}
|
||||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
err = sftpUploadFile(testFilePath, testFileName+".quota", testFileSize, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("file upload error: %v", err)
|
t.Errorf("file upload error: %v", err)
|
||||||
}
|
}
|
||||||
err = sftpUploadFile(testFilePath, testFileName+".1", testFileSize, client)
|
err = sftpUploadFile(testFilePath, testFileName+".quota.1", testFileSize, client)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("user is over quota file upload must fail")
|
t.Errorf("user is over quota file upload must fail")
|
||||||
}
|
}
|
||||||
|
err = client.Remove(testFileName + ".quota")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = api.RemoveUser(user, http.StatusOK)
|
err = api.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -57,9 +57,11 @@ func (t *Transfer) Close() error {
|
||||||
executeAction(operationUpload, t.user.Username, t.path, "")
|
executeAction(operationUpload, t.user.Username, t.path, "")
|
||||||
}
|
}
|
||||||
removeTransfer(t)
|
removeTransfer(t)
|
||||||
if t.transferType == transferUpload && t.bytesReceived > 0 && t.isNewFile {
|
if t.transferType == transferUpload {
|
||||||
numFiles := 0
|
numFiles := 0
|
||||||
numFiles++
|
if t.isNewFile {
|
||||||
|
numFiles = 1
|
||||||
|
}
|
||||||
dataprovider.UpdateUserQuota(dataProvider, t.user.Username, numFiles, t.bytesReceived, false)
|
dataprovider.UpdateUserQuota(dataProvider, t.user.Username, numFiles, t.bytesReceived, false)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|
Loading…
Reference in a new issue