mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 00:50:31 +00:00
event action: add update modtime to fs rename
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
a5c5e85144
commit
81433e00d1
30 changed files with 383 additions and 182 deletions
16
go.mod
16
go.mod
|
@ -4,7 +4,7 @@ go 1.22.2
|
|||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.43.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
|
@ -70,7 +70,7 @@ require (
|
|||
golang.org/x/crypto v0.26.0
|
||||
golang.org/x/net v0.28.0
|
||||
golang.org/x/oauth2 v0.22.0
|
||||
golang.org/x/sys v0.23.0
|
||||
golang.org/x/sys v0.24.0
|
||||
golang.org/x/term v0.23.0
|
||||
golang.org/x/time v0.6.0
|
||||
google.golang.org/api v0.191.0
|
||||
|
@ -80,9 +80,9 @@ require (
|
|||
require (
|
||||
cloud.google.com/go v0.115.0 // indirect
|
||||
cloud.google.com/go/auth v0.8.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.12 // indirect
|
||||
cloud.google.com/go/iam v1.1.13 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
|
@ -167,15 +167,15 @@ require (
|
|||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240805194559-2c9e96a0b5d4 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240805194559-2c9e96a0b5d4 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240805194559-2c9e96a0b5d4 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240808171019-573a1156607a // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240808171019-573a1156607a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240808171019-573a1156607a // indirect
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
|
32
go.sum
32
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/auth v0.8.0 h1:y8jUJLl/Fg+qNBWxP/Hox2ezJvjkrPb952PC1p0G6A4=
|
||||
cloud.google.com/go/auth v0.8.0/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
|
||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||
cloud.google.com/go/iam v1.1.12 h1:JixGLimRrNGcxvJEQ8+clfLxPlbeZA6MuRJ+qJNQ5Xw=
|
||||
cloud.google.com/go/iam v1.1.12/go.mod h1:9LDX8J7dN5YRyzVHxwQzrQs9opFFqn0Mxs9nAeB+Hhg=
|
||||
cloud.google.com/go/iam v1.1.13 h1:7zWBXG9ERbMLrzQBRhFliAV+kjcRToDTgQT3CTwYyv4=
|
||||
cloud.google.com/go/iam v1.1.13/go.mod h1:K8mY0uSXwEXS30KrnVb+j54LB/ntfZu1dr+4zFMNbus=
|
||||
cloud.google.com/go/kms v1.18.4 h1:dYN3OCsQ6wJLLtOnI8DGUwQ5shMusXsWCCC+s09ATsk=
|
||||
cloud.google.com/go/kms v1.18.4/go.mod h1:SG1bgQ3UWW6/KdPo9uuJnzELXY5YTTMJtDYvajiQ22g=
|
||||
cloud.google.com/go/longrunning v0.5.11 h1:Havn1kGjz3whCfoD8dxMLP73Ph5w+ODyZB9RUsDxtGk=
|
||||
|
@ -18,8 +18,8 @@ cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502Jw
|
|||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 h1:GJHeeA2N7xrG3q30L2UXDyuWRzDM900/65j70wcM4Ww=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
|
@ -419,8 +419,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
|
|||
gocloud.dev v0.38.0 h1:SpxfaOc/Fp4PeO8ui7wRcCZV0EgXZ+IWcVSLn6ZMSw0=
|
||||
gocloud.dev v0.38.0/go.mod h1:3XjKvd2E5iVNu/xFImRzjN0d/fkNHe4s0RiKidpEUMQ=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
|
@ -479,8 +479,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
|
@ -525,12 +525,12 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
|||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240805194559-2c9e96a0b5d4 h1:g+rQ3aqOyXK/0qwnC5TGUXnyIeipstP5SsniB9uPJ2c=
|
||||
google.golang.org/genproto v0.0.0-20240805194559-2c9e96a0b5d4/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240805194559-2c9e96a0b5d4 h1:ABEBT/sZ7We8zd7A5f3KO6zMQe+s3901H7l8Whhijt0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240805194559-2c9e96a0b5d4/go.mod h1:4+X6GvPs+25wZKbQq9qyAXrwIRExv7w0Ea6MgZLZiDM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240805194559-2c9e96a0b5d4 h1:OsSGQeIIsyOEOimVxLEIL4rwGcnrjOydQaiA2bOnZUM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240805194559-2c9e96a0b5d4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/genproto v0.0.0-20240808171019-573a1156607a h1:3JVv3Ujh+kGiajpSqHWnbWPuu0nQqMZ3hASNDDF9974=
|
||||
google.golang.org/genproto v0.0.0-20240808171019-573a1156607a/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240808171019-573a1156607a h1:KyUe15n7B1YCu+kMmPtlXxgkLQbp+Dw0tCRZf9Sd+CE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240808171019-573a1156607a/go.mod h1:4+X6GvPs+25wZKbQq9qyAXrwIRExv7w0Ea6MgZLZiDM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240808171019-573a1156607a h1:EKiZZXueP9/T68B8Nl0GAx9cjbQnCId0yP3qPMgaaHs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240808171019-573a1156607a/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
|
|
|
@ -787,10 +787,12 @@ func (c *BaseConnection) Copy(virtualSourcePath, virtualTargetPath string) error
|
|||
|
||||
// Rename renames (moves) virtualSourcePath to virtualTargetPath
|
||||
func (c *BaseConnection) Rename(virtualSourcePath, virtualTargetPath string) error {
|
||||
return c.renameInternal(virtualSourcePath, virtualTargetPath, false)
|
||||
return c.renameInternal(virtualSourcePath, virtualTargetPath, false, vfs.CheckParentDir)
|
||||
}
|
||||
|
||||
func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath string, checkParentDestination bool) error {
|
||||
func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath string,
|
||||
checkParentDestination bool, checks int,
|
||||
) error {
|
||||
if virtualSourcePath == virtualTargetPath {
|
||||
return fmt.Errorf("the rename source and target cannot be the same: %w", c.GetOpUnsupportedError())
|
||||
}
|
||||
|
@ -844,7 +846,7 @@ func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath str
|
|||
defer close(done)
|
||||
go keepConnectionAlive(c, done, 2*time.Minute)
|
||||
|
||||
files, size, err := fsDst.Rename(fsSourcePath, fsTargetPath)
|
||||
files, size, err := fsDst.Rename(fsSourcePath, fsTargetPath, checks)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelError, "failed to rename %q -> %q: %+v", fsSourcePath, fsTargetPath, err)
|
||||
return c.GetFsError(fsSrc, err)
|
||||
|
|
|
@ -1753,7 +1753,7 @@ func executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer,
|
|||
return nil
|
||||
}
|
||||
|
||||
func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *strings.Replacer,
|
||||
func executeRenameFsActionForUser(renames []dataprovider.RenameConfig, replacer *strings.Replacer,
|
||||
user dataprovider.User,
|
||||
) error {
|
||||
user, err := getUserForEventAction(user)
|
||||
|
@ -1770,7 +1770,11 @@ func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *str
|
|||
for _, item := range renames {
|
||||
source := util.CleanPath(replaceWithReplacer(item.Key, replacer))
|
||||
target := util.CleanPath(replaceWithReplacer(item.Value, replacer))
|
||||
if err = conn.renameInternal(source, target, true); err != nil {
|
||||
checks := 0
|
||||
if item.UpdateModTime {
|
||||
checks += vfs.CheckUpdateModTime
|
||||
}
|
||||
if err = conn.renameInternal(source, target, true, checks); err != nil {
|
||||
return fmt.Errorf("unable to rename %q->%q, user %q: %w", source, target, user.Username, err)
|
||||
}
|
||||
eventManagerLog(logger.LevelDebug, "rename %q->%q ok, user %q", source, target, user.Username)
|
||||
|
@ -1832,7 +1836,7 @@ func executeExistFsActionForUser(exist []string, replacer *strings.Replacer,
|
|||
return nil
|
||||
}
|
||||
|
||||
func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *strings.Replacer,
|
||||
func executeRenameFsRuleAction(renames []dataprovider.RenameConfig, replacer *strings.Replacer,
|
||||
conditions dataprovider.ConditionOptions, params *EventParams,
|
||||
) error {
|
||||
users, err := params.getUsers()
|
||||
|
|
|
@ -1235,10 +1235,12 @@ func TestEventRuleActions(t *testing.T) {
|
|||
action.Options = dataprovider.BaseEventActionOptions{
|
||||
FsConfig: dataprovider.EventActionFilesystemConfig{
|
||||
Type: dataprovider.FilesystemActionRename,
|
||||
Renames: []dataprovider.KeyValue{
|
||||
Renames: []dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "/source",
|
||||
Value: "/target",
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "/source",
|
||||
Value: "/target",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1778,10 +1780,12 @@ func TestFilesystemActionErrors(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = dataprovider.AddUser(&user, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
err = executeRenameFsActionForUser([]dataprovider.KeyValue{
|
||||
err = executeRenameFsActionForUser([]dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "/p1",
|
||||
Value: "/p1",
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "/p1",
|
||||
Value: "/p1",
|
||||
},
|
||||
},
|
||||
}, testReplacer, user)
|
||||
if assert.Error(t, err) {
|
||||
|
@ -1792,10 +1796,12 @@ func TestFilesystemActionErrors(t *testing.T) {
|
|||
Options: dataprovider.BaseEventActionOptions{
|
||||
FsConfig: dataprovider.EventActionFilesystemConfig{
|
||||
Type: dataprovider.FilesystemActionRename,
|
||||
Renames: []dataprovider.KeyValue{
|
||||
Renames: []dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "/p2",
|
||||
Value: "/p2",
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "/p2",
|
||||
Value: "/p2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4301,10 +4301,12 @@ func TestEventRuleFsActions(t *testing.T) {
|
|||
Options: dataprovider.BaseEventActionOptions{
|
||||
FsConfig: dataprovider.EventActionFilesystemConfig{
|
||||
Type: dataprovider.FilesystemActionRename,
|
||||
Renames: []dataprovider.KeyValue{
|
||||
Renames: []dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "/{{VirtualDirPath}}/{{ObjectName}}",
|
||||
Value: "/{{ObjectName}}_renamed",
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "/{{VirtualDirPath}}/{{ObjectName}}",
|
||||
Value: "/{{ObjectName}}_renamed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -4711,10 +4713,13 @@ func TestEventRulePreDelete(t *testing.T) {
|
|||
Options: dataprovider.BaseEventActionOptions{
|
||||
FsConfig: dataprovider.EventActionFilesystemConfig{
|
||||
Type: dataprovider.FilesystemActionRename,
|
||||
Renames: []dataprovider.KeyValue{
|
||||
Renames: []dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "/{{VirtualPath}}",
|
||||
Value: fmt.Sprintf("/%s/{{VirtualPath}}", movePath),
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "/{{VirtualPath}}",
|
||||
Value: fmt.Sprintf("/%s/{{VirtualPath}}", movePath),
|
||||
},
|
||||
UpdateModTime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -4768,59 +4773,83 @@ func TestEventRulePreDelete(t *testing.T) {
|
|||
QuotaFiles: 1000,
|
||||
},
|
||||
}
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u = getTestSFTPUser()
|
||||
u.QuotaFiles = 1000
|
||||
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
conn, client, err := getSftpClient(user)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
testDir := "sub dir"
|
||||
err = client.MkdirAll(testDir)
|
||||
assert.NoError(t, err)
|
||||
err = writeSFTPFile(testFileName, 100, client)
|
||||
assert.NoError(t, err)
|
||||
err = writeSFTPFile(path.Join(testDir, testFileName), 100, client)
|
||||
assert.NoError(t, err)
|
||||
err = client.Remove(testFileName)
|
||||
assert.NoError(t, err)
|
||||
err = client.Remove(path.Join(testDir, testFileName))
|
||||
assert.NoError(t, err)
|
||||
// check files
|
||||
_, err = client.Stat(testFileName)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
_, err = client.Stat(path.Join(testDir, testFileName))
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
_, err = client.Stat(path.Join("/", movePath, testFileName))
|
||||
assert.NoError(t, err)
|
||||
_, err = client.Stat(path.Join("/", movePath, testDir, testFileName))
|
||||
assert.NoError(t, err)
|
||||
// check quota
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, user.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), user.UsedQuotaSize)
|
||||
folder, _, err := httpdtest.GetFolderByName(movePath, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(200), folder.UsedQuotaSize)
|
||||
// pre-delete action is not executed in movePath
|
||||
err = client.Remove(path.Join("/", movePath, testFileName))
|
||||
assert.NoError(t, err)
|
||||
// check quota
|
||||
folder, _, err = httpdtest.GetFolderByName(movePath, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(100), folder.UsedQuotaSize)
|
||||
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||
conn, client, err := getSftpClient(user)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
testDir := "sub dir"
|
||||
err = client.MkdirAll(testDir)
|
||||
assert.NoError(t, err)
|
||||
err = writeSFTPFile(testFileName, 100, client)
|
||||
assert.NoError(t, err)
|
||||
err = writeSFTPFile(path.Join(testDir, testFileName), 100, client)
|
||||
assert.NoError(t, err)
|
||||
modTime := time.Now().Add(-36 * time.Hour)
|
||||
err = client.Chtimes(testFileName, modTime, modTime)
|
||||
assert.NoError(t, err)
|
||||
err = client.Remove(testFileName)
|
||||
assert.NoError(t, err)
|
||||
err = client.Remove(path.Join(testDir, testFileName))
|
||||
assert.NoError(t, err)
|
||||
// check files
|
||||
_, err = client.Stat(testFileName)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
_, err = client.Stat(path.Join(testDir, testFileName))
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
info, err := client.Stat(path.Join("/", movePath, testFileName))
|
||||
assert.NoError(t, err)
|
||||
diff := math.Abs(time.Until(info.ModTime()).Seconds())
|
||||
assert.LessOrEqual(t, diff, float64(2))
|
||||
|
||||
_, err = client.Stat(path.Join("/", movePath, testDir, testFileName))
|
||||
assert.NoError(t, err)
|
||||
// check quota
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
if user.Username == localUser.Username {
|
||||
assert.Equal(t, 0, user.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), user.UsedQuotaSize)
|
||||
folder, _, err := httpdtest.GetFolderByName(movePath, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(200), folder.UsedQuotaSize)
|
||||
} else {
|
||||
assert.Equal(t, 1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(100), user.UsedQuotaSize)
|
||||
}
|
||||
// pre-delete action is not executed in movePath
|
||||
err = client.Remove(path.Join("/", movePath, testFileName))
|
||||
assert.NoError(t, err)
|
||||
if user.Username == localUser.Username {
|
||||
// check quota
|
||||
folder, _, err := httpdtest.GetFolderByName(movePath, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(100), folder.UsedQuotaSize)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: movePath}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -4848,10 +4877,12 @@ func TestEventRulePreDownloadUpload(t *testing.T) {
|
|||
Options: dataprovider.BaseEventActionOptions{
|
||||
FsConfig: dataprovider.EventActionFilesystemConfig{
|
||||
Type: dataprovider.FilesystemActionRename,
|
||||
Renames: []dataprovider.KeyValue{
|
||||
Renames: []dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "/missing source",
|
||||
Value: "/missing target",
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "/missing source",
|
||||
Value: "/missing target",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -394,7 +394,7 @@ func (t *BaseTransfer) Close() error {
|
|||
t.effectiveFsPath, err)
|
||||
} else if t.isAtomicUpload() {
|
||||
if t.ErrTransfer == nil || Config.UploadMode&UploadModeAtomicWithResume != 0 {
|
||||
_, _, err = t.Fs.Rename(t.effectiveFsPath, t.fsPath)
|
||||
_, _, err = t.Fs.Rename(t.effectiveFsPath, t.fsPath, 0)
|
||||
t.Connection.Log(logger.LevelDebug, "atomic upload completed, rename: %q -> %q, error: %v",
|
||||
t.effectiveFsPath, t.fsPath, err)
|
||||
// the file must be removed if it is uploaded to a path outside the home dir and cannot be renamed
|
||||
|
|
|
@ -660,12 +660,21 @@ func (c *EventActionFsCompress) validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// RenameConfig defines the configuration for a filesystem rename
|
||||
type RenameConfig struct {
|
||||
// key is the source and target the value
|
||||
KeyValue
|
||||
// This setting only applies to storage providers that support
|
||||
// changing modification times.
|
||||
UpdateModTime bool `json:"update_modtime,omitempty"`
|
||||
}
|
||||
|
||||
// EventActionFilesystemConfig defines the configuration for filesystem actions
|
||||
type EventActionFilesystemConfig struct {
|
||||
// Filesystem actions, see the above enum
|
||||
Type int `json:"type,omitempty"`
|
||||
// files/dirs to rename, key is the source and target the value
|
||||
Renames []KeyValue `json:"renames,omitempty"`
|
||||
// files/dirs to rename
|
||||
Renames []RenameConfig `json:"renames,omitempty"`
|
||||
// directories to create
|
||||
MkDirs []string `json:"mkdirs,omitempty"`
|
||||
// files/dirs to delete
|
||||
|
@ -706,9 +715,9 @@ func (c *EventActionFilesystemConfig) validateRenames() error {
|
|||
if len(c.Renames) == 0 {
|
||||
return util.NewI18nError(util.NewValidationError("no path to rename specified"), util.I18nErrorPathRequired)
|
||||
}
|
||||
for idx, kv := range c.Renames {
|
||||
key := strings.TrimSpace(kv.Key)
|
||||
value := strings.TrimSpace(kv.Value)
|
||||
for idx, cfg := range c.Renames {
|
||||
key := strings.TrimSpace(cfg.Key)
|
||||
value := strings.TrimSpace(cfg.Value)
|
||||
if key == "" || value == "" {
|
||||
return util.NewValidationError("invalid paths to rename")
|
||||
}
|
||||
|
@ -726,9 +735,12 @@ func (c *EventActionFilesystemConfig) validateRenames() error {
|
|||
util.I18nErrorRootNotAllowed,
|
||||
)
|
||||
}
|
||||
c.Renames[idx] = KeyValue{
|
||||
Key: key,
|
||||
Value: value,
|
||||
c.Renames[idx] = RenameConfig{
|
||||
KeyValue: KeyValue{
|
||||
Key: key,
|
||||
Value: value,
|
||||
},
|
||||
UpdateModTime: cfg.UpdateModTime,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -892,7 +904,7 @@ func (c *EventActionFilesystemConfig) getACopy() EventActionFilesystemConfig {
|
|||
|
||||
return EventActionFilesystemConfig{
|
||||
Type: c.Type,
|
||||
Renames: cloneKeyValues(c.Renames),
|
||||
Renames: cloneRenameConfigs(c.Renames),
|
||||
MkDirs: mkdirs,
|
||||
Deletes: deletes,
|
||||
Exist: exist,
|
||||
|
@ -1833,6 +1845,20 @@ func (r *EventRule) RenderAsJSON(reload bool) ([]byte, error) {
|
|||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
func cloneRenameConfigs(renames []RenameConfig) []RenameConfig {
|
||||
res := make([]RenameConfig, 0, len(renames))
|
||||
for _, c := range renames {
|
||||
res = append(res, RenameConfig{
|
||||
KeyValue: KeyValue{
|
||||
Key: c.Key,
|
||||
Value: c.Value,
|
||||
},
|
||||
UpdateModTime: c.UpdateModTime,
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func cloneKeyValues(keyVals []KeyValue) []KeyValue {
|
||||
res := make([]KeyValue, 0, len(keyVals))
|
||||
for _, kv := range keyVals {
|
||||
|
|
|
@ -465,7 +465,7 @@ func (c *Connection) handleFTPUploadToExistingFile(fs vfs.Fs, flags int, resolve
|
|||
}
|
||||
|
||||
if common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {
|
||||
_, _, err = fs.Rename(resolvedPath, filePath)
|
||||
_, _, err = fs.Rename(resolvedPath, filePath, 0)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelError, "error renaming existing file for atomic upload, source: %q, dest: %q, err: %+v",
|
||||
resolvedPath, filePath, err)
|
||||
|
|
|
@ -404,7 +404,7 @@ func (fs MockOsFs) Remove(name string, _ bool) error {
|
|||
}
|
||||
|
||||
// Rename renames (moves) source to target
|
||||
func (fs MockOsFs) Rename(source, target string) (int, int64, error) {
|
||||
func (fs MockOsFs) Rename(source, target string, _ int) (int, int64, error) {
|
||||
if fs.err != nil {
|
||||
return -1, -1, fs.err
|
||||
}
|
||||
|
|
|
@ -176,7 +176,7 @@ func (c *Connection) getFileWriter(name string) (io.WriteCloser, error) {
|
|||
}
|
||||
|
||||
if common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {
|
||||
_, _, err = fs.Rename(p, filePath)
|
||||
_, _, err = fs.Rename(p, filePath, 0)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelError, "error renaming existing file for atomic upload, source: %q, dest: %q, err: %+v",
|
||||
p, filePath, err)
|
||||
|
|
|
@ -2458,28 +2458,34 @@ func TestEventActionValidation(t *testing.T) {
|
|||
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(resp), "no path to rename specified")
|
||||
action.Options.FsConfig.Renames = []dataprovider.KeyValue{
|
||||
action.Options.FsConfig.Renames = []dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "",
|
||||
Value: "/adir",
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "",
|
||||
Value: "/adir",
|
||||
},
|
||||
},
|
||||
}
|
||||
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(resp), "invalid paths to rename")
|
||||
action.Options.FsConfig.Renames = []dataprovider.KeyValue{
|
||||
action.Options.FsConfig.Renames = []dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "adir",
|
||||
Value: "/adir",
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "adir",
|
||||
Value: "/adir",
|
||||
},
|
||||
},
|
||||
}
|
||||
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(resp), "rename source and target cannot be equal")
|
||||
action.Options.FsConfig.Renames = []dataprovider.KeyValue{
|
||||
action.Options.FsConfig.Renames = []dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "/",
|
||||
Value: "/dir",
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "/",
|
||||
Value: "/dir",
|
||||
},
|
||||
},
|
||||
}
|
||||
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
|
||||
|
@ -23974,16 +23980,19 @@ func TestWebEventAction(t *testing.T) {
|
|||
|
||||
action.Options.FsConfig = dataprovider.EventActionFilesystemConfig{
|
||||
Type: dataprovider.FilesystemActionRename,
|
||||
Renames: []dataprovider.KeyValue{
|
||||
Renames: []dataprovider.RenameConfig{
|
||||
{
|
||||
Key: "/src",
|
||||
Value: "/target",
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: "/src",
|
||||
Value: "/target",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
form.Set("fs_action_type", fmt.Sprintf("%d", action.Options.FsConfig.Type))
|
||||
form.Set("fs_rename[0][fs_rename_source]", action.Options.FsConfig.Renames[0].Key)
|
||||
form.Set("fs_rename[0][fs_rename_target]", action.Options.FsConfig.Renames[0].Value)
|
||||
form.Set("fs_rename[0][fs_rename_options][]", "1")
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),
|
||||
bytes.NewBuffer([]byte(form.Encode())))
|
||||
assert.NoError(t, err)
|
||||
|
@ -23995,7 +24004,9 @@ func TestWebEventAction(t *testing.T) {
|
|||
actionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, action.Type, actionGet.Type)
|
||||
assert.Len(t, actionGet.Options.FsConfig.Renames, 1)
|
||||
if assert.Len(t, actionGet.Options.FsConfig.Renames, 1) {
|
||||
assert.True(t, actionGet.Options.FsConfig.Renames[0].UpdateModTime)
|
||||
}
|
||||
|
||||
action.Options.FsConfig = dataprovider.EventActionFilesystemConfig{
|
||||
Type: dataprovider.FilesystemActionCopy,
|
||||
|
|
|
@ -2201,6 +2201,28 @@ func getKeyValsFromPostFields(r *http.Request, key, val string) []dataprovider.K
|
|||
return res
|
||||
}
|
||||
|
||||
func getRenameConfigsFromPostFields(r *http.Request) []dataprovider.RenameConfig {
|
||||
var res []dataprovider.RenameConfig
|
||||
keys := r.Form["fs_rename_source"]
|
||||
values := r.Form["fs_rename_target"]
|
||||
|
||||
for idx, k := range keys {
|
||||
v := values[idx]
|
||||
if k != "" && v != "" {
|
||||
opts := r.Form["fs_rename_options"+strconv.Itoa(idx)]
|
||||
res = append(res, dataprovider.RenameConfig{
|
||||
KeyValue: dataprovider.KeyValue{
|
||||
Key: k,
|
||||
Value: v,
|
||||
},
|
||||
UpdateModTime: slices.Contains(opts, "1"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func getFoldersRetentionFromPostFields(r *http.Request) ([]dataprovider.FolderRetention, error) {
|
||||
var res []dataprovider.FolderRetention
|
||||
paths := r.Form["folder_retention_path"]
|
||||
|
@ -2310,6 +2332,8 @@ func updateRepeaterFormActionFields(r *http.Request) {
|
|||
base, _ := strings.CutSuffix(k, "[fs_rename_source]")
|
||||
r.Form.Add("fs_rename_source", strings.TrimSpace(r.Form.Get(k)))
|
||||
r.Form.Add("fs_rename_target", strings.TrimSpace(r.Form.Get(base+"[fs_rename_target]")))
|
||||
r.Form["fs_rename_options"+strconv.Itoa(len(r.Form["fs_rename_source"])-1)] =
|
||||
r.Form[base+"[fs_rename_options][]"]
|
||||
continue
|
||||
}
|
||||
if hasPrefixAndSuffix(k, "fs_copy[", "][fs_copy_source]") {
|
||||
|
@ -2398,7 +2422,7 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
|
|||
},
|
||||
FsConfig: dataprovider.EventActionFilesystemConfig{
|
||||
Type: fsActionType,
|
||||
Renames: getKeyValsFromPostFields(r, "fs_rename_source", "fs_rename_target"),
|
||||
Renames: getRenameConfigsFromPostFields(r),
|
||||
Deletes: getSliceFromDelimitedValues(r.Form.Get("fs_delete_paths"), ","),
|
||||
MkDirs: getSliceFromDelimitedValues(r.Form.Get("fs_mkdir_paths"), ","),
|
||||
Exist: getSliceFromDelimitedValues(r.Form.Get("fs_exist_paths"), ","),
|
||||
|
|
|
@ -2601,9 +2601,28 @@ func compareUserFilePatternsFilters(expected sdk.BaseUserFilters, actual sdk.Bas
|
|||
return nil
|
||||
}
|
||||
|
||||
func compareRenameConfigs(expected, actual []dataprovider.RenameConfig) error {
|
||||
if len(expected) != len(actual) {
|
||||
return errors.New("rename configs mismatch")
|
||||
}
|
||||
for _, ex := range expected {
|
||||
found := false
|
||||
for _, ac := range actual {
|
||||
if ac.Key == ex.Key && ac.Value == ex.Value && ac.UpdateModTime == ex.UpdateModTime {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return errors.New("rename configs mismatch")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareKeyValues(expected, actual []dataprovider.KeyValue) error {
|
||||
if len(expected) != len(actual) {
|
||||
return errors.New("kay values mismatch")
|
||||
return errors.New("key values mismatch")
|
||||
}
|
||||
for _, ex := range expected {
|
||||
found := false
|
||||
|
@ -2614,7 +2633,7 @@ func compareKeyValues(expected, actual []dataprovider.KeyValue) error {
|
|||
}
|
||||
}
|
||||
if !found {
|
||||
return errors.New("kay values mismatch")
|
||||
return errors.New("key values mismatch")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -2731,7 +2750,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
|
|||
if expected.Type != actual.Type {
|
||||
return errors.New("fs type mismatch")
|
||||
}
|
||||
if err := compareKeyValues(expected.Renames, actual.Renames); err != nil {
|
||||
if err := compareRenameConfigs(expected.Renames, actual.Renames); err != nil {
|
||||
return errors.New("fs renames mismatch")
|
||||
}
|
||||
if err := compareKeyValues(expected.Copy, actual.Copy); err != nil {
|
||||
|
|
|
@ -457,7 +457,7 @@ func (c *Connection) handleSFTPUploadToExistingFile(fs vfs.Fs, pflags sftp.FileO
|
|||
}
|
||||
|
||||
if common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {
|
||||
_, _, err = fs.Rename(resolvedPath, filePath)
|
||||
_, _, err = fs.Rename(resolvedPath, filePath, 0)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelError, "error renaming existing file for atomic upload, source: %q, dest: %q, err: %+v",
|
||||
resolvedPath, filePath, err)
|
||||
|
|
|
@ -146,7 +146,7 @@ func (fs MockOsFs) Remove(name string, _ bool) error {
|
|||
}
|
||||
|
||||
// Rename renames (moves) source to target
|
||||
func (fs MockOsFs) Rename(source, target string) (int, int64, error) {
|
||||
func (fs MockOsFs) Rename(source, target string, _ int) (int, int64, error) {
|
||||
if fs.err != nil {
|
||||
return -1, -1, fs.err
|
||||
}
|
||||
|
|
|
@ -330,7 +330,7 @@ func (c *scpCommand) handleUpload(uploadFilePath string, sizeToRead int64) error
|
|||
}
|
||||
|
||||
if common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {
|
||||
_, _, err = fs.Rename(p, filePath)
|
||||
_, _, err = fs.Rename(p, filePath, 0)
|
||||
if err != nil {
|
||||
c.connection.Log(logger.LevelError, "error renaming existing file for atomic upload, source: %q, dest: %q, err: %v",
|
||||
p, filePath, err)
|
||||
|
|
|
@ -306,19 +306,21 @@ func (fs *AzureBlobFs) Create(name string, flag, checks int) (File, PipeWriter,
|
|||
}
|
||||
|
||||
// Rename renames (moves) source to target.
|
||||
func (fs *AzureBlobFs) Rename(source, target string) (int, int64, error) {
|
||||
func (fs *AzureBlobFs) Rename(source, target string, checks int) (int, int64, error) {
|
||||
if source == target {
|
||||
return -1, -1, nil
|
||||
}
|
||||
_, err := fs.Stat(path.Dir(target))
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
if checks&CheckParentDir != 0 {
|
||||
_, err := fs.Stat(path.Dir(target))
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
}
|
||||
fi, err := fs.Stat(source)
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
return fs.renameInternal(source, target, fi, 0)
|
||||
return fs.renameInternal(source, target, fi, 0, checks&CheckUpdateModTime != 0)
|
||||
}
|
||||
|
||||
// Remove removes the named file or (empty) directory.
|
||||
|
@ -398,7 +400,17 @@ func (fs *AzureBlobFs) Chtimes(name string, _, mtime time.Time, isUploading bool
|
|||
if metadata == nil {
|
||||
metadata = make(map[string]*string)
|
||||
}
|
||||
metadata[lastModifiedField] = to.Ptr(strconv.FormatInt(mtime.UnixMilli(), 10))
|
||||
found := false
|
||||
for k := range metadata {
|
||||
if strings.ToLower(k) == lastModifiedField {
|
||||
metadata[k] = to.Ptr(strconv.FormatInt(mtime.UnixMilli(), 10))
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
metadata[lastModifiedField] = to.Ptr(strconv.FormatInt(mtime.UnixMilli(), 10))
|
||||
}
|
||||
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
@ -667,7 +679,7 @@ func (fs *AzureBlobFs) CopyFile(source, target string, srcInfo os.FileInfo) (int
|
|||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
if err := fs.copyFileInternal(source, target, srcInfo); err != nil {
|
||||
if err := fs.copyFileInternal(source, target, srcInfo, true); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return numFiles, sizeDiff, nil
|
||||
|
@ -750,13 +762,13 @@ func (fs *AzureBlobFs) setConfigDefaults() {
|
|||
}
|
||||
}
|
||||
|
||||
func (fs *AzureBlobFs) copyFileInternal(source, target string, srcInfo os.FileInfo) error {
|
||||
func (fs *AzureBlobFs) copyFileInternal(source, target string, srcInfo os.FileInfo, updateModTime bool) error {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxLongTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
srcBlob := fs.containerClient.NewBlockBlobClient(source)
|
||||
dstBlob := fs.containerClient.NewBlockBlobClient(target)
|
||||
resp, err := dstBlob.StartCopyFromURL(ctx, srcBlob.URL(), fs.getCopyOptions(srcInfo))
|
||||
resp, err := dstBlob.StartCopyFromURL(ctx, srcBlob.URL(), fs.getCopyOptions(srcInfo, updateModTime))
|
||||
if err != nil {
|
||||
metric.AZCopyObjectCompleted(err)
|
||||
return err
|
||||
|
@ -789,7 +801,9 @@ func (fs *AzureBlobFs) copyFileInternal(source, target string, srcInfo os.FileIn
|
|||
return nil
|
||||
}
|
||||
|
||||
func (fs *AzureBlobFs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int) (int, int64, error) {
|
||||
func (fs *AzureBlobFs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int,
|
||||
updateModTime bool,
|
||||
) (int, int64, error) {
|
||||
var numFiles int
|
||||
var filesSize int64
|
||||
|
||||
|
@ -807,7 +821,7 @@ func (fs *AzureBlobFs) renameInternal(source, target string, srcInfo os.FileInfo
|
|||
return numFiles, filesSize, err
|
||||
}
|
||||
if renameMode == 1 {
|
||||
files, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion)
|
||||
files, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion, updateModTime)
|
||||
numFiles += files
|
||||
filesSize += size
|
||||
if err != nil {
|
||||
|
@ -815,7 +829,7 @@ func (fs *AzureBlobFs) renameInternal(source, target string, srcInfo os.FileInfo
|
|||
}
|
||||
}
|
||||
} else {
|
||||
if err := fs.copyFileInternal(source, target, srcInfo); err != nil {
|
||||
if err := fs.copyFileInternal(source, target, srcInfo, updateModTime); err != nil {
|
||||
return numFiles, filesSize, err
|
||||
}
|
||||
numFiles++
|
||||
|
@ -1102,20 +1116,27 @@ func (*AzureBlobFs) readFill(r io.Reader, buf []byte) (n int, err error) {
|
|||
return n, err
|
||||
}
|
||||
|
||||
func (fs *AzureBlobFs) getCopyOptions(srcInfo os.FileInfo) *blob.StartCopyFromURLOptions {
|
||||
func (fs *AzureBlobFs) getCopyOptions(srcInfo os.FileInfo, updateModTime bool) *blob.StartCopyFromURLOptions {
|
||||
copyOptions := &blob.StartCopyFromURLOptions{}
|
||||
if fs.config.AccessTier != "" {
|
||||
copyOptions.Tier = (*blob.AccessTier)(&fs.config.AccessTier)
|
||||
}
|
||||
metadata := make(map[string]*string)
|
||||
for k, v := range getMetadata(srcInfo) {
|
||||
if v != "" {
|
||||
metadata[k] = to.Ptr(v)
|
||||
if updateModTime {
|
||||
metadata := make(map[string]*string)
|
||||
for k, v := range getMetadata(srcInfo) {
|
||||
if v != "" {
|
||||
if strings.ToLower(k) == lastModifiedField {
|
||||
metadata[k] = to.Ptr("0")
|
||||
} else {
|
||||
metadata[k] = to.Ptr(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(metadata) > 0 {
|
||||
copyOptions.Metadata = metadata
|
||||
}
|
||||
}
|
||||
if len(metadata) > 0 {
|
||||
copyOptions.Metadata = metadata
|
||||
}
|
||||
|
||||
return copyOptions
|
||||
}
|
||||
|
||||
|
|
|
@ -255,19 +255,21 @@ func (fs *GCSFs) Create(name string, flag, checks int) (File, PipeWriter, func()
|
|||
}
|
||||
|
||||
// Rename renames (moves) source to target.
|
||||
func (fs *GCSFs) Rename(source, target string) (int, int64, error) {
|
||||
func (fs *GCSFs) Rename(source, target string, checks int) (int, int64, error) {
|
||||
if source == target {
|
||||
return -1, -1, nil
|
||||
}
|
||||
_, err := fs.Stat(path.Dir(target))
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
if checks&CheckParentDir != 0 {
|
||||
_, err := fs.Stat(path.Dir(target))
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
}
|
||||
fi, err := fs.getObjectStat(source)
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
return fs.renameInternal(source, target, fi, 0)
|
||||
return fs.renameInternal(source, target, fi, 0, checks&CheckUpdateModTime != 0)
|
||||
}
|
||||
|
||||
// Remove removes the named file or (empty) directory.
|
||||
|
@ -651,7 +653,7 @@ func (fs *GCSFs) CopyFile(source, target string, srcInfo os.FileInfo) (int, int6
|
|||
}
|
||||
conditions = &storage.Conditions{DoesNotExist: true}
|
||||
}
|
||||
if err := fs.copyFileInternal(source, target, conditions, srcInfo); err != nil {
|
||||
if err := fs.copyFileInternal(source, target, conditions, srcInfo, true); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return numFiles, sizeDiff, nil
|
||||
|
@ -753,7 +755,9 @@ func (fs *GCSFs) composeObjects(ctx context.Context, dst, partialObject *storage
|
|||
return err
|
||||
}
|
||||
|
||||
func (fs *GCSFs) copyFileInternal(source, target string, conditions *storage.Conditions, srcInfo os.FileInfo) error {
|
||||
func (fs *GCSFs) copyFileInternal(source, target string, conditions *storage.Conditions,
|
||||
srcInfo os.FileInfo, updateModTime bool,
|
||||
) error {
|
||||
src := fs.svc.Bucket(fs.config.Bucket).Object(source)
|
||||
dst := fs.svc.Bucket(fs.config.Bucket).Object(target)
|
||||
if conditions != nil {
|
||||
|
@ -785,6 +789,9 @@ func (fs *GCSFs) copyFileInternal(source, target string, conditions *storage.Con
|
|||
copier.ContentType = contentType
|
||||
}
|
||||
metadata := getMetadata(srcInfo)
|
||||
if updateModTime && len(metadata) > 0 {
|
||||
delete(metadata, lastModifiedField)
|
||||
}
|
||||
if len(metadata) > 0 {
|
||||
copier.Metadata = metadata
|
||||
}
|
||||
|
@ -793,7 +800,9 @@ func (fs *GCSFs) copyFileInternal(source, target string, conditions *storage.Con
|
|||
return err
|
||||
}
|
||||
|
||||
func (fs *GCSFs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int) (int, int64, error) {
|
||||
func (fs *GCSFs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int,
|
||||
updateModTime bool,
|
||||
) (int, int64, error) {
|
||||
var numFiles int
|
||||
var filesSize int64
|
||||
|
||||
|
@ -811,7 +820,7 @@ func (fs *GCSFs) renameInternal(source, target string, srcInfo os.FileInfo, recu
|
|||
return numFiles, filesSize, err
|
||||
}
|
||||
if renameMode == 1 {
|
||||
files, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion)
|
||||
files, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion, updateModTime)
|
||||
numFiles += files
|
||||
filesSize += size
|
||||
if err != nil {
|
||||
|
@ -819,7 +828,7 @@ func (fs *GCSFs) renameInternal(source, target string, srcInfo os.FileInfo, recu
|
|||
}
|
||||
}
|
||||
} else {
|
||||
if err := fs.copyFileInternal(source, target, nil, srcInfo); err != nil {
|
||||
if err := fs.copyFileInternal(source, target, nil, srcInfo, updateModTime); err != nil {
|
||||
return numFiles, filesSize, err
|
||||
}
|
||||
numFiles++
|
||||
|
|
|
@ -384,7 +384,7 @@ func (fs *HTTPFs) Create(name string, flag, checks int) (File, PipeWriter, func(
|
|||
}
|
||||
|
||||
// Rename renames (moves) source to target.
|
||||
func (fs *HTTPFs) Rename(source, target string) (int, int64, error) {
|
||||
func (fs *HTTPFs) Rename(source, target string, checks int) (int, int64, error) {
|
||||
if source == target {
|
||||
return -1, -1, nil
|
||||
}
|
||||
|
@ -397,6 +397,9 @@ func (fs *HTTPFs) Rename(source, target string) (int, int64, error) {
|
|||
return -1, -1, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if checks&CheckUpdateModTime != 0 {
|
||||
fs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck
|
||||
}
|
||||
return -1, -1, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -176,7 +176,7 @@ func (fs *OsFs) Create(name string, flag, _ int) (File, PipeWriter, func(), erro
|
|||
}
|
||||
|
||||
// Rename renames (moves) source to target
|
||||
func (fs *OsFs) Rename(source, target string) (int, int64, error) {
|
||||
func (fs *OsFs) Rename(source, target string, checks int) (int, int64, error) {
|
||||
if source == target {
|
||||
return -1, -1, nil
|
||||
}
|
||||
|
@ -199,9 +199,15 @@ func (fs *OsFs) Rename(source, target string) (int, int64, error) {
|
|||
fsLog(fs, logger.LevelError, "cross device copy error: %v", err)
|
||||
return -1, -1, err
|
||||
}
|
||||
if checks&CheckUpdateModTime != 0 {
|
||||
fs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck
|
||||
}
|
||||
err = os.RemoveAll(source)
|
||||
return -1, -1, err
|
||||
}
|
||||
if checks&CheckUpdateModTime != 0 && err == nil {
|
||||
fs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck
|
||||
}
|
||||
return -1, -1, err
|
||||
}
|
||||
|
||||
|
|
|
@ -334,19 +334,21 @@ func (fs *S3Fs) Create(name string, flag, checks int) (File, PipeWriter, func(),
|
|||
}
|
||||
|
||||
// Rename renames (moves) source to target.
|
||||
func (fs *S3Fs) Rename(source, target string) (int, int64, error) {
|
||||
func (fs *S3Fs) Rename(source, target string, checks int) (int, int64, error) {
|
||||
if source == target {
|
||||
return -1, -1, nil
|
||||
}
|
||||
_, err := fs.Stat(path.Dir(target))
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
if checks&CheckParentDir != 0 {
|
||||
_, err := fs.Stat(path.Dir(target))
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
}
|
||||
fi, err := fs.Stat(source)
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
return fs.renameInternal(source, target, fi, 0)
|
||||
return fs.renameInternal(source, target, fi, 0, checks&CheckUpdateModTime != 0)
|
||||
}
|
||||
|
||||
// Remove removes the named file or (empty) directory.
|
||||
|
@ -700,21 +702,29 @@ func (fs *S3Fs) copyFileInternal(source, target string, srcInfo os.FileInfo) err
|
|||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
_, err := fs.svc.CopyObject(ctx, &s3.CopyObjectInput{
|
||||
copyObject := &s3.CopyObjectInput{
|
||||
Bucket: aws.String(fs.config.Bucket),
|
||||
CopySource: aws.String(copySource),
|
||||
Key: aws.String(target),
|
||||
StorageClass: types.StorageClass(fs.config.StorageClass),
|
||||
ACL: types.ObjectCannedACL(fs.config.ACL),
|
||||
ContentType: util.NilIfEmpty(contentType),
|
||||
Metadata: getMetadata(srcInfo),
|
||||
})
|
||||
}
|
||||
|
||||
metadata := getMetadata(srcInfo)
|
||||
if len(metadata) > 0 {
|
||||
copyObject.Metadata = metadata
|
||||
}
|
||||
|
||||
_, err := fs.svc.CopyObject(ctx, copyObject)
|
||||
|
||||
metric.S3CopyObjectCompleted(err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *S3Fs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int) (int, int64, error) {
|
||||
func (fs *S3Fs) renameInternal(source, target string, srcInfo os.FileInfo, recursion int,
|
||||
updateModTime bool,
|
||||
) (int, int64, error) {
|
||||
var numFiles int
|
||||
var filesSize int64
|
||||
|
||||
|
@ -732,7 +742,7 @@ func (fs *S3Fs) renameInternal(source, target string, srcInfo os.FileInfo, recur
|
|||
return numFiles, filesSize, err
|
||||
}
|
||||
if renameMode == 1 {
|
||||
files, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion)
|
||||
files, size, err := doRecursiveRename(fs, source, target, fs.renameInternal, recursion, updateModTime)
|
||||
numFiles += files
|
||||
filesSize += size
|
||||
if err != nil {
|
||||
|
|
|
@ -479,7 +479,7 @@ func (fs *SFTPFs) Create(name string, flag, _ int) (File, PipeWriter, func(), er
|
|||
}
|
||||
|
||||
// Rename renames (moves) source to target.
|
||||
func (fs *SFTPFs) Rename(source, target string) (int, int64, error) {
|
||||
func (fs *SFTPFs) Rename(source, target string, checks int) (int, int64, error) {
|
||||
if source == target {
|
||||
return -1, -1, nil
|
||||
}
|
||||
|
@ -489,9 +489,15 @@ func (fs *SFTPFs) Rename(source, target string) (int, int64, error) {
|
|||
}
|
||||
if _, ok := client.HasExtension("posix-rename@openssh.com"); ok {
|
||||
err := client.PosixRename(source, target)
|
||||
if checks&CheckUpdateModTime != 0 && err == nil {
|
||||
fs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck
|
||||
}
|
||||
return -1, -1, err
|
||||
}
|
||||
err = client.Rename(source, target)
|
||||
if checks&CheckUpdateModTime != 0 && err == nil {
|
||||
fs.Chtimes(target, time.Now(), time.Now(), false) //nolint:errcheck
|
||||
}
|
||||
return -1, -1, err
|
||||
}
|
||||
|
||||
|
|
|
@ -52,8 +52,9 @@ const (
|
|||
|
||||
// Additional checks for files
|
||||
const (
|
||||
CheckParentDir = 1
|
||||
CheckResume = 2
|
||||
CheckParentDir = 1
|
||||
CheckResume = 2
|
||||
CheckUpdateModTime = 4
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -121,7 +122,7 @@ type Fs interface {
|
|||
Lstat(name string) (os.FileInfo, error)
|
||||
Open(name string, offset int64) (File, PipeReader, func(), error)
|
||||
Create(name string, flag, checks int) (File, PipeWriter, func(), error)
|
||||
Rename(source, target string) (int, int64, error)
|
||||
Rename(source, target string, checks int) (int, int64, error)
|
||||
Remove(name string, isDir bool) error
|
||||
Mkdir(name string) error
|
||||
Symlink(source, target string) error
|
||||
|
@ -1084,7 +1085,7 @@ func IsUploadResumeSupported(fs Fs, size int64) bool {
|
|||
}
|
||||
|
||||
func getLastModified(metadata map[string]string) int64 {
|
||||
if val, ok := metadata[lastModifiedField]; ok {
|
||||
if val, ok := metadata[lastModifiedField]; ok && val != "" {
|
||||
lastModified, err := strconv.ParseInt(val, 10, 64)
|
||||
if err == nil {
|
||||
return lastModified
|
||||
|
@ -1167,8 +1168,8 @@ func getLocalTempDir() string {
|
|||
}
|
||||
|
||||
func doRecursiveRename(fs Fs, source, target string,
|
||||
renameFn func(string, string, os.FileInfo, int) (int, int64, error),
|
||||
recursion int,
|
||||
renameFn func(string, string, os.FileInfo, int, bool) (int, int64, error),
|
||||
recursion int, updateModTime bool,
|
||||
) (int, int64, error) {
|
||||
var numFiles int
|
||||
var filesSize int64
|
||||
|
@ -1193,7 +1194,7 @@ func doRecursiveRename(fs Fs, source, target string,
|
|||
for _, info := range entries {
|
||||
sourceEntry := fs.Join(source, info.Name())
|
||||
targetEntry := fs.Join(target, info.Name())
|
||||
files, size, err := renameFn(sourceEntry, targetEntry, info, recursion)
|
||||
files, size, err := renameFn(sourceEntry, targetEntry, info, recursion, updateModTime)
|
||||
if err != nil {
|
||||
if fs.IsNotExist(err) {
|
||||
fsLog(fs, logger.LevelInfo, "skipping rename for %q: %v", sourceEntry, err)
|
||||
|
|
|
@ -254,7 +254,7 @@ func (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePat
|
|||
maxWriteSize, _ := c.GetMaxWriteSize(diskQuota, false, fileSize, fs.IsUploadResumeSupported())
|
||||
|
||||
if common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {
|
||||
_, _, err = fs.Rename(resolvedPath, filePath)
|
||||
_, _, err = fs.Rename(resolvedPath, filePath, 0)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelError, "error renaming existing file for atomic upload, source: %q, dest: %q, err: %+v",
|
||||
resolvedPath, filePath, err)
|
||||
|
|
|
@ -312,7 +312,7 @@ func (fs *MockOsFs) Remove(name string, _ bool) error {
|
|||
}
|
||||
|
||||
// Rename renames (moves) source to target
|
||||
func (fs *MockOsFs) Rename(source, target string) (int, int64, error) {
|
||||
func (fs *MockOsFs) Rename(source, target string, _ int) (int, int64, error) {
|
||||
err := os.Rename(source, target)
|
||||
return -1, -1, err
|
||||
}
|
||||
|
|
|
@ -6973,6 +6973,14 @@ components:
|
|||
type: string
|
||||
value:
|
||||
type: string
|
||||
RenameConfig:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/KeyValue'
|
||||
- type: object
|
||||
properties:
|
||||
update_modtime:
|
||||
type: boolean
|
||||
description: 'Update modification time. This setting is not recursive and only applies to storage providers that support changing modification times'
|
||||
HTTPPart:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -7105,7 +7113,7 @@ components:
|
|||
renames:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/KeyValue'
|
||||
$ref: '#/components/schemas/RenameConfig'
|
||||
mkdirs:
|
||||
type: array
|
||||
items:
|
||||
|
|
|
@ -1007,6 +1007,7 @@
|
|||
"archive_path": "Archive path",
|
||||
"archive_path_help": "Full path, as seen by SFTPGo users, to the zip archive to create. Placeholders are supported. If the specified file already exists, it is overwritten",
|
||||
"placeholders_modal_title": "Supported placeholders",
|
||||
"update_mod_times": "Update timestamp",
|
||||
"types": {
|
||||
"http": "HTTP",
|
||||
"email": "Email",
|
||||
|
|
|
@ -1007,6 +1007,7 @@
|
|||
"archive_path": "Percorso dell'archivio",
|
||||
"archive_path_help": "Percorso completo, come visto dagli utenti SFTPGo, dell'archivio zip da creare. I segnaposto sono supportati. Se il file specificato esiste già, verrà sovrascritto",
|
||||
"placeholders_modal_title": "Segnaposto supportati",
|
||||
"update_mod_times": "Aggiorna timestamp",
|
||||
"types": {
|
||||
"http": "HTTP",
|
||||
"email": "Email",
|
||||
|
|
|
@ -653,12 +653,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<div data-repeater-item>
|
||||
<div data-repeater-item>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-5 mt-3 mt-md-8">
|
||||
<div class="col-md-4 mt-3 mt-md-8">
|
||||
<input data-i18n="[placeholder]actions.source_path" type="text" class="form-control" name="fs_rename_source" value="{{$val.Key}}" spellcheck="false" />
|
||||
</div>
|
||||
<div class="col-md-6 mt-3 mt-md-8">
|
||||
<div class="col-md-4 mt-3 mt-md-8">
|
||||
<input data-i18n="[placeholder]actions.target_path" type="text" class="form-control" name="fs_rename_target" value="{{$val.Value}}" spellcheck="false" />
|
||||
</div>
|
||||
<div class="col-md-3 mt-3 mt-md-8">
|
||||
<select name="fs_rename_options" data-i18n="[data-placeholder]general.options" class="form-select select-repetear" data-allow-clear="true" data-close-on-select="false" data-hide-search="true" multiple>
|
||||
<option value=""></option>
|
||||
<option value="1" data-i18n="actions.update_mod_times" {{if $val.UpdateModTime}}selected{{end}}>Change times</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1 mt-3 mt-md-8">
|
||||
<a href="#" data-repeater-delete
|
||||
class="btn btn-light-danger ps-5 pe-4">
|
||||
|
@ -677,12 +683,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
{{- else}}
|
||||
<div data-repeater-item>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-5 mt-3 mt-md-8">
|
||||
<div class="col-md-4 mt-3 mt-md-8">
|
||||
<input data-i18n="[placeholder]actions.source_path" type="text" class="form-control" name="fs_rename_source" value="" spellcheck="false" />
|
||||
</div>
|
||||
<div class="col-md-6 mt-3 mt-md-8">
|
||||
<div class="col-md-4 mt-3 mt-md-8">
|
||||
<input data-i18n="[placeholder]actions.target_path" type="text" class="form-control" name="fs_rename_target" value="" spellcheck="false" />
|
||||
</div>
|
||||
<div class="col-md-3 mt-3 mt-md-8">
|
||||
<select name="fs_rename_options" data-i18n="[data-placeholder]general.options" class="form-select select-repetear" data-allow-clear="true" data-close-on-select="false" data-hide-search="true" multiple>
|
||||
<option value=""></option>
|
||||
<option value="1" data-i18n="actions.update_mod_times">Update timestamps</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1 mt-3 mt-md-8">
|
||||
<a href="#" data-repeater-delete
|
||||
class="btn btn-light-danger ps-5 pe-4">
|
||||
|
|
Loading…
Reference in a new issue