Simplify vault/config APIs for SQLCipher-first storage model
This commit is contained in:
parent
9e92619fc2
commit
f6ff9d2acb
@ -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);
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
26
README.md
26
README.md
@ -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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user