Files
GoMFT/internal/storage/connector_service.go
StarFleetCPTN dc665b9fa4 feat: Enhance configuration forms and validation for storage providers
- Added hidden input fields to ensure critical values are submitted with the form for S3 and other storage providers.
- Implemented JavaScript functionality to auto-populate hidden fields based on user input for S3-compatible providers.
- Updated validation logic for WebDAV and SMB providers to ensure required fields are checked.
- Enhanced the configuration handler to explicitly set fields for various storage provider types, including S3, Wasabi, MinIO, and B2.
- Improved error handling and logging for form submissions and provider credential management.
2025-04-18 02:43:22 -07:00

221 lines
7.8 KiB
Go

package storage
import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/starfleetcptn/gomft/internal/encryption"
"github.com/starfleetcptn/gomft/internal/rclone_service"
)
// ConnectorService manages storage provider connection testing
type ConnectorService struct {
dbInstance *db.DB
encryptionSvc *encryption.EncryptionService
credentialEncryptor *encryption.CredentialEncryptor
}
// NewConnectorService creates a new ConnectorService
func NewConnectorService(dbInstance *db.DB) (*ConnectorService, error) {
// Get the global encryption service
encryptionSvc, err := encryption.GetGlobalEncryptionService()
if err != nil {
return nil, fmt.Errorf("failed to get encryption service: %w", err)
}
// Get the global credential encryptor
credentialEncryptor, err := encryption.GetGlobalCredentialEncryptor()
if err != nil {
return nil, fmt.Errorf("failed to get credential encryptor: %w", err)
}
return &ConnectorService{
dbInstance: dbInstance,
encryptionSvc: encryptionSvc,
credentialEncryptor: credentialEncryptor,
}, nil
}
// TestConnection tests a connection to a storage provider using rclone
func (s *ConnectorService) TestConnection(ctx context.Context, providerID uint, userID uint) (*db.ConnectionResult, error) {
// Get the provider from the database with owner check
provider, err := s.dbInstance.GetStorageProviderWithOwnerCheck(providerID, userID)
if err != nil {
return nil, fmt.Errorf("failed to get storage provider: %w", err)
}
// Decrypt sensitive fields
if err := s.decryptProviderCredentials(provider); err != nil {
return nil, fmt.Errorf("failed to decrypt credentials: %w", err)
}
// Create a temporary TransferConfig with just the source fields populated
tempConfig := createTempTransferConfig(provider)
// Use the rclone service to test the connection
success, message, err := rclone_service.TestRcloneConnection(*tempConfig, "source", s.dbInstance)
// Create the connection result
result := &db.ConnectionResult{
Success: success,
Message: message,
Timestamp: time.Now(),
}
// If there was an error, add it to the result
if err != nil {
errorCode := determineErrorCode(err.Error())
result.Error = &db.ConnectorError{
Code: errorCode,
Message: err.Error(),
Err: err,
}
}
// Record the test result in logs (without sensitive info)
s.logConnectionTest(provider, result)
return result, nil
}
// createTempTransferConfig creates a temporary TransferConfig for connection testing
func createTempTransferConfig(provider *db.StorageProvider) *db.TransferConfig {
config := &db.TransferConfig{
SourceType: string(provider.Type),
SourceHost: provider.Host,
SourcePort: provider.Port,
}
// Set the right credential fields based on provider type
switch provider.Type {
case db.ProviderTypeSFTP, db.ProviderTypeFTP, db.ProviderTypeSMB, db.ProviderTypeHetzner:
config.SourceUser = provider.Username
config.SourcePassword = provider.Password
config.SourceKeyFile = provider.KeyFile
config.SourceDomain = provider.Domain
// Set passive mode for FTP
if provider.Type == db.ProviderTypeFTP && provider.PassiveMode != nil {
passive := provider.GetPassiveMode()
config.SetSourcePassiveMode(passive)
}
case db.ProviderTypeS3, db.ProviderTypeWasabi, db.ProviderTypeMinio:
config.SourceAccessKey = provider.AccessKey
config.SourceSecretKey = provider.SecretKey
config.SourceBucket = provider.Bucket
config.SourceRegion = provider.Region
config.SourceEndpoint = provider.Endpoint
case db.ProviderTypeB2:
// B2 uses AccessKey as account and SecretKey as application key
config.SourceAccessKey = provider.AccessKey
config.SourceSecretKey = provider.SecretKey
config.SourceBucket = provider.Bucket
config.SourceRegion = provider.Region
config.SourceEndpoint = provider.Endpoint
case db.ProviderTypeOneDrive, db.ProviderTypeGoogleDrive, db.ProviderTypeGooglePhoto:
config.SourceClientID = provider.ClientID
config.SourceClientSecret = provider.ClientSecret
config.SourceDriveID = provider.DriveID
config.SourceTeamDrive = provider.TeamDrive
// For Google Photos, we would set read-only mode if the method existed
// Currently commented out as SetSourceReadOnly doesn't exist
// if provider.Type == db.ProviderTypeGooglePhoto && provider.ReadOnly != nil {
// readonly := provider.GetReadOnly()
// config.SetSourceReadOnly(readonly)
// }
}
return config
}
// determineErrorCode maps rclone error messages to our error code system
func determineErrorCode(errMsg string) string {
switch {
case strings.Contains(errMsg, "connection refused"), strings.Contains(errMsg, "dial tcp"):
return db.ErrorCodeConnection
case strings.Contains(errMsg, "no such host"), strings.Contains(errMsg, "network is unreachable"):
return db.ErrorCodeNetwork
case strings.Contains(errMsg, "timeout"), strings.Contains(errMsg, "timed out"):
return db.ErrorCodeTimeout
case strings.Contains(errMsg, "authentication failed"), strings.Contains(errMsg, "login incorrect"),
strings.Contains(errMsg, "permission denied"), strings.Contains(errMsg, "invalid credentials"):
return db.ErrorCodeAuthentication
case strings.Contains(errMsg, "directory not found"), strings.Contains(errMsg, "no such file"):
return db.ErrorCodeResourceNotFound
case strings.Contains(errMsg, "invalid parameters"):
return db.ErrorCodeInvalidParams
default:
return db.ErrorCodeUnknown
}
}
// decryptProviderCredentials decrypts the provider's sensitive fields
func (s *ConnectorService) decryptProviderCredentials(provider *db.StorageProvider) error {
// Handle different provider types
switch provider.Type {
case db.ProviderTypeSFTP, db.ProviderTypeFTP, db.ProviderTypeSMB, db.ProviderTypeHetzner:
if provider.EncryptedPassword != "" {
password, err := s.credentialEncryptor.Decrypt(provider.EncryptedPassword)
if err != nil {
return fmt.Errorf("failed to decrypt password: %w", err)
}
provider.Password = password
}
case db.ProviderTypeS3, db.ProviderTypeWasabi, db.ProviderTypeMinio, db.ProviderTypeB2:
if provider.EncryptedSecretKey != "" {
secretKey, err := s.credentialEncryptor.Decrypt(provider.EncryptedSecretKey)
if err != nil {
return fmt.Errorf("failed to decrypt secret key: %w", err)
}
provider.SecretKey = secretKey
log.Printf("DEBUG: Decrypted secret key for %s provider (ID: %d, Type: %s) with length: %d",
provider.Name, provider.ID, provider.Type, len(provider.SecretKey))
} else {
log.Printf("WARNING: No encrypted secret key found for %s provider (ID: %d, Type: %s)",
provider.Name, provider.ID, provider.Type)
}
case db.ProviderTypeOneDrive, db.ProviderTypeGoogleDrive, db.ProviderTypeGooglePhoto:
if provider.EncryptedClientSecret != "" {
clientSecret, err := s.credentialEncryptor.Decrypt(provider.EncryptedClientSecret)
if err != nil {
return fmt.Errorf("failed to decrypt client secret: %w", err)
}
provider.ClientSecret = clientSecret
}
if provider.EncryptedRefreshToken != "" {
refreshToken, err := s.credentialEncryptor.Decrypt(provider.EncryptedRefreshToken)
if err != nil {
return fmt.Errorf("failed to decrypt refresh token: %w", err)
}
provider.RefreshToken = refreshToken
}
}
return nil
}
// logConnectionTest logs the connection test result without sensitive information
func (s *ConnectorService) logConnectionTest(provider *db.StorageProvider, result *db.ConnectionResult) {
if result.Success {
log.Printf("Connection test successful for provider %s (ID: %d, Type: %s)",
provider.Name, provider.ID, provider.Type)
} else {
errorCode := "unknown"
if result.Error != nil {
errorCode = result.Error.Code
}
log.Printf("Connection test failed for provider %s (ID: %d, Type: %s): %s [%s]",
provider.Name, provider.ID, provider.Type, result.Message, errorCode)
}
}