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)) } // --------------------------------------------------------------------------- // 通用 Rclone 后端(支持全部 70+ 后端) // --------------------------------------------------------------------------- type RcloneFactory struct{} func NewRcloneFactory() RcloneFactory { return RcloneFactory{} } func (RcloneFactory) Type() storage.ProviderType { return storage.ProviderTypeRclone } func (RcloneFactory) SensitiveFields() []string { return []string{"pass", "password", "secret_access_key", "client_secret", "token"} } func (RcloneFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { backend, _ := rawConfig["backend"].(string) backend = strings.TrimSpace(backend) if backend == "" { return nil, fmt.Errorf("rclone backend type is required") } root, _ := rawConfig["root"].(string) root = strings.TrimSpace(root) // 构建连接字符串::backend,key1=val1,key2=val2:root var b strings.Builder b.WriteString(":") b.WriteString(backend) for key, val := range rawConfig { if key == "backend" || key == "root" { continue } strVal := fmt.Sprintf("%v", val) if strings.TrimSpace(strVal) == "" { continue } b.WriteString(",") b.WriteString(key) b.WriteString("=") b.WriteString(quoteParam(strVal)) } b.WriteString(":") b.WriteString(root) return newFs(ctx, storage.ProviderTypeRclone, b.String()) } // ListBackends 返回所有可用的 rclone 后端及其配置选项。 func ListBackends() []BackendInfo { var backends []BackendInfo for _, ri := range fs.Registry { if ri.Name == "union" || ri.Name == "crypt" || ri.Name == "chunker" || ri.Name == "compress" || ri.Name == "hasher" || ri.Name == "combine" { continue // 跳过组合/加密类后端 } info := BackendInfo{ Name: ri.Name, Description: ri.Description, } for _, opt := range ri.Options { if opt.Hide != 0 { continue } // 跳过 rclone 为每个后端自动添加的通用选项 if opt.Name == "description" { continue } info.Options = append(info.Options, BackendOption{ Key: opt.Name, Label: opt.Help, Required: opt.Required, IsPassword: opt.IsPassword, }) } backends = append(backends, info) } return backends } // BackendInfo 描述一个 rclone 后端。 type BackendInfo struct { Name string `json:"name"` Description string `json:"description"` Options []BackendOption `json:"options"` } // BackendOption 描述一个后端配置选项。 type BackendOption struct { Key string `json:"key"` Label string `json:"label"` Required bool `json:"required"` IsPassword bool `json:"isPassword"` } // --------------------------------------------------------------------------- // 通用 BackendFactory — 为任意 rclone 后端自动生成独立 Factory // --------------------------------------------------------------------------- // GenericBackendFactory 为单个 rclone 后端创建独立的 ProviderFactory。 // 用户存储目标的 type 直接是后端名(如 "sftp"),与 "s3"、"ftp" 完全平级。 type GenericBackendFactory struct { backendType string sensitive []string } // NewBackendFactory 为指定 rclone 后端创建一个 Factory。 func NewBackendFactory(backendType string) GenericBackendFactory { var sensitive []string for _, ri := range fs.Registry { if ri.Name == backendType { for _, opt := range ri.Options { if opt.IsPassword { sensitive = append(sensitive, opt.Name) } } break } } return GenericBackendFactory{backendType: backendType, sensitive: sensitive} } func (f GenericBackendFactory) Type() storage.ProviderType { return storage.ProviderType(f.backendType) } func (f GenericBackendFactory) SensitiveFields() []string { return f.sensitive } func (f GenericBackendFactory) New(ctx context.Context, rawConfig map[string]any) (storage.StorageProvider, error) { root, _ := rawConfig["root"].(string) root = strings.TrimSpace(root) var b strings.Builder b.WriteString(":") b.WriteString(f.backendType) for key, val := range rawConfig { if key == "root" { continue } strVal := fmt.Sprintf("%v", val) if strings.TrimSpace(strVal) == "" { continue } b.WriteString(",") b.WriteString(key) b.WriteString("=") b.WriteString(quoteParam(strVal)) } b.WriteString(":") b.WriteString(root) return newFs(ctx, storage.ProviderType(f.backendType), b.String()) } // RegisterAllBackends 将所有 rclone 后端注册为独立 Factory 到 Registry。 // 已存在的内置类型(s3, ftp 等)不会被覆盖。 func RegisterAllBackends(registry *storage.Registry) { builtinTypes := map[string]bool{ "local_disk": true, "s3": true, "webdav": true, "google_drive": true, "ftp": true, "aliyun_oss": true, "tencent_cos": true, "qiniu_kodo": true, "rclone": true, "local": true, } for _, info := range ListBackends() { if builtinTypes[info.Name] { continue } registry.Register(NewBackendFactory(info.Name)) } }