浏览代码

event action: add update modtime to fs rename

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 1 年之前
父节点
当前提交
81433e00d1

+ 8 - 8
go.mod

@@ -4,7 +4,7 @@ go 1.22.2
 
 
 require (
 require (
 	cloud.google.com/go/storage v1.43.0
 	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/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0
 	github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
 	github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
 	github.com/alexedwards/argon2id v1.0.0
 	github.com/alexedwards/argon2id v1.0.0
@@ -70,7 +70,7 @@ require (
 	golang.org/x/crypto v0.26.0
 	golang.org/x/crypto v0.26.0
 	golang.org/x/net v0.28.0
 	golang.org/x/net v0.28.0
 	golang.org/x/oauth2 v0.22.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/term v0.23.0
 	golang.org/x/time v0.6.0
 	golang.org/x/time v0.6.0
 	google.golang.org/api v0.191.0
 	google.golang.org/api v0.191.0
@@ -80,9 +80,9 @@ require (
 require (
 require (
 	cloud.google.com/go v0.115.0 // indirect
 	cloud.google.com/go v0.115.0 // indirect
 	cloud.google.com/go/auth v0.8.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/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
 	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
 	github.com/ajg/form v1.5.1 // 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/metric v1.28.0 // indirect
 	go.opentelemetry.io/otel/trace v1.28.0 // indirect
 	go.opentelemetry.io/otel/trace v1.28.0 // indirect
 	go.uber.org/multierr v1.11.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/mod v0.20.0 // indirect
 	golang.org/x/sync v0.8.0 // indirect
 	golang.org/x/sync v0.8.0 // indirect
 	golang.org/x/text v0.17.0 // indirect
 	golang.org/x/text v0.17.0 // indirect
 	golang.org/x/tools v0.24.0 // indirect
 	golang.org/x/tools v0.24.0 // indirect
 	golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // 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/grpc v1.65.0 // indirect
 	google.golang.org/protobuf v1.34.2 // indirect
 	google.golang.org/protobuf v1.34.2 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect

+ 16 - 16
go.sum

@@ -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 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 h1:y8jUJLl/Fg+qNBWxP/Hox2ezJvjkrPb952PC1p0G6A4=
 cloud.google.com/go/auth v0.8.0/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc=
 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 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
 cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
 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 h1:dYN3OCsQ6wJLLtOnI8DGUwQ5shMusXsWCCC+s09ATsk=
 cloud.google.com/go/kms v1.18.4/go.mod h1:SG1bgQ3UWW6/KdPo9uuJnzELXY5YTTMJtDYvajiQ22g=
 cloud.google.com/go/kms v1.18.4/go.mod h1:SG1bgQ3UWW6/KdPo9uuJnzELXY5YTTMJtDYvajiQ22g=
 cloud.google.com/go/longrunning v0.5.11 h1:Havn1kGjz3whCfoD8dxMLP73Ph5w+ODyZB9RUsDxtGk=
 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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 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 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 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/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
 github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
 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 h1:SpxfaOc/Fp4PeO8ui7wRcCZV0EgXZ+IWcVSLn6ZMSw0=
 gocloud.dev v0.38.0/go.mod h1:3XjKvd2E5iVNu/xFImRzjN0d/fkNHe4s0RiKidpEUMQ=
 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-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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 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.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.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.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/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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 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-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-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-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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=

+ 5 - 3
internal/common/connection.go

@@ -787,10 +787,12 @@ func (c *BaseConnection) Copy(virtualSourcePath, virtualTargetPath string) error
 
 
 // Rename renames (moves) virtualSourcePath to virtualTargetPath
 // Rename renames (moves) virtualSourcePath to virtualTargetPath
 func (c *BaseConnection) Rename(virtualSourcePath, virtualTargetPath string) error {
 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 {
 	if virtualSourcePath == virtualTargetPath {
 		return fmt.Errorf("the rename source and target cannot be the same: %w", c.GetOpUnsupportedError())
 		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)
 	defer close(done)
 	go keepConnectionAlive(c, done, 2*time.Minute)
 	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 {
 	if err != nil {
 		c.Log(logger.LevelError, "failed to rename %q -> %q: %+v", fsSourcePath, fsTargetPath, err)
 		c.Log(logger.LevelError, "failed to rename %q -> %q: %+v", fsSourcePath, fsTargetPath, err)
 		return c.GetFsError(fsSrc, err)
 		return c.GetFsError(fsSrc, err)

+ 7 - 3
internal/common/eventmanager.go

@@ -1753,7 +1753,7 @@ func executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer,
 	return nil
 	return nil
 }
 }
 
 
-func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *strings.Replacer,
+func executeRenameFsActionForUser(renames []dataprovider.RenameConfig, replacer *strings.Replacer,
 	user dataprovider.User,
 	user dataprovider.User,
 ) error {
 ) error {
 	user, err := getUserForEventAction(user)
 	user, err := getUserForEventAction(user)
@@ -1770,7 +1770,11 @@ func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *str
 	for _, item := range renames {
 	for _, item := range renames {
 		source := util.CleanPath(replaceWithReplacer(item.Key, replacer))
 		source := util.CleanPath(replaceWithReplacer(item.Key, replacer))
 		target := util.CleanPath(replaceWithReplacer(item.Value, 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)
 			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)
 		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
 	return nil
 }
 }
 
 
-func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *strings.Replacer,
+func executeRenameFsRuleAction(renames []dataprovider.RenameConfig, replacer *strings.Replacer,
 	conditions dataprovider.ConditionOptions, params *EventParams,
 	conditions dataprovider.ConditionOptions, params *EventParams,
 ) error {
 ) error {
 	users, err := params.getUsers()
 	users, err := params.getUsers()

+ 15 - 9
internal/common/eventmanager_test.go

@@ -1235,10 +1235,12 @@ func TestEventRuleActions(t *testing.T) {
 	action.Options = dataprovider.BaseEventActionOptions{
 	action.Options = dataprovider.BaseEventActionOptions{
 		FsConfig: dataprovider.EventActionFilesystemConfig{
 		FsConfig: dataprovider.EventActionFilesystemConfig{
 			Type: dataprovider.FilesystemActionRename,
 			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)
 	assert.NoError(t, err)
 	err = dataprovider.AddUser(&user, "", "", "")
 	err = dataprovider.AddUser(&user, "", "", "")
 	assert.NoError(t, err)
 	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)
 	}, testReplacer, user)
 	if assert.Error(t, err) {
 	if assert.Error(t, err) {
@@ -1792,10 +1796,12 @@ func TestFilesystemActionErrors(t *testing.T) {
 		Options: dataprovider.BaseEventActionOptions{
 		Options: dataprovider.BaseEventActionOptions{
 			FsConfig: dataprovider.EventActionFilesystemConfig{
 			FsConfig: dataprovider.EventActionFilesystemConfig{
 				Type: dataprovider.FilesystemActionRename,
 				Type: dataprovider.FilesystemActionRename,
-				Renames: []dataprovider.KeyValue{
+				Renames: []dataprovider.RenameConfig{
 					{
 					{
-						Key:   "/p2",
-						Value: "/p2",
+						KeyValue: dataprovider.KeyValue{
+							Key:   "/p2",
+							Value: "/p2",
+						},
 					},
 					},
 				},
 				},
 			},
 			},

+ 84 - 53
internal/common/protocol_test.go

@@ -4301,10 +4301,12 @@ func TestEventRuleFsActions(t *testing.T) {
 		Options: dataprovider.BaseEventActionOptions{
 		Options: dataprovider.BaseEventActionOptions{
 			FsConfig: dataprovider.EventActionFilesystemConfig{
 			FsConfig: dataprovider.EventActionFilesystemConfig{
 				Type: dataprovider.FilesystemActionRename,
 				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{
 		Options: dataprovider.BaseEventActionOptions{
 			FsConfig: dataprovider.EventActionFilesystemConfig{
 			FsConfig: dataprovider.EventActionFilesystemConfig{
 				Type: dataprovider.FilesystemActionRename,
 				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,
 			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)
 	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)
 	_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
 	_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	_, err = httpdtest.RemoveUser(user, http.StatusOK)
+	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
 	assert.NoError(t, err)
 	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)
 	assert.NoError(t, err)
 	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: movePath}, http.StatusOK)
 	_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: movePath}, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
@@ -4848,10 +4877,12 @@ func TestEventRulePreDownloadUpload(t *testing.T) {
 		Options: dataprovider.BaseEventActionOptions{
 		Options: dataprovider.BaseEventActionOptions{
 			FsConfig: dataprovider.EventActionFilesystemConfig{
 			FsConfig: dataprovider.EventActionFilesystemConfig{
 				Type: dataprovider.FilesystemActionRename,
 				Type: dataprovider.FilesystemActionRename,
-				Renames: []dataprovider.KeyValue{
+				Renames: []dataprovider.RenameConfig{
 					{
 					{
-						Key:   "/missing source",
-						Value: "/missing target",
+						KeyValue: dataprovider.KeyValue{
+							Key:   "/missing source",
+							Value: "/missing target",
+						},
 					},
 					},
 				},
 				},
 			},
 			},

+ 1 - 1
internal/common/transfer.go

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

+ 35 - 9
internal/dataprovider/eventrule.go

@@ -660,12 +660,21 @@ func (c *EventActionFsCompress) validate() error {
 	return nil
 	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
 // EventActionFilesystemConfig defines the configuration for filesystem actions
 type EventActionFilesystemConfig struct {
 type EventActionFilesystemConfig struct {
 	// Filesystem actions, see the above enum
 	// Filesystem actions, see the above enum
 	Type int `json:"type,omitempty"`
 	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
 	// directories to create
 	MkDirs []string `json:"mkdirs,omitempty"`
 	MkDirs []string `json:"mkdirs,omitempty"`
 	// files/dirs to delete
 	// files/dirs to delete
@@ -706,9 +715,9 @@ func (c *EventActionFilesystemConfig) validateRenames() error {
 	if len(c.Renames) == 0 {
 	if len(c.Renames) == 0 {
 		return util.NewI18nError(util.NewValidationError("no path to rename specified"), util.I18nErrorPathRequired)
 		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 == "" {
 		if key == "" || value == "" {
 			return util.NewValidationError("invalid paths to rename")
 			return util.NewValidationError("invalid paths to rename")
 		}
 		}
@@ -726,9 +735,12 @@ func (c *EventActionFilesystemConfig) validateRenames() error {
 				util.I18nErrorRootNotAllowed,
 				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
 	return nil
@@ -892,7 +904,7 @@ func (c *EventActionFilesystemConfig) getACopy() EventActionFilesystemConfig {
 
 
 	return EventActionFilesystemConfig{
 	return EventActionFilesystemConfig{
 		Type:    c.Type,
 		Type:    c.Type,
-		Renames: cloneKeyValues(c.Renames),
+		Renames: cloneRenameConfigs(c.Renames),
 		MkDirs:  mkdirs,
 		MkDirs:  mkdirs,
 		Deletes: deletes,
 		Deletes: deletes,
 		Exist:   exist,
 		Exist:   exist,
@@ -1833,6 +1845,20 @@ func (r *EventRule) RenderAsJSON(reload bool) ([]byte, error) {
 	return json.Marshal(r)
 	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 {
 func cloneKeyValues(keyVals []KeyValue) []KeyValue {
 	res := make([]KeyValue, 0, len(keyVals))
 	res := make([]KeyValue, 0, len(keyVals))
 	for _, kv := range keyVals {
 	for _, kv := range keyVals {

+ 1 - 1
internal/ftpd/handler.go

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

+ 1 - 1
internal/ftpd/internal_test.go

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

+ 1 - 1
internal/httpd/handler.go

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

+ 24 - 13
internal/httpd/httpd_test.go

@@ -2458,28 +2458,34 @@ func TestEventActionValidation(t *testing.T) {
 	_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
 	_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Contains(t, string(resp), "no path to rename specified")
 	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)
 	_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Contains(t, string(resp), "invalid paths to rename")
 	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)
 	_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Contains(t, string(resp), "rename source and target cannot be equal")
 	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)
 	_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
@@ -23974,16 +23980,19 @@ func TestWebEventAction(t *testing.T) {
 
 
 	action.Options.FsConfig = dataprovider.EventActionFilesystemConfig{
 	action.Options.FsConfig = dataprovider.EventActionFilesystemConfig{
 		Type: dataprovider.FilesystemActionRename,
 		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_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_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_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),
 	req, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),
 		bytes.NewBuffer([]byte(form.Encode())))
 		bytes.NewBuffer([]byte(form.Encode())))
 	assert.NoError(t, err)
 	assert.NoError(t, err)
@@ -23995,7 +24004,9 @@ func TestWebEventAction(t *testing.T) {
 	actionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)
 	actionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Equal(t, action.Type, actionGet.Type)
 	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{
 	action.Options.FsConfig = dataprovider.EventActionFilesystemConfig{
 		Type: dataprovider.FilesystemActionCopy,
 		Type: dataprovider.FilesystemActionCopy,

+ 25 - 1
internal/httpd/webadmin.go

@@ -2201,6 +2201,28 @@ func getKeyValsFromPostFields(r *http.Request, key, val string) []dataprovider.K
 	return res
 	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) {
 func getFoldersRetentionFromPostFields(r *http.Request) ([]dataprovider.FolderRetention, error) {
 	var res []dataprovider.FolderRetention
 	var res []dataprovider.FolderRetention
 	paths := r.Form["folder_retention_path"]
 	paths := r.Form["folder_retention_path"]
@@ -2310,6 +2332,8 @@ func updateRepeaterFormActionFields(r *http.Request) {
 			base, _ := strings.CutSuffix(k, "[fs_rename_source]")
 			base, _ := strings.CutSuffix(k, "[fs_rename_source]")
 			r.Form.Add("fs_rename_source", strings.TrimSpace(r.Form.Get(k)))
 			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.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
 			continue
 		}
 		}
 		if hasPrefixAndSuffix(k, "fs_copy[", "][fs_copy_source]") {
 		if hasPrefixAndSuffix(k, "fs_copy[", "][fs_copy_source]") {
@@ -2398,7 +2422,7 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
 		},
 		},
 		FsConfig: dataprovider.EventActionFilesystemConfig{
 		FsConfig: dataprovider.EventActionFilesystemConfig{
 			Type:    fsActionType,
 			Type:    fsActionType,
-			Renames: getKeyValsFromPostFields(r, "fs_rename_source", "fs_rename_target"),
+			Renames: getRenameConfigsFromPostFields(r),
 			Deletes: getSliceFromDelimitedValues(r.Form.Get("fs_delete_paths"), ","),
 			Deletes: getSliceFromDelimitedValues(r.Form.Get("fs_delete_paths"), ","),
 			MkDirs:  getSliceFromDelimitedValues(r.Form.Get("fs_mkdir_paths"), ","),
 			MkDirs:  getSliceFromDelimitedValues(r.Form.Get("fs_mkdir_paths"), ","),
 			Exist:   getSliceFromDelimitedValues(r.Form.Get("fs_exist_paths"), ","),
 			Exist:   getSliceFromDelimitedValues(r.Form.Get("fs_exist_paths"), ","),

+ 22 - 3
internal/httpdtest/httpdtest.go

@@ -2601,9 +2601,28 @@ func compareUserFilePatternsFilters(expected sdk.BaseUserFilters, actual sdk.Bas
 	return nil
 	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 {
 func compareKeyValues(expected, actual []dataprovider.KeyValue) error {
 	if len(expected) != len(actual) {
 	if len(expected) != len(actual) {
-		return errors.New("kay values mismatch")
+		return errors.New("key values mismatch")
 	}
 	}
 	for _, ex := range expected {
 	for _, ex := range expected {
 		found := false
 		found := false
@@ -2614,7 +2633,7 @@ func compareKeyValues(expected, actual []dataprovider.KeyValue) error {
 			}
 			}
 		}
 		}
 		if !found {
 		if !found {
-			return errors.New("kay values mismatch")
+			return errors.New("key values mismatch")
 		}
 		}
 	}
 	}
 	return nil
 	return nil
@@ -2731,7 +2750,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
 	if expected.Type != actual.Type {
 	if expected.Type != actual.Type {
 		return errors.New("fs type mismatch")
 		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")
 		return errors.New("fs renames mismatch")
 	}
 	}
 	if err := compareKeyValues(expected.Copy, actual.Copy); err != nil {
 	if err := compareKeyValues(expected.Copy, actual.Copy); err != nil {

+ 1 - 1
internal/sftpd/handler.go

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

+ 1 - 1
internal/sftpd/internal_test.go

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

+ 1 - 1
internal/sftpd/scp.go

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

+ 41 - 20
internal/vfs/azblobfs.go

@@ -306,19 +306,21 @@ func (fs *AzureBlobFs) Create(name string, flag, checks int) (File, PipeWriter,
 }
 }
 
 
 // Rename renames (moves) source to target.
 // 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 {
 	if source == target {
 		return -1, -1, nil
 		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)
 	fi, err := fs.Stat(source)
 	if err != nil {
 	if err != nil {
 		return -1, -1, err
 		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.
 // 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 {
 	if metadata == nil {
 		metadata = make(map[string]*string)
 		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))
 	ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
 	defer cancelFn()
 	defer cancelFn()
@@ -667,7 +679,7 @@ func (fs *AzureBlobFs) CopyFile(source, target string, srcInfo os.FileInfo) (int
 			return 0, 0, err
 			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 0, 0, err
 	}
 	}
 	return numFiles, sizeDiff, nil
 	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))
 	ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxLongTimeout))
 	defer cancelFn()
 	defer cancelFn()
 
 
 	srcBlob := fs.containerClient.NewBlockBlobClient(source)
 	srcBlob := fs.containerClient.NewBlockBlobClient(source)
 	dstBlob := fs.containerClient.NewBlockBlobClient(target)
 	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 {
 	if err != nil {
 		metric.AZCopyObjectCompleted(err)
 		metric.AZCopyObjectCompleted(err)
 		return err
 		return err
@@ -789,7 +801,9 @@ func (fs *AzureBlobFs) copyFileInternal(source, target string, srcInfo os.FileIn
 	return nil
 	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 numFiles int
 	var filesSize int64
 	var filesSize int64
 
 
@@ -807,7 +821,7 @@ func (fs *AzureBlobFs) renameInternal(source, target string, srcInfo os.FileInfo
 			return numFiles, filesSize, err
 			return numFiles, filesSize, err
 		}
 		}
 		if renameMode == 1 {
 		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
 			numFiles += files
 			filesSize += size
 			filesSize += size
 			if err != nil {
 			if err != nil {
@@ -815,7 +829,7 @@ func (fs *AzureBlobFs) renameInternal(source, target string, srcInfo os.FileInfo
 			}
 			}
 		}
 		}
 	} else {
 	} else {
-		if err := fs.copyFileInternal(source, target, srcInfo); err != nil {
+		if err := fs.copyFileInternal(source, target, srcInfo, updateModTime); err != nil {
 			return numFiles, filesSize, err
 			return numFiles, filesSize, err
 		}
 		}
 		numFiles++
 		numFiles++
@@ -1102,20 +1116,27 @@ func (*AzureBlobFs) readFill(r io.Reader, buf []byte) (n int, err error) {
 	return n, err
 	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{}
 	copyOptions := &blob.StartCopyFromURLOptions{}
 	if fs.config.AccessTier != "" {
 	if fs.config.AccessTier != "" {
 		copyOptions.Tier = (*blob.AccessTier)(&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
 	return copyOptions
 }
 }
 
 

+ 19 - 10
internal/vfs/gcsfs.go

@@ -255,19 +255,21 @@ func (fs *GCSFs) Create(name string, flag, checks int) (File, PipeWriter, func()
 }
 }
 
 
 // Rename renames (moves) source to target.
 // 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 {
 	if source == target {
 		return -1, -1, nil
 		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)
 	fi, err := fs.getObjectStat(source)
 	if err != nil {
 	if err != nil {
 		return -1, -1, err
 		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.
 // 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}
 		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 0, 0, err
 	}
 	}
 	return numFiles, sizeDiff, nil
 	return numFiles, sizeDiff, nil
@@ -753,7 +755,9 @@ func (fs *GCSFs) composeObjects(ctx context.Context, dst, partialObject *storage
 	return err
 	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)
 	src := fs.svc.Bucket(fs.config.Bucket).Object(source)
 	dst := fs.svc.Bucket(fs.config.Bucket).Object(target)
 	dst := fs.svc.Bucket(fs.config.Bucket).Object(target)
 	if conditions != nil {
 	if conditions != nil {
@@ -785,6 +789,9 @@ func (fs *GCSFs) copyFileInternal(source, target string, conditions *storage.Con
 		copier.ContentType = contentType
 		copier.ContentType = contentType
 	}
 	}
 	metadata := getMetadata(srcInfo)
 	metadata := getMetadata(srcInfo)
+	if updateModTime && len(metadata) > 0 {
+		delete(metadata, lastModifiedField)
+	}
 	if len(metadata) > 0 {
 	if len(metadata) > 0 {
 		copier.Metadata = metadata
 		copier.Metadata = metadata
 	}
 	}
@@ -793,7 +800,9 @@ func (fs *GCSFs) copyFileInternal(source, target string, conditions *storage.Con
 	return err
 	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 numFiles int
 	var filesSize int64
 	var filesSize int64
 
 
@@ -811,7 +820,7 @@ func (fs *GCSFs) renameInternal(source, target string, srcInfo os.FileInfo, recu
 			return numFiles, filesSize, err
 			return numFiles, filesSize, err
 		}
 		}
 		if renameMode == 1 {
 		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
 			numFiles += files
 			filesSize += size
 			filesSize += size
 			if err != nil {
 			if err != nil {
@@ -819,7 +828,7 @@ func (fs *GCSFs) renameInternal(source, target string, srcInfo os.FileInfo, recu
 			}
 			}
 		}
 		}
 	} else {
 	} 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
 			return numFiles, filesSize, err
 		}
 		}
 		numFiles++
 		numFiles++

+ 4 - 1
internal/vfs/httpfs.go

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

+ 7 - 1
internal/vfs/osfs.go

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

+ 20 - 10
internal/vfs/s3fs.go

@@ -334,19 +334,21 @@ func (fs *S3Fs) Create(name string, flag, checks int) (File, PipeWriter, func(),
 }
 }
 
 
 // Rename renames (moves) source to target.
 // 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 {
 	if source == target {
 		return -1, -1, nil
 		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)
 	fi, err := fs.Stat(source)
 	if err != nil {
 	if err != nil {
 		return -1, -1, err
 		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.
 // 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))
 	ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
 	defer cancelFn()
 	defer cancelFn()
 
 
-	_, err := fs.svc.CopyObject(ctx, &s3.CopyObjectInput{
+	copyObject := &s3.CopyObjectInput{
 		Bucket:       aws.String(fs.config.Bucket),
 		Bucket:       aws.String(fs.config.Bucket),
 		CopySource:   aws.String(copySource),
 		CopySource:   aws.String(copySource),
 		Key:          aws.String(target),
 		Key:          aws.String(target),
 		StorageClass: types.StorageClass(fs.config.StorageClass),
 		StorageClass: types.StorageClass(fs.config.StorageClass),
 		ACL:          types.ObjectCannedACL(fs.config.ACL),
 		ACL:          types.ObjectCannedACL(fs.config.ACL),
 		ContentType:  util.NilIfEmpty(contentType),
 		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)
 	metric.S3CopyObjectCompleted(err)
 	return 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 numFiles int
 	var filesSize int64
 	var filesSize int64
 
 
@@ -732,7 +742,7 @@ func (fs *S3Fs) renameInternal(source, target string, srcInfo os.FileInfo, recur
 			return numFiles, filesSize, err
 			return numFiles, filesSize, err
 		}
 		}
 		if renameMode == 1 {
 		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
 			numFiles += files
 			filesSize += size
 			filesSize += size
 			if err != nil {
 			if err != nil {

+ 7 - 1
internal/vfs/sftpfs.go

@@ -479,7 +479,7 @@ func (fs *SFTPFs) Create(name string, flag, _ int) (File, PipeWriter, func(), er
 }
 }
 
 
 // Rename renames (moves) source to target.
 // 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 {
 	if source == target {
 		return -1, -1, nil
 		return -1, -1, nil
 	}
 	}
@@ -489,9 +489,15 @@ func (fs *SFTPFs) Rename(source, target string) (int, int64, error) {
 	}
 	}
 	if _, ok := client.HasExtension("[email protected]"); ok {
 	if _, ok := client.HasExtension("[email protected]"); ok {
 		err := client.PosixRename(source, target)
 		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
 		return -1, -1, err
 	}
 	}
 	err = client.Rename(source, target)
 	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
 	return -1, -1, err
 }
 }
 
 

+ 8 - 7
internal/vfs/vfs.go

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

+ 1 - 1
internal/webdavd/handler.go

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

+ 1 - 1
internal/webdavd/internal_test.go

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

+ 9 - 1
openapi/openapi.yaml

@@ -6973,6 +6973,14 @@ components:
           type: string
           type: string
         value:
         value:
           type: string
           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:
     HTTPPart:
       type: object
       type: object
       properties:
       properties:
@@ -7105,7 +7113,7 @@ components:
         renames:
         renames:
           type: array
           type: array
           items:
           items:
-            $ref: '#/components/schemas/KeyValue'
+            $ref: '#/components/schemas/RenameConfig'
         mkdirs:
         mkdirs:
           type: array
           type: array
           items:
           items:

+ 1 - 0
static/locales/en/translation.json

@@ -1007,6 +1007,7 @@
         "archive_path": "Archive path",
         "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",
         "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",
         "placeholders_modal_title": "Supported placeholders",
+        "update_mod_times": "Update timestamp",
         "types": {
         "types": {
             "http": "HTTP",
             "http": "HTTP",
             "email": "Email",
             "email": "Email",

+ 1 - 0
static/locales/it/translation.json

@@ -1007,6 +1007,7 @@
         "archive_path": "Percorso dell'archivio",
         "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",
         "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",
         "placeholders_modal_title": "Segnaposto supportati",
+        "update_mod_times": "Aggiorna timestamp",
         "types": {
         "types": {
             "http": "HTTP",
             "http": "HTTP",
             "email": "Email",
             "email": "Email",

+ 16 - 4
templates/webadmin/eventaction.html

@@ -653,12 +653,18 @@ explicit grant from the SFTPGo Team ([email protected]).
                                 <div data-repeater-item>
                                 <div data-repeater-item>
                                     <div data-repeater-item>
                                     <div data-repeater-item>
                                         <div class="form-group row">
                                         <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" />
                                                 <input data-i18n="[placeholder]actions.source_path" type="text" class="form-control" name="fs_rename_source" value="{{$val.Key}}" spellcheck="false" />
                                             </div>
                                             </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" />
                                                 <input data-i18n="[placeholder]actions.target_path" type="text" class="form-control" name="fs_rename_target" value="{{$val.Value}}" spellcheck="false" />
                                             </div>
                                             </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">
                                             <div class="col-md-1 mt-3 mt-md-8">
                                                 <a href="#" data-repeater-delete
                                                 <a href="#" data-repeater-delete
                                                     class="btn btn-light-danger ps-5 pe-4">
                                                     class="btn btn-light-danger ps-5 pe-4">
@@ -677,12 +683,18 @@ explicit grant from the SFTPGo Team ([email protected]).
                                 {{- else}}
                                 {{- else}}
                                 <div data-repeater-item>
                                 <div data-repeater-item>
                                     <div class="form-group row">
                                     <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" />
                                             <input data-i18n="[placeholder]actions.source_path" type="text" class="form-control" name="fs_rename_source" value="" spellcheck="false" />
                                         </div>
                                         </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" />
                                             <input data-i18n="[placeholder]actions.target_path" type="text" class="form-control" name="fs_rename_target" value="" spellcheck="false" />
                                         </div>
                                         </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">
                                         <div class="col-md-1 mt-3 mt-md-8">
                                             <a href="#" data-repeater-delete
                                             <a href="#" data-repeater-delete
                                                 class="btn btn-light-danger ps-5 pe-4">
                                                 class="btn btn-light-danger ps-5 pe-4">