From f4d2271cc1bd6f8075abe24a8113a210272339a9 Mon Sep 17 00:00:00 2001 From: Awuqing <3184394176@qq.com> Date: Tue, 31 Mar 2026 22:52:16 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E4=BC=A0=E8=BE=93=E5=B1=82=E9=9B=86=E6=88=90=20rclone=20?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=E8=87=AA=E7=A0=94=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 8 种存储后端(本地磁盘、S3、WebDAV、Google Drive、FTP、阿里云 OSS、 腾讯云 COS、七牛 Kodo)的底层传输从 4 个独立 SDK 自研实现替换为 rclone fs 接口统一驱动。 - 新建 storage/rclone/ 包(~410 行胶水代码),包含通用 Provider 和 8 种 配置映射 Factory - 删除 10 个旧 provider 包(~1000 行),净减少约 1000 行代码 - StorageProvider 接口、前端 UI、数据库模型、备份执行引擎全部零改动 - 获得 rclone 工业级传输能力(分片上传、断点续传、自动重试) --- server/go.mod | 146 +++-- server/go.sum | 537 +++++++++++++++--- server/internal/app/app.go | 25 +- .../service/backup_execution_service_test.go | 4 +- server/internal/storage/aliyun/factory.go | 66 --- server/internal/storage/ftp/provider.go | 226 -------- .../internal/storage/googledrive/provider.go | 299 ---------- .../storage/googledrive/provider_test.go | 75 --- server/internal/storage/localdisk/provider.go | 137 ----- .../storage/localdisk/provider_test.go | 52 -- server/internal/storage/qiniu/factory.go | 73 --- server/internal/storage/rclone/backends.go | 11 + server/internal/storage/rclone/factory.go | 347 +++++++++++ server/internal/storage/rclone/provider.go | 112 ++++ .../internal/storage/rclone/provider_test.go | 129 +++++ server/internal/storage/s3/provider.go | 126 ---- server/internal/storage/s3/provider_test.go | 78 --- .../internal/storage/s3provider/provider.go | 9 - server/internal/storage/tencent/factory.go | 60 -- server/internal/storage/webdav/provider.go | 126 ---- .../internal/storage/webdav/provider_test.go | 79 --- .../storage/webdavprovider/provider.go | 9 - 22 files changed, 1160 insertions(+), 1566 deletions(-) delete mode 100644 server/internal/storage/aliyun/factory.go delete mode 100644 server/internal/storage/ftp/provider.go delete mode 100644 server/internal/storage/googledrive/provider.go delete mode 100644 server/internal/storage/googledrive/provider_test.go delete mode 100644 server/internal/storage/localdisk/provider.go delete mode 100644 server/internal/storage/localdisk/provider_test.go delete mode 100644 server/internal/storage/qiniu/factory.go create mode 100644 server/internal/storage/rclone/backends.go create mode 100644 server/internal/storage/rclone/factory.go create mode 100644 server/internal/storage/rclone/provider.go create mode 100644 server/internal/storage/rclone/provider_test.go delete mode 100644 server/internal/storage/s3/provider.go delete mode 100644 server/internal/storage/s3/provider_test.go delete mode 100644 server/internal/storage/s3provider/provider.go delete mode 100644 server/internal/storage/tencent/factory.go delete mode 100644 server/internal/storage/webdav/provider.go delete mode 100644 server/internal/storage/webdav/provider_test.go delete mode 100644 server/internal/storage/webdavprovider/provider.go diff --git a/server/go.mod b/server/go.mod index ae4b871..27840eb 100644 --- a/server/go.mod +++ b/server/go.mod @@ -3,93 +3,153 @@ module backupx/server go 1.25.0 require ( - github.com/aws/aws-sdk-go-v2 v1.41.3 - github.com/aws/aws-sdk-go-v2/credentials v1.19.11 - github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 github.com/gin-gonic/gin v1.10.1 github.com/glebarez/sqlite v1.11.0 - github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/natefinch/lumberjack v2.0.0+incompatible + github.com/rclone/rclone v1.73.3 github.com/robfig/cron/v3 v3.0.1 github.com/spf13/viper v1.20.0 - github.com/studio-b12/gowebdav v0.12.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.45.0 - golang.org/x/oauth2 v0.25.0 - google.golang.org/api v0.215.0 + golang.org/x/crypto v0.48.0 + golang.org/x/oauth2 v0.34.0 + google.golang.org/api v0.255.0 gorm.io/gorm v1.25.12 ) require ( - cloud.google.com/go/auth v0.13.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + github.com/Azure/go-ntlmssp v0.0.2-0.20251110135918-10b7b7e7cd26 // indirect github.com/BurntSushi/toml v1.6.0 // indirect + github.com/IBM/go-sdk-core/v5 v5.18.5 // indirect + github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd // indirect + github.com/abbot/go-http-auth v0.4.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect + github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect github.com/aws/smithy-go v1.24.2 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/buger/jsonparser v1.1.2 // indirect + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.6.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/ebitengine/purego v0.9.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.11 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-chi/chi/v5 v5.2.5 // indirect + github.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/errors v0.22.4 // indirect + github.com/go-openapi/strfmt v0.25.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/s2a-go v0.1.8 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/google/pprof v0.0.0-20240509144519-723abb6459b7 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect - github.com/googleapis/gax-go/v2 v2.14.1 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/jlaffaye/ftp v0.2.0 // indirect + github.com/jlaffaye/ftp v0.2.1-0.20240918233326-1b970516f5d3 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/lanrat/extsort v1.4.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncw/swift/v2 v2.0.5 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/onsi/gomega v1.34.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/peterh/liner v1.2.2 // indirect + github.com/pkg/xattr v0.4.12 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rfjakob/eme v1.1.2 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.10 // indirect + github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect - go.opentelemetry.io/otel v1.29.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect - go.opentelemetry.io/otel/trace v1.29.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zeebo/assert v1.3.1 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.mongodb.org/mongo-driver v1.17.6 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect - google.golang.org/grpc v1.67.3 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/arch v0.14.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.22.5 // indirect diff --git a/server/go.sum b/server/go.sum index a840bf4..91b2cf2 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,21 +1,79 @@ -cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= -cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= -cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= -cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8= +github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.5.3 h1:sxgSqOB9CDToiaVFpxuvb5wGgGqWa3lCShcm5o0n3bE= +github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.5.3/go.mod h1:XdED8i399lEVblYHTZM8eXaP07gv4Z58IL6ueMlVlrg= +github.com/Azure/go-ntlmssp v0.0.2-0.20251110135918-10b7b7e7cd26 h1:gy/jrlpp8EfSyA73a51fofoSfhp5rPNQAUvDr4Dm91c= +github.com/Azure/go-ntlmssp v0.0.2-0.20251110135918-10b7b7e7cd26/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/FilenCloudDienste/filen-sdk-go v0.0.37 h1:W8S9TrAyZ4//3PXsU6+Bi+fe/6uIL986GyS7PVzIDL4= +github.com/FilenCloudDienste/filen-sdk-go v0.0.37/go.mod h1:0cBhKXQg49XbKZZfk5TCDa3sVLP+xMxZTWL+7KY0XR0= +github.com/Files-com/files-sdk-go/v3 v3.2.264 h1:lMHTplAYI9FtmCo/QOcpRxmPA5REVAct1r2riQmDQKw= +github.com/Files-com/files-sdk-go/v3 v3.2.264/go.mod h1:wGqkOzRu/ClJibvDgcfuJNAqI2nLhe8g91tPlDKRCdE= +github.com/IBM/go-sdk-core/v5 v5.18.5 h1:g0JRl3sYXJczB/yuDlrN6x22LJ6jIxhp0Sa4ARNW60c= +github.com/IBM/go-sdk-core/v5 v5.18.5/go.mod h1:KonTFRR+8ZSgw5cxBSYo6E4WZoY1+7n1kfHM82VcjFU= +github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE= +github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug= +github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= +github.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e h1:lCsqUUACrcMC83lg5rTo9Y0PnPItE61JSfvMyIcANwk= +github.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= +github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= +github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI= +github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk= +github.com/ProtonMail/gopenpgp/v2 v2.9.0 h1:ruLzBmwe4dR1hdnrsEJ/S7psSBmV15gFttFUPP/+/kE= +github.com/ProtonMail/gopenpgp/v2 v2.9.0/go.mod h1:IldDyh9Hv1ZCCYatTuuEt1XZJ0OPjxLpTarDfglih7s= +github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= +github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= +github.com/a1ex3/zstd-seekable-format-go/pkg v0.10.0 h1:iLDOF0rdGTrol/q8OfPIIs5kLD8XvA2q75o6Uq/tgak= +github.com/a1ex3/zstd-seekable-format-go/pkg v0.10.0/go.mod h1:DrEWcQJjz7t5iF2duaiyhg4jyoF0kxOD6LtECNGkZ/Q= +github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3 h1:hhdWprfSpFbN7lz3W1gM40vOgvSh1WCSMxYD6gGB4Hs= +github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3/go.mod h1:XaUnRxSCYgL3kkgX0QHIV0D+znljPIDImxlv2kbGv0Y= +github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0= +github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM= +github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs= +github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/appscode/go-querystring v0.0.0-20170504095604-0126cfb3f1dc h1:LoL75er+LKDHDUfU5tRvFwxH0LjPpZN8OoG8Ll+liGU= +github.com/appscode/go-querystring v0.0.0-20170504095604-0126cfb3f1dc/go.mod h1:w648aMHEgFYS6xb0KVMMtZ2uMeemhiKCuD2vj6gY52A= github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA= github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y= +github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c= github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc= github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4 h1:2fjfz3/G9BRvIKuNZ655GwzpklC2kEH0cowZQGO7uBg= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4/go.mod h1:Ymws824lvMypLFPwyyUXM52SXuGgxpu0+DISLfKvB+c= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg= @@ -28,188 +86,474 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHe github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= +github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bradenaw/juniper v0.15.3 h1:RHIAMEDTpvmzV1wg1jMAHGOoI2oJUSPx3lxRldXnFGo= +github.com/bradenaw/juniper v0.15.3/go.mod h1:UX4FX57kVSaDp4TPqvSjkAAewmRFAfXf27BOs5z9dq8= +github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= +github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= +github.com/buengese/sgzip v0.1.1 h1:ry+T8l1mlmiWEsDrH/YHZnCVWD2S3im1KLsyO+8ZmTU= +github.com/buengese/sgzip v0.1.1/go.mod h1:i5ZiXGF3fhV7gL1xaRRL1nDnmpNj0X061FQzOS8VMas= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/calebcase/tmpfile v1.0.3 h1:BZrOWZ79gJqQ3XbAQlihYZf/YCV0H4KPIdM5K5oMpJo= +github.com/calebcase/tmpfile v1.0.3/go.mod h1:UAUc01aHeC+pudPagY/lWvt2qS9ZO5Zzof6/tIUzqeI= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chilts/sid v0.0.0-20190607042430-660e94789ec9 h1:z0uK8UQqjMVYzvk4tiiu3obv2B44+XBsvgEJREQfnO8= +github.com/chilts/sid v0.0.0-20190607042430-660e94789ec9/go.mod h1:Jl2neWsQaDanWORdqZ4emBl50J4/aRBBS4FyyG9/PFo= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudinary/cloudinary-go/v2 v2.13.0 h1:ugiQwb7DwpWQnete2AZkTh94MonZKmxD7hDGy1qTzDs= +github.com/cloudinary/cloudinary-go/v2 v2.13.0/go.mod h1:ireC4gqVetsjVhYlwjUJwKTbZuWjEIynbR9zQTlqsvo= +github.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc h1:t8YjNUCt1DimB4HCIXBztwWMhgxr5yG5/YaRl9Afdfg= +github.com/cloudsoda/go-smb2 v0.0.0-20250228001242-d4c70e6251cc/go.mod h1:CgWpFCFWzzEA5hVkhAc6DZZzGd3czx+BblvOzjmg6KA= +github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc h1:0xCWmFKBmarCqqqLeM7jFBSw/Or81UEElFqO8MY+GDs= +github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc/go.mod h1:uvR42Hb/t52HQd7x5/ZLzZEK8oihrFpgnodIJ1vte2E= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/colinmarc/hdfs/v2 v2.4.0 h1:v6R8oBx/Wu9fHpdPoJJjpGSUxo8NhHIwrwsfhFvU9W0= +github.com/colinmarc/hdfs/v2 v2.4.0/go.mod h1:0NAO+/3knbMx6+5pCv+Hcbaz4xn/Zzbn9+WIib2rKVI= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= +github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= +github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= +github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo= +github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8= +github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k= +github.com/dromara/dongle v1.0.1 h1:si/7UP/EXxnFVZok1cNos70GiMGxInAYMilHQFP5dJs= +github.com/dromara/dongle v1.0.1/go.mod h1:ebFhTaDgxaDIKppycENTWlBsxz8mWCPWOLnsEgDpMv4= +github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5 h1:FT+t0UEDykcor4y3dMVKXIiWJETBpRgERYTGlmMd7HU= +github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5/go.mod h1:rSS3kM9XMzSQ6pw91Qgd6yB5jdt70N4OdtrAf74As5M= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg= +github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= +github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg= +github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64= +github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348 h1:JnrjqG5iR07/8k7NqrLNilRsl3s1EPRQEGvbPyOce68= +github.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348/go.mod h1:Czxo/d1g948LtrALAZdL04TL/HnkopquAjxYUuI02bo= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM= +github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= +github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= +github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= -github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= -github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/pprof v0.0.0-20240509144519-723abb6459b7 h1:velgFPYr1X9TDwLIfkV7fWqsFlf7TeP11M/7kPd/dVI= +github.com/google/pprof v0.0.0-20240509144519-723abb6459b7/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= -github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= +github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/internxt/rclone-adapter v0.0.0-20260220172730-613f4cc8b8fd h1:dSIuz2mpJAPQfhHYtG57D0qwSkgC/vQ69gHfeyQ4kxA= +github.com/internxt/rclone-adapter v0.0.0-20260220172730-613f4cc8b8fd/go.mod h1:vdPya4AIcDjvng4ViaAzqjegJf0VHYpYHQguFx5xBp0= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg= -github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI= +github.com/jlaffaye/ftp v0.2.1-0.20240918233326-1b970516f5d3 h1:ZxO6Qr2GOXPdcW80Mcn3nemvilMPvpWqxrNfK2ZnNNs= +github.com/jlaffaye/ftp v0.2.1-0.20240918233326-1b970516f5d3/go.mod h1:dvLUr/8Fs9a2OBrEnCC5duphbkz/k/mSy5OkXg3PAgI= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolio/noiseconn v0.0.0-20231127013910-f6d9ecbf1de7 h1:JcltaO1HXM5S2KYOYcKgAV7slU0xPy1OcvrVgn98sRQ= +github.com/jtolio/noiseconn v0.0.0-20231127013910-f6d9ecbf1de7/go.mod h1:MEkhEPFwP3yudWO0lj6vfYpLIB+3eIcuIW+e0AZzUQk= +github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 h1:G+9t9cEtnC9jFiTxyptEKuNIAbiN5ZCQzX2a74lj3xg= +github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004/go.mod h1:KmHnJWQrgEvbuy0vcvj00gtMqbvNn1L+3YUZLK/B92c= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/koofr/go-httpclient v0.0.0-20240520111329-e20f8f203988 h1:CjEMN21Xkr9+zwPmZPaJJw+apzVbjGL5uK/6g9Q2jGU= +github.com/koofr/go-httpclient v0.0.0-20240520111329-e20f8f203988/go.mod h1:/agobYum3uo/8V6yPVnq+R82pyVGCeuWW5arT4Txn8A= +github.com/koofr/go-koofrclient v0.0.0-20221207135200-cbd7fc9ad6a6 h1:FHVoZMOVRA+6/y4yRlbiR3WvsrOcKBd/f64H7YiWR2U= +github.com/koofr/go-koofrclient v0.0.0-20221207135200-cbd7fc9ad6a6/go.mod h1:MRAz4Gsxd+OzrZ0owwrUHc0zLESL+1Y5syqK/sJxK2A= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lanrat/extsort v1.4.2 h1:akbLIdo4PhNZtvjpaWnbXtGMmLtnGzXplkzfgl+XTTY= +github.com/lanrat/extsort v1.4.2/go.mod h1:hceP6kxKPKebjN1RVrDBXMXXECbaI41Y94tt6MDazc4= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lpar/date v1.0.0 h1:bq/zVqFTUmsxvd/CylidY4Udqpr9BOFrParoP6p0x/I= +github.com/lpar/date v1.0.0/go.mod h1:KjYe0dDyMQTgpqcUz4LEIeM5VZwhggjVx/V2dtc8NSo= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/ncw/swift/v2 v2.0.5 h1:9o5Gsd7bInAFEqsGPcaUdsboMbqf8lnNtxqWKFT9iz8= +github.com/ncw/swift/v2 v2.0.5/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/oracle/oci-go-sdk/v65 v65.104.0 h1:l9awEvzWvxmYhy/97A0hZ87pa7BncYXmcO/S8+rvgK0= +github.com/oracle/oci-go-sdk/v65 v65.104.0/go.mod h1:oB8jFGVc/7/zJ+DbleE8MzGHjhs2ioCz5stRTdZdIcY= +github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= +github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14 h1:XeOYlK9W1uCmhjJSsY78Mcuh7MVkNjTzmHx1yBzizSU= +github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14/go.mod h1:jVblp62SafmidSkvWrXyxAme3gaTfEtWwRPGz5cpvHg= +github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= +github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= +github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= +github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM= +github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= +github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/putdotio/go-putio/putio v0.0.0-20200123120452-16d982cac2b8 h1:Y258uzXU/potCYnQd1r6wlAnoMB68BiCkCcCnKx1SH8= +github.com/putdotio/go-putio/putio v0.0.0-20200123120452-16d982cac2b8/go.mod h1:bSJjRokAHHOhA+XFxplld8w2R/dXLH7Z3BZ532vhFwU= +github.com/rclone/Proton-API-Bridge v1.0.1-0.20260127174007-77f974840d11 h1:4MI2alxM/Ye2gIRBlYf28JGWTipZ4Zz7yAziPKrttjs= +github.com/rclone/Proton-API-Bridge v1.0.1-0.20260127174007-77f974840d11/go.mod h1:3HLX7dwZgvB7nt+Yl/xdzVPcargQ1yBmJEUg3n+jMKM= +github.com/rclone/go-proton-api v1.0.1-0.20260127173028-eb465cac3b18 h1:Lc+d3ISfQaMJKWZOE7z4ZSY4RVmdzbn1B0IM8xN18qM= +github.com/rclone/go-proton-api v1.0.1-0.20260127173028-eb465cac3b18/go.mod h1:LB2kCEaZMzNn3ocdz+qYfxXmuLxxN0ka62KJd2x53Bc= +github.com/rclone/rclone v1.73.3 h1:XKlobcnXxxzxnB6UBSVtRB+UeZmYDV9B4QExVSSGoAY= +github.com/rclone/rclone v1.73.3/go.mod h1:QJDWatpAY9sKGXfpKZUXbThvtHoeo78DcFP2+/cbkvc= +github.com/relvacode/iso8601 v1.7.0 h1:BXy+V60stMP6cpswc+a93Mq3e65PfXCgDFfhvNNGrdo= +github.com/relvacode/iso8601 v1.7.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rfjakob/eme v1.1.2 h1:SxziR8msSOElPayZNFfQw4Tjx/Sbaeeh3eRvrHVMUs4= +github.com/rfjakob/eme v1.1.2/go.mod h1:cVvpasglm/G3ngEfcfT/Wt0GwhkuO32pf/poW6Nyk1k= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA= +github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM= +github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= +github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= +github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spacemonkeygo/monkit/v3 v3.0.25-0.20251022131615-eb24eb109368 h1:GyYC5Ntqk/yy9lEIGE7chdIvt4zP44taycwd9YDSGdc= +github.com/spacemonkeygo/monkit/v3 v3.0.25-0.20251022131615-eb24eb109368/go.mod h1:XkZYGzknZwkD0AKUnZaSXhRiVTLCkq7CWVa3IsE72gA= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM= -github.com/studio-b12/gowebdav v0.12.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/t3rm1n4l/go-mega v0.0.0-20251031123324-a804aaa87491 h1:rrGZv6xYk37hx0tW2sYfgbO0PqStbHqz6Bq6oc9Hurg= +github.com/t3rm1n4l/go-mega v0.0.0-20251031123324-a804aaa87491/go.mod h1:ykucQyiE9Q2qx1wLlEtZkkNn1IURib/2O+Mvd25i1Fo= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/unknwon/goconfig v1.0.0 h1:rS7O+CmUdli1T+oDm7fYj1MwqNWtEJfNj+FqcUHML8U= +github.com/unknwon/goconfig v1.0.0/go.mod h1:qu2ZQ/wcC/if2u32263HTVC39PeOQRSmidQk3DuDFQ8= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yunify/qingstor-sdk-go/v3 v3.2.0 h1:9sB2WZMgjwSUNZhrgvaNGazVltoFUUfuS9f0uCWtTr8= +github.com/yunify/qingstor-sdk-go/v3 v3.2.0/go.mod h1:KciFNuMu6F4WLk9nGwwK69sCGKLCdd9f97ac/wfumS4= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A= +github.com/zeebo/assert v1.3.1/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= +golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= +golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -google.golang.org/api v0.215.0 h1:jdYF4qnyczlEz2ReWIsosNLDuzXyvFHJtI5gcr0J7t0= -google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= -google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8= -google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.255.0 h1:OaF+IbRwOottVCYV2wZan7KUq7UeNUQn1BcPc4K7lE4= +google.golang.org/api v0.255.0/go.mod h1:d1/EtvCLdtiWEV4rAEHDHGh2bCnqsWhw+M8y2ECN4a8= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY= +gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -225,5 +569,18 @@ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= +moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +storj.io/common v0.0.0-20251107171817-6221ae45072c h1:UDXSrdeLJe3QFouavSW10fYdpclK0YNu3KvQHzqq2+k= +storj.io/common v0.0.0-20251107171817-6221ae45072c/go.mod h1:XNX7uykja6aco92y2y8RuqaXIDRPpt1YA2OQDKlKEUk= +storj.io/drpc v0.0.35-0.20250513201419-f7819ea69b55 h1:8OE12DvUnB9lfZcHe7IDGsuhjrY9GBAr964PVHmhsro= +storj.io/drpc v0.0.35-0.20250513201419-f7819ea69b55/go.mod h1:Y9LZaa8esL1PW2IDMqJE7CFSNq7d5bQ3RI7mGPtmKMg= +storj.io/eventkit v0.0.0-20250410172343-61f26d3de156 h1:5MZ0CyMbG6Pi0rRzUWVG6dvpXjbBYEX2oyXuj+tT+sk= +storj.io/eventkit v0.0.0-20250410172343-61f26d3de156/go.mod h1:CpnM6kfZV58dcq3lpbo/IQ4/KoutarnTSHY0GYVwnYw= +storj.io/infectious v0.0.2 h1:rGIdDC/6gNYAStsxsZU79D/MqFjNyJc1tsyyj9sTl7Q= +storj.io/infectious v0.0.2/go.mod h1:QEjKKww28Sjl1x8iDsjBpOM4r1Yp8RsowNcItsZJ1Vs= +storj.io/picobuf v0.0.4 h1:qswHDla+YZ2TovGtMnU4astjvrADSIz84FXRn0qgP6o= +storj.io/picobuf v0.0.4/go.mod h1:hSMxmZc58MS/2qSLy1I0idovlO7+6K47wIGUyRZa6mg= +storj.io/uplink v1.13.1 h1:C8RdW/upALoCyuF16Lod9XGCXEdbJAS+ABQy9JO/0pA= +storj.io/uplink v1.13.1/go.mod h1:x0MQr4UfFsQBwgVWZAtEsLpuwAn6dg7G0Mpne1r516E= diff --git a/server/internal/app/app.go b/server/internal/app/app.go index f9db063..6452813 100644 --- a/server/internal/app/app.go +++ b/server/internal/app/app.go @@ -20,14 +20,7 @@ import ( "backupx/server/internal/service" "backupx/server/internal/storage" "backupx/server/internal/storage/codec" - "backupx/server/internal/storage/googledrive" - "backupx/server/internal/storage/localdisk" - storageAliyun "backupx/server/internal/storage/aliyun" - storageFTP "backupx/server/internal/storage/ftp" - storageTencent "backupx/server/internal/storage/tencent" - storageQiniu "backupx/server/internal/storage/qiniu" - storageS3 "backupx/server/internal/storage/s3" - storageWebDAV "backupx/server/internal/storage/webdav" + storageRclone "backupx/server/internal/storage/rclone" "go.uber.org/zap" "gorm.io/gorm" ) @@ -70,14 +63,14 @@ func New(ctx context.Context, cfg config.Config, version string) (*Application, systemService := service.NewSystemService(cfg, version, time.Now().UTC()) configCipher := codec.NewConfigCipher(resolvedSecurity.EncryptionKey) storageRegistry := storage.NewRegistry( - localdisk.NewFactory(), - storageS3.NewFactory(), - storageWebDAV.NewFactory(), - googledrive.NewFactory(), - storageAliyun.NewFactory(), - storageTencent.NewFactory(), - storageQiniu.NewFactory(), - storageFTP.NewFactory(), + storageRclone.NewLocalDiskFactory(), + storageRclone.NewS3Factory(), + storageRclone.NewWebDAVFactory(), + storageRclone.NewGoogleDriveFactory(), + storageRclone.NewAliyunOSSFactory(), + storageRclone.NewTencentCOSFactory(), + storageRclone.NewQiniuKodoFactory(), + storageRclone.NewFTPFactory(), ) storageTargetService := service.NewStorageTargetService(storageTargetRepo, oauthSessionRepo, storageRegistry, configCipher) storageTargetService.SetBackupTaskRepository(backupTaskRepo) diff --git a/server/internal/service/backup_execution_service_test.go b/server/internal/service/backup_execution_service_test.go index 057539f..99b6074 100644 --- a/server/internal/service/backup_execution_service_test.go +++ b/server/internal/service/backup_execution_service_test.go @@ -15,7 +15,7 @@ import ( "backupx/server/internal/repository" "backupx/server/internal/storage" "backupx/server/internal/storage/codec" - "backupx/server/internal/storage/localdisk" + storageRclone "backupx/server/internal/storage/rclone" ) func newExecutionTestServices(t *testing.T) (*BackupExecutionService, *BackupRecordService, repository.BackupTaskRepository, repository.StorageTargetRepository, repository.BackupRecordRepository, string, string) { @@ -53,7 +53,7 @@ func newExecutionTestServices(t *testing.T) (*BackupExecutionService, *BackupRec } logHub := backup.NewLogHub() runnerRegistry := backup.NewRegistry(backup.NewFileRunner(), backup.NewMySQLRunner(nil), backup.NewSQLiteRunner(), backup.NewPostgreSQLRunner(nil)) - storageRegistry := storage.NewRegistry(localdisk.NewFactory()) + storageRegistry := storage.NewRegistry(storageRclone.NewLocalDiskFactory()) retentionService := backupretention.NewService(records) tempDir := filepath.Join(baseDir, "tmp") if err := os.MkdirAll(tempDir, 0o755); err != nil { diff --git a/server/internal/storage/aliyun/factory.go b/server/internal/storage/aliyun/factory.go deleted file mode 100644 index c222b8f..0000000 --- a/server/internal/storage/aliyun/factory.go +++ /dev/null @@ -1,66 +0,0 @@ -// Package aliyun provides an Aliyun OSS storage factory that delegates to the S3-compatible engine. -// Aliyun OSS is fully S3-compatible; we auto-assemble the endpoint from the user-provided region. -package aliyun - -import ( - "context" - "fmt" - "strings" - - "backupx/server/internal/storage" - "backupx/server/internal/storage/s3" -) - -// Config is the user-facing configuration for Aliyun OSS. -type Config struct { - Region string `json:"region"` - Bucket string `json:"bucket"` - AccessKeyID string `json:"accessKeyId"` - SecretAccessKey string `json:"secretAccessKey"` - Endpoint string `json:"endpoint"` // optional override - InternalNetwork bool `json:"internalNetwork"` // use -internal endpoint -} - -// Factory creates Aliyun OSS providers by composing the S3 engine. -type Factory struct { - s3Factory s3.Factory -} - -func NewFactory() Factory { - return Factory{s3Factory: s3.NewFactory()} -} - -func (Factory) Type() storage.ProviderType { return storage.ProviderTypeAliyunOSS } -func (Factory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} } - -func (f Factory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { - cfg, err := storage.DecodeConfig[Config](rawConfig) - if err != nil { - return nil, err - } - - endpoint := strings.TrimSpace(cfg.Endpoint) - if endpoint == "" { - region := strings.TrimSpace(cfg.Region) - if region == "" { - return nil, fmt.Errorf("aliyun oss region is required") - } - suffix := "aliyuncs.com" - if cfg.InternalNetwork { - endpoint = fmt.Sprintf("https://oss-%s-internal.%s", region, suffix) - } else { - endpoint = fmt.Sprintf("https://oss-%s.%s", region, suffix) - } - } - - // Delegate to S3 engine with assembled endpoint. - s3Config := map[string]any{ - "endpoint": endpoint, - "region": cfg.Region, - "bucket": cfg.Bucket, - "accessKeyId": cfg.AccessKeyID, - "secretAccessKey": cfg.SecretAccessKey, - "forcePathStyle": false, // Aliyun OSS uses virtual-hosted style - } - return f.s3Factory.New(ctx, s3Config) -} diff --git a/server/internal/storage/ftp/provider.go b/server/internal/storage/ftp/provider.go deleted file mode 100644 index 4b05748..0000000 --- a/server/internal/storage/ftp/provider.go +++ /dev/null @@ -1,226 +0,0 @@ -package ftp - -import ( - "bytes" - "context" - "fmt" - "io" - "path" - "strings" - "time" - - "backupx/server/internal/storage" - - "github.com/jlaffaye/ftp" -) - -// Provider implements storage.StorageProvider for FTP. -type Provider struct { - config storage.FTPConfig -} - -// Factory creates FTP storage providers. -type Factory struct{} - -// NewFactory returns a new FTP Factory. -func NewFactory() Factory { - return Factory{} -} - -func (Factory) Type() storage.ProviderType { return storage.ProviderTypeFTP } -func (Factory) SensitiveFields() []string { return []string{"username", "password"} } - -func (f Factory) New(_ context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { - cfg, err := storage.DecodeConfig[storage.FTPConfig](rawConfig) - if err != nil { - return nil, err - } - if strings.TrimSpace(cfg.Host) == "" { - return nil, fmt.Errorf("FTP host is required") - } - if cfg.Port == 0 { - cfg.Port = 21 - } - return &Provider{config: cfg}, nil -} - -func (p *Provider) Type() storage.ProviderType { return storage.ProviderTypeFTP } - -// dial establishes a connection to the FTP server and logs in. -func (p *Provider) dial() (*ftp.ServerConn, error) { - addr := fmt.Sprintf("%s:%d", p.config.Host, p.config.Port) - - var opts []ftp.DialOption - opts = append(opts, ftp.DialWithTimeout(30*time.Second)) - if p.config.UseTLS { - opts = append(opts, ftp.DialWithExplicitTLS(nil)) - } - - conn, err := ftp.Dial(addr, opts...) - if err != nil { - return nil, fmt.Errorf("connect to FTP server %s: %w", addr, err) - } - - username := p.config.Username - if username == "" { - username = "anonymous" - } - if err := conn.Login(username, p.config.Password); err != nil { - conn.Quit() - return nil, fmt.Errorf("FTP login: %w", err) - } - - return conn, nil -} - -func (p *Provider) TestConnection(_ context.Context) error { - conn, err := p.dial() - if err != nil { - return err - } - defer conn.Quit() - - basePath := p.normalizeBasePath() - if err := p.ensureDir(conn, basePath); err != nil { - return fmt.Errorf("ensure FTP base path: %w", err) - } - _, err = conn.List(basePath) - if err != nil { - return fmt.Errorf("list FTP base path: %w", err) - } - return nil -} - -func (p *Provider) Upload(_ context.Context, objectKey string, reader io.Reader, _ int64, _ map[string]string) error { - conn, err := p.dial() - if err != nil { - return err - } - defer conn.Quit() - - objectPath := p.resolvePath(objectKey) - dir := path.Dir(objectPath) - if err := p.ensureDir(conn, dir); err != nil { - return fmt.Errorf("create FTP directories: %w", err) - } - - // Read all data into buffer since FTP STOR needs the full stream - data, err := io.ReadAll(reader) - if err != nil { - return fmt.Errorf("read upload data: %w", err) - } - - if err := conn.Stor(objectPath, bytes.NewReader(data)); err != nil { - return fmt.Errorf("FTP upload: %w", err) - } - return nil -} - -func (p *Provider) Download(_ context.Context, objectKey string) (io.ReadCloser, error) { - conn, err := p.dial() - if err != nil { - return nil, err - } - - objectPath := p.resolvePath(objectKey) - resp, err := conn.Retr(objectPath) - if err != nil { - conn.Quit() - return nil, fmt.Errorf("FTP download: %w", err) - } - - // Wrap the response to also close the FTP connection when done - return &ftpReadCloser{ReadCloser: resp, conn: conn}, nil -} - -func (p *Provider) Delete(_ context.Context, objectKey string) error { - conn, err := p.dial() - if err != nil { - return err - } - defer conn.Quit() - - objectPath := p.resolvePath(objectKey) - if err := conn.Delete(objectPath); err != nil { - return fmt.Errorf("FTP delete: %w", err) - } - return nil -} - -func (p *Provider) List(_ context.Context, prefix string) ([]storage.ObjectInfo, error) { - conn, err := p.dial() - if err != nil { - return nil, err - } - defer conn.Quit() - - basePath := p.normalizeBasePath() - entries, err := conn.List(basePath) - if err != nil { - return nil, fmt.Errorf("FTP list: %w", err) - } - - items := make([]storage.ObjectInfo, 0, len(entries)) - for _, entry := range entries { - if entry.Type == ftp.EntryTypeFolder { - continue - } - key := strings.TrimPrefix(path.Join(strings.TrimPrefix(basePath, "/"), entry.Name), "/") - if prefix != "" && !strings.HasPrefix(key, prefix) { - continue - } - items = append(items, storage.ObjectInfo{ - Key: key, - Size: int64(entry.Size), - UpdatedAt: entry.Time.UTC(), - }) - } - return items, nil -} - -// normalizeBasePath returns a cleaned base path with leading slash. -func (p *Provider) normalizeBasePath() string { - clean := path.Clean("/" + strings.TrimSpace(p.config.BasePath)) - if clean == "." { - return "/" - } - return clean -} - -// resolvePath returns the full FTP path for the given object key. -func (p *Provider) resolvePath(objectKey string) string { - cleanKey := path.Clean("/" + strings.TrimSpace(objectKey)) - return path.Clean(path.Join(p.normalizeBasePath(), cleanKey)) -} - -// ensureDir creates all directories in the path recursively. -func (p *Provider) ensureDir(conn *ftp.ServerConn, dirPath string) error { - parts := strings.Split(strings.Trim(dirPath, "/"), "/") - current := "" - for _, part := range parts { - if part == "" { - continue - } - current = current + "/" + part - if err := conn.MakeDir(current); err != nil { - // Ignore errors if directory already exists - // FTP doesn't have a standard "mkdir if not exists" - _ = err - } - } - return nil -} - -// ftpReadCloser wraps an io.ReadCloser from FTP and closes the connection when done. -type ftpReadCloser struct { - io.ReadCloser - conn *ftp.ServerConn -} - -func (f *ftpReadCloser) Close() error { - err := f.ReadCloser.Close() - if f.conn != nil { - f.conn.Quit() - } - return err -} diff --git a/server/internal/storage/googledrive/provider.go b/server/internal/storage/googledrive/provider.go deleted file mode 100644 index a68b9f1..0000000 --- a/server/internal/storage/googledrive/provider.go +++ /dev/null @@ -1,299 +0,0 @@ -package googledrive - -import ( - "context" - "fmt" - "io" - "path" - "strings" - "time" - - "backupx/server/internal/storage" - "golang.org/x/oauth2" - googleoauth "golang.org/x/oauth2/google" - "google.golang.org/api/drive/v3" - "google.golang.org/api/option" -) - - -type fileInfo struct { - ID string - Name string - Size int64 - ModifiedTime time.Time -} - -type client interface { - TestConnection(context.Context, string) error - Upload(context.Context, string, string, io.Reader) error - Download(context.Context, string, string) (io.ReadCloser, error) - Delete(context.Context, string, string) error - List(context.Context, string, string) ([]storage.ObjectInfo, error) - EnsureFolder(ctx context.Context, parentID, name string) (string, error) -} - -type Provider struct { - client client - rootFolder string // user-configured folderId, empty means Drive root - folderCache map[string]string // cache: path -> folderID -} - -type Factory struct { - newClient func(context.Context, storage.GoogleDriveConfig) (client, error) -} - -func NewFactory() Factory { - return Factory{newClient: newDriveClient} -} - -func (Factory) Type() storage.ProviderType { return storage.ProviderTypeGoogleDrive } -func (Factory) SensitiveFields() []string { - return []string{"clientId", "clientSecret", "refreshToken"} -} - -func (f Factory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { - cfg, err := storage.DecodeConfig[storage.GoogleDriveConfig](rawConfig) - if err != nil { - return nil, err - } - cfg = cfg.Normalize() - if strings.TrimSpace(cfg.ClientID) == "" || strings.TrimSpace(cfg.ClientSecret) == "" { - return nil, fmt.Errorf("google drive client credentials are required") - } - if strings.TrimSpace(cfg.RefreshToken) == "" { - return nil, fmt.Errorf("google drive refresh token is required") - } - newClient := f.newClient - if newClient == nil { - newClient = NewFactory().newClient - } - client, err := newClient(ctx, cfg) - if err != nil { - return nil, err - } - return &Provider{ - client: client, - rootFolder: strings.TrimSpace(cfg.FolderID), - folderCache: make(map[string]string), - }, nil -} - -func (p *Provider) Type() storage.ProviderType { return storage.ProviderTypeGoogleDrive } - -// ensureFolderPath creates nested folders for a path like "BackupX/file/260308" -// and returns the deepest folder's ID. -func (p *Provider) ensureFolderPath(ctx context.Context, folderPath string) (string, error) { - if folderPath == "" || folderPath == "." { - return p.rootFolder, nil - } - if cached, ok := p.folderCache[folderPath]; ok { - return cached, nil - } - parts := strings.Split(path.Clean(folderPath), "/") - parentID := p.rootFolder - builtPath := "" - for _, part := range parts { - if part == "" || part == "." { - continue - } - if builtPath == "" { - builtPath = part - } else { - builtPath = builtPath + "/" + part - } - if cached, ok := p.folderCache[builtPath]; ok { - parentID = cached - continue - } - folderID, err := p.client.EnsureFolder(ctx, parentID, part) - if err != nil { - return "", fmt.Errorf("ensure folder %s: %w", builtPath, err) - } - p.folderCache[builtPath] = folderID - parentID = folderID - } - return parentID, nil -} - -func (p *Provider) TestConnection(ctx context.Context) error { - return p.client.TestConnection(ctx, p.rootFolder) -} - -func (p *Provider) Upload(ctx context.Context, objectKey string, reader io.Reader, _ int64, _ map[string]string) error { - dir := path.Dir(objectKey) - folderID, err := p.ensureFolderPath(ctx, dir) - if err != nil { - return err - } - return p.client.Upload(ctx, folderID, objectKey, reader) -} - -func (p *Provider) Download(ctx context.Context, objectKey string) (io.ReadCloser, error) { - dir := path.Dir(objectKey) - folderID, err := p.ensureFolderPath(ctx, dir) - if err != nil { - return nil, err - } - return p.client.Download(ctx, folderID, objectKey) -} - -func (p *Provider) Delete(ctx context.Context, objectKey string) error { - dir := path.Dir(objectKey) - folderID, err := p.ensureFolderPath(ctx, dir) - if err != nil { - return err - } - return p.client.Delete(ctx, folderID, objectKey) -} - -func (p *Provider) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) { - dir := path.Dir(prefix) - folderID, err := p.ensureFolderPath(ctx, dir) - if err != nil { - return nil, err - } - return p.client.List(ctx, folderID, prefix) -} - -type driveClient struct { - service *drive.Service -} - -func newDriveClient(ctx context.Context, cfg storage.GoogleDriveConfig) (client, error) { - cfg = cfg.Normalize() - oauthCfg := &oauth2.Config{ - ClientID: cfg.ClientID, - ClientSecret: cfg.ClientSecret, - RedirectURL: cfg.RedirectURL, - Endpoint: googleoauth.Endpoint, - Scopes: []string{drive.DriveScope}, - } - httpClient := oauthCfg.Client(ctx, &oauth2.Token{RefreshToken: cfg.RefreshToken}) - service, err := drive.NewService(ctx, option.WithHTTPClient(httpClient)) - if err != nil { - return nil, fmt.Errorf("create google drive service: %w", err) - } - return &driveClient{service: service}, nil -} - -func (c *driveClient) TestConnection(ctx context.Context, folderID string) error { - if strings.TrimSpace(folderID) == "" { - _, err := c.service.About.Get().Fields("user").Context(ctx).Do() - if err != nil { - return fmt.Errorf("test google drive connection: %w", err) - } - return nil - } - _, err := c.service.Files.Get(folderID).Fields("id").Context(ctx).Do() - if err != nil { - return fmt.Errorf("test google drive folder: %w", err) - } - return nil -} - -func (c *driveClient) EnsureFolder(ctx context.Context, parentID, name string) (string, error) { - // Search for existing folder - query := fmt.Sprintf("name = '%s' and mimeType = 'application/vnd.google-apps.folder' and trashed = false", escapeQuery(name)) - if strings.TrimSpace(parentID) != "" { - query += fmt.Sprintf(" and '%s' in parents", escapeQuery(parentID)) - } else { - query += " and 'root' in parents" - } - result, err := c.service.Files.List().Q(query).PageSize(1).Fields("files(id)").Context(ctx).Do() - if err != nil { - return "", fmt.Errorf("search for folder %s: %w", name, err) - } - if len(result.Files) > 0 { - return result.Files[0].Id, nil - } - // Create the folder - folder := &drive.File{ - Name: name, - MimeType: "application/vnd.google-apps.folder", - } - if strings.TrimSpace(parentID) != "" { - folder.Parents = []string{parentID} - } - created, err := c.service.Files.Create(folder).Fields("id").Context(ctx).Do() - if err != nil { - return "", fmt.Errorf("create folder %s: %w", name, err) - } - return created.Id, nil -} - -func (c *driveClient) Upload(ctx context.Context, folderID, objectKey string, reader io.Reader) error { - file := &drive.File{Name: path.Base(objectKey)} - if strings.TrimSpace(folderID) != "" { - file.Parents = []string{folderID} - } - _, err := c.service.Files.Create(file).Media(reader).Context(ctx).Do() - if err != nil { - return fmt.Errorf("upload google drive object: %w", err) - } - return nil -} - -func (c *driveClient) Download(ctx context.Context, folderID, objectKey string) (io.ReadCloser, error) { - file, err := c.findFile(ctx, folderID, objectKey) - if err != nil { - return nil, err - } - response, err := c.service.Files.Get(file.ID).Context(ctx).Download() - if err != nil { - return nil, fmt.Errorf("download google drive object: %w", err) - } - return response.Body, nil -} - -func (c *driveClient) Delete(ctx context.Context, folderID, objectKey string) error { - file, err := c.findFile(ctx, folderID, objectKey) - if err != nil { - return err - } - if err := c.service.Files.Delete(file.ID).Context(ctx).Do(); err != nil { - return fmt.Errorf("delete google drive object: %w", err) - } - return nil -} - -func (c *driveClient) List(ctx context.Context, folderID, prefix string) ([]storage.ObjectInfo, error) { - query := "trashed = false" - if strings.TrimSpace(folderID) != "" { - query += fmt.Sprintf(" and '%s' in parents", escapeQuery(folderID)) - } - if strings.TrimSpace(prefix) != "" { - query += fmt.Sprintf(" and name contains '%s'", escapeQuery(prefix)) - } - result, err := c.service.Files.List().Q(query).Fields("files(id,name,size,modifiedTime)").Context(ctx).Do() - if err != nil { - return nil, fmt.Errorf("list google drive objects: %w", err) - } - items := make([]storage.ObjectInfo, 0, len(result.Files)) - for _, file := range result.Files { - modifiedAt, _ := time.Parse(time.RFC3339, file.ModifiedTime) - items = append(items, storage.ObjectInfo{Key: file.Name, Size: file.Size, UpdatedAt: modifiedAt.UTC()}) - } - return items, nil -} - -func (c *driveClient) findFile(ctx context.Context, folderID, objectKey string) (*fileInfo, error) { - query := fmt.Sprintf("name = '%s' and trashed = false", escapeQuery(path.Base(objectKey))) - if strings.TrimSpace(folderID) != "" { - query += fmt.Sprintf(" and '%s' in parents", escapeQuery(folderID)) - } - result, err := c.service.Files.List().Q(query).PageSize(1).Fields("files(id,name,size,modifiedTime)").Context(ctx).Do() - if err != nil { - return nil, fmt.Errorf("query google drive object: %w", err) - } - if len(result.Files) == 0 { - return nil, fmt.Errorf("google drive object not found: %s", objectKey) - } - file := result.Files[0] - modifiedAt, _ := time.Parse(time.RFC3339, file.ModifiedTime) - return &fileInfo{ID: file.Id, Name: file.Name, Size: file.Size, ModifiedTime: modifiedAt.UTC()}, nil -} - -func escapeQuery(value string) string { - return strings.ReplaceAll(value, "'", "\\'") -} - diff --git a/server/internal/storage/googledrive/provider_test.go b/server/internal/storage/googledrive/provider_test.go deleted file mode 100644 index 0320788..0000000 --- a/server/internal/storage/googledrive/provider_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package googledrive - -import ( - "context" - "io" - "strings" - "testing" - "time" - - "backupx/server/internal/storage" -) - -type fakeClient struct{ data map[string]string } - -func (c *fakeClient) TestConnection(context.Context, string) error { return nil } -func (c *fakeClient) Upload(_ context.Context, _ string, objectKey string, reader io.Reader) error { - content, _ := io.ReadAll(reader) - c.data[objectKey] = string(content) - return nil -} -func (c *fakeClient) Download(_ context.Context, _ string, objectKey string) (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader(c.data[objectKey])), nil -} -func (c *fakeClient) Delete(_ context.Context, _ string, objectKey string) error { - delete(c.data, objectKey) - return nil -} -func (c *fakeClient) List(_ context.Context, _ string, prefix string) ([]storage.ObjectInfo, error) { - items := make([]storage.ObjectInfo, 0) - for key, value := range c.data { - if prefix == "" || strings.HasPrefix(key, prefix) { - items = append(items, storage.ObjectInfo{Key: key, Size: int64(len(value)), UpdatedAt: time.Now().UTC()}) - } - } - return items, nil -} -func (c *fakeClient) EnsureFolder(_ context.Context, _, name string) (string, error) { - return "fake-folder-" + name, nil -} - -func TestGoogleDriveProviderCRUD(t *testing.T) { - factory := Factory{newClient: func(context.Context, storage.GoogleDriveConfig) (client, error) { - return &fakeClient{data: make(map[string]string)}, nil - }} - providerAny, err := factory.New(context.Background(), map[string]any{"clientId": "id", "clientSecret": "secret", "refreshToken": "refresh"}) - if err != nil { - t.Fatalf("Factory.New returned error: %v", err) - } - provider := providerAny.(*Provider) - if err := provider.TestConnection(context.Background()); err != nil { - t.Fatalf("TestConnection returned error: %v", err) - } - if err := provider.Upload(context.Background(), "backup.tar.gz", strings.NewReader("payload"), 7, nil); err != nil { - t.Fatalf("Upload returned error: %v", err) - } - reader, err := provider.Download(context.Background(), "backup.tar.gz") - if err != nil { - t.Fatalf("Download returned error: %v", err) - } - defer reader.Close() - content, _ := io.ReadAll(reader) - if string(content) != "payload" { - t.Fatalf("unexpected content: %s", string(content)) - } - items, err := provider.List(context.Background(), "backup") - if err != nil { - t.Fatalf("List returned error: %v", err) - } - if len(items) != 1 || items[0].Key != "backup.tar.gz" { - t.Fatalf("unexpected list result: %#v", items) - } - if err := provider.Delete(context.Background(), "backup.tar.gz"); err != nil { - t.Fatalf("Delete returned error: %v", err) - } -} diff --git a/server/internal/storage/localdisk/provider.go b/server/internal/storage/localdisk/provider.go deleted file mode 100644 index 6a0bd28..0000000 --- a/server/internal/storage/localdisk/provider.go +++ /dev/null @@ -1,137 +0,0 @@ -package localdisk - -import ( - "context" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - "strings" - - "backupx/server/internal/storage" -) - -type Provider struct { - basePath string -} - -type Factory struct{} - -func NewFactory() Factory { return Factory{} } - -func (Factory) Type() storage.ProviderType { return storage.ProviderTypeLocalDisk } -func (Factory) SensitiveFields() []string { return nil } - -func (Factory) New(_ context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { - cfg, err := storage.DecodeConfig[storage.LocalDiskConfig](rawConfig) - if err != nil { - return nil, err - } - if strings.TrimSpace(cfg.BasePath) == "" { - return nil, fmt.Errorf("local disk basePath is required") - } - return &Provider{basePath: filepath.Clean(cfg.BasePath)}, nil -} - -func (p *Provider) Type() storage.ProviderType { return storage.ProviderTypeLocalDisk } - -func (p *Provider) TestConnection(_ context.Context) error { - if err := os.MkdirAll(p.basePath, 0o755); err != nil { - return fmt.Errorf("ensure local disk base path: %w", err) - } - tempFile, err := os.CreateTemp(p.basePath, ".backupx-connection-test-*") - if err != nil { - return fmt.Errorf("write access check failed: %w", err) - } - name := tempFile.Name() - _ = tempFile.Close() - _ = os.Remove(name) - return nil -} - -func (p *Provider) Upload(_ context.Context, objectKey string, reader io.Reader, _ int64, _ map[string]string) error { - targetPath, err := p.resolvePath(objectKey) - if err != nil { - return err - } - if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { - return fmt.Errorf("create local disk directories: %w", err) - } - file, err := os.Create(targetPath) - if err != nil { - return fmt.Errorf("create local disk object: %w", err) - } - defer file.Close() - if _, err := io.Copy(file, reader); err != nil { - return fmt.Errorf("write local disk object: %w", err) - } - return nil -} - -func (p *Provider) Download(_ context.Context, objectKey string) (io.ReadCloser, error) { - targetPath, err := p.resolvePath(objectKey) - if err != nil { - return nil, err - } - file, err := os.Open(targetPath) - if err != nil { - return nil, fmt.Errorf("open local disk object: %w", err) - } - return file, nil -} - -func (p *Provider) Delete(_ context.Context, objectKey string) error { - targetPath, err := p.resolvePath(objectKey) - if err != nil { - return err - } - if err := os.Remove(targetPath); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("delete local disk object: %w", err) - } - return nil -} - -func (p *Provider) List(_ context.Context, prefix string) ([]storage.ObjectInfo, error) { - items := make([]storage.ObjectInfo, 0) - err := filepath.WalkDir(p.basePath, func(path string, entry fs.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - if entry.IsDir() { - return nil - } - rel, err := filepath.Rel(p.basePath, path) - if err != nil { - return err - } - key := filepath.ToSlash(rel) - if prefix != "" && !strings.HasPrefix(key, prefix) { - return nil - } - info, err := entry.Info() - if err != nil { - return err - } - items = append(items, storage.ObjectInfo{Key: key, Size: info.Size(), UpdatedAt: info.ModTime().UTC()}) - return nil - }) - if err != nil && !os.IsNotExist(err) { - return nil, fmt.Errorf("list local disk objects: %w", err) - } - return items, nil -} - -func (p *Provider) resolvePath(objectKey string) (string, error) { - cleanBase := filepath.Clean(p.basePath) - cleanKey := filepath.Clean(filepath.FromSlash(strings.TrimSpace(objectKey))) - if cleanKey == "." || cleanKey == string(filepath.Separator) || cleanKey == "" { - return "", fmt.Errorf("object key is required") - } - fullPath := filepath.Clean(filepath.Join(cleanBase, cleanKey)) - baseWithSep := cleanBase + string(filepath.Separator) - if fullPath != cleanBase && !strings.HasPrefix(fullPath, baseWithSep) { - return "", fmt.Errorf("object key escapes base path") - } - return fullPath, nil -} diff --git a/server/internal/storage/localdisk/provider_test.go b/server/internal/storage/localdisk/provider_test.go deleted file mode 100644 index c6e8d49..0000000 --- a/server/internal/storage/localdisk/provider_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package localdisk - -import ( - "context" - "io" - "strings" - "testing" -) - -func TestLocalDiskProviderCRUD(t *testing.T) { - providerAny, err := (Factory{}).New(context.Background(), map[string]any{"basePath": t.TempDir()}) - if err != nil { - t.Fatalf("Factory.New returned error: %v", err) - } - provider := providerAny.(*Provider) - if err := provider.TestConnection(context.Background()); err != nil { - t.Fatalf("TestConnection returned error: %v", err) - } - if err := provider.Upload(context.Background(), "daily/backup.txt", strings.NewReader("hello"), 5, nil); err != nil { - t.Fatalf("Upload returned error: %v", err) - } - reader, err := provider.Download(context.Background(), "daily/backup.txt") - if err != nil { - t.Fatalf("Download returned error: %v", err) - } - defer reader.Close() - content, _ := io.ReadAll(reader) - if string(content) != "hello" { - t.Fatalf("expected downloaded content to match, got %s", string(content)) - } - items, err := provider.List(context.Background(), "daily") - if err != nil { - t.Fatalf("List returned error: %v", err) - } - if len(items) != 1 || items[0].Key != "daily/backup.txt" { - t.Fatalf("unexpected list result: %#v", items) - } - if err := provider.Delete(context.Background(), "daily/backup.txt"); err != nil { - t.Fatalf("Delete returned error: %v", err) - } -} - -func TestLocalDiskProviderRejectsTraversal(t *testing.T) { - providerAny, err := (Factory{}).New(context.Background(), map[string]any{"basePath": t.TempDir()}) - if err != nil { - t.Fatalf("Factory.New returned error: %v", err) - } - provider := providerAny.(*Provider) - if _, err := provider.resolvePath("../escape.txt"); err == nil { - t.Fatalf("expected traversal to be rejected") - } -} diff --git a/server/internal/storage/qiniu/factory.go b/server/internal/storage/qiniu/factory.go deleted file mode 100644 index 6872a05..0000000 --- a/server/internal/storage/qiniu/factory.go +++ /dev/null @@ -1,73 +0,0 @@ -// Package qiniu provides a Qiniu Cloud Kodo storage factory that delegates to the S3-compatible engine. -// Qiniu Kodo is S3-compatible; we auto-assemble the endpoint from the user-provided region. -package qiniu - -import ( - "context" - "fmt" - "strings" - - "backupx/server/internal/storage" - "backupx/server/internal/storage/s3" -) - -// Config is the user-facing configuration for Qiniu Kodo. -type Config struct { - Region string `json:"region"` // e.g. z0, z1, z2, na0, as0 - Bucket string `json:"bucket"` - AccessKey string `json:"accessKeyId"` - SecretKey string `json:"secretAccessKey"` - Endpoint string `json:"endpoint"` // optional override -} - -// regionEndpoints maps Qiniu storage region codes to their S3-compatible endpoints. -var regionEndpoints = map[string]string{ - "z0": "https://s3-cn-east-1.qiniucs.com", - "cn-east-2": "https://s3-cn-east-2.qiniucs.com", - "z1": "https://s3-cn-north-1.qiniucs.com", - "z2": "https://s3-cn-south-1.qiniucs.com", - "na0": "https://s3-us-north-1.qiniucs.com", - "as0": "https://s3-ap-southeast-1.qiniucs.com", -} - -// Factory creates Qiniu Kodo providers by composing the S3 engine. -type Factory struct { - s3Factory s3.Factory -} - -func NewFactory() Factory { - return Factory{s3Factory: s3.NewFactory()} -} - -func (Factory) Type() storage.ProviderType { return storage.ProviderTypeQiniuKodo } -func (Factory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} } - -func (f Factory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { - cfg, err := storage.DecodeConfig[Config](rawConfig) - if err != nil { - return nil, err - } - - endpoint := strings.TrimSpace(cfg.Endpoint) - if endpoint == "" { - region := strings.TrimSpace(cfg.Region) - if region == "" { - return nil, fmt.Errorf("qiniu kodo region is required") - } - var ok bool - endpoint, ok = regionEndpoints[region] - if !ok { - return nil, fmt.Errorf("unsupported qiniu region: %s (supported: z0, cn-east-2, z1, z2, na0, as0)", region) - } - } - - s3Config := map[string]any{ - "endpoint": endpoint, - "region": cfg.Region, - "bucket": cfg.Bucket, - "accessKeyId": cfg.AccessKey, - "secretAccessKey": cfg.SecretKey, - "forcePathStyle": true, // Qiniu S3-compatible uses path-style - } - return f.s3Factory.New(ctx, s3Config) -} diff --git a/server/internal/storage/rclone/backends.go b/server/internal/storage/rclone/backends.go new file mode 100644 index 0000000..3084f92 --- /dev/null +++ b/server/internal/storage/rclone/backends.go @@ -0,0 +1,11 @@ +// Package rclone 提供基于 rclone 的统一存储后端实现。 +// 按需引入 rclone backend,避免 backend/all 导致二进制膨胀。 +package rclone + +import ( + _ "github.com/rclone/rclone/backend/drive" + _ "github.com/rclone/rclone/backend/ftp" + _ "github.com/rclone/rclone/backend/local" + _ "github.com/rclone/rclone/backend/s3" + _ "github.com/rclone/rclone/backend/webdav" +) diff --git a/server/internal/storage/rclone/factory.go b/server/internal/storage/rclone/factory.go new file mode 100644 index 0000000..63739b8 --- /dev/null +++ b/server/internal/storage/rclone/factory.go @@ -0,0 +1,347 @@ +package rclone + +import ( + "context" + "fmt" + "strings" + + "backupx/server/internal/storage" + + "github.com/rclone/rclone/fs" +) + +// --------------------------------------------------------------------------- +// 辅助函数 +// --------------------------------------------------------------------------- + +// quoteParam 对 rclone 连接字符串中含特殊字符的值加单引号保护。 +func quoteParam(s string) string { + if s == "" { + return s + } + if !strings.ContainsAny(s, ",:='") { + return s + } + return "'" + strings.ReplaceAll(s, "'", "''") + "'" +} + +// newFs 创建 rclone fs.Fs 实例并包装为 Provider。 +func newFs(ctx context.Context, providerType storage.ProviderType, remote string) (*Provider, error) { + rfs, err := fs.NewFs(ctx, remote) + if err != nil { + return nil, fmt.Errorf("create rclone fs for %s: %w", providerType, err) + } + return newProvider(providerType, rfs), nil +} + +// --------------------------------------------------------------------------- +// LocalDisk +// --------------------------------------------------------------------------- + +type LocalDiskFactory struct{} + +func NewLocalDiskFactory() LocalDiskFactory { return LocalDiskFactory{} } + +func (LocalDiskFactory) Type() storage.ProviderType { return storage.ProviderTypeLocalDisk } +func (LocalDiskFactory) SensitiveFields() []string { return nil } + +func (LocalDiskFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { + cfg, err := storage.DecodeConfig[storage.LocalDiskConfig](rawConfig) + if err != nil { + return nil, err + } + basePath := strings.TrimSpace(cfg.BasePath) + if basePath == "" { + return nil, fmt.Errorf("local disk basePath is required") + } + return newFs(ctx, storage.ProviderTypeLocalDisk, basePath) +} + +// --------------------------------------------------------------------------- +// S3 +// --------------------------------------------------------------------------- + +type S3Factory struct{} + +func NewS3Factory() S3Factory { return S3Factory{} } + +func (S3Factory) Type() storage.ProviderType { return storage.ProviderTypeS3 } +func (S3Factory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} } + +func (S3Factory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { + cfg, err := storage.DecodeConfig[storage.S3Config](rawConfig) + if err != nil { + return nil, err + } + if strings.TrimSpace(cfg.Bucket) == "" { + return nil, fmt.Errorf("s3 bucket is required") + } + if strings.TrimSpace(cfg.AccessKeyID) == "" || strings.TrimSpace(cfg.SecretAccessKey) == "" { + return nil, fmt.Errorf("s3 credentials are required") + } + return newFs(ctx, storage.ProviderTypeS3, buildS3Remote("Other", cfg.AccessKeyID, cfg.SecretAccessKey, cfg.Endpoint, cfg.Region, cfg.Bucket, cfg.ForcePathStyle)) +} + +// buildS3Remote 构建 S3 兼容存储的 rclone 连接字符串。 +func buildS3Remote(provider, keyID, secret, endpoint, region, bucket string, pathStyle bool) string { + var b strings.Builder + b.WriteString(":s3,provider=") + b.WriteString(quoteParam(provider)) + b.WriteString(",access_key_id=") + b.WriteString(quoteParam(keyID)) + b.WriteString(",secret_access_key=") + b.WriteString(quoteParam(secret)) + if strings.TrimSpace(endpoint) != "" { + b.WriteString(",endpoint=") + b.WriteString(quoteParam(strings.TrimRight(endpoint, "/"))) + } + if strings.TrimSpace(region) != "" { + b.WriteString(",region=") + b.WriteString(quoteParam(region)) + } + if pathStyle { + b.WriteString(",force_path_style=true") + } + b.WriteString(",env_auth=false,no_check_bucket=true:") + b.WriteString(bucket) + return b.String() +} + +// --------------------------------------------------------------------------- +// WebDAV +// --------------------------------------------------------------------------- + +type WebDAVFactory struct{} + +func NewWebDAVFactory() WebDAVFactory { return WebDAVFactory{} } + +func (WebDAVFactory) Type() storage.ProviderType { return storage.ProviderTypeWebDAV } +func (WebDAVFactory) SensitiveFields() []string { return []string{"username", "password"} } + +func (WebDAVFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { + cfg, err := storage.DecodeConfig[storage.WebDAVConfig](rawConfig) + if err != nil { + return nil, err + } + if strings.TrimSpace(cfg.Endpoint) == "" { + return nil, fmt.Errorf("webdav endpoint is required") + } + remote := fmt.Sprintf(":webdav,url=%s,user=%s,pass=%s:%s", + quoteParam(strings.TrimRight(cfg.Endpoint, "/")), + quoteParam(cfg.Username), + quoteParam(cfg.Password), + strings.TrimSpace(cfg.BasePath)) + return newFs(ctx, storage.ProviderTypeWebDAV, remote) +} + +// --------------------------------------------------------------------------- +// Google Drive +// --------------------------------------------------------------------------- + +type GoogleDriveFactory struct{} + +func NewGoogleDriveFactory() GoogleDriveFactory { return GoogleDriveFactory{} } + +func (GoogleDriveFactory) Type() storage.ProviderType { return storage.ProviderTypeGoogleDrive } +func (GoogleDriveFactory) SensitiveFields() []string { + return []string{"clientId", "clientSecret", "refreshToken"} +} + +func (GoogleDriveFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { + cfg, err := storage.DecodeConfig[storage.GoogleDriveConfig](rawConfig) + if err != nil { + return nil, err + } + cfg = cfg.Normalize() + if strings.TrimSpace(cfg.ClientID) == "" || strings.TrimSpace(cfg.ClientSecret) == "" { + return nil, fmt.Errorf("google drive client credentials are required") + } + if strings.TrimSpace(cfg.RefreshToken) == "" { + return nil, fmt.Errorf("google drive refresh token is required") + } + // 构造 rclone 所需的 OAuth2 token JSON + tokenJSON := fmt.Sprintf(`{"access_token":"","token_type":"Bearer","refresh_token":"%s","expiry":"0001-01-01T00:00:00Z"}`, + strings.ReplaceAll(cfg.RefreshToken, `"`, `\"`)) + + var b strings.Builder + b.WriteString(":drive,client_id=") + b.WriteString(quoteParam(cfg.ClientID)) + b.WriteString(",client_secret=") + b.WriteString(quoteParam(cfg.ClientSecret)) + b.WriteString(",token=") + b.WriteString(quoteParam(tokenJSON)) + if strings.TrimSpace(cfg.FolderID) != "" { + b.WriteString(",root_folder_id=") + b.WriteString(quoteParam(cfg.FolderID)) + } + b.WriteString(":") + return newFs(ctx, storage.ProviderTypeGoogleDrive, b.String()) +} + +// --------------------------------------------------------------------------- +// FTP +// --------------------------------------------------------------------------- + +type FTPFactory struct{} + +func NewFTPFactory() FTPFactory { return FTPFactory{} } + +func (FTPFactory) Type() storage.ProviderType { return storage.ProviderTypeFTP } +func (FTPFactory) SensitiveFields() []string { return []string{"username", "password"} } + +func (FTPFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { + cfg, err := storage.DecodeConfig[storage.FTPConfig](rawConfig) + if err != nil { + return nil, err + } + if strings.TrimSpace(cfg.Host) == "" { + return nil, fmt.Errorf("FTP host is required") + } + port := cfg.Port + if port == 0 { + port = 21 + } + username := strings.TrimSpace(cfg.Username) + if username == "" { + username = "anonymous" + } + var b strings.Builder + b.WriteString(fmt.Sprintf(":ftp,host=%s,port=%d,user=%s,pass=%s", + quoteParam(cfg.Host), port, quoteParam(username), quoteParam(cfg.Password))) + if cfg.UseTLS { + b.WriteString(",tls=true,explicit_tls=true") + } + b.WriteString(":") + basePath := strings.TrimSpace(cfg.BasePath) + if basePath != "" { + b.WriteString(basePath) + } + return newFs(ctx, storage.ProviderTypeFTP, b.String()) +} + +// --------------------------------------------------------------------------- +// 阿里云 OSS(委托 S3 引擎) +// --------------------------------------------------------------------------- + +type AliyunOSSFactory struct{} + +func NewAliyunOSSFactory() AliyunOSSFactory { return AliyunOSSFactory{} } + +func (AliyunOSSFactory) Type() storage.ProviderType { return storage.ProviderTypeAliyunOSS } +func (AliyunOSSFactory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} } + +// AliyunConfig 是阿里云 OSS 的用户配置。 +type AliyunConfig struct { + Region string `json:"region"` + Bucket string `json:"bucket"` + AccessKeyID string `json:"accessKeyId"` + SecretAccessKey string `json:"secretAccessKey"` + Endpoint string `json:"endpoint"` + InternalNetwork bool `json:"internalNetwork"` +} + +func (AliyunOSSFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { + cfg, err := storage.DecodeConfig[AliyunConfig](rawConfig) + if err != nil { + return nil, err + } + endpoint := strings.TrimSpace(cfg.Endpoint) + if endpoint == "" { + region := strings.TrimSpace(cfg.Region) + if region == "" { + return nil, fmt.Errorf("aliyun oss region is required") + } + if cfg.InternalNetwork { + endpoint = fmt.Sprintf("https://oss-%s-internal.aliyuncs.com", region) + } else { + endpoint = fmt.Sprintf("https://oss-%s.aliyuncs.com", region) + } + } + return newFs(ctx, storage.ProviderTypeAliyunOSS, buildS3Remote("Alibaba", cfg.AccessKeyID, cfg.SecretAccessKey, endpoint, cfg.Region, cfg.Bucket, false)) +} + +// --------------------------------------------------------------------------- +// 腾讯云 COS(委托 S3 引擎) +// --------------------------------------------------------------------------- + +type TencentCOSFactory struct{} + +func NewTencentCOSFactory() TencentCOSFactory { return TencentCOSFactory{} } + +func (TencentCOSFactory) Type() storage.ProviderType { return storage.ProviderTypeTencentCOS } +func (TencentCOSFactory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} } + +// TencentConfig 是腾讯云 COS 的用户配置。 +type TencentConfig struct { + Region string `json:"region"` + Bucket string `json:"bucket"` + SecretID string `json:"accessKeyId"` + SecretKey string `json:"secretAccessKey"` + Endpoint string `json:"endpoint"` +} + +func (TencentCOSFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { + cfg, err := storage.DecodeConfig[TencentConfig](rawConfig) + if err != nil { + return nil, err + } + endpoint := strings.TrimSpace(cfg.Endpoint) + if endpoint == "" { + region := strings.TrimSpace(cfg.Region) + if region == "" { + return nil, fmt.Errorf("tencent cos region is required") + } + endpoint = fmt.Sprintf("https://cos.%s.myqcloud.com", region) + } + return newFs(ctx, storage.ProviderTypeTencentCOS, buildS3Remote("TencentCOS", cfg.SecretID, cfg.SecretKey, endpoint, cfg.Region, cfg.Bucket, false)) +} + +// --------------------------------------------------------------------------- +// 七牛云 Kodo(委托 S3 引擎) +// --------------------------------------------------------------------------- + +type QiniuKodoFactory struct{} + +func NewQiniuKodoFactory() QiniuKodoFactory { return QiniuKodoFactory{} } + +func (QiniuKodoFactory) Type() storage.ProviderType { return storage.ProviderTypeQiniuKodo } +func (QiniuKodoFactory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} } + +// QiniuConfig 是七牛云 Kodo 的用户配置。 +type QiniuConfig struct { + Region string `json:"region"` + Bucket string `json:"bucket"` + AccessKey string `json:"accessKeyId"` + SecretKey string `json:"secretAccessKey"` + Endpoint string `json:"endpoint"` +} + +// regionEndpoints 映射七牛区域代码到 S3 兼容 endpoint。 +var regionEndpoints = map[string]string{ + "z0": "https://s3-cn-east-1.qiniucs.com", + "cn-east-2": "https://s3-cn-east-2.qiniucs.com", + "z1": "https://s3-cn-north-1.qiniucs.com", + "z2": "https://s3-cn-south-1.qiniucs.com", + "na0": "https://s3-us-north-1.qiniucs.com", + "as0": "https://s3-ap-southeast-1.qiniucs.com", +} + +func (QiniuKodoFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { + cfg, err := storage.DecodeConfig[QiniuConfig](rawConfig) + if err != nil { + return nil, err + } + endpoint := strings.TrimSpace(cfg.Endpoint) + if endpoint == "" { + region := strings.TrimSpace(cfg.Region) + if region == "" { + return nil, fmt.Errorf("qiniu kodo region is required") + } + var ok bool + endpoint, ok = regionEndpoints[region] + if !ok { + return nil, fmt.Errorf("unsupported qiniu region: %s (supported: z0, cn-east-2, z1, z2, na0, as0)", region) + } + } + return newFs(ctx, storage.ProviderTypeQiniuKodo, buildS3Remote("Qiniu", cfg.AccessKey, cfg.SecretKey, endpoint, cfg.Region, cfg.Bucket, true)) +} diff --git a/server/internal/storage/rclone/provider.go b/server/internal/storage/rclone/provider.go new file mode 100644 index 0000000..53ec2aa --- /dev/null +++ b/server/internal/storage/rclone/provider.go @@ -0,0 +1,112 @@ +package rclone + +import ( + "context" + "fmt" + "io" + "strings" + "time" + + "backupx/server/internal/storage" + + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/object" + "github.com/rclone/rclone/fs/walk" +) + +// Provider 包装 rclone fs.Fs,实现 storage.StorageProvider 接口。 +type Provider struct { + providerType storage.ProviderType + rfs fs.Fs +} + +func newProvider(providerType storage.ProviderType, rfs fs.Fs) *Provider { + return &Provider{providerType: providerType, rfs: rfs} +} + +func (p *Provider) Type() storage.ProviderType { return p.providerType } + +// TestConnection 通过列出根目录验证连通性。 +func (p *Provider) TestConnection(ctx context.Context) error { + _, err := p.rfs.List(ctx, "") + if err != nil { + return fmt.Errorf("rclone test connection: %w", err) + } + return nil +} + +// Upload 通过 rclone fs.Fs.Put 上传文件。 +func (p *Provider) Upload(ctx context.Context, objectKey string, reader io.Reader, size int64, _ map[string]string) error { + dir := pathDir(objectKey) + if dir != "" && dir != "." { + if err := p.rfs.Mkdir(ctx, dir); err != nil { + return fmt.Errorf("rclone mkdir %s: %w", dir, err) + } + } + info := object.NewStaticObjectInfo(objectKey, time.Now(), size, true, nil, nil) + if _, err := p.rfs.Put(ctx, reader, info); err != nil { + return fmt.Errorf("rclone upload %s: %w", objectKey, err) + } + return nil +} + +// Download 通过 rclone 获取对象并返回 io.ReadCloser。 +func (p *Provider) Download(ctx context.Context, objectKey string) (io.ReadCloser, error) { + obj, err := p.rfs.NewObject(ctx, objectKey) + if err != nil { + return nil, fmt.Errorf("rclone find object %s: %w", objectKey, err) + } + reader, err := obj.Open(ctx) + if err != nil { + return nil, fmt.Errorf("rclone download %s: %w", objectKey, err) + } + return reader, nil +} + +// Delete 通过 rclone 删除远端对象。 +func (p *Provider) Delete(ctx context.Context, objectKey string) error { + obj, err := p.rfs.NewObject(ctx, objectKey) + if err != nil { + return fmt.Errorf("rclone find object %s: %w", objectKey, err) + } + if err := obj.Remove(ctx); err != nil { + return fmt.Errorf("rclone delete %s: %w", objectKey, err) + } + return nil +} + +// List 递归列出指定前缀下的所有对象。 +func (p *Provider) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) { + var items []storage.ObjectInfo + err := walk.ListR(ctx, p.rfs, prefix, true, -1, walk.ListObjects, func(entries fs.DirEntries) error { + for _, entry := range entries { + obj, ok := entry.(fs.Object) + if !ok { + continue + } + key := obj.Remote() + if prefix != "" && !strings.HasPrefix(key, prefix) { + continue + } + items = append(items, storage.ObjectInfo{ + Key: key, + Size: obj.Size(), + UpdatedAt: obj.ModTime(ctx), + }) + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("rclone list %s: %w", prefix, err) + } + return items, nil +} + +// pathDir 返回 objectKey 的目录部分(正斜杠分隔)。 +func pathDir(objectKey string) string { + idx := strings.LastIndex(objectKey, "/") + if idx < 0 { + return "" + } + return objectKey[:idx] +} diff --git a/server/internal/storage/rclone/provider_test.go b/server/internal/storage/rclone/provider_test.go new file mode 100644 index 0000000..e972c0d --- /dev/null +++ b/server/internal/storage/rclone/provider_test.go @@ -0,0 +1,129 @@ +package rclone + +import ( + "context" + "io" + "strings" + "testing" +) + +func TestProviderLocalDiskCRUD(t *testing.T) { + factory := NewLocalDiskFactory() + provider, err := factory.New(context.Background(), map[string]any{"basePath": t.TempDir()}) + if err != nil { + t.Fatalf("Factory.New returned error: %v", err) + } + if err := provider.TestConnection(context.Background()); err != nil { + t.Fatalf("TestConnection returned error: %v", err) + } + + // Upload + if err := provider.Upload(context.Background(), "daily/backup.txt", strings.NewReader("hello"), 5, nil); err != nil { + t.Fatalf("Upload returned error: %v", err) + } + + // Download + reader, err := provider.Download(context.Background(), "daily/backup.txt") + if err != nil { + t.Fatalf("Download returned error: %v", err) + } + defer reader.Close() + content, _ := io.ReadAll(reader) + if string(content) != "hello" { + t.Fatalf("expected 'hello', got %q", string(content)) + } + + // List with prefix + items, err := provider.List(context.Background(), "daily") + if err != nil { + t.Fatalf("List returned error: %v", err) + } + if len(items) != 1 || items[0].Key != "daily/backup.txt" { + t.Fatalf("unexpected list result: %#v", items) + } + + // Delete + if err := provider.Delete(context.Background(), "daily/backup.txt"); err != nil { + t.Fatalf("Delete returned error: %v", err) + } + + // List after delete should be empty + items, err = provider.List(context.Background(), "daily") + if err != nil { + t.Fatalf("List after delete returned error: %v", err) + } + if len(items) != 0 { + t.Fatalf("expected empty list after delete, got %d items", len(items)) + } +} + +func TestProviderLocalDiskRequiresBasePath(t *testing.T) { + _, err := NewLocalDiskFactory().New(context.Background(), map[string]any{"basePath": ""}) + if err == nil { + t.Fatal("expected error for empty basePath") + } +} + +func TestProviderS3RequiresBucketAndCredentials(t *testing.T) { + factory := NewS3Factory() + _, err := factory.New(context.Background(), map[string]any{"bucket": "", "accessKeyId": "a", "secretAccessKey": "b"}) + if err == nil || !strings.Contains(err.Error(), "bucket") { + t.Fatalf("expected bucket required error, got %v", err) + } + _, err = factory.New(context.Background(), map[string]any{"bucket": "demo", "accessKeyId": "", "secretAccessKey": "b"}) + if err == nil || !strings.Contains(err.Error(), "credentials") { + t.Fatalf("expected credentials required error, got %v", err) + } +} + +func TestQuoteParam(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"simple", "simple"}, + {"", ""}, + {"has,comma", "'has,comma'"}, + {"has:colon", "'has:colon'"}, + {"has=equals", "'has=equals'"}, + {"has'quote", "'has''quote'"}, + {"a,b:c=d'e", "'a,b:c=d''e'"}, + } + for _, tt := range tests { + got := quoteParam(tt.input) + if got != tt.expected { + t.Errorf("quoteParam(%q) = %q, want %q", tt.input, got, tt.expected) + } + } +} + +func TestBuildS3Remote(t *testing.T) { + remote := buildS3Remote("Alibaba", "keyID", "secret", "https://oss-cn-hangzhou.aliyuncs.com", "cn-hangzhou", "my-bucket", false) + if !strings.Contains(remote, "provider=Alibaba") { + t.Fatalf("expected provider=Alibaba in remote: %s", remote) + } + if !strings.Contains(remote, ":my-bucket") { + t.Fatalf("expected :my-bucket suffix in remote: %s", remote) + } + if !strings.HasPrefix(remote, ":s3,") { + t.Fatalf("expected :s3, prefix in remote: %s", remote) + } +} + +func TestPathDir(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"BackupX/file/260308/backup.tar.gz", "BackupX/file/260308"}, + {"backup.tar.gz", ""}, + {"a/b", "a"}, + {"", ""}, + } + for _, tt := range tests { + got := pathDir(tt.input) + if got != tt.expected { + t.Errorf("pathDir(%q) = %q, want %q", tt.input, got, tt.expected) + } + } +} diff --git a/server/internal/storage/s3/provider.go b/server/internal/storage/s3/provider.go deleted file mode 100644 index 1ce053a..0000000 --- a/server/internal/storage/s3/provider.go +++ /dev/null @@ -1,126 +0,0 @@ -package s3 - -import ( - "context" - "fmt" - "io" - "strings" - "time" - - "backupx/server/internal/storage" - awscore "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/credentials" - awss3 "github.com/aws/aws-sdk-go-v2/service/s3" -) - -type client interface { - HeadBucket(context.Context, *awss3.HeadBucketInput, ...func(*awss3.Options)) (*awss3.HeadBucketOutput, error) - PutObject(context.Context, *awss3.PutObjectInput, ...func(*awss3.Options)) (*awss3.PutObjectOutput, error) - GetObject(context.Context, *awss3.GetObjectInput, ...func(*awss3.Options)) (*awss3.GetObjectOutput, error) - DeleteObject(context.Context, *awss3.DeleteObjectInput, ...func(*awss3.Options)) (*awss3.DeleteObjectOutput, error) - ListObjectsV2(context.Context, *awss3.ListObjectsV2Input, ...func(*awss3.Options)) (*awss3.ListObjectsV2Output, error) -} - -type Provider struct { - client client - bucket string -} - -type Factory struct { - newClient func(cfg storage.S3Config) client -} - -func NewFactory() Factory { - return Factory{newClient: func(cfg storage.S3Config) client { - region := strings.TrimSpace(cfg.Region) - if region == "" { - region = "us-east-1" - } - awsConfig := awscore.Config{ - Region: region, - Credentials: credentials.NewStaticCredentialsProvider(cfg.AccessKeyID, cfg.SecretAccessKey, ""), - } - return awss3.NewFromConfig(awsConfig, func(options *awss3.Options) { - options.UsePathStyle = cfg.ForcePathStyle - if strings.TrimSpace(cfg.Endpoint) != "" { - options.BaseEndpoint = awscore.String(strings.TrimRight(cfg.Endpoint, "/")) - } - }) - }} -} - -func (Factory) Type() storage.ProviderType { return storage.ProviderTypeS3 } -func (Factory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} } - -func (f Factory) New(_ context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { - cfg, err := storage.DecodeConfig[storage.S3Config](rawConfig) - if err != nil { - return nil, err - } - if strings.TrimSpace(cfg.Bucket) == "" { - return nil, fmt.Errorf("s3 bucket is required") - } - if strings.TrimSpace(cfg.AccessKeyID) == "" || strings.TrimSpace(cfg.SecretAccessKey) == "" { - return nil, fmt.Errorf("s3 credentials are required") - } - newClient := f.newClient - if newClient == nil { - factory := NewFactory() - newClient = factory.newClient - } - return &Provider{client: newClient(cfg), bucket: cfg.Bucket}, nil -} - -func (p *Provider) Type() storage.ProviderType { return storage.ProviderTypeS3 } - -func (p *Provider) TestConnection(ctx context.Context) error { - _, err := p.client.HeadBucket(ctx, &awss3.HeadBucketInput{Bucket: awscore.String(p.bucket)}) - if err != nil { - return fmt.Errorf("test s3 connection: %w", err) - } - return nil -} - -func (p *Provider) Upload(ctx context.Context, objectKey string, reader io.Reader, _ int64, metadata map[string]string) error { - _, err := p.client.PutObject(ctx, &awss3.PutObjectInput{Bucket: awscore.String(p.bucket), Key: awscore.String(objectKey), Body: reader, Metadata: metadata}) - if err != nil { - return fmt.Errorf("upload s3 object: %w", err) - } - return nil -} - -func (p *Provider) Download(ctx context.Context, objectKey string) (io.ReadCloser, error) { - result, err := p.client.GetObject(ctx, &awss3.GetObjectInput{Bucket: awscore.String(p.bucket), Key: awscore.String(objectKey)}) - if err != nil { - return nil, fmt.Errorf("download s3 object: %w", err) - } - return result.Body, nil -} - -func (p *Provider) Delete(ctx context.Context, objectKey string) error { - _, err := p.client.DeleteObject(ctx, &awss3.DeleteObjectInput{Bucket: awscore.String(p.bucket), Key: awscore.String(objectKey)}) - if err != nil { - return fmt.Errorf("delete s3 object: %w", err) - } - return nil -} - -func (p *Provider) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) { - result, err := p.client.ListObjectsV2(ctx, &awss3.ListObjectsV2Input{Bucket: awscore.String(p.bucket), Prefix: awscore.String(prefix)}) - if err != nil { - return nil, fmt.Errorf("list s3 objects: %w", err) - } - items := make([]storage.ObjectInfo, 0, len(result.Contents)) - for _, object := range result.Contents { - updatedAt := time.Time{} - if object.LastModified != nil { - updatedAt = object.LastModified.UTC() - } - size := int64(0) - if object.Size != nil { - size = *object.Size - } - items = append(items, storage.ObjectInfo{Key: awscore.ToString(object.Key), Size: size, UpdatedAt: updatedAt}) - } - return items, nil -} diff --git a/server/internal/storage/s3/provider_test.go b/server/internal/storage/s3/provider_test.go deleted file mode 100644 index 205f513..0000000 --- a/server/internal/storage/s3/provider_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package s3 - -import ( - "bytes" - "context" - "io" - "strings" - "testing" - "time" - - "backupx/server/internal/storage" - - awscore "github.com/aws/aws-sdk-go-v2/aws" - awss3 "github.com/aws/aws-sdk-go-v2/service/s3" - awss3types "github.com/aws/aws-sdk-go-v2/service/s3/types" -) - -type fakeClient struct{ data map[string]string } - -func (c *fakeClient) HeadBucket(context.Context, *awss3.HeadBucketInput, ...func(*awss3.Options)) (*awss3.HeadBucketOutput, error) { - return &awss3.HeadBucketOutput{}, nil -} - -func (c *fakeClient) PutObject(_ context.Context, input *awss3.PutObjectInput, _ ...func(*awss3.Options)) (*awss3.PutObjectOutput, error) { - body, _ := io.ReadAll(input.Body) - c.data[awscore.ToString(input.Key)] = string(body) - return &awss3.PutObjectOutput{}, nil -} - -func (c *fakeClient) GetObject(_ context.Context, input *awss3.GetObjectInput, _ ...func(*awss3.Options)) (*awss3.GetObjectOutput, error) { - return &awss3.GetObjectOutput{Body: io.NopCloser(strings.NewReader(c.data[awscore.ToString(input.Key)]))}, nil -} - -func (c *fakeClient) DeleteObject(_ context.Context, input *awss3.DeleteObjectInput, _ ...func(*awss3.Options)) (*awss3.DeleteObjectOutput, error) { - delete(c.data, awscore.ToString(input.Key)) - return &awss3.DeleteObjectOutput{}, nil -} - -func (c *fakeClient) ListObjectsV2(_ context.Context, _ *awss3.ListObjectsV2Input, _ ...func(*awss3.Options)) (*awss3.ListObjectsV2Output, error) { - now := time.Now().UTC() - return &awss3.ListObjectsV2Output{Contents: []awss3types.Object{{Key: awscore.String("backup.tar.gz"), Size: awscore.Int64(10), LastModified: &now}}}, nil -} - -func TestS3ProviderCRUD(t *testing.T) { - factory := Factory{newClient: func(cfg storage.S3Config) client { - return &fakeClient{data: make(map[string]string)} - }} - providerAny, err := factory.New(context.Background(), map[string]any{"bucket": "demo", "accessKeyId": "a", "secretAccessKey": "b"}) - if err != nil { - t.Fatalf("Factory.New returned error: %v", err) - } - provider := providerAny.(*Provider) - if err := provider.TestConnection(context.Background()); err != nil { - t.Fatalf("TestConnection returned error: %v", err) - } - if err := provider.Upload(context.Background(), "backup.tar.gz", bytes.NewBufferString("payload"), 7, nil); err != nil { - t.Fatalf("Upload returned error: %v", err) - } - reader, err := provider.Download(context.Background(), "backup.tar.gz") - if err != nil { - t.Fatalf("Download returned error: %v", err) - } - defer reader.Close() - content, _ := io.ReadAll(reader) - if string(content) != "payload" { - t.Fatalf("unexpected content: %s", string(content)) - } - items, err := provider.List(context.Background(), "backup") - if err != nil { - t.Fatalf("List returned error: %v", err) - } - if len(items) != 1 || items[0].Key != "backup.tar.gz" { - t.Fatalf("unexpected list result: %#v", items) - } - if err := provider.Delete(context.Background(), "backup.tar.gz"); err != nil { - t.Fatalf("Delete returned error: %v", err) - } -} diff --git a/server/internal/storage/s3provider/provider.go b/server/internal/storage/s3provider/provider.go deleted file mode 100644 index eb961e2..0000000 --- a/server/internal/storage/s3provider/provider.go +++ /dev/null @@ -1,9 +0,0 @@ -package s3provider - -import "backupx/server/internal/storage/s3" - -type Factory = s3.Factory - -func NewFactory() Factory { - return s3.NewFactory() -} diff --git a/server/internal/storage/tencent/factory.go b/server/internal/storage/tencent/factory.go deleted file mode 100644 index 7b4e6e2..0000000 --- a/server/internal/storage/tencent/factory.go +++ /dev/null @@ -1,60 +0,0 @@ -// Package tencent provides a Tencent Cloud COS storage factory that delegates to the S3-compatible engine. -// Tencent COS is fully S3-compatible; we auto-assemble the endpoint from region and appId. -package tencent - -import ( - "context" - "fmt" - "strings" - - "backupx/server/internal/storage" - "backupx/server/internal/storage/s3" -) - -// Config is the user-facing configuration for Tencent COS. -type Config struct { - Region string `json:"region"` - Bucket string `json:"bucket"` // format: bucketname-appid - SecretID string `json:"accessKeyId"` - SecretKey string `json:"secretAccessKey"` - Endpoint string `json:"endpoint"` // optional override -} - -// Factory creates Tencent COS providers by composing the S3 engine. -type Factory struct { - s3Factory s3.Factory -} - -func NewFactory() Factory { - return Factory{s3Factory: s3.NewFactory()} -} - -func (Factory) Type() storage.ProviderType { return storage.ProviderTypeTencentCOS } -func (Factory) SensitiveFields() []string { return []string{"accessKeyId", "secretAccessKey"} } - -func (f Factory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { - cfg, err := storage.DecodeConfig[Config](rawConfig) - if err != nil { - return nil, err - } - - endpoint := strings.TrimSpace(cfg.Endpoint) - if endpoint == "" { - region := strings.TrimSpace(cfg.Region) - if region == "" { - return nil, fmt.Errorf("tencent cos region is required") - } - // Tencent COS S3-compatible endpoint format - endpoint = fmt.Sprintf("https://cos.%s.myqcloud.com", region) - } - - s3Config := map[string]any{ - "endpoint": endpoint, - "region": cfg.Region, - "bucket": cfg.Bucket, - "accessKeyId": cfg.SecretID, - "secretAccessKey": cfg.SecretKey, - "forcePathStyle": false, // COS uses virtual-hosted style - } - return f.s3Factory.New(ctx, s3Config) -} diff --git a/server/internal/storage/webdav/provider.go b/server/internal/storage/webdav/provider.go deleted file mode 100644 index 2e1aafc..0000000 --- a/server/internal/storage/webdav/provider.go +++ /dev/null @@ -1,126 +0,0 @@ -package webdav - -import ( - "context" - "fmt" - "io" - "os" - "path" - "strings" - - "backupx/server/internal/storage" - gowebdav "github.com/studio-b12/gowebdav" -) - -type client interface { - ReadDir(path string) ([]os.FileInfo, error) - WriteStream(path string, stream io.Reader, perm os.FileMode) error - ReadStream(path string) (io.ReadCloser, error) - Remove(path string) error - MkdirAll(path string, perm os.FileMode) error - Stat(path string) (os.FileInfo, error) -} - -type Provider struct { - client client - basePath string -} - -type Factory struct { - newClient func(cfg storage.WebDAVConfig) client -} - -func NewFactory() Factory { - return Factory{newClient: func(cfg storage.WebDAVConfig) client { - return gowebdav.NewClient(strings.TrimRight(cfg.Endpoint, "/"), cfg.Username, cfg.Password) - }} -} - -func (Factory) Type() storage.ProviderType { return storage.ProviderTypeWebDAV } -func (Factory) SensitiveFields() []string { return []string{"username", "password"} } - -func (f Factory) New(_ context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { - cfg, err := storage.DecodeConfig[storage.WebDAVConfig](rawConfig) - if err != nil { - return nil, err - } - if strings.TrimSpace(cfg.Endpoint) == "" { - return nil, fmt.Errorf("webdav endpoint is required") - } - newClient := f.newClient - if newClient == nil { - factory := NewFactory() - newClient = factory.newClient - } - return &Provider{client: newClient(cfg), basePath: normalizeBasePath(cfg.BasePath)}, nil -} - -func (p *Provider) Type() storage.ProviderType { return storage.ProviderTypeWebDAV } - -func (p *Provider) TestConnection(_ context.Context) error { - if err := p.client.MkdirAll(p.basePath, 0o755); err != nil { - return fmt.Errorf("ensure webdav base path: %w", err) - } - if _, err := p.client.Stat(p.basePath); err != nil { - return fmt.Errorf("stat webdav base path: %w", err) - } - return nil -} - -func (p *Provider) Upload(_ context.Context, objectKey string, reader io.Reader, _ int64, _ map[string]string) error { - objectPath := p.resolvePath(objectKey) - if err := p.client.MkdirAll(path.Dir(objectPath), 0o755); err != nil { - return fmt.Errorf("create webdav directories: %w", err) - } - if err := p.client.WriteStream(objectPath, reader, 0o644); err != nil { - return fmt.Errorf("write webdav object: %w", err) - } - return nil -} - -func (p *Provider) Download(_ context.Context, objectKey string) (io.ReadCloser, error) { - reader, err := p.client.ReadStream(p.resolvePath(objectKey)) - if err != nil { - return nil, fmt.Errorf("read webdav object: %w", err) - } - return reader, nil -} - -func (p *Provider) Delete(_ context.Context, objectKey string) error { - if err := p.client.Remove(p.resolvePath(objectKey)); err != nil { - return fmt.Errorf("delete webdav object: %w", err) - } - return nil -} - -func (p *Provider) List(_ context.Context, prefix string) ([]storage.ObjectInfo, error) { - entries, err := p.client.ReadDir(p.basePath) - if err != nil { - return nil, fmt.Errorf("list webdav directory: %w", err) - } - items := make([]storage.ObjectInfo, 0, len(entries)) - for _, entry := range entries { - if entry.IsDir() { - continue - } - key := strings.TrimPrefix(path.Join(strings.TrimPrefix(p.basePath, "/"), entry.Name()), "/") - if prefix != "" && !strings.HasPrefix(key, prefix) { - continue - } - items = append(items, storage.ObjectInfo{Key: key, Size: entry.Size(), UpdatedAt: entry.ModTime().UTC()}) - } - return items, nil -} - -func normalizeBasePath(value string) string { - clean := path.Clean("/" + strings.TrimSpace(value)) - if clean == "." { - return "/" - } - return clean -} - -func (p *Provider) resolvePath(objectKey string) string { - cleanKey := path.Clean("/" + strings.TrimSpace(objectKey)) - return path.Clean(path.Join(p.basePath, cleanKey)) -} diff --git a/server/internal/storage/webdav/provider_test.go b/server/internal/storage/webdav/provider_test.go deleted file mode 100644 index 7a73f68..0000000 --- a/server/internal/storage/webdav/provider_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package webdav - -import ( - "context" - "io" - "os" - "strings" - "testing" - "time" - - "backupx/server/internal/storage" -) - -type fakeFileInfo struct { - name string - size int64 - mod time.Time - dir bool -} - -func (f fakeFileInfo) Name() string { return f.name } -func (f fakeFileInfo) Size() int64 { return f.size } -func (f fakeFileInfo) Mode() os.FileMode { return 0 } -func (f fakeFileInfo) ModTime() time.Time { return f.mod } -func (f fakeFileInfo) IsDir() bool { return f.dir } -func (f fakeFileInfo) Sys() any { return nil } - -type fakeClient struct{ data map[string]string } - -func (c *fakeClient) ReadDir(_ string) ([]os.FileInfo, error) { - return []os.FileInfo{fakeFileInfo{name: "backup.tar.gz", size: int64(len(c.data["/storage/backup.tar.gz"])), mod: time.Now().UTC()}}, nil -} -func (c *fakeClient) WriteStream(path string, stream io.Reader, _ os.FileMode) error { - content, _ := io.ReadAll(stream) - c.data[path] = string(content) - return nil -} -func (c *fakeClient) ReadStream(path string) (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader(c.data[path])), nil -} -func (c *fakeClient) Remove(path string) error { delete(c.data, path); return nil } -func (c *fakeClient) MkdirAll(_ string, _ os.FileMode) error { return nil } -func (c *fakeClient) Stat(path string) (os.FileInfo, error) { - return fakeFileInfo{name: path, dir: true}, nil -} - -func TestWebDAVProviderCRUD(t *testing.T) { - factory := Factory{newClient: func(storage.WebDAVConfig) client { return &fakeClient{data: make(map[string]string)} }} - providerAny, err := factory.New(context.Background(), map[string]any{"endpoint": "http://dav.example.com", "basePath": "/storage"}) - if err != nil { - t.Fatalf("Factory.New returned error: %v", err) - } - provider := providerAny.(*Provider) - if err := provider.TestConnection(context.Background()); err != nil { - t.Fatalf("TestConnection returned error: %v", err) - } - if err := provider.Upload(context.Background(), "backup.tar.gz", strings.NewReader("payload"), 7, nil); err != nil { - t.Fatalf("Upload returned error: %v", err) - } - reader, err := provider.Download(context.Background(), "backup.tar.gz") - if err != nil { - t.Fatalf("Download returned error: %v", err) - } - defer reader.Close() - content, _ := io.ReadAll(reader) - if string(content) != "payload" { - t.Fatalf("unexpected content: %s", string(content)) - } - items, err := provider.List(context.Background(), "storage") - if err != nil { - t.Fatalf("List returned error: %v", err) - } - if len(items) != 1 || items[0].Key != "storage/backup.tar.gz" { - t.Fatalf("unexpected list result: %#v", items) - } - if err := provider.Delete(context.Background(), "backup.tar.gz"); err != nil { - t.Fatalf("Delete returned error: %v", err) - } -} diff --git a/server/internal/storage/webdavprovider/provider.go b/server/internal/storage/webdavprovider/provider.go deleted file mode 100644 index 3eda6e7..0000000 --- a/server/internal/storage/webdavprovider/provider.go +++ /dev/null @@ -1,9 +0,0 @@ -package webdavprovider - -import "backupx/server/internal/storage/webdav" - -type Factory = webdav.Factory - -func NewFactory() Factory { - return webdav.NewFactory() -}