Add client DB migration screen (#74)

This commit is contained in:
Leendert de Borst
2024-07-08 12:55:12 +02:00
parent af1e813c48
commit db62eeec22
4 changed files with 216 additions and 8 deletions

View File

@@ -21,6 +21,10 @@
{
<ErrorVaultDecrypt />
}
else if(IsPendingMigrations)
{
<PendingMigrations />
}
else
{
<VaultDecryptionProgress />
@@ -39,6 +43,7 @@
@code {
private bool IsDbInitialized { get; set; } = false;
private bool IsDbDecryptionError { get; set; } = false;
private bool IsPendingMigrations { get; set; } = false;
private const int MinimumLoadingTimeMs = 800;
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
@@ -50,6 +55,7 @@
// Reset local state
IsDbInitialized = false;
IsDbDecryptionError = false;
IsPendingMigrations = false;
DbService.GetState().StateChanged += OnDatabaseStateChanged;
AuthStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
@@ -96,6 +102,11 @@
IsDbDecryptionError = true;
StateHasChanged();
}
else if (currentState.Status == DbServiceState.DatabaseStatus.PendingMigrations)
{
IsPendingMigrations = true;
StateHasChanged();
}
}
private async void OnDatabaseStateChanged(object? sender, DbServiceState.DatabaseState newState)
@@ -112,6 +123,10 @@
{
IsDbDecryptionError = true;
}
else if (newState.Status == DbServiceState.DatabaseStatus.PendingMigrations)
{
IsPendingMigrations = true;
}
StateHasChanged();
}

View File

@@ -0,0 +1,65 @@
@inject DbService DbService
@inject GlobalNotificationService GlobalNotificationService
<div class="fixed inset-0 overflow-y-auto h-full w-full flex items-center justify-center">
<div class="relative p-8 bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md mx-auto">
<div class="text-center">
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">Vault needs to be upgraded.</h2>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">AliasVault has been updated which requires your vault to be upgraded in order to be compatible with the new datastructure.
This upgrade should only take a few seconds.</p>
<div class="mt-4">
@if (ErrorMessage.Length > 0)
{
<AlertMessageError Message="@ErrorMessage" />
}
@if (IsPendingMigrations)
{
<svg class="mx-auto animate-spin h-12 w-12 text-primary-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
}
else
{
<button @onclick="MigrateDatabase" type="button" class="px-4 py-2 text-white bg-primary-600 rounded-lg hover:bg-primary-700 focus:ring-4 focus:ring-primary-300 dark:bg-primary-500 dark:hover:bg-primary-600 dark:focus:ring-primary-800">
Start upgrade process
</button>
}
</div>
</div>
</div>
</div>
@code {
private bool IsPendingMigrations { get; set; } = false;
private string ErrorMessage { get; set; } = string.Empty;
private async Task MigrateDatabase()
{
// Show loading indicator
IsPendingMigrations = true;
ErrorMessage = String.Empty;
StateHasChanged();
// Simulate a delay.
await Task.Delay(1000);
// Migrate the database
if (await DbService.MigrateDatabaseAsync())
{
// Migration successful
GlobalNotificationService.AddSuccessMessage("Database upgrade successful.", true);
}
else
{
// Migration failed
ErrorMessage = "Database upgrade failed. Please try again or contact support.";
}
// Reset local state
IsPendingMigrations = false;
StateHasChanged();
}
}

View File

@@ -82,9 +82,19 @@ public class DbService : IDisposable
var loaded = await LoadDatabaseFromServerAsync();
if (loaded)
{
_isSuccessfullyInitialized = true;
_state.UpdateState(DbServiceState.DatabaseStatus.Ready);
Console.WriteLine("Database succesfully loaded from server.");
Console.WriteLine("Database successfully loaded from server.");
// Check if database is up to date with migrations.
var pendingMigrations = await _dbContext.Database.GetPendingMigrationsAsync();
if (pendingMigrations.Any())
{
_state.UpdateState(DbServiceState.DatabaseStatus.PendingMigrations);
}
else
{
_isSuccessfullyInitialized = true;
_state.UpdateState(DbServiceState.DatabaseStatus.Ready);
}
}
else
{
@@ -174,6 +184,27 @@ public class DbService : IDisposable
return base64String;
}
/// <summary>
/// Migrate the database structure to the latest version.
/// </summary>
/// <returns>Bool which indicates if migration was succesful.</returns>
public async Task<bool> MigrateDatabaseAsync()
{
try
{
await _dbContext.Database.MigrateAsync();
_isSuccessfullyInitialized = true;
_state.UpdateState(DbServiceState.DatabaseStatus.Ready);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return false;
}
return true;
}
/// <summary>
/// Clears the database connection and creates a new one so that the database is empty.
/// </summary>
@@ -233,25 +264,25 @@ public class DbService : IDisposable
var tempFileName = Path.GetRandomFileName();
await File.WriteAllBytesAsync(tempFileName, bytes);
using (var command = _sqlConnection.CreateCommand())
/*using (var command = _sqlConnection.CreateCommand())
{
// Empty all tables in the original database
command.CommandText = @"
SELECT 'DELETE FROM ' || name || ';'
FROM sqlite_master
WHERE type = 'table' AND name NOT LIKE 'sqlite_%';";
var dropTableCommands = new List<string>();
var emptyTableCommands = new List<string>();
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
dropTableCommands.Add(reader.GetString(0));
emptyTableCommands.Add(reader.GetString(0));
}
}
foreach (var dropTableCommand in dropTableCommands)
foreach (var emptyTableCommand in emptyTableCommands)
{
command.CommandText = dropTableCommand;
command.CommandText = emptyTableCommand;
await command.ExecuteNonQueryAsync();
}
@@ -282,6 +313,98 @@ public class DbService : IDisposable
command.CommandText = "DETACH DATABASE importDb";
await command.ExecuteNonQueryAsync();
}
*/
using (var command = _sqlConnection.CreateCommand())
{
Console.WriteLine("Dropping main tables..");
// Disable foreign key constraints
command.CommandText = "PRAGMA foreign_keys = OFF;";
await command.ExecuteNonQueryAsync();
// Drop all tables in the original database
command.CommandText = @"
SELECT 'DROP TABLE IF EXISTS ' || name || ';'
FROM sqlite_master
WHERE type = 'table' AND name NOT LIKE 'sqlite_%';";
var dropTableCommands = new List<string>();
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
dropTableCommands.Add(reader.GetString(0));
}
}
foreach (var dropTableCommand in dropTableCommands)
{
Console.WriteLine("Dropping table..");
Console.WriteLine("Drop command: " + dropTableCommand);
command.CommandText = dropTableCommand;
await command.ExecuteNonQueryAsync();
}
// Attach the imported database
command.CommandText = "ATTACH DATABASE @fileName AS importDb";
command.Parameters.Add(new SqliteParameter("@fileName", tempFileName));
await command.ExecuteNonQueryAsync();
Console.WriteLine("Make create table statements from import db..");
// Get CREATE TABLE statements from the imported database
command.CommandText = @"
SELECT sql
FROM importDb.sqlite_master
WHERE type = 'table' AND name NOT LIKE 'sqlite_%';";
var createTableCommands = new List<string>();
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
createTableCommands.Add(reader.GetString(0));
}
}
// Create tables in the main database
Console.WriteLine("Create tables in main db..");
foreach (var createTableCommand in createTableCommands)
{
command.CommandText = createTableCommand;
await command.ExecuteNonQueryAsync();
}
// Copy data from imported database to main database
Console.WriteLine("Copy from import to main db.");
command.CommandText = @"
SELECT 'INSERT INTO main.' || name || ' SELECT * FROM importDb.' || name || ';'
FROM importDb.sqlite_master
WHERE type = 'table' AND name NOT LIKE 'sqlite_%';";
var tableInsertCommands = new List<string>();
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
tableInsertCommands.Add(reader.GetString(0));
}
}
foreach (var tableInsertCommand in tableInsertCommands)
{
command.CommandText = tableInsertCommand;
await command.ExecuteNonQueryAsync();
}
// Detach the imported database
command.CommandText = "DETACH DATABASE importDb";
await command.ExecuteNonQueryAsync();
// Re-enable foreign key constraints
command.CommandText = "PRAGMA foreign_keys = ON;";
await command.ExecuteNonQueryAsync();
}
File.Delete(tempFileName);
}

View File

@@ -34,6 +34,11 @@ public class DbServiceState
/// </summary>
DecryptionFailed,
/// <summary>
/// Database has been decrypted but has pending migrations and needs to be updated.
/// </summary>
PendingMigrations,
/// <summary>
/// Database is ready but no task is currently in progress.
/// </summary>