mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 23:50:48 +02:00
- 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.
221 lines
7.8 KiB
Go
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)
|
|
}
|
|
}
|