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 ( 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 v0.0.0-20240808171019-573a1156607a // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240805194559-2c9e96a0b5d4 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240808171019-573a1156607a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240805194559-2c9e96a0b5d4 // 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

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 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.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= 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.13 h1:7zWBXG9ERbMLrzQBRhFliAV+kjcRToDTgQT3CTwYyv4=
cloud.google.com/go/iam v1.1.12/go.mod h1:9LDX8J7dN5YRyzVHxwQzrQs9opFFqn0Mxs9nAeB+Hhg= 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.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
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/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-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-20240808171019-573a1156607a h1:3JVv3Ujh+kGiajpSqHWnbWPuu0nQqMZ3hASNDDF9974=
google.golang.org/genproto v0.0.0-20240805194559-2c9e96a0b5d4/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc= google.golang.org/genproto v0.0.0-20240808171019-573a1156607a/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-20240808171019-573a1156607a h1:KyUe15n7B1YCu+kMmPtlXxgkLQbp+Dw0tCRZf9Sd+CE=
google.golang.org/genproto/googleapis/api v0.0.0-20240805194559-2c9e96a0b5d4/go.mod h1:4+X6GvPs+25wZKbQq9qyAXrwIRExv7w0Ea6MgZLZiDM= 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-20240805194559-2c9e96a0b5d4 h1:OsSGQeIIsyOEOimVxLEIL4rwGcnrjOydQaiA2bOnZUM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240808171019-573a1156607a h1:EKiZZXueP9/T68B8Nl0GAx9cjbQnCId0yP3qPMgaaHs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240805194559-2c9e96a0b5d4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 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=

View file

@ -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)

View file

@ -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()

View file

@ -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", KeyValue: dataprovider.KeyValue{
Value: "/target", 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", KeyValue: dataprovider.KeyValue{
Value: "/p1", 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", KeyValue: dataprovider.KeyValue{
Value: "/p2", Key: "/p2",
Value: "/p2",
},
}, },
}, },
}, },

View file

@ -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}}", KeyValue: dataprovider.KeyValue{
Value: "/{{ObjectName}}_renamed", 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}}", KeyValue: dataprovider.KeyValue{
Value: fmt.Sprintf("/%s/{{VirtualPath}}", movePath), 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" for _, user := range []dataprovider.User{localUser, sftpUser} {
err = client.MkdirAll(testDir) conn, client, err := getSftpClient(user)
assert.NoError(t, err) if assert.NoError(t, err) {
err = writeSFTPFile(testFileName, 100, client) defer conn.Close()
assert.NoError(t, err) defer client.Close()
err = writeSFTPFile(path.Join(testDir, testFileName), 100, client)
assert.NoError(t, err) testDir := "sub dir"
err = client.Remove(testFileName) err = client.MkdirAll(testDir)
assert.NoError(t, err) assert.NoError(t, err)
err = client.Remove(path.Join(testDir, testFileName)) err = writeSFTPFile(testFileName, 100, client)
assert.NoError(t, err) assert.NoError(t, err)
// check files err = writeSFTPFile(path.Join(testDir, testFileName), 100, client)
_, err = client.Stat(testFileName) assert.NoError(t, err)
assert.ErrorIs(t, err, os.ErrNotExist) modTime := time.Now().Add(-36 * time.Hour)
_, err = client.Stat(path.Join(testDir, testFileName)) err = client.Chtimes(testFileName, modTime, modTime)
assert.ErrorIs(t, err, os.ErrNotExist) assert.NoError(t, err)
_, err = client.Stat(path.Join("/", movePath, testFileName)) err = client.Remove(testFileName)
assert.NoError(t, err) assert.NoError(t, err)
_, err = client.Stat(path.Join("/", movePath, testDir, testFileName)) err = client.Remove(path.Join(testDir, testFileName))
assert.NoError(t, err) assert.NoError(t, err)
// check quota // check files
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) _, err = client.Stat(testFileName)
assert.NoError(t, err) assert.ErrorIs(t, err, os.ErrNotExist)
assert.Equal(t, 0, user.UsedQuotaFiles) _, err = client.Stat(path.Join(testDir, testFileName))
assert.Equal(t, int64(0), user.UsedQuotaSize) assert.ErrorIs(t, err, os.ErrNotExist)
folder, _, err := httpdtest.GetFolderByName(movePath, http.StatusOK) info, err := client.Stat(path.Join("/", movePath, testFileName))
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 2, folder.UsedQuotaFiles) diff := math.Abs(time.Until(info.ModTime()).Seconds())
assert.Equal(t, int64(200), folder.UsedQuotaSize) assert.LessOrEqual(t, diff, float64(2))
// pre-delete action is not executed in movePath
err = client.Remove(path.Join("/", movePath, testFileName)) _, err = client.Stat(path.Join("/", movePath, testDir, testFileName))
assert.NoError(t, err) assert.NoError(t, err)
// check quota // check quota
folder, _, err = httpdtest.GetFolderByName(movePath, http.StatusOK) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 1, folder.UsedQuotaFiles) if user.Username == localUser.Username {
assert.Equal(t, int64(100), folder.UsedQuotaSize) 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", KeyValue: dataprovider.KeyValue{
Value: "/missing target", Key: "/missing source",
Value: "/missing target",
},
}, },
}, },
}, },

View file

@ -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

View file

@ -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 // files/dirs to rename
Renames []KeyValue `json:"renames,omitempty"` 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 { for idx, cfg := range c.Renames {
key := strings.TrimSpace(kv.Key) key := strings.TrimSpace(cfg.Key)
value := strings.TrimSpace(kv.Value) 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{ c.Renames[idx] = RenameConfig{
Key: key, KeyValue: KeyValue{
Value: value, 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 {

View file

@ -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)

View file

@ -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
} }

View file

@ -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)

View file

@ -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: "", KeyValue: dataprovider.KeyValue{
Value: "/adir", 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", KeyValue: dataprovider.KeyValue{
Value: "/adir", 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: "/", KeyValue: dataprovider.KeyValue{
Value: "/dir", 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", KeyValue: dataprovider.KeyValue{
Value: "/target", 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,

View file

@ -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"), ","),

View file

@ -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 {

View file

@ -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)

View file

@ -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
} }

View file

@ -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)

View file

@ -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 checks&CheckParentDir != 0 {
if err != nil { _, err := fs.Stat(path.Dir(target))
return -1, -1, err 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) if updateModTime {
for k, v := range getMetadata(srcInfo) { metadata := make(map[string]*string)
if v != "" { for k, v := range getMetadata(srcInfo) {
metadata[k] = to.Ptr(v) 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
} }

View file

@ -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 checks&CheckParentDir != 0 {
if err != nil { _, err := fs.Stat(path.Dir(target))
return -1, -1, err 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++

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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 checks&CheckParentDir != 0 {
if err != nil { _, err := fs.Stat(path.Dir(target))
return -1, -1, err 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 {

View file

@ -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("posix-rename@openssh.com"); ok { if _, ok := client.HasExtension("posix-rename@openssh.com"); 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
} }

View file

@ -52,8 +52,9 @@ const (
// Additional checks for files // Additional checks for files
const ( const (
CheckParentDir = 1 CheckParentDir = 1
CheckResume = 2 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), renameFn func(string, string, os.FileInfo, int, bool) (int, int64, error),
recursion int, 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)

View file

@ -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)

View file

@ -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
} }

View file

@ -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:

View file

@ -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",

View file

@ -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",

View file

@ -653,12 +653,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<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 (support@sftpgo.com).
{{- 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">