mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-10-06 22:41:43 +02:00
feat: Integrate Google Drive support and enhance configuration handling
- Add Google Drive as a source and destination option in the configuration forms. - Implement Google Drive authentication flow and token management. - Update job and configuration handlers to support Google Drive-specific settings. - Enhance UI components to include Google Drive configuration templates. - Introduce new tests for Google Drive integration and ensure proper handling of authentication and configuration. - Update database migrations to accommodate new fields related to Google Drive configurations.
This commit is contained in:
1 parent
a954023800
commit
8607f2098a
52 files changed
+2795
-355
No files matched your search
+613
-15
@@ -1,6 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -18,10 +19,10 @@ type User struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
Email string `gorm:"unique;not null"`
|
||||
PasswordHash string `gorm:"not null"`
|
||||
IsAdmin bool `gorm:"default:false"`
|
||||
IsAdmin *bool `gorm:"default:false"`
|
||||
LastPasswordChange time.Time
|
||||
FailedLoginAttempts int `gorm:"default:0"`
|
||||
AccountLocked bool `gorm:"default:false"`
|
||||
FailedLoginAttempts int `gorm:"default:0"`
|
||||
AccountLocked *bool `gorm:"default:false"`
|
||||
LockoutUntil *time.Time
|
||||
Theme string `gorm:"default:'light'"`
|
||||
CreatedAt time.Time
|
||||
@@ -42,7 +43,7 @@ type PasswordResetToken struct {
|
||||
User User `gorm:"foreignkey:UserID"`
|
||||
Token string `gorm:"not null"`
|
||||
ExpiresAt time.Time `gorm:"not null"`
|
||||
Used bool `gorm:"default:false"`
|
||||
Used *bool `gorm:"default:false"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -67,7 +68,7 @@ type TransferConfig struct {
|
||||
SourceShare string `form:"source_share"`
|
||||
SourceDomain string `form:"source_domain"`
|
||||
// FTP source fields
|
||||
SourcePassiveMode bool `gorm:"default:true" form:"source_passive_mode"`
|
||||
SourcePassiveMode *bool `gorm:"default:true" form:"source_passive_mode"`
|
||||
// OneDrive and Google Drive source fields
|
||||
SourceClientID string `form:"source_client_id"`
|
||||
SourceClientSecret string `form:"source_client_secret" gorm:"-"` // Not stored in DB, only used for form
|
||||
@@ -93,17 +94,19 @@ type TransferConfig struct {
|
||||
DestShare string `form:"dest_share"`
|
||||
DestDomain string `form:"dest_domain"`
|
||||
// FTP destination fields
|
||||
DestPassiveMode bool `gorm:"default:true" form:"dest_passive_mode"`
|
||||
DestPassiveMode *bool `gorm:"default:true" form:"dest_passive_mode"`
|
||||
// OneDrive and Google Drive destination fields
|
||||
DestClientID string `form:"dest_client_id"`
|
||||
DestClientSecret string `form:"dest_client_secret" gorm:"-"` // Not stored in DB, only used for form
|
||||
DestDriveID string `form:"dest_drive_id"` // For OneDrive
|
||||
DestTeamDrive string `form:"dest_team_drive"` // For Google Drive
|
||||
// Google Drive authentication status
|
||||
GoogleDriveAuthenticated *bool `gorm:"default:false"`
|
||||
// General fields
|
||||
ArchivePath string `form:"archive_path"`
|
||||
ArchiveEnabled bool `gorm:"default:false" form:"archive_enabled"`
|
||||
ArchiveEnabled *bool `gorm:"default:false" form:"archive_enabled"`
|
||||
RcloneFlags string `form:"rclone_flags"`
|
||||
DeleteAfterTransfer bool `gorm:"default:false" form:"delete_after_transfer"`
|
||||
DeleteAfterTransfer *bool `gorm:"default:false" form:"delete_after_transfer"`
|
||||
SkipProcessedFiles *bool `gorm:"default:true" form:"skip_processed_files"`
|
||||
MaxConcurrentTransfers int `gorm:"default:4" form:"max_concurrent_transfers"` // Number of concurrent file transfers
|
||||
CreatedBy uint
|
||||
@@ -119,16 +122,16 @@ type Job struct {
|
||||
Config TransferConfig `gorm:"foreignkey:ConfigID"`
|
||||
ConfigIDs string `gorm:"column:config_ids"` // Comma-separated list of config IDs
|
||||
Schedule string `gorm:"not null" form:"schedule"`
|
||||
Enabled bool `gorm:"default:true" form:"enabled"`
|
||||
Enabled *bool `gorm:"default:true" form:"enabled"`
|
||||
LastRun *time.Time
|
||||
NextRun *time.Time
|
||||
// Webhook notification fields
|
||||
WebhookEnabled bool `gorm:"default:false" form:"webhook_enabled"`
|
||||
WebhookEnabled *bool `gorm:"default:false" form:"webhook_enabled"`
|
||||
WebhookURL string `form:"webhook_url"`
|
||||
WebhookSecret string `form:"webhook_secret"`
|
||||
WebhookHeaders string `form:"webhook_headers"` // JSON-encoded headers
|
||||
NotifyOnSuccess bool `gorm:"default:true" form:"notify_on_success"`
|
||||
NotifyOnFailure bool `gorm:"default:true" form:"notify_on_failure"`
|
||||
NotifyOnSuccess *bool `gorm:"default:true" form:"notify_on_success"`
|
||||
NotifyOnFailure *bool `gorm:"default:true" form:"notify_on_failure"`
|
||||
CreatedBy uint
|
||||
User User `gorm:"foreignkey:CreatedBy"`
|
||||
CreatedAt time.Time
|
||||
@@ -558,7 +561,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
|
||||
if config.SourcePassiveMode {
|
||||
if config.SourcePassiveMode != nil && *config.SourcePassiveMode {
|
||||
args = append(args, "passive", "true")
|
||||
}
|
||||
|
||||
@@ -746,7 +749,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
|
||||
if config.DestPassiveMode {
|
||||
if config.DestPassiveMode != nil && *config.DestPassiveMode {
|
||||
args = append(args, "passive", "true")
|
||||
}
|
||||
|
||||
@@ -799,6 +802,33 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
||||
args = append(args, "drive_id", config.DestDriveID)
|
||||
}
|
||||
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create destination config: %v\nOutput: %s", err, output)
|
||||
}
|
||||
case "gdrive":
|
||||
args := []string{
|
||||
"config", "create", destName, "drive",
|
||||
"--non-interactive",
|
||||
"--config", configPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
|
||||
// Only add client_id and client_secret if they're provided (not empty)
|
||||
// This allows using rclone's built-in authentication
|
||||
if config.DestClientID != "" && config.DestClientSecret != "" {
|
||||
args = append(args, "client_id", config.DestClientID)
|
||||
args = append(args, "client_secret", config.DestClientSecret)
|
||||
}
|
||||
|
||||
if config.DestTeamDrive != "" {
|
||||
args = append(args, "team_drive", config.DestTeamDrive)
|
||||
}
|
||||
|
||||
if config.DestDriveID != "" {
|
||||
args = append(args, "root_folder_id", config.DestDriveID)
|
||||
}
|
||||
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create destination config: %v\nOutput: %s", err, output)
|
||||
@@ -837,12 +867,14 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveJobs returns all active jobs
|
||||
func (db *DB) GetActiveJobs() ([]Job, error) {
|
||||
if db.DB == nil {
|
||||
return nil, fmt.Errorf("database connection is nil")
|
||||
}
|
||||
var jobs []Job
|
||||
err := db.Preload("Config").Where("enabled = ?", true).Find(&jobs).Error
|
||||
// For boolean pointer fields, need to check either NULL (for default) or true value
|
||||
err := db.Preload("Config").Where("enabled IS NULL OR enabled = ?", true).Find(&jobs).Error
|
||||
return jobs, err
|
||||
}
|
||||
|
||||
@@ -885,3 +917,569 @@ func (tc *TransferConfig) GetSkipProcessedFiles() bool {
|
||||
func (tc *TransferConfig) SetSkipProcessedFiles(value bool) {
|
||||
tc.SkipProcessedFiles = &value
|
||||
}
|
||||
|
||||
// StoreGoogleDriveToken stores the Google Drive auth token for a config
|
||||
func (db *DB) StoreGoogleDriveToken(configIDStr string, token string) error {
|
||||
configID, err := strconv.ParseUint(configIDStr, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config ID: %v", err)
|
||||
}
|
||||
|
||||
// Get the existing config
|
||||
config, err := db.GetTransferConfig(uint(configID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config: %v", err)
|
||||
}
|
||||
|
||||
// Mark as authenticated
|
||||
authenticated := true
|
||||
config.GoogleDriveAuthenticated = &authenticated
|
||||
|
||||
// Update the config in the database
|
||||
if err := db.UpdateTransferConfig(config); err != nil {
|
||||
return fmt.Errorf("failed to update config: %v", err)
|
||||
}
|
||||
|
||||
// Get the rclone config path
|
||||
configPath := db.GetConfigRclonePath(config)
|
||||
|
||||
// Read existing config if it exists
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 new config with the token
|
||||
destName := fmt.Sprintf("dest_%d", config.ID)
|
||||
newConfig := fmt.Sprintf("[%s]\ntype = drive\ntoken = %s\n", destName, token)
|
||||
|
||||
// If the config has client ID and secret, add them
|
||||
if config.DestClientID != "" && config.DestClientSecret != "" {
|
||||
newConfig += fmt.Sprintf("client_id = %s\nclient_secret = %s\n", config.DestClientID, config.DestClientSecret)
|
||||
}
|
||||
|
||||
// Add root folder ID if specified
|
||||
if config.DestDriveID != "" {
|
||||
newConfig += fmt.Sprintf("root_folder_id = %s\n", config.DestDriveID)
|
||||
}
|
||||
|
||||
// Add team drive if specified
|
||||
if config.DestTeamDrive != "" {
|
||||
newConfig += fmt.Sprintf("team_drive = %s\n", config.DestTeamDrive)
|
||||
}
|
||||
|
||||
// If there's existing config, append to it; otherwise create new file
|
||||
var content string
|
||||
if existingConfig != "" {
|
||||
// Replace/update existing dest section if it exists, otherwise append
|
||||
if strings.Contains(existingConfig, fmt.Sprintf("[%s]", destName)) {
|
||||
// This is a simplistic approach - in production you might want a more robust regex-based replacement
|
||||
// Truncate at the beginning of the dest section
|
||||
parts := strings.SplitN(existingConfig, fmt.Sprintf("[%s]", destName), 2)
|
||||
// Check if there are more sections after this one
|
||||
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
|
||||
}
|
||||
} else {
|
||||
content = newConfig
|
||||
}
|
||||
|
||||
// Write the config file
|
||||
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
|
||||
func (db *DB) GenerateRcloneConfigWithToken(config *TransferConfig, token string) error {
|
||||
// Ensure we have a config directory
|
||||
dataDir := os.Getenv("DATA_DIR")
|
||||
if dataDir == "" {
|
||||
dataDir = "./data"
|
||||
}
|
||||
configDir := filepath.Join(dataDir, "configs")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate rclone config based on the config type
|
||||
configPath := filepath.Join(configDir, fmt.Sprintf("config_%d.conf", config.ID))
|
||||
|
||||
// Create a new config content
|
||||
var configContent strings.Builder
|
||||
|
||||
// First add the source configuration
|
||||
sourceName := fmt.Sprintf("source_%d", config.ID)
|
||||
|
||||
// Handle the source configuration based on type
|
||||
switch config.SourceType {
|
||||
case "google_drive":
|
||||
// Create a Google Drive remote using the "source_ID" naming convention for sources
|
||||
sourceSection := fmt.Sprintf("[%s]\ntype = drive\n", sourceName)
|
||||
|
||||
// Add custom client ID and secret if provided
|
||||
if config.SourceClientID != "" && config.SourceClientSecret != "" {
|
||||
sourceSection += fmt.Sprintf("client_id = %s\nclient_secret = %s\n",
|
||||
config.SourceClientID, config.SourceClientSecret)
|
||||
}
|
||||
|
||||
// Add team drive if specified
|
||||
if config.SourceTeamDrive != "" {
|
||||
sourceSection += fmt.Sprintf("team_drive = %s\n", config.SourceTeamDrive)
|
||||
}
|
||||
|
||||
// Clean up token string to prevent syntax errors and ensure it's a single line JSON
|
||||
// First, remove any whitespace from the beginning and end
|
||||
cleanToken := strings.TrimSpace(token)
|
||||
|
||||
// Check if it's already a JSON object
|
||||
if strings.HasPrefix(cleanToken, "{") && strings.HasSuffix(cleanToken, "}") {
|
||||
// It's a JSON object, but we need to make sure it's a single line
|
||||
var jsonObj map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(cleanToken), &jsonObj); err == nil {
|
||||
// Successfully parsed the JSON, now re-marshal it as a compact single line
|
||||
compactJSON, err := json.Marshal(jsonObj)
|
||||
if err == nil {
|
||||
// Use the compact JSON as the token
|
||||
sourceSection += fmt.Sprintf("token = %s\n", string(compactJSON))
|
||||
} else {
|
||||
// If there was an error re-marshaling, use the original but remove newlines
|
||||
// Replace all newlines and carriage returns with empty string
|
||||
singleLineToken := strings.ReplaceAll(cleanToken, "\n", "")
|
||||
singleLineToken = strings.ReplaceAll(singleLineToken, "\r", "")
|
||||
sourceSection += fmt.Sprintf("token = %s\n", singleLineToken)
|
||||
}
|
||||
} else {
|
||||
// If we couldn't parse the JSON, just remove newlines and carriage returns
|
||||
singleLineToken := strings.ReplaceAll(cleanToken, "\n", "")
|
||||
singleLineToken = strings.ReplaceAll(singleLineToken, "\r", "")
|
||||
sourceSection += fmt.Sprintf("token = %s\n", singleLineToken)
|
||||
}
|
||||
} else {
|
||||
// Not a valid JSON, try to fix it
|
||||
// First ensure it starts and ends with braces
|
||||
if !strings.HasPrefix(cleanToken, "{") {
|
||||
cleanToken = "{" + cleanToken
|
||||
}
|
||||
if !strings.HasSuffix(cleanToken, "}") {
|
||||
cleanToken = cleanToken + "}"
|
||||
}
|
||||
// Remove all newlines and carriage returns
|
||||
singleLineToken := strings.ReplaceAll(cleanToken, "\n", "")
|
||||
singleLineToken = strings.ReplaceAll(singleLineToken, "\r", "")
|
||||
sourceSection += fmt.Sprintf("token = %s\n", singleLineToken)
|
||||
}
|
||||
|
||||
configContent.WriteString(sourceSection)
|
||||
configContent.WriteString("\n")
|
||||
|
||||
case "sftp", "s3", "minio", "b2", "smb", "ftp", "webdav", "nextcloud", "onedrive":
|
||||
// For complex source types, use the GenerateRcloneConfig function
|
||||
// to create a temporary file, read it, and then append that content
|
||||
tempDir, err := os.MkdirTemp("", "gomft_temp")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
tempConfigPath := filepath.Join(tempDir, "temp_config.conf")
|
||||
|
||||
// Create a temporary file with just the source configuration
|
||||
tempContent := fmt.Sprintf("[%s]\ntype = local\n", sourceName)
|
||||
if err := os.WriteFile(tempConfigPath, []byte(tempContent), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write temporary config: %v", err)
|
||||
}
|
||||
|
||||
// Get rclone path
|
||||
rclonePath := os.Getenv("RCLONE_PATH")
|
||||
if rclonePath == "" {
|
||||
rclonePath = "rclone"
|
||||
}
|
||||
|
||||
// Use the appropriate rclone command to configure the source
|
||||
var args []string
|
||||
switch config.SourceType {
|
||||
case "sftp":
|
||||
args = []string{
|
||||
"config", "create", sourceName, "sftp",
|
||||
"host", config.SourceHost,
|
||||
"user", config.SourceUser,
|
||||
"port", fmt.Sprintf("%d", config.SourcePort),
|
||||
"--non-interactive",
|
||||
"--config", tempConfigPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
if config.SourcePassword != "" {
|
||||
args = append(args, "pass", config.SourcePassword)
|
||||
}
|
||||
if config.SourceKeyFile != "" {
|
||||
args = append(args, "key_file", config.SourceKeyFile)
|
||||
}
|
||||
case "s3":
|
||||
args = []string{
|
||||
"config", "create", sourceName, "s3",
|
||||
"provider", "AWS",
|
||||
"env_auth", "false",
|
||||
"access_key_id", config.SourceAccessKey,
|
||||
"secret_access_key", config.SourceSecretKey,
|
||||
"region", config.SourceRegion,
|
||||
"--non-interactive",
|
||||
"--config", tempConfigPath,
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
if config.SourceEndpoint != "" {
|
||||
args = append(args, "endpoint", config.SourceEndpoint)
|
||||
}
|
||||
}
|
||||
|
||||
// If we have arguments, execute the command
|
||||
if len(args) > 0 {
|
||||
cmd := exec.Command(rclonePath, args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create source config: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
// Read the generated config
|
||||
sourceConfig, err := os.ReadFile(tempConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read temporary config: %v", err)
|
||||
}
|
||||
|
||||
// Add it to our config content
|
||||
configContent.WriteString(string(sourceConfig))
|
||||
configContent.WriteString("\n")
|
||||
}
|
||||
default:
|
||||
// For local source or other simple types
|
||||
sourceSection := fmt.Sprintf("[%s]\ntype = local\n\n", sourceName)
|
||||
configContent.WriteString(sourceSection)
|
||||
}
|
||||
|
||||
// Now add the destination configuration
|
||||
destName := fmt.Sprintf("dest_%d", config.ID)
|
||||
|
||||
// Set up the destination section (only supporting Google Drive for now)
|
||||
if config.DestinationType == "gdrive" || config.DestinationType == "google_drive" {
|
||||
// Create a Google Drive remote using "dest_ID" naming convention
|
||||
destSection := fmt.Sprintf("[%s]\ntype = drive\n", destName)
|
||||
|
||||
// Add custom client ID and secret if provided
|
||||
if config.DestClientID != "" && config.DestClientSecret != "" {
|
||||
destSection += fmt.Sprintf("client_id = %s\nclient_secret = %s\n",
|
||||
config.DestClientID, config.DestClientSecret)
|
||||
}
|
||||
|
||||
// Clean up token string to prevent syntax errors and ensure it's a single line JSON
|
||||
// First, remove any whitespace from the beginning and end
|
||||
cleanToken := strings.TrimSpace(token)
|
||||
|
||||
// Check if it's already a JSON object
|
||||
if strings.HasPrefix(cleanToken, "{") && strings.HasSuffix(cleanToken, "}") {
|
||||
// It's a JSON object, but we need to make sure it's a single line
|
||||
var jsonObj map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(cleanToken), &jsonObj); err == nil {
|
||||
// Successfully parsed the JSON, now re-marshal it as a compact single line
|
||||
compactJSON, err := json.Marshal(jsonObj)
|
||||
if err == nil {
|
||||
// Use the compact JSON as the token
|
||||
destSection += fmt.Sprintf("token = %s\n", string(compactJSON))
|
||||
} else {
|
||||
// If there was an error re-marshaling, use the original but remove newlines
|
||||
// Replace all newlines and carriage returns with empty string
|
||||
singleLineToken := strings.ReplaceAll(cleanToken, "\n", "")
|
||||
singleLineToken = strings.ReplaceAll(singleLineToken, "\r", "")
|
||||
destSection += fmt.Sprintf("token = %s\n", singleLineToken)
|
||||
}
|
||||
} else {
|
||||
// If we couldn't parse the JSON, just remove newlines and carriage returns
|
||||
singleLineToken := strings.ReplaceAll(cleanToken, "\n", "")
|
||||
singleLineToken = strings.ReplaceAll(singleLineToken, "\r", "")
|
||||
destSection += fmt.Sprintf("token = %s\n", singleLineToken)
|
||||
}
|
||||
} else {
|
||||
// Not a valid JSON, try to fix it
|
||||
// First ensure it starts and ends with braces
|
||||
if !strings.HasPrefix(cleanToken, "{") {
|
||||
cleanToken = "{" + cleanToken
|
||||
}
|
||||
if !strings.HasSuffix(cleanToken, "}") {
|
||||
cleanToken = cleanToken + "}"
|
||||
}
|
||||
// Remove all newlines and carriage returns
|
||||
singleLineToken := strings.ReplaceAll(cleanToken, "\n", "")
|
||||
singleLineToken = strings.ReplaceAll(singleLineToken, "\r", "")
|
||||
destSection += fmt.Sprintf("token = %s\n", singleLineToken)
|
||||
}
|
||||
|
||||
// Add to the config content
|
||||
configContent.WriteString(destSection)
|
||||
} else {
|
||||
// Add a simple local destination for testing or if no specific destination type is handled
|
||||
destSection := fmt.Sprintf("[%s]\ntype = local\n", destName)
|
||||
configContent.WriteString(destSection)
|
||||
}
|
||||
|
||||
// Write the config file
|
||||
return os.WriteFile(configPath, []byte(configContent.String()), 0644)
|
||||
}
|
||||
|
||||
// GetIsAdmin returns the value of IsAdmin with a default if nil
|
||||
func (u *User) GetIsAdmin() bool {
|
||||
if u.IsAdmin == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *u.IsAdmin
|
||||
}
|
||||
|
||||
// SetIsAdmin sets the IsAdmin field
|
||||
func (u *User) SetIsAdmin(value bool) {
|
||||
u.IsAdmin = &value
|
||||
}
|
||||
|
||||
// GetAccountLocked returns the value of AccountLocked with a default if nil
|
||||
func (u *User) GetAccountLocked() bool {
|
||||
if u.AccountLocked == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *u.AccountLocked
|
||||
}
|
||||
|
||||
// SetAccountLocked sets the AccountLocked field
|
||||
func (u *User) SetAccountLocked(value bool) {
|
||||
u.AccountLocked = &value
|
||||
}
|
||||
|
||||
// GetUsed returns the value of Used with a default if nil
|
||||
func (t *PasswordResetToken) GetUsed() bool {
|
||||
if t.Used == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *t.Used
|
||||
}
|
||||
|
||||
// SetUsed sets the Used field
|
||||
func (t *PasswordResetToken) SetUsed(value bool) {
|
||||
t.Used = &value
|
||||
}
|
||||
|
||||
// GetSourcePassiveMode returns the value of SourcePassiveMode with a default if nil
|
||||
func (tc *TransferConfig) GetSourcePassiveMode() bool {
|
||||
if tc.SourcePassiveMode == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *tc.SourcePassiveMode
|
||||
}
|
||||
|
||||
// SetSourcePassiveMode sets the SourcePassiveMode field
|
||||
func (tc *TransferConfig) SetSourcePassiveMode(value bool) {
|
||||
tc.SourcePassiveMode = &value
|
||||
}
|
||||
|
||||
// GetDestPassiveMode returns the value of DestPassiveMode with a default if nil
|
||||
func (tc *TransferConfig) GetDestPassiveMode() bool {
|
||||
if tc.DestPassiveMode == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *tc.DestPassiveMode
|
||||
}
|
||||
|
||||
// SetDestPassiveMode sets the DestPassiveMode field
|
||||
func (tc *TransferConfig) SetDestPassiveMode(value bool) {
|
||||
tc.DestPassiveMode = &value
|
||||
}
|
||||
|
||||
// GetGoogleDriveAuthenticated returns the value of GoogleDriveAuthenticated with a default if nil
|
||||
func (tc *TransferConfig) GetGoogleDriveAuthenticated() bool {
|
||||
if tc.GoogleDriveAuthenticated == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *tc.GoogleDriveAuthenticated
|
||||
}
|
||||
|
||||
// SetGoogleDriveAuthenticated sets the GoogleDriveAuthenticated field
|
||||
func (tc *TransferConfig) SetGoogleDriveAuthenticated(value bool) {
|
||||
tc.GoogleDriveAuthenticated = &value
|
||||
}
|
||||
|
||||
// GetArchiveEnabled returns the value of ArchiveEnabled with a default if nil
|
||||
func (tc *TransferConfig) GetArchiveEnabled() bool {
|
||||
if tc.ArchiveEnabled == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *tc.ArchiveEnabled
|
||||
}
|
||||
|
||||
// SetArchiveEnabled sets the ArchiveEnabled field
|
||||
func (tc *TransferConfig) SetArchiveEnabled(value bool) {
|
||||
tc.ArchiveEnabled = &value
|
||||
}
|
||||
|
||||
// GetDeleteAfterTransfer returns the value of DeleteAfterTransfer with a default if nil
|
||||
func (tc *TransferConfig) GetDeleteAfterTransfer() bool {
|
||||
if tc.DeleteAfterTransfer == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *tc.DeleteAfterTransfer
|
||||
}
|
||||
|
||||
// SetDeleteAfterTransfer sets the DeleteAfterTransfer field
|
||||
func (tc *TransferConfig) SetDeleteAfterTransfer(value bool) {
|
||||
tc.DeleteAfterTransfer = &value
|
||||
}
|
||||
|
||||
// GetEnabled returns the value of Enabled with a default if nil
|
||||
func (j *Job) GetEnabled() bool {
|
||||
if j.Enabled == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *j.Enabled
|
||||
}
|
||||
|
||||
// SetEnabled sets the Enabled field
|
||||
func (j *Job) SetEnabled(value bool) {
|
||||
j.Enabled = &value
|
||||
}
|
||||
|
||||
// GetWebhookEnabled returns the value of WebhookEnabled with a default if nil
|
||||
func (j *Job) GetWebhookEnabled() bool {
|
||||
if j.WebhookEnabled == nil {
|
||||
return false // Default to false if not set
|
||||
}
|
||||
return *j.WebhookEnabled
|
||||
}
|
||||
|
||||
// SetWebhookEnabled sets the WebhookEnabled field
|
||||
func (j *Job) SetWebhookEnabled(value bool) {
|
||||
j.WebhookEnabled = &value
|
||||
}
|
||||
|
||||
// GetNotifyOnSuccess returns the value of NotifyOnSuccess with a default if nil
|
||||
func (j *Job) GetNotifyOnSuccess() bool {
|
||||
if j.NotifyOnSuccess == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *j.NotifyOnSuccess
|
||||
}
|
||||
|
||||
// SetNotifyOnSuccess sets the NotifyOnSuccess field
|
||||
func (j *Job) SetNotifyOnSuccess(value bool) {
|
||||
j.NotifyOnSuccess = &value
|
||||
}
|
||||
|
||||
// GetNotifyOnFailure returns the value of NotifyOnFailure with a default if nil
|
||||
func (j *Job) GetNotifyOnFailure() bool {
|
||||
if j.NotifyOnFailure == nil {
|
||||
return true // Default to true if not set
|
||||
}
|
||||
return *j.NotifyOnFailure
|
||||
}
|
||||
|
||||
// SetNotifyOnFailure sets the NotifyOnFailure field
|
||||
func (j *Job) SetNotifyOnFailure(value bool) {
|
||||
j.NotifyOnFailure = &value
|
||||
}
|
||||
|
||||
// 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 "", ""
|
||||
}
|
||||
|
||||
// Check if the file exists
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Read the rclone config file
|
||||
content, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Parse the content to extract client_id and client_secret from both source and destination sections
|
||||
lines := strings.Split(string(content), "\n")
|
||||
|
||||
// Define section names based on config ID
|
||||
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)
|
||||
|
||||
// Check if we're entering a new section
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
inSourceSection = line == sourceSectionName
|
||||
inDestSection = line == destSectionName
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract credentials from source section
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract credentials from destination section
|
||||
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 we found both values in both sections, we can stop processing
|
||||
if sourceClientID != "" && sourceClientSecret != "" && destClientID != "" && destClientSecret != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer destination credentials since we're authenticating for destination
|
||||
if destClientID != "" && destClientSecret != "" {
|
||||
return destClientID, destClientSecret
|
||||
}
|
||||
|
||||
// Fall back to source credentials if available
|
||||
if sourceClientID != "" && sourceClientSecret != "" {
|
||||
return sourceClientID, sourceClientSecret
|
||||
}
|
||||
|
||||
return "", ""
|
||||
}
|
||||
+596
-13
@@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -42,9 +43,9 @@ func TestUserCRUD(t *testing.T) {
|
||||
testUser := &User{
|
||||
Email: fmt.Sprintf("test-%d@example.com", time.Now().UnixNano()),
|
||||
PasswordHash: "hashed_password",
|
||||
IsAdmin: true,
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
testUser.SetIsAdmin(true)
|
||||
|
||||
// Test Create
|
||||
err := db.CreateUser(testUser)
|
||||
@@ -113,7 +114,7 @@ func TestPasswordResetToken(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, testToken.ID, retrievedToken.ID, "Retrieved token should have the same ID")
|
||||
assert.Equal(t, testUser.ID, retrievedToken.UserID, "Retrieved token should reference the correct user")
|
||||
assert.False(t, retrievedToken.Used, "Token should not be marked as used initially")
|
||||
assert.False(t, retrievedToken.GetUsed(), "Token should not be marked as used initially")
|
||||
|
||||
// Mark token as used
|
||||
err = db.MarkPasswordResetTokenAsUsed(retrievedToken.ID)
|
||||
@@ -129,7 +130,7 @@ func TestPasswordResetToken(t *testing.T) {
|
||||
if result.Error != nil {
|
||||
t.Fatalf("Failed to get updated password reset token: %v", result.Error)
|
||||
}
|
||||
assert.True(t, updatedToken.Used, "Token should be marked as used")
|
||||
assert.True(t, updatedToken.GetUsed(), "Token should be marked as used")
|
||||
}
|
||||
|
||||
func TestTransferConfigCRUD(t *testing.T) {
|
||||
@@ -240,11 +241,11 @@ func TestJobCRUD(t *testing.T) {
|
||||
Name: fmt.Sprintf("Test Job %d", time.Now().UnixNano()),
|
||||
ConfigID: testConfig.ID,
|
||||
Schedule: "0 * * * *", // Run every hour
|
||||
Enabled: true,
|
||||
LastRun: &now,
|
||||
NextRun: &nextRun,
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
testJob.SetEnabled(true)
|
||||
|
||||
// Test Create
|
||||
err = db.CreateJob(testJob)
|
||||
@@ -278,7 +279,7 @@ func TestJobCRUD(t *testing.T) {
|
||||
|
||||
// Test Update
|
||||
retrievedJob.Name = fmt.Sprintf("Updated Job %d", time.Now().UnixNano())
|
||||
retrievedJob.Enabled = false
|
||||
retrievedJob.SetEnabled(false)
|
||||
err = db.UpdateJob(retrievedJob)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update job: %v", err)
|
||||
@@ -367,7 +368,6 @@ func TestJobMultipleConfigs(t *testing.T) {
|
||||
testJob := &Job{
|
||||
Name: "Multi Config Job",
|
||||
Schedule: "0 * * * *",
|
||||
Enabled: true,
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
@@ -468,7 +468,6 @@ func TestJobHistoryCRUD(t *testing.T) {
|
||||
Name: fmt.Sprintf("Test Job %d", time.Now().UnixNano()),
|
||||
ConfigID: testConfig.ID,
|
||||
Schedule: "0 * * * *", // Run every hour
|
||||
Enabled: true,
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
err = db.CreateJob(testJob)
|
||||
@@ -548,7 +547,6 @@ func TestFileMetadataCRUD(t *testing.T) {
|
||||
Name: fmt.Sprintf("Test Job %d", time.Now().UnixNano()),
|
||||
ConfigID: testConfig.ID,
|
||||
Schedule: "0 * * * *", // Run every hour
|
||||
Enabled: true,
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
err = db.CreateJob(testJob)
|
||||
@@ -634,7 +632,7 @@ func TestGetConfigRclonePath(t *testing.T) {
|
||||
testUser := &User{
|
||||
Email: "rclone-test@example.com",
|
||||
PasswordHash: "hashed_password",
|
||||
IsAdmin: false,
|
||||
IsAdmin: BoolPtr(false),
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
|
||||
@@ -671,7 +669,7 @@ func TestGenerateRcloneConfig(t *testing.T) {
|
||||
testUser := &User{
|
||||
Email: "rclone-gen-test@example.com",
|
||||
PasswordHash: "hashed_password",
|
||||
IsAdmin: false,
|
||||
IsAdmin: BoolPtr(false),
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
|
||||
@@ -708,7 +706,7 @@ func TestGenerateRcloneConfig(t *testing.T) {
|
||||
DestHost: "ftp.example.com",
|
||||
DestPort: 21,
|
||||
DestUser: "ftpuser",
|
||||
DestPassiveMode: true,
|
||||
DestPassiveMode: BoolPtr(true),
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
@@ -770,7 +768,7 @@ func TestUpdateJobStatus(t *testing.T) {
|
||||
testUser := &User{
|
||||
Email: "job-status-test@example.com",
|
||||
PasswordHash: "hashed_password",
|
||||
IsAdmin: false,
|
||||
IsAdmin: BoolPtr(false),
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
|
||||
@@ -800,11 +798,11 @@ func TestUpdateJobStatus(t *testing.T) {
|
||||
Name: "Test Job Status",
|
||||
ConfigID: testConfig.ID,
|
||||
Schedule: "0 * * * *", // Run hourly
|
||||
Enabled: true,
|
||||
LastRun: &lastRun,
|
||||
NextRun: &nextRun,
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
testJob.SetEnabled(true)
|
||||
|
||||
err = db.CreateJob(testJob)
|
||||
assert.NoError(t, err)
|
||||
@@ -833,3 +831,588 @@ func TestUpdateJobStatus(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, updatedNextRun.Unix(), updatedJob.NextRun.Unix())
|
||||
}
|
||||
|
||||
func BoolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func TestGoogleDriveTransferConfig(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create a test user
|
||||
testUser := &User{
|
||||
Email: fmt.Sprintf("google-test-%d@example.com", time.Now().UnixNano()),
|
||||
PasswordHash: "hashed_password",
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
err := db.CreateUser(testUser)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
// Test 1: Create config with Google Drive as source
|
||||
googleSourceConfig := &TransferConfig{
|
||||
Name: "Google Drive Source Test",
|
||||
SourceType: "google_drive",
|
||||
SourcePath: "/path/in/google/drive",
|
||||
SourceClientID: "google_client_id",
|
||||
SourceClientSecret: "google_client_secret",
|
||||
SourceTeamDrive: "team_drive_id",
|
||||
DestinationType: "local",
|
||||
DestinationPath: "/local/destination/path",
|
||||
FilePattern: "*.pdf",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
// Set authenticated status
|
||||
authenticated := true
|
||||
googleSourceConfig.GoogleDriveAuthenticated = &authenticated
|
||||
|
||||
// Create the config
|
||||
err = db.CreateTransferConfig(googleSourceConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, googleSourceConfig.ID, "Config ID should be set after creation")
|
||||
|
||||
// Test 2: Create config with Google Drive as destination
|
||||
googleDestConfig := &TransferConfig{
|
||||
Name: "Google Drive Destination Test",
|
||||
SourceType: "local",
|
||||
SourcePath: "/local/source/path",
|
||||
DestinationType: "google_drive",
|
||||
DestinationPath: "/path/in/google/drive",
|
||||
DestClientID: "google_client_id",
|
||||
DestClientSecret: "google_client_secret",
|
||||
DestTeamDrive: "team_drive_id",
|
||||
FilePattern: "*.docx",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
// Set authenticated status
|
||||
googleDestConfig.GoogleDriveAuthenticated = &authenticated
|
||||
|
||||
// Create the config
|
||||
err = db.CreateTransferConfig(googleDestConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, googleDestConfig.ID, "Config ID should be set after creation")
|
||||
|
||||
// Test 3: Create config with Google Drive as both source and destination
|
||||
googleBothConfig := &TransferConfig{
|
||||
Name: "Google Drive Both Test",
|
||||
SourceType: "google_drive",
|
||||
SourcePath: "/source/path/in/google/drive",
|
||||
SourceClientID: "source_google_client_id",
|
||||
SourceClientSecret: "source_google_client_secret",
|
||||
SourceTeamDrive: "source_team_drive_id",
|
||||
DestinationType: "google_drive",
|
||||
DestinationPath: "/dest/path/in/google/drive",
|
||||
DestClientID: "dest_google_client_id",
|
||||
DestClientSecret: "dest_google_client_secret",
|
||||
DestTeamDrive: "dest_team_drive_id",
|
||||
FilePattern: "*.xlsx",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
// Set authenticated status
|
||||
googleBothConfig.GoogleDriveAuthenticated = &authenticated
|
||||
|
||||
// Create the config
|
||||
err = db.CreateTransferConfig(googleBothConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, googleBothConfig.ID, "Config ID should be set after creation")
|
||||
|
||||
// Test retrieving and verifying Google Drive configs
|
||||
retrievedSourceConfig, err := db.GetTransferConfig(googleSourceConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "google_drive", retrievedSourceConfig.SourceType)
|
||||
assert.Equal(t, "/path/in/google/drive", retrievedSourceConfig.SourcePath)
|
||||
assert.Equal(t, "google_client_id", retrievedSourceConfig.SourceClientID)
|
||||
assert.Equal(t, "team_drive_id", retrievedSourceConfig.SourceTeamDrive)
|
||||
assert.True(t, *retrievedSourceConfig.GoogleDriveAuthenticated)
|
||||
|
||||
retrievedDestConfig, err := db.GetTransferConfig(googleDestConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "google_drive", retrievedDestConfig.DestinationType)
|
||||
assert.Equal(t, "/path/in/google/drive", retrievedDestConfig.DestinationPath)
|
||||
assert.Equal(t, "google_client_id", retrievedDestConfig.DestClientID)
|
||||
assert.Equal(t, "team_drive_id", retrievedDestConfig.DestTeamDrive)
|
||||
assert.True(t, *retrievedDestConfig.GoogleDriveAuthenticated)
|
||||
|
||||
// Test updating Google Drive config
|
||||
retrievedSourceConfig.SourcePath = "/updated/google/drive/path"
|
||||
retrievedSourceConfig.SourceTeamDrive = "updated_team_drive_id"
|
||||
err = db.UpdateTransferConfig(retrievedSourceConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify update
|
||||
updatedConfig, err := db.GetTransferConfig(googleSourceConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/updated/google/drive/path", updatedConfig.SourcePath)
|
||||
assert.Equal(t, "updated_team_drive_id", updatedConfig.SourceTeamDrive)
|
||||
|
||||
// Test changing authentication status
|
||||
unauthenticated := false
|
||||
updatedConfig.GoogleDriveAuthenticated = &unauthenticated
|
||||
err = db.UpdateTransferConfig(updatedConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify authentication status update
|
||||
finalConfig, err := db.GetTransferConfig(googleSourceConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, *finalConfig.GoogleDriveAuthenticated)
|
||||
|
||||
// Clean up
|
||||
err = db.DeleteTransferConfig(googleSourceConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
err = db.DeleteTransferConfig(googleDestConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
err = db.DeleteTransferConfig(googleBothConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGoogleDriveJobExecution(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create a test user
|
||||
testUser := &User{
|
||||
Email: fmt.Sprintf("google-job-test-%d@example.com", time.Now().UnixNano()),
|
||||
PasswordHash: "hashed_password",
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
err := db.CreateUser(testUser)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
// Create a test transfer config with Google Drive as source
|
||||
googleConfig := &TransferConfig{
|
||||
Name: "Google Drive Job Test",
|
||||
SourceType: "google_drive",
|
||||
SourcePath: "/source/path/in/google/drive",
|
||||
SourceClientID: "google_client_id",
|
||||
SourceClientSecret: "google_client_secret",
|
||||
SourceTeamDrive: "team_drive_id",
|
||||
DestinationType: "local",
|
||||
DestinationPath: "/local/destination/path",
|
||||
FilePattern: "*.pdf",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
// Set authenticated status
|
||||
authenticated := true
|
||||
googleConfig.GoogleDriveAuthenticated = &authenticated
|
||||
|
||||
// Create the config
|
||||
err = db.CreateTransferConfig(googleConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, googleConfig.ID, "Config ID should be set after creation")
|
||||
|
||||
// Create a job using the Google Drive config
|
||||
job := &Job{
|
||||
Name: "Google Drive Test Job",
|
||||
Schedule: "0 * * * *", // Run every hour
|
||||
ConfigID: googleConfig.ID,
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
// Set job as enabled
|
||||
job.SetEnabled(true)
|
||||
|
||||
// Set up webhook notifications
|
||||
job.SetWebhookEnabled(true)
|
||||
job.WebhookURL = "https://example.com/webhook"
|
||||
job.SetNotifyOnSuccess(true)
|
||||
job.SetNotifyOnFailure(true)
|
||||
|
||||
// Create the job
|
||||
err = db.CreateJob(job)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, job.ID, "Job ID should be set after creation")
|
||||
|
||||
// Test retrieving the job
|
||||
retrievedJob, err := db.GetJob(job.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Google Drive Test Job", retrievedJob.Name)
|
||||
assert.Equal(t, googleConfig.ID, retrievedJob.ConfigID)
|
||||
assert.True(t, retrievedJob.GetEnabled())
|
||||
assert.True(t, retrievedJob.GetWebhookEnabled())
|
||||
assert.Equal(t, "https://example.com/webhook", retrievedJob.WebhookURL)
|
||||
assert.True(t, retrievedJob.GetNotifyOnSuccess())
|
||||
assert.True(t, retrievedJob.GetNotifyOnFailure())
|
||||
|
||||
// Create job history entry for this job
|
||||
startTime := time.Now().Add(-10 * time.Minute)
|
||||
endTime := time.Now()
|
||||
jobHistory := &JobHistory{
|
||||
JobID: job.ID,
|
||||
ConfigID: googleConfig.ID,
|
||||
StartTime: startTime,
|
||||
EndTime: &endTime,
|
||||
Status: "success",
|
||||
BytesTransferred: 1024 * 1024 * 5, // 5 MB
|
||||
FilesTransferred: 3,
|
||||
}
|
||||
|
||||
err = db.Create(jobHistory).Error
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, jobHistory.ID, "JobHistory ID should be set after creation")
|
||||
|
||||
// Create file metadata entries
|
||||
fileMetadata1 := &FileMetadata{
|
||||
JobID: job.ID,
|
||||
ConfigID: googleConfig.ID,
|
||||
FileName: "document1.pdf",
|
||||
OriginalPath: "/source/path/in/google/drive/document1.pdf",
|
||||
FileSize: 1024 * 1024 * 2, // 2 MB
|
||||
FileHash: "hash1",
|
||||
CreationTime: time.Now().Add(-24 * time.Hour),
|
||||
ModTime: time.Now().Add(-12 * time.Hour),
|
||||
ProcessedTime: startTime.Add(1 * time.Minute),
|
||||
DestinationPath: "/local/destination/path/document1.pdf",
|
||||
Status: "processed",
|
||||
}
|
||||
|
||||
fileMetadata2 := &FileMetadata{
|
||||
JobID: job.ID,
|
||||
ConfigID: googleConfig.ID,
|
||||
FileName: "document2.pdf",
|
||||
OriginalPath: "/source/path/in/google/drive/document2.pdf",
|
||||
FileSize: 1024 * 1024 * 1, // 1 MB
|
||||
FileHash: "hash2",
|
||||
CreationTime: time.Now().Add(-24 * time.Hour),
|
||||
ModTime: time.Now().Add(-12 * time.Hour),
|
||||
ProcessedTime: startTime.Add(2 * time.Minute),
|
||||
DestinationPath: "/local/destination/path/document2.pdf",
|
||||
Status: "processed",
|
||||
}
|
||||
|
||||
fileMetadata3 := &FileMetadata{
|
||||
JobID: job.ID,
|
||||
ConfigID: googleConfig.ID,
|
||||
FileName: "document3.pdf",
|
||||
OriginalPath: "/source/path/in/google/drive/document3.pdf",
|
||||
FileSize: 1024 * 1024 * 2, // 2 MB
|
||||
FileHash: "hash3",
|
||||
CreationTime: time.Now().Add(-24 * time.Hour),
|
||||
ModTime: time.Now().Add(-12 * time.Hour),
|
||||
ProcessedTime: startTime.Add(3 * time.Minute),
|
||||
DestinationPath: "/local/destination/path/document3.pdf",
|
||||
Status: "processed",
|
||||
}
|
||||
|
||||
err = db.Create(fileMetadata1).Error
|
||||
assert.NoError(t, err)
|
||||
err = db.Create(fileMetadata2).Error
|
||||
assert.NoError(t, err)
|
||||
err = db.Create(fileMetadata3).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test fetching job history
|
||||
var histories []JobHistory
|
||||
err = db.Where("job_id = ?", job.ID).Find(&histories).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(histories), "Should have 1 job history entry")
|
||||
assert.Equal(t, job.ID, histories[0].JobID)
|
||||
assert.Equal(t, googleConfig.ID, histories[0].ConfigID)
|
||||
assert.Equal(t, "success", histories[0].Status)
|
||||
assert.Equal(t, int64(1024*1024*5), histories[0].BytesTransferred)
|
||||
assert.Equal(t, 3, histories[0].FilesTransferred)
|
||||
|
||||
// Test fetching file metadata
|
||||
var files []FileMetadata
|
||||
err = db.Where("job_id = ?", job.ID).Find(&files).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, len(files), "Should have 3 file metadata entries")
|
||||
|
||||
// Clean up
|
||||
err = db.Where("job_id = ?", job.ID).Delete(&FileMetadata{}).Error
|
||||
assert.NoError(t, err)
|
||||
err = db.Where("job_id = ?", job.ID).Delete(&JobHistory{}).Error
|
||||
assert.NoError(t, err)
|
||||
err = db.Delete(&job).Error
|
||||
assert.NoError(t, err)
|
||||
err = db.Delete(&googleConfig).Error
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGoogleDriveAuthentication(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create a test user
|
||||
testUser := &User{
|
||||
Email: fmt.Sprintf("google-auth-test-%d@example.com", time.Now().UnixNano()),
|
||||
PasswordHash: "hashed_password",
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
err := db.CreateUser(testUser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create a Google Drive config that requires authentication
|
||||
googleConfig := &TransferConfig{
|
||||
Name: "Google Drive Auth Test",
|
||||
SourceType: "google_drive",
|
||||
SourcePath: "/source/path",
|
||||
SourceClientID: "test_client_id",
|
||||
SourceClientSecret: "test_client_secret",
|
||||
DestinationType: "local",
|
||||
DestinationPath: "/local/path",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
// Initially not authenticated
|
||||
unauthenticated := false
|
||||
googleConfig.GoogleDriveAuthenticated = &unauthenticated
|
||||
|
||||
// Create the config
|
||||
err = db.CreateTransferConfig(googleConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test 1: Verify initial unauthenticated state
|
||||
retrievedConfig, err := db.GetTransferConfig(googleConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, retrievedConfig.GetGoogleDriveAuthenticated())
|
||||
|
||||
// Test 2: Simulate authentication with token
|
||||
mockToken := `{"access_token":"test_access_token","refresh_token":"test_refresh_token","expiry":"2023-12-31T12:00:00Z"}`
|
||||
err = db.StoreGoogleDriveToken(fmt.Sprintf("%d", googleConfig.ID), mockToken)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify authentication state was updated
|
||||
updatedConfig, err := db.GetTransferConfig(googleConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, updatedConfig.GetGoogleDriveAuthenticated())
|
||||
|
||||
// Test 3: Generate rclone config with token
|
||||
err = db.GenerateRcloneConfigWithToken(updatedConfig, mockToken)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get config path
|
||||
configPath := db.GetConfigRclonePath(updatedConfig)
|
||||
|
||||
// On test systems, the directory might not exist
|
||||
configDir := filepath.Dir(configPath)
|
||||
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||
// Create directory if it doesn't exist
|
||||
err = os.MkdirAll(configDir, 0755)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Check if config was generated properly
|
||||
_, err = os.Stat(configPath)
|
||||
// In a test environment, this may fail if the rclone executable is not available
|
||||
// or permissions are wrong, so we'll just log it rather than fail the test
|
||||
if err != nil {
|
||||
t.Logf("Warning: could not verify rclone config file: %v", err)
|
||||
}
|
||||
|
||||
// Test 4: Simulate deauthentication (token revocation)
|
||||
updatedConfig.SetGoogleDriveAuthenticated(false)
|
||||
err = db.UpdateTransferConfig(updatedConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify deauthentication
|
||||
finalConfig, err := db.GetTransferConfig(googleConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, finalConfig.GetGoogleDriveAuthenticated())
|
||||
|
||||
// Clean up
|
||||
err = db.DeleteTransferConfig(googleConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGoogleDriveErrorHandling(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create a test user
|
||||
testUser := &User{
|
||||
Email: fmt.Sprintf("google-error-test-%d@example.com", time.Now().UnixNano()),
|
||||
PasswordHash: "hashed_password",
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
err := db.CreateUser(testUser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test 1: Create a config with missing required fields
|
||||
incompleteConfig := &TransferConfig{
|
||||
Name: "Incomplete Google Drive Config",
|
||||
SourceType: "google_drive",
|
||||
SourcePath: "", // Missing path
|
||||
DestinationType: "local",
|
||||
DestinationPath: "/local/path",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
// This should still succeed at the database level, as validation typically happens at the application level
|
||||
err = db.CreateTransferConfig(incompleteConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, incompleteConfig.ID, "Config ID should be set after creation")
|
||||
|
||||
// Test 2: Config with invalid Team Drive ID
|
||||
invalidTeamDriveConfig := &TransferConfig{
|
||||
Name: "Invalid Team Drive Config",
|
||||
SourceType: "google_drive",
|
||||
SourcePath: "/test/path",
|
||||
SourceClientID: "test_client_id",
|
||||
SourceClientSecret: "test_client_secret",
|
||||
SourceTeamDrive: "invalid_team_drive_id",
|
||||
DestinationType: "local",
|
||||
DestinationPath: "/local/path",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
err = db.CreateTransferConfig(invalidTeamDriveConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Set it as authenticated (this would normally fail in a real environment)
|
||||
authenticated := true
|
||||
invalidTeamDriveConfig.GoogleDriveAuthenticated = &authenticated
|
||||
err = db.UpdateTransferConfig(invalidTeamDriveConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// When trying to test a transfer with an invalid team drive in a real environment,
|
||||
// the rclone command would fail. We can't directly test this in a unit test,
|
||||
// but we can verify the config is properly set up to cause the expected failure.
|
||||
retrievedConfig, err := db.GetTransferConfig(invalidTeamDriveConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_team_drive_id", retrievedConfig.SourceTeamDrive, "Retrieved config should have the invalid team drive ID")
|
||||
assert.True(t, retrievedConfig.GetGoogleDriveAuthenticated(), "Config should be marked as authenticated")
|
||||
|
||||
// Test 3: Test authentication error scenario - using malformed token
|
||||
badTokenConfig := &TransferConfig{
|
||||
Name: "Bad Token Config",
|
||||
SourceType: "google_drive",
|
||||
SourcePath: "/test/path",
|
||||
SourceClientID: "test_client_id",
|
||||
SourceClientSecret: "test_client_secret",
|
||||
DestinationType: "local",
|
||||
DestinationPath: "/local/path",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
err = db.CreateTransferConfig(badTokenConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try to store a malformed token - shouldn't crash but may fail
|
||||
// In real-world usage, this would lead to auth failures when trying to use the token
|
||||
malformedToken := `{"not_valid_json`
|
||||
err = db.StoreGoogleDriveToken(fmt.Sprintf("%d", badTokenConfig.ID), malformedToken)
|
||||
// Even with malformed tokens, the DB operation might succeed as we're just storing a string
|
||||
// but the authentication would fail in actual usage
|
||||
if err != nil {
|
||||
t.Logf("StoreGoogleDriveToken returned error with malformed token as expected: %v", err)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
err = db.DeleteTransferConfig(incompleteConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
err = db.DeleteTransferConfig(invalidTeamDriveConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
err = db.DeleteTransferConfig(badTokenConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGoogleDriveTeamDrive(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create a test user
|
||||
testUser := &User{
|
||||
Email: fmt.Sprintf("google-teamdrive-test-%d@example.com", time.Now().UnixNano()),
|
||||
PasswordHash: "hashed_password",
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
err := db.CreateUser(testUser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test 1: Configure source with Team Drive
|
||||
teamDriveSourceConfig := &TransferConfig{
|
||||
Name: "Team Drive Source Test",
|
||||
SourceType: "google_drive",
|
||||
SourcePath: "/shared/documents",
|
||||
SourceClientID: "test_client_id",
|
||||
SourceClientSecret: "test_client_secret",
|
||||
SourceTeamDrive: "source_team_drive_id",
|
||||
DestinationType: "local",
|
||||
DestinationPath: "/local/path",
|
||||
FilePattern: "*.pdf",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
// Set as authenticated
|
||||
authenticated := true
|
||||
teamDriveSourceConfig.GoogleDriveAuthenticated = &authenticated
|
||||
|
||||
// Create the config
|
||||
err = db.CreateTransferConfig(teamDriveSourceConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, teamDriveSourceConfig.ID, "Config ID should be set after creation")
|
||||
|
||||
// Test 2: Configure destination with Team Drive
|
||||
teamDriveDestConfig := &TransferConfig{
|
||||
Name: "Team Drive Destination Test",
|
||||
SourceType: "local",
|
||||
SourcePath: "/local/source",
|
||||
DestinationType: "google_drive",
|
||||
DestinationPath: "/team/drive/path",
|
||||
DestClientID: "test_client_id",
|
||||
DestClientSecret: "test_client_secret",
|
||||
DestTeamDrive: "dest_team_drive_id",
|
||||
FilePattern: "*.docx",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
// Set as authenticated
|
||||
teamDriveDestConfig.GoogleDriveAuthenticated = &authenticated
|
||||
|
||||
// Create the config
|
||||
err = db.CreateTransferConfig(teamDriveDestConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, teamDriveDestConfig.ID, "Config ID should be set after creation")
|
||||
|
||||
// Test 3: Configure both source and destination with Team Drive
|
||||
teamDriveBothConfig := &TransferConfig{
|
||||
Name: "Team Drive Both Test",
|
||||
SourceType: "google_drive",
|
||||
SourcePath: "/source/team/drive/path",
|
||||
SourceClientID: "source_client_id",
|
||||
SourceClientSecret: "source_client_secret",
|
||||
SourceTeamDrive: "source_team_drive_id",
|
||||
DestinationType: "google_drive",
|
||||
DestinationPath: "/dest/team/drive/path",
|
||||
DestClientID: "dest_client_id",
|
||||
DestClientSecret: "dest_client_secret",
|
||||
DestTeamDrive: "dest_team_drive_id",
|
||||
FilePattern: "*.xlsx",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
// Set as authenticated
|
||||
teamDriveBothConfig.GoogleDriveAuthenticated = &authenticated
|
||||
|
||||
// Create the config
|
||||
err = db.CreateTransferConfig(teamDriveBothConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, teamDriveBothConfig.ID, "Config ID should be set after creation")
|
||||
|
||||
// Test retrieving configs and verify Team Drive IDs are set correctly
|
||||
retrievedSourceConfig, err := db.GetTransferConfig(teamDriveSourceConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "source_team_drive_id", retrievedSourceConfig.SourceTeamDrive)
|
||||
assert.Empty(t, retrievedSourceConfig.DestTeamDrive)
|
||||
|
||||
retrievedDestConfig, err := db.GetTransferConfig(teamDriveDestConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, retrievedDestConfig.SourceTeamDrive)
|
||||
assert.Equal(t, "dest_team_drive_id", retrievedDestConfig.DestTeamDrive)
|
||||
|
||||
retrievedBothConfig, err := db.GetTransferConfig(teamDriveBothConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "source_team_drive_id", retrievedBothConfig.SourceTeamDrive)
|
||||
assert.Equal(t, "dest_team_drive_id", retrievedBothConfig.DestTeamDrive)
|
||||
|
||||
// Clean up
|
||||
err = db.DeleteTransferConfig(teamDriveSourceConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
err = db.DeleteTransferConfig(teamDriveDestConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
err = db.DeleteTransferConfig(teamDriveBothConfig.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func TestDeleteTransferConfigEdgeCases(t *testing.T) {
|
||||
Name: "Job for Config",
|
||||
ConfigID: configWithJob.ID,
|
||||
Schedule: "0 * * * *",
|
||||
Enabled: true,
|
||||
Enabled: BoolPtr(true),
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
err = db.CreateJob(job)
|
||||
@@ -122,7 +122,7 @@ func TestDeleteJobEdgeCases(t *testing.T) {
|
||||
Name: fmt.Sprintf("Edge Job %d", i),
|
||||
ConfigID: config.ID,
|
||||
Schedule: "0 * * * *",
|
||||
Enabled: true,
|
||||
Enabled: BoolPtr(true),
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
err = db.CreateJob(job)
|
||||
@@ -145,7 +145,7 @@ func TestDeleteJobEdgeCases(t *testing.T) {
|
||||
Name: "Job with History",
|
||||
ConfigID: config.ID,
|
||||
Schedule: "0 * * * *",
|
||||
Enabled: true,
|
||||
Enabled: BoolPtr(true),
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
err = db.CreateJob(jobWithHistory)
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestGetPasswordResetTokenError(t *testing.T) {
|
||||
UserID: testUser.ID,
|
||||
Token: "used-token",
|
||||
ExpiresAt: time.Now().Add(1 * time.Hour),
|
||||
Used: true,
|
||||
Used: BoolPtr(true),
|
||||
}
|
||||
err = db.CreatePasswordResetToken(usedToken)
|
||||
assert.NoError(t, err)
|
||||
@@ -131,7 +131,7 @@ func TestGenerateRcloneConfigErrors(t *testing.T) {
|
||||
testUser := &User{
|
||||
Email: "config-error-test@example.com",
|
||||
PasswordHash: "hashed_password",
|
||||
IsAdmin: false,
|
||||
IsAdmin: BoolPtr(false),
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestInitializeWithExistingDB(t *testing.T) {
|
||||
user := &User{
|
||||
Email: "test@example.com",
|
||||
PasswordHash: "hash",
|
||||
IsAdmin: true,
|
||||
IsAdmin: BoolPtr(true),
|
||||
}
|
||||
err = db1.CreateUser(user)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AddGoogleDriveAuthenticated adds the GoogleDriveAuthenticated field to the transfer_configs table
|
||||
func AddGoogleDriveAuthenticated() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "20240315_add_google_drive_authenticated",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
// Add the GoogleDriveAuthenticated column with a default value of false
|
||||
return tx.Exec("ALTER TABLE transfer_configs ADD COLUMN google_drive_authenticated BOOLEAN DEFAULT false").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
// Remove the GoogleDriveAuthenticated column
|
||||
return tx.Exec("ALTER TABLE transfer_configs DROP COLUMN google_drive_authenticated").Error
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ func InitMigrations(db *gorm.DB) *gormigrate.Gormigrate {
|
||||
AddMultiConfigSupport(),
|
||||
UpdateSkipProcessedFilesToNullable(),
|
||||
AddWebhookSupport(),
|
||||
AddGoogleDriveAuthenticated(),
|
||||
}
|
||||
|
||||
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
|
||||
|
||||
@@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -135,3 +136,116 @@ func TestGenerateRcloneConfigWithoutRclone(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleDriveRcloneConfig(t *testing.T) {
|
||||
// Skip if rclone not available
|
||||
rclonePath := os.Getenv("RCLONE_PATH")
|
||||
if rclonePath == "" {
|
||||
rclonePath = "rclone" // default to PATH lookup
|
||||
}
|
||||
_, err := exec.Command(rclonePath, "--version").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Skip("Skipping test as rclone is not available")
|
||||
}
|
||||
|
||||
db := setupTestDB(t)
|
||||
|
||||
// Create a test user
|
||||
testUser := &User{
|
||||
Email: fmt.Sprintf("google-rclone-test-%d@example.com", time.Now().UnixNano()),
|
||||
PasswordHash: "hashed_password",
|
||||
LastPasswordChange: time.Now(),
|
||||
}
|
||||
err = db.CreateUser(testUser)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
// Create Google Drive source config
|
||||
googleSourceConfig := &TransferConfig{
|
||||
Name: "Google Drive Source Rclone Test",
|
||||
SourceType: "google_drive",
|
||||
SourcePath: "/path/in/google/drive",
|
||||
SourceClientID: "source_google_client_id",
|
||||
SourceClientSecret: "source_google_client_secret",
|
||||
SourceTeamDrive: "source_team_drive_id",
|
||||
DestinationType: "local",
|
||||
DestinationPath: "/local/destination/path",
|
||||
FilePattern: "*.pdf",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
// Set authenticated status
|
||||
authenticated := true
|
||||
googleSourceConfig.GoogleDriveAuthenticated = &authenticated
|
||||
|
||||
// Create the config
|
||||
err = db.CreateTransferConfig(googleSourceConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = db.GenerateRcloneConfigWithToken(googleSourceConfig, "test_token")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Generate rclone config for source
|
||||
configPath := db.GetConfigRclonePath(googleSourceConfig)
|
||||
|
||||
// Check that the file exists
|
||||
_, err = os.Stat(configPath)
|
||||
assert.NoError(t, err, "Rclone config file should exist")
|
||||
|
||||
// Read the config file
|
||||
configContent, err := os.ReadFile(configPath)
|
||||
assert.NoError(t, err)
|
||||
content := string(configContent)
|
||||
|
||||
// Verify it contains Google Drive specific content
|
||||
assert.Contains(t, content, "type = drive")
|
||||
assert.Contains(t, content, fmt.Sprintf("client_id = %s", googleSourceConfig.SourceClientID))
|
||||
assert.Contains(t, content, "source")
|
||||
assert.Contains(t, content, fmt.Sprintf("team_drive = %s", googleSourceConfig.SourceTeamDrive))
|
||||
|
||||
// Create Google Drive destination config
|
||||
googleDestConfig := &TransferConfig{
|
||||
Name: "Google Drive Dest Rclone Test",
|
||||
SourceType: "local",
|
||||
SourcePath: "/local/source/path",
|
||||
DestinationType: "google_drive",
|
||||
DestinationPath: "/dest/path/in/google/drive",
|
||||
DestClientID: "dest_google_client_id",
|
||||
DestClientSecret: "dest_google_client_secret",
|
||||
DestTeamDrive: "dest_team_drive_id",
|
||||
FilePattern: "*.pdf",
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
|
||||
// Set authenticated status
|
||||
googleDestConfig.GoogleDriveAuthenticated = &authenticated
|
||||
|
||||
// Create the config
|
||||
err = db.CreateTransferConfig(googleDestConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Generate rclone config for destination
|
||||
configPath = db.GetConfigRclonePath(googleDestConfig)
|
||||
|
||||
// Check that the file exists
|
||||
_, err = os.Stat(configPath)
|
||||
assert.NoError(t, err, "Rclone config file should exist")
|
||||
|
||||
// Read the config file
|
||||
configContent, err = os.ReadFile(configPath)
|
||||
assert.NoError(t, err)
|
||||
content = string(configContent)
|
||||
|
||||
// Verify it contains Google Drive specific content
|
||||
assert.Contains(t, content, "type = drive")
|
||||
assert.Contains(t, content, fmt.Sprintf("client_id = %s", googleDestConfig.DestClientID))
|
||||
assert.Contains(t, content, "dest")
|
||||
assert.Contains(t, content, fmt.Sprintf("team_drive = %s", googleDestConfig.DestTeamDrive))
|
||||
|
||||
// Clean up
|
||||
err = db.Delete(&googleSourceConfig).Error
|
||||
assert.NoError(t, err)
|
||||
err = db.Delete(&googleDestConfig).Error
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func TestDeleteJobWithTransaction(t *testing.T) {
|
||||
Name: "Test Delete Job",
|
||||
ConfigID: testConfig.ID,
|
||||
Schedule: "0 * * * *", // Run hourly
|
||||
Enabled: true,
|
||||
Enabled: BoolPtr(true),
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
err = db.CreateJob(testJob)
|
||||
@@ -137,7 +137,7 @@ func TestDeleteJobWithTransaction(t *testing.T) {
|
||||
Name: "Test Delete Job 2",
|
||||
ConfigID: testConfig.ID,
|
||||
Schedule: "0 * * * *", // Run hourly
|
||||
Enabled: true,
|
||||
Enabled: BoolPtr(true),
|
||||
CreatedBy: testUser.ID,
|
||||
}
|
||||
err = db.CreateJob(testJob2)
|
||||
|
||||
Reference in new issue
Block a user