feat: Enhance configuration handling and UI for job management

- Update .gitignore to include new provider files and ensure proper tracking.
- Refactor job and config forms to support multiple configuration selections, improving user experience.
- Implement logic to handle the initialization of skipProcessedFiles with a default value.
- Add new provider form templates for better organization and management of source and destination configurations.
- Enhance tests for provider forms and job configurations to ensure robust functionality.
- Introduce nullable handling for skipProcessedFiles in the database schema and update related migrations.
- Improve error handling and validation in job creation and editing processes.
This commit is contained in:
StarFleetCPTN committed 2025-03-14 16:30:43 -07:00
1 parent d6fa0c1603
commit 558e81c7e8
33 files changed
+2018 -776

No files matched your search

+17 -1
View File
@@ -104,7 +104,7 @@ type TransferConfig struct {
ArchiveEnabled bool `gorm:"default:false" form:"archive_enabled"`
RcloneFlags string `form:"rclone_flags"`
DeleteAfterTransfer bool `gorm:"default:false" form:"delete_after_transfer"`
SkipProcessedFiles bool `gorm:"default:true" form:"skip_processed_files"`
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
User User `gorm:"foreignkey:CreatedBy"`
@@ -831,6 +831,9 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
}
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
return jobs, err
@@ -862,3 +865,16 @@ func (db *DB) GetConfigsForJob(jobID uint) ([]TransferConfig, error) {
return configs, nil
}
// GetSkipProcessedFiles returns the value of SkipProcessedFiles with a default if nil
func (tc *TransferConfig) GetSkipProcessedFiles() bool {
if tc.SkipProcessedFiles == nil {
return true // Default to true if not set
}
return *tc.SkipProcessedFiles
}
// SetSkipProcessedFiles sets the SkipProcessedFiles field
func (tc *TransferConfig) SetSkipProcessedFiles(value bool) {
tc.SkipProcessedFiles = &value
}
+131
View File
@@ -303,6 +303,137 @@ func TestJobCRUD(t *testing.T) {
assert.Error(t, err, "Getting deleted job should return an error")
}
// Helper function to test if a config ID is selected for a job
func configSelected(job *Job, configID uint) bool {
// Check if the job has the config ID in its list
for _, id := range job.GetConfigIDsList() {
if id == configID {
return true
}
}
// As a fallback, check the primary ConfigID
return job.ConfigID == configID
}
func TestJobMultipleConfigs(t *testing.T) {
db := setupTestDB(t)
// Create a test user
testUser := &User{
Email: fmt.Sprintf("test-multi-%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 multiple test configs
config1 := &TransferConfig{
Name: "Test Config 1",
SourceType: "local",
SourcePath: "/source/path1",
DestinationType: "local",
DestinationPath: "/destination/path1",
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(config1)
assert.NoError(t, err)
config2 := &TransferConfig{
Name: "Test Config 2",
SourceType: "local",
SourcePath: "/source/path2",
DestinationType: "local",
DestinationPath: "/destination/path2",
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(config2)
assert.NoError(t, err)
config3 := &TransferConfig{
Name: "Test Config 3",
SourceType: "local",
SourcePath: "/source/path3",
DestinationType: "local",
DestinationPath: "/destination/path3",
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(config3)
assert.NoError(t, err)
// Test 1: Create job with multiple configs
testJob := &Job{
Name: "Multi Config Job",
Schedule: "0 * * * *",
Enabled: true,
CreatedBy: testUser.ID,
}
// Set multiple config IDs
configIDs := []uint{config1.ID, config2.ID, config3.ID}
testJob.SetConfigIDsList(configIDs)
// Verify ConfigIDs string format
assert.Contains(t, testJob.ConfigIDs, fmt.Sprintf("%d", config1.ID))
assert.Contains(t, testJob.ConfigIDs, fmt.Sprintf("%d", config2.ID))
assert.Contains(t, testJob.ConfigIDs, fmt.Sprintf("%d", config3.ID))
// Verify ConfigID is set to the first config
assert.Equal(t, config1.ID, testJob.ConfigID)
// Save the job
err = db.CreateJob(testJob)
assert.NoError(t, err)
// Test 2: Retrieve job and check config IDs
retrievedJob, err := db.GetJob(testJob.ID)
assert.NoError(t, err)
// Verify retrieved config IDs
retrievedIDs := retrievedJob.GetConfigIDsList()
assert.Len(t, retrievedIDs, 3)
assert.Contains(t, retrievedIDs, config1.ID)
assert.Contains(t, retrievedIDs, config2.ID)
assert.Contains(t, retrievedIDs, config3.ID)
// Test 3: Test configSelected function
assert.True(t, configSelected(retrievedJob, config1.ID))
assert.True(t, configSelected(retrievedJob, config2.ID))
assert.True(t, configSelected(retrievedJob, config3.ID))
assert.False(t, configSelected(retrievedJob, uint(999)))
// Test 4: Get configs for job
configs, err := db.GetConfigsForJob(testJob.ID)
assert.NoError(t, err)
assert.Len(t, configs, 3)
// Verify config names are correct
configNames := make([]string, len(configs))
for i, config := range configs {
configNames[i] = config.Name
}
assert.Contains(t, configNames, "Test Config 1")
assert.Contains(t, configNames, "Test Config 2")
assert.Contains(t, configNames, "Test Config 3")
// Test 5: Update config IDs
updatedIDs := []uint{config1.ID, config3.ID} // Remove config2
retrievedJob.SetConfigIDsList(updatedIDs)
err = db.UpdateJob(retrievedJob)
assert.NoError(t, err)
// Verify update
updatedJob, err := db.GetJob(testJob.ID)
assert.NoError(t, err)
updatedRetrievedIDs := updatedJob.GetConfigIDsList()
assert.Len(t, updatedRetrievedIDs, 2)
assert.Contains(t, updatedRetrievedIDs, config1.ID)
assert.Contains(t, updatedRetrievedIDs, config3.ID)
assert.NotContains(t, updatedRetrievedIDs, config2.ID)
}
func TestJobHistoryCRUD(t *testing.T) {
db := setupTestDB(t)
+1
View File
@@ -14,6 +14,7 @@ func InitMigrations(db *gorm.DB) *gormigrate.Gormigrate {
AddSkipProcessedFilesColumn(),
AddMaxConcurrentTransfersColumn(),
AddMultiConfigSupport(),
UpdateSkipProcessedFilesToNullable(),
}
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
@@ -0,0 +1,70 @@
package migrations
import (
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
// UpdateSkipProcessedFilesToNullable changes the skip_processed_files column to be nullable
func UpdateSkipProcessedFilesToNullable() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "20250515_update_skip_processed_files_to_nullable",
Migrate: func(tx *gorm.DB) error {
// SQLite specific command - this would need to be adjusted for other databases
return tx.Exec("ALTER TABLE transfer_configs RENAME TO transfer_configs_old; " +
"CREATE TABLE transfer_configs (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"name VARCHAR(255) NOT NULL, " +
"source_type VARCHAR(255) NOT NULL, " +
"source_path VARCHAR(255) NOT NULL, " +
"source_host VARCHAR(255), " +
"source_port INTEGER DEFAULT 22, " +
"source_user VARCHAR(255), " +
"source_key_file VARCHAR(255), " +
"source_bucket VARCHAR(255), " +
"source_region VARCHAR(255), " +
"source_access_key VARCHAR(255), " +
"source_endpoint VARCHAR(255), " +
"source_share VARCHAR(255), " +
"source_domain VARCHAR(255), " +
"source_passive_mode BOOLEAN DEFAULT true, " +
"source_client_id VARCHAR(255), " +
"source_drive_id VARCHAR(255), " +
"source_team_drive VARCHAR(255), " +
"file_pattern VARCHAR(255) DEFAULT '*', " +
"output_pattern VARCHAR(255), " +
"destination_type VARCHAR(255) NOT NULL, " +
"destination_path VARCHAR(255) NOT NULL, " +
"dest_host VARCHAR(255), " +
"dest_port INTEGER DEFAULT 22, " +
"dest_user VARCHAR(255), " +
"dest_key_file VARCHAR(255), " +
"dest_bucket VARCHAR(255), " +
"dest_region VARCHAR(255), " +
"dest_access_key VARCHAR(255), " +
"dest_endpoint VARCHAR(255), " +
"dest_share VARCHAR(255), " +
"dest_domain VARCHAR(255), " +
"dest_passive_mode BOOLEAN DEFAULT true, " +
"dest_client_id VARCHAR(255), " +
"dest_drive_id VARCHAR(255), " +
"dest_team_drive VARCHAR(255), " +
"archive_path VARCHAR(255), " +
"archive_enabled BOOLEAN DEFAULT false, " +
"rclone_flags VARCHAR(255), " +
"delete_after_transfer BOOLEAN DEFAULT false, " +
"skip_processed_files BOOLEAN DEFAULT true, " + // Keep as BOOLEAN, but now it's nullable
"max_concurrent_transfers INTEGER DEFAULT 4, " +
"created_by INTEGER, " +
"created_at DATETIME, " +
"updated_at DATETIME" +
"); " +
"INSERT INTO transfer_configs SELECT * FROM transfer_configs_old; " +
"DROP TABLE transfer_configs_old;").Error
},
Rollback: func(tx *gorm.DB) error {
// No need to rollback as the data structure remains compatible
return nil
},
}
}