- Move standalone fragment storage from unencrypted SQLite to the existing encrypted SQLCipher database (journal_cache.db) - Add IDatabaseSessionService/DatabaseSessionService for shared encrypted connection management after authentication - Update fragments table schema: nullable entry_id, add guid column - Reorganize flat Services/ directory (28 files) into 9 domain modules: Ai, Config, Database, Entries, Fragments, Logging, Sidecar, Speech, Vault - Update all namespace declarations and using statements across all projects - Update REFACTORING_SUMMARY.md with all changes Co-Authored-By: Warp <agent@warp.dev>
100 lines
3.0 KiB
C#
100 lines
3.0 KiB
C#
using Microsoft.Data.Sqlite;
|
|
|
|
namespace Journal.Core.Services.Database;
|
|
|
|
public sealed class DatabaseSessionService(IJournalDatabaseService database) : IDatabaseSessionService, IDisposable
|
|
{
|
|
private readonly IJournalDatabaseService _database = database;
|
|
private readonly Lock _lock = new();
|
|
private string? _password;
|
|
private string? _dataDirectory;
|
|
private SqliteConnection? _connection;
|
|
|
|
public bool IsUnlocked
|
|
{
|
|
get
|
|
{
|
|
lock (_lock) { return _password is not null; }
|
|
}
|
|
}
|
|
|
|
public void SetPassword(string password, string? dataDirectory = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(password))
|
|
throw new ArgumentException("Password cannot be empty.", nameof(password));
|
|
|
|
lock (_lock)
|
|
{
|
|
// If password or directory changed, close the old connection
|
|
if (_connection is not null &&
|
|
(_password != password || _dataDirectory != dataDirectory))
|
|
{
|
|
_connection.Dispose();
|
|
_connection = null;
|
|
}
|
|
|
|
_password = password;
|
|
_dataDirectory = dataDirectory;
|
|
}
|
|
}
|
|
|
|
public SqliteConnection GetConnection()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (_password is null)
|
|
throw new InvalidOperationException(
|
|
"Database is locked. Authenticate first (e.g. vault.load_all or db.hydrate_workspace).");
|
|
|
|
if (_connection is not null)
|
|
return _connection;
|
|
|
|
_connection = OpenEncryptedConnection(_password, _dataDirectory);
|
|
EnsureSchema(_connection, _database);
|
|
return _connection;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_connection?.Dispose();
|
|
_connection = null;
|
|
}
|
|
}
|
|
|
|
private SqliteConnection OpenEncryptedConnection(string password, string? dataDirectory)
|
|
{
|
|
var dbPath = _database.GetDatabasePath(dataDirectory);
|
|
var directory = Path.GetDirectoryName(dbPath);
|
|
if (!string.IsNullOrWhiteSpace(directory))
|
|
Directory.CreateDirectory(directory);
|
|
|
|
var connection = new SqliteConnection(
|
|
$"Data Source={dbPath};Mode=ReadWriteCreate;Pooling=False");
|
|
connection.Open();
|
|
|
|
using var keyCmd = connection.CreateCommand();
|
|
keyCmd.CommandText = _database.BuildPragmaKeyStatement(password) + ";";
|
|
keyCmd.ExecuteNonQuery();
|
|
|
|
// Verify the key is correct
|
|
using var verifyCmd = connection.CreateCommand();
|
|
verifyCmd.CommandText = "SELECT count(*) FROM sqlite_master;";
|
|
_ = verifyCmd.ExecuteScalar();
|
|
|
|
return connection;
|
|
}
|
|
|
|
private static void EnsureSchema(SqliteConnection connection, IJournalDatabaseService database)
|
|
{
|
|
foreach (var statement in database.GetSchemaStatements().Values)
|
|
{
|
|
using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = statement;
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
}
|