-
+
-
Important Note About Google Photos
-
+
Important Note About Google Photos
+
When downloading from Google Photos, be aware that some original metadata may not be preserved. Google Photos processes and may compress some images upon upload.
diff --git a/components/providers/source/local.templ b/components/providers/source/local.templ
index 7e98f7b..3cb9848 100644
--- a/components/providers/source/local.templ
+++ b/components/providers/source/local.templ
@@ -6,7 +6,7 @@ templ LocalSourceForm() {
Local Path
-
- Absolute path to the local directory containing the files to transfer.
+
+ Full path to the local directory containing your files
+
+
+
+
+
+
+ Check Location
+
-}
\ No newline at end of file
+}
diff --git a/internal/api/api.go b/internal/api/api.go
index 8d6d14f..fcd1579 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -711,7 +711,7 @@ func handleListHistory(database *db.DB) gin.HandlerFunc {
// For now, just return the most recent 100 history entries for the user's jobs
var history []db.JobHistory
err := database.DB.
- Joins("JOIN jobs ON job_histories.job_id = jobs.id").
+ Joins("JOIN jobs ON job_history.job_id = jobs.id").
Where("jobs.created_by = ?", userID).
Order("start_time DESC").
Limit(100).
diff --git a/internal/db/db.go b/internal/db/db.go
index 09e295b..0b773bf 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -11,7 +11,7 @@ import (
"time"
"github.com/glebarez/sqlite"
- "github.com/starfleetcptn/gomft/internal/auth"
+ "github.com/starfleetcptn/gomft/internal/db/migrations"
"gorm.io/gorm"
)
@@ -109,7 +109,8 @@ type TransferConfig struct {
DestStartYear int `form:"dest_start_year"` // For Google Photos
DestIncludeArchived *bool `form:"dest_include_archived"` // For Google Photos
// Security fields
- UseBuiltinAuth *bool `form:"use_builtin_auth"` // For Google and other OAuth services
+ UseBuiltinAuthSource *bool `form:"use_builtin_auth_source"` // For Google and other OAuth services
+ UseBuiltinAuthDest *bool `form:"use_builtin_auth_dest"` // For Google and other OAuth services
GoogleDriveAuthenticated *bool // Whether Google Drive auth is completed
// General fields
ArchivePath string `form:"archive_path"`
@@ -250,10 +251,10 @@ func Initialize(dbPath string) (*DB, error) {
return nil, fmt.Errorf("failed to connect to database: %v", err)
}
- // Auto migrate the schema
- err = db.AutoMigrate(&User{}, &auth.PasswordHistory{}, &PasswordResetToken{}, &TransferConfig{}, &Job{}, &JobHistory{}, &FileMetadata{})
- if err != nil {
- return nil, fmt.Errorf("failed to migrate database: %v", err)
+ // Initialize and run migrations
+ m := migrations.InitMigrations(db)
+ if err := m.Migrate(); err != nil {
+ return nil, fmt.Errorf("failed to run migrations: %v", err)
}
return &DB{DB: db}, nil
@@ -627,7 +628,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create source config: %v\nOutput: %s", err, output)
}
- case "google_drive":
+ case "gdrive":
args := []string{
"config", "create", sourceName, "drive",
"client_id", config.SourceClientID,
@@ -906,24 +907,6 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
args = append(args, "include_archived", "true")
}
- 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 "google_drive":
- args := []string{
- "config", "create", destName, "drive",
- "client_id", config.DestClientID,
- "client_secret", config.DestClientSecret,
- "--non-interactive",
- "--config", configPath,
- "--log-level", "ERROR",
- }
-
- if config.DestTeamDrive != "" {
- args = append(args, "team_drive", config.DestTeamDrive)
- }
-
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)
@@ -1461,3 +1444,29 @@ func (db *DB) GetGDriveCredentialsFromConfig(config *TransferConfig) (string, st
return "", ""
}
+
+// GetUseBuiltinAuthSource returns the value of UseBuiltinAuthSource with a default if nil
+func (tc *TransferConfig) GetUseBuiltinAuthSource() bool {
+ if tc.UseBuiltinAuthSource == nil {
+ return true // Default to true if not set
+ }
+ return *tc.UseBuiltinAuthSource
+}
+
+// SetUseBuiltinAuthSource sets the UseBuiltinAuthSource field
+func (tc *TransferConfig) SetUseBuiltinAuthSource(value bool) {
+ tc.UseBuiltinAuthSource = &value
+}
+
+// GetUseBuiltinAuthDest returns the value of UseBuiltinAuthDest with a default if nil
+func (tc *TransferConfig) GetUseBuiltinAuthDest() bool {
+ if tc.UseBuiltinAuthDest == nil {
+ return true // Default to true if not set
+ }
+ return *tc.UseBuiltinAuthDest
+}
+
+// SetUseBuiltinAuthDest sets the UseBuiltinAuthDest field
+func (tc *TransferConfig) SetUseBuiltinAuthDest(value bool) {
+ tc.UseBuiltinAuthDest = &value
+}
diff --git a/internal/db/db_test.go b/internal/db/db_test.go
index c1e605c..f178ef3 100644
--- a/internal/db/db_test.go
+++ b/internal/db/db_test.go
@@ -853,7 +853,7 @@ func TestGoogleDriveTransferConfig(t *testing.T) {
// Test 1: Create config with Google Drive as source
googleSourceConfig := &TransferConfig{
Name: "Google Drive Source Test",
- SourceType: "google_drive",
+ SourceType: "gdrive",
SourcePath: "/path/in/google/drive",
SourceClientID: "google_client_id",
SourceClientSecret: "google_client_secret",
@@ -878,7 +878,7 @@ func TestGoogleDriveTransferConfig(t *testing.T) {
Name: "Google Drive Destination Test",
SourceType: "local",
SourcePath: "/local/source/path",
- DestinationType: "google_drive",
+ DestinationType: "gdrive",
DestinationPath: "/path/in/google/drive",
DestClientID: "google_client_id",
DestClientSecret: "google_client_secret",
@@ -898,12 +898,12 @@ func TestGoogleDriveTransferConfig(t *testing.T) {
// Test 3: Create config with Google Drive as both source and destination
googleBothConfig := &TransferConfig{
Name: "Google Drive Both Test",
- SourceType: "google_drive",
+ SourceType: "gdrive",
SourcePath: "/source/path/in/google/drive",
SourceClientID: "source_google_client_id",
SourceClientSecret: "source_google_client_secret",
SourceTeamDrive: "source_team_drive_id",
- DestinationType: "google_drive",
+ DestinationType: "gdrive",
DestinationPath: "/dest/path/in/google/drive",
DestClientID: "dest_google_client_id",
DestClientSecret: "dest_google_client_secret",
@@ -923,7 +923,7 @@ func TestGoogleDriveTransferConfig(t *testing.T) {
// 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, "gdrive", 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)
@@ -931,7 +931,7 @@ func TestGoogleDriveTransferConfig(t *testing.T) {
retrievedDestConfig, err := db.GetTransferConfig(googleDestConfig.ID)
assert.NoError(t, err)
- assert.Equal(t, "google_drive", retrievedDestConfig.DestinationType)
+ assert.Equal(t, "gdrive", 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)
@@ -986,7 +986,7 @@ func TestGoogleDriveJobExecution(t *testing.T) {
// Create a test transfer config with Google Drive as source
googleConfig := &TransferConfig{
Name: "Google Drive Job Test",
- SourceType: "google_drive",
+ SourceType: "gdrive",
SourcePath: "/source/path/in/google/drive",
SourceClientID: "google_client_id",
SourceClientSecret: "google_client_secret",
@@ -1149,7 +1149,7 @@ func TestGoogleDriveAuthentication(t *testing.T) {
// Create a Google Drive config that requires authentication
googleConfig := &TransferConfig{
Name: "Google Drive Auth Test",
- SourceType: "google_drive",
+ SourceType: "gdrive",
SourcePath: "/source/path",
SourceClientID: "test_client_id",
SourceClientSecret: "test_client_secret",
@@ -1234,7 +1234,7 @@ func TestGoogleDriveErrorHandling(t *testing.T) {
// Test 1: Create a config with missing required fields
incompleteConfig := &TransferConfig{
Name: "Incomplete Google Drive Config",
- SourceType: "google_drive",
+ SourceType: "gdrive",
SourcePath: "", // Missing path
DestinationType: "local",
DestinationPath: "/local/path",
@@ -1249,7 +1249,7 @@ func TestGoogleDriveErrorHandling(t *testing.T) {
// Test 2: Config with invalid Team Drive ID
invalidTeamDriveConfig := &TransferConfig{
Name: "Invalid Team Drive Config",
- SourceType: "google_drive",
+ SourceType: "gdrive",
SourcePath: "/test/path",
SourceClientID: "test_client_id",
SourceClientSecret: "test_client_secret",
@@ -1279,7 +1279,7 @@ func TestGoogleDriveErrorHandling(t *testing.T) {
// Test 3: Test authentication error scenario - using malformed token
badTokenConfig := &TransferConfig{
Name: "Bad Token Config",
- SourceType: "google_drive",
+ SourceType: "gdrive",
SourcePath: "/test/path",
SourceClientID: "test_client_id",
SourceClientSecret: "test_client_secret",
@@ -1325,7 +1325,7 @@ func TestGoogleDriveTeamDrive(t *testing.T) {
// Test 1: Configure source with Team Drive
teamDriveSourceConfig := &TransferConfig{
Name: "Team Drive Source Test",
- SourceType: "google_drive",
+ SourceType: "gdrive",
SourcePath: "/shared/documents",
SourceClientID: "test_client_id",
SourceClientSecret: "test_client_secret",
@@ -1350,7 +1350,7 @@ func TestGoogleDriveTeamDrive(t *testing.T) {
Name: "Team Drive Destination Test",
SourceType: "local",
SourcePath: "/local/source",
- DestinationType: "google_drive",
+ DestinationType: "gdrive",
DestinationPath: "/team/drive/path",
DestClientID: "test_client_id",
DestClientSecret: "test_client_secret",
@@ -1370,12 +1370,12 @@ func TestGoogleDriveTeamDrive(t *testing.T) {
// Test 3: Configure both source and destination with Team Drive
teamDriveBothConfig := &TransferConfig{
Name: "Team Drive Both Test",
- SourceType: "google_drive",
+ SourceType: "gdrive",
SourcePath: "/source/team/drive/path",
SourceClientID: "source_client_id",
SourceClientSecret: "source_client_secret",
SourceTeamDrive: "source_team_drive_id",
- DestinationType: "google_drive",
+ DestinationType: "gdrive",
DestinationPath: "/dest/team/drive/path",
DestClientID: "dest_client_id",
DestClientSecret: "dest_client_secret",
@@ -1434,7 +1434,7 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
// Test 1: Create config with Google Photos as source
sourceReadOnly := true
sourceIncludeArchived := false
- useBuiltinAuth := true
+ useBuiltinAuthSource := true
googleSourceConfig := &TransferConfig{
Name: "Google Photos Source Test",
SourceType: "gphotos",
@@ -1444,7 +1444,7 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
SourceReadOnly: &sourceReadOnly,
SourceStartYear: 2015,
SourceIncludeArchived: &sourceIncludeArchived,
- UseBuiltinAuth: &useBuiltinAuth,
+ UseBuiltinAuthSource: &useBuiltinAuthSource,
DestinationType: "local",
DestinationPath: "/local/destination/path",
FilePattern: "*.jpg",
@@ -1463,6 +1463,7 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
// Test 2: Create config with Google Photos as destination
destReadOnly := false
destIncludeArchived := true
+ useBuiltinAuthDest := true
googleDestConfig := &TransferConfig{
Name: "Google Photos Destination Test",
SourceType: "local",
@@ -1474,7 +1475,7 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
DestReadOnly: &destReadOnly,
DestStartYear: 2018,
DestIncludeArchived: &destIncludeArchived,
- UseBuiltinAuth: &useBuiltinAuth,
+ UseBuiltinAuthDest: &useBuiltinAuthDest,
FilePattern: "*.png",
CreatedBy: testUser.ID,
}
@@ -1504,7 +1505,8 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
DestReadOnly: &destReadOnly,
DestStartYear: 2020,
DestIncludeArchived: &destIncludeArchived,
- UseBuiltinAuth: &useBuiltinAuth,
+ UseBuiltinAuthSource: &useBuiltinAuthSource,
+ UseBuiltinAuthDest: &useBuiltinAuthDest,
FilePattern: "*.jpeg",
CreatedBy: testUser.ID,
}
@@ -1524,7 +1526,7 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
assert.Equal(t, sourceReadOnly, *retrievedConfig.SourceReadOnly)
assert.Equal(t, 2015, retrievedConfig.SourceStartYear)
assert.Equal(t, sourceIncludeArchived, *retrievedConfig.SourceIncludeArchived)
- assert.Equal(t, useBuiltinAuth, *retrievedConfig.UseBuiltinAuth)
+ assert.Equal(t, useBuiltinAuthSource, *retrievedConfig.UseBuiltinAuthSource)
assert.Equal(t, true, retrievedConfig.GetGoogleAuthenticated())
retrievedConfig, err = db.GetTransferConfig(googleDestConfig.ID)
@@ -1533,7 +1535,7 @@ func TestGooglePhotosTransferConfig(t *testing.T) {
assert.Equal(t, destReadOnly, *retrievedConfig.DestReadOnly)
assert.Equal(t, 2018, retrievedConfig.DestStartYear)
assert.Equal(t, destIncludeArchived, *retrievedConfig.DestIncludeArchived)
- assert.Equal(t, useBuiltinAuth, *retrievedConfig.UseBuiltinAuth)
+ assert.Equal(t, useBuiltinAuthDest, *retrievedConfig.UseBuiltinAuthDest)
assert.Equal(t, true, retrievedConfig.GetGoogleAuthenticated())
}
@@ -1573,7 +1575,7 @@ func TestGooglePhotosRcloneConfig(t *testing.T) {
// TEST 1: Google Photos as source with standard options
sourceReadOnly := true
sourceIncludeArchived := false
- useBuiltinAuth := true
+ useBuiltinAuthSource := true
gphotosSourceConfig := &TransferConfig{
ID: 1, // Force ID for predictable config path
Name: "Google Photos Source Config",
@@ -1584,7 +1586,7 @@ func TestGooglePhotosRcloneConfig(t *testing.T) {
SourceReadOnly: &sourceReadOnly,
SourceStartYear: 2015,
SourceIncludeArchived: &sourceIncludeArchived,
- UseBuiltinAuth: &useBuiltinAuth,
+ UseBuiltinAuthSource: &useBuiltinAuthSource,
DestinationType: "local",
DestinationPath: "/tmp/destination",
FilePattern: "*.jpg",
@@ -1617,6 +1619,7 @@ func TestGooglePhotosRcloneConfig(t *testing.T) {
// TEST 2: Google Photos as destination with authenticated token
destReadOnly := false
destIncludeArchived := true
+ useBuiltinAuthDest := true
gphotosDestConfig := &TransferConfig{
ID: 2, // Force ID for predictable config path
Name: "Google Photos Destination Config",
@@ -1629,7 +1632,7 @@ func TestGooglePhotosRcloneConfig(t *testing.T) {
DestReadOnly: &destReadOnly,
DestStartYear: 2018,
DestIncludeArchived: &destIncludeArchived,
- UseBuiltinAuth: &useBuiltinAuth,
+ UseBuiltinAuthDest: &useBuiltinAuthDest,
FilePattern: "*.png",
CreatedBy: testUser.ID,
}
@@ -1686,7 +1689,7 @@ func TestGooglePhotosAuthentication(t *testing.T) {
// Create a transfer config with Google Photos
readOnly := true
includeArchived := false
- useBuiltinAuth := true
+ useBuiltinAuthSource := true
gPhotosConfig := &TransferConfig{
Name: "Test Google Photos Auth",
SourceType: "gphotos",
@@ -1696,7 +1699,7 @@ func TestGooglePhotosAuthentication(t *testing.T) {
SourceReadOnly: &readOnly,
SourceStartYear: 2015,
SourceIncludeArchived: &includeArchived,
- UseBuiltinAuth: &useBuiltinAuth,
+ UseBuiltinAuthSource: &useBuiltinAuthSource,
DestinationType: "local",
DestinationPath: "/tmp/destination",
FilePattern: "*.jpg",
diff --git a/internal/db/migrations/001_initial_schema.go b/internal/db/migrations/001_initial_schema.go
new file mode 100644
index 0000000..1ea80da
--- /dev/null
+++ b/internal/db/migrations/001_initial_schema.go
@@ -0,0 +1,278 @@
+package migrations
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/go-gormigrate/gormigrate/v2"
+ "gorm.io/gorm"
+)
+
+func InitialSchema() *gormigrate.Migration {
+ return &gormigrate.Migration{
+ ID: "001_initial_schema",
+ Migrate: func(tx *gorm.DB) error {
+ // Check if any tables exist (indicating an existing database)
+ var count int64
+ if err := tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&count).Error; err != nil {
+ return fmt.Errorf("failed to check for existing tables: %v", err)
+ }
+
+ // If tables exist, create a backup
+ if count > 0 {
+ // Get the database path
+ sqlDB, err := tx.DB()
+ if err != nil {
+ return fmt.Errorf("failed to get underlying database: %v", err)
+ }
+
+ var seq int
+ var name, dbPath string
+ if err := sqlDB.QueryRow("PRAGMA database_list").Scan(&seq, &name, &dbPath); err != nil {
+ return fmt.Errorf("failed to get database path: %v", err)
+ }
+
+ // Create backup file with timestamp
+ backupFile := fmt.Sprintf("%s.backup.%s", dbPath, time.Now().Format("20060102_150405"))
+
+ // Read original database
+ data, err := os.ReadFile(dbPath)
+ if err != nil {
+ return fmt.Errorf("failed to read database for backup: %v", err)
+ }
+
+ // Write backup
+ if err := os.WriteFile(backupFile, data, 0600); err != nil {
+ return fmt.Errorf("failed to create database backup: %v", err)
+ }
+
+ fmt.Printf("Created database backup at: %s\n", backupFile)
+ }
+
+ // Disable foreign key constraints while creating tables
+ if err := tx.Exec("PRAGMA foreign_keys = OFF").Error; err != nil {
+ return fmt.Errorf("failed to disable foreign key constraints: %v", err)
+ }
+ defer func() {
+ if err := tx.Exec("PRAGMA foreign_keys = ON").Error; err != nil {
+ fmt.Printf("Warning: failed to re-enable foreign key constraints: %v\n", err)
+ }
+ }()
+
+ // Change the table name from job_histories to job_history
+ if err := tx.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='job_histories'").Scan(&count).Error; err != nil {
+ return err
+ }
+
+ if count > 0 {
+ if err := tx.Exec("ALTER TABLE job_histories RENAME TO job_history").Error; err != nil {
+ return err
+ }
+ }
+
+ // Change the table name from password_histories to password_history
+ count = 0
+ if err := tx.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='password_histories'").Scan(&count).Error; err != nil {
+ return err
+ }
+
+ if count > 0 {
+ if err := tx.Exec("ALTER TABLE password_histories RENAME TO password_history").Error; err != nil {
+ return err
+ }
+ }
+
+ // Create Users table
+ if err := tx.Exec(`CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ email VARCHAR(255) NOT NULL UNIQUE,
+ password_hash VARCHAR(255) NOT NULL,
+ is_admin BOOLEAN DEFAULT FALSE,
+ last_password_change DATETIME,
+ failed_login_attempts INTEGER DEFAULT 0,
+ account_locked BOOLEAN DEFAULT FALSE,
+ lockout_until DATETIME,
+ theme VARCHAR(255) DEFAULT 'light',
+ created_at DATETIME,
+ updated_at DATETIME
+ )`).Error; err != nil {
+ return err
+ }
+
+ // Create PasswordHistory table
+ if err := tx.Exec(`CREATE TABLE IF NOT EXISTS password_history (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ password_hash VARCHAR(255) NOT NULL,
+ created_at DATETIME,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+ )`).Error; err != nil {
+ return err
+ }
+
+ // Create PasswordResetToken table
+ if err := tx.Exec(`CREATE TABLE IF NOT EXISTS password_reset_tokens (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ token VARCHAR(255) NOT NULL,
+ expires_at DATETIME NOT NULL,
+ used BOOLEAN DEFAULT FALSE,
+ created_at DATETIME,
+ updated_at DATETIME,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+ )`).Error; err != nil {
+ return err
+ }
+
+ // Create TransferConfigs table
+ if err := tx.Exec(`CREATE TABLE IF NOT EXISTS transfer_configs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name VARCHAR(255) NOT NULL,
+ source_type VARCHAR(255) NOT NULL,
+ source_path TEXT NOT NULL,
+ source_host VARCHAR(255),
+ source_port INTEGER DEFAULT 22,
+ source_user VARCHAR(255),
+ source_key_file TEXT,
+ 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),
+ source_read_only BOOLEAN,
+ source_start_year INTEGER,
+ source_include_archived BOOLEAN,
+ file_pattern VARCHAR(255) DEFAULT '*',
+ output_pattern TEXT,
+ destination_type VARCHAR(255) NOT NULL,
+ destination_path TEXT NOT NULL,
+ dest_host VARCHAR(255),
+ dest_port INTEGER DEFAULT 22,
+ dest_user VARCHAR(255),
+ dest_key_file TEXT,
+ 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),
+ dest_read_only BOOLEAN,
+ dest_start_year INTEGER,
+ dest_include_archived BOOLEAN,
+ use_builtin_auth_source BOOLEAN DEFAULT TRUE,
+ use_builtin_auth_dest BOOLEAN DEFAULT TRUE,
+ google_drive_authenticated BOOLEAN,
+ archive_path TEXT,
+ archive_enabled BOOLEAN DEFAULT FALSE,
+ rclone_flags TEXT,
+ delete_after_transfer BOOLEAN DEFAULT FALSE,
+ skip_processed_files BOOLEAN DEFAULT TRUE,
+ max_concurrent_transfers INTEGER DEFAULT 4,
+ created_by INTEGER,
+ created_at DATETIME,
+ updated_at DATETIME,
+ FOREIGN KEY (created_by) REFERENCES users(id)
+ )`).Error; err != nil {
+ return err
+ }
+
+ // Create Jobs table
+ if err := tx.Exec(`CREATE TABLE IF NOT EXISTS jobs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name VARCHAR(255),
+ config_id INTEGER NOT NULL,
+ config_ids TEXT,
+ schedule VARCHAR(255) NOT NULL,
+ enabled BOOLEAN DEFAULT TRUE,
+ last_run DATETIME,
+ next_run DATETIME,
+ webhook_enabled BOOLEAN DEFAULT FALSE,
+ webhook_url TEXT,
+ webhook_secret TEXT,
+ webhook_headers TEXT,
+ notify_on_success BOOLEAN DEFAULT TRUE,
+ notify_on_failure BOOLEAN DEFAULT TRUE,
+ created_by INTEGER,
+ created_at DATETIME,
+ updated_at DATETIME,
+ FOREIGN KEY (config_id) REFERENCES transfer_configs(id),
+ FOREIGN KEY (created_by) REFERENCES users(id)
+ )`).Error; err != nil {
+ return err
+ }
+
+ // Create JobHistory table
+ if err := tx.Exec(`CREATE TABLE IF NOT EXISTS job_history (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ job_id INTEGER NOT NULL,
+ config_id INTEGER DEFAULT 0,
+ start_time DATETIME NOT NULL,
+ end_time DATETIME,
+ status VARCHAR(255) NOT NULL,
+ bytes_transferred INTEGER,
+ files_transferred INTEGER,
+ error_message TEXT,
+ FOREIGN KEY (job_id) REFERENCES jobs(id)
+ )`).Error; err != nil {
+ return err
+ }
+
+ // Create FileMetadata table
+ if err := tx.Exec(`CREATE TABLE IF NOT EXISTS file_metadata (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ job_id INTEGER NOT NULL,
+ config_id INTEGER DEFAULT 0,
+ file_name VARCHAR(255) NOT NULL,
+ original_path TEXT NOT NULL,
+ file_size INTEGER NOT NULL,
+ file_hash VARCHAR(255),
+ creation_time DATETIME,
+ mod_time DATETIME,
+ processed_time DATETIME NOT NULL,
+ destination_path TEXT NOT NULL,
+ status VARCHAR(255) NOT NULL,
+ error_message TEXT,
+ created_at DATETIME,
+ updated_at DATETIME,
+ FOREIGN KEY (job_id) REFERENCES jobs(id)
+ )`).Error; err != nil {
+ return err
+ }
+
+ // Re-enable foreign key constraints and verify integrity
+ if err := tx.Exec("PRAGMA foreign_key_check").Error; err != nil {
+ return fmt.Errorf("foreign key integrity check failed: %v", err)
+ }
+
+ return nil
+ },
+ Rollback: func(tx *gorm.DB) error {
+ // Drop tables in reverse order to handle foreign key constraints
+ tables := []string{
+ "file_metadata",
+ "job_history",
+ "jobs",
+ "transfer_configs",
+ "password_reset_tokens",
+ "password_history",
+ "users",
+ }
+ for _, table := range tables {
+ if err := tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", table)).Error; err != nil {
+ return err
+ }
+ }
+ return nil
+ },
+ }
+}
diff --git a/internal/db/migrations/002_update_builtin_auth_fields.go b/internal/db/migrations/002_update_builtin_auth_fields.go
new file mode 100644
index 0000000..90c9287
--- /dev/null
+++ b/internal/db/migrations/002_update_builtin_auth_fields.go
@@ -0,0 +1,49 @@
+package migrations
+
+import (
+ "github.com/go-gormigrate/gormigrate/v2"
+ "gorm.io/gorm"
+)
+
+// UpdateBuiltinAuthFields updates the use_builtin_auth field to separate source and destination fields
+func UpdateBuiltinAuthFields() *gormigrate.Migration {
+ return &gormigrate.Migration{
+ ID: "002_update_builtin_auth_fields",
+ Migrate: func(tx *gorm.DB) error {
+ // First, add the new columns
+ if err := tx.Exec(`ALTER TABLE transfer_configs ADD COLUMN use_builtin_auth_source BOOLEAN`).Error; err != nil {
+ return err
+ }
+ if err := tx.Exec(`ALTER TABLE transfer_configs ADD COLUMN use_builtin_auth_dest BOOLEAN`).Error; err != nil {
+ return err
+ }
+
+ // Copy the old value to both new columns
+ if err := tx.Exec(`UPDATE transfer_configs SET
+ use_builtin_auth_source = use_builtin_auth,
+ use_builtin_auth_dest = use_builtin_auth`).Error; err != nil {
+ return err
+ }
+
+ // Drop the old column
+ return tx.Exec(`ALTER TABLE transfer_configs DROP COLUMN use_builtin_auth`).Error
+ },
+ Rollback: func(tx *gorm.DB) error {
+ // Add back the original column
+ if err := tx.Exec(`ALTER TABLE transfer_configs ADD COLUMN use_builtin_auth BOOLEAN`).Error; err != nil {
+ return err
+ }
+
+ // Copy the source value back (could also use dest, they should be the same)
+ if err := tx.Exec(`UPDATE transfer_configs SET use_builtin_auth = use_builtin_auth_source`).Error; err != nil {
+ return err
+ }
+
+ // Drop the new columns
+ if err := tx.Exec(`ALTER TABLE transfer_configs DROP COLUMN use_builtin_auth_source`).Error; err != nil {
+ return err
+ }
+ return tx.Exec(`ALTER TABLE transfer_configs DROP COLUMN use_builtin_auth_dest`).Error
+ },
+ }
+}
diff --git a/internal/db/migrations/003_update_gdrive_type.go b/internal/db/migrations/003_update_gdrive_type.go
new file mode 100644
index 0000000..b92cccd
--- /dev/null
+++ b/internal/db/migrations/003_update_gdrive_type.go
@@ -0,0 +1,31 @@
+package migrations
+
+import (
+ "github.com/go-gormigrate/gormigrate/v2"
+ "gorm.io/gorm"
+)
+
+// UpdateGDriveType updates the source_type and destination_type from 'google_drive' to 'gdrive'
+func UpdateGDriveType() *gormigrate.Migration {
+ return &gormigrate.Migration{
+ ID: "003_update_gdrive_type",
+ Migrate: func(tx *gorm.DB) error {
+ // Update source_type
+ if err := tx.Exec(`UPDATE transfer_configs SET source_type = 'gdrive' WHERE source_type = 'google_drive'`).Error; err != nil {
+ return err
+ }
+
+ // Update destination_type
+ return tx.Exec(`UPDATE transfer_configs SET destination_type = 'gdrive' WHERE destination_type = 'google_drive'`).Error
+ },
+ Rollback: func(tx *gorm.DB) error {
+ // Revert source_type
+ if err := tx.Exec(`UPDATE transfer_configs SET source_type = 'google_drive' WHERE source_type = 'gdrive'`).Error; err != nil {
+ return err
+ }
+
+ // Revert destination_type
+ return tx.Exec(`UPDATE transfer_configs SET destination_type = 'google_drive' WHERE destination_type = 'gdrive'`).Error
+ },
+ }
+}
diff --git a/internal/db/migrations/add_cloud_storage_fields.go b/internal/db/migrations/add_cloud_storage_fields.go
deleted file mode 100644
index 3582df0..0000000
--- a/internal/db/migrations/add_cloud_storage_fields.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package migrations
-
-import (
- "github.com/go-gormigrate/gormigrate/v2"
- "gorm.io/gorm"
-)
-
-// AddCloudStorageFields adds fields for WebDAV, NextCloud, OneDrive, and Google Drive
-func AddCloudStorageFields() *gormigrate.Migration {
- return &gormigrate.Migration{
- ID: "add_cloud_storage_fields",
- Migrate: func(tx *gorm.DB) error {
- // Add source fields
- if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN source_client_id VARCHAR(255)").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN source_drive_id VARCHAR(255)").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN source_team_drive VARCHAR(255)").Error; err != nil {
- return err
- }
-
- // Add destination fields
- if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN dest_client_id VARCHAR(255)").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN dest_drive_id VARCHAR(255)").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN dest_team_drive VARCHAR(255)").Error; err != nil {
- return err
- }
-
- return nil
- },
- Rollback: func(tx *gorm.DB) error {
- // Drop source fields
- if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN source_client_id").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN source_drive_id").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN source_team_drive").Error; err != nil {
- return err
- }
-
- // Drop destination fields
- if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN dest_client_id").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN dest_drive_id").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN dest_team_drive").Error; err != nil {
- return err
- }
-
- return nil
- },
- }
-}
\ No newline at end of file
diff --git a/internal/db/migrations/add_delete_after_transfer.go b/internal/db/migrations/add_delete_after_transfer.go
deleted file mode 100644
index afe379c..0000000
--- a/internal/db/migrations/add_delete_after_transfer.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package migrations
-
-import (
- "github.com/go-gormigrate/gormigrate/v2"
- "gorm.io/gorm"
-)
-
-// AddDeleteAfterTransferColumn adds the delete_after_transfer column to transfer_configs table
-func AddDeleteAfterTransferColumn() *gormigrate.Migration {
- return &gormigrate.Migration{
- ID: "add_delete_after_transfer_column",
- Migrate: func(tx *gorm.DB) error {
- return tx.Exec("ALTER TABLE transfer_configs ADD COLUMN delete_after_transfer BOOLEAN NOT NULL DEFAULT false").Error
- },
- Rollback: func(tx *gorm.DB) error {
- return tx.Exec("ALTER TABLE transfer_configs DROP COLUMN delete_after_transfer").Error
- },
- }
-}
\ No newline at end of file
diff --git a/internal/db/migrations/add_google_drive_authenticated.go b/internal/db/migrations/add_google_drive_authenticated.go
deleted file mode 100644
index 29210db..0000000
--- a/internal/db/migrations/add_google_drive_authenticated.go
+++ /dev/null
@@ -1,21 +0,0 @@
-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
- },
- }
-}
diff --git a/internal/db/migrations/add_google_photos_support.go b/internal/db/migrations/add_google_photos_support.go
deleted file mode 100644
index 2e2255d..0000000
--- a/internal/db/migrations/add_google_photos_support.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package migrations
-
-import (
- "github.com/go-gormigrate/gormigrate/v2"
- "gorm.io/gorm"
-)
-
-// AddGooglePhotosSupport adds Google Photos related fields to the transfer_configs table
-func AddGooglePhotosSupport() *gormigrate.Migration {
- return &gormigrate.Migration{
- ID: "20240518_add_google_photos_support",
- Migrate: func(tx *gorm.DB) error {
- // Add Google Photos source fields
- if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN source_read_only BOOLEAN DEFAULT false").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN source_start_year INTEGER DEFAULT 0").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN source_include_archived BOOLEAN DEFAULT false").Error; err != nil {
- return err
- }
-
- // Add Google Photos destination fields
- if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN dest_read_only BOOLEAN DEFAULT false").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN dest_start_year INTEGER DEFAULT 0").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs ADD COLUMN dest_include_archived BOOLEAN DEFAULT false").Error; err != nil {
- return err
- }
-
- // Add OAuth field
- return tx.Exec("ALTER TABLE transfer_configs ADD COLUMN use_builtin_auth BOOLEAN DEFAULT true").Error
- },
- Rollback: func(tx *gorm.DB) error {
- // Remove all added columns in reverse order
- if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN use_builtin_auth").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN dest_include_archived").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN dest_start_year").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN dest_read_only").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN source_include_archived").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE transfer_configs DROP COLUMN source_start_year").Error; err != nil {
- return err
- }
- return tx.Exec("ALTER TABLE transfer_configs DROP COLUMN source_read_only").Error
- },
- }
-}
diff --git a/internal/db/migrations/add_max_concurrent_transfers.go b/internal/db/migrations/add_max_concurrent_transfers.go
deleted file mode 100644
index 8eb69ca..0000000
--- a/internal/db/migrations/add_max_concurrent_transfers.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package migrations
-
-import (
- "github.com/go-gormigrate/gormigrate/v2"
- "gorm.io/gorm"
-)
-
-// AddMaxConcurrentTransfersColumn adds the max_concurrent_transfers column to transfer_configs table
-func AddMaxConcurrentTransfersColumn() *gormigrate.Migration {
- return &gormigrate.Migration{
- ID: "20250311_add_max_concurrent_transfers",
- Migrate: func(tx *gorm.DB) error {
- // Add max_concurrent_transfers column with default value of 4
- return tx.Exec("ALTER TABLE transfer_configs ADD COLUMN max_concurrent_transfers INTEGER DEFAULT 4").Error
- },
- Rollback: func(tx *gorm.DB) error {
- // Drop the column if needed
- return tx.Exec("ALTER TABLE transfer_configs DROP COLUMN max_concurrent_transfers").Error
- },
- }
-}
diff --git a/internal/db/migrations/add_multi_config_support.go b/internal/db/migrations/add_multi_config_support.go
deleted file mode 100644
index f65dca1..0000000
--- a/internal/db/migrations/add_multi_config_support.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package migrations
-
-import (
- "github.com/go-gormigrate/gormigrate/v2"
- "gorm.io/gorm"
-)
-
-// AddMultiConfigSupport adds support for multiple configurations per job
-func AddMultiConfigSupport() *gormigrate.Migration {
- return &gormigrate.Migration{
- ID: "20250315_add_multi_config_support",
- Migrate: func(tx *gorm.DB) error {
- // Add config_ids column to jobs table
- if err := tx.Exec("ALTER TABLE jobs ADD COLUMN config_ids TEXT").Error; err != nil {
- return err
- }
-
- // Add config_id column to job_histories table
- if err := tx.Exec("ALTER TABLE job_histories ADD COLUMN config_id INTEGER").Error; err != nil {
- return err
- }
-
- // Add config_id column to file_metadata table
- if err := tx.Exec("ALTER TABLE file_metadata ADD COLUMN config_id INTEGER").Error; err != nil {
- return err
- }
-
- // Update existing jobs to set the config_ids field to match the current config_id
- if err := tx.Exec("UPDATE jobs SET config_ids = config_id WHERE config_id > 0").Error; err != nil {
- return err
- }
-
- return nil
- },
- Rollback: func(tx *gorm.DB) error {
- // Drop the config_id columns from job_histories and file_metadata
- if err := tx.Exec("ALTER TABLE job_histories DROP COLUMN config_id").Error; err != nil {
- return err
- }
-
- if err := tx.Exec("ALTER TABLE file_metadata DROP COLUMN config_id").Error; err != nil {
- return err
- }
-
- // Drop the config_ids column from jobs
- return tx.Exec("ALTER TABLE jobs DROP COLUMN config_ids").Error
- },
- }
-}
diff --git a/internal/db/migrations/add_skip_processed_files.go b/internal/db/migrations/add_skip_processed_files.go
deleted file mode 100644
index f6939b1..0000000
--- a/internal/db/migrations/add_skip_processed_files.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package migrations
-
-import (
- "github.com/go-gormigrate/gormigrate/v2"
- "gorm.io/gorm"
-)
-
-// AddSkipProcessedFilesColumn adds the skip_processed_files column to transfer_configs table
-func AddSkipProcessedFilesColumn() *gormigrate.Migration {
- return &gormigrate.Migration{
- ID: "20250310_add_skip_processed_files",
- Migrate: func(tx *gorm.DB) error {
- // Add skip_processed_files column with default value of true
- return tx.Exec("ALTER TABLE transfer_configs ADD COLUMN skip_processed_files BOOLEAN DEFAULT true").Error
- },
- Rollback: func(tx *gorm.DB) error {
- // Drop the column if needed
- return tx.Exec("ALTER TABLE transfer_configs DROP COLUMN skip_processed_files").Error
- },
- }
-}
diff --git a/internal/db/migrations/add_webhook_support.go b/internal/db/migrations/add_webhook_support.go
deleted file mode 100644
index ce4d1c4..0000000
--- a/internal/db/migrations/add_webhook_support.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package migrations
-
-import (
- "github.com/go-gormigrate/gormigrate/v2"
- "gorm.io/gorm"
-)
-
-// AddWebhookSupport adds webhook notification fields to the jobs table
-func AddWebhookSupport() *gormigrate.Migration {
- return &gormigrate.Migration{
- ID: "20240618_add_webhook_support",
- Migrate: func(tx *gorm.DB) error {
- // Add webhook URL field
- if err := tx.Exec("ALTER TABLE jobs ADD COLUMN webhook_enabled BOOLEAN DEFAULT false").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE jobs ADD COLUMN webhook_url VARCHAR(255)").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE jobs ADD COLUMN webhook_secret VARCHAR(255)").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE jobs ADD COLUMN webhook_headers TEXT").Error; err != nil {
- return err
- }
-
- // Add notification settings
- if err := tx.Exec("ALTER TABLE jobs ADD COLUMN notify_on_success BOOLEAN DEFAULT true").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE jobs ADD COLUMN notify_on_failure BOOLEAN DEFAULT true").Error; err != nil {
- return err
- }
-
- return nil
- },
- Rollback: func(tx *gorm.DB) error {
- // Drop the webhook fields from jobs
- if err := tx.Exec("ALTER TABLE jobs DROP COLUMN webhook_enabled").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE jobs DROP COLUMN webhook_url").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE jobs DROP COLUMN webhook_secret").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE jobs DROP COLUMN webhook_headers").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE jobs DROP COLUMN notify_on_success").Error; err != nil {
- return err
- }
- if err := tx.Exec("ALTER TABLE jobs DROP COLUMN notify_on_failure").Error; err != nil {
- return err
- }
-
- return nil
- },
- }
-}
diff --git a/internal/db/migrations/migrations.go b/internal/db/migrations/migrations.go
index fea061e..bcc2688 100644
--- a/internal/db/migrations/migrations.go
+++ b/internal/db/migrations/migrations.go
@@ -8,16 +8,9 @@ import (
// InitMigrations initializes the migrations
func InitMigrations(db *gorm.DB) *gormigrate.Gormigrate {
migrations := []*gormigrate.Migration{
- // ... existing migrations
- AddDeleteAfterTransferColumn(),
- AddCloudStorageFields(),
- AddSkipProcessedFilesColumn(),
- AddMaxConcurrentTransfersColumn(),
- AddMultiConfigSupport(),
- UpdateSkipProcessedFilesToNullable(),
- AddWebhookSupport(),
- AddGoogleDriveAuthenticated(),
- AddGooglePhotosSupport(),
+ InitialSchema(),
+ UpdateBuiltinAuthFields(),
+ UpdateGDriveType(),
}
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
diff --git a/internal/db/migrations/update_skip_processed_files_to_nullable.go b/internal/db/migrations/update_skip_processed_files_to_nullable.go
deleted file mode 100644
index f32428b..0000000
--- a/internal/db/migrations/update_skip_processed_files_to_nullable.go
+++ /dev/null
@@ -1,70 +0,0 @@
-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
- },
- }
-}
diff --git a/internal/db/rclone_test.go b/internal/db/rclone_test.go
index 57dc5d5..b49a5b5 100644
--- a/internal/db/rclone_test.go
+++ b/internal/db/rclone_test.go
@@ -72,7 +72,7 @@ func TestGenerateRcloneConfigWithoutRclone(t *testing.T) {
assert.NoError(t, err)
// Test configs for different source types
- sourceTypes := []string{"sftp", "s3", "minio", "b2", "smb", "ftp", "webdav", "nextcloud", "onedrive", "google_drive"}
+ sourceTypes := []string{"sftp", "s3", "minio", "b2", "smb", "ftp", "webdav", "nextcloud", "onedrive", "gdrive"}
for _, sourceType := range sourceTypes {
testConfig := &TransferConfig{
@@ -102,7 +102,7 @@ func TestGenerateRcloneConfigWithoutRclone(t *testing.T) {
}
// Test configs for different destination types
- destTypes := []string{"sftp", "s3", "minio", "b2", "smb", "ftp", "webdav", "nextcloud", "onedrive", "google_drive"}
+ destTypes := []string{"sftp", "s3", "minio", "b2", "smb", "ftp", "webdav", "nextcloud", "onedrive", "gdrive"}
for _, destType := range destTypes {
testConfig := &TransferConfig{
@@ -164,7 +164,7 @@ func TestGoogleDriveRcloneConfig(t *testing.T) {
// Create Google Drive source config
googleSourceConfig := &TransferConfig{
Name: "Google Drive Source Rclone Test",
- SourceType: "google_drive",
+ SourceType: "gdrive",
SourcePath: "/path/in/google/drive",
SourceClientID: "source_google_client_id",
SourceClientSecret: "source_google_client_secret",
@@ -209,7 +209,7 @@ func TestGoogleDriveRcloneConfig(t *testing.T) {
Name: "Google Drive Dest Rclone Test",
SourceType: "local",
SourcePath: "/local/source/path",
- DestinationType: "google_drive",
+ DestinationType: "gdrive",
DestinationPath: "/dest/path/in/google/drive",
DestClientID: "dest_google_client_id",
DestClientSecret: "dest_google_client_secret",
diff --git a/internal/web/handlers/config_handlers.go b/internal/web/handlers/config_handlers.go
index dc886a5..4db27d3 100644
--- a/internal/web/handlers/config_handlers.go
+++ b/internal/web/handlers/config_handlers.go
@@ -71,6 +71,7 @@ func (h *Handlers) HandleEditConfig(c *gin.Context) {
// HandleCreateConfig handles the POST /configs route
func (h *Handlers) HandleCreateConfig(c *gin.Context) {
var config db.TransferConfig
+
if err := c.ShouldBind(&config); err != nil {
log.Printf("Error binding config form: %v", err)
c.String(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %v", err))
@@ -118,9 +119,13 @@ func (h *Handlers) HandleCreateConfig(c *gin.Context) {
sourceIncludeArchivedValue := sourceIncludeArchivedVal == "on" || sourceIncludeArchivedVal == "true"
config.SourceIncludeArchived = &sourceIncludeArchivedValue
- useBuiltinAuthVal := c.Request.FormValue("use_builtin_auth")
- useBuiltinAuthValue := useBuiltinAuthVal == "on" || useBuiltinAuthVal == "true"
- config.UseBuiltinAuth = &useBuiltinAuthValue
+ useBuiltinAuthSourceVal := c.Request.FormValue("use_builtin_auth_source")
+ useBuiltinAuthSourceValue := useBuiltinAuthSourceVal == "on" || useBuiltinAuthSourceVal == "true"
+ config.UseBuiltinAuthSource = &useBuiltinAuthSourceValue
+
+ useBuiltinAuthDestVal := c.Request.FormValue("use_builtin_auth_dest")
+ useBuiltinAuthDestValue := useBuiltinAuthDestVal == "on" || useBuiltinAuthDestVal == "true"
+ config.UseBuiltinAuthDest = &useBuiltinAuthDestValue
if err := h.DB.Create(&config).Error; err != nil {
log.Printf("Error creating config: %v", err)
@@ -209,9 +214,13 @@ func (h *Handlers) HandleUpdateConfig(c *gin.Context) {
sourceIncludeArchivedValue := sourceIncludeArchivedVal == "on" || sourceIncludeArchivedVal == "true"
config.SourceIncludeArchived = &sourceIncludeArchivedValue
- useBuiltinAuthVal := c.Request.FormValue("use_builtin_auth")
- useBuiltinAuthValue := useBuiltinAuthVal == "on" || useBuiltinAuthVal == "true"
- config.UseBuiltinAuth = &useBuiltinAuthValue
+ useBuiltinAuthSourceVal := c.Request.FormValue("use_builtin_auth_source")
+ useBuiltinAuthSourceValue := useBuiltinAuthSourceVal == "on" || useBuiltinAuthSourceVal == "true"
+ config.UseBuiltinAuthSource = &useBuiltinAuthSourceValue
+
+ useBuiltinAuthDestVal := c.Request.FormValue("use_builtin_auth_dest")
+ useBuiltinAuthDestValue := useBuiltinAuthDestVal == "on" || useBuiltinAuthDestVal == "true"
+ config.UseBuiltinAuthDest = &useBuiltinAuthDestValue
// Preserve fields that shouldn't be updated
config.CreatedBy = oldConfig.CreatedBy
diff --git a/internal/web/handlers/gdrive_handlers.go b/internal/web/handlers/gdrive_handlers.go
index 96ed2c1..c8ad5ae 100644
--- a/internal/web/handlers/gdrive_handlers.go
+++ b/internal/web/handlers/gdrive_handlers.go
@@ -37,7 +37,7 @@ func (h *Handlers) HandleGDriveAuth(c *gin.Context) {
}
// Ensure it's a Google Drive or Google Photos configuration
- if config.DestinationType != "gdrive" && config.DestinationType != "gphotos" {
+ if config.SourceType != "gdrive" && config.DestinationType != "gdrive" && config.SourceType != "gphotos" && config.DestinationType != "gphotos" {
RenderErrorPage(c, "Not a Google configuration", "The selected configuration is not set up for Google Drive or Google Photos")
return
}
@@ -365,8 +365,8 @@ func (h *Handlers) HandleGDriveTokenProcess(c *gin.Context) {
return
}
- // Ensure it's a Google Drive configuration
- if config.DestinationType != "gdrive" {
+ // Ensure it's a Google Drive or Google Photos configuration
+ if config.SourceType != "gdrive" && config.DestinationType != "gdrive" && config.SourceType != "gphotos" && config.DestinationType != "gphotos" {
RenderErrorPage(c, "Not a Google Drive configuration", "")
return
}
diff --git a/internal/web/handlers/gdrive_handlers_test.go b/internal/web/handlers/gdrive_handlers_test.go
index 30d507f..db510a2 100644
--- a/internal/web/handlers/gdrive_handlers_test.go
+++ b/internal/web/handlers/gdrive_handlers_test.go
@@ -64,26 +64,26 @@ func (h *MockHandlers) HandleGDriveAuth(c *gin.Context) {
// Get the config ID from the query parameter
configIDStr := c.Param("id")
if configIDStr == "" {
- RenderErrorPage(c, "Missing configuration ID", "")
+ RenderErrorPageTest(c, "Missing configuration ID", "")
return
}
configID, err := strconv.ParseUint(configIDStr, 10, 64)
if err != nil {
- RenderErrorPage(c, "Invalid configuration ID", err.Error())
+ RenderErrorPageTest(c, "Invalid configuration ID", err.Error())
return
}
// Get the configuration
config, err := h.DB.GetTransferConfig(uint(configID))
if err != nil {
- RenderErrorPage(c, "Configuration not found", err.Error())
+ RenderErrorPageTest(c, "Configuration not found", err.Error())
return
}
// Ensure it's a Google Drive or Google Photos configuration
if config.DestinationType != "gdrive" && config.DestinationType != "gphotos" {
- RenderErrorPage(c, "Not a Google configuration", "The selected configuration is not set up for Google Drive or Google Photos")
+ RenderErrorPageTest(c, "Not a Google configuration", "The selected configuration is not set up for Google Drive or Google Photos")
return
}
@@ -96,14 +96,14 @@ func (h *MockHandlers) HandleGDriveAuth(c *gin.Context) {
// Get Rclone Config Path
rcloneConfigPath := h.DB.GetConfigRclonePath(config)
if rcloneConfigPath == "" {
- RenderErrorPage(c, "Rclone config not found", "The selected configuration does not have a valid rclone config")
+ RenderErrorPageTest(c, "Rclone config not found", "The selected configuration does not have a valid rclone config")
return
}
// Create a temporary config file for authentication
tempConfigDir := filepath.Join(dataDir, "temp")
if err := os.MkdirAll(tempConfigDir, 0755); err != nil {
- RenderErrorPage(c, "Failed to create temporary directory", err.Error())
+ RenderErrorPageTest(c, "Failed to create temporary directory", err.Error())
return
}
@@ -180,7 +180,7 @@ func (h *MockHandlers) HandleGDriveAuthCallback(c *gin.Context) {
// Get auth code from query parameters
authCode := c.Query("code")
if authCode == "" {
- RenderErrorPage(c, "Authentication failed", "No authorization code received from Google")
+ RenderErrorPageTest(c, "Authentication failed", "No authorization code received from Google")
return
}
@@ -188,27 +188,27 @@ func (h *MockHandlers) HandleGDriveAuthCallback(c *gin.Context) {
state := c.Query("state")
storedState, err := c.Cookie("gdrive_auth_state")
if err != nil || state != storedState {
- RenderErrorPage(c, "Authentication failed", "Invalid state parameter")
+ RenderErrorPageTest(c, "Authentication failed", "Invalid state parameter")
return
}
// Get config ID from cookie
configIDStr, err := c.Cookie("gdrive_config_id")
if err != nil {
- RenderErrorPage(c, "Authentication failed", "Unable to retrieve configuration ID")
+ RenderErrorPageTest(c, "Authentication failed", "Unable to retrieve configuration ID")
return
}
configID, err := strconv.ParseUint(configIDStr, 10, 64)
if err != nil {
- RenderErrorPage(c, "Invalid configuration ID", err.Error())
+ RenderErrorPageTest(c, "Invalid configuration ID", err.Error())
return
}
// Get the configuration
config, err := h.DB.GetTransferConfig(uint(configID))
if err != nil {
- RenderErrorPage(c, "Failed to get configuration", err.Error())
+ RenderErrorPageTest(c, "Failed to get configuration", err.Error())
return
}
@@ -219,7 +219,7 @@ func (h *MockHandlers) HandleGDriveAuthCallback(c *gin.Context) {
// Update the config with the token
err = h.DB.GenerateRcloneConfigWithToken(config, mockToken)
if err != nil {
- RenderErrorPage(c, "Failed to update configuration", err.Error())
+ RenderErrorPageTest(c, "Failed to update configuration", err.Error())
return
}
@@ -420,8 +420,8 @@ func TestHandleGDriveAuth_NonGoogleConfig(t *testing.T) {
assert.Contains(t, w.Body.String(), "Not a Google configuration")
}
-// RenderErrorPage renders an error page with the given message
-func RenderErrorPage(c *gin.Context, title string, details string) {
+// RenderErrorPageTest renders an error page with the given message
+func RenderErrorPageTest(c *gin.Context, title string, details string) {
// Here we'd typically use a component for error display
// For now, we'll just render a simple HTML error page for testing
errorHTML := fmt.Sprintf("
Error: %s %s
", title, details)
diff --git a/internal/web/handlers/path_handlers.go b/internal/web/handlers/path_handlers.go
new file mode 100644
index 0000000..28447d2
--- /dev/null
+++ b/internal/web/handlers/path_handlers.go
@@ -0,0 +1,76 @@
+package handlers
+
+import (
+ "net/http"
+ "os"
+ "path/filepath"
+
+ "github.com/gin-gonic/gin"
+)
+
+// HandleCheckPath validates if a given path exists and is accessible
+func (h *Handlers) HandleCheckPath(c *gin.Context) {
+ path := c.Query("path")
+ if path == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "valid": false,
+ "error": "No path provided",
+ })
+ return
+ }
+
+ // Clean and resolve the path
+ path = filepath.Clean(path)
+ absPath, err := filepath.Abs(path)
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "valid": false,
+ "error": "Invalid path format",
+ })
+ return
+ }
+
+ // Check if path exists
+ info, err := os.Stat(absPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ c.JSON(http.StatusOK, gin.H{
+ "valid": false,
+ "error": "Path does not exist",
+ })
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "valid": false,
+ "error": "Error accessing path: " + err.Error(),
+ })
+ return
+ }
+
+ // Check if it's a directory
+ if !info.IsDir() {
+ c.JSON(http.StatusOK, gin.H{
+ "valid": false,
+ "error": "Path exists but is not a directory",
+ })
+ return
+ }
+
+ // Check if we have read access
+ testFile := filepath.Join(absPath, ".gomft_test")
+ f, err := os.OpenFile(testFile, os.O_CREATE|os.O_WRONLY, 0666)
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "valid": false,
+ "error": "Directory exists but is not writable",
+ })
+ return
+ }
+ f.Close()
+ os.Remove(testFile)
+
+ c.JSON(http.StatusOK, gin.H{
+ "valid": true,
+ "error": "",
+ })
+}
diff --git a/internal/web/handlers/routes.go b/internal/web/handlers/routes.go
index d22dfce..e57ca11 100644
--- a/internal/web/handlers/routes.go
+++ b/internal/web/handlers/routes.go
@@ -32,6 +32,9 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
authorized.POST("/configs/:id", h.HandleUpdateConfig)
authorized.DELETE("/configs/:id", h.HandleDeleteConfig)
+ // Path validation endpoint
+ authorized.GET("/check-path", h.HandleCheckPath)
+
// Google Drive authentication routes
authorized.GET("/configs/:id/gdrive-auth", h.HandleGDriveAuth)
authorized.GET("/configs/gdrive-callback", h.HandleGDriveAuthCallback)
diff --git a/static/img/logo.png b/static/img/logo.png
new file mode 100644
index 0000000..083c0ea
Binary files /dev/null and b/static/img/logo.png differ
diff --git a/static/img/logo.svg b/static/img/logo.svg
new file mode 100644
index 0000000..78af8aa
--- /dev/null
+++ b/static/img/logo.svg
@@ -0,0 +1,450 @@
+
+
+
+
+
+