mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-10-26 06:50:26 +01:00
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.
This commit is contained in:
1 parent
88d0ac815a
commit
31871bd16e
59 files changed
+15842
-594
No files matched your search
@@ -0,0 +1,241 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ValidateStorageProvider validates a storage provider based on its type
|
||||
func (sp *StorageProvider) Validate() error {
|
||||
// Special case - if we have an empty struct or just an ID (could happen during GORM operations like foreign key checks)
|
||||
if (sp.ID > 0 && sp.Name == "" && sp.Type == "") || (sp.ID == 0 && sp.Name == "" && sp.Type == "") {
|
||||
log.Printf("Skipping validation for StorageProvider: ID=%d without other data (likely a reference check)", sp.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Common validations
|
||||
if strings.TrimSpace(sp.Name) == "" {
|
||||
return errors.New("provider name cannot be empty")
|
||||
}
|
||||
|
||||
// Type-specific validations
|
||||
switch sp.Type {
|
||||
case ProviderTypeSFTP, ProviderTypeHetzner:
|
||||
return sp.validateSFTP()
|
||||
case ProviderTypeS3:
|
||||
return sp.validateS3()
|
||||
case ProviderTypeFTP:
|
||||
return sp.validateFTP()
|
||||
case ProviderTypeSMB:
|
||||
return sp.validateSMB()
|
||||
case ProviderTypeOneDrive:
|
||||
return sp.validateOneDrive()
|
||||
case ProviderTypeGoogleDrive:
|
||||
return sp.validateGoogleDrive()
|
||||
case ProviderTypeGooglePhoto:
|
||||
return sp.validateGooglePhoto()
|
||||
case ProviderTypeLocal:
|
||||
return sp.validateLocal()
|
||||
default:
|
||||
return fmt.Errorf("unsupported provider type: %s", sp.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// validateSFTP validates SFTP-specific fields
|
||||
func (sp *StorageProvider) validateSFTP() error {
|
||||
if strings.TrimSpace(sp.Host) == "" {
|
||||
return errors.New("host is required for SFTP provider")
|
||||
}
|
||||
|
||||
if sp.Port <= 0 {
|
||||
return errors.New("invalid port for SFTP provider")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(sp.Username) == "" {
|
||||
return errors.New("username is required for SFTP provider")
|
||||
}
|
||||
|
||||
// Either password or key file must be provided
|
||||
if strings.TrimSpace(sp.Password) == "" && strings.TrimSpace(sp.EncryptedPassword) == "" && strings.TrimSpace(sp.KeyFile) == "" {
|
||||
return errors.New("either password or key file is required for SFTP provider")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateS3 validates S3-specific fields
|
||||
func (sp *StorageProvider) validateS3() error {
|
||||
// For S3, either AccessKey or Username is used
|
||||
if strings.TrimSpace(sp.AccessKey) == "" && strings.TrimSpace(sp.Username) == "" {
|
||||
return errors.New("access key is required for S3 provider")
|
||||
}
|
||||
|
||||
// Either SecretKey or EncryptedSecretKey must be provided
|
||||
if strings.TrimSpace(sp.SecretKey) == "" && strings.TrimSpace(sp.EncryptedSecretKey) == "" {
|
||||
return errors.New("secret key is required for S3 provider")
|
||||
}
|
||||
|
||||
// Region is required for most S3 providers
|
||||
if strings.TrimSpace(sp.Region) == "" {
|
||||
return errors.New("region is required for S3 provider")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateFTP validates FTP-specific fields
|
||||
func (sp *StorageProvider) validateFTP() error {
|
||||
if strings.TrimSpace(sp.Host) == "" {
|
||||
return errors.New("host is required for FTP provider")
|
||||
}
|
||||
|
||||
if sp.Port <= 0 {
|
||||
return errors.New("invalid port for FTP provider")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(sp.Username) == "" {
|
||||
return errors.New("username is required for FTP provider")
|
||||
}
|
||||
|
||||
// Either password or encrypted password must be provided
|
||||
if strings.TrimSpace(sp.Password) == "" && strings.TrimSpace(sp.EncryptedPassword) == "" {
|
||||
return errors.New("password is required for FTP provider")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateSMB validates SMB-specific fields
|
||||
func (sp *StorageProvider) validateSMB() error {
|
||||
if strings.TrimSpace(sp.Host) == "" {
|
||||
return errors.New("host is required for SMB provider")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(sp.Share) == "" {
|
||||
return errors.New("share is required for SMB provider")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(sp.Username) == "" {
|
||||
return errors.New("username is required for SMB provider")
|
||||
}
|
||||
|
||||
// Either password or encrypted password must be provided
|
||||
if strings.TrimSpace(sp.Password) == "" && strings.TrimSpace(sp.EncryptedPassword) == "" {
|
||||
return errors.New("password is required for SMB provider")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateOneDrive validates OneDrive-specific fields
|
||||
func (sp *StorageProvider) validateOneDrive() error {
|
||||
if strings.TrimSpace(sp.ClientID) == "" {
|
||||
return errors.New("client ID is required for OneDrive provider")
|
||||
}
|
||||
|
||||
// Either ClientSecret or EncryptedClientSecret must be provided
|
||||
if strings.TrimSpace(sp.ClientSecret) == "" && strings.TrimSpace(sp.EncryptedClientSecret) == "" {
|
||||
return errors.New("client secret is required for OneDrive provider")
|
||||
}
|
||||
|
||||
// For authenticated providers, RefreshToken must be set
|
||||
if sp.GetAuthenticated() && strings.TrimSpace(sp.EncryptedRefreshToken) == "" && strings.TrimSpace(sp.RefreshToken) == "" {
|
||||
return errors.New("refresh token is required for authenticated OneDrive provider")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateGoogleDrive validates Google Drive-specific fields
|
||||
func (sp *StorageProvider) validateGoogleDrive() error {
|
||||
// If not using builtin auth, ClientID and ClientSecret are required
|
||||
if !sp.GetUseBuiltinAuth() {
|
||||
if strings.TrimSpace(sp.ClientID) == "" {
|
||||
return errors.New("client ID is required for Google Drive provider when not using builtin auth")
|
||||
}
|
||||
|
||||
// Either ClientSecret or EncryptedClientSecret must be provided
|
||||
if strings.TrimSpace(sp.ClientSecret) == "" && strings.TrimSpace(sp.EncryptedClientSecret) == "" {
|
||||
return errors.New("client secret is required for Google Drive provider when not using builtin auth")
|
||||
}
|
||||
}
|
||||
|
||||
// For authenticated providers, RefreshToken must be set
|
||||
if sp.GetAuthenticated() && strings.TrimSpace(sp.EncryptedRefreshToken) == "" && strings.TrimSpace(sp.RefreshToken) == "" {
|
||||
return errors.New("refresh token is required for authenticated Google Drive provider")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateGooglePhoto validates Google Photos-specific fields
|
||||
func (sp *StorageProvider) validateGooglePhoto() error {
|
||||
// Similar to Google Drive
|
||||
return sp.validateGoogleDrive()
|
||||
}
|
||||
|
||||
// validateLocal validates Local-specific fields
|
||||
func (sp *StorageProvider) validateLocal() error {
|
||||
// Local providers don't need additional validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeSave is a GORM hook that runs before saving the provider
|
||||
func (sp *StorageProvider) BeforeSave(tx *gorm.DB) error {
|
||||
// Check if this is a reference check by examining the GORM operation
|
||||
if tx.Statement.SQL.String() == "" {
|
||||
// No explicit SQL means this might be part of a preload or association check
|
||||
|
||||
// Case 1: Empty struct (as you already have)
|
||||
if sp.ID == 0 && sp.Name == "" && sp.Type == "" {
|
||||
log.Printf("BeforeSave: Skipping validation for empty StorageProvider")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Case 2: ID-only struct (foreign key reference check)
|
||||
if sp.ID > 0 && sp.Name == "" && sp.Type == "" {
|
||||
log.Printf("BeforeSave: Skipping validation for StorageProvider ID=%d (reference check)", sp.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Case 3: Minimal data loaded from database for relationship check
|
||||
// Check if only a few fields are populated (typically ID and maybe a couple others)
|
||||
populatedFields := 0
|
||||
if sp.ID > 0 {
|
||||
populatedFields++
|
||||
}
|
||||
if sp.Name != "" {
|
||||
populatedFields++
|
||||
}
|
||||
if string(sp.Type) != "" {
|
||||
populatedFields++
|
||||
}
|
||||
if sp.Host != "" {
|
||||
populatedFields++
|
||||
}
|
||||
if sp.Username != "" {
|
||||
populatedFields++
|
||||
}
|
||||
|
||||
// If we have just a few populated fields, it's likely a reference check
|
||||
if populatedFields <= 3 {
|
||||
log.Printf("BeforeSave: Skipping validation for partially loaded StorageProvider ID=%d (likely reference check)", sp.ID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is called from a foreign key operation on another model
|
||||
stmt := tx.Statement
|
||||
if stmt.Schema != nil && stmt.Schema.Table != "storage_providers" {
|
||||
log.Printf("BeforeSave: Skipping validation for StorageProvider ID=%d (called from %s table operation)",
|
||||
sp.ID, stmt.Schema.Table)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("BeforeSave: Validating StorageProvider: ID=%d, Name=%s, Type=%s", sp.ID, sp.Name, sp.Type)
|
||||
return sp.Validate()
|
||||
}
|
||||
Reference in new issue
Block a user