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;
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 EntryListPayload();
internal sealed record EntryLoadPayload(string FilePath);

View File

@ -346,11 +346,11 @@ public class Entry(
var saveCurrentPayload = DeserializePayload<VaultPayload>(cmd.Payload);
if (saveCurrentPayload is null)
return Error("Missing or invalid payload");
result = _vaultStorage.SaveCurrentMonthVault(
_vaultStorage.RebuildAllVaults(
saveCurrentPayload.Password,
saveCurrentPayload.VaultDirectory,
ResolveVaultStorageDirectory(),
ParseNowOrDefault(saveCurrentPayload.NowUtc));
ResolveVaultStorageDirectory());
result = true;
break;
case "vault.rebuild_all":
var rebuildPayload = DeserializePayload<VaultPayload>(cmd.Payload);
@ -445,23 +445,6 @@ public class Entry(
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)
{
if (!VaultSyncActions.Contains(action))

View File

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

View File

@ -11,7 +11,6 @@ public sealed class JournalConfigService : IJournalConfigService
var projectRoot = ResolveProjectRoot();
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 logDirectory = ResolvePath("JOURNAL_LOG_DIR", Path.Combine(projectRoot, "logs"));
@ -37,13 +36,11 @@ public sealed class JournalConfigService : IJournalConfigService
return new JournalConfig(
ProjectRoot: projectRoot,
AppDirectory: appDirectory,
DataDirectory: dataDirectory,
VaultDirectory: vaultDirectory,
LogDirectory: logDirectory,
PidFile: pidFile,
ServerControlFile: serverControlFile,
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") ?? "",
CloudAiApiUrl: Environment.GetEnvironmentVariable("CLOUDAI_API_URL") ?? "",
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.Text;
using System.Globalization;
using Journal.Core.Dtos;
using Journal.Core.Services.Config;
using Microsoft.Data.Sqlite;
@ -171,17 +172,17 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
EnsureSchema(connection);
var runtimeReady = HasRequiredTables(connection);
var entryFilesProcessed = Directory.GetFiles(directory, "*.md", SearchOption.TopDirectoryOnly).Length;
var entryDocumentsProcessed = CountEntryDocuments(connection);
var schemaPath = WriteSchemaBootstrap(directory);
return new JournalDatabaseHydrationResult(
DatabasePath: GetDatabasePath(directory),
SchemaBootstrapPath: schemaPath,
EntryFilesProcessed: entryFilesProcessed,
EntryFilesProcessed: entryDocumentsProcessed,
RuntimeReady: runtimeReady,
Message: runtimeReady
? "Workspace hydration completed with SQLCipher runtime schema validation."
: "Workspace hydration completed, but required schema tables were not found.");
? "Workspace hydration completed with SQLCipher runtime schema validation and document store readiness."
: "Workspace hydration completed, but required SQLCipher schema tables were not found.");
}
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()
{
var overrideDir = Environment.GetEnvironmentVariable("JOURNAL_DATABASE_DIR");

View File

@ -2,9 +2,7 @@ namespace Journal.Core.Services.Vault;
public interface IVaultStorageService
{
string GetMonthlyVaultFileName(DateTime date);
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 ClearDataDirectory(string dataDirectory);
}

View File

@ -13,8 +13,6 @@ public class VaultStorageService(IVaultCryptoService crypto, IJournalDatabaseSer
private const string DatabaseVaultPrefix = "_db_";
private const string DatabaseVaultSuffix = ".vault";
public string GetMonthlyVaultFileName(DateTime date) => date.ToString("yyyy-MM") + ".vault";
public bool LoadAllVaults(string password, string vaultDirectory, string 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)
{
EnsureRequiredArguments(password, vaultDirectory, dataDirectory);

View File

@ -185,9 +185,9 @@ dotnet run --project Journal.SmokeTests
| Variable | Default | Description |
|----------|---------|-------------|
| `JOURNAL_PROJECT_ROOT` | auto-detected | Override project root (vault + data path resolution) |
| `JOURNAL_DATA_DIR` | `<root>/journal/data` | Override decrypted data directory |
| `JOURNAL_PROJECT_ROOT` | auto-detected | Override project root (vault path resolution) |
| `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_PYTHON_EXE` | `python` | Python executable for AI/speech sidecar |
| `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.update` | Update todo item | `id`, `payload` |
| `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.save` | Save/merge entry content | `payload.content`, optional `payload.filePath`, `payload.mode`, `payload.fileName` |
| `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.save` | Save/create a template | `payload.name` |
| `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.load_all` | Decrypt all monthly vaults → data dir | `payload.password`, `payload.vaultDirectory`, `payload.dataDirectory` |
| `vault.save_current_month` | Encrypt only current month (optimized) | `payload.password`, `payload.vaultDirectory`, `payload.dataDirectory` |
| `vault.rebuild_all` | Rebuild all monthly vaults from data | `payload.password`, `payload.vaultDirectory`, `payload.dataDirectory` |
| `vault.clear_data_directory` | Wipe decrypted data directory | `payload.dataDirectory` |
| `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` |
@ -416,8 +416,8 @@ dotnet run --project Journal.Sidecar -- search "common text" --tag stress --type
- Pass `--password <value>` → non-interactive/automation mode
**Optional path overrides:**
- `--vault-dir <path>` / `--data-dir <path>`
- Env fallback: `JOURNAL_VAULT_DIR`, `JOURNAL_DATA_DIR`, `JOURNAL_APP_DIR`
- `--vault-dir <path>`
- Env fallback: `JOURNAL_VAULT_DIR`, `JOURNAL_DATABASE_DIR`, `JOURNAL_APP_DIR`
**Search CLI flags:**
- positional `query` (optional)
@ -428,8 +428,6 @@ dotnet run --project Journal.Sidecar -- search "common text" --tag stress --type
- `--section` / `-sec`
- `--checked` / `-chk` (repeatable)
- `--unchecked` / `-uchk` (repeatable)
- `--data-dir <path>` (optional override)
---
## Publishing
@ -532,7 +530,7 @@ Quick reference:
## 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.
- `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.