mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 00:50:31 +00:00
fs actions: add first upload/download
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
9ddd2d3588
commit
3e8254e398
23 changed files with 629 additions and 44 deletions
|
@ -10,8 +10,10 @@ The `hook` can be defined as the absolute path of your program or an HTTP URL.
|
|||
The following `actions` are supported:
|
||||
|
||||
- `download`
|
||||
- `first-download`
|
||||
- `pre-download`
|
||||
- `upload`
|
||||
- `first-upload`
|
||||
- `pre-upload`
|
||||
- `delete`
|
||||
- `pre-delete`
|
||||
|
@ -20,7 +22,7 @@ The following `actions` are supported:
|
|||
- `rmdir`
|
||||
- `ssh_cmd`
|
||||
|
||||
The `upload` condition includes both uploads to new files and overwrite of existing ones. If an upload is aborted for quota limits SFTPGo tries to remove the partial file, so if the notification reports a zero size file and a quota exceeded error the file has been deleted. The `ssh_cmd` condition will be triggered after a command is successfully executed via SSH. `scp` will trigger the `download` and `upload` conditions and not `ssh_cmd`.
|
||||
The `upload` condition includes both uploads to new files and overwrite of existing ones. If an upload is aborted for quota limits SFTPGo tries to remove the partial file, so if the notification reports a zero size file and a quota exceeded error the file has been deleted. The `ssh_cmd` condition will be triggered after a command is successfully executed via SSH. `scp` will trigger the `download` and `upload` conditions and not `ssh_cmd`. The `first-download` and `first-upload` action are executed only if no error occour and they don't exclude the `download` and `upload` notifications, so you will get both the `first-upload` and `upload` notification after the first successful upload and the same for the first successful download.
|
||||
For cloud backends directories are virtual, they are created implicitly when you upload a file and are implicitly removed when the last file within a directory is removed. The `mkdir` and `rmdir` notifications are sent only when a directory is explicitly created or removed.
|
||||
|
||||
The notification will indicate if an error is detected and so, for example, a partial file is uploaded.
|
||||
|
|
4
go.mod
4
go.mod
|
@ -20,7 +20,7 @@ require (
|
|||
github.com/cockroachdb/cockroach-go/v2 v2.2.15
|
||||
github.com/coreos/go-oidc/v3 v3.2.0
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||
github.com/fclairamb/ftpserverlib v0.19.0
|
||||
github.com/fclairamb/ftpserverlib v0.19.1
|
||||
github.com/fclairamb/go-log v0.4.1
|
||||
github.com/go-acme/lego/v4 v4.8.0
|
||||
github.com/go-chi/chi/v5 v5.0.8-0.20220512131524-9e71a0d4b3d6
|
||||
|
@ -51,7 +51,7 @@ require (
|
|||
github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5
|
||||
github.com/rs/xid v1.4.0
|
||||
github.com/rs/zerolog v1.27.0
|
||||
github.com/sftpgo/sdk v0.1.2-0.20220815185512-a4dc48b3993e
|
||||
github.com/sftpgo/sdk v0.1.2-0.20220821164353-a9b95497604e
|
||||
github.com/shirou/gopsutil/v3 v3.22.7
|
||||
github.com/spf13/afero v1.9.2
|
||||
github.com/spf13/cobra v1.5.0
|
||||
|
|
8
go.sum
8
go.sum
|
@ -284,8 +284,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
|
|||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fclairamb/ftpserverlib v0.19.0 h1:5QcSQ0OIJBlezIqmGehiL/AVsRb6dIkMxbkuhyPkESM=
|
||||
github.com/fclairamb/ftpserverlib v0.19.0/go.mod h1:pmukdVOFKKUY9zjWRoxFW8JAljyulC/uK5FfusJzK2E=
|
||||
github.com/fclairamb/ftpserverlib v0.19.1 h1:OIqW+AdcsUEq4apudrluDD1c4iCRidLAoQzJRBUJnbg=
|
||||
github.com/fclairamb/ftpserverlib v0.19.1/go.mod h1:cVeFR3wvEjgtK99686UXJaTvqZk8jbjHFnhaC23LGpc=
|
||||
github.com/fclairamb/go-log v0.4.1 h1:rLtdSG9x2pK41AIAnE8WYpl05xBJfw1ZyYxZaXFcBsM=
|
||||
github.com/fclairamb/go-log v0.4.1/go.mod h1:sw1KvnkZ4wKCYkvy4SL3qVZcJSWFP8Ure4pM3z+KNn4=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
|
@ -714,8 +714,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
|||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/sftpgo/sdk v0.1.2-0.20220815185512-a4dc48b3993e h1:EJiTi+f2QCiDoGj1EBq6o1RX+JrtZnvTE6yKt3ks1B8=
|
||||
github.com/sftpgo/sdk v0.1.2-0.20220815185512-a4dc48b3993e/go.mod h1:RL4HeorXC6XgqtkLYnQUSogLdsdMfbsogIvdBVLuy4w=
|
||||
github.com/sftpgo/sdk v0.1.2-0.20220821164353-a9b95497604e h1:Up8iLVu+PPd5ejyG8fi8910IC4JO+A1/COJf+sJWHI8=
|
||||
github.com/sftpgo/sdk v0.1.2-0.20220821164353-a9b95497604e/go.mod h1:fxFs5FP9bhi3ObH+7qdxZF+2QOk8J/u4GAR5yuX5jMg=
|
||||
github.com/shirou/gopsutil/v3 v3.22.7 h1:flKnuCMfUUrO+oAvwAd6GKZgnPzr098VA/UJ14nhJd4=
|
||||
github.com/shirou/gopsutil/v3 v3.22.7/go.mod h1:s648gW4IywYzUfE/KjXxUsqrqx/T2xO5VqOXxONeRfI=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
|
|
|
@ -45,21 +45,23 @@ import (
|
|||
|
||||
// constants
|
||||
const (
|
||||
logSender = "common"
|
||||
uploadLogSender = "Upload"
|
||||
downloadLogSender = "Download"
|
||||
renameLogSender = "Rename"
|
||||
rmdirLogSender = "Rmdir"
|
||||
mkdirLogSender = "Mkdir"
|
||||
symlinkLogSender = "Symlink"
|
||||
removeLogSender = "Remove"
|
||||
chownLogSender = "Chown"
|
||||
chmodLogSender = "Chmod"
|
||||
chtimesLogSender = "Chtimes"
|
||||
truncateLogSender = "Truncate"
|
||||
operationDownload = "download"
|
||||
operationUpload = "upload"
|
||||
operationDelete = "delete"
|
||||
logSender = "common"
|
||||
uploadLogSender = "Upload"
|
||||
downloadLogSender = "Download"
|
||||
renameLogSender = "Rename"
|
||||
rmdirLogSender = "Rmdir"
|
||||
mkdirLogSender = "Mkdir"
|
||||
symlinkLogSender = "Symlink"
|
||||
removeLogSender = "Remove"
|
||||
chownLogSender = "Chown"
|
||||
chmodLogSender = "Chmod"
|
||||
chtimesLogSender = "Chtimes"
|
||||
truncateLogSender = "Truncate"
|
||||
operationDownload = "download"
|
||||
operationUpload = "upload"
|
||||
operationFirstDownload = "first-download"
|
||||
operationFirstUpload = "first-upload"
|
||||
operationDelete = "delete"
|
||||
// Pre-download action name
|
||||
OperationPreDownload = "pre-download"
|
||||
// Pre-upload action name
|
||||
|
|
|
@ -1190,6 +1190,45 @@ func TestVfsSameResource(t *testing.T) {
|
|||
assert.False(t, res)
|
||||
}
|
||||
|
||||
func TestUpdateTransferTimestamps(t *testing.T) {
|
||||
username := "user_test_timestamps"
|
||||
user := &dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: username,
|
||||
HomeDir: filepath.Join(os.TempDir(), username),
|
||||
Status: 1,
|
||||
Permissions: map[string][]string{
|
||||
"/": {dataprovider.PermAny},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := dataprovider.AddUser(user, "", "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), user.FirstUpload)
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
|
||||
err = dataprovider.UpdateUserTransferTimestamps(username, true)
|
||||
assert.NoError(t, err)
|
||||
userGet, err := dataprovider.UserExists(username)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, userGet.FirstUpload, int64(0))
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
err = dataprovider.UpdateUserTransferTimestamps(username, false)
|
||||
assert.NoError(t, err)
|
||||
userGet, err = dataprovider.UserExists(username)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, userGet.FirstUpload, int64(0))
|
||||
assert.Greater(t, userGet.FirstDownload, int64(0))
|
||||
// updating again must fail
|
||||
err = dataprovider.UpdateUserTransferTimestamps(username, true)
|
||||
assert.Error(t, err)
|
||||
err = dataprovider.UpdateUserTransferTimestamps(username, false)
|
||||
assert.Error(t, err)
|
||||
// cleanup
|
||||
err = dataprovider.DeleteUser(username, "", "")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func BenchmarkBcryptHashing(b *testing.B) {
|
||||
bcryptPassword := "bcryptpassword"
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
|
|
@ -39,6 +39,8 @@ type BaseConnection struct {
|
|||
// last activity for this connection.
|
||||
// Since this field is accessed atomically we put it as first element of the struct to achieve 64 bit alignment
|
||||
lastActivity int64
|
||||
uploadDone atomic.Bool
|
||||
downloadDone atomic.Bool
|
||||
// unique ID for a transfer.
|
||||
// This field is accessed atomically so we put it at the beginning of the struct to achieve 64 bit alignment
|
||||
transferID int64
|
||||
|
|
|
@ -3764,6 +3764,135 @@ func TestEventRuleFsActions(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEventRuleFirstUploadDownloadActions(t *testing.T) {
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 2525,
|
||||
From: "notify@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir)
|
||||
require.NoError(t, err)
|
||||
a1 := dataprovider.BaseEventAction{
|
||||
Name: "action1",
|
||||
Type: dataprovider.ActionTypeEmail,
|
||||
Options: dataprovider.BaseEventActionOptions{
|
||||
EmailConfig: dataprovider.EventActionEmailConfig{
|
||||
Recipients: []string{"test@example.com"},
|
||||
Subject: `"{{Event}}" from "{{Name}}"`,
|
||||
Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}}",
|
||||
},
|
||||
},
|
||||
}
|
||||
action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
r1 := dataprovider.EventRule{
|
||||
Name: "test first upload rule",
|
||||
Trigger: dataprovider.EventTriggerFsEvent,
|
||||
Conditions: dataprovider.EventConditions{
|
||||
FsEvents: []string{"first-upload"},
|
||||
},
|
||||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action1.Name,
|
||||
},
|
||||
Order: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
r2 := dataprovider.EventRule{
|
||||
Name: "test first download rule",
|
||||
Trigger: dataprovider.EventTriggerFsEvent,
|
||||
Conditions: dataprovider.EventConditions{
|
||||
FsEvents: []string{"first-download"},
|
||||
},
|
||||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action1.Name,
|
||||
},
|
||||
Order: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
rule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
conn, client, err := getSftpClient(user)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
testFileSize := int64(32768)
|
||||
lastReceivedEmail.reset()
|
||||
err = writeSFTPFileNoCheck(testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
assert.Eventually(t, func() bool {
|
||||
return lastReceivedEmail.get().From != ""
|
||||
}, 1500*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: "first-upload" from "%s"`, user.Username))
|
||||
lastReceivedEmail.reset()
|
||||
// a new upload will not produce a new notification
|
||||
err = writeSFTPFileNoCheck(testFileName+"_1", 32768, client)
|
||||
assert.NoError(t, err)
|
||||
assert.Never(t, func() bool {
|
||||
return lastReceivedEmail.get().From != ""
|
||||
}, 1000*time.Millisecond, 100*time.Millisecond)
|
||||
// the same for download
|
||||
f, err := client.Open(testFileName)
|
||||
assert.NoError(t, err)
|
||||
contents := make([]byte, testFileSize)
|
||||
n, err := io.ReadFull(f, contents)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int(testFileSize), n)
|
||||
err = f.Close()
|
||||
assert.NoError(t, err)
|
||||
assert.Eventually(t, func() bool {
|
||||
return lastReceivedEmail.get().From != ""
|
||||
}, 1500*time.Millisecond, 100*time.Millisecond)
|
||||
email = lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: "first-download" from "%s"`, user.Username))
|
||||
// download again
|
||||
lastReceivedEmail.reset()
|
||||
f, err = client.Open(testFileName)
|
||||
assert.NoError(t, err)
|
||||
contents = make([]byte, testFileSize)
|
||||
n, err = io.ReadFull(f, contents)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int(testFileSize), n)
|
||||
err = f.Close()
|
||||
assert.NoError(t, err)
|
||||
assert.Never(t, func() bool {
|
||||
return lastReceivedEmail.get().From != ""
|
||||
}, 1000*time.Millisecond, 100*time.Millisecond)
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventRule(rule2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
||||
smtpCfg = smtp.Config{}
|
||||
err = smtpCfg.Initialize(configDir)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEventRuleCertificate(t *testing.T) {
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
|
|
|
@ -386,19 +386,20 @@ func (t *BaseTransfer) Close() error {
|
|||
}
|
||||
}
|
||||
elapsed := time.Since(t.start).Nanoseconds() / 1000000
|
||||
var uploadFileSize int64
|
||||
if t.transferType == TransferDownload {
|
||||
logger.TransferLog(downloadLogSender, t.fsPath, elapsed, atomic.LoadInt64(&t.BytesSent), t.Connection.User.Username,
|
||||
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
|
||||
ExecuteActionNotification(t.Connection, operationDownload, t.fsPath, t.requestPath, "", "", "", //nolint:errcheck
|
||||
atomic.LoadInt64(&t.BytesSent), t.ErrTransfer)
|
||||
} else {
|
||||
fileSize := atomic.LoadInt64(&t.BytesReceived) + t.MinWriteOffset
|
||||
uploadFileSize = atomic.LoadInt64(&t.BytesReceived) + t.MinWriteOffset
|
||||
if statSize, errStat := t.getUploadFileSize(); errStat == nil {
|
||||
fileSize = statSize
|
||||
uploadFileSize = statSize
|
||||
}
|
||||
t.Connection.Log(logger.LevelDebug, "uploaded file size %v", fileSize)
|
||||
numFiles, fileSize = t.executeUploadHook(numFiles, fileSize)
|
||||
t.updateQuota(numFiles, fileSize)
|
||||
t.Connection.Log(logger.LevelDebug, "upload file size %v", uploadFileSize)
|
||||
numFiles, uploadFileSize = t.executeUploadHook(numFiles, uploadFileSize)
|
||||
t.updateQuota(numFiles, uploadFileSize)
|
||||
t.updateTimes()
|
||||
logger.TransferLog(uploadLogSender, t.fsPath, elapsed, atomic.LoadInt64(&t.BytesReceived), t.Connection.User.Username,
|
||||
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
|
||||
|
@ -409,9 +410,33 @@ func (t *BaseTransfer) Close() error {
|
|||
err = t.ErrTransfer
|
||||
}
|
||||
}
|
||||
t.updateTransferTimestamps(uploadFileSize)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *BaseTransfer) updateTransferTimestamps(uploadFileSize int64) {
|
||||
if t.ErrTransfer != nil {
|
||||
return
|
||||
}
|
||||
if t.transferType == TransferUpload {
|
||||
if t.Connection.User.FirstUpload == 0 && !t.Connection.uploadDone.Load() {
|
||||
if err := dataprovider.UpdateUserTransferTimestamps(t.Connection.User.Username, true); err == nil {
|
||||
t.Connection.uploadDone.Store(true)
|
||||
ExecuteActionNotification(t.Connection, operationFirstUpload, t.fsPath, t.requestPath, "", //nolint:errcheck
|
||||
"", "", uploadFileSize, t.ErrTransfer)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if t.Connection.User.FirstDownload == 0 && !t.Connection.downloadDone.Load() && atomic.LoadInt64(&t.BytesSent) > 0 {
|
||||
if err := dataprovider.UpdateUserTransferTimestamps(t.Connection.User.Username, false); err == nil {
|
||||
t.Connection.downloadDone.Store(true)
|
||||
ExecuteActionNotification(t.Connection, operationFirstDownload, t.fsPath, t.requestPath, "", //nolint:errcheck
|
||||
"", "", atomic.LoadInt64(&t.BytesSent), t.ErrTransfer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *BaseTransfer) executeUploadHook(numFiles int, fileSize int64) (int, int64) {
|
||||
err := ExecuteActionNotification(t.Connection, operationUpload, t.fsPath, t.requestPath, "", "", "",
|
||||
fileSize, t.ErrTransfer)
|
||||
|
|
|
@ -35,7 +35,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
boltDatabaseVersion = 20
|
||||
boltDatabaseVersion = 21
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -570,6 +570,8 @@ func (p *BoltProvider) addUser(user *User) error {
|
|||
user.UsedUploadDataTransfer = 0
|
||||
user.UsedDownloadDataTransfer = 0
|
||||
user.LastLogin = 0
|
||||
user.FirstDownload = 0
|
||||
user.FirstUpload = 0
|
||||
user.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
for idx := range user.VirtualFolders {
|
||||
|
@ -621,6 +623,8 @@ func (p *BoltProvider) updateUser(user *User) error {
|
|||
user.UsedUploadDataTransfer = oldUser.UsedUploadDataTransfer
|
||||
user.UsedDownloadDataTransfer = oldUser.UsedDownloadDataTransfer
|
||||
user.LastLogin = oldUser.LastLogin
|
||||
user.FirstDownload = oldUser.FirstDownload
|
||||
user.FirstUpload = oldUser.FirstUpload
|
||||
user.CreatedAt = oldUser.CreatedAt
|
||||
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
buf, err := json.Marshal(user)
|
||||
|
@ -2433,6 +2437,63 @@ func (p *BoltProvider) updateTaskTimestamp(name string) error {
|
|||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func (p *BoltProvider) setFirstDownloadTimestamp(username string) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, err := p.getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var u []byte
|
||||
if u = bucket.Get([]byte(username)); u == nil {
|
||||
return util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist, unable to set download timestamp",
|
||||
username))
|
||||
}
|
||||
var user User
|
||||
err = json.Unmarshal(u, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.FirstDownload > 0 {
|
||||
return util.NewGenericError(fmt.Sprintf("first download already set to %v",
|
||||
util.GetTimeFromMsecSinceEpoch(user.FirstDownload)))
|
||||
}
|
||||
user.FirstDownload = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
buf, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(username), buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *BoltProvider) setFirstUploadTimestamp(username string) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, err := p.getUsersBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var u []byte
|
||||
if u = bucket.Get([]byte(username)); u == nil {
|
||||
return util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist, unable to set upload timestamp",
|
||||
username))
|
||||
}
|
||||
var user User
|
||||
if err = json.Unmarshal(u, &user); err != nil {
|
||||
return err
|
||||
}
|
||||
if user.FirstUpload > 0 {
|
||||
return util.NewGenericError(fmt.Sprintf("first upload already set to %v",
|
||||
util.GetTimeFromMsecSinceEpoch(user.FirstUpload)))
|
||||
}
|
||||
user.FirstUpload = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
buf, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(username), buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *BoltProvider) close() error {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
@ -2460,10 +2521,10 @@ func (p *BoltProvider) migrateDatabase() error {
|
|||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 19:
|
||||
logger.InfoToConsole(fmt.Sprintf("updating database version: %d -> 20", version))
|
||||
providerLog(logger.LevelInfo, "updating database version: %d -> 20", version)
|
||||
return updateBoltDatabaseVersion(p.dbHandle, 20)
|
||||
case version == 19, version == 20:
|
||||
logger.InfoToConsole(fmt.Sprintf("updating database version: %d -> 21", version))
|
||||
providerLog(logger.LevelInfo, "updating database version: %d -> 21", version)
|
||||
return updateBoltDatabaseVersion(p.dbHandle, 21)
|
||||
default:
|
||||
if version > boltDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
|
||||
|
@ -2485,9 +2546,9 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
|
|||
return errors.New("current version match target version, nothing to do")
|
||||
}
|
||||
switch dbVersion.Version {
|
||||
case 20:
|
||||
logger.InfoToConsole("downgrading database version: 20 -> 19")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19")
|
||||
case 20, 21:
|
||||
logger.InfoToConsole("downgrading database version: %d -> 19", dbVersion.Version)
|
||||
providerLog(logger.LevelInfo, "downgrading database version: %d -> 19", dbVersion.Version)
|
||||
err := p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
for _, bucketName := range [][]byte{actionsBucket, rulesBucket} {
|
||||
err := tx.DeleteBucket(bucketName)
|
||||
|
|
|
@ -768,6 +768,8 @@ type Provider interface {
|
|||
addTask(name string) error
|
||||
updateTask(name string, version int64) error
|
||||
updateTaskTimestamp(name string) error
|
||||
setFirstDownloadTimestamp(username string) error
|
||||
setFirstUploadTimestamp(username string) error
|
||||
checkAvailability() error
|
||||
close() error
|
||||
reloadConfig() error
|
||||
|
@ -1399,6 +1401,22 @@ func UpdateUserTransferQuota(user *User, uploadSize, downloadSize int64, reset b
|
|||
return nil
|
||||
}
|
||||
|
||||
// UpdateUserTransferTimestamps updates the first download/upload fields if unset
|
||||
func UpdateUserTransferTimestamps(username string, isUpload bool) error {
|
||||
if isUpload {
|
||||
err := provider.setFirstUploadTimestamp(username)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "unable to set first upload: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
err := provider.setFirstDownloadTimestamp(username)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "unable to set first download: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetUsedQuota returns the used quota for the given SFTPGo user.
|
||||
func GetUsedQuota(username string) (int, int64, int64, int64, error) {
|
||||
if config.TrackQuota == 0 {
|
||||
|
@ -3538,6 +3556,8 @@ func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFi
|
|||
userUsedUploadTransfer := u.UsedUploadDataTransfer
|
||||
userLastQuotaUpdate := u.LastQuotaUpdate
|
||||
userLastLogin := u.LastLogin
|
||||
userFirstDownload := u.FirstDownload
|
||||
userFirstUpload := u.FirstUpload
|
||||
userCreatedAt := u.CreatedAt
|
||||
totpConfig := u.Filters.TOTPConfig
|
||||
recoveryCodes := u.Filters.RecoveryCodes
|
||||
|
@ -3552,6 +3572,8 @@ func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFi
|
|||
u.UsedDownloadDataTransfer = userUsedDownloadTransfer
|
||||
u.LastQuotaUpdate = userLastQuotaUpdate
|
||||
u.LastLogin = userLastLogin
|
||||
u.FirstDownload = userFirstDownload
|
||||
u.FirstUpload = userFirstUpload
|
||||
u.CreatedAt = userCreatedAt
|
||||
if userID == 0 {
|
||||
err = provider.addUser(&u)
|
||||
|
@ -3787,6 +3809,8 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
|
|||
user.UsedDownloadDataTransfer = u.UsedDownloadDataTransfer
|
||||
user.LastQuotaUpdate = u.LastQuotaUpdate
|
||||
user.LastLogin = u.LastLogin
|
||||
user.FirstDownload = u.FirstDownload
|
||||
user.FirstUpload = u.FirstUpload
|
||||
user.CreatedAt = u.CreatedAt
|
||||
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
// preserve TOTP config and recovery codes
|
||||
|
@ -3859,6 +3883,8 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
|
|||
user.UsedDownloadDataTransfer = u.UsedDownloadDataTransfer
|
||||
user.LastQuotaUpdate = u.LastQuotaUpdate
|
||||
user.LastLogin = u.LastLogin
|
||||
user.FirstDownload = u.FirstDownload
|
||||
user.FirstUpload = u.FirstUpload
|
||||
// preserve TOTP config and recovery codes
|
||||
user.Filters.TOTPConfig = u.Filters.TOTPConfig
|
||||
user.Filters.RecoveryCodes = u.Filters.RecoveryCodes
|
||||
|
|
|
@ -145,7 +145,8 @@ func getFsActionTypeAsString(value int) string {
|
|||
// TODO: replace the copied strings with shared constants
|
||||
var (
|
||||
// SupportedFsEvents defines the supported filesystem events
|
||||
SupportedFsEvents = []string{"upload", "download", "delete", "rename", "mkdir", "rmdir", "ssh_cmd"}
|
||||
SupportedFsEvents = []string{"upload", "first-upload", "download", "first-download", "delete", "rename",
|
||||
"mkdir", "rmdir", "ssh_cmd"}
|
||||
// SupportedProviderEvents defines the supported provider events
|
||||
SupportedProviderEvents = []string{operationAdd, operationUpdate, operationDelete}
|
||||
// SupportedRuleConditionProtocols defines the supported protcols for rule conditions
|
||||
|
|
|
@ -326,6 +326,8 @@ func (p *MemoryProvider) addUser(user *User) error {
|
|||
user.UsedUploadDataTransfer = 0
|
||||
user.UsedDownloadDataTransfer = 0
|
||||
user.LastLogin = 0
|
||||
user.FirstUpload = 0
|
||||
user.FirstDownload = 0
|
||||
user.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
user.VirtualFolders = p.joinUserVirtualFoldersFields(user)
|
||||
|
@ -378,6 +380,8 @@ func (p *MemoryProvider) updateUser(user *User) error {
|
|||
user.UsedUploadDataTransfer = u.UsedUploadDataTransfer
|
||||
user.UsedDownloadDataTransfer = u.UsedDownloadDataTransfer
|
||||
user.LastLogin = u.LastLogin
|
||||
user.FirstDownload = u.FirstDownload
|
||||
user.FirstUpload = u.FirstUpload
|
||||
user.CreatedAt = u.CreatedAt
|
||||
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
user.ID = u.ID
|
||||
|
@ -2203,6 +2207,44 @@ func (p *MemoryProvider) updateTaskTimestamp(name string) error {
|
|||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) setFirstDownloadTimestamp(username string) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return errMemoryProviderClosed
|
||||
}
|
||||
user, err := p.userExistsInternal(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.FirstDownload > 0 {
|
||||
return util.NewGenericError(fmt.Sprintf("first download already set to %v",
|
||||
util.GetTimeFromMsecSinceEpoch(user.FirstDownload)))
|
||||
}
|
||||
user.FirstDownload = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
p.dbHandle.users[user.Username] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) setFirstUploadTimestamp(username string) error {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
if p.dbHandle.isClosed {
|
||||
return errMemoryProviderClosed
|
||||
}
|
||||
user, err := p.userExistsInternal(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.FirstUpload > 0 {
|
||||
return util.NewGenericError(fmt.Sprintf("first upload already set to %v",
|
||||
util.GetTimeFromMsecSinceEpoch(user.FirstUpload)))
|
||||
}
|
||||
user.FirstUpload = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
p.dbHandle.users[user.Username] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) getNextID() int64 {
|
||||
nextID := int64(1)
|
||||
for _, v := range p.dbHandle.users {
|
||||
|
|
|
@ -164,6 +164,12 @@ const (
|
|||
"DROP TABLE `{{events_actions}}` CASCADE;" +
|
||||
"DROP TABLE `{{tasks}}` CASCADE;" +
|
||||
"ALTER TABLE `{{users}}` DROP COLUMN `deleted_at`;"
|
||||
mysqlV21SQL = "ALTER TABLE `{{users}}` ADD COLUMN `first_download` bigint DEFAULT 0 NOT NULL; " +
|
||||
"ALTER TABLE `{{users}}` ALTER COLUMN `first_download` DROP DEFAULT; " +
|
||||
"ALTER TABLE `{{users}}` ADD COLUMN `first_upload` bigint DEFAULT 0 NOT NULL; " +
|
||||
"ALTER TABLE `{{users}}` ALTER COLUMN `first_upload` DROP DEFAULT;"
|
||||
mysqlV21DownSQL = "ALTER TABLE `{{users}}` DROP COLUMN `first_upload`; " +
|
||||
"ALTER TABLE `{{users}}` DROP COLUMN `first_download`;"
|
||||
)
|
||||
|
||||
// MySQLProvider defines the auth provider for MySQL/MariaDB database
|
||||
|
@ -616,6 +622,14 @@ func (p *MySQLProvider) updateTaskTimestamp(name string) error {
|
|||
return sqlCommonUpdateTaskTimestamp(name, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) setFirstDownloadTimestamp(username string) error {
|
||||
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) setFirstUploadTimestamp(username string) error {
|
||||
return sqlCommonSetFirstUploadTimestamp(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *MySQLProvider) close() error {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
@ -657,6 +671,8 @@ func (p *MySQLProvider) migrateDatabase() error { //nolint:dupl
|
|||
return err
|
||||
case version == 19:
|
||||
return updateMySQLDatabaseFromV19(p.dbHandle)
|
||||
case version == 20:
|
||||
return updateMySQLDatabaseFromV20(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
|
||||
|
@ -681,6 +697,8 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
|
|||
switch dbVersion.Version {
|
||||
case 20:
|
||||
return downgradeMySQLDatabaseFromV20(p.dbHandle)
|
||||
case 21:
|
||||
return downgradeMySQLDatabaseFromV21(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
|
@ -692,13 +710,27 @@ func (p *MySQLProvider) resetDatabase() error {
|
|||
}
|
||||
|
||||
func updateMySQLDatabaseFromV19(dbHandle *sql.DB) error {
|
||||
return updateMySQLDatabaseFrom19To20(dbHandle)
|
||||
if err := updateMySQLDatabaseFrom19To20(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
return updateMySQLDatabaseFromV20(dbHandle)
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFromV20(dbHandle *sql.DB) error {
|
||||
return updateMySQLDatabaseFrom20To21(dbHandle)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFromV20(dbHandle *sql.DB) error {
|
||||
return downgradeMySQLDatabaseFrom20To19(dbHandle)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFromV21(dbHandle *sql.DB) error {
|
||||
if err := downgradeMySQLDatabaseFrom21To20(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
return downgradeMySQLDatabaseFromV20(dbHandle)
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFrom19To20(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 19 -> 20")
|
||||
providerLog(logger.LevelInfo, "updating database version: 19 -> 20")
|
||||
|
@ -711,6 +743,13 @@ func updateMySQLDatabaseFrom19To20(dbHandle *sql.DB) error {
|
|||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 20, true)
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFrom20To21(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 20 -> 21")
|
||||
providerLog(logger.LevelInfo, "updating database version: 20 -> 21")
|
||||
sql := strings.ReplaceAll(mysqlV21SQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 21, true)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFrom20To19(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 20 -> 19")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19")
|
||||
|
@ -721,3 +760,10 @@ func downgradeMySQLDatabaseFrom20To19(dbHandle *sql.DB) error {
|
|||
sql = strings.ReplaceAll(sql, "{{tasks}}", sqlTableTasks)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 19, false)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFrom21To20(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 21 -> 20")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 21 -> 20")
|
||||
sql := strings.ReplaceAll(mysqlV21DownSQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 20, false)
|
||||
}
|
||||
|
|
|
@ -175,6 +175,14 @@ DROP TABLE "{{events_rules}}" CASCADE;
|
|||
DROP TABLE "{{events_actions}}" CASCADE;
|
||||
DROP TABLE "{{tasks}}" CASCADE;
|
||||
ALTER TABLE "{{users}}" DROP COLUMN "deleted_at" CASCADE;
|
||||
`
|
||||
pgsqlV21SQL = `ALTER TABLE "{{users}}" ADD COLUMN "first_download" bigint DEFAULT 0 NOT NULL;
|
||||
ALTER TABLE "{{users}}" ALTER COLUMN "first_download" DROP DEFAULT;
|
||||
ALTER TABLE "{{users}}" ADD COLUMN "first_upload" bigint DEFAULT 0 NOT NULL;
|
||||
ALTER TABLE "{{users}}" ALTER COLUMN "first_upload" DROP DEFAULT;
|
||||
`
|
||||
pgsqlV21DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "first_upload" CASCADE;
|
||||
ALTER TABLE "{{users}}" DROP COLUMN "first_download" CASCADE;
|
||||
`
|
||||
)
|
||||
|
||||
|
@ -591,6 +599,14 @@ func (p *PGSQLProvider) updateTaskTimestamp(name string) error {
|
|||
return sqlCommonUpdateTaskTimestamp(name, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) setFirstDownloadTimestamp(username string) error {
|
||||
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) setFirstUploadTimestamp(username string) error {
|
||||
return sqlCommonSetFirstUploadTimestamp(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *PGSQLProvider) close() error {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
@ -632,6 +648,8 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
|
|||
return err
|
||||
case version == 19:
|
||||
return updatePgSQLDatabaseFromV19(p.dbHandle)
|
||||
case version == 20:
|
||||
return updatePgSQLDatabaseFromV20(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
|
||||
|
@ -656,6 +674,8 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
|
|||
switch dbVersion.Version {
|
||||
case 20:
|
||||
return downgradePgSQLDatabaseFromV20(p.dbHandle)
|
||||
case 21:
|
||||
return downgradePgSQLDatabaseFromV21(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
|
@ -667,13 +687,27 @@ func (p *PGSQLProvider) resetDatabase() error {
|
|||
}
|
||||
|
||||
func updatePgSQLDatabaseFromV19(dbHandle *sql.DB) error {
|
||||
return updatePgSQLDatabaseFrom19To20(dbHandle)
|
||||
if err := updatePgSQLDatabaseFrom19To20(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
return updatePgSQLDatabaseFromV20(dbHandle)
|
||||
}
|
||||
|
||||
func updatePgSQLDatabaseFromV20(dbHandle *sql.DB) error {
|
||||
return updatePgSQLDatabaseFrom20To21(dbHandle)
|
||||
}
|
||||
|
||||
func downgradePgSQLDatabaseFromV20(dbHandle *sql.DB) error {
|
||||
return downgradePgSQLDatabaseFrom20To19(dbHandle)
|
||||
}
|
||||
|
||||
func downgradePgSQLDatabaseFromV21(dbHandle *sql.DB) error {
|
||||
if err := downgradePgSQLDatabaseFrom21To20(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
return downgradePgSQLDatabaseFromV20(dbHandle)
|
||||
}
|
||||
|
||||
func updatePgSQLDatabaseFrom19To20(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 19 -> 20")
|
||||
providerLog(logger.LevelInfo, "updating database version: 19 -> 20")
|
||||
|
@ -686,6 +720,13 @@ func updatePgSQLDatabaseFrom19To20(dbHandle *sql.DB) error {
|
|||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 20, true)
|
||||
}
|
||||
|
||||
func updatePgSQLDatabaseFrom20To21(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 20 -> 21")
|
||||
providerLog(logger.LevelInfo, "updating database version: 20 -> 21")
|
||||
sql := strings.ReplaceAll(pgsqlV21SQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 21, true)
|
||||
}
|
||||
|
||||
func downgradePgSQLDatabaseFrom20To19(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 20 -> 19")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19")
|
||||
|
@ -696,3 +737,10 @@ func downgradePgSQLDatabaseFrom20To19(dbHandle *sql.DB) error {
|
|||
sql = strings.ReplaceAll(sql, "{{tasks}}", sqlTableTasks)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 19, false)
|
||||
}
|
||||
|
||||
func downgradePgSQLDatabaseFrom21To20(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 21 -> 20")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 21 -> 20")
|
||||
sql := strings.ReplaceAll(pgsqlV21DownSQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 20, false)
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
sqlDatabaseVersion = 20
|
||||
sqlDatabaseVersion = 21
|
||||
defaultSQLQueryTimeout = 10 * time.Second
|
||||
longSQLQueryTimeout = 60 * time.Second
|
||||
)
|
||||
|
@ -893,6 +893,30 @@ func sqlCommonSetUpdatedAt(username string, dbHandle *sql.DB) {
|
|||
}
|
||||
}
|
||||
|
||||
func sqlCommonSetFirstDownloadTimestamp(username string, dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getSetFirstDownloadQuery()
|
||||
res, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlCommonRequireRowAffected(res)
|
||||
}
|
||||
|
||||
func sqlCommonSetFirstUploadTimestamp(username string, dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
q := getSetFirstUploadQuery()
|
||||
res, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlCommonRequireRowAffected(res)
|
||||
}
|
||||
|
||||
func sqlCommonUpdateLastLogin(username string, dbHandle *sql.DB) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
@ -1730,7 +1754,8 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
|
|||
&user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,
|
||||
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
|
||||
&additionalInfo, &description, &email, &user.CreatedAt, &user.UpdatedAt, &user.UploadDataTransfer, &user.DownloadDataTransfer,
|
||||
&user.TotalDataTransfer, &user.UsedUploadDataTransfer, &user.UsedDownloadDataTransfer, &user.DeletedAt)
|
||||
&user.TotalDataTransfer, &user.UsedUploadDataTransfer, &user.UsedDownloadDataTransfer, &user.DeletedAt, &user.FirstDownload,
|
||||
&user.FirstUpload)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return user, util.NewRecordNotFoundError(err.Error())
|
||||
|
|
|
@ -160,7 +160,13 @@ CREATE INDEX "{{prefix}}users_deleted_at_idx" ON "{{users}}" ("deleted_at");
|
|||
DROP TABLE "{{events_rules}}";
|
||||
DROP TABLE "{{events_actions}}";
|
||||
DROP TABLE "{{tasks}}";
|
||||
DROP INDEX IF EXISTS "{{prefix}}users_deleted_at_idx";
|
||||
ALTER TABLE "{{users}}" DROP COLUMN "deleted_at";
|
||||
`
|
||||
sqliteV21SQL = `ALTER TABLE "{{users}}" ADD COLUMN "first_download" bigint DEFAULT 0 NOT NULL;
|
||||
ALTER TABLE "{{users}}" ADD COLUMN "first_upload" bigint DEFAULT 0 NOT NULL;`
|
||||
sqliteV21DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "first_upload";
|
||||
ALTER TABLE "{{users}}" DROP COLUMN "first_download";
|
||||
`
|
||||
)
|
||||
|
||||
|
@ -563,6 +569,14 @@ func (p *SQLiteProvider) updateTaskTimestamp(name string) error {
|
|||
return sqlCommonUpdateTaskTimestamp(name, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) setFirstDownloadTimestamp(username string) error {
|
||||
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) setFirstUploadTimestamp(username string) error {
|
||||
return sqlCommonSetFirstUploadTimestamp(username, p.dbHandle)
|
||||
}
|
||||
|
||||
func (p *SQLiteProvider) close() error {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
@ -604,6 +618,8 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
|
|||
return err
|
||||
case version == 19:
|
||||
return updateSQLiteDatabaseFromV19(p.dbHandle)
|
||||
case version == 20:
|
||||
return updateSQLiteDatabaseFromV20(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
|
||||
|
@ -628,6 +644,8 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
|
|||
switch dbVersion.Version {
|
||||
case 20:
|
||||
return downgradeSQLiteDatabaseFromV20(p.dbHandle)
|
||||
case 21:
|
||||
return downgradeSQLiteDatabaseFromV21(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
|
@ -639,13 +657,27 @@ func (p *SQLiteProvider) resetDatabase() error {
|
|||
}
|
||||
|
||||
func updateSQLiteDatabaseFromV19(dbHandle *sql.DB) error {
|
||||
return updateSQLiteDatabaseFrom19To20(dbHandle)
|
||||
if err := updateSQLiteDatabaseFrom19To20(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
return updateSQLiteDatabaseFromV20(dbHandle)
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFromV20(dbHandle *sql.DB) error {
|
||||
return updateSQLiteDatabaseFrom20To21(dbHandle)
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFromV20(dbHandle *sql.DB) error {
|
||||
return downgradeSQLiteDatabaseFrom20To19(dbHandle)
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFromV21(dbHandle *sql.DB) error {
|
||||
if err := downgradeSQLiteDatabaseFrom21To20(dbHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
return downgradeSQLiteDatabaseFromV20(dbHandle)
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFrom19To20(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 19 -> 20")
|
||||
providerLog(logger.LevelInfo, "updating database version: 19 -> 20")
|
||||
|
@ -658,6 +690,13 @@ func updateSQLiteDatabaseFrom19To20(dbHandle *sql.DB) error {
|
|||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 20, true)
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFrom20To21(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 20 -> 21")
|
||||
providerLog(logger.LevelInfo, "updating database version: 20 -> 21")
|
||||
sql := strings.ReplaceAll(sqliteV21SQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 21, true)
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFrom20To19(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 20 -> 19")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19")
|
||||
|
@ -666,9 +705,17 @@ func downgradeSQLiteDatabaseFrom20To19(dbHandle *sql.DB) error {
|
|||
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
|
||||
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{tasks}}", sqlTableTasks)
|
||||
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 19, false)
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFrom21To20(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 21 -> 20")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 21 -> 20")
|
||||
sql := strings.ReplaceAll(sqliteV21DownSQL, "{{users}}", sqlTableUsers)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 20, false)
|
||||
}
|
||||
|
||||
/*func setPragmaFK(dbHandle *sql.DB, value string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
|
|
@ -26,7 +26,7 @@ const (
|
|||
selectUserFields = "id,username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,used_quota_size," +
|
||||
"used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,expiration_date,last_login,status,filters,filesystem," +
|
||||
"additional_info,description,email,created_at,updated_at,upload_data_transfer,download_data_transfer,total_data_transfer," +
|
||||
"used_upload_data_transfer,used_download_data_transfer,deleted_at"
|
||||
"used_upload_data_transfer,used_download_data_transfer,deleted_at,first_download,first_upload"
|
||||
selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update,name,description,filesystem"
|
||||
selectAdminFields = "id,username,password,status,email,permissions,filters,additional_info,description,created_at,updated_at,last_login"
|
||||
selectAPIKeyFields = "key_id,name,api_key,scope,created_at,updated_at,last_use_at,expires_at,description,user_id,admin_id"
|
||||
|
@ -457,6 +457,16 @@ func getSetUpdateAtQuery() string {
|
|||
return fmt.Sprintf(`UPDATE %s SET updated_at = %s WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
||||
func getSetFirstUploadQuery() string {
|
||||
return fmt.Sprintf(`UPDATE %s SET first_upload = %s WHERE username = %s AND first_upload = 0`,
|
||||
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
||||
func getSetFirstDownloadQuery() string {
|
||||
return fmt.Sprintf(`UPDATE %s SET first_download = %s WHERE username = %s AND first_download = 0`,
|
||||
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
||||
func getUpdateLastLoginQuery() string {
|
||||
return fmt.Sprintf(`UPDATE %s SET last_login = %s WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
@ -484,8 +494,8 @@ func getAddUserQuery() string {
|
|||
return fmt.Sprintf(`INSERT INTO %s (username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,
|
||||
used_quota_size,used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,status,last_login,expiration_date,filters,
|
||||
filesystem,additional_info,description,email,created_at,updated_at,upload_data_transfer,download_data_transfer,total_data_transfer,
|
||||
used_upload_data_transfer,used_download_data_transfer,deleted_at)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0,%s,%s,%s,0,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0)`,
|
||||
used_upload_data_transfer,used_download_data_transfer,deleted_at,first_download,first_upload)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0,%s,%s,%s,0,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0,0,0)`,
|
||||
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],
|
||||
sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],
|
||||
sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14],
|
||||
|
|
|
@ -1878,6 +1878,8 @@ func (u *User) getACopy() User {
|
|||
Status: u.Status,
|
||||
ExpirationDate: u.ExpirationDate,
|
||||
LastLogin: u.LastLogin,
|
||||
FirstDownload: u.FirstDownload,
|
||||
FirstUpload: u.FirstUpload,
|
||||
AdditionalInfo: u.AdditionalInfo,
|
||||
Description: u.Description,
|
||||
CreatedAt: u.CreatedAt,
|
||||
|
|
|
@ -533,8 +533,16 @@ func TestBasicFTPHandling(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = ftpUploadFile(testFilePath, path.Join("/missing_dir", testFileName), testFileSize, client, 0)
|
||||
assert.Error(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), user.FirstUpload)
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, user.FirstUpload, int64(0))
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
// overwrite an existing file
|
||||
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
|
||||
assert.NoError(t, err)
|
||||
|
@ -545,6 +553,8 @@ func TestBasicFTPHandling(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
assert.Greater(t, user.FirstUpload, int64(0))
|
||||
assert.Greater(t, user.FirstDownload, int64(0))
|
||||
err = client.Rename(testFileName, testFileName+"1")
|
||||
assert.NoError(t, err)
|
||||
err = client.Delete(testFileName)
|
||||
|
|
|
@ -1995,6 +1995,8 @@ func TestUserTimestamps(t *testing.T) {
|
|||
createdAt := user.CreatedAt
|
||||
updatedAt := user.UpdatedAt
|
||||
assert.Equal(t, int64(0), user.LastLogin)
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
assert.Equal(t, int64(0), user.FirstUpload)
|
||||
assert.Greater(t, createdAt, int64(0))
|
||||
assert.Greater(t, updatedAt, int64(0))
|
||||
mappedPath := filepath.Join(os.TempDir(), "mapped_dir")
|
||||
|
@ -2010,6 +2012,8 @@ func TestUserTimestamps(t *testing.T) {
|
|||
user, resp, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err, string(resp))
|
||||
assert.Equal(t, int64(0), user.LastLogin)
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
assert.Equal(t, int64(0), user.FirstUpload)
|
||||
assert.Equal(t, createdAt, user.CreatedAt)
|
||||
assert.Greater(t, user.UpdatedAt, updatedAt)
|
||||
updatedAt = user.UpdatedAt
|
||||
|
@ -2023,6 +2027,8 @@ func TestUserTimestamps(t *testing.T) {
|
|||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), user.LastLogin)
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
assert.Equal(t, int64(0), user.FirstUpload)
|
||||
assert.Equal(t, createdAt, user.CreatedAt)
|
||||
assert.Greater(t, user.UpdatedAt, updatedAt)
|
||||
updatedAt = user.UpdatedAt
|
||||
|
@ -2032,6 +2038,8 @@ func TestUserTimestamps(t *testing.T) {
|
|||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), user.LastLogin)
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
assert.Equal(t, int64(0), user.FirstUpload)
|
||||
assert.Equal(t, createdAt, user.CreatedAt)
|
||||
assert.Greater(t, user.UpdatedAt, updatedAt)
|
||||
|
||||
|
@ -13266,6 +13274,10 @@ func TestWebFilesAPI(t *testing.T) {
|
|||
assert.Contains(t, rr.Body.String(), "Unable to parse multipart form")
|
||||
_, err = reader.Seek(0, io.SeekStart)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), user.FirstUpload)
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
// set the proper content type
|
||||
req, err = http.NewRequest(http.MethodPost, userFilesPath, reader)
|
||||
assert.NoError(t, err)
|
||||
|
@ -13273,6 +13285,10 @@ func TestWebFilesAPI(t *testing.T) {
|
|||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, user.FirstUpload, int64(0))
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
// check we have 2 files
|
||||
req, err = http.NewRequest(http.MethodGet, userDirsPath, nil)
|
||||
assert.NoError(t, err)
|
||||
|
@ -13283,6 +13299,17 @@ func TestWebFilesAPI(t *testing.T) {
|
|||
err = json.NewDecoder(rr.Body).Decode(&contents)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, contents, 2)
|
||||
// download a file
|
||||
req, err = http.NewRequest(http.MethodGet, userFilesPath+"?path=file1.txt", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Equal(t, "file1 content", rr.Body.String())
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, user.FirstUpload, int64(0))
|
||||
assert.Greater(t, user.FirstDownload, int64(0))
|
||||
// overwrite the existing files
|
||||
_, err = reader.Seek(0, io.SeekStart)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -510,8 +510,16 @@ func TestBasicSFTPHandling(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, path.Join("/missing_dir", testFileName), testFileSize, client)
|
||||
assert.Error(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), user.FirstUpload)
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, user.FirstUpload, int64(0))
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
|
@ -527,6 +535,8 @@ func TestBasicSFTPHandling(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)
|
||||
assert.Greater(t, user.FirstUpload, int64(0))
|
||||
assert.Greater(t, user.FirstDownload, int64(0))
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(localDownloadPath)
|
||||
|
@ -9550,10 +9560,22 @@ func TestSCPBasicHandling(t *testing.T) {
|
|||
// test to download a missing file
|
||||
err = scpDownload(localPath, remoteDownPath, false, false)
|
||||
assert.Error(t, err, "downloading a missing file via scp must fail")
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), user.FirstUpload)
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
err = scpUpload(testFilePath, remoteUpPath, false, false)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, user.FirstUpload, int64(0))
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
err = scpDownload(localPath, remoteDownPath, false, false)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, user.FirstUpload, int64(0))
|
||||
assert.Greater(t, user.FirstDownload, int64(0))
|
||||
fi, err := os.Stat(localPath)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, testFileSize, fi.Size())
|
||||
|
|
|
@ -526,9 +526,17 @@ func TestBasicHandling(t *testing.T) {
|
|||
expectedQuotaFiles := 1
|
||||
err = createTestFile(testFilePath, testFileSize)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), user.FirstUpload)
|
||||
assert.Equal(t, int64(0), user.FirstDownload)
|
||||
err = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,
|
||||
true, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, user.FirstUpload, int64(0))
|
||||
assert.Greater(t, user.FirstDownload, int64(0)) // webdav read the mime type
|
||||
// overwrite an existing file
|
||||
err = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,
|
||||
true, testFileSize, client)
|
||||
|
@ -544,6 +552,8 @@ func TestBasicHandling(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
assert.Greater(t, user.FirstUpload, int64(0))
|
||||
assert.Greater(t, user.FirstDownload, int64(0))
|
||||
err = client.Rename(testFileName, testFileName+"1", false)
|
||||
assert.NoError(t, err)
|
||||
_, err = client.Stat(testFileName)
|
||||
|
@ -563,6 +573,7 @@ func TestBasicHandling(t *testing.T) {
|
|||
assert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)
|
||||
err = downloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||
assert.Error(t, err)
|
||||
|
||||
testDir := "testdir"
|
||||
err = client.Mkdir(testDir, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -5201,6 +5201,14 @@ components:
|
|||
type: integer
|
||||
format: int64
|
||||
description: Last user login as unix timestamp in milliseconds. It is saved at most once every 10 minutes
|
||||
first_download:
|
||||
type: integer
|
||||
format: int64
|
||||
description: first download time as unix timestamp in milliseconds
|
||||
first_upload:
|
||||
type: integer
|
||||
format: int64
|
||||
description: first upload time as unix timestamp in milliseconds
|
||||
filters:
|
||||
$ref: '#/components/schemas/UserFilters'
|
||||
filesystem:
|
||||
|
|
Loading…
Reference in a new issue