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;
|
||||
|
||||
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);
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
26
README.md
26
README.md
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user