Backend cleanup: remove schema file bootstrap and finalize SQLCipher-only DB init

This commit is contained in:
Jacob Schmidt 2026-02-28 17:49:18 -06:00
parent f6ff9d2acb
commit aafb08e63f
8 changed files with 37 additions and 71 deletions

View File

@ -15,7 +15,7 @@ internal sealed record EntryTemplateLoadPayload(string FilePath);
internal sealed record EntryTemplateDeletePayload(string FilePath);
public sealed record EntryTemplateLoadResult(string FileName, string FilePath, string Content);
public sealed record EntryTemplateSavePayload(string Name, string Content, string? FilePath = null);
internal sealed record DatabasePayload(string Password, string? DataDirectory = null);
internal sealed record DatabasePayload(string Password);
internal sealed record AiSummarizeEntryPayload(string Content, string? FileStem = null);
internal sealed record AiSummarizeAllPayload(List<string>? Entries);
internal sealed record AiChatPayload(string Prompt);

View File

@ -6,13 +6,11 @@ public sealed record JournalDatabaseStatus(
int Iterations,
string KeyDerivation,
IReadOnlyList<string> SchemaTables,
string SchemaBootstrapPath,
bool RuntimeReady,
string RuntimeMessage);
public sealed record JournalDatabaseHydrationResult(
string DatabasePath,
string SchemaBootstrapPath,
int EntryFilesProcessed,
bool RuntimeReady,
string Message);

View File

@ -342,16 +342,6 @@ public class Entry(
_databaseSession.SetPassword(loadPayload.Password);
result = loaded;
break;
case "vault.save_current_month":
var saveCurrentPayload = DeserializePayload<VaultPayload>(cmd.Payload);
if (saveCurrentPayload is null)
return Error("Missing or invalid payload");
_vaultStorage.RebuildAllVaults(
saveCurrentPayload.Password,
saveCurrentPayload.VaultDirectory,
ResolveVaultStorageDirectory());
result = true;
break;
case "vault.rebuild_all":
var rebuildPayload = DeserializePayload<VaultPayload>(cmd.Payload);
if (rebuildPayload is null)
@ -373,20 +363,25 @@ public class Entry(
var dbStatusPayload = DeserializePayload<DatabasePayload>(cmd.Payload);
if (dbStatusPayload is null || string.IsNullOrWhiteSpace(dbStatusPayload.Password))
return Error("Missing or invalid payload");
result = _database.GetStatus(dbStatusPayload.Password, dbStatusPayload.DataDirectory);
result = _database.GetStatus(dbStatusPayload.Password);
break;
case "db.initialize_schema":
var dbInitPayload = DeserializePayload<DatabasePayload>(cmd.Payload);
if (dbInitPayload is null)
if (dbInitPayload is null || string.IsNullOrWhiteSpace(dbInitPayload.Password))
return Error("Missing or invalid payload");
var schemaPath = _database.WriteSchemaBootstrap(dbInitPayload.DataDirectory);
result = new { schemaPath };
var initResult = _database.HydrateWorkspace(dbInitPayload.Password);
result = new
{
initialized = initResult.RuntimeReady,
databasePath = initResult.DatabasePath,
initResult.Message
};
break;
case "db.hydrate_workspace":
var dbHydratePayload = DeserializePayload<DatabasePayload>(cmd.Payload);
if (dbHydratePayload is null || string.IsNullOrWhiteSpace(dbHydratePayload.Password))
return Error("Missing or invalid payload");
result = _database.HydrateWorkspace(dbHydratePayload.Password, dbHydratePayload.DataDirectory);
result = _database.HydrateWorkspace(dbHydratePayload.Password);
_databaseSession.SetPassword(dbHydratePayload.Password);
break;
default:
@ -450,7 +445,7 @@ public class Entry(
if (!VaultSyncActions.Contains(action))
return;
if (!_databaseSession.TryGetSession(out var password, out _))
if (!_databaseSession.TryGetSession(out var password))
return;
try

View File

@ -7,7 +7,6 @@ public sealed class DatabaseSessionService(IJournalDatabaseService database) : I
private readonly IJournalDatabaseService _database = database;
private readonly Lock _lock = new();
private string? _password;
private string? _dataDirectory;
private SqliteConnection? _connection;
public bool IsUnlocked
@ -18,38 +17,34 @@ public sealed class DatabaseSessionService(IJournalDatabaseService database) : I
}
}
public void SetPassword(string password, string? dataDirectory = null)
public void SetPassword(string password)
{
if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("Password cannot be empty.", nameof(password));
lock (_lock)
{
if (_connection is not null &&
(_password != password || _dataDirectory != dataDirectory))
if (_connection is not null && _password != password)
{
_connection.Dispose();
_connection = null;
}
_password = password;
_dataDirectory = dataDirectory;
}
}
public bool TryGetSession(out string password, out string? dataDirectory)
public bool TryGetSession(out string password)
{
lock (_lock)
{
if (string.IsNullOrWhiteSpace(_password))
{
password = "";
dataDirectory = null;
return false;
}
password = _password;
dataDirectory = _dataDirectory;
return true;
}
}
@ -65,7 +60,7 @@ public sealed class DatabaseSessionService(IJournalDatabaseService database) : I
if (_connection is not null)
return _connection;
_connection = _database.OpenEncryptedConnection(_password, _dataDirectory);
_connection = _database.OpenEncryptedConnection(_password);
_database.EnsureSchema(_connection);
return _connection;
}

View File

@ -5,8 +5,8 @@ namespace Journal.Core.Services.Database;
public interface IDatabaseSessionService
{
bool IsUnlocked { get; }
void SetPassword(string password, string? dataDirectory = null);
bool TryGetSession(out string password, out string? dataDirectory);
void SetPassword(string password);
bool TryGetSession(out string password);
SqliteConnection GetConnection();
void CloseConnection();
}

View File

@ -5,13 +5,12 @@ namespace Journal.Core.Services.Database;
public interface IJournalDatabaseService
{
string GetDatabasePath(string? dataDirectory = null);
string GetDatabasePath();
byte[] DeriveDatabaseKey(string password);
string BuildPragmaKeyStatement(string password);
IReadOnlyDictionary<string, string> GetSchemaStatements();
SqliteConnection OpenEncryptedConnection(string password, string? dataDirectory = null);
SqliteConnection OpenEncryptedConnection(string password);
void EnsureSchema(SqliteConnection connection);
string WriteSchemaBootstrap(string? dataDirectory = null);
JournalDatabaseStatus GetStatus(string password, string? dataDirectory = null);
JournalDatabaseHydrationResult HydrateWorkspace(string password, string? dataDirectory = null);
JournalDatabaseStatus GetStatus(string password);
JournalDatabaseHydrationResult HydrateWorkspace(string password);
}

View File

@ -19,7 +19,7 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
private readonly IJournalConfigService _config = config;
public string GetDatabasePath(string? dataDirectory = null)
public string GetDatabasePath()
{
var directory = ResolveDatabaseDirectory();
@ -133,51 +133,30 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
};
}
public string WriteSchemaBootstrap(string? dataDirectory = null)
{
var directory = ResolveDatabaseDirectory();
Directory.CreateDirectory(directory);
var bootstrapPath = Path.GetFullPath(Path.Combine(directory, "journal_schema.sql"));
var statements = GetSchemaStatements()
.Select(pair => $"-- {pair.Key}\n{pair.Value.Trim()}")
.ToArray();
var content = string.Join("\n\n", statements) + "\n";
File.WriteAllText(bootstrapPath, content);
return bootstrapPath;
}
public JournalDatabaseStatus GetStatus(string password, string? dataDirectory = null)
public JournalDatabaseStatus GetStatus(string password)
{
var tables = GetSchemaStatements().Keys.OrderBy(x => x, StringComparer.Ordinal).ToArray();
var bootstrapPath = WriteSchemaBootstrap(dataDirectory);
var runtime = ProbeRuntime(password, dataDirectory);
var runtime = ProbeRuntime(password);
return new JournalDatabaseStatus(
DatabasePath: GetDatabasePath(dataDirectory),
DatabasePath: GetDatabasePath(),
KeyLengthBytes: DeriveDatabaseKey(password).Length,
Iterations: Iterations,
KeyDerivation: "PBKDF2-HMAC-SHA256",
SchemaTables: tables,
SchemaBootstrapPath: bootstrapPath,
RuntimeReady: runtime.Ready,
RuntimeMessage: runtime.Message);
}
public JournalDatabaseHydrationResult HydrateWorkspace(string password, string? dataDirectory = null)
public JournalDatabaseHydrationResult HydrateWorkspace(string password)
{
var directory = ResolveDatabaseDirectory();
Directory.CreateDirectory(directory);
using var connection = OpenEncryptedConnection(password, directory);
using var connection = OpenEncryptedConnection(password);
EnsureSchema(connection);
var runtimeReady = HasRequiredTables(connection);
var entryDocumentsProcessed = CountEntryDocuments(connection);
var schemaPath = WriteSchemaBootstrap(directory);
return new JournalDatabaseHydrationResult(
DatabasePath: GetDatabasePath(directory),
SchemaBootstrapPath: schemaPath,
DatabasePath: GetDatabasePath(),
EntryFilesProcessed: entryDocumentsProcessed,
RuntimeReady: runtimeReady,
Message: runtimeReady
@ -200,14 +179,14 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
}
}
public SqliteConnection OpenEncryptedConnection(string password, string? dataDirectory = null)
public SqliteConnection OpenEncryptedConnection(string password)
{
if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("Password cannot be empty.", nameof(password));
EnsureSqliteInitialized();
var connection = new SqliteConnection($"Data Source={GetDatabasePath(dataDirectory)};Mode=ReadWriteCreate;Pooling=False");
var connection = new SqliteConnection($"Data Source={GetDatabasePath()};Mode=ReadWriteCreate;Pooling=False");
connection.Open();
using var keyCmd = connection.CreateCommand();
@ -246,11 +225,11 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
return RequiredSchemaTables.All(existing.Contains);
}
private (bool Ready, string Message) ProbeRuntime(string password, string? dataDirectory)
private (bool Ready, string Message) ProbeRuntime(string password)
{
try
{
using var connection = OpenEncryptedConnection(password, dataDirectory);
using var connection = OpenEncryptedConnection(password);
EnsureSchema(connection);
var ready = HasRequiredTables(connection);
return ready
@ -282,4 +261,5 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
return Path.GetFullPath(Path.Combine(_config.Current.VaultDirectory, "db"));
}
}

View File

@ -381,12 +381,11 @@ Error:
| `search.entries` | Search entries with filters | optional query/section/date/tags/types/checked/unchecked |
| `vault.initialize` | Ensure vault directory exists | `payload.password`, `payload.vaultDirectory` |
| `vault.load_all` | Restore encrypted SQLCipher DB snapshot from vault | `payload.password`, `payload.vaultDirectory` |
| `vault.save_current_month` | Alias for vault rebuild (DB snapshot persist) | `payload.password`, `payload.vaultDirectory` |
| `vault.rebuild_all` | Persist encrypted SQLCipher DB snapshot to vault | `payload.password`, `payload.vaultDirectory` |
| `vault.clear_data_directory` | No-op for SQLCipher-first mode (compat command) | — |
| `db.status` | DB key/schema compatibility snapshot | `payload.password`, optional `payload.dataDirectory` |
| `db.initialize_schema` | Write SQL schema bootstrap file | optional `payload.dataDirectory` |
| `db.hydrate_workspace` | Bootstrap DB + set session password | `payload.password`, optional `payload.dataDirectory` |
| `db.status` | DB key/schema compatibility snapshot | `payload.password` |
| `db.initialize_schema` | Initialize SQLCipher schema in the database file | `payload.password` |
| `db.hydrate_workspace` | Bootstrap DB + set session password | `payload.password` |
| `config.get` | Return current config snapshot | — |
| `ai.health` | AI provider health status | — |
| `ai.summarize_entry` | Summarize one entry | `payload.content`, optional `payload.fileStem` |