mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-06 22:36:27 -04:00
Add client DB migration screen (#74)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user