Simplify vault/config APIs for SQLCipher-first storage model

This commit is contained in:
Jacob Schmidt 2026-02-28 17:37:54 -06:00
parent 9e92619fc2
commit f6ff9d2acb
8 changed files with 32 additions and 64 deletions

View File

@ -1,7 +1,7 @@
namespace Journal.Core.Dtos; namespace Journal.Core.Dtos;
internal sealed record VaultInitializePayload(string Password, string VaultDirectory); internal sealed record VaultInitializePayload(string Password, string VaultDirectory);
internal sealed record VaultPayload(string Password, string VaultDirectory, string? NowUtc = null); internal sealed record VaultPayload(string Password, string VaultDirectory);
internal sealed record ClearDataPayload(); internal sealed record ClearDataPayload();
internal sealed record EntryListPayload(); internal sealed record EntryListPayload();
internal sealed record EntryLoadPayload(string FilePath); internal sealed record EntryLoadPayload(string FilePath);

View File

@ -346,11 +346,11 @@ public class Entry(
var saveCurrentPayload = DeserializePayload<VaultPayload>(cmd.Payload); var saveCurrentPayload = DeserializePayload<VaultPayload>(cmd.Payload);
if (saveCurrentPayload is null) if (saveCurrentPayload is null)
return Error("Missing or invalid payload"); return Error("Missing or invalid payload");
result = _vaultStorage.SaveCurrentMonthVault( _vaultStorage.RebuildAllVaults(
saveCurrentPayload.Password, saveCurrentPayload.Password,
saveCurrentPayload.VaultDirectory, saveCurrentPayload.VaultDirectory,
ResolveVaultStorageDirectory(), ResolveVaultStorageDirectory());
ParseNowOrDefault(saveCurrentPayload.NowUtc)); result = true;
break; break;
case "vault.rebuild_all": case "vault.rebuild_all":
var rebuildPayload = DeserializePayload<VaultPayload>(cmd.Payload); var rebuildPayload = DeserializePayload<VaultPayload>(cmd.Payload);
@ -445,23 +445,6 @@ public class Entry(
return payload.Value.Deserialize<T>(JsonOptions); return payload.Value.Deserialize<T>(JsonOptions);
} }
private static DateTime ParseNowOrDefault(string? nowUtc)
{
if (string.IsNullOrWhiteSpace(nowUtc))
return DateTime.UtcNow;
if (DateTime.TryParse(
nowUtc,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var parsed))
{
return parsed;
}
throw new ArgumentException("Invalid nowUtc value. Expected ISO date/time.");
}
private void TryAutoSyncVault(string action, string correlationId) private void TryAutoSyncVault(string action, string correlationId)
{ {
if (!VaultSyncActions.Contains(action)) if (!VaultSyncActions.Contains(action))

View File

@ -3,13 +3,11 @@ namespace Journal.Core.Models;
public sealed record JournalConfig( public sealed record JournalConfig(
string ProjectRoot, string ProjectRoot,
string AppDirectory, string AppDirectory,
string DataDirectory,
string VaultDirectory, string VaultDirectory,
string LogDirectory, string LogDirectory,
string PidFile, string PidFile,
string ServerControlFile, string ServerControlFile,
string DatabaseFilename, string DatabaseFilename,
string MonthlyVaultFormat,
string CloudAiApiKey, string CloudAiApiKey,
string CloudAiApiUrl, string CloudAiApiUrl,
string LlamaCppUrl, string LlamaCppUrl,

View File

@ -11,7 +11,6 @@ public sealed class JournalConfigService : IJournalConfigService
var projectRoot = ResolveProjectRoot(); var projectRoot = ResolveProjectRoot();
var appDirectory = ResolvePath("JOURNAL_APP_DIR", Path.Combine(projectRoot, "journal")); var appDirectory = ResolvePath("JOURNAL_APP_DIR", Path.Combine(projectRoot, "journal"));
var dataDirectory = ResolvePath("JOURNAL_DATA_DIR", Path.Combine(appDirectory, "data"));
var vaultDirectory = ResolvePath("JOURNAL_VAULT_DIR", Path.Combine(appDirectory, "vault")); var vaultDirectory = ResolvePath("JOURNAL_VAULT_DIR", Path.Combine(appDirectory, "vault"));
var logDirectory = ResolvePath("JOURNAL_LOG_DIR", Path.Combine(projectRoot, "logs")); var logDirectory = ResolvePath("JOURNAL_LOG_DIR", Path.Combine(projectRoot, "logs"));
@ -37,13 +36,11 @@ public sealed class JournalConfigService : IJournalConfigService
return new JournalConfig( return new JournalConfig(
ProjectRoot: projectRoot, ProjectRoot: projectRoot,
AppDirectory: appDirectory, AppDirectory: appDirectory,
DataDirectory: dataDirectory,
VaultDirectory: vaultDirectory, VaultDirectory: vaultDirectory,
LogDirectory: logDirectory, LogDirectory: logDirectory,
PidFile: pidFile, PidFile: pidFile,
ServerControlFile: serverControlFile, ServerControlFile: serverControlFile,
DatabaseFilename: Environment.GetEnvironmentVariable("JOURNAL_DATABASE_FILENAME") ?? "journal_cache.db", DatabaseFilename: Environment.GetEnvironmentVariable("JOURNAL_DATABASE_FILENAME") ?? "journal_cache.db",
MonthlyVaultFormat: Environment.GetEnvironmentVariable("JOURNAL_MONTHLY_VAULT_FORMAT") ?? "%Y-%m.vault",
CloudAiApiKey: Environment.GetEnvironmentVariable("CLOUDAI_API_KEY") ?? "", CloudAiApiKey: Environment.GetEnvironmentVariable("CLOUDAI_API_KEY") ?? "",
CloudAiApiUrl: Environment.GetEnvironmentVariable("CLOUDAI_API_URL") ?? "", CloudAiApiUrl: Environment.GetEnvironmentVariable("CLOUDAI_API_URL") ?? "",
LlamaCppUrl: Environment.GetEnvironmentVariable("LLAMA_CPP_URL") ?? "http://127.0.0.1:8085/v1/completions", LlamaCppUrl: Environment.GetEnvironmentVariable("LLAMA_CPP_URL") ?? "http://127.0.0.1:8085/v1/completions",

View File

@ -1,5 +1,6 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Globalization;
using Journal.Core.Dtos; using Journal.Core.Dtos;
using Journal.Core.Services.Config; using Journal.Core.Services.Config;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
@ -171,17 +172,17 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
EnsureSchema(connection); EnsureSchema(connection);
var runtimeReady = HasRequiredTables(connection); var runtimeReady = HasRequiredTables(connection);
var entryFilesProcessed = Directory.GetFiles(directory, "*.md", SearchOption.TopDirectoryOnly).Length; var entryDocumentsProcessed = CountEntryDocuments(connection);
var schemaPath = WriteSchemaBootstrap(directory); var schemaPath = WriteSchemaBootstrap(directory);
return new JournalDatabaseHydrationResult( return new JournalDatabaseHydrationResult(
DatabasePath: GetDatabasePath(directory), DatabasePath: GetDatabasePath(directory),
SchemaBootstrapPath: schemaPath, SchemaBootstrapPath: schemaPath,
EntryFilesProcessed: entryFilesProcessed, EntryFilesProcessed: entryDocumentsProcessed,
RuntimeReady: runtimeReady, RuntimeReady: runtimeReady,
Message: runtimeReady Message: runtimeReady
? "Workspace hydration completed with SQLCipher runtime schema validation." ? "Workspace hydration completed with SQLCipher runtime schema validation and document store readiness."
: "Workspace hydration completed, but required schema tables were not found."); : "Workspace hydration completed, but required SQLCipher schema tables were not found.");
} }
private static void EnsureSqliteInitialized() private static void EnsureSqliteInitialized()
@ -262,6 +263,17 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
} }
} }
private static int CountEntryDocuments(SqliteConnection connection)
{
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT COUNT(*) FROM entry_documents;";
var scalar = cmd.ExecuteScalar();
if (scalar is null || scalar is DBNull)
return 0;
return Convert.ToInt32(scalar, CultureInfo.InvariantCulture);
}
private string ResolveDatabaseDirectory() private string ResolveDatabaseDirectory()
{ {
var overrideDir = Environment.GetEnvironmentVariable("JOURNAL_DATABASE_DIR"); var overrideDir = Environment.GetEnvironmentVariable("JOURNAL_DATABASE_DIR");

View File

@ -2,9 +2,7 @@ namespace Journal.Core.Services.Vault;
public interface IVaultStorageService public interface IVaultStorageService
{ {
string GetMonthlyVaultFileName(DateTime date);
bool LoadAllVaults(string password, string vaultDirectory, string dataDirectory); bool LoadAllVaults(string password, string vaultDirectory, string dataDirectory);
bool SaveCurrentMonthVault(string password, string vaultDirectory, string dataDirectory, DateTime now);
void RebuildAllVaults(string password, string vaultDirectory, string dataDirectory); void RebuildAllVaults(string password, string vaultDirectory, string dataDirectory);
void ClearDataDirectory(string dataDirectory); void ClearDataDirectory(string dataDirectory);
} }

View File

@ -13,8 +13,6 @@ public class VaultStorageService(IVaultCryptoService crypto, IJournalDatabaseSer
private const string DatabaseVaultPrefix = "_db_"; private const string DatabaseVaultPrefix = "_db_";
private const string DatabaseVaultSuffix = ".vault"; private const string DatabaseVaultSuffix = ".vault";
public string GetMonthlyVaultFileName(DateTime date) => date.ToString("yyyy-MM") + ".vault";
public bool LoadAllVaults(string password, string vaultDirectory, string dataDirectory) public bool LoadAllVaults(string password, string vaultDirectory, string dataDirectory)
{ {
EnsureRequiredArguments(password, vaultDirectory, dataDirectory); EnsureRequiredArguments(password, vaultDirectory, dataDirectory);
@ -30,22 +28,6 @@ public class VaultStorageService(IVaultCryptoService crypto, IJournalDatabaseSer
} }
} }
public bool SaveCurrentMonthVault(string password, string vaultDirectory, string dataDirectory, DateTime now)
{
EnsureRequiredArguments(password, vaultDirectory, dataDirectory);
lock (_vaultIoLock)
{
Directory.CreateDirectory(vaultDirectory);
var dbDirectory = GetDatabaseDirectory();
if (!Directory.Exists(dbDirectory))
return false;
SaveDatabaseVaults(password, vaultDirectory, dbDirectory);
return true;
}
}
public void RebuildAllVaults(string password, string vaultDirectory, string dataDirectory) public void RebuildAllVaults(string password, string vaultDirectory, string dataDirectory)
{ {
EnsureRequiredArguments(password, vaultDirectory, dataDirectory); EnsureRequiredArguments(password, vaultDirectory, dataDirectory);

View File

@ -185,9 +185,9 @@ dotnet run --project Journal.SmokeTests
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `JOURNAL_PROJECT_ROOT` | auto-detected | Override project root (vault + data path resolution) | | `JOURNAL_PROJECT_ROOT` | auto-detected | Override project root (vault path resolution) |
| `JOURNAL_DATA_DIR` | `<root>/journal/data` | Override decrypted data directory |
| `JOURNAL_VAULT_DIR` | `<root>/journal/vault` | Override vault directory | | `JOURNAL_VAULT_DIR` | `<root>/journal/vault` | Override vault directory |
| `JOURNAL_DATABASE_DIR` | `<vault>/db` | Override SQLCipher database directory |
| `JOURNAL_AI_PROVIDER` | `none` | `none` or `python-sidecar` | | `JOURNAL_AI_PROVIDER` | `none` | `none` or `python-sidecar` |
| `JOURNAL_PYTHON_EXE` | `python` | Python executable for AI/speech sidecar | | `JOURNAL_PYTHON_EXE` | `python` | Python executable for AI/speech sidecar |
| `JOURNAL_AI_SIDECAR_PATH` | auto | Path to Python AI sidecar script | | `JOURNAL_AI_SIDECAR_PATH` | auto | Path to Python AI sidecar script |
@ -370,20 +370,20 @@ Error:
| `todos.items.create` | Add todo item | `payload` | | `todos.items.create` | Add todo item | `payload` |
| `todos.items.update` | Update todo item | `id`, `payload` | | `todos.items.update` | Update todo item | `id`, `payload` |
| `todos.items.delete` | Delete todo item | `id` | | `todos.items.delete` | Delete todo item | `id` |
| `entries.list` | List decrypted `.md` entries | optional `payload.dataDirectory` | | `entries.list` | List persisted entries from SQLCipher store | — |
| `entries.load` | Load one entry file | `payload.filePath` | | `entries.load` | Load one entry file | `payload.filePath` |
| `entries.save` | Save/merge entry content | `payload.content`, optional `payload.filePath`, `payload.mode`, `payload.fileName` | | `entries.save` | Save/merge entry content | `payload.content`, optional `payload.filePath`, `payload.mode`, `payload.fileName` |
| `entries.delete` | Delete an entry file | `payload.filePath` | | `entries.delete` | Delete an entry file | `payload.filePath` |
| `templates.list` | List `.template.md` files | optional `payload.dataDirectory` | | `templates.list` | List templates from SQLCipher store | — |
| `templates.load` | Load a template | `payload.filePath` | | `templates.load` | Load a template | `payload.filePath` |
| `templates.save` | Save/create a template | `payload.name` | | `templates.save` | Save/create a template | `payload.name` |
| `templates.delete` | Delete a template | `payload.filePath` | | `templates.delete` | Delete a template | `payload.filePath` |
| `search.entries` | Search entries with filters | `payload.dataDirectory`, 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` | Decrypt all monthly vaults → data dir | `payload.password`, `payload.vaultDirectory`, `payload.dataDirectory` | | `vault.load_all` | Restore encrypted SQLCipher DB snapshot from vault | `payload.password`, `payload.vaultDirectory` |
| `vault.save_current_month` | Encrypt only current month (optimized) | `payload.password`, `payload.vaultDirectory`, `payload.dataDirectory` | | `vault.save_current_month` | Alias for vault rebuild (DB snapshot persist) | `payload.password`, `payload.vaultDirectory` |
| `vault.rebuild_all` | Rebuild all monthly vaults from data | `payload.password`, `payload.vaultDirectory`, `payload.dataDirectory` | | `vault.rebuild_all` | Persist encrypted SQLCipher DB snapshot to vault | `payload.password`, `payload.vaultDirectory` |
| `vault.clear_data_directory` | Wipe decrypted data directory | `payload.dataDirectory` | | `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`, optional `payload.dataDirectory` |
| `db.initialize_schema` | Write SQL schema bootstrap file | 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.hydrate_workspace` | Bootstrap DB + set session password | `payload.password`, optional `payload.dataDirectory` |
@ -416,8 +416,8 @@ dotnet run --project Journal.Sidecar -- search "common text" --tag stress --type
- Pass `--password <value>` → non-interactive/automation mode - Pass `--password <value>` → non-interactive/automation mode
**Optional path overrides:** **Optional path overrides:**
- `--vault-dir <path>` / `--data-dir <path>` - `--vault-dir <path>`
- Env fallback: `JOURNAL_VAULT_DIR`, `JOURNAL_DATA_DIR`, `JOURNAL_APP_DIR` - Env fallback: `JOURNAL_VAULT_DIR`, `JOURNAL_DATABASE_DIR`, `JOURNAL_APP_DIR`
**Search CLI flags:** **Search CLI flags:**
- positional `query` (optional) - positional `query` (optional)
@ -428,8 +428,6 @@ dotnet run --project Journal.Sidecar -- search "common text" --tag stress --type
- `--section` / `-sec` - `--section` / `-sec`
- `--checked` / `-chk` (repeatable) - `--checked` / `-chk` (repeatable)
- `--unchecked` / `-uchk` (repeatable) - `--unchecked` / `-uchk` (repeatable)
- `--data-dir <path>` (optional override)
--- ---
## Publishing ## Publishing
@ -532,7 +530,7 @@ Quick reference:
## Notes ## Notes
- Decrypted journal data in `journal/data/` is cleared on graceful shutdown (`vault.clear_data_directory`). - Journal content and templates persist in SQLCipher (`entry_documents`) under the vault DB directory.
- The legacy Python placeholder file `_init_vault.vault` is treated as obsolete — the C# backend ignores and removes it during vault load. - The legacy Python placeholder file `_init_vault.vault` is treated as obsolete — the C# backend ignores and removes it during vault load.
- `Journal.WebGateway` is intentionally excluded from `Journal.slnx`; it is built/run independently via `dotnet` or the scripts wrappers. - `Journal.WebGateway` is intentionally excluded from `Journal.slnx`; it is built/run independently via `dotnet` or the scripts wrappers.
- On Windows + Tauri, the sidecar process is spawned with `CREATE_NO_WINDOW` to suppress the console window. - On Windows + Tauri, the sidecar process is spawned with `CREATE_NO_WINDOW` to suppress the console window.