mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-09 08:00:49 +02:00
1723 lines
59 KiB
Go
1723 lines
59 KiB
Go
package db
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/starfleetcptn/gomft/internal/encryption"
|
|
)
|
|
|
|
// --- TransferConfig Store Methods ---
|
|
|
|
// CreateTransferConfig creates a new transfer config record
|
|
func (db *DB) CreateTransferConfig(config *TransferConfig) error {
|
|
return db.Create(config).Error
|
|
}
|
|
|
|
// GetTransferConfigs retrieves all transfer configs for a user
|
|
func (db *DB) GetTransferConfigs(userID uint) ([]TransferConfig, error) {
|
|
var configs []TransferConfig
|
|
err := db.Preload("SourceProvider").Preload("DestinationProvider").Where("created_by = ?", userID).Find(&configs).Error
|
|
return configs, err
|
|
}
|
|
|
|
// GetTransferConfig retrieves a single transfer config by ID
|
|
func (db *DB) GetTransferConfig(id uint) (*TransferConfig, error) {
|
|
var config TransferConfig
|
|
err := db.Preload("SourceProvider").Preload("DestinationProvider").First(&config, id).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &config, nil
|
|
}
|
|
|
|
// UpdateTransferConfig updates an existing transfer config record
|
|
func (db *DB) UpdateTransferConfig(config *TransferConfig) error {
|
|
return db.Save(config).Error
|
|
}
|
|
|
|
// DeleteTransferConfig deletes a transfer config record after checking dependencies
|
|
func (db *DB) DeleteTransferConfig(id uint) error {
|
|
// First check if any jobs are using this config
|
|
var count int64
|
|
// Need to check both ConfigID and ConfigIDs list
|
|
// This check might need refinement depending on how ConfigIDs is used reliably
|
|
if err := db.Model(&Job{}).Where("config_id = ? OR config_ids LIKE ?", id, "%"+strconv.FormatUint(uint64(id), 10)+"%").Count(&count).Error; err != nil {
|
|
return fmt.Errorf("failed to check for dependent jobs: %v", err)
|
|
}
|
|
if count > 0 {
|
|
return fmt.Errorf("cannot delete config: %d jobs are using this configuration", count)
|
|
}
|
|
|
|
// Delete the config
|
|
return db.Delete(&TransferConfig{}, id).Error
|
|
}
|
|
|
|
// GetConfigRclonePath returns the path to the rclone config file for a given transfer config
|
|
func (db *DB) GetConfigRclonePath(config *TransferConfig) string {
|
|
// Get data directory from environment or use default
|
|
dataDir := os.Getenv("DATA_DIR")
|
|
if dataDir == "" {
|
|
dataDir = "./data"
|
|
}
|
|
|
|
// Store configs in the data directory
|
|
return filepath.Join(dataDir, "configs", fmt.Sprintf("config_%d.conf", config.ID))
|
|
}
|
|
|
|
// GenerateRcloneConfig generates the rclone config file content based on TransferConfig
|
|
// This function now primarily focuses on generating the content string or calling rclone config create
|
|
func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
|
configPath := db.GetConfigRclonePath(config)
|
|
|
|
// Get the directory part of the path
|
|
configDir := filepath.Dir(configPath)
|
|
|
|
// Ensure configs directory exists
|
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create configs directory: %v", err)
|
|
}
|
|
|
|
// Get the rclone path from the environment variable or use the default path
|
|
rclonePath := os.Getenv("RCLONE_PATH")
|
|
if rclonePath == "" {
|
|
rclonePath = "rclone"
|
|
}
|
|
|
|
// Ensure providers are loaded if using references
|
|
if config.IsUsingSourceProviderReference() && config.SourceProvider == nil {
|
|
provider, err := db.GetStorageProvider(*config.SourceProviderID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load source provider (ID %d): %v", *config.SourceProviderID, err)
|
|
}
|
|
config.SetSourceProvider(provider)
|
|
}
|
|
|
|
if config.IsUsingDestinationProviderReference() && config.DestinationProvider == nil {
|
|
provider, err := db.GetStorageProvider(*config.DestinationProviderID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load destination provider (ID %d): %v", *config.DestinationProviderID, err)
|
|
}
|
|
config.SetDestinationProvider(provider)
|
|
}
|
|
|
|
// If we have a destination provider but no ID or zero ID, fix it
|
|
if config.DestinationProvider != nil && (config.DestinationProviderID == nil || *config.DestinationProviderID == 0) {
|
|
config.SetDestinationProvider(config.DestinationProvider)
|
|
}
|
|
|
|
// If we have an ID but no provider, load it
|
|
if config.DestinationProviderID != nil && *config.DestinationProviderID > 0 && config.DestinationProvider == nil {
|
|
provider, err := db.GetStorageProvider(*config.DestinationProviderID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load destination provider (ID %d): %v", *config.DestinationProviderID, err)
|
|
}
|
|
config.SetDestinationProvider(provider)
|
|
}
|
|
|
|
// Double check that everything is synchronized
|
|
if config.IsUsingDestinationProviderReference() {
|
|
if config.DestinationProvider == nil {
|
|
return fmt.Errorf("destination provider reference is set (ID %d) but provider is nil", *config.DestinationProviderID)
|
|
}
|
|
if config.DestinationProviderID == nil || *config.DestinationProviderID != config.DestinationProvider.ID {
|
|
config.SetDestinationProvider(config.DestinationProvider) // Re-sync the ID
|
|
}
|
|
}
|
|
|
|
// Get source credentials, either from provider or directly from config
|
|
sourceCredentials, err := config.GetSourceCredentials(db)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get source credentials: %v", err)
|
|
}
|
|
|
|
// Get source type either from provider or directly from config
|
|
sourceType := config.SourceType
|
|
if sourceTypeFromCreds, ok := sourceCredentials["type"].(StorageProviderType); ok {
|
|
sourceType = string(sourceTypeFromCreds)
|
|
} else if sourceTypeFromCreds, ok := sourceCredentials["type"].(string); ok {
|
|
sourceType = sourceTypeFromCreds
|
|
}
|
|
|
|
sourceName := fmt.Sprintf("source_%d", config.ID)
|
|
fmt.Printf("Generated source name: %s\n", sourceName)
|
|
fmt.Printf("Final source type being used: %s\n", sourceType)
|
|
|
|
// Generate rclone config using rclone CLI for source
|
|
switch sourceType {
|
|
case "sftp", "hetzner":
|
|
args := []string{
|
|
"config", "create", sourceName, "sftp",
|
|
"host", getStringValue(sourceCredentials, "host", config.SourceHost),
|
|
"user", getStringValue(sourceCredentials, "username", config.SourceUser),
|
|
"port", fmt.Sprintf("%d", getIntValue(sourceCredentials, "port", config.SourcePort)),
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// First try to get password from direct form input (transient)
|
|
password := ""
|
|
if config.SourcePassword != "" {
|
|
password = config.SourcePassword
|
|
} else if encryptedPwd, ok := sourceCredentials["encrypted_password"].(string); ok && encryptedPwd != "" {
|
|
// For provider references, get the decrypted password
|
|
decryptedPwd, err := db.DecryptCredential(encryptedPwd)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt source password: %v", err)
|
|
}
|
|
password = decryptedPwd
|
|
} else if pwVal, ok := sourceCredentials["password"].(string); ok && pwVal != "" {
|
|
// For backward compatibility
|
|
password = pwVal
|
|
}
|
|
|
|
if password != "" {
|
|
args = append(args, "pass", password)
|
|
}
|
|
|
|
keyFile := getStringValue(sourceCredentials, "key_file", config.SourceKeyFile)
|
|
if keyFile != "" {
|
|
args = append(args, "key_file", keyFile)
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create source config (sftp): %v\nOutput: %s", err, output)
|
|
}
|
|
case "smb":
|
|
args := []string{
|
|
"config", "create", sourceName, "smb",
|
|
"host", getStringValue(sourceCredentials, "host", config.SourceHost),
|
|
"user", getStringValue(sourceCredentials, "username", config.SourceUser),
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Get domain if provided
|
|
domain := getStringValue(sourceCredentials, "domain", config.SourceDomain)
|
|
if domain != "" {
|
|
args = append(args, "domain", domain)
|
|
}
|
|
|
|
// Get port if specified (default is 445)
|
|
port := getIntValue(sourceCredentials, "port", config.SourcePort)
|
|
if port > 0 && port != 445 {
|
|
args = append(args, "port", fmt.Sprintf("%d", port))
|
|
}
|
|
|
|
// Get share if provided
|
|
share := getStringValue(sourceCredentials, "share", config.SourceShare)
|
|
if share != "" {
|
|
args = append(args, "share", share)
|
|
}
|
|
|
|
// Handle password
|
|
password := ""
|
|
if config.SourcePassword != "" {
|
|
password = config.SourcePassword
|
|
} else if encryptedPwd, ok := sourceCredentials["encrypted_password"].(string); ok && encryptedPwd != "" {
|
|
decryptedPwd, err := db.DecryptCredential(encryptedPwd)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt source password: %v", err)
|
|
}
|
|
password = decryptedPwd
|
|
} else if pwVal, ok := sourceCredentials["password"].(string); ok && pwVal != "" {
|
|
password = pwVal
|
|
}
|
|
|
|
if password != "" {
|
|
args = append(args, "pass", password)
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create source config (smb): %v\nOutput: %s", err, output)
|
|
}
|
|
case "s3":
|
|
args := []string{
|
|
"config", "create", sourceName, "s3",
|
|
"provider", "AWS", // Assuming AWS provider, adjust if needed
|
|
"env_auth", "false",
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Handle access key
|
|
accessKey := getStringValue(sourceCredentials, "access_key", config.SourceAccessKey)
|
|
if accessKey != "" {
|
|
args = append(args, "access_key_id", accessKey)
|
|
}
|
|
|
|
// Handle secret key with proper decryption if from provider
|
|
secretKey := ""
|
|
if config.SourceSecretKey != "" {
|
|
// Direct input from form (transient)
|
|
secretKey = config.SourceSecretKey
|
|
} else if encryptedSecret, ok := sourceCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
|
// Provider reference with encrypted secret
|
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt source secret key: %v", err)
|
|
}
|
|
secretKey = decryptedSecret
|
|
} else if secretVal, ok := sourceCredentials["secret_key"].(string); ok && secretVal != "" {
|
|
// Backward compatibility
|
|
secretKey = secretVal
|
|
}
|
|
|
|
if secretKey != "" {
|
|
args = append(args, "secret_access_key", secretKey)
|
|
}
|
|
|
|
// Add region
|
|
region := getStringValue(sourceCredentials, "region", config.SourceRegion)
|
|
if region != "" {
|
|
args = append(args, "region", region)
|
|
}
|
|
|
|
endpoint := getStringValue(sourceCredentials, "endpoint", config.SourceEndpoint)
|
|
if endpoint != "" {
|
|
args = append(args, "endpoint", endpoint)
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create source config (s3): %v\nOutput: %s", err, output)
|
|
}
|
|
case "wasabi":
|
|
args := []string{
|
|
"config", "create", sourceName, "s3",
|
|
"provider", "Wasabi",
|
|
"env_auth", "false",
|
|
"access_key_id", getStringValue(sourceCredentials, "access_key", config.SourceAccessKey),
|
|
"secret_access_key", getStringOrDefault(sourceCredentials, "secret_key", config.SourceSecretKey),
|
|
"region", getStringValue(sourceCredentials, "region", config.SourceRegion),
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
endpoint := getStringValue(sourceCredentials, "endpoint", config.SourceEndpoint)
|
|
if endpoint == "" {
|
|
endpoint = "s3.wasabisys.com"
|
|
}
|
|
|
|
args = append(args, "endpoint", endpoint)
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create source config (wasabi): %v\nOutput: %s", err, output)
|
|
}
|
|
case "b2":
|
|
args := []string{
|
|
"config", "create", sourceName, "b2",
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
// Handle account ID (access key)
|
|
accountID := getStringValue(sourceCredentials, "access_key", config.SourceAccessKey)
|
|
if accountID != "" {
|
|
args = append(args, "account", accountID)
|
|
}
|
|
|
|
// Handle secret key with proper decryption if from provider
|
|
secretKey := ""
|
|
if config.SourceSecretKey != "" {
|
|
// Direct input from form (transient)
|
|
secretKey = config.SourceSecretKey
|
|
} else if encryptedSecret, ok := sourceCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
|
// Provider reference with encrypted secret
|
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt source secret key: %v", err)
|
|
}
|
|
secretKey = decryptedSecret
|
|
} else if secretVal, ok := sourceCredentials["secret_key"].(string); ok && secretVal != "" {
|
|
// Backward compatibility
|
|
secretKey = secretVal
|
|
}
|
|
|
|
if secretKey != "" {
|
|
args = append(args, "key", secretKey)
|
|
}
|
|
if config.SourceEndpoint != "" {
|
|
args = append(args, "endpoint", config.SourceEndpoint)
|
|
}
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create source config (b2): %v\nOutput: %s", err, output)
|
|
}
|
|
case "minio":
|
|
args := []string{
|
|
"config", "create", sourceName, "s3",
|
|
"provider", "Minio",
|
|
"env_auth", "false",
|
|
"access_key_id", getStringValue(sourceCredentials, "access_key", config.SourceAccessKey),
|
|
"endpoint", getStringValue(sourceCredentials, "endpoint", config.SourceEndpoint),
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
// Handle secret key with proper decryption if from provider
|
|
secretKey := ""
|
|
if config.SourceSecretKey != "" {
|
|
// Direct input from form (transient)
|
|
secretKey = config.SourceSecretKey
|
|
} else if encryptedSecret, ok := sourceCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
|
// Provider reference with encrypted secret
|
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt source secret key: %v", err)
|
|
}
|
|
secretKey = decryptedSecret
|
|
} else if secretVal, ok := sourceCredentials["secret_key"].(string); ok && secretVal != "" {
|
|
// Backward compatibility
|
|
secretKey = secretVal
|
|
}
|
|
|
|
if secretKey != "" {
|
|
args = append(args, "secret_access_key", secretKey)
|
|
}
|
|
|
|
// Add region if specified
|
|
if getStringValue(sourceCredentials, "region", config.SourceRegion) != "" {
|
|
args = append(args, "region", getStringValue(sourceCredentials, "region", config.SourceRegion))
|
|
}
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create source config (minio): %v\nOutput: %s", err, output)
|
|
}
|
|
case "webdav", "nextcloud": // Handle both webdav and nextcloud similarly
|
|
// Construct the WebDAV URL
|
|
// Parse the provided source URL, assuming it includes the scheme
|
|
inputURL := getStringValue(sourceCredentials, "host", config.SourceHost)
|
|
fmt.Printf("Input URL: %s\n", inputURL)
|
|
parsedURL, err := url.Parse(inputURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse source URL '%s': %v", inputURL, err)
|
|
}
|
|
// Validate that both scheme and host are present
|
|
if parsedURL.Scheme == "" || parsedURL.Host == "" {
|
|
return fmt.Errorf("invalid source URL '%s': must include scheme (http/https) and host", inputURL)
|
|
}
|
|
// Use the scheme and host from the parsed URL
|
|
webdavURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
|
|
|
|
// Determine vendor based on type
|
|
vendor := "other" // Default vendor
|
|
if config.SourceType == "nextcloud" {
|
|
vendor = "nextcloud"
|
|
|
|
// Construct the full Nextcloud path using the parsed base URL
|
|
webdavURL = fmt.Sprintf("%s/remote.php/dav/files/%s/", webdavURL, config.SourceUser)
|
|
}
|
|
|
|
args := []string{
|
|
"config", "create", sourceName, "webdav",
|
|
"url", webdavURL,
|
|
"vendor", vendor,
|
|
"user", getStringValue(sourceCredentials, "username", config.SourceUser),
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Handle password
|
|
password := ""
|
|
if config.SourcePassword != "" {
|
|
password = config.SourcePassword
|
|
} else if encryptedPwd, ok := sourceCredentials["encrypted_password"].(string); ok && encryptedPwd != "" {
|
|
decryptedPwd, err := db.DecryptCredential(encryptedPwd)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt source password: %v", err)
|
|
}
|
|
password = decryptedPwd
|
|
} else if pwVal, ok := sourceCredentials["password"].(string); ok && pwVal != "" {
|
|
password = pwVal
|
|
}
|
|
|
|
if password != "" {
|
|
args = append(args, "pass", password)
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
errorMsg := fmt.Sprintf("failed to create source config (%s): %v", config.SourceType, err)
|
|
// Check if output contains useful info, especially for auth errors
|
|
if len(output) > 0 {
|
|
errorMsg += fmt.Sprintf("\nOutput: %s", output)
|
|
}
|
|
return fmt.Errorf("%v", errorMsg)
|
|
}
|
|
|
|
case "local":
|
|
// For local source, ensure the section exists but might not need specific rclone config create
|
|
content := fmt.Sprintf("[%s]\ntype = local\n\n", sourceName)
|
|
if err := os.WriteFile(configPath, []byte(content), 0600); err != nil {
|
|
return fmt.Errorf("failed to write source config (local): %v", err)
|
|
}
|
|
case "drive":
|
|
// For Google Drive, we need client ID and secret
|
|
clientID := getStringValue(sourceCredentials, "client_id", config.SourceClientID)
|
|
|
|
// Get client secret with proper decryption if from provider
|
|
clientSecret := ""
|
|
if config.SourceClientSecret != "" {
|
|
// Direct input from form (transient)
|
|
clientSecret = config.SourceClientSecret
|
|
} else if encryptedSecret, ok := sourceCredentials["encrypted_client_secret"].(string); ok && encryptedSecret != "" {
|
|
// Provider reference with encrypted secret
|
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt source client secret: %v", err)
|
|
}
|
|
clientSecret = decryptedSecret
|
|
}
|
|
|
|
// Get refresh token if available
|
|
refreshToken := getStringOrDefault(sourceCredentials, "token", "")
|
|
if refreshToken == "" {
|
|
refreshToken = getStringOrDefault(sourceCredentials, "refresh_token", "")
|
|
}
|
|
|
|
if refreshToken == "" {
|
|
if encryptedToken, ok := sourceCredentials["encrypted_refresh_token"].(string); ok && encryptedToken != "" {
|
|
decryptedToken, err := db.DecryptCredential(encryptedToken)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt source refresh token: %v", err)
|
|
}
|
|
refreshToken = decryptedToken
|
|
}
|
|
}
|
|
|
|
// If not found in credentials, check if using a provider reference
|
|
if refreshToken == "" && config.IsUsingSourceProviderReference() && config.SourceProvider != nil {
|
|
refreshToken = config.SourceProvider.RefreshToken
|
|
}
|
|
|
|
// Clean up the token
|
|
if refreshToken != "" {
|
|
refreshToken = strings.TrimSpace(refreshToken)
|
|
refreshToken = strings.ReplaceAll(refreshToken, "\n", "")
|
|
refreshToken = strings.ReplaceAll(refreshToken, "\r", "")
|
|
refreshToken = strings.Join(strings.Fields(refreshToken), "")
|
|
}
|
|
|
|
// Create rclone config for Google Drive
|
|
args := []string{
|
|
"config", "create", sourceName, "drive",
|
|
"client_id", clientID,
|
|
"client_secret", clientSecret,
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Add team drive or drive ID if specified
|
|
teamDrive := getStringValue(sourceCredentials, "team_drive", config.SourceTeamDrive)
|
|
if teamDrive != "" {
|
|
args = append(args, "team_drive", teamDrive)
|
|
}
|
|
|
|
driveID := getStringValue(sourceCredentials, "drive_id", config.SourceDriveID)
|
|
if driveID != "" {
|
|
args = append(args, "drive_id", driveID)
|
|
}
|
|
|
|
// If we have a refresh token, add it
|
|
if refreshToken != "" {
|
|
args = append(args, "token", fmt.Sprintf("%s", refreshToken))
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
errorMsg := fmt.Sprintf("failed to create source config (drive): %v", err)
|
|
// Check if output contains useful info, especially for auth errors
|
|
if len(output) > 0 {
|
|
errorMsg += fmt.Sprintf("\nOutput: %s", output)
|
|
}
|
|
return fmt.Errorf("%v", errorMsg)
|
|
}
|
|
case "gphotos":
|
|
// For Google Photos, we need client ID and secret
|
|
clientID := getStringValue(sourceCredentials, "client_id", config.SourceClientID)
|
|
|
|
// Get client secret with proper decryption if from provider
|
|
clientSecret := ""
|
|
if config.SourceClientSecret != "" {
|
|
// Direct input from form (transient)
|
|
clientSecret = config.SourceClientSecret
|
|
} else if encryptedSecret, ok := sourceCredentials["encrypted_client_secret"].(string); ok && encryptedSecret != "" {
|
|
// Provider reference with encrypted secret
|
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt source client secret: %v", err)
|
|
}
|
|
clientSecret = decryptedSecret
|
|
}
|
|
|
|
// Get refresh token if available
|
|
refreshToken := getStringOrDefault(sourceCredentials, "token", "")
|
|
if refreshToken == "" {
|
|
refreshToken = getStringOrDefault(sourceCredentials, "refresh_token", "")
|
|
}
|
|
|
|
if refreshToken == "" {
|
|
if encryptedToken, ok := sourceCredentials["encrypted_refresh_token"].(string); ok && encryptedToken != "" {
|
|
decryptedToken, err := db.DecryptCredential(encryptedToken)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt source refresh token: %v", err)
|
|
}
|
|
refreshToken = decryptedToken
|
|
}
|
|
}
|
|
|
|
// Clean up the token
|
|
if refreshToken != "" {
|
|
refreshToken = strings.TrimSpace(refreshToken)
|
|
refreshToken = strings.ReplaceAll(refreshToken, "\n", "")
|
|
refreshToken = strings.ReplaceAll(refreshToken, "\r", "")
|
|
refreshToken = strings.Join(strings.Fields(refreshToken), "")
|
|
}
|
|
|
|
// Create rclone config for Google Photos
|
|
args := []string{
|
|
"config", "create", sourceName, "gphotos",
|
|
"client_id", clientID,
|
|
"client_secret", clientSecret,
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Add read-only flag if specified
|
|
readOnly := false
|
|
if readOnlyVal, ok := sourceCredentials["read_only"].(bool); ok {
|
|
readOnly = readOnlyVal
|
|
} else if config.SourceReadOnly != nil {
|
|
readOnly = *config.SourceReadOnly
|
|
}
|
|
if readOnly {
|
|
args = append(args, "read_only", "true")
|
|
}
|
|
|
|
// Add start year if specified
|
|
startYear := getIntValue(sourceCredentials, "start_year", config.SourceStartYear)
|
|
if startYear > 0 {
|
|
args = append(args, "start_year", fmt.Sprintf("%d", startYear))
|
|
}
|
|
|
|
// Add include archived if specified
|
|
includeArchived := false
|
|
if includeArchivedVal, ok := sourceCredentials["include_archived"].(bool); ok {
|
|
includeArchived = includeArchivedVal
|
|
} else if config.SourceIncludeArchived != nil {
|
|
includeArchived = *config.SourceIncludeArchived
|
|
}
|
|
if includeArchived {
|
|
args = append(args, "include_archived", "true")
|
|
}
|
|
|
|
// If we have a refresh token, add it
|
|
if refreshToken != "" {
|
|
args = append(args, "token", fmt.Sprintf("%s", refreshToken))
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
errorMsg := fmt.Sprintf("failed to create source config (gphotos): %v", err)
|
|
// Check if output contains useful info, especially for auth errors
|
|
if len(output) > 0 {
|
|
errorMsg += fmt.Sprintf("\nOutput: %s", output)
|
|
}
|
|
return fmt.Errorf("%v", errorMsg)
|
|
}
|
|
default:
|
|
// Handle unknown or unsupported source types if necessary
|
|
return fmt.Errorf("unsupported source type for rclone config generation: %s", config.SourceType)
|
|
|
|
}
|
|
|
|
// Get destination credentials, either from provider or directly from config
|
|
destCredentials, err := config.GetDestinationCredentials(db)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get destination credentials: %v", err)
|
|
}
|
|
|
|
// Get destination type either from provider or directly from config
|
|
destType := config.DestinationType
|
|
if destTypeFromCreds, ok := destCredentials["type"].(StorageProviderType); ok {
|
|
destType = string(destTypeFromCreds)
|
|
} else if destTypeFromCreds, ok := destCredentials["type"].(string); ok {
|
|
destType = destTypeFromCreds
|
|
}
|
|
|
|
destName := fmt.Sprintf("dest_%d", config.ID)
|
|
// Generate rclone config using rclone CLI for destination
|
|
switch destType {
|
|
case "sftp", "hetzner":
|
|
args := []string{
|
|
"config", "create", destName, "sftp",
|
|
"host", getStringValue(destCredentials, "host", config.DestHost),
|
|
"user", getStringValue(destCredentials, "username", config.DestUser),
|
|
"port", fmt.Sprintf("%d", getIntValue(destCredentials, "port", config.DestPort)),
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
password := ""
|
|
if config.DestPassword != "" {
|
|
password = config.DestPassword
|
|
} else if encryptedPwd, ok := destCredentials["encrypted_password"].(string); ok && encryptedPwd != "" {
|
|
// For provider references, get the decrypted password
|
|
decryptedPwd, err := db.DecryptCredential(encryptedPwd)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt destination password: %v", err)
|
|
}
|
|
password = decryptedPwd
|
|
} else if pwVal, ok := destCredentials["password"].(string); ok && pwVal != "" {
|
|
// For backward compatibility
|
|
password = pwVal
|
|
}
|
|
|
|
if password != "" {
|
|
args = append(args, "pass", password)
|
|
}
|
|
|
|
keyFile := getStringValue(destCredentials, "key_file", config.DestKeyFile)
|
|
if keyFile != "" {
|
|
args = append(args, "key_file", keyFile)
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create destination config (sftp): %v\nOutput: %s", err, output)
|
|
}
|
|
case "smb":
|
|
args := []string{
|
|
"config", "create", destName, "smb",
|
|
"host", getStringValue(destCredentials, "host", config.DestHost),
|
|
"user", getStringValue(destCredentials, "username", config.DestUser),
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Get domain if provided
|
|
domain := getStringValue(destCredentials, "domain", config.DestDomain)
|
|
if domain != "" {
|
|
args = append(args, "domain", domain)
|
|
}
|
|
|
|
// Get port if specified (default is 445)
|
|
port := getIntValue(destCredentials, "port", config.DestPort)
|
|
if port > 0 && port != 445 {
|
|
args = append(args, "port", fmt.Sprintf("%d", port))
|
|
}
|
|
|
|
// Get share if provided
|
|
share := getStringValue(destCredentials, "share", config.DestShare)
|
|
if share != "" {
|
|
args = append(args, "share", share)
|
|
}
|
|
|
|
// Handle password
|
|
password := ""
|
|
if config.DestPassword != "" {
|
|
password = config.DestPassword
|
|
} else if encryptedPwd, ok := destCredentials["encrypted_password"].(string); ok && encryptedPwd != "" {
|
|
decryptedPwd, err := db.DecryptCredential(encryptedPwd)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt destination password: %v", err)
|
|
}
|
|
password = decryptedPwd
|
|
} else if pwVal, ok := destCredentials["password"].(string); ok && pwVal != "" {
|
|
password = pwVal
|
|
}
|
|
|
|
if password != "" {
|
|
args = append(args, "pass", password)
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create destination config (smb): %v\nOutput: %s", err, output)
|
|
}
|
|
case "s3":
|
|
args := []string{
|
|
"config", "create", destName, "s3",
|
|
"provider", "AWS", // Assuming AWS provider
|
|
"env_auth", "false",
|
|
"access_key_id", getStringValue(destCredentials, "access_key", config.DestAccessKey),
|
|
"region", getStringValue(destCredentials, "region", config.DestRegion),
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Handle secret key with proper decryption if from provider
|
|
secretKey := ""
|
|
if config.DestSecretKey != "" {
|
|
// Direct input from form (transient)
|
|
secretKey = config.DestSecretKey
|
|
} else if encryptedSecret, ok := destCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
|
// Provider reference with encrypted secret
|
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt destination secret key: %v", err)
|
|
}
|
|
secretKey = decryptedSecret
|
|
} else if secretVal, ok := destCredentials["secret_key"].(string); ok && secretVal != "" {
|
|
// Backward compatibility
|
|
secretKey = secretVal
|
|
}
|
|
|
|
if secretKey != "" {
|
|
args = append(args, "secret_access_key", secretKey)
|
|
}
|
|
|
|
endpoint := getStringValue(destCredentials, "endpoint", config.DestEndpoint)
|
|
if endpoint != "" {
|
|
args = append(args, "endpoint", endpoint)
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create destination config (s3): %v\nOutput: %s", err, output)
|
|
}
|
|
case "wasabi":
|
|
args := []string{
|
|
"config", "create", destName, "s3",
|
|
"provider", "Wasabi",
|
|
"env_auth", "false",
|
|
"access_key_id", getStringValue(destCredentials, "access_key", config.DestAccessKey),
|
|
"region", getStringValue(destCredentials, "region", config.DestRegion),
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Handle secret key with proper decryption if from provider
|
|
secretKey := ""
|
|
if config.DestSecretKey != "" {
|
|
// Direct input from form (transient)
|
|
secretKey = config.DestSecretKey
|
|
} else if encryptedSecret, ok := destCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
|
// Provider reference with encrypted secret
|
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt destination secret key: %v", err)
|
|
}
|
|
secretKey = decryptedSecret
|
|
} else if secretVal, ok := destCredentials["secret_key"].(string); ok && secretVal != "" {
|
|
// Backward compatibility
|
|
secretKey = secretVal
|
|
}
|
|
|
|
if secretKey != "" {
|
|
args = append(args, "secret_access_key", secretKey)
|
|
}
|
|
|
|
endpoint := getStringValue(destCredentials, "endpoint", config.DestEndpoint)
|
|
if endpoint == "" {
|
|
endpoint = "s3.wasabisys.com"
|
|
}
|
|
|
|
args = append(args, "endpoint", endpoint)
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create destination config (wasabi): %v\nOutput: %s", err, output)
|
|
}
|
|
case "b2":
|
|
args := []string{
|
|
"config", "create", destName, "b2",
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Handle account ID (access key)
|
|
accountID := getStringValue(destCredentials, "access_key", config.DestAccessKey)
|
|
if accountID != "" {
|
|
args = append(args, "account", accountID)
|
|
}
|
|
|
|
// Handle secret key with proper decryption if from provider
|
|
secretKey := ""
|
|
if config.DestSecretKey != "" {
|
|
// Direct input from form (transient)
|
|
secretKey = config.DestSecretKey
|
|
} else if encryptedSecret, ok := destCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
|
// Provider reference with encrypted secret
|
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt destination secret key: %v", err)
|
|
}
|
|
secretKey = decryptedSecret
|
|
} else if secretVal, ok := destCredentials["secret_key"].(string); ok && secretVal != "" {
|
|
// Backward compatibility
|
|
secretKey = secretVal
|
|
}
|
|
|
|
if secretKey != "" {
|
|
args = append(args, "key", secretKey)
|
|
}
|
|
|
|
// Handle application key (secret key) with proper decryption if from provider
|
|
appKey := ""
|
|
if config.DestSecretKey != "" {
|
|
// Direct input from form (transient)
|
|
appKey = config.DestSecretKey
|
|
} else if encryptedSecret, ok := destCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
|
// Provider reference with encrypted secret
|
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt destination secret key: %v", err)
|
|
}
|
|
appKey = decryptedSecret
|
|
} else if secretVal, ok := destCredentials["secret_key"].(string); ok && secretVal != "" {
|
|
// Backward compatibility
|
|
appKey = secretVal
|
|
}
|
|
|
|
if appKey != "" {
|
|
args = append(args, "key", appKey)
|
|
}
|
|
|
|
endpoint := getStringValue(destCredentials, "endpoint", config.DestEndpoint)
|
|
if endpoint != "" {
|
|
args = append(args, "endpoint", endpoint)
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create destination config (b2): %v\nOutput: %s", err, output)
|
|
}
|
|
case "minio":
|
|
args := []string{
|
|
"config", "create", destName, "s3",
|
|
"provider", "Minio",
|
|
"env_auth", "false",
|
|
"access_key_id", getStringValue(destCredentials, "access_key", config.DestAccessKey),
|
|
"endpoint", getStringValue(destCredentials, "endpoint", config.DestEndpoint),
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Handle secret key with proper decryption if from provider
|
|
secretKey := ""
|
|
if config.DestSecretKey != "" {
|
|
// Direct input from form (transient)
|
|
secretKey = config.DestSecretKey
|
|
} else if encryptedSecret, ok := destCredentials["encrypted_secret_key"].(string); ok && encryptedSecret != "" {
|
|
// Provider reference with encrypted secret
|
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt destination secret key: %v", err)
|
|
}
|
|
secretKey = decryptedSecret
|
|
} else if secretVal, ok := destCredentials["secret_key"].(string); ok && secretVal != "" {
|
|
// Backward compatibility
|
|
secretKey = secretVal
|
|
}
|
|
|
|
if secretKey != "" {
|
|
args = append(args, "secret_access_key", secretKey)
|
|
}
|
|
|
|
// Add region if specified
|
|
if getStringValue(destCredentials, "region", config.DestRegion) != "" {
|
|
args = append(args, "region", getStringValue(destCredentials, "region", config.DestRegion))
|
|
}
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to create destination config (minio): %v\nOutput: %s", err, output)
|
|
}
|
|
case "webdav", "nextcloud": // Combined case for WebDAV and Nextcloud
|
|
// Parse and reconstruct the WebDAV URL robustly
|
|
// Parse the provided destination URL, assuming it includes the scheme
|
|
inputURL := getStringValue(destCredentials, "host", config.DestHost)
|
|
parsedURL, err := url.Parse(inputURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse destination URL '%s': %v", inputURL, err)
|
|
}
|
|
// Validate that both scheme and host are present
|
|
if parsedURL.Scheme == "" || parsedURL.Host == "" {
|
|
return fmt.Errorf("invalid destination URL '%s': must include scheme (http/https) and host", inputURL)
|
|
}
|
|
// Use the scheme and host from the parsed URL
|
|
webdavURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
|
|
|
|
// Determine vendor based on type
|
|
vendor := "other" // Default vendor
|
|
if config.DestinationType == "nextcloud" {
|
|
vendor = "nextcloud"
|
|
|
|
webdavURL = fmt.Sprintf("%s/remote.php/dav/files/%s/", webdavURL, getStringValue(destCredentials, "username", config.DestUser)) // Corrected variable
|
|
}
|
|
|
|
args := []string{
|
|
"config", "create", destName, "webdav",
|
|
"url", webdavURL, // Use the parsed and reconstructed URL
|
|
"vendor", vendor,
|
|
"user", getStringValue(destCredentials, "username", config.DestUser),
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Handle password with proper decryption if from provider
|
|
password := ""
|
|
if config.DestPassword != "" {
|
|
// Direct input from form (transient)
|
|
password = config.DestPassword
|
|
} else if encryptedPwd, ok := destCredentials["encrypted_password"].(string); ok && encryptedPwd != "" {
|
|
// Provider reference with encrypted password
|
|
decryptedPwd, err := db.DecryptCredential(encryptedPwd)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt destination password: %v", err)
|
|
}
|
|
password = decryptedPwd
|
|
} else if pwVal, ok := destCredentials["password"].(string); ok && pwVal != "" {
|
|
// Backward compatibility
|
|
password = pwVal
|
|
}
|
|
|
|
if password != "" {
|
|
args = append(args, "pass", password)
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
errorMsg := fmt.Sprintf("failed to create destination config (%s): %v", config.DestinationType, err)
|
|
if len(output) > 0 {
|
|
errorMsg += fmt.Sprintf("\nOutput: %s", output)
|
|
}
|
|
return fmt.Errorf("%v", errorMsg)
|
|
}
|
|
case "local":
|
|
// Append local config section
|
|
content := fmt.Sprintf("\n[%s]\ntype = local\n", destName)
|
|
f, err := os.OpenFile(configPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open config file for appending (local dest): %v", err)
|
|
}
|
|
defer f.Close()
|
|
if _, err := f.WriteString(content); err != nil {
|
|
return fmt.Errorf("failed to write destination config (local): %v", err)
|
|
}
|
|
case "drive":
|
|
// For Google Drive, we need client ID and secret
|
|
clientID := getStringValue(destCredentials, "client_id", config.DestClientID)
|
|
|
|
// Get client secret with proper decryption if from provider
|
|
clientSecret := ""
|
|
if config.DestClientSecret != "" {
|
|
// Direct input from form (transient)
|
|
clientSecret = config.DestClientSecret
|
|
} else if encryptedSecret, ok := destCredentials["encrypted_client_secret"].(string); ok && encryptedSecret != "" {
|
|
// Provider reference with encrypted secret
|
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt destination client secret: %v", err)
|
|
}
|
|
clientSecret = decryptedSecret
|
|
}
|
|
|
|
// Get refresh token if available
|
|
refreshToken := getStringOrDefault(destCredentials, "token", "")
|
|
if refreshToken == "" {
|
|
refreshToken = getStringOrDefault(destCredentials, "refresh_token", "")
|
|
}
|
|
|
|
if refreshToken == "" {
|
|
if encryptedToken, ok := destCredentials["encrypted_refresh_token"].(string); ok && encryptedToken != "" {
|
|
decryptedToken, err := db.DecryptCredential(encryptedToken)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt source refresh token: %v", err)
|
|
}
|
|
refreshToken = decryptedToken
|
|
}
|
|
}
|
|
|
|
// Clean up the token
|
|
if refreshToken != "" {
|
|
refreshToken = strings.TrimSpace(refreshToken)
|
|
refreshToken = strings.ReplaceAll(refreshToken, "\n", "")
|
|
refreshToken = strings.ReplaceAll(refreshToken, "\r", "")
|
|
refreshToken = strings.Join(strings.Fields(refreshToken), "")
|
|
}
|
|
|
|
// Create rclone config for Google Drive
|
|
args := []string{
|
|
"config", "create", destName, "drive",
|
|
"client_id", clientID,
|
|
"client_secret", clientSecret,
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Add team drive or drive ID if specified
|
|
teamDrive := getStringValue(destCredentials, "team_drive", config.DestTeamDrive)
|
|
if teamDrive != "" {
|
|
args = append(args, "team_drive", teamDrive)
|
|
}
|
|
|
|
driveID := getStringValue(destCredentials, "drive_id", config.DestDriveID)
|
|
if driveID != "" {
|
|
args = append(args, "drive_id", driveID)
|
|
}
|
|
|
|
// If we have a refresh token, add it
|
|
if refreshToken != "" {
|
|
args = append(args, "token", fmt.Sprintf("%s", refreshToken))
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
errorMsg := fmt.Sprintf("failed to create destination config (drive): %v", err)
|
|
// Check if output contains useful info, especially for auth errors
|
|
if len(output) > 0 {
|
|
errorMsg += fmt.Sprintf("\nOutput: %s", output)
|
|
}
|
|
return fmt.Errorf("%v", errorMsg)
|
|
}
|
|
case "gphotos":
|
|
// For Google Photos, we need client ID and secret
|
|
clientID := getStringValue(destCredentials, "client_id", config.DestClientID)
|
|
|
|
// Get client secret with proper decryption if from provider
|
|
clientSecret := ""
|
|
if config.DestClientSecret != "" {
|
|
// Direct input from form (transient)
|
|
clientSecret = config.DestClientSecret
|
|
} else if encryptedSecret, ok := destCredentials["encrypted_client_secret"].(string); ok && encryptedSecret != "" {
|
|
// Provider reference with encrypted secret
|
|
decryptedSecret, err := db.DecryptCredential(encryptedSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt destination client secret: %v", err)
|
|
}
|
|
clientSecret = decryptedSecret
|
|
}
|
|
|
|
// Get refresh token if available
|
|
refreshToken := getStringOrDefault(destCredentials, "token", "")
|
|
if refreshToken == "" {
|
|
refreshToken = getStringOrDefault(destCredentials, "refresh_token", "")
|
|
}
|
|
|
|
if refreshToken == "" {
|
|
if encryptedToken, ok := destCredentials["encrypted_refresh_token"].(string); ok && encryptedToken != "" {
|
|
decryptedToken, err := db.DecryptCredential(encryptedToken)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt source refresh token: %v", err)
|
|
}
|
|
refreshToken = decryptedToken
|
|
}
|
|
}
|
|
|
|
// Clean up the token
|
|
if refreshToken != "" {
|
|
refreshToken = strings.TrimSpace(refreshToken)
|
|
refreshToken = strings.ReplaceAll(refreshToken, "\n", "")
|
|
refreshToken = strings.ReplaceAll(refreshToken, "\r", "")
|
|
}
|
|
|
|
// Create rclone config for Google Photos
|
|
args := []string{
|
|
"config", "create", destName, "gphotos",
|
|
"client_id", clientID,
|
|
"client_secret", clientSecret,
|
|
"--non-interactive",
|
|
"--config", configPath,
|
|
"--log-level", "ERROR",
|
|
}
|
|
|
|
// Add read-only flag if specified
|
|
readOnly := false
|
|
if readOnlyVal, ok := destCredentials["read_only"].(bool); ok {
|
|
readOnly = readOnlyVal
|
|
} else if config.DestReadOnly != nil {
|
|
readOnly = *config.DestReadOnly
|
|
}
|
|
if readOnly {
|
|
args = append(args, "read_only", "true")
|
|
}
|
|
|
|
// Add start year if specified
|
|
startYear := getIntValue(destCredentials, "start_year", config.DestStartYear)
|
|
if startYear > 0 {
|
|
args = append(args, "start_year", fmt.Sprintf("%d", startYear))
|
|
}
|
|
|
|
// Add include archived if specified
|
|
includeArchived := false
|
|
if includeArchivedVal, ok := destCredentials["include_archived"].(bool); ok {
|
|
includeArchived = includeArchivedVal
|
|
} else if config.DestIncludeArchived != nil {
|
|
includeArchived = *config.DestIncludeArchived
|
|
}
|
|
if includeArchived {
|
|
args = append(args, "include_archived", "true")
|
|
}
|
|
|
|
// If we have a refresh token, add it
|
|
if refreshToken != "" {
|
|
args = append(args, "token", fmt.Sprintf("%s", refreshToken))
|
|
}
|
|
|
|
cmd := exec.Command(rclonePath, args...)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
errorMsg := fmt.Sprintf("failed to create destination config (gphotos): %v", err)
|
|
// Check if output contains useful info, especially for auth errors
|
|
if len(output) > 0 {
|
|
errorMsg += fmt.Sprintf("\nOutput: %s", output)
|
|
}
|
|
return fmt.Errorf("%v", errorMsg)
|
|
}
|
|
default:
|
|
// Handle unknown or unsupported destination types if necessary
|
|
return fmt.Errorf("unsupported destination type for rclone config generation: %s", config.DestinationType)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper functions to get values from credentials map
|
|
func getStringValue(creds map[string]interface{}, key, defaultValue string) string {
|
|
if val, ok := creds[key].(string); ok && val != "" {
|
|
return val
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func getStringOrDefault(creds map[string]interface{}, key, defaultValue string) string {
|
|
if defaultValue != "" {
|
|
return defaultValue // Prefer the value passed directly for sensitive fields
|
|
}
|
|
if val, ok := creds[key].(string); ok {
|
|
return val
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getIntValue(creds map[string]interface{}, key string, defaultValue int) int {
|
|
if val, ok := creds[key].(int); ok {
|
|
return val
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// StoreGoogleDriveToken stores the Google Drive auth token for a config
|
|
func (db *DB) StoreGoogleDriveToken(configIDStr string, token string) error {
|
|
// Remove all whitespace to ensure the token is a single line
|
|
token = strings.Join(strings.Fields(token), "")
|
|
configID, err := strconv.ParseUint(configIDStr, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid config ID: %v", err)
|
|
}
|
|
|
|
config, err := db.GetTransferConfig(uint(configID))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get config: %v", err)
|
|
}
|
|
|
|
authenticated := true
|
|
config.GoogleDriveAuthenticated = &authenticated
|
|
|
|
if err := db.UpdateTransferConfig(config); err != nil {
|
|
return fmt.Errorf("failed to update config: %v", err)
|
|
}
|
|
|
|
// Check if we're using a provider reference and update the provider instead
|
|
if config.IsUsingDestinationProviderReference() && config.DestinationProvider != nil &&
|
|
(config.DestinationProvider.Type == "drive" || config.DestinationProvider.Type == "gphotos") {
|
|
// Update the provider with the token
|
|
provider := config.DestinationProvider
|
|
provider.RefreshToken = token // Set the clear token temporarily
|
|
provider.SetAuthenticated(true)
|
|
|
|
// Update the provider in the database
|
|
if err := db.UpdateStorageProvider(provider); err != nil {
|
|
return fmt.Errorf("failed to update provider with token: %v", err)
|
|
}
|
|
|
|
// Continue with creating the rclone config file since this is still needed for transfers
|
|
} else if config.IsUsingSourceProviderReference() && config.SourceProvider != nil &&
|
|
(config.SourceProvider.Type == "drive" || config.SourceProvider.Type == "gphotos") {
|
|
// Update the provider with the token
|
|
provider := config.SourceProvider
|
|
provider.RefreshToken = token // Set the clear token temporarily
|
|
provider.SetAuthenticated(true)
|
|
|
|
// Update the provider in the database
|
|
if err := db.UpdateStorageProvider(provider); err != nil {
|
|
return fmt.Errorf("failed to update provider with token: %v", err)
|
|
}
|
|
|
|
// Continue with creating the rclone config file since this is still needed for transfers
|
|
}
|
|
|
|
// Legacy fallback for direct token storage in config file
|
|
configPath := db.GetConfigRclonePath(config)
|
|
existingConfig := ""
|
|
if _, err := os.Stat(configPath); err == nil {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read existing config: %v", err)
|
|
}
|
|
existingConfig = string(data)
|
|
}
|
|
|
|
configDir := filepath.Dir(configPath)
|
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create config directory: %v", err)
|
|
}
|
|
|
|
destName := fmt.Sprintf("dest_%d", config.ID)
|
|
newConfig := fmt.Sprintf("[%s]\ntype = drive\ntoken = %s\n", destName, token)
|
|
|
|
if config.DestClientID != "" && config.DestClientSecret != "" {
|
|
newConfig += fmt.Sprintf("client_id = %s\nclient_secret = %s\n", config.DestClientID, config.DestClientSecret)
|
|
}
|
|
if config.DestDriveID != "" {
|
|
newConfig += fmt.Sprintf("root_folder_id = %s\n", config.DestDriveID)
|
|
}
|
|
if config.DestTeamDrive != "" {
|
|
newConfig += fmt.Sprintf("team_drive = %s\n", config.DestTeamDrive)
|
|
}
|
|
|
|
var content string
|
|
sectionHeader := fmt.Sprintf("[%s]", destName)
|
|
if strings.Contains(existingConfig, sectionHeader) {
|
|
parts := strings.SplitN(existingConfig, sectionHeader, 2)
|
|
nextSectionIdx := strings.Index(parts[1], "[")
|
|
if nextSectionIdx != -1 {
|
|
content = parts[0] + newConfig + parts[1][nextSectionIdx:]
|
|
} else {
|
|
content = parts[0] + newConfig
|
|
}
|
|
} else {
|
|
content = existingConfig + "\n" + newConfig
|
|
}
|
|
|
|
if err := os.WriteFile(configPath, []byte(content), 0600); err != nil {
|
|
return fmt.Errorf("failed to write config: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GenerateRcloneConfigWithToken generates a rclone config file for a transfer config with a provided token
|
|
// Note: This seems partially redundant with StoreGoogleDriveToken and GenerateRcloneConfig. Consolidate if possible.
|
|
func (db *DB) GenerateRcloneConfigWithToken(config *TransferConfig, token string) error {
|
|
configPath := db.GetConfigRclonePath(config)
|
|
if configPath == "" {
|
|
return fmt.Errorf("failed to get config path")
|
|
}
|
|
|
|
token = strings.TrimSpace(token)
|
|
token = strings.ReplaceAll(token, "\n", "")
|
|
token = strings.ReplaceAll(token, "\r", "")
|
|
|
|
var configType, section, clientID, clientSecret string
|
|
var readOnly, includeArchived *bool
|
|
var startYear int
|
|
|
|
// Determine if source or destination needs token update
|
|
if config.DestinationType == "drive" || config.DestinationType == "gphotos" {
|
|
configType = config.DestinationType
|
|
section = "dest"
|
|
clientID = config.DestClientID
|
|
clientSecret = config.DestClientSecret
|
|
readOnly = config.DestReadOnly
|
|
startYear = config.DestStartYear
|
|
includeArchived = config.DestIncludeArchived
|
|
} else if config.SourceType == "drive" || config.SourceType == "gphotos" {
|
|
configType = config.SourceType
|
|
section = "source"
|
|
clientID = config.SourceClientID
|
|
clientSecret = config.SourceClientSecret
|
|
readOnly = config.SourceReadOnly
|
|
startYear = config.SourceStartYear
|
|
includeArchived = config.SourceIncludeArchived
|
|
} else {
|
|
return fmt.Errorf("config is not for Google Drive or Google Photos")
|
|
}
|
|
|
|
contentBytes, err := os.ReadFile(configPath)
|
|
if err != nil && !os.IsNotExist(err) { // Allow file not existing yet
|
|
return fmt.Errorf("failed to read config file: %v", err)
|
|
}
|
|
content := string(contentBytes)
|
|
|
|
var sectionContent string
|
|
sectionHeader := fmt.Sprintf("[%s_%d]", section, config.ID)
|
|
|
|
if configType == "drive" {
|
|
sectionContent = sectionHeader + "\ntype = drive\n"
|
|
if clientID != "" {
|
|
sectionContent += fmt.Sprintf("client_id = %s\n", clientID)
|
|
}
|
|
if clientSecret != "" {
|
|
sectionContent += fmt.Sprintf("client_secret = %s\n", clientSecret)
|
|
}
|
|
sectionContent += fmt.Sprintf("token = %s\n", token)
|
|
if section == "source" && config.SourceTeamDrive != "" {
|
|
sectionContent += fmt.Sprintf("team_drive = %s\n", config.SourceTeamDrive)
|
|
}
|
|
if section == "dest" && config.DestTeamDrive != "" {
|
|
sectionContent += fmt.Sprintf("team_drive = %s\n", config.DestTeamDrive)
|
|
}
|
|
if section == "dest" && config.DestDriveID != "" {
|
|
sectionContent += fmt.Sprintf("root_folder_id = %s\n", config.DestDriveID)
|
|
} // Use DestDriveID for root_folder_id
|
|
} else if configType == "gphotos" {
|
|
sectionContent = sectionHeader + "\ntype = google photos\n"
|
|
if clientID != "" {
|
|
sectionContent += fmt.Sprintf("client_id = %s\n", clientID)
|
|
}
|
|
if clientSecret != "" {
|
|
sectionContent += fmt.Sprintf("client_secret = %s\n", clientSecret)
|
|
}
|
|
sectionContent += fmt.Sprintf("token = %s\n", token)
|
|
if readOnly != nil && *readOnly {
|
|
sectionContent += "read_only = true\n"
|
|
}
|
|
if startYear > 0 {
|
|
sectionContent += fmt.Sprintf("start_year = %d\n", startYear)
|
|
}
|
|
if includeArchived != nil && *includeArchived {
|
|
sectionContent += "include_archived = true\n"
|
|
}
|
|
}
|
|
|
|
// Replace or append logic
|
|
sectionPattern := regexp.MustCompile(fmt.Sprintf(`(?m)^%s[^\[]*`, regexp.QuoteMeta(sectionHeader))) // Match section start to next section or EOF
|
|
if sectionPattern.MatchString(content) {
|
|
content = sectionPattern.ReplaceAllString(content, sectionContent)
|
|
} else {
|
|
if content != "" && !strings.HasSuffix(content, "\n\n") { // Ensure separation
|
|
if !strings.HasSuffix(content, "\n") {
|
|
content += "\n"
|
|
}
|
|
content += "\n"
|
|
}
|
|
content += sectionContent
|
|
}
|
|
|
|
// Ensure directory exists
|
|
configDir := filepath.Dir(configPath)
|
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create config directory: %v", err)
|
|
}
|
|
|
|
// Write the updated config file
|
|
if err := os.WriteFile(configPath, []byte(content), 0600); err != nil { // Use 0600 for sensitive files
|
|
return fmt.Errorf("failed to write updated config file: %v", err)
|
|
}
|
|
|
|
// Update the authentication status in DB
|
|
authenticated := true
|
|
if config.DestinationType == "drive" || config.DestinationType == "gphotos" {
|
|
config.SetGoogleAuthenticated(authenticated)
|
|
} else if config.SourceType == "drive" || config.SourceType == "gphotos" {
|
|
config.SetGoogleAuthenticated(authenticated)
|
|
}
|
|
// Persist the change (assuming UpdateTransferConfig saves the whole object)
|
|
if err := db.UpdateTransferConfig(config); err != nil {
|
|
return fmt.Errorf("failed to update config authentication status: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetGDriveCredentialsFromConfig extracts Google Drive client ID and secret from an existing rclone config file
|
|
func (db *DB) GetGDriveCredentialsFromConfig(config *TransferConfig) (string, string) {
|
|
configPath := db.GetConfigRclonePath(config)
|
|
if configPath == "" {
|
|
return "", ""
|
|
}
|
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
|
return "", ""
|
|
}
|
|
|
|
content, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return "", ""
|
|
}
|
|
|
|
lines := strings.Split(string(content), "\n")
|
|
sourceSectionName := fmt.Sprintf("[source_%d]", config.ID)
|
|
destSectionName := fmt.Sprintf("[dest_%d]", config.ID)
|
|
var inSourceSection, inDestSection bool
|
|
var sourceClientID, sourceClientSecret, destClientID, destClientSecret string
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
|
inSourceSection = line == sourceSectionName
|
|
inDestSection = line == destSectionName
|
|
continue
|
|
}
|
|
if inSourceSection {
|
|
if strings.HasPrefix(line, "client_id") {
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) == 2 {
|
|
sourceClientID = strings.TrimSpace(parts[1])
|
|
}
|
|
} else if strings.HasPrefix(line, "client_secret") {
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) == 2 {
|
|
sourceClientSecret = strings.TrimSpace(parts[1])
|
|
}
|
|
}
|
|
}
|
|
if inDestSection {
|
|
if strings.HasPrefix(line, "client_id") {
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) == 2 {
|
|
destClientID = strings.TrimSpace(parts[1])
|
|
}
|
|
} else if strings.HasPrefix(line, "client_secret") {
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) == 2 {
|
|
destClientSecret = strings.TrimSpace(parts[1])
|
|
}
|
|
}
|
|
}
|
|
if sourceClientID != "" && sourceClientSecret != "" && destClientID != "" && destClientSecret != "" {
|
|
break
|
|
}
|
|
}
|
|
|
|
if destClientID != "" && destClientSecret != "" {
|
|
return destClientID, destClientSecret
|
|
}
|
|
if sourceClientID != "" && sourceClientSecret != "" {
|
|
return sourceClientID, sourceClientSecret
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
// ConvertToProviderReferences converts a TransferConfig that uses embedded credentials
|
|
// to one that uses StorageProvider references.
|
|
func (db *DB) ConvertToProviderReferences(config *TransferConfig) error {
|
|
// Skip if already using both provider references
|
|
if config.IsUsingProviderReferences() {
|
|
return nil
|
|
}
|
|
|
|
tx := db.Begin()
|
|
if tx.Error != nil {
|
|
return fmt.Errorf("failed to start transaction: %v", tx.Error)
|
|
}
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
tx.Rollback()
|
|
}
|
|
}()
|
|
|
|
// Convert source if needed
|
|
if !config.IsUsingSourceProviderReference() && config.SourceType != "" {
|
|
// Create new provider from source fields
|
|
provider := &StorageProvider{
|
|
Name: fmt.Sprintf("%s Source - %s", config.Name, config.SourceType),
|
|
Type: StorageProviderType(config.SourceType),
|
|
CreatedBy: config.CreatedBy,
|
|
// Copy all relevant source fields to provider fields
|
|
Host: config.SourceHost,
|
|
Port: config.SourcePort,
|
|
Username: config.SourceUser,
|
|
KeyFile: config.SourceKeyFile,
|
|
Bucket: config.SourceBucket,
|
|
Region: config.SourceRegion,
|
|
AccessKey: config.SourceAccessKey,
|
|
Share: config.SourceShare,
|
|
Domain: config.SourceDomain,
|
|
PassiveMode: config.SourcePassiveMode,
|
|
ClientID: config.SourceClientID,
|
|
DriveID: config.SourceDriveID,
|
|
TeamDrive: config.SourceTeamDrive,
|
|
ReadOnly: config.SourceReadOnly,
|
|
StartYear: config.SourceStartYear,
|
|
IncludeArchived: config.SourceIncludeArchived,
|
|
UseBuiltinAuth: config.UseBuiltinAuthSource,
|
|
}
|
|
|
|
// Handle fields that need encryption
|
|
if config.SourcePassword != "" {
|
|
encryptedPwd, err := db.EncryptCredential(config.SourcePassword)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return fmt.Errorf("failed to encrypt source password: %v", err)
|
|
}
|
|
provider.EncryptedPassword = encryptedPwd
|
|
}
|
|
|
|
if config.SourceSecretKey != "" {
|
|
encryptedSecret, err := db.EncryptCredential(config.SourceSecretKey)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return fmt.Errorf("failed to encrypt source secret key: %v", err)
|
|
}
|
|
provider.EncryptedSecretKey = encryptedSecret
|
|
}
|
|
|
|
if config.SourceClientSecret != "" {
|
|
encryptedClientSecret, err := db.EncryptCredential(config.SourceClientSecret)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return fmt.Errorf("failed to encrypt source client secret: %v", err)
|
|
}
|
|
provider.EncryptedClientSecret = encryptedClientSecret
|
|
}
|
|
|
|
// Save the new provider
|
|
if err := tx.Create(provider).Error; err != nil {
|
|
tx.Rollback()
|
|
return fmt.Errorf("failed to create source provider: %v", err)
|
|
}
|
|
|
|
// Update the config to reference the new provider
|
|
config.SetSourceProvider(provider)
|
|
}
|
|
|
|
// Convert destination if needed
|
|
if !config.IsUsingDestinationProviderReference() && config.DestinationType != "" {
|
|
// Create new provider from destination fields
|
|
provider := &StorageProvider{
|
|
Name: fmt.Sprintf("%s Destination - %s", config.Name, config.DestinationType),
|
|
Type: StorageProviderType(config.DestinationType),
|
|
CreatedBy: config.CreatedBy,
|
|
// Copy all relevant destination fields to provider fields
|
|
Host: config.DestHost,
|
|
Port: config.DestPort,
|
|
Username: config.DestUser,
|
|
KeyFile: config.DestKeyFile,
|
|
Bucket: config.DestBucket,
|
|
Region: config.DestRegion,
|
|
AccessKey: config.DestAccessKey,
|
|
Share: config.DestShare,
|
|
Domain: config.DestDomain,
|
|
PassiveMode: config.DestPassiveMode,
|
|
ClientID: config.DestClientID,
|
|
DriveID: config.DestDriveID,
|
|
TeamDrive: config.DestTeamDrive,
|
|
ReadOnly: config.DestReadOnly,
|
|
StartYear: config.DestStartYear,
|
|
IncludeArchived: config.DestIncludeArchived,
|
|
UseBuiltinAuth: config.UseBuiltinAuthDest,
|
|
}
|
|
|
|
// For Google Drive/Photos, carry over authentication status
|
|
if config.DestinationType == "drive" || config.DestinationType == "gphotos" {
|
|
provider.SetAuthenticated(config.GetGoogleAuthenticated())
|
|
}
|
|
|
|
// Handle fields that need encryption
|
|
if config.DestPassword != "" {
|
|
encryptedPwd, err := db.EncryptCredential(config.DestPassword)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return fmt.Errorf("failed to encrypt destination password: %v", err)
|
|
}
|
|
provider.EncryptedPassword = encryptedPwd
|
|
}
|
|
|
|
if config.DestSecretKey != "" {
|
|
encryptedSecret, err := db.EncryptCredential(config.DestSecretKey)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return fmt.Errorf("failed to encrypt destination secret key: %v", err)
|
|
}
|
|
provider.EncryptedSecretKey = encryptedSecret
|
|
}
|
|
|
|
if config.DestClientSecret != "" {
|
|
encryptedClientSecret, err := db.EncryptCredential(config.DestClientSecret)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return fmt.Errorf("failed to encrypt destination client secret: %v", err)
|
|
}
|
|
provider.EncryptedClientSecret = encryptedClientSecret
|
|
}
|
|
|
|
// Save the new provider
|
|
if err := tx.Create(provider).Error; err != nil {
|
|
tx.Rollback()
|
|
return fmt.Errorf("failed to create destination provider: %v", err)
|
|
}
|
|
|
|
// Update the config to reference the new provider
|
|
config.SetDestinationProvider(provider)
|
|
}
|
|
|
|
// Save the updated config
|
|
if err := tx.Save(config).Error; err != nil {
|
|
tx.Rollback()
|
|
return fmt.Errorf("failed to update config with provider references: %v", err)
|
|
}
|
|
|
|
return tx.Commit().Error
|
|
}
|
|
|
|
// EncryptCredential encrypts a sensitive credential value
|
|
func (db *DB) EncryptCredential(value string) (string, error) {
|
|
// Create a credential encryptor
|
|
encryptor, err := encryption.GetGlobalCredentialEncryptor()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get credential encryptor: %w", err)
|
|
}
|
|
|
|
// Encrypt the value using the generic credential type
|
|
encrypted, err := encryptor.Encrypt(value, encryption.TypeGeneric)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to encrypt credential: %w", err)
|
|
}
|
|
|
|
return encrypted, nil
|
|
}
|
|
|
|
// DecryptCredential decrypts a sensitive credential value
|
|
func (db *DB) DecryptCredential(encryptedValue string) (string, error) {
|
|
// Create a credential encryptor
|
|
encryptor, err := encryption.GetGlobalCredentialEncryptor()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get credential encryptor: %w", err)
|
|
}
|
|
|
|
// Check if value is already encrypted with our prefix
|
|
if !encryptor.IsEncrypted(encryptedValue) {
|
|
// Handle legacy format (temporary backward compatibility)
|
|
if strings.HasPrefix(encryptedValue, "encrypted_") {
|
|
return strings.TrimPrefix(encryptedValue, "encrypted_"), nil
|
|
}
|
|
// Not encrypted, return as-is
|
|
return encryptedValue, nil
|
|
}
|
|
|
|
// Decrypt the value
|
|
decrypted, err := encryptor.Decrypt(encryptedValue)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to decrypt credential: %w", err)
|
|
}
|
|
|
|
return decrypted, nil
|
|
}
|
|
|
|
// UpdateStorageProvider updates an existing storage provider
|