diff --git a/install.sh b/install.sh index 801600db9..ebb6d0486 100755 --- a/install.sh +++ b/install.sh @@ -1659,7 +1659,7 @@ handle_migrate_db() { SQLITE_DB_NAME=$(basename "$SQLITE_DB_ABS") # Get PostgreSQL password from .env file - POSTGRES_PASSWORD=$(grep "^POSTGRES_PASSWORD=" "$ENV_FILE" | cut -d '=' -f2) + POSTGRES_PASSWORD=$(grep "^POSTGRES_PASSWORD=" "$ENV_FILE" | cut -d= -f2-) if [ -z "$POSTGRES_PASSWORD" ]; then printf "${RED}Error: POSTGRES_PASSWORD not found in .env file${NC}\n" exit 1 @@ -1670,9 +1670,17 @@ handle_migrate_db() { NETWORK_NAME=$(echo "$NETWORK_NAME" | tr '[:upper:]' '[:lower:]') printf "\n${YELLOW}Warning: This will migrate data from your SQLite database to PostgreSQL.${NC}\n" + printf "\n" + printf "This is a one-time operation necessary when upgrading from <= 0.9.x to 0.10.0+ and only needs to be run once.\n" + printf "\n" printf "Source database: ${CYAN}${SQLITE_DB_ABS}${NC}\n" printf "Target: PostgreSQL database (using connection string from docker-compose.yml)\n" printf "Make sure you have backed up your data before proceeding.\n" + + printf "\n${RED}WARNING: This operation will DELETE ALL EXISTING DATA in the PostgreSQL database.${NC}\n" + printf "${RED}Only proceed if you understand that any current PostgreSQL data will be permanently lost.${NC}\n" + printf "\n" + read -p "Continue with migration? [y/N]: " confirm if [[ ! $confirm =~ ^[Yy]$ ]]; then printf "${YELLOW}Migration cancelled.${NC}\n" @@ -1705,14 +1713,14 @@ handle_migrate_db() { docker run --rm \ --network="${NETWORK_NAME}" \ -v "${SQLITE_DB_DIR}:/sqlite" \ - ${GITHUB_CONTAINER_REGISTRY}-installcli migrate-sqlite "/sqlite/${SQLITE_DB_NAME}" "Host=postgres;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD}" + installcli migrate-sqlite "/sqlite/${SQLITE_DB_NAME}" "Host=postgres;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD}" else # Run migration with volume mount using pre-built image docker run --rm \ --network="${NETWORK_NAME}" \ -v "${SQLITE_DB_DIR}:/sqlite" \ - installcli migrate-sqlite "/sqlite/${SQLITE_DB_NAME}" "Host=postgres;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD}" - fi + ${GITHUB_CONTAINER_REGISTRY}-installcli:0.10.0 migrate-sqlite "/sqlite/${SQLITE_DB_NAME}" "Host=postgres;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD}" + fi printf "${GREEN}> Check migration output above for details.${NC}\n" } diff --git a/src/Utilities/AliasVault.InstallCli/Program.cs b/src/Utilities/AliasVault.InstallCli/Program.cs index af56080da..5d30a2703 100644 --- a/src/Utilities/AliasVault.InstallCli/Program.cs +++ b/src/Utilities/AliasVault.InstallCli/Program.cs @@ -104,21 +104,40 @@ public partial class Program var optionsBuilderSqlite = new DbContextOptionsBuilder() .UseSqlite(sqliteConnString); + var optionsBuilderPg = new DbContextOptionsBuilder() + .UseNpgsql(pgConnString); + // Make sure sqlite is on latest version migration Console.WriteLine("Update sqlite database to latest version..."); await using var sqliteContext = new AliasServerDbContextSqlite(optionsBuilderSqlite.Options); await sqliteContext.Database.MigrateAsync(); Console.WriteLine("Updating finished."); - var optionsBuilderPg = new DbContextOptionsBuilder() - .UseNpgsql(pgConnString); - // Make sure postgres is on latest version migration Console.WriteLine("Update postgres database to latest version..."); await using var pgContext = new AliasServerDbContextPostgresql(optionsBuilderPg.Options); await pgContext.Database.MigrateAsync(); Console.WriteLine("Updating finished."); + Console.WriteLine("Truncating existing tables in reverse dependency order..."); + + // Truncate tables in reverse order of dependencies + await TruncateTable(pgContext.EmailAttachments, "EmailAttachments"); + await TruncateTable(pgContext.Emails, "Emails"); + await TruncateTable(pgContext.UserTokens, "UserTokens"); + await TruncateTable(pgContext.UserRoles, "UserRoles"); + await TruncateTable(pgContext.UserLogin, "UserLogins"); + await TruncateTable(pgContext.UserEmailClaims, "UserEmailClaims"); + await TruncateTable(pgContext.Vaults, "Vaults"); + await TruncateTable(pgContext.UserEncryptionKeys, "UserEncryptionKeys"); + await TruncateTable(pgContext.AliasVaultUserRefreshTokens, "AliasVaultUserRefreshTokens"); + await TruncateTable(pgContext.AuthLogs, "AuthLogs"); + await TruncateTable(pgContext.DataProtectionKeys, "DataProtectionKeys"); + await TruncateTable(pgContext.ServerSettings, "ServerSettings"); + await TruncateTable(pgContext.AliasVaultUsers, "AliasVaultUsers"); + await TruncateTable(pgContext.AliasVaultRoles, "AliasVaultRoles"); + await TruncateTable(pgContext.AdminUsers, "AdminUsers"); + Console.WriteLine("Starting content migration..."); // First, migrate tables without foreign key dependencies @@ -155,6 +174,25 @@ public partial class Program } } + /// + /// Truncates a table in the PostgreSQL database. + /// + /// The entity type of the table being truncated. + /// The database table to truncate. + /// The name of the table being truncated (for logging purposes). + /// A task representing the asynchronous truncation operation. + private static async Task TruncateTable(DbSet table, string tableName) + where T : class + { + Console.WriteLine($"Truncating table {tableName}..."); + var count = await table.CountAsync(); + if (count > 0) + { + await table.ExecuteDeleteAsync(); + Console.WriteLine($"Removed {count} records from {tableName}"); + } + } + /// /// Migrates data from one database table to another, handling the transfer in batches. /// @@ -184,6 +222,15 @@ public partial class Program if (items.Count > 0) { + // Remove any existing entries in the destination table + var existingEntries = await destination.ToListAsync(); + if (existingEntries.Any()) + { + Console.WriteLine($"Removing {existingEntries.Count} existing entries from {tableName}..."); + destination.RemoveRange(existingEntries); + await destinationContext.SaveChangesAsync(); + } + const int batchSize = 30; foreach (var batch in items.Chunk(batchSize)) { @@ -220,9 +267,9 @@ public partial class Program } // Ensure that the amount of records in the source and destination tables match - if (await source.CountAsync() != await destination.CountAsync()) + if (await source.CountAsync() > await destination.CountAsync()) { - throw new ArgumentException($"The amount of records in the source and destination tables do not match. Check if the migration is working correctly."); + throw new ArgumentException($"The amount of records in the source is greater than the destination. Check if the migration is working correctly."); } } }