event action: add update modtime to fs rename

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-08-09 20:18:33 +02:00
parent a5c5e85144
commit 81433e00d1
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
30 changed files with 383 additions and 182 deletions

16
go.mod
View file

@ -4,7 +4,7 @@ go 1.22.2
require (
cloud.google.com/go/storage v1.43.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/alexedwards/argon2id v1.0.0
@ -70,7 +70,7 @@ require (
golang.org/x/crypto v0.26.0
golang.org/x/net v0.28.0
golang.org/x/oauth2 v0.22.0
golang.org/x/sys v0.23.0
golang.org/x/sys v0.24.0
golang.org/x/term v0.23.0
golang.org/x/time v0.6.0
google.golang.org/api v0.191.0
@ -80,9 +80,9 @@ require (
require (
cloud.google.com/go v0.115.0 // indirect
cloud.google.com/go/auth v0.8.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
cloud.google.com/go/compute/metadata v0.5.0 // indirect
cloud.google.com/go/iam v1.1.12 // indirect
cloud.google.com/go/iam v1.1.13 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/ajg/form v1.5.1 // indirect
@ -167,15 +167,15 @@ require (
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.24.0 // indirect
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
google.golang.org/genproto v0.0.0-20240805194559-2c9e96a0b5d4 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240805194559-2c9e96a0b5d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240805194559-2c9e96a0b5d4 // indirect
google.golang.org/genproto v0.0.0-20240808171019-573a1156607a // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240808171019-573a1156607a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240808171019-573a1156607a // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect

32
go.sum
View file

@ -3,12 +3,12 @@ cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
cloud.google.com/go/auth v0.8.0 h1:y8jUJLl/Fg+qNBWxP/Hox2ezJvjkrPb952PC1p0G6A4=
cloud.google.com/go/auth v0.8.0/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc=
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/iam v1.1.12 h1:JixGLimRrNGcxvJEQ8+clfLxPlbeZA6MuRJ+qJNQ5Xw=
cloud.google.com/go/iam v1.1.12/go.mod h1:9LDX8J7dN5YRyzVHxwQzrQs9opFFqn0Mxs9nAeB+Hhg=
cloud.google.com/go/iam v1.1.13 h1:7zWBXG9ERbMLrzQBRhFliAV+kjcRToDTgQT3CTwYyv4=
cloud.google.com/go/iam v1.1.13/go.mod h1:K8mY0uSXwEXS30KrnVb+j54LB/ntfZu1dr+4zFMNbus=
cloud.google.com/go/kms v1.18.4 h1:dYN3OCsQ6wJLLtOnI8DGUwQ5shMusXsWCCC+s09ATsk=
cloud.google.com/go/kms v1.18.4/go.mod h1:SG1bgQ3UWW6/KdPo9uuJnzELXY5YTTMJtDYvajiQ22g=
cloud.google.com/go/longrunning v0.5.11 h1:Havn1kGjz3whCfoD8dxMLP73Ph5w+ODyZB9RUsDxtGk=
@ -18,8 +18,8 @@ cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502Jw
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 h1:GJHeeA2N7xrG3q30L2UXDyuWRzDM900/65j70wcM4Ww=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
@ -419,8 +419,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
gocloud.dev v0.38.0 h1:SpxfaOc/Fp4PeO8ui7wRcCZV0EgXZ+IWcVSLn6ZMSw0=
gocloud.dev v0.38.0/go.mod h1:3XjKvd2E5iVNu/xFImRzjN0d/fkNHe4s0RiKidpEUMQ=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@ -479,8 +479,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -525,12 +525,12 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240805194559-2c9e96a0b5d4 h1:g+rQ3aqOyXK/0qwnC5TGUXnyIeipstP5SsniB9uPJ2c=
google.golang.org/genproto v0.0.0-20240805194559-2c9e96a0b5d4/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc=
google.golang.org/genproto/googleapis/api v0.0.0-20240805194559-2c9e96a0b5d4 h1:ABEBT/sZ7We8zd7A5f3KO6zMQe+s3901H7l8Whhijt0=
google.golang.org/genproto/googleapis/api v0.0.0-20240805194559-2c9e96a0b5d4/go.mod h1:4+X6GvPs+25wZKbQq9qyAXrwIRExv7w0Ea6MgZLZiDM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240805194559-2c9e96a0b5d4 h1:OsSGQeIIsyOEOimVxLEIL4rwGcnrjOydQaiA2bOnZUM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240805194559-2c9e96a0b5d4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/genproto v0.0.0-20240808171019-573a1156607a h1:3JVv3Ujh+kGiajpSqHWnbWPuu0nQqMZ3hASNDDF9974=
google.golang.org/genproto v0.0.0-20240808171019-573a1156607a/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc=
google.golang.org/genproto/googleapis/api v0.0.0-20240808171019-573a1156607a h1:KyUe15n7B1YCu+kMmPtlXxgkLQbp+Dw0tCRZf9Sd+CE=
google.golang.org/genproto/googleapis/api v0.0.0-20240808171019-573a1156607a/go.mod h1:4+X6GvPs+25wZKbQq9qyAXrwIRExv7w0Ea6MgZLZiDM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240808171019-573a1156607a h1:EKiZZXueP9/T68B8Nl0GAx9cjbQnCId0yP3qPMgaaHs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240808171019-573a1156607a/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=

View file

@ -787,10 +787,12 @@ func (c *BaseConnection) Copy(virtualSourcePath, virtualTargetPath string) error
// Rename renames (moves) virtualSourcePath to virtualTargetPath
func (c *BaseConnection) Rename(virtualSourcePath, virtualTargetPath string) error {
return c.renameInternal(virtualSourcePath, virtualTargetPath, false)
return c.renameInternal(virtualSourcePath, virtualTargetPath, false, vfs.CheckParentDir)
}
func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath string, checkParentDestination bool) error {
func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath string,
checkParentDestination bool, checks int,
) error {
if virtualSourcePath == virtualTargetPath {
return fmt.Errorf("the rename source and target cannot be the same: %w", c.GetOpUnsupportedError())
}
@ -844,7 +846,7 @@ func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath str
defer close(done)
go keepConnectionAlive(c, done, 2*time.Minute)
files, size, err := fsDst.Rename(fsSourcePath, fsTargetPath)
files, size, err := fsDst.Rename(fsSourcePath, fsTargetPath, checks)
if err != nil {
c.Log(logger.LevelError, "failed to rename %q -> %q: %+v", fsSourcePath, fsTargetPath, err)
return c.GetFsError(fsSrc, err)

View file

@ -1753,7 +1753,7 @@ func executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer,
return nil
}
func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *strings.Replacer,
func executeRenameFsActionForUser(renames []dataprovider.RenameConfig, replacer *strings.Replacer,
user dataprovider.User,
) error {
user, err := getUserForEventAction(user)
@ -1770,7 +1770,11 @@ func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *str
for _, item := range renames {
source := util.CleanPath(replaceWithReplacer(item.Key, replacer))
target := util.CleanPath(replaceWithReplacer(item.Value, replacer))
if err = conn.renameInternal(source, target, true); err != nil {
checks := 0
if item.UpdateModTime {
checks += vfs.CheckUpdateModTime
}
if err = conn.renameInternal(source, target, true, checks); err != nil {
return fmt.Errorf("unable to rename %q->%q, user %q: %w", source, target, user.Username, err)
}
eventManagerLog(logger.LevelDebug, "rename %q->%q ok, user %q", source, target, user.Username)
@ -1832,7 +1836,7 @@ func executeExistFsActionForUser(exist []string, replacer *strings.Replacer,
return nil
}
func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *strings.Replacer,
func executeRenameFsRuleAction(renames []dataprovider.RenameConfig, replacer *strings.Replacer,
conditions dataprovider.ConditionOptions, params *EventParams,
) error {
users, err := params.getUsers()

View file

@ -1235,10 +1235,12 @@ func TestEventRuleActions(t *testing.T) {
action.Options = dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionRename,
Renames: []dataprovider.KeyValue{
Renames: []dataprovider.RenameConfig{
{
Key: "/source",
Value: "/target",
KeyValue: dataprovider.KeyValue{
Key: "/source",
Value: "/target",
},
},
},
},
@ -1778,10 +1780,12 @@ func TestFilesystemActionErrors(t *testing.T) {
assert.NoError(t, err)
err = dataprovider.AddUser(&user, "", "", "")
assert.NoError(t, err)
err = executeRenameFsActionForUser([]dataprovider.KeyValue{
err = executeRenameFsActionForUser([]dataprovider.RenameConfig{
{
Key: "/p1",
Value: "/p1",
KeyValue: dataprovider.KeyValue{
Key: "/p1",
Value: "/p1",
},
},
}, testReplacer, user)
if assert.Error(t, err) {
@ -1792,10 +1796,12 @@ func TestFilesystemActionErrors(t *testing.T) {
Options: dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionRename,
Renames: []dataprovider.KeyValue{
Renames: []dataprovider.RenameConfig{
{
Key: "/p2",
Value: "/p2",
KeyValue: dataprovider.KeyValue{
Key: "/p2",
Value: "/p2",
},
},
},
},

View file

@ -4301,10 +4301,12 @@ func TestEventRuleFsActions(t *testing.T) {
Options: dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionRename,
Renames: []dataprovider.KeyValue{
Renames: []dataprovider.RenameConfig{
{
Key: "/{{VirtualDirPath}}/{{ObjectName}}",
Value: "/{{ObjectName}}_renamed",
KeyValue: dataprovider.KeyValue{
Key: "/{{VirtualDirPath}}/{{ObjectName}}",
Value: "/{{ObjectName}}_renamed",
},
},
},
},
@ -4711,10 +4713,13 @@ func TestEventRulePreDelete(t *testing.T) {
Options: dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionRename,
Renames: []dataprovider.KeyValue{
Renames: []dataprovider.RenameConfig{
{
Key: "/{{VirtualPath}}",
Value: fmt.Sprintf("/%s/{{VirtualPath}}", movePath),
KeyValue: dataprovider.KeyValue{
Key: "/{{VirtualPath}}",
Value: fmt.Sprintf("/%s/{{VirtualPath}}", movePath),
},
UpdateModTime: true,
},
},
},
@ -4768,59 +4773,83 @@ func TestEventRulePreDelete(t *testing.T) {
QuotaFiles: 1000,
},
}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
u = getTestSFTPUser()
u.QuotaFiles = 1000
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
conn, client, err := getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
testDir := "sub dir"
err = client.MkdirAll(testDir)
assert.NoError(t, err)
err = writeSFTPFile(testFileName, 100, client)
assert.NoError(t, err)
err = writeSFTPFile(path.Join(testDir, testFileName), 100, client)
assert.NoError(t, err)
err = client.Remove(testFileName)
assert.NoError(t, err)
err = client.Remove(path.Join(testDir, testFileName))
assert.NoError(t, err)
// check files
_, err = client.Stat(testFileName)
assert.ErrorIs(t, err, os.ErrNotExist)
_, err = client.Stat(path.Join(testDir, testFileName))
assert.ErrorIs(t, err, os.ErrNotExist)
_, err = client.Stat(path.Join("/", movePath, testFileName))
assert.NoError(t, err)
_, err = client.Stat(path.Join("/", movePath, testDir, testFileName))
assert.NoError(t, err)
// check quota
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, 0, user.UsedQuotaFiles)
assert.Equal(t, int64(0), user.UsedQuotaSize)
folder, _, err := httpdtest.GetFolderByName(movePath, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, 2, folder.UsedQuotaFiles)
assert.Equal(t, int64(200), folder.UsedQuotaSize)
// pre-delete action is not executed in movePath
err = client.Remove(path.Join("/", movePath, testFileName))
assert.NoError(t, err)
// check quota
folder, _, err = httpdtest.GetFolderByName(movePath, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, 1, folder.UsedQuotaFiles)
assert.Equal(t, int64(100), folder.UsedQuotaSize)
for _, user := range []dataprovider.User{localUser, sftpUser} {
conn, client, err := getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
testDir := "sub dir"
err = client.MkdirAll(testDir)
assert.NoError(t, err)
err = writeSFTPFile(testFileName, 100, client)
assert.NoError(t, err)
err = writeSFTPFile(path.Join(testDir, testFileName), 100, client)
assert.NoError(t, err)
modTime := time.Now().Add(-36 * time.Hour)
err = client.Chtimes(testFileName, modTime, modTime)
assert.NoError(t, err)
err = client.Remove(testFileName)
assert.NoError(t, err)
err = client.Remove(path.Join(testDir, testFileName))
assert.NoError(t, err)
// check files
_, err = client.Stat(testFileName)
assert.ErrorIs(t, err, os.ErrNotExist)
_, err = client.Stat(path.Join(testDir, testFileName))
assert.ErrorIs(t, err, os.ErrNotExist)
info, err := client.Stat(path.Join("/", movePath, testFileName))
assert.NoError(t, err)
diff := math.Abs(time.Until(info.ModTime()).Seconds())
assert.LessOrEqual(t, diff, float64(2))
_, err = client.Stat(path.Join("/", movePath, testDir, testFileName))
assert.NoError(t, err)
// check quota
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
if user.Username == localUser.Username {
assert.Equal(t, 0, user.UsedQuotaFiles)
assert.Equal(t, int64(0), user.UsedQuotaSize)
folder, _, err := httpdtest.GetFolderByName(movePath, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, 2, folder.UsedQuotaFiles)
assert.Equal(t, int64(200), folder.UsedQuotaSize)
} else {
assert.Equal(t, 1, user.UsedQuotaFiles)
assert.Equal(t, int64(100), user.UsedQuotaSize)
}
// pre-delete action is not executed in movePath
err = client.Remove(path.Join("/", movePath, testFileName))
assert.NoError(t, err)
if user.Username == localUser.Username {
// check quota
folder, _, err := httpdtest.GetFolderByName(movePath, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, 1, folder.UsedQuotaFiles)
assert.Equal(t, int64(100), folder.UsedQuotaSize)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
}
}
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: movePath}, http.StatusOK)
assert.NoError(t, err)
@ -4848,10 +4877,12 @@ func TestEventRulePreDownloadUpload(t *testing.T) {
Options: dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionRename,
Renames: []dataprovider.KeyValue{
Renames: []dataprovider.RenameConfig{
{
Key: "/missing source",
Value: "/missing target",
KeyValue: dataprovider.KeyValue{
Key: "/missing source",
Value: "/missing target",
},
},
},
},

View file

@ -394,7 +394,7 @@ func (t *BaseTransfer) Close() error {
t.effectiveFsPath, err)
} else if t.isAtomicUpload() {
if t.ErrTransfer == nil || Config.UploadMode&UploadModeAtomicWithResume != 0 {
_, _, err = t.Fs.Rename(t.effectiveFsPath, t.fsPath)
_, _, err = t.Fs.Rename(t.effectiveFsPath, t.fsPath, 0)
t.Connection.Log(logger.LevelDebug, "atomic upload completed, rename: %q -> %q, error: %v",
t.effectiveFsPath, t.fsPath, err)
// the file must be removed if it is uploaded to a path outside the home dir and cannot be renamed

View file

@ -660,12 +660,21 @@ func (c *EventActionFsCompress) validate() error {
return nil
}
// RenameConfig defines the configuration for a filesystem rename
type RenameConfig struct {
// key is the source and target the value
KeyValue
// This setting only applies to storage providers that support
// changing modification times.
UpdateModTime bool `json:"update_modtime,omitempty"`
}
// EventActionFilesystemConfig defines the configuration for filesystem actions
type EventActionFilesystemConfig struct {
// Filesystem actions, see the above enum
Type int `json:"type,omitempty"`
// files/dirs to rename, key is the source and target the value
Renames []KeyValue `json:"renames,omitempty"`
// files/dirs to rename
Renames []RenameConfig `json:"renames,omitempty"`
// directories to create
MkDirs []string `json:"mkdirs,omitempty"`
// files/dirs to delete
@ -706,9 +715,9 @@ func (c *EventActionFilesystemConfig) validateRenames() error {
if len(c.Renames) == 0 {
return util.NewI18nError(util.NewValidationError("no path to rename specified"), util.I18nErrorPathRequired)
}
for idx, kv := range c.Renames {
key := strings.TrimSpace(kv.Key)
value := strings.TrimSpace(kv.Value)
for idx, cfg := range c.Renames {
key := strings.TrimSpace(cfg.Key)
value := strings.TrimSpace(cfg.Value)
if key == "" || value == "" {
return util.NewValidationError("invalid paths to rename")
}
@ -726,9 +735,12 @@ func (c *EventActionFilesystemConfig) validateRenames() error {
util.I18nErrorRootNotAllowed,
)
}
c.Renames[idx] = KeyValue{
Key: key,
Value: value,
c.Renames[idx] = RenameConfig{
KeyValue: KeyValue{
Key: key,
Value: value,
},
UpdateModTime: cfg.UpdateModTime,
}
}
return nil
@ -892,7 +904,7 @@ func (c *EventActionFilesystemConfig) getACopy() EventActionFilesystemConfig {
return EventActionFilesystemConfig{
Type: c.Type,
Renames: cloneKeyValues(c.Renames),
Renames: cloneRenameConfigs(c.Renames),
MkDirs: mkdirs,
Deletes: deletes,
Exist: exist,
@ -1833,6 +1845,20 @@ func (r *EventRule) RenderAsJSON(reload bool) ([]byte, error) {
return json.Marshal(r)
}
func cloneRenameConfigs(renames []RenameConfig) []RenameConfig {
res := make([]RenameConfig, 0, len(renames))
for _, c := range renames {
res = append(res, RenameConfig{
KeyValue: KeyValue{
Key: c.Key,
Value: c.Value,
},
UpdateModTime: c.UpdateModTime,
})
}
return res
}
func cloneKeyValues(keyVals []KeyValue) []KeyValue {
res := make([]KeyValue, 0, len(keyVals))
for _, kv := range keyVals {

View file

@ -465,7 +465,7 @@ func (c *Connection) handleFTPUploadToExistingFile(fs vfs.Fs, flags int, resolve
}
if common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {
_, _, err = fs.Rename(resolvedPath, filePath)
_, _, err = fs.Rename(resolvedPath, filePath, 0)
if err != nil {
c.Log(logger.LevelError, "error renaming existing file for atomic upload, source: %q, dest: %q, err: %+v",
resolvedPath, filePath, err)

View file

@ -404,7 +404,7 @@ func (fs MockOsFs) Remove(name string, _ bool) error {
}
// Rename renames (moves) source to target
func (fs MockOsFs) Rename(source, target string) (int, int64, error) {
func (fs MockOsFs) Rename(source, target string, _ int) (int, int64, error) {
if fs.err != nil {
return -1, -1, fs.err
}

View file

@ -176,7 +176,7 @@ func (c *Connection) getFileWriter(name string) (io.WriteCloser, error) {
}
if common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {
_, _, err = fs.Rename(p, filePath)
_, _, err = fs.Rename(p, filePath, 0)
if err != nil {
c.Log(logger.LevelError, "error renaming existing file for atomic upload, source: %q, dest: %q, err: %+v",
p, filePath, err)

View file

@ -2458,28 +2458,34 @@ func TestEventActionValidation(t *testing.T) {
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
assert.NoError(t, err)
assert.Contains(t, string(resp), "no path to rename specified")
action.Options.FsConfig.Renames = []dataprovider.KeyValue{
action.Options.FsConfig.Renames = []dataprovider.RenameConfig{
{
Key: "",
Value: "/adir",
KeyValue: dataprovider.KeyValue{
Key: "",
Value: "/adir",
},
},
}
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
assert.NoError(t, err)
assert.Contains(t, string(resp), "invalid paths to rename")
action.Options.FsConfig.Renames = []dataprovider.KeyValue{
action.Options.FsConfig.Renames = []dataprovider.RenameConfig{
{
Key: "adir",
Value: "/adir",
KeyValue: dataprovider.KeyValue{
Key: "adir",
Value: "/adir",
},
},
}
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
assert.NoError(t, err)
assert.Contains(t, string(resp), "rename source and target cannot be equal")
action.Options.FsConfig.Renames = []dataprovider.KeyValue{
action.Options.FsConfig.Renames = []dataprovider.RenameConfig{
{
Key: "/",
Value: "/dir",
KeyValue: dataprovider.KeyValue{
Key: "/",
Value: "/dir",
},
},
}
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
@ -23974,16 +23980,19 @@ func TestWebEventAction(t *testing.T) {
action.Options.FsConfig = dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionRename,
Renames: []dataprovider.KeyValue{
Renames: []dataprovider.RenameConfig{
{
Key: "/src",
Value: "/target",
KeyValue: dataprovider.KeyValue{
Key: "/src",
Value: "/target",
},
},
},
}
form.Set("fs_action_type", fmt.Sprintf("%d", action.Options.FsConfig.Type))
form.Set("fs_rename[0][fs_rename_source]", action.Options.FsConfig.Renames[0].Key)
form.Set("fs_rename[0][fs_rename_target]", action.Options.FsConfig.Renames[0].Value)
form.Set("fs_rename[0][fs_rename_options][]", "1")
req, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),
bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
@ -23995,7 +24004,9 @@ func TestWebEventAction(t *testing.T) {
actionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, action.Type, actionGet.Type)
assert.Len(t, actionGet.Options.FsConfig.Renames, 1)
if assert.Len(t, actionGet.Options.FsConfig.Renames, 1) {
assert.True(t, actionGet.Options.FsConfig.Renames[0].UpdateModTime)
}
action.Options.FsConfig = dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionCopy,

View file

@ -2201,6 +2201,28 @@ func getKeyValsFromPostFields(r *http.Request, key, val string) []dataprovider.K
return res
}
func getRenameConfigsFromPostFields(r *http.Request) []dataprovider.RenameConfig {
var res []dataprovider.RenameConfig
keys := r.Form["fs_rename_source"]
values := r.Form["fs_rename_target"]
for idx, k := range keys {
v := values[idx]
if k != "" && v != "" {
opts := r.Form["fs_rename_options"+strconv.Itoa(idx)]
res = append(res, dataprovider.RenameConfig{
KeyValue: dataprovider.KeyValue{
Key: k,
Value: v,
},
UpdateModTime: slices.Contains(opts, "1"),
})
}
}
return res
}
func getFoldersRetentionFromPostFields(r *http.Request) ([]dataprovider.FolderRetention, error) {
var res []dataprovider.FolderRetention
paths := r.Form["folder_retention_path"]
@ -2310,6 +2332,8 @@ func updateRepeaterFormActionFields(r *http.Request) {
base, _ := strings.CutSuffix(k, "[fs_rename_source]")
r.Form.Add("fs_rename_source", strings.TrimSpace(r.Form.Get(k)))
r.Form.Add("fs_rename_target", strings.TrimSpace(r.Form.Get(base+"[fs_rename_target]")))
r.Form["fs_rename_options"+strconv.Itoa(len(r.Form["fs_rename_source"])-1)] =
r.Form[base+"[fs_rename_options][]"]
continue
}
if hasPrefixAndSuffix(k, "fs_copy[", "][fs_copy_source]") {
@ -2398,7 +2422,7 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
},
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: fsActionType,
Renames: getKeyValsFromPostFields(r, "fs_rename_source", "fs_rename_target"),
Renames: getRenameConfigsFromPostFields(r),
Deletes: getSliceFromDelimitedValues(r.Form.Get("fs_delete_paths"), ","),
MkDirs: getSliceFromDelimitedValues(r.Form.Get("fs_mkdir_paths"), ","),
Exist: getSliceFromDelimitedValues(r.Form.Get("fs_exist_paths"), ","),

View file

@ -2601,9 +2601,28 @@ func compareUserFilePatternsFilters(expected sdk.BaseUserFilters, actual sdk.Bas
return nil
}
func compareRenameConfigs(expected, actual []dataprovider.RenameConfig) error {
if len(expected) != len(actual) {
return errors.New("rename configs mismatch")
}
for _, ex := range expected {
found := false
for _, ac := range actual {
if ac.Key == ex.Key && ac.Value == ex.Value && ac.UpdateModTime == ex.UpdateModTime {
found = true
break
}
}
if !found {
return errors.New("rename configs mismatch")
}
}
return nil
}
func compareKeyValues(expected, actual []dataprovider.KeyValue) error {
if len(expected) != len(actual) {
return errors.New("kay values mismatch")
return errors.New("key values mismatch")
}
for _, ex := range expected {
found := false
@ -2614,7 +2633,7 @@ func compareKeyValues(expected, actual []dataprovider.KeyValue) error {
}
}
if !found {
return errors.New("kay values mismatch")
return errors.New("key values mismatch")
}
}
return nil
@ -2731,7 +2750,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
if expected.Type != actual.Type {
return errors.New("fs type mismatch")
}
if err := compareKeyValues(expected.Renames, actual.Renames); err != nil {
if err := compareRenameConfigs(expected.Renames, actual.Renames); err != nil {
return errors.New("fs renames mismatch")
}
if err := compareKeyValues(expected.Copy, actual.Copy); err != nil {

View file

@ -457,7 +457,7 @@ func (c *Connection) handleSFTPUploadToExistingFile(fs vfs.Fs, pflags sftp.FileO
}
if common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {
_, _, err = fs.Rename(resolvedPath, filePath)
_, _, err = fs.Rename(resolvedPath, filePath, 0)
if err != nil {
c.Log(logger.LevelError, "error renaming existing file for atomic upload, source: %q, dest: %q, err: %+v",
resolvedPath, filePath, err)

View file

@ -146,7 +146,7 @@ func (fs MockOsFs) Remove(name string, _ bool) error {
}
// Rename renames (moves) source to target
func (fs MockOsFs) Rename(source, target string) (int, int64, error) {
func (fs MockOsFs) Rename(source, target string, _ int) (int, int64, error) {
if fs.err != nil {
return -1, -1, fs.err
}

View file

@ -330,7 +330,7 @@ func (c *scpCommand) handleUpload(uploadFilePath string, sizeToRead int64) error
}
if common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {
_, _, err = fs.Rename(p, filePath)
_, _, err = fs.Rename(p, filePath, 0)
if err != nil {
c.connection.Log(logger.LevelError, "error renaming existing file for atomic upload, source: %q, dest: %q, err: %v",
p, filePath, err)

View file

@ -306,19 +306,21 @@ func (fs *AzureBlobFs) Create(name string, flag, checks int) (File, PipeWriter,
}
// Rename renames (moves) source to target.
func (fs *AzureBlobFs) Rename(source, target string) (int, int64, error) {
func (fs *AzureBlobFs) Rename(source, target string, checks int) (int, int64, error) {
if source == target {
return -1, -1, nil
}
_, err := fs.Stat(path.Dir(target))
if err != nil {
return -1, -1, err
if checks&CheckParentDir != 0 {
_, err := fs.Stat(path.Dir(target))
if err != nil {
return -1, -1, err
}
}
fi, err := fs.Stat(source)
if err != nil {
return -1, -1, err
}
return fs.renameInternal(source, target, fi, 0)
return fs.renameInternal(source, target, fi, 0, checks&CheckUpdateModTime != 0)
}
// Remove removes the named file or (empty) directory.
@ -398,7 +400,17 @@ func (fs *AzureBlobFs) Chtimes(name string, _, mtime time.Time, isUploading bool
if metadata == nil {
metadata = make(map[string]*string)
}
metadata[lastModifiedField] = to.Ptr(strconv.FormatInt(mtime.UnixMilli(), 10))
found := false
for k := range metadata {
if strings.ToLower(k) == lastModifiedField {
metadata[k] = to.Ptr(strconv.FormatInt(mtime.UnixMilli(), 10))
found = true
break
}
}
if !found {
metadata[lastModifiedField] = to.Ptr(strconv.FormatInt(mtime.UnixMilli(), 10))
}
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
@ -667,7 +679,7 @@ func (fs *AzureBlobFs) CopyFile(source, target string, srcInfo os.FileInfo) (int
return 0, 0, err
}
}
if err := fs.copyFileInternal(source, target, srcInfo); err != nil {
if err := fs.copyFileInternal(source, target, srcInfo, true); err != nil {
return 0, 0, err
}
return numFiles, sizeDiff, nil
@ -750,13 +762,13 @@ func (fs *AzureBlobFs) setConfigDefaults() {
}
}
func (fs *AzureBlobFs) copyFileInternal(source, target string, srcInfo os.FileInfo) error {
func (fs *AzureBlobFs) copyFileInternal(source, target string, srcInfo os.FileInfo, updateModTime bool) error {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxLongTimeout))
defer cancelFn()
srcBlob := fs.containerClient.NewBlockBlobClient(source)
dstBlob := fs.containerClient.NewBlockBlobClient(target)
resp, err := dstBlob.StartCopyFromURL(ctx, srcBlob.URL(), fs.getCopyOptions(srcInfo))
resp, err := dstBlob.StartCopyFromURL(ctx, srcBlob.URL(), fs.getCopyOptions(srcInfo, updateModTime))
if err != nil {
metric.AZCopyObjectCompleted(err)
return err
@ -789,7 +801,9 @@ func (fs *AzureBlobFs) copyFileInternal(source, target string, srcInfo os.FileIn
return nil
}
func (fs *AzureBlobFs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int) (int, int64, error) {
func (fs *AzureBlobFs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int,
updateModTime bool,
) (int, int64, error) {
var numFiles int
var filesSize int64
@ -807,7 +821,7 @@ func (fs *AzureBlobFs) renameInternal(source, target string, srcInfo os.FileInfo
return numFiles, filesSize, err
}
if renameMode == 1 {
files, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion)
files, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion, updateModTime)
numFiles += files
filesSize += size
if err != nil {
@ -815,7 +829,7 @@ func (fs *AzureBlobFs) renameInternal(source, target string, srcInfo os.FileInfo
}
}
} else {
if err := fs.copyFileInternal(source, target, srcInfo); err != nil {
if err := fs.copyFileInternal(source, target, srcInfo, updateModTime); err != nil {
return numFiles, filesSize, err
}
numFiles++
@ -1102,20 +1116,27 @@ func (*AzureBlobFs) readFill(r io.Reader, buf []byte) (n int, err error) {
return n, err
}
func (fs *AzureBlobFs) getCopyOptions(srcInfo os.FileInfo) *blob.StartCopyFromURLOptions {
func (fs *AzureBlobFs) getCopyOptions(srcInfo os.FileInfo, updateModTime bool) *blob.StartCopyFromURLOptions {
copyOptions := &blob.StartCopyFromURLOptions{}
if fs.config.AccessTier != "" {
copyOptions.Tier = (*blob.AccessTier)(&fs.config.AccessTier)
}
metadata := make(map[string]*string)
for k, v := range getMetadata(srcInfo) {
if v != "" {
metadata[k] = to.Ptr(v)
if updateModTime {
metadata := make(map[string]*string)
for k, v := range getMetadata(srcInfo) {
if v != "" {
if strings.ToLower(k) == lastModifiedField {
metadata[k] = to.Ptr("0")
} else {
metadata[k] = to.Ptr(v)
}
}
}
if len(metadata) > 0 {
copyOptions.Metadata = metadata
}
}
if len(metadata) > 0 {
copyOptions.Metadata = metadata
}
return copyOptions
}

View file

@ -255,19 +255,21 @@ func (fs *GCSFs) Create(name string, flag, checks int) (File, PipeWriter, func()
}
// Rename renames (moves) source to target.
func (fs *GCSFs) Rename(source, target string) (int, int64, error) {
func (fs *GCSFs) Rename(source, target string, checks int) (int, int64, error) {
if source == target {
return -1, -1, nil
}
_, err := fs.Stat(path.Dir(target))
if err != nil {
return -1, -1, err
if checks&CheckParentDir != 0 {
_, err := fs.Stat(path.Dir(target))
if err != nil {
return -1, -1, err
}
}
fi, err := fs.getObjectStat(source)
if err != nil {
return -1, -1, err
}
return fs.renameInternal(source, target, fi, 0)
return fs.renameInternal(source, target, fi, 0, checks&CheckUpdateModTime != 0)
}
// Remove removes the named file or (empty) directory.
@ -651,7 +653,7 @@ func (fs *GCSFs) CopyFile(source, target string, srcInfo os.FileInfo) (int, int6
}
conditions = &storage.Conditions{DoesNotExist: true}
}
if err := fs.copyFileInternal(source, target, conditions, srcInfo); err != nil {
if err := fs.copyFileInternal(source, target, conditions, srcInfo, true); err != nil {
return 0, 0, err
}
return numFiles, sizeDiff, nil
@ -753,7 +755,9 @@ func (fs *GCSFs) composeObjects(ctx context.Context, dst, partialObject *storage
return err
}
func (fs *GCSFs) copyFileInternal(source, target string, conditions *storage.Conditions, srcInfo os.FileInfo) error {
func (fs *GCSFs) copyFileInternal(source, target string, conditions *storage.Conditions,
srcInfo os.FileInfo, updateModTime bool,
) error {
src := fs.svc.Bucket(fs.config.Bucket).Object(source)
dst := fs.svc.Bucket(fs.config.Bucket).Object(target)
if conditions != nil {
@ -785,6 +789,9 @@ func (fs *GCSFs) copyFileInternal(source, target string, conditions *storage.Con
copier.ContentType = contentType
}
metadata := getMetadata(srcInfo)
if updateModTime && len(metadata) > 0 {
delete(metadata, lastModifiedField)
}
if len(metadata) > 0 {
copier.Metadata = metadata
}
@ -793,7 +800,9 @@ func (fs *GCSFs) copyFileInternal(source, target string, conditions *storage.Con
return err
}
func (fs *GCSFs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int) (int, int64, error) {
func (fs *GCSFs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int,
updateModTime bool,
) (int, int64, error) {
var numFiles int
var filesSize int64
@ -811,7 +820,7 @@ func (fs *GCSFs) renameInternal(source, target string, srcInfo os.FileInfo, recu
return numFiles, filesSize, err
}
if renameMode == 1 {
files, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion)
files, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion, updateModTime)
numFiles += files
filesSize += size
if err != nil {
@ -819,7 +828,7 @@ func (fs *GCSFs) renameInternal(source, target string, srcInfo os.FileInfo, recu
}
}
} else {
if err := fs.copyFileInternal(source, target, nil, srcInfo); err != nil {
if err := fs.copyFileInternal(source, target, nil, srcInfo, updateModTime); err != nil {
return numFiles, filesSize, err
}
numFiles++

View file

@ -384,7 +384,7 @@ func (fs *HTTPFs) Create(name string, flag, checks int) (File, PipeWriter, func(
}
// Rename renames (moves) source to target.
func (fs *HTTPFs) Rename(source, target string) (int, int64, error) {
func (fs *HTTPFs) Rename(source, target string, checks int) (int, int64, error) {
if source == target {
return -1, -1, nil
}
@ -397,6 +397,9 @@ func (fs *HTTPFs) Rename(source, target string) (int, int64, error) {
return -1, -1, err
}
defer resp.Body.Close()
if checks&CheckUpdateModTime != 0 {
fs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck
}
return -1, -1, nil
}

View file

@ -176,7 +176,7 @@ func (fs *OsFs) Create(name string, flag, _ int) (File, PipeWriter, func(), erro
}
// Rename renames (moves) source to target
func (fs *OsFs) Rename(source, target string) (int, int64, error) {
func (fs *OsFs) Rename(source, target string, checks int) (int, int64, error) {
if source == target {
return -1, -1, nil
}
@ -199,9 +199,15 @@ func (fs *OsFs) Rename(source, target string) (int, int64, error) {
fsLog(fs, logger.LevelError, "cross device copy error: %v", err)
return -1, -1, err
}
if checks&CheckUpdateModTime != 0 {
fs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck
}
err = os.RemoveAll(source)
return -1, -1, err
}
if checks&CheckUpdateModTime != 0 && err == nil {
fs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck
}
return -1, -1, err
}

View file

@ -334,19 +334,21 @@ func (fs *S3Fs) Create(name string, flag, checks int) (File, PipeWriter, func(),
}
// Rename renames (moves) source to target.
func (fs *S3Fs) Rename(source, target string) (int, int64, error) {
func (fs *S3Fs) Rename(source, target string, checks int) (int, int64, error) {
if source == target {
return -1, -1, nil
}
_, err := fs.Stat(path.Dir(target))
if err != nil {
return -1, -1, err
if checks&CheckParentDir != 0 {
_, err := fs.Stat(path.Dir(target))
if err != nil {
return -1, -1, err
}
}
fi, err := fs.Stat(source)
if err != nil {
return -1, -1, err
}
return fs.renameInternal(source, target, fi, 0)
return fs.renameInternal(source, target, fi, 0, checks&CheckUpdateModTime != 0)
}
// Remove removes the named file or (empty) directory.
@ -700,21 +702,29 @@ func (fs *S3Fs) copyFileInternal(source, target string, srcInfo os.FileInfo) err
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
_, err := fs.svc.CopyObject(ctx, &s3.CopyObjectInput{
copyObject := &s3.CopyObjectInput{
Bucket: aws.String(fs.config.Bucket),
CopySource: aws.String(copySource),
Key: aws.String(target),
StorageClass: types.StorageClass(fs.config.StorageClass),
ACL: types.ObjectCannedACL(fs.config.ACL),
ContentType: util.NilIfEmpty(contentType),
Metadata: getMetadata(srcInfo),
})
}
metadata := getMetadata(srcInfo)
if len(metadata) > 0 {
copyObject.Metadata = metadata
}
_, err := fs.svc.CopyObject(ctx, copyObject)
metric.S3CopyObjectCompleted(err)
return err
}
func (fs *S3Fs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int) (int, int64, error) {
func (fs *S3Fs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int,
updateModTime bool,
) (int, int64, error) {
var numFiles int
var filesSize int64
@ -732,7 +742,7 @@ func (fs *S3Fs) renameInternal(source, target string, srcInfo os.FileInfo, recur
return numFiles, filesSize, err
}
if renameMode == 1 {
files, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion)
files, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion, updateModTime)
numFiles += files
filesSize += size
if err != nil {

View file

@ -479,7 +479,7 @@ func (fs *SFTPFs) Create(name string, flag, _ int) (File, PipeWriter, func(), er
}
// Rename renames (moves) source to target.
func (fs *SFTPFs) Rename(source, target string) (int, int64, error) {
func (fs *SFTPFs) Rename(source, target string, checks int) (int, int64, error) {
if source == target {
return -1, -1, nil
}
@ -489,9 +489,15 @@ func (fs *SFTPFs) Rename(source, target string) (int, int64, error) {
}
if _, ok := client.HasExtension("posix-rename@openssh.com"); ok {
err := client.PosixRename(source, target)
if checks&CheckUpdateModTime != 0 && err == nil {
fs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck
}
return -1, -1, err
}
err = client.Rename(source, target)
if checks&CheckUpdateModTime != 0 && err == nil {
fs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck
}
return -1, -1, err
}

View file

@ -52,8 +52,9 @@ const (
// Additional checks for files
const (
CheckParentDir = 1
CheckResume = 2
CheckParentDir = 1
CheckResume = 2
CheckUpdateModTime = 4
)
var (
@ -121,7 +122,7 @@ type Fs interface {
Lstat(name string) (os.FileInfo, error)
Open(name string, offset int64) (File, PipeReader, func(), error)
Create(name string, flag, checks int) (File, PipeWriter, func(), error)
Rename(source, target string) (int, int64, error)
Rename(source, target string, checks int) (int, int64, error)
Remove(name string, isDir bool) error
Mkdir(name string) error
Symlink(source, target string) error
@ -1084,7 +1085,7 @@ func IsUploadResumeSupported(fs Fs, size int64) bool {
}
func getLastModified(metadata map[string]string) int64 {
if val, ok := metadata[lastModifiedField]; ok {
if val, ok := metadata[lastModifiedField]; ok && val != "" {
lastModified, err := strconv.ParseInt(val, 10, 64)
if err == nil {
return lastModified
@ -1167,8 +1168,8 @@ func getLocalTempDir() string {
}
func doRecursiveRename(fs Fs, source, target string,
renameFn func(string, string, os.FileInfo, int) (int, int64, error),
recursion int,
renameFn func(string, string, os.FileInfo, int, bool) (int, int64, error),
recursion int, updateModTime bool,
) (int, int64, error) {
var numFiles int
var filesSize int64
@ -1193,7 +1194,7 @@ func doRecursiveRename(fs Fs, source, target string,
for _, info := range entries {
sourceEntry := fs.Join(source, info.Name())
targetEntry := fs.Join(target, info.Name())
files, size, err := renameFn(sourceEntry, targetEntry, info, recursion)
files, size, err := renameFn(sourceEntry, targetEntry, info, recursion, updateModTime)
if err != nil {
if fs.IsNotExist(err) {
fsLog(fs, logger.LevelInfo, "skipping rename for %q: %v", sourceEntry, err)

View file

@ -254,7 +254,7 @@ func (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePat
maxWriteSize, _ := c.GetMaxWriteSize(diskQuota, false, fileSize, fs.IsUploadResumeSupported())
if common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {
_, _, err = fs.Rename(resolvedPath, filePath)
_, _, err = fs.Rename(resolvedPath, filePath, 0)
if err != nil {
c.Log(logger.LevelError, "error renaming existing file for atomic upload, source: %q, dest: %q, err: %+v",
resolvedPath, filePath, err)

View file

@ -312,7 +312,7 @@ func (fs *MockOsFs) Remove(name string, _ bool) error {
}
// Rename renames (moves) source to target
func (fs *MockOsFs) Rename(source, target string) (int, int64, error) {
func (fs *MockOsFs) Rename(source, target string, _ int) (int, int64, error) {
err := os.Rename(source, target)
return -1, -1, err
}

View file

@ -6973,6 +6973,14 @@ components:
type: string
value:
type: string
RenameConfig:
allOf:
- $ref: '#/components/schemas/KeyValue'
- type: object
properties:
update_modtime:
type: boolean
description: 'Update modification time. This setting is not recursive and only applies to storage providers that support changing modification times'
HTTPPart:
type: object
properties:
@ -7105,7 +7113,7 @@ components:
renames:
type: array
items:
$ref: '#/components/schemas/KeyValue'
$ref: '#/components/schemas/RenameConfig'
mkdirs:
type: array
items:

View file

@ -1007,6 +1007,7 @@
"archive_path": "Archive path",
"archive_path_help": "Full path, as seen by SFTPGo users, to the zip archive to create. Placeholders are supported. If the specified file already exists, it is overwritten",
"placeholders_modal_title": "Supported placeholders",
"update_mod_times": "Update timestamp",
"types": {
"http": "HTTP",
"email": "Email",

View file

@ -1007,6 +1007,7 @@
"archive_path": "Percorso dell'archivio",
"archive_path_help": "Percorso completo, come visto dagli utenti SFTPGo, dell'archivio zip da creare. I segnaposto sono supportati. Se il file specificato esiste già, verrà sovrascritto",
"placeholders_modal_title": "Segnaposto supportati",
"update_mod_times": "Aggiorna timestamp",
"types": {
"http": "HTTP",
"email": "Email",

View file

@ -653,12 +653,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div data-repeater-item>
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-5 mt-3 mt-md-8">
<div class="col-md-4 mt-3 mt-md-8">
<input data-i18n="[placeholder]actions.source_path" type="text" class="form-control" name="fs_rename_source" value="{{$val.Key}}" spellcheck="false" />
</div>
<div class="col-md-6 mt-3 mt-md-8">
<div class="col-md-4 mt-3 mt-md-8">
<input data-i18n="[placeholder]actions.target_path" type="text" class="form-control" name="fs_rename_target" value="{{$val.Value}}" spellcheck="false" />
</div>
<div class="col-md-3 mt-3 mt-md-8">
<select name="fs_rename_options" data-i18n="[data-placeholder]general.options" class="form-select select-repetear" data-allow-clear="true" data-close-on-select="false" data-hide-search="true" multiple>
<option value=""></option>
<option value="1" data-i18n="actions.update_mod_times" {{if $val.UpdateModTime}}selected{{end}}>Change times</option>
</select>
</div>
<div class="col-md-1 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger ps-5 pe-4">
@ -677,12 +683,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- else}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-5 mt-3 mt-md-8">
<div class="col-md-4 mt-3 mt-md-8">
<input data-i18n="[placeholder]actions.source_path" type="text" class="form-control" name="fs_rename_source" value="" spellcheck="false" />
</div>
<div class="col-md-6 mt-3 mt-md-8">
<div class="col-md-4 mt-3 mt-md-8">
<input data-i18n="[placeholder]actions.target_path" type="text" class="form-control" name="fs_rename_target" value="" spellcheck="false" />
</div>
<div class="col-md-3 mt-3 mt-md-8">
<select name="fs_rename_options" data-i18n="[data-placeholder]general.options" class="form-select select-repetear" data-allow-clear="true" data-close-on-select="false" data-hide-search="true" multiple>
<option value=""></option>
<option value="1" data-i18n="actions.update_mod_times">Update timestamps</option>
</select>
</div>
<div class="col-md-1 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger ps-5 pe-4">