Files
GoMFT/internal/storage/connector_service.go
T
StarFleetCPTN 31871bd16e feat: Implement storage provider management functionality
- Added new routes and handlers for managing storage providers, including creation, editing, and deletion.
- Introduced a new StorageProvider form component for user input.
- Enhanced the database schema to support storage provider references in transfer configurations.
- Implemented encryption for sensitive fields in storage provider data.
- Added tests for storage provider API endpoints and integration with the database.
- Updated frontend components to support storage provider selection and testing.
2025-04-16 17:18:53 -07:00

208 lines
7.1 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:
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:
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
}
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)
}
}