diff --git a/go.mod b/go.mod
index 7fcdf339..2d50f28b 100644
--- a/go.mod
+++ b/go.mod
@@ -9,10 +9,10 @@ require (
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/aws/aws-sdk-go-v2 v1.16.11
- github.com/aws/aws-sdk-go-v2/config v1.16.1
- github.com/aws/aws-sdk-go-v2/credentials v1.12.13
+ github.com/aws/aws-sdk-go-v2/config v1.17.1
+ github.com/aws/aws-sdk-go-v2/credentials v1.12.14
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12
- github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.25
+ github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.12
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.5
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.17
@@ -51,7 +51,7 @@ require (
github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5
github.com/rs/xid v1.4.0
github.com/rs/zerolog v1.27.0
- github.com/sftpgo/sdk v0.1.2-0.20220727164210-06723ba7ce9a
+ github.com/sftpgo/sdk v0.1.2-0.20220815185512-a4dc48b3993e
github.com/shirou/gopsutil/v3 v3.22.7
github.com/spf13/afero v1.9.2
github.com/spf13/cobra v1.5.0
@@ -89,7 +89,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.12 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.11.16 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 // indirect
github.com/aws/smithy-go v1.12.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
@@ -124,8 +124,8 @@ require (
github.com/lestrrat-go/option v1.0.0 // indirect
github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281 // indirect
github.com/magiconair/properties v1.8.6 // indirect
- github.com/mattn/go-colorable v0.1.12 // indirect
- github.com/mattn/go-isatty v0.0.14 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
@@ -155,7 +155,7 @@ require (
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto v0.0.0-20220812140447-cec7f5303424 // indirect
+ google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959 // indirect
google.golang.org/grpc v1.48.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/go.sum b/go.sum
index d00ad533..a54b1d48 100644
--- a/go.sum
+++ b/go.sum
@@ -150,17 +150,17 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 h1:zfT11pa7ifu/VlLDpmc5OY2W4nYmnKkFDGeMVnmqAI0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4/go.mod h1:ES0I1GBs+YYgcDS1ek47Erbn4TOL811JKqBXtgzqyZ8=
github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg=
-github.com/aws/aws-sdk-go-v2/config v1.16.1 h1:jasqFPOoNPXHOYGEEuvyT87ACiXhD3OkQckIm5uqi5I=
-github.com/aws/aws-sdk-go-v2/config v1.16.1/go.mod h1:4SKzBMiB8lV0fw2w7eDBo/LjQyHFITN4vUUuqpurFmI=
+github.com/aws/aws-sdk-go-v2/config v1.17.1 h1:BWxTjokU/69BZ4DnLrZco6OvBDii6ToEdfBL/y5I1nA=
+github.com/aws/aws-sdk-go-v2/config v1.17.1/go.mod h1:uOxDHjBemNTF2Zos+fgG0NNfE86wn1OAHDTGxjMEYi0=
github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g=
-github.com/aws/aws-sdk-go-v2/credentials v1.12.13 h1:cuPzIsjKAWBUAAk8ZUR2l02Sxafl9hiaMsc7tlnjwAY=
-github.com/aws/aws-sdk-go-v2/credentials v1.12.13/go.mod h1:9fDEemXizwXrxPU1MTzv69LP/9D8HVl5qHAQO9A9ikY=
+github.com/aws/aws-sdk-go-v2/credentials v1.12.14 h1:AtVG/amkjbDBfnPr/tuW2IG18HGNznP6L12Dx0rLz+Q=
+github.com/aws/aws-sdk-go-v2/credentials v1.12.14/go.mod h1:opAndTyq+YN7IpVG57z2CeNuXSQMqTYxGGlYH0m0RMY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 h1:wgJBHO58Pc1V1QAnzdVM3JK3WbE/6eUF0JxCZ+/izz0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12/go.mod h1:aZ4vZnyUuxedC7eD4JyEHpGnCz+O2sHQEx3VvAwklSE=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.25 h1:ShUxLkMxarXylGxfYwg8p+xEKY+C1y54oUU3wFsUMFo=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.25/go.mod h1:cam5wV1ebd3ZVuh2r2CA8FtSAA/eUMtRH4owk0ygfFs=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27 h1:xFXIMBci0UXStoOHq/8w0XIZPB2hgb9CD7uATJhqt10=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27/go.mod h1:+tj2cHQkChanggNZn1J2fJ1Cv6RO1TV0AA3472do31I=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 h1:OmiwoVyLKEqqD5GvB683dbSqxiOfvx4U2lDZhG2Esc4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18/go.mod h1:348MLhzV1GSlZSMusdwQpXKbhD7X2gbI/TxwAPKkYZQ=
@@ -197,8 +197,8 @@ github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZP
github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM=
github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU=
-github.com/aws/aws-sdk-go-v2/service/sso v1.11.16 h1:YK8L7TNlGwMWHYqLs+i6dlITpxqzq08FqQUy26nm+T8=
-github.com/aws/aws-sdk-go-v2/service/sso v1.11.16/go.mod h1:mS5xqLZc/6kc06IpXn5vRxdLaED+jEuaSRv5BxtnsiY=
+github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 h1:pXxu9u2z1UqSbjO9YA8kmFJBhFc1EVTDaf7A+S+Ivq8=
+github.com/aws/aws-sdk-go-v2/service/sso v1.11.17/go.mod h1:mS5xqLZc/6kc06IpXn5vRxdLaED+jEuaSRv5BxtnsiY=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 h1:dl8T0PJlN92rvEGOEUiD0+YPYdPEaCZK0TqHukvSfII=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.13/go.mod h1:Ru3QVMLygVs/07UQ3YDur1AQZZp2tUNje8wfloFttC0=
@@ -602,14 +602,16 @@ github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
@@ -712,8 +714,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
-github.com/sftpgo/sdk v0.1.2-0.20220727164210-06723ba7ce9a h1:X9qPZ+GPQ87TnBDNZN6dyX7FkjhwnFh98WgB6Y1T5O8=
-github.com/sftpgo/sdk v0.1.2-0.20220727164210-06723ba7ce9a/go.mod h1:RL4HeorXC6XgqtkLYnQUSogLdsdMfbsogIvdBVLuy4w=
+github.com/sftpgo/sdk v0.1.2-0.20220815185512-a4dc48b3993e h1:EJiTi+f2QCiDoGj1EBq6o1RX+JrtZnvTE6yKt3ks1B8=
+github.com/sftpgo/sdk v0.1.2-0.20220815185512-a4dc48b3993e/go.mod h1:RL4HeorXC6XgqtkLYnQUSogLdsdMfbsogIvdBVLuy4w=
github.com/shirou/gopsutil/v3 v3.22.7 h1:flKnuCMfUUrO+oAvwAd6GKZgnPzr098VA/UJ14nhJd4=
github.com/shirou/gopsutil/v3 v3.22.7/go.mod h1:s648gW4IywYzUfE/KjXxUsqrqx/T2xO5VqOXxONeRfI=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@@ -1226,8 +1228,8 @@ google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljW
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
-google.golang.org/genproto v0.0.0-20220812140447-cec7f5303424 h1:zZnTt15U44/Txe/9cN/tVbteBkPMiyXK48hPsKRmqj4=
-google.golang.org/genproto v0.0.0-20220812140447-cec7f5303424/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
+google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959 h1:hw4Y42zL1VyVKxPgRHHh191fpVBGV8sNVmcow5Z8VXY=
+google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
diff --git a/internal/common/common_test.go b/internal/common/common_test.go
index 7142a565..0dc5732c 100644
--- a/internal/common/common_test.go
+++ b/internal/common/common_test.go
@@ -1061,6 +1061,135 @@ func TestUserRecentActivity(t *testing.T) {
assert.True(t, res)
}
+func TestVfsSameResource(t *testing.T) {
+ fs := vfs.Filesystem{}
+ other := vfs.Filesystem{}
+ res := fs.IsSameResource(other)
+ assert.True(t, res)
+ fs = vfs.Filesystem{
+ Provider: sdk.S3FilesystemProvider,
+ S3Config: vfs.S3FsConfig{
+ BaseS3FsConfig: sdk.BaseS3FsConfig{
+ Bucket: "a",
+ Region: "b",
+ },
+ },
+ }
+ other = vfs.Filesystem{
+ Provider: sdk.S3FilesystemProvider,
+ S3Config: vfs.S3FsConfig{
+ BaseS3FsConfig: sdk.BaseS3FsConfig{
+ Bucket: "a",
+ Region: "c",
+ },
+ },
+ }
+ res = fs.IsSameResource(other)
+ assert.False(t, res)
+ other = vfs.Filesystem{
+ Provider: sdk.S3FilesystemProvider,
+ S3Config: vfs.S3FsConfig{
+ BaseS3FsConfig: sdk.BaseS3FsConfig{
+ Bucket: "a",
+ Region: "b",
+ },
+ },
+ }
+ res = fs.IsSameResource(other)
+ assert.True(t, res)
+ fs = vfs.Filesystem{
+ Provider: sdk.GCSFilesystemProvider,
+ GCSConfig: vfs.GCSFsConfig{
+ BaseGCSFsConfig: sdk.BaseGCSFsConfig{
+ Bucket: "b",
+ },
+ },
+ }
+ other = vfs.Filesystem{
+ Provider: sdk.GCSFilesystemProvider,
+ GCSConfig: vfs.GCSFsConfig{
+ BaseGCSFsConfig: sdk.BaseGCSFsConfig{
+ Bucket: "c",
+ },
+ },
+ }
+ res = fs.IsSameResource(other)
+ assert.False(t, res)
+ other = vfs.Filesystem{
+ Provider: sdk.GCSFilesystemProvider,
+ GCSConfig: vfs.GCSFsConfig{
+ BaseGCSFsConfig: sdk.BaseGCSFsConfig{
+ Bucket: "b",
+ },
+ },
+ }
+ res = fs.IsSameResource(other)
+ assert.True(t, res)
+ sasURL := kms.NewPlainSecret("http://127.0.0.1/sasurl")
+ fs = vfs.Filesystem{
+ Provider: sdk.AzureBlobFilesystemProvider,
+ AzBlobConfig: vfs.AzBlobFsConfig{
+ BaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{
+ AccountName: "a",
+ },
+ SASURL: sasURL,
+ },
+ }
+ err := fs.Validate("data1")
+ assert.NoError(t, err)
+ other = vfs.Filesystem{
+ Provider: sdk.AzureBlobFilesystemProvider,
+ AzBlobConfig: vfs.AzBlobFsConfig{
+ BaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{
+ AccountName: "a",
+ },
+ SASURL: sasURL,
+ },
+ }
+ err = other.Validate("data2")
+ assert.NoError(t, err)
+ err = fs.AzBlobConfig.SASURL.TryDecrypt()
+ assert.NoError(t, err)
+ err = other.AzBlobConfig.SASURL.TryDecrypt()
+ assert.NoError(t, err)
+ res = fs.IsSameResource(other)
+ assert.True(t, res)
+ fs.AzBlobConfig.AccountName = "b"
+ res = fs.IsSameResource(other)
+ assert.False(t, res)
+ fs.AzBlobConfig.AccountName = "a"
+ other.AzBlobConfig.SASURL = kms.NewPlainSecret("http://127.1.1.1/sasurl")
+ err = other.Validate("data2")
+ assert.NoError(t, err)
+ err = other.AzBlobConfig.SASURL.TryDecrypt()
+ assert.NoError(t, err)
+ res = fs.IsSameResource(other)
+ assert.False(t, res)
+ fs = vfs.Filesystem{
+ Provider: sdk.HTTPFilesystemProvider,
+ HTTPConfig: vfs.HTTPFsConfig{
+ BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
+ Endpoint: "http://127.0.0.1/httpfs",
+ Username: "a",
+ },
+ },
+ }
+ other = vfs.Filesystem{
+ Provider: sdk.HTTPFilesystemProvider,
+ HTTPConfig: vfs.HTTPFsConfig{
+ BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
+ Endpoint: "http://127.0.0.1/httpfs",
+ Username: "b",
+ },
+ },
+ }
+ res = fs.IsSameResource(other)
+ assert.True(t, res)
+ fs.HTTPConfig.EqualityCheckMode = 1
+ res = fs.IsSameResource(other)
+ assert.False(t, res)
+}
+
func BenchmarkBcryptHashing(b *testing.B) {
bcryptPassword := "bcryptpassword"
for i := 0; i < b.N; i++ {
diff --git a/internal/common/connection.go b/internal/common/connection.go
index 1a823ab3..9189fd9a 100644
--- a/internal/common/connection.go
+++ b/internal/common/connection.go
@@ -526,7 +526,7 @@ func (c *BaseConnection) Rename(virtualSourcePath, virtualTargetPath string) err
c.Log(logger.LevelInfo, "denying cross rename due to space limit")
return c.GetGenericError(ErrQuotaExceeded)
}
- if err := fsSrc.Rename(fsSourcePath, fsTargetPath); err != nil {
+ if err := fsDst.Rename(fsSourcePath, fsTargetPath); err != nil {
c.Log(logger.LevelError, "failed to rename %#v -> %#v: %+v", fsSourcePath, fsTargetPath, err)
return c.GetFsError(fsSrc, err)
}
@@ -845,8 +845,8 @@ func (c *BaseConnection) hasRenamePerms(virtualSourcePath, virtualTargetPath str
func (c *BaseConnection) isRenamePermitted(fsSrc, fsDst vfs.Fs, fsSourcePath, fsTargetPath, virtualSourcePath,
virtualTargetPath string, fi os.FileInfo,
) bool {
- if !c.isLocalOrSameFolderRename(virtualSourcePath, virtualTargetPath) {
- c.Log(logger.LevelInfo, "rename %#v->%#v is not allowed: the paths must be local or on the same virtual folder",
+ if !c.isSameResourceRename(virtualSourcePath, virtualTargetPath) {
+ c.Log(logger.LevelInfo, "rename %#v->%#v is not allowed: the paths must be on the same resource",
virtualSourcePath, virtualTargetPath)
return false
}
@@ -1088,8 +1088,7 @@ func (c *BaseConnection) HasSpace(checkFiles, getUsage bool, requestPath string)
return result, transferQuota
}
-// returns true if this is a rename on the same fs or local virtual folders
-func (c *BaseConnection) isLocalOrSameFolderRename(virtualSourcePath, virtualTargetPath string) bool {
+func (c *BaseConnection) isSameResourceRename(virtualSourcePath, virtualTargetPath string) bool {
sourceFolder, errSrc := c.User.GetVirtualFolderForPath(virtualSourcePath)
dstFolder, errDst := c.User.GetVirtualFolderForPath(virtualTargetPath)
if errSrc != nil && errDst != nil {
@@ -1099,27 +1098,13 @@ func (c *BaseConnection) isLocalOrSameFolderRename(virtualSourcePath, virtualTar
if sourceFolder.Name == dstFolder.Name {
return true
}
- // we have different folders, only local fs is supported
- if sourceFolder.FsConfig.Provider == sdk.LocalFilesystemProvider &&
- dstFolder.FsConfig.Provider == sdk.LocalFilesystemProvider {
- return true
- }
- return false
- }
- if c.User.FsConfig.Provider != sdk.LocalFilesystemProvider {
- return false
+ // we have different folders, check if they point to the same resource
+ return sourceFolder.FsConfig.IsSameResource(dstFolder.FsConfig)
}
if errSrc == nil {
- if sourceFolder.FsConfig.Provider == sdk.LocalFilesystemProvider {
- return true
- }
+ return sourceFolder.FsConfig.IsSameResource(c.User.FsConfig)
}
- if errDst == nil {
- if dstFolder.FsConfig.Provider == sdk.LocalFilesystemProvider {
- return true
- }
- }
- return false
+ return dstFolder.FsConfig.IsSameResource(c.User.FsConfig)
}
func (c *BaseConnection) isCrossFoldersRequest(virtualSourcePath, virtualTargetPath string) bool {
diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go
index eff2795a..1231ec61 100644
--- a/internal/common/protocol_test.go
+++ b/internal/common/protocol_test.go
@@ -55,6 +55,7 @@ import (
"github.com/drakkan/sftpgo/v2/internal/kms"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/mfa"
+ "github.com/drakkan/sftpgo/v2/internal/sftpd"
"github.com/drakkan/sftpgo/v2/internal/smtp"
"github.com/drakkan/sftpgo/v2/internal/util"
"github.com/drakkan/sftpgo/v2/internal/vfs"
@@ -132,6 +133,9 @@ func TestMain(m *testing.M) {
sftpdConf := config.GetSFTPDConfig()
sftpdConf.Bindings[0].Port = 4022
+ sftpdConf.Bindings = append(sftpdConf.Bindings, sftpd.Binding{
+ Port: 4024,
+ })
sftpdConf.KeyboardInteractiveAuthentication = true
httpdConf := config.GetHTTPDConfig()
@@ -2309,6 +2313,216 @@ func TestVirtualFoldersLink(t *testing.T) {
assert.NoError(t, err)
}
+func TestCrossFolderRename(t *testing.T) {
+ folder1 := "folder1"
+ folder2 := "folder2"
+ folder3 := "folder3"
+ folder4 := "folder4"
+ folder5 := "folder5"
+ folder6 := "folder6"
+ folder7 := "folder7"
+
+ baseUser, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
+ assert.NoError(t, err, string(resp))
+
+ u := getCryptFsUser()
+ u.Username += "_crypt"
+ u.VirtualFolders = []vfs.VirtualFolder{
+ {
+ BaseVirtualFolder: vfs.BaseVirtualFolder{
+ Name: folder1,
+ MappedPath: filepath.Join(os.TempDir(), folder1),
+ FsConfig: vfs.Filesystem{
+ Provider: sdk.CryptedFilesystemProvider,
+ CryptConfig: vfs.CryptFsConfig{
+ Passphrase: kms.NewPlainSecret(defaultPassword),
+ },
+ },
+ },
+ VirtualPath: path.Join("/", folder1),
+ QuotaSize: -1,
+ QuotaFiles: -1,
+ },
+ {
+ BaseVirtualFolder: vfs.BaseVirtualFolder{
+ Name: folder2,
+ MappedPath: filepath.Join(os.TempDir(), folder2),
+ FsConfig: vfs.Filesystem{
+ Provider: sdk.CryptedFilesystemProvider,
+ CryptConfig: vfs.CryptFsConfig{
+ Passphrase: kms.NewPlainSecret(defaultPassword),
+ },
+ },
+ },
+ VirtualPath: path.Join("/", folder2),
+ QuotaSize: -1,
+ QuotaFiles: -1,
+ },
+ {
+ BaseVirtualFolder: vfs.BaseVirtualFolder{
+ Name: folder3,
+ MappedPath: filepath.Join(os.TempDir(), folder3),
+ FsConfig: vfs.Filesystem{
+ Provider: sdk.CryptedFilesystemProvider,
+ CryptConfig: vfs.CryptFsConfig{
+ Passphrase: kms.NewPlainSecret(defaultPassword + "mod"),
+ },
+ },
+ },
+ VirtualPath: path.Join("/", folder3),
+ QuotaSize: -1,
+ QuotaFiles: -1,
+ },
+ {
+ BaseVirtualFolder: vfs.BaseVirtualFolder{
+ Name: folder4,
+ MappedPath: filepath.Join(os.TempDir(), folder4),
+ FsConfig: vfs.Filesystem{
+ Provider: sdk.SFTPFilesystemProvider,
+ SFTPConfig: vfs.SFTPFsConfig{
+ BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+ Endpoint: sftpServerAddr,
+ Username: baseUser.Username,
+ Prefix: path.Join("/", folder4),
+ },
+ Password: kms.NewPlainSecret(defaultPassword),
+ },
+ },
+ },
+ VirtualPath: path.Join("/", folder4),
+ QuotaSize: -1,
+ QuotaFiles: -1,
+ },
+ {
+ BaseVirtualFolder: vfs.BaseVirtualFolder{
+ Name: folder5,
+ MappedPath: filepath.Join(os.TempDir(), folder5),
+ FsConfig: vfs.Filesystem{
+ Provider: sdk.SFTPFilesystemProvider,
+ SFTPConfig: vfs.SFTPFsConfig{
+ BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+ Endpoint: sftpServerAddr,
+ Username: baseUser.Username,
+ Prefix: path.Join("/", folder5),
+ },
+ Password: kms.NewPlainSecret(defaultPassword),
+ },
+ },
+ },
+ VirtualPath: path.Join("/", folder5),
+ QuotaSize: -1,
+ QuotaFiles: -1,
+ },
+ {
+ BaseVirtualFolder: vfs.BaseVirtualFolder{
+ Name: folder6,
+ MappedPath: filepath.Join(os.TempDir(), folder6),
+ FsConfig: vfs.Filesystem{
+ Provider: sdk.SFTPFilesystemProvider,
+ SFTPConfig: vfs.SFTPFsConfig{
+ BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+ Endpoint: "127.0.0.1:4024",
+ Username: baseUser.Username,
+ Prefix: path.Join("/", folder6),
+ },
+ Password: kms.NewPlainSecret(defaultPassword),
+ },
+ },
+ },
+ VirtualPath: path.Join("/", folder6),
+ QuotaSize: -1,
+ QuotaFiles: -1,
+ },
+ {
+ BaseVirtualFolder: vfs.BaseVirtualFolder{
+ Name: folder7,
+ MappedPath: filepath.Join(os.TempDir(), folder7),
+ FsConfig: vfs.Filesystem{
+ Provider: sdk.SFTPFilesystemProvider,
+ SFTPConfig: vfs.SFTPFsConfig{
+ BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
+ Endpoint: sftpServerAddr,
+ Username: baseUser.Username,
+ Prefix: path.Join("/", folder4),
+ },
+ Password: kms.NewPlainSecret(defaultPassword),
+ },
+ },
+ },
+ VirtualPath: path.Join("/", folder7),
+ QuotaSize: -1,
+ QuotaFiles: -1,
+ },
+ }
+
+ user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
+ assert.NoError(t, err, string(resp))
+ conn, client, err := getSftpClient(user)
+ if assert.NoError(t, err) {
+ defer conn.Close()
+ defer client.Close()
+
+ subDir := "testSubDir"
+ err = client.Mkdir(subDir)
+ assert.NoError(t, err)
+ err = writeSFTPFile(path.Join(subDir, "afile.bin"), 64, client)
+ assert.NoError(t, err)
+ err = client.Rename(subDir, path.Join("/", folder1, subDir))
+ assert.NoError(t, err)
+ _, err = client.Stat(path.Join("/", folder1, subDir))
+ assert.NoError(t, err)
+ _, err = client.Stat(path.Join("/", folder1, subDir, "afile.bin"))
+ assert.NoError(t, err)
+ err = client.Rename(path.Join("/", folder1, subDir), path.Join("/", folder2, subDir))
+ assert.NoError(t, err)
+ _, err = client.Stat(path.Join("/", folder2, subDir))
+ assert.NoError(t, err)
+ _, err = client.Stat(path.Join("/", folder2, subDir, "afile.bin"))
+ assert.NoError(t, err)
+ err = client.Rename(path.Join("/", folder2, subDir), path.Join("/", folder3, subDir))
+ assert.ErrorIs(t, err, os.ErrPermission)
+ err = writeSFTPFile(path.Join("/", folder3, "file.bin"), 64, client)
+ assert.NoError(t, err)
+ err = client.Rename(path.Join("/", folder3, "file.bin"), "/renamed.bin")
+ assert.ErrorIs(t, err, os.ErrPermission)
+ err = client.Rename(path.Join("/", folder3, "file.bin"), path.Join("/", folder2, "/renamed.bin"))
+ assert.ErrorIs(t, err, os.ErrPermission)
+ err = client.Rename(path.Join("/", folder3, "file.bin"), path.Join("/", folder3, "/renamed.bin"))
+ assert.NoError(t, err)
+ err = writeSFTPFile("/afile.bin", 64, client)
+ assert.NoError(t, err)
+ err = client.Rename("afile.bin", path.Join("/", folder4, "afile_renamed.bin"))
+ assert.ErrorIs(t, err, os.ErrPermission)
+ err = writeSFTPFile(path.Join("/", folder4, "afile.bin"), 64, client)
+ assert.NoError(t, err)
+ err = client.Rename(path.Join("/", folder4, "afile.bin"), path.Join("/", folder5, "afile_renamed.bin"))
+ assert.NoError(t, err)
+ err = client.Rename(path.Join("/", folder5, "afile_renamed.bin"), path.Join("/", folder6, "afile_renamed.bin"))
+ assert.ErrorIs(t, err, os.ErrPermission)
+ err = writeSFTPFile(path.Join("/", folder4, "afile.bin"), 64, client)
+ assert.NoError(t, err)
+ _, err = client.Stat(path.Join("/", folder7, "afile.bin"))
+ assert.NoError(t, err)
+ err = client.Rename(path.Join("/", folder4, "afile.bin"), path.Join("/", folder7, "afile.bin"))
+ assert.NoError(t, err)
+ }
+
+ _, err = httpdtest.RemoveUser(user, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(user.GetHomeDir())
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveUser(baseUser, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(baseUser.GetHomeDir())
+ assert.NoError(t, err)
+ for _, folderName := range []string{folder1, folder2, folder3, folder4, folder5, folder6, folder7} {
+ _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(filepath.Join(os.TempDir(), folderName))
+ assert.NoError(t, err)
+ }
+}
+
func TestDirs(t *testing.T) {
u := getTestUser()
mappedPath := filepath.Join(os.TempDir(), "vdir")
diff --git a/internal/dataprovider/user.go b/internal/dataprovider/user.go
index aad24f7c..69f38112 100644
--- a/internal/dataprovider/user.go
+++ b/internal/dataprovider/user.go
@@ -300,7 +300,7 @@ func (u *User) isFsEqual(other *User) bool {
if u.FsConfig.Provider == sdk.LocalFilesystemProvider && u.GetHomeDir() != other.GetHomeDir() {
return false
}
- if !u.FsConfig.IsEqual(&other.FsConfig) {
+ if !u.FsConfig.IsEqual(other.FsConfig) {
return false
}
if u.Filters.StartDirectory != other.Filters.StartDirectory {
@@ -319,7 +319,7 @@ func (u *User) isFsEqual(other *User) bool {
if f.FsConfig.Provider == sdk.LocalFilesystemProvider && f.MappedPath != f1.MappedPath {
return false
}
- if !f.FsConfig.IsEqual(&f1.FsConfig) {
+ if !f.FsConfig.IsEqual(f1.FsConfig) {
return false
}
}
diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go
index 60ad7c7c..90cb2740 100644
--- a/internal/httpd/httpd_test.go
+++ b/internal/httpd/httpd_test.go
@@ -4300,6 +4300,7 @@ func TestUserSFTPFs(t *testing.T) {
user.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(sftpPrivateKey)
user.FsConfig.SFTPConfig.Fingerprints = []string{sftpPkeyFingerprint}
user.FsConfig.SFTPConfig.BufferSize = 2
+ user.FsConfig.SFTPConfig.EqualityCheckMode = 1
_, resp, err := httpdtest.UpdateUser(user, http.StatusBadRequest, "")
assert.NoError(t, err)
assert.Contains(t, string(resp), "invalid endpoint")
@@ -18070,6 +18071,7 @@ func TestWebUserHTTPFsMock(t *testing.T) {
form.Set("patterns1", "*.zip")
form.Set("pattern_type1", "denied")
form.Set("max_upload_file_size", "0")
+ form.Set("http_equality_check_mode", "true")
b, contentType, _ := getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
setJWTCookieForReq(req, webToken)
@@ -18097,7 +18099,9 @@ func TestWebUserHTTPFsMock(t *testing.T) {
assert.NotEmpty(t, updateUser.FsConfig.HTTPConfig.APIKey.GetPayload())
assert.Empty(t, updateUser.FsConfig.HTTPConfig.APIKey.GetKey())
assert.Empty(t, updateUser.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
+ assert.Equal(t, 1, updateUser.FsConfig.HTTPConfig.EqualityCheckMode)
// now check that a redacted password is not saved
+ form.Set("http_equality_check_mode", "")
form.Set("http_password", " "+redactedSecret+" ")
form.Set("http_api_key", redactedSecret)
b, contentType, _ = getMultipartFormData(form, "", "")
@@ -18121,6 +18125,7 @@ func TestWebUserHTTPFsMock(t *testing.T) {
assert.Equal(t, updateUser.FsConfig.HTTPConfig.APIKey.GetPayload(), lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetPayload())
assert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetKey())
assert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
+ assert.Equal(t, 0, lastUpdatedUser.FsConfig.HTTPConfig.EqualityCheckMode)
req, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)
assert.NoError(t, err)
@@ -18491,6 +18496,7 @@ func TestWebUserSFTPFsMock(t *testing.T) {
form.Set("sftp_fingerprints", user.FsConfig.SFTPConfig.Fingerprints[0])
form.Set("sftp_prefix", user.FsConfig.SFTPConfig.Prefix)
form.Set("sftp_disable_concurrent_reads", "true")
+ form.Set("sftp_equality_check_mode", "true")
form.Set("sftp_buffer_size", strconv.FormatInt(user.FsConfig.SFTPConfig.BufferSize, 10))
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
@@ -18526,10 +18532,12 @@ func TestWebUserSFTPFsMock(t *testing.T) {
assert.Len(t, updateUser.FsConfig.SFTPConfig.Fingerprints, 1)
assert.Equal(t, user.FsConfig.SFTPConfig.BufferSize, updateUser.FsConfig.SFTPConfig.BufferSize)
assert.Contains(t, updateUser.FsConfig.SFTPConfig.Fingerprints, sftpPkeyFingerprint)
+ assert.Equal(t, 1, updateUser.FsConfig.SFTPConfig.EqualityCheckMode)
// now check that a redacted credentials are not saved
form.Set("sftp_password", redactedSecret+" ")
form.Set("sftp_private_key", redactedSecret)
form.Set("sftp_key_passphrase", redactedSecret)
+ form.Set("sftp_equality_check_mode", "")
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
setJWTCookieForReq(req, webToken)
@@ -18555,6 +18563,7 @@ func TestWebUserSFTPFsMock(t *testing.T) {
assert.Equal(t, updateUser.FsConfig.SFTPConfig.KeyPassphrase.GetPayload(), lastUpdatedUser.FsConfig.SFTPConfig.KeyPassphrase.GetPayload())
assert.Empty(t, lastUpdatedUser.FsConfig.SFTPConfig.KeyPassphrase.GetKey())
assert.Empty(t, lastUpdatedUser.FsConfig.SFTPConfig.KeyPassphrase.GetAdditionalData())
+ assert.Equal(t, 0, lastUpdatedUser.FsConfig.SFTPConfig.EqualityCheckMode)
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)
setBearerForReq(req, apiToken)
rr = executeRequest(req)
diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go
index 52af6a0c..21898e44 100644
--- a/internal/httpd/webadmin.go
+++ b/internal/httpd/webadmin.go
@@ -1414,6 +1414,11 @@ func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {
config.Prefix = r.Form.Get("sftp_prefix")
config.DisableCouncurrentReads = r.Form.Get("sftp_disable_concurrent_reads") != ""
config.BufferSize, err = strconv.ParseInt(r.Form.Get("sftp_buffer_size"), 10, 64)
+ if r.Form.Get("sftp_equality_check_mode") != "" {
+ config.EqualityCheckMode = 1
+ } else {
+ config.EqualityCheckMode = 0
+ }
if err != nil {
return config, fmt.Errorf("invalid SFTP buffer size: %w", err)
}
@@ -1427,6 +1432,11 @@ func getHTTPFsConfig(r *http.Request) vfs.HTTPFsConfig {
config.SkipTLSVerify = r.Form.Get("http_skip_tls_verify") != ""
config.Password = getSecretFromFormField(r, "http_password")
config.APIKey = getSecretFromFormField(r, "http_api_key")
+ if r.Form.Get("http_equality_check_mode") != "" {
+ config.EqualityCheckMode = 1
+ } else {
+ config.EqualityCheckMode = 0
+ }
return config
}
diff --git a/internal/httpdtest/httpdtest.go b/internal/httpdtest/httpdtest.go
index 66364d61..2b978a2c 100644
--- a/internal/httpdtest/httpdtest.go
+++ b/internal/httpdtest/httpdtest.go
@@ -1867,6 +1867,9 @@ func compareHTTPFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error
if expected.HTTPConfig.SkipTLSVerify != actual.HTTPConfig.SkipTLSVerify {
return errors.New("HTTPFs skip_tls_verify mismatch")
}
+ if expected.SFTPConfig.EqualityCheckMode != actual.SFTPConfig.EqualityCheckMode {
+ return errors.New("HTTPFs equality_check_mode mismatch")
+ }
if err := checkEncryptedSecret(expected.HTTPConfig.Password, actual.HTTPConfig.Password); err != nil {
return fmt.Errorf("HTTPFs password mismatch: %v", err)
}
@@ -1889,6 +1892,9 @@ func compareSFTPFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error
if expected.SFTPConfig.BufferSize != actual.SFTPConfig.BufferSize {
return errors.New("SFTPFs buffer_size mismatch")
}
+ if expected.SFTPConfig.EqualityCheckMode != actual.SFTPConfig.EqualityCheckMode {
+ return errors.New("SFTPFs equality_check_mode mismatch")
+ }
if err := checkEncryptedSecret(expected.SFTPConfig.Password, actual.SFTPConfig.Password); err != nil {
return fmt.Errorf("SFTPFs password mismatch: %v", err)
}
diff --git a/internal/sftpd/httpfs_test.go b/internal/sftpd/httpfs_test.go
index cfee8fab..dde8dd96 100644
--- a/internal/sftpd/httpfs_test.go
+++ b/internal/sftpd/httpfs_test.go
@@ -198,8 +198,9 @@ func TestHTTPFsVirtualFolder(t *testing.T) {
Provider: sdk.HTTPFilesystemProvider,
HTTPConfig: vfs.HTTPFsConfig{
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
- Endpoint: fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
- Username: defaultHTTPFsUsername,
+ Endpoint: fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
+ Username: defaultHTTPFsUsername,
+ EqualityCheckMode: 1,
},
},
},
@@ -240,6 +241,7 @@ func TestHTTPFsVirtualFolder(t *testing.T) {
func TestHTTPFsWalk(t *testing.T) {
user := getTestUserWithHTTPFs(false)
+ user.FsConfig.HTTPConfig.EqualityCheckMode = 1
httpFs, err := user.GetFilesystem("")
require.NoError(t, err)
basePath := filepath.Join(os.TempDir(), "httpfs", user.FsConfig.HTTPConfig.Username)
diff --git a/internal/sftpd/sftpd_test.go b/internal/sftpd/sftpd_test.go
index 4c93fdc7..4bab99a9 100644
--- a/internal/sftpd/sftpd_test.go
+++ b/internal/sftpd/sftpd_test.go
@@ -5224,8 +5224,9 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
Provider: sdk.SFTPFilesystemProvider,
SFTPConfig: vfs.SFTPFsConfig{
BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
- Endpoint: sftpServerAddr,
- Username: user2.Username,
+ Endpoint: sftpServerAddr,
+ Username: user2.Username,
+ EqualityCheckMode: 1,
},
Password: kms.NewPlainSecret(defaultPassword),
},
diff --git a/internal/vfs/filesystem.go b/internal/vfs/filesystem.go
index 4dc87cec..20e0c826 100644
--- a/internal/vfs/filesystem.go
+++ b/internal/vfs/filesystem.go
@@ -104,23 +104,46 @@ func (f *Filesystem) SetNilSecretsIfEmpty() {
}
// IsEqual returns true if the fs is equal to other
-func (f *Filesystem) IsEqual(other *Filesystem) bool {
+func (f *Filesystem) IsEqual(other Filesystem) bool {
if f.Provider != other.Provider {
return false
}
switch f.Provider {
case sdk.S3FilesystemProvider:
- return f.S3Config.isEqual(&other.S3Config)
+ return f.S3Config.isEqual(other.S3Config)
case sdk.GCSFilesystemProvider:
- return f.GCSConfig.isEqual(&other.GCSConfig)
+ return f.GCSConfig.isEqual(other.GCSConfig)
case sdk.AzureBlobFilesystemProvider:
- return f.AzBlobConfig.isEqual(&other.AzBlobConfig)
+ return f.AzBlobConfig.isEqual(other.AzBlobConfig)
case sdk.CryptedFilesystemProvider:
- return f.CryptConfig.isEqual(&other.CryptConfig)
+ return f.CryptConfig.isEqual(other.CryptConfig)
case sdk.SFTPFilesystemProvider:
- return f.SFTPConfig.isEqual(&other.SFTPConfig)
+ return f.SFTPConfig.isEqual(other.SFTPConfig)
case sdk.HTTPFilesystemProvider:
- return f.HTTPConfig.isEqual(&other.HTTPConfig)
+ return f.HTTPConfig.isEqual(other.HTTPConfig)
+ default:
+ return true
+ }
+}
+
+// IsSameResource returns true if fs point to the same resource as other
+func (f *Filesystem) IsSameResource(other Filesystem) bool {
+ if f.Provider != other.Provider {
+ return false
+ }
+ switch f.Provider {
+ case sdk.S3FilesystemProvider:
+ return f.S3Config.isSameResource(other.S3Config)
+ case sdk.GCSFilesystemProvider:
+ return f.GCSConfig.isSameResource(other.GCSConfig)
+ case sdk.AzureBlobFilesystemProvider:
+ return f.AzBlobConfig.isSameResource(other.AzBlobConfig)
+ case sdk.CryptedFilesystemProvider:
+ return f.CryptConfig.isSameResource(other.CryptConfig)
+ case sdk.SFTPFilesystemProvider:
+ return f.SFTPConfig.isSameResource(other.SFTPConfig)
+ case sdk.HTTPFilesystemProvider:
+ return f.HTTPConfig.isSameResource(other.HTTPConfig)
default:
return true
}
@@ -314,6 +337,7 @@ func (f *Filesystem) GetACopy() Filesystem {
Prefix: f.SFTPConfig.Prefix,
DisableCouncurrentReads: f.SFTPConfig.DisableCouncurrentReads,
BufferSize: f.SFTPConfig.BufferSize,
+ EqualityCheckMode: f.SFTPConfig.EqualityCheckMode,
},
Password: f.SFTPConfig.Password.Clone(),
PrivateKey: f.SFTPConfig.PrivateKey.Clone(),
@@ -321,9 +345,10 @@ func (f *Filesystem) GetACopy() Filesystem {
},
HTTPConfig: HTTPFsConfig{
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
- Endpoint: f.HTTPConfig.Endpoint,
- Username: f.HTTPConfig.Username,
- SkipTLSVerify: f.HTTPConfig.SkipTLSVerify,
+ Endpoint: f.HTTPConfig.Endpoint,
+ Username: f.HTTPConfig.Username,
+ SkipTLSVerify: f.HTTPConfig.SkipTLSVerify,
+ EqualityCheckMode: f.HTTPConfig.EqualityCheckMode,
},
Password: f.HTTPConfig.Password.Clone(),
APIKey: f.HTTPConfig.APIKey.Clone(),
diff --git a/internal/vfs/httpfs.go b/internal/vfs/httpfs.go
index f64474c4..ccca25f4 100644
--- a/internal/vfs/httpfs.go
+++ b/internal/vfs/httpfs.go
@@ -90,7 +90,7 @@ func (c *HTTPFsConfig) setEmptyCredentialsIfNil() {
}
}
-func (c *HTTPFsConfig) isEqual(other *HTTPFsConfig) bool {
+func (c *HTTPFsConfig) isEqual(other HTTPFsConfig) bool {
if c.Endpoint != other.Endpoint {
return false
}
@@ -108,6 +108,15 @@ func (c *HTTPFsConfig) isEqual(other *HTTPFsConfig) bool {
return c.APIKey.IsEqual(other.APIKey)
}
+func (c *HTTPFsConfig) isSameResource(other HTTPFsConfig) bool {
+ if c.EqualityCheckMode > 0 || other.EqualityCheckMode > 0 {
+ if c.Username != other.Username {
+ return false
+ }
+ }
+ return c.Endpoint == other.Endpoint
+}
+
// validate returns an error if the configuration is not valid
func (c *HTTPFsConfig) validate() error {
c.setEmptyCredentialsIfNil()
@@ -128,6 +137,9 @@ func (c *HTTPFsConfig) validate() error {
return fmt.Errorf("httpfs: invalid unix domain socket path: %q", socketPath)
}
}
+ if !isEqualityCheckModeValid(c.EqualityCheckMode) {
+ return errors.New("invalid equality_check_mode")
+ }
if c.Password.IsEncrypted() && !c.Password.IsValid() {
return errors.New("httpfs: invalid encrypted password")
}
@@ -357,6 +369,9 @@ func (fs *HTTPFs) Create(name string, flag int) (File, *PipeWriter, func(), erro
// Rename renames (moves) source to target.
func (fs *HTTPFs) Rename(source, target string) error {
+ if source == target {
+ return nil
+ }
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
diff --git a/internal/vfs/osfs.go b/internal/vfs/osfs.go
index 224b1425..0bf3a548 100644
--- a/internal/vfs/osfs.go
+++ b/internal/vfs/osfs.go
@@ -116,6 +116,9 @@ func (*OsFs) Create(name string, flag int) (File, *PipeWriter, func(), error) {
// Rename renames (moves) source to target
func (fs *OsFs) Rename(source, target string) error {
+ if source == target {
+ return nil
+ }
err := os.Rename(source, target)
if err != nil && isCrossDeviceError(err) {
fsLog(fs, logger.LevelError, "cross device error detected while renaming %#v -> %#v. Trying a copy and remove, this could take a long time",
diff --git a/internal/vfs/sftpfs.go b/internal/vfs/sftpfs.go
index cf650acc..2b23e465 100644
--- a/internal/vfs/sftpfs.go
+++ b/internal/vfs/sftpfs.go
@@ -83,7 +83,7 @@ func (c *SFTPFsConfig) setNilSecretsIfEmpty() {
}
}
-func (c *SFTPFsConfig) isEqual(other *SFTPFsConfig) bool {
+func (c *SFTPFsConfig) isEqual(other SFTPFsConfig) bool {
if c.Endpoint != other.Endpoint {
return false
}
@@ -130,6 +130,15 @@ func (c *SFTPFsConfig) setEmptyCredentialsIfNil() {
}
}
+func (c *SFTPFsConfig) isSameResource(other SFTPFsConfig) bool {
+ if c.EqualityCheckMode > 0 || other.EqualityCheckMode > 0 {
+ if c.Username != other.Username {
+ return false
+ }
+ }
+ return c.Endpoint == other.Endpoint
+}
+
// validate returns an error if the configuration is not valid
func (c *SFTPFsConfig) validate() error {
c.setEmptyCredentialsIfNil()
@@ -146,6 +155,9 @@ func (c *SFTPFsConfig) validate() error {
if c.BufferSize < 0 || c.BufferSize > 16 {
return errors.New("invalid buffer_size, valid range is 0-16")
}
+ if !isEqualityCheckModeValid(c.EqualityCheckMode) {
+ return errors.New("invalid equality_check_mode")
+ }
if err := c.validateCredentials(); err != nil {
return err
}
@@ -378,6 +390,9 @@ func (fs *SFTPFs) Create(name string, flag int) (File, *PipeWriter, func(), erro
// Rename renames (moves) source to target.
func (fs *SFTPFs) Rename(source, target string) error {
+ if source == target {
+ return nil
+ }
if err := fs.checkConnection(); err != nil {
return err
}
diff --git a/internal/vfs/vfs.go b/internal/vfs/vfs.go
index a3b9faf6..e30e7a86 100644
--- a/internal/vfs/vfs.go
+++ b/internal/vfs/vfs.go
@@ -169,7 +169,7 @@ func (c *S3FsConfig) HideConfidentialData() {
}
}
-func (c *S3FsConfig) isEqual(other *S3FsConfig) bool {
+func (c *S3FsConfig) isEqual(other S3FsConfig) bool {
if c.Bucket != other.Bucket {
return false
}
@@ -204,7 +204,7 @@ func (c *S3FsConfig) isEqual(other *S3FsConfig) bool {
return c.isSecretEqual(other)
}
-func (c *S3FsConfig) areMultipartFieldsEqual(other *S3FsConfig) bool {
+func (c *S3FsConfig) areMultipartFieldsEqual(other S3FsConfig) bool {
if c.UploadPartSize != other.UploadPartSize {
return false
}
@@ -226,7 +226,7 @@ func (c *S3FsConfig) areMultipartFieldsEqual(other *S3FsConfig) bool {
return true
}
-func (c *S3FsConfig) isSecretEqual(other *S3FsConfig) bool {
+func (c *S3FsConfig) isSecretEqual(other S3FsConfig) bool {
if c.AccessSecret == nil {
c.AccessSecret = kms.NewEmptySecret()
}
@@ -283,6 +283,16 @@ func (c *S3FsConfig) checkPartSizeAndConcurrency() error {
return nil
}
+func (c *S3FsConfig) isSameResource(other S3FsConfig) bool {
+ if c.Bucket != other.Bucket {
+ return false
+ }
+ if c.Endpoint != other.Endpoint {
+ return false
+ }
+ return c.Region == other.Region
+}
+
// validate returns an error if the configuration is not valid
func (c *S3FsConfig) validate() error {
if c.AccessSecret == nil {
@@ -341,7 +351,7 @@ func (c *GCSFsConfig) ValidateAndEncryptCredentials(additionalData string) error
return nil
}
-func (c *GCSFsConfig) isEqual(other *GCSFsConfig) bool {
+func (c *GCSFsConfig) isEqual(other GCSFsConfig) bool {
if c.Bucket != other.Bucket {
return false
}
@@ -366,6 +376,10 @@ func (c *GCSFsConfig) isEqual(other *GCSFsConfig) bool {
return c.Credentials.IsEqual(other.Credentials)
}
+func (c *GCSFsConfig) isSameResource(other GCSFsConfig) bool {
+ return c.Bucket == other.Bucket
+}
+
// validate returns an error if the configuration is not valid
func (c *GCSFsConfig) validate() error {
if c.Credentials == nil || c.AutomaticCredentials == 1 {
@@ -414,7 +428,7 @@ func (c *AzBlobFsConfig) HideConfidentialData() {
}
}
-func (c *AzBlobFsConfig) isEqual(other *AzBlobFsConfig) bool {
+func (c *AzBlobFsConfig) isEqual(other AzBlobFsConfig) bool {
if c.Container != other.Container {
return false
}
@@ -457,7 +471,7 @@ func (c *AzBlobFsConfig) isEqual(other *AzBlobFsConfig) bool {
return c.isSecretEqual(other)
}
-func (c *AzBlobFsConfig) isSecretEqual(other *AzBlobFsConfig) bool {
+func (c *AzBlobFsConfig) isSecretEqual(other AzBlobFsConfig) bool {
if c.AccountKey == nil {
c.AccountKey = kms.NewEmptySecret()
}
@@ -533,6 +547,16 @@ func (c *AzBlobFsConfig) tryDecrypt() error {
return nil
}
+func (c *AzBlobFsConfig) isSameResource(other AzBlobFsConfig) bool {
+ if c.AccountName != other.AccountName {
+ return false
+ }
+ if c.Endpoint != other.Endpoint {
+ return false
+ }
+ return c.SASURL.GetPayload() == other.SASURL.GetPayload()
+}
+
// validate returns an error if the configuration is not valid
func (c *AzBlobFsConfig) validate() error {
if c.AccountKey == nil {
@@ -578,7 +602,7 @@ func (c *CryptFsConfig) HideConfidentialData() {
}
}
-func (c *CryptFsConfig) isEqual(other *CryptFsConfig) bool {
+func (c *CryptFsConfig) isEqual(other CryptFsConfig) bool {
if c.Passphrase == nil {
c.Passphrase = kms.NewEmptySecret()
}
@@ -602,6 +626,10 @@ func (c *CryptFsConfig) ValidateAndEncryptCredentials(additionalData string) err
return nil
}
+func (c *CryptFsConfig) isSameResource(other CryptFsConfig) bool {
+ return c.Passphrase.GetPayload() == other.Passphrase.GetPayload()
+}
+
// validate returns an error if the configuration is not valid
func (c *CryptFsConfig) validate() error {
if c.Passphrase == nil || c.Passphrase.IsEmpty() {
@@ -656,6 +684,10 @@ func (p *PipeWriter) Write(data []byte) (int, error) {
return p.writer.Write(data)
}
+func isEqualityCheckModeValid(mode int) bool {
+ return mode >= 0 || mode <= 1
+}
+
// IsDirectory checks if a path exists and is a directory
func IsDirectory(fs Fs, path string) (bool, error) {
fileInfo, err := fs.Stat(path)
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 402181f4..6f202046 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -4957,6 +4957,15 @@ components:
maximum: 16
example: 2
description: The size of the buffer (in MB) to use for transfers. By enabling buffering, the reads and writes, from/to the remote SFTP server, are split in multiple concurrent requests and this allows data to be transferred at a faster rate, over high latency networks, by overlapping round-trip times. With buffering enabled, resuming uploads is not supported and a file cannot be opened for both reading and writing at the same time. 0 means disabled.
+ equality_check_mode:
+ type: integer
+ enum:
+ - 0
+ - 1
+ description: |
+ Defines how to check if this config points to the same server as another config. If different configs point to the same server the renaming between the fs configs is allowed:
+ * `0` username and endpoint must match. This is the default
+ * `1` only the endpoint must match
HTTPFsConfig:
type: object
properties:
@@ -4971,6 +4980,15 @@ components:
$ref: '#/components/schemas/Secret'
skip_tls_verify:
type: boolean
+ equality_check_mode:
+ type: integer
+ enum:
+ - 0
+ - 1
+ description: |
+ Defines how to check if this config points to the same server as another config. If different configs point to the same server the renaming between the fs configs is allowed:
+ * `0` username and endpoint must match. This is the default
+ * `1` only the endpoint must match
FilesystemConfig:
type: object
properties:
diff --git a/templates/webadmin/fsconfig.html b/templates/webadmin/fsconfig.html
index eefa3ab1..ff28baa6 100644
--- a/templates/webadmin/fsconfig.html
+++ b/templates/webadmin/fsconfig.html
@@ -481,6 +481,17 @@ along with this program. If not, see