mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
- Added logic to drop any existing temporary tables from previous failed migration runs before renaming the current table. - Enhanced error logging to provide warnings if the drop operation fails, ensuring smoother migration execution. https://github.com/StarFleetCPTN/GoMFT/issues/72
288 lines
9.8 KiB
Go
288 lines
9.8 KiB
Go
package migrations
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/go-gormigrate/gormigrate/v2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// AlterBooleanDefaults changes boolean columns with default:true to pointers
|
|
// using explicit table recreation with raw SQL for SQLite compatibility.
|
|
func AlterBooleanDefaults() *gormigrate.Migration {
|
|
|
|
// --- Raw SQL CREATE TABLE statements for the target schema ---
|
|
|
|
const createNotificationServicesSQL = `
|
|
CREATE TABLE notification_services (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
is_enabled INTEGER DEFAULT 1, -- Target: *bool, SQLite uses 0/1, default true
|
|
config TEXT,
|
|
description TEXT,
|
|
event_triggers TEXT DEFAULT '[]',
|
|
payload_template TEXT,
|
|
secret_key TEXT,
|
|
retry_policy TEXT DEFAULT 'simple',
|
|
last_used timestamp,
|
|
success_count INTEGER DEFAULT 0,
|
|
failure_count INTEGER DEFAULT 0,
|
|
created_by INTEGER,
|
|
created_at timestamp,
|
|
updated_at timestamp
|
|
);`
|
|
|
|
const createAuthProvidersSQL = `
|
|
CREATE TABLE auth_providers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
enabled INTEGER DEFAULT 1, -- Target: *bool, SQLite uses 0/1, default true
|
|
description TEXT,
|
|
provider_url TEXT,
|
|
client_id TEXT,
|
|
client_secret TEXT,
|
|
redirect_url TEXT,
|
|
scopes TEXT,
|
|
attribute_mapping TEXT,
|
|
config TEXT,
|
|
icon_url TEXT,
|
|
successful_logins INTEGER DEFAULT 0,
|
|
last_used timestamp,
|
|
created_at timestamp,
|
|
updated_at timestamp
|
|
);`
|
|
|
|
const createTransferConfigsSQL = `
|
|
CREATE TABLE transfer_configs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
source_type TEXT NOT NULL,
|
|
source_path TEXT NOT NULL,
|
|
source_host TEXT,
|
|
source_port INTEGER DEFAULT 22,
|
|
source_user TEXT,
|
|
source_key_file TEXT,
|
|
source_bucket TEXT,
|
|
source_region TEXT,
|
|
source_access_key TEXT,
|
|
source_endpoint TEXT,
|
|
source_share TEXT,
|
|
source_domain TEXT,
|
|
source_passive_mode INTEGER DEFAULT 1, -- Already *bool, keep default
|
|
source_client_id TEXT,
|
|
source_drive_id TEXT,
|
|
source_team_drive TEXT,
|
|
source_read_only INTEGER,
|
|
source_start_year INTEGER,
|
|
source_include_archived INTEGER,
|
|
file_pattern TEXT DEFAULT '*',
|
|
output_pattern TEXT,
|
|
destination_type TEXT NOT NULL,
|
|
destination_path TEXT NOT NULL,
|
|
dest_host TEXT,
|
|
dest_port INTEGER DEFAULT 22,
|
|
dest_user TEXT,
|
|
dest_key_file TEXT,
|
|
dest_bucket TEXT,
|
|
dest_region TEXT,
|
|
dest_access_key TEXT,
|
|
dest_endpoint TEXT,
|
|
dest_share TEXT,
|
|
dest_domain TEXT,
|
|
dest_passive_mode INTEGER DEFAULT 1, -- Already *bool, keep default
|
|
dest_client_id TEXT,
|
|
dest_drive_id TEXT,
|
|
dest_team_drive TEXT,
|
|
dest_read_only INTEGER,
|
|
dest_start_year INTEGER,
|
|
dest_include_archived INTEGER,
|
|
use_builtin_auth_source INTEGER,
|
|
use_builtin_auth_dest INTEGER,
|
|
google_drive_authenticated INTEGER,
|
|
archive_path TEXT,
|
|
archive_enabled INTEGER DEFAULT 0,
|
|
rclone_flags TEXT,
|
|
command_id INTEGER DEFAULT 1,
|
|
command_flags TEXT,
|
|
command_flag_values TEXT,
|
|
delete_after_transfer INTEGER DEFAULT 0,
|
|
skip_processed_files INTEGER DEFAULT 1, -- Target: *bool, SQLite uses 0/1, default true
|
|
max_concurrent_transfers INTEGER DEFAULT 4,
|
|
created_by INTEGER,
|
|
created_at timestamp,
|
|
updated_at timestamp
|
|
);`
|
|
|
|
// --- End Raw SQL ---
|
|
|
|
return &gormigrate.Migration{
|
|
ID: "012_alter_boolean_defaults",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
// --- Backup Logic (copied) ---
|
|
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 count > 0 {
|
|
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)
|
|
}
|
|
backupDir := os.Getenv("BACKUP_DIR")
|
|
if backupDir == "" {
|
|
backupDir = "/app/backups"
|
|
if _, err := os.Stat(backupDir); os.IsNotExist(err) {
|
|
backupDir = "backups"
|
|
}
|
|
}
|
|
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create backup directory: %v", err)
|
|
}
|
|
dbFileName := filepath.Base(dbPath)
|
|
backupFileName := fmt.Sprintf("%s.backup.%s", dbFileName, time.Now().Format("20060102_150405"))
|
|
backupFile := filepath.Join(backupDir, backupFileName)
|
|
data, err := os.ReadFile(dbPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read database for backup: %v", err)
|
|
}
|
|
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)
|
|
}
|
|
// --- End Backup Logic ---
|
|
|
|
// --- Table Recreation Logic for SQLite ---
|
|
if err := tx.Exec("PRAGMA foreign_keys = OFF").Error; err != nil {
|
|
return fmt.Errorf("failed to disable foreign keys: %w", err)
|
|
}
|
|
defer func() {
|
|
if err := tx.Exec("PRAGMA foreign_keys = ON").Error; err != nil {
|
|
fmt.Printf("Warning: failed to re-enable foreign keys: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
// Helper function for table recreation
|
|
recreateTable := func(tableName, createSQL string) error {
|
|
fmt.Printf("Recreating table %s...\n", tableName)
|
|
oldTableName := fmt.Sprintf("_%s_old", tableName)
|
|
|
|
// Drop the old temp table if it exists from a previous failed run
|
|
if err := tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", oldTableName)).Error; err != nil {
|
|
// Log the error but proceed, as the rename might still work or fail for the intended reason
|
|
fmt.Printf("Warning: failed to drop potential leftover table %s: %v\n", oldTableName, err)
|
|
}
|
|
|
|
// Rename old table
|
|
if err := tx.Exec(fmt.Sprintf("ALTER TABLE %s RENAME TO %s", tableName, oldTableName)).Error; err == nil {
|
|
fmt.Printf("Renamed %s to %s.\n", tableName, oldTableName)
|
|
|
|
// Create new table using raw SQL
|
|
fmt.Printf("Creating new %s table...\n", tableName)
|
|
if err := tx.Exec(createSQL).Error; err != nil {
|
|
return fmt.Errorf("failed to create new %s table: %w", tableName, err)
|
|
}
|
|
fmt.Printf("New %s table created.\n", tableName)
|
|
|
|
// Copy data
|
|
fmt.Printf("Copying data to new %s table...\n", tableName)
|
|
// IMPORTANT: Ensure column order/names match if schema changed beyond types/defaults
|
|
if err := tx.Exec(fmt.Sprintf("INSERT INTO %s SELECT * FROM %s", tableName, oldTableName)).Error; err != nil {
|
|
return fmt.Errorf("failed to copy data to new %s table: %w", tableName, err)
|
|
}
|
|
fmt.Printf("Data copied to %s.\n", tableName)
|
|
|
|
// Drop old table
|
|
if err := tx.Exec(fmt.Sprintf("DROP TABLE %s", oldTableName)).Error; err != nil {
|
|
return fmt.Errorf("failed to drop old %s table: %w", tableName, err)
|
|
}
|
|
fmt.Printf("Successfully recreated %s.\n", tableName)
|
|
} else {
|
|
// Check if rename failed because table doesn't exist (fresh install)
|
|
var tableExists int
|
|
tx.Raw(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='%s'", tableName)).Scan(&tableExists)
|
|
if tableExists == 0 {
|
|
fmt.Printf("%s table does not exist, creating.\n", tableName)
|
|
if err := tx.Exec(createSQL).Error; err != nil { // Create table directly
|
|
return fmt.Errorf("failed to create new %s table: %w", tableName, err)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("failed to rename %s: %w", tableName, err) // Real rename error
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Recreate tables
|
|
if err := recreateTable("notification_services", createNotificationServicesSQL); err != nil {
|
|
return err
|
|
}
|
|
if err := recreateTable("auth_providers", createAuthProvidersSQL); err != nil {
|
|
return err
|
|
}
|
|
if err := recreateTable("transfer_configs", createTransferConfigsSQL); err != nil {
|
|
return err
|
|
}
|
|
|
|
// --- End Table Recreation Logic ---
|
|
|
|
// Create audit log entry
|
|
now := time.Now()
|
|
details, auditErr := json.Marshal(map[string]interface{}{
|
|
"tables_affected": []string{"notification_services", "auth_providers", "transfer_configs"},
|
|
"columns_altered": []string{"is_enabled", "enabled", "skip_processed_files"},
|
|
"new_type": "*bool (pointer to boolean)",
|
|
"method": "Table recreation (SQLite - Raw SQL)",
|
|
"message": "Changed boolean columns with default:true to pointers to handle false values correctly with GORM.",
|
|
})
|
|
if auditErr != nil {
|
|
fmt.Printf("Warning: Failed to marshal audit log details: %v\n", auditErr)
|
|
}
|
|
|
|
if auditErr == nil {
|
|
if auditExecErr := tx.Exec(`
|
|
INSERT INTO audit_logs (action, entity_type, entity_id, user_id, details, created_at, updated_at, timestamp)
|
|
VALUES ('schema_update', 'multiple_tables', 0, 1, ?, ?, ?, ?)
|
|
`, string(details), now, now, now).Error; auditExecErr != nil {
|
|
fmt.Printf("Warning: Failed to insert audit log: %v\n", auditExecErr)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
// Rollback is complex and risky with table recreation. Log skip.
|
|
now := time.Now()
|
|
details, err := json.Marshal(map[string]interface{}{
|
|
"migration_id": "012_alter_boolean_defaults",
|
|
"message": "Skipping rollback of boolean column type changes (via table recreation) due to complexity/potential data loss.",
|
|
})
|
|
if err != nil {
|
|
fmt.Printf("Warning: Failed to marshal rollback audit log details: %v\n", err)
|
|
}
|
|
|
|
if err == nil {
|
|
if auditExecErr := tx.Exec(`
|
|
INSERT INTO audit_logs (action, entity_type, entity_id, user_id, details, created_at, updated_at, timestamp)
|
|
VALUES ('migration_rollback', 'multiple_tables', 0, 1, ?, ?, ?, ?)
|
|
`, string(details), now, now, now).Error; auditExecErr != nil {
|
|
fmt.Printf("Warning: Failed to insert rollback audit log: %v\n", auditExecErr)
|
|
}
|
|
}
|
|
fmt.Println("Rollback for migration 012_alter_boolean_defaults skipped for safety.")
|
|
return nil
|
|
},
|
|
}
|
|
}
|