Backend cleanup: remove schema file bootstrap and finalize SQLCipher-only DB init
This commit is contained in:
parent
f6ff9d2acb
commit
aafb08e63f
@ -15,7 +15,7 @@ internal sealed record EntryTemplateLoadPayload(string FilePath);
|
|||||||
internal sealed record EntryTemplateDeletePayload(string FilePath);
|
internal sealed record EntryTemplateDeletePayload(string FilePath);
|
||||||
public sealed record EntryTemplateLoadResult(string FileName, string FilePath, string Content);
|
public sealed record EntryTemplateLoadResult(string FileName, string FilePath, string Content);
|
||||||
public sealed record EntryTemplateSavePayload(string Name, string Content, string? FilePath = null);
|
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 AiSummarizeEntryPayload(string Content, string? FileStem = null);
|
||||||
internal sealed record AiSummarizeAllPayload(List<string>? Entries);
|
internal sealed record AiSummarizeAllPayload(List<string>? Entries);
|
||||||
internal sealed record AiChatPayload(string Prompt);
|
internal sealed record AiChatPayload(string Prompt);
|
||||||
|
|||||||
@ -6,13 +6,11 @@ public sealed record JournalDatabaseStatus(
|
|||||||
int Iterations,
|
int Iterations,
|
||||||
string KeyDerivation,
|
string KeyDerivation,
|
||||||
IReadOnlyList<string> SchemaTables,
|
IReadOnlyList<string> SchemaTables,
|
||||||
string SchemaBootstrapPath,
|
|
||||||
bool RuntimeReady,
|
bool RuntimeReady,
|
||||||
string RuntimeMessage);
|
string RuntimeMessage);
|
||||||
|
|
||||||
public sealed record JournalDatabaseHydrationResult(
|
public sealed record JournalDatabaseHydrationResult(
|
||||||
string DatabasePath,
|
string DatabasePath,
|
||||||
string SchemaBootstrapPath,
|
|
||||||
int EntryFilesProcessed,
|
int EntryFilesProcessed,
|
||||||
bool RuntimeReady,
|
bool RuntimeReady,
|
||||||
string Message);
|
string Message);
|
||||||
|
|||||||
@ -342,16 +342,6 @@ public class Entry(
|
|||||||
_databaseSession.SetPassword(loadPayload.Password);
|
_databaseSession.SetPassword(loadPayload.Password);
|
||||||
result = loaded;
|
result = loaded;
|
||||||
break;
|
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":
|
case "vault.rebuild_all":
|
||||||
var rebuildPayload = DeserializePayload<VaultPayload>(cmd.Payload);
|
var rebuildPayload = DeserializePayload<VaultPayload>(cmd.Payload);
|
||||||
if (rebuildPayload is null)
|
if (rebuildPayload is null)
|
||||||
@ -373,20 +363,25 @@ public class Entry(
|
|||||||
var dbStatusPayload = DeserializePayload<DatabasePayload>(cmd.Payload);
|
var dbStatusPayload = DeserializePayload<DatabasePayload>(cmd.Payload);
|
||||||
if (dbStatusPayload is null || string.IsNullOrWhiteSpace(dbStatusPayload.Password))
|
if (dbStatusPayload is null || string.IsNullOrWhiteSpace(dbStatusPayload.Password))
|
||||||
return Error("Missing or invalid payload");
|
return Error("Missing or invalid payload");
|
||||||
result = _database.GetStatus(dbStatusPayload.Password, dbStatusPayload.DataDirectory);
|
result = _database.GetStatus(dbStatusPayload.Password);
|
||||||
break;
|
break;
|
||||||
case "db.initialize_schema":
|
case "db.initialize_schema":
|
||||||
var dbInitPayload = DeserializePayload<DatabasePayload>(cmd.Payload);
|
var dbInitPayload = DeserializePayload<DatabasePayload>(cmd.Payload);
|
||||||
if (dbInitPayload is null)
|
if (dbInitPayload is null || string.IsNullOrWhiteSpace(dbInitPayload.Password))
|
||||||
return Error("Missing or invalid payload");
|
return Error("Missing or invalid payload");
|
||||||
var schemaPath = _database.WriteSchemaBootstrap(dbInitPayload.DataDirectory);
|
var initResult = _database.HydrateWorkspace(dbInitPayload.Password);
|
||||||
result = new { schemaPath };
|
result = new
|
||||||
|
{
|
||||||
|
initialized = initResult.RuntimeReady,
|
||||||
|
databasePath = initResult.DatabasePath,
|
||||||
|
initResult.Message
|
||||||
|
};
|
||||||
break;
|
break;
|
||||||
case "db.hydrate_workspace":
|
case "db.hydrate_workspace":
|
||||||
var dbHydratePayload = DeserializePayload<DatabasePayload>(cmd.Payload);
|
var dbHydratePayload = DeserializePayload<DatabasePayload>(cmd.Payload);
|
||||||
if (dbHydratePayload is null || string.IsNullOrWhiteSpace(dbHydratePayload.Password))
|
if (dbHydratePayload is null || string.IsNullOrWhiteSpace(dbHydratePayload.Password))
|
||||||
return Error("Missing or invalid payload");
|
return Error("Missing or invalid payload");
|
||||||
result = _database.HydrateWorkspace(dbHydratePayload.Password, dbHydratePayload.DataDirectory);
|
result = _database.HydrateWorkspace(dbHydratePayload.Password);
|
||||||
_databaseSession.SetPassword(dbHydratePayload.Password);
|
_databaseSession.SetPassword(dbHydratePayload.Password);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -450,7 +445,7 @@ public class Entry(
|
|||||||
if (!VaultSyncActions.Contains(action))
|
if (!VaultSyncActions.Contains(action))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!_databaseSession.TryGetSession(out var password, out _))
|
if (!_databaseSession.TryGetSession(out var password))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|||||||
@ -7,7 +7,6 @@ public sealed class DatabaseSessionService(IJournalDatabaseService database) : I
|
|||||||
private readonly IJournalDatabaseService _database = database;
|
private readonly IJournalDatabaseService _database = database;
|
||||||
private readonly Lock _lock = new();
|
private readonly Lock _lock = new();
|
||||||
private string? _password;
|
private string? _password;
|
||||||
private string? _dataDirectory;
|
|
||||||
private SqliteConnection? _connection;
|
private SqliteConnection? _connection;
|
||||||
|
|
||||||
public bool IsUnlocked
|
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))
|
if (string.IsNullOrWhiteSpace(password))
|
||||||
throw new ArgumentException("Password cannot be empty.", nameof(password));
|
throw new ArgumentException("Password cannot be empty.", nameof(password));
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_connection is not null &&
|
if (_connection is not null && _password != password)
|
||||||
(_password != password || _dataDirectory != dataDirectory))
|
|
||||||
{
|
{
|
||||||
_connection.Dispose();
|
_connection.Dispose();
|
||||||
_connection = null;
|
_connection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_password = password;
|
_password = password;
|
||||||
_dataDirectory = dataDirectory;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetSession(out string password, out string? dataDirectory)
|
public bool TryGetSession(out string password)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(_password))
|
if (string.IsNullOrWhiteSpace(_password))
|
||||||
{
|
{
|
||||||
password = "";
|
password = "";
|
||||||
dataDirectory = null;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
password = _password;
|
password = _password;
|
||||||
dataDirectory = _dataDirectory;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,7 +60,7 @@ public sealed class DatabaseSessionService(IJournalDatabaseService database) : I
|
|||||||
if (_connection is not null)
|
if (_connection is not null)
|
||||||
return _connection;
|
return _connection;
|
||||||
|
|
||||||
_connection = _database.OpenEncryptedConnection(_password, _dataDirectory);
|
_connection = _database.OpenEncryptedConnection(_password);
|
||||||
_database.EnsureSchema(_connection);
|
_database.EnsureSchema(_connection);
|
||||||
return _connection;
|
return _connection;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,8 @@ namespace Journal.Core.Services.Database;
|
|||||||
public interface IDatabaseSessionService
|
public interface IDatabaseSessionService
|
||||||
{
|
{
|
||||||
bool IsUnlocked { get; }
|
bool IsUnlocked { get; }
|
||||||
void SetPassword(string password, string? dataDirectory = null);
|
void SetPassword(string password);
|
||||||
bool TryGetSession(out string password, out string? dataDirectory);
|
bool TryGetSession(out string password);
|
||||||
SqliteConnection GetConnection();
|
SqliteConnection GetConnection();
|
||||||
void CloseConnection();
|
void CloseConnection();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,12 @@ namespace Journal.Core.Services.Database;
|
|||||||
|
|
||||||
public interface IJournalDatabaseService
|
public interface IJournalDatabaseService
|
||||||
{
|
{
|
||||||
string GetDatabasePath(string? dataDirectory = null);
|
string GetDatabasePath();
|
||||||
byte[] DeriveDatabaseKey(string password);
|
byte[] DeriveDatabaseKey(string password);
|
||||||
string BuildPragmaKeyStatement(string password);
|
string BuildPragmaKeyStatement(string password);
|
||||||
IReadOnlyDictionary<string, string> GetSchemaStatements();
|
IReadOnlyDictionary<string, string> GetSchemaStatements();
|
||||||
SqliteConnection OpenEncryptedConnection(string password, string? dataDirectory = null);
|
SqliteConnection OpenEncryptedConnection(string password);
|
||||||
void EnsureSchema(SqliteConnection connection);
|
void EnsureSchema(SqliteConnection connection);
|
||||||
string WriteSchemaBootstrap(string? dataDirectory = null);
|
JournalDatabaseStatus GetStatus(string password);
|
||||||
JournalDatabaseStatus GetStatus(string password, string? dataDirectory = null);
|
JournalDatabaseHydrationResult HydrateWorkspace(string password);
|
||||||
JournalDatabaseHydrationResult HydrateWorkspace(string password, string? dataDirectory = null);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
|
|||||||
|
|
||||||
private readonly IJournalConfigService _config = config;
|
private readonly IJournalConfigService _config = config;
|
||||||
|
|
||||||
public string GetDatabasePath(string? dataDirectory = null)
|
public string GetDatabasePath()
|
||||||
{
|
{
|
||||||
var directory = ResolveDatabaseDirectory();
|
var directory = ResolveDatabaseDirectory();
|
||||||
|
|
||||||
@ -133,51 +133,30 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public string WriteSchemaBootstrap(string? dataDirectory = null)
|
public JournalDatabaseStatus GetStatus(string password)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
var tables = GetSchemaStatements().Keys.OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
var tables = GetSchemaStatements().Keys.OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||||
var bootstrapPath = WriteSchemaBootstrap(dataDirectory);
|
var runtime = ProbeRuntime(password);
|
||||||
var runtime = ProbeRuntime(password, dataDirectory);
|
|
||||||
return new JournalDatabaseStatus(
|
return new JournalDatabaseStatus(
|
||||||
DatabasePath: GetDatabasePath(dataDirectory),
|
DatabasePath: GetDatabasePath(),
|
||||||
KeyLengthBytes: DeriveDatabaseKey(password).Length,
|
KeyLengthBytes: DeriveDatabaseKey(password).Length,
|
||||||
Iterations: Iterations,
|
Iterations: Iterations,
|
||||||
KeyDerivation: "PBKDF2-HMAC-SHA256",
|
KeyDerivation: "PBKDF2-HMAC-SHA256",
|
||||||
SchemaTables: tables,
|
SchemaTables: tables,
|
||||||
SchemaBootstrapPath: bootstrapPath,
|
|
||||||
RuntimeReady: runtime.Ready,
|
RuntimeReady: runtime.Ready,
|
||||||
RuntimeMessage: runtime.Message);
|
RuntimeMessage: runtime.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public JournalDatabaseHydrationResult HydrateWorkspace(string password, string? dataDirectory = null)
|
public JournalDatabaseHydrationResult HydrateWorkspace(string password)
|
||||||
{
|
{
|
||||||
var directory = ResolveDatabaseDirectory();
|
using var connection = OpenEncryptedConnection(password);
|
||||||
Directory.CreateDirectory(directory);
|
|
||||||
|
|
||||||
using var connection = OpenEncryptedConnection(password, directory);
|
|
||||||
EnsureSchema(connection);
|
EnsureSchema(connection);
|
||||||
var runtimeReady = HasRequiredTables(connection);
|
var runtimeReady = HasRequiredTables(connection);
|
||||||
|
|
||||||
var entryDocumentsProcessed = CountEntryDocuments(connection);
|
var entryDocumentsProcessed = CountEntryDocuments(connection);
|
||||||
var schemaPath = WriteSchemaBootstrap(directory);
|
|
||||||
|
|
||||||
return new JournalDatabaseHydrationResult(
|
return new JournalDatabaseHydrationResult(
|
||||||
DatabasePath: GetDatabasePath(directory),
|
DatabasePath: GetDatabasePath(),
|
||||||
SchemaBootstrapPath: schemaPath,
|
|
||||||
EntryFilesProcessed: entryDocumentsProcessed,
|
EntryFilesProcessed: entryDocumentsProcessed,
|
||||||
RuntimeReady: runtimeReady,
|
RuntimeReady: runtimeReady,
|
||||||
Message: 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))
|
if (string.IsNullOrWhiteSpace(password))
|
||||||
throw new ArgumentException("Password cannot be empty.", nameof(password));
|
throw new ArgumentException("Password cannot be empty.", nameof(password));
|
||||||
|
|
||||||
EnsureSqliteInitialized();
|
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();
|
connection.Open();
|
||||||
|
|
||||||
using var keyCmd = connection.CreateCommand();
|
using var keyCmd = connection.CreateCommand();
|
||||||
@ -246,11 +225,11 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
|
|||||||
return RequiredSchemaTables.All(existing.Contains);
|
return RequiredSchemaTables.All(existing.Contains);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (bool Ready, string Message) ProbeRuntime(string password, string? dataDirectory)
|
private (bool Ready, string Message) ProbeRuntime(string password)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var connection = OpenEncryptedConnection(password, dataDirectory);
|
using var connection = OpenEncryptedConnection(password);
|
||||||
EnsureSchema(connection);
|
EnsureSchema(connection);
|
||||||
var ready = HasRequiredTables(connection);
|
var ready = HasRequiredTables(connection);
|
||||||
return ready
|
return ready
|
||||||
@ -282,4 +261,5 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
|
|||||||
|
|
||||||
return Path.GetFullPath(Path.Combine(_config.Current.VaultDirectory, "db"));
|
return Path.GetFullPath(Path.Combine(_config.Current.VaultDirectory, "db"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -381,12 +381,11 @@ Error:
|
|||||||
| `search.entries` | Search entries with filters | optional query/section/date/tags/types/checked/unchecked |
|
| `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.initialize` | Ensure vault directory exists | `payload.password`, `payload.vaultDirectory` |
|
||||||
| `vault.load_all` | Restore encrypted SQLCipher DB snapshot from vault | `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.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) | — |
|
| `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.status` | DB key/schema compatibility snapshot | `payload.password` |
|
||||||
| `db.initialize_schema` | Write SQL schema bootstrap file | optional `payload.dataDirectory` |
|
| `db.initialize_schema` | Initialize SQLCipher schema in the database file | `payload.password` |
|
||||||
| `db.hydrate_workspace` | Bootstrap DB + set session password | `payload.password`, optional `payload.dataDirectory` |
|
| `db.hydrate_workspace` | Bootstrap DB + set session password | `payload.password` |
|
||||||
| `config.get` | Return current config snapshot | — |
|
| `config.get` | Return current config snapshot | — |
|
||||||
| `ai.health` | AI provider health status | — |
|
| `ai.health` | AI provider health status | — |
|
||||||
| `ai.summarize_entry` | Summarize one entry | `payload.content`, optional `payload.fileStem` |
|
| `ai.summarize_entry` | Summarize one entry | `payload.content`, optional `payload.fileStem` |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user