Remove data-directory markdown flow and complete SQLCipher backend cleanup

This commit is contained in:
Jacob Schmidt 2026-02-28 17:31:53 -06:00
parent 72f8221605
commit 9e92619fc2
12 changed files with 115 additions and 142 deletions

View File

@ -1,20 +1,20 @@
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 DataDirectory, string? NowUtc = null); internal sealed record VaultPayload(string Password, string VaultDirectory, string? NowUtc = null);
internal sealed record ClearDataPayload(string DataDirectory); internal sealed record ClearDataPayload();
internal sealed record EntryListPayload(string? DataDirectory = null); internal sealed record EntryListPayload();
internal sealed record EntryLoadPayload(string FilePath); internal sealed record EntryLoadPayload(string FilePath);
public sealed record EntrySavePayload(string Content, string? FilePath = null, string? Mode = null, string? FileName = null); public sealed record EntrySavePayload(string Content, string? FilePath = null, string? Mode = null, string? FileName = null);
public sealed record EntryListItem(string FileName, string FilePath); public sealed record EntryListItem(string FileName, string FilePath);
public sealed record EntryLoadResult(string FileName, string FilePath, JournalEntryDto Entry); public sealed record EntryLoadResult(string FileName, string FilePath, JournalEntryDto Entry);
public sealed record EntrySaveResult(string FilePath); public sealed record EntrySaveResult(string FilePath);
internal sealed record EntryDeletePayload(string FilePath); internal sealed record EntryDeletePayload(string FilePath);
internal sealed record EntryTemplateListPayload(string? DataDirectory = null); internal sealed record EntryTemplateListPayload();
internal sealed record EntryTemplateLoadPayload(string FilePath); 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, string? DataDirectory = 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, string? DataDirectory = null);
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);
@ -30,7 +30,6 @@ internal sealed record SpeechTranscribePayload(
int? SimulateDelayMs = null, int? SimulateDelayMs = null,
int? Simulate_Delay_Ms = null); int? Simulate_Delay_Ms = null);
internal sealed record SearchEntriesPayload( internal sealed record SearchEntriesPayload(
string DataDirectory,
string? Query = null, string? Query = null,
string? Section = null, string? Section = null,
string? StartDate = null, string? StartDate = null,

View File

@ -1,7 +1,6 @@
namespace Journal.Core.Dtos; namespace Journal.Core.Dtos;
public sealed record EntrySearchRequestDto( public sealed record EntrySearchRequestDto(
string DataDirectory,
string? Query = null, string? Query = null,
string? Section = null, string? Section = null,
string? StartDate = null, string? StartDate = null,

View File

@ -214,10 +214,9 @@ public class Entry(
break; break;
case "search.entries": case "search.entries":
var searchPayload = DeserializePayload<SearchEntriesPayload>(cmd.Payload); var searchPayload = DeserializePayload<SearchEntriesPayload>(cmd.Payload);
if (searchPayload is null || string.IsNullOrWhiteSpace(searchPayload.DataDirectory)) if (searchPayload is null)
return Error("Missing or invalid payload"); return Error("Missing or invalid payload");
var searchRequest = new EntrySearchRequestDto( var searchRequest = new EntrySearchRequestDto(
DataDirectory: searchPayload.DataDirectory,
Query: searchPayload.Query, Query: searchPayload.Query,
Section: searchPayload.Section, Section: searchPayload.Section,
StartDate: searchPayload.StartDate, StartDate: searchPayload.StartDate,
@ -229,18 +228,12 @@ public class Entry(
result = await _entrySearch.SearchEntriesAsync(searchRequest); result = await _entrySearch.SearchEntriesAsync(searchRequest);
break; break;
case "entries.list": case "entries.list":
var listPayload = DeserializePayload<EntryListPayload>(cmd.Payload); _ = DeserializePayload<EntryListPayload>(cmd.Payload);
var listDataDirectory = !string.IsNullOrWhiteSpace(listPayload?.DataDirectory) result = _entryFiles.ListEntries();
? listPayload.DataDirectory
: _config.Current.DataDirectory;
result = _entryFiles.ListEntries(listDataDirectory);
break; break;
case "templates.list": case "templates.list":
var templateListPayload = DeserializePayload<EntryTemplateListPayload>(cmd.Payload); _ = DeserializePayload<EntryTemplateListPayload>(cmd.Payload);
var templateListDirectory = !string.IsNullOrWhiteSpace(templateListPayload?.DataDirectory) result = _entryFiles.ListTemplates();
? templateListPayload.DataDirectory
: _config.Current.DataDirectory;
result = _entryFiles.ListTemplates(templateListDirectory);
break; break;
case "entries.load": case "entries.load":
var loadEntryPayload = DeserializePayload<EntryLoadPayload>(cmd.Payload); var loadEntryPayload = DeserializePayload<EntryLoadPayload>(cmd.Payload);
@ -258,13 +251,13 @@ public class Entry(
var saveEntryPayload = DeserializePayload<EntrySavePayload>(cmd.Payload); var saveEntryPayload = DeserializePayload<EntrySavePayload>(cmd.Payload);
if (saveEntryPayload is null || string.IsNullOrWhiteSpace(saveEntryPayload.Content)) if (saveEntryPayload is null || string.IsNullOrWhiteSpace(saveEntryPayload.Content))
return Error("Missing or invalid payload"); return Error("Missing or invalid payload");
result = _entryFiles.SaveEntry(saveEntryPayload, _config.Current.DataDirectory); result = _entryFiles.SaveEntry(saveEntryPayload);
break; break;
case "templates.save": case "templates.save":
var saveTemplatePayload = DeserializePayload<EntryTemplateSavePayload>(cmd.Payload); var saveTemplatePayload = DeserializePayload<EntryTemplateSavePayload>(cmd.Payload);
if (saveTemplatePayload is null || string.IsNullOrWhiteSpace(saveTemplatePayload.Name)) if (saveTemplatePayload is null || string.IsNullOrWhiteSpace(saveTemplatePayload.Name))
return Error("Missing or invalid payload"); return Error("Missing or invalid payload");
result = _entryFiles.SaveTemplate(saveTemplatePayload, _config.Current.DataDirectory); result = _entryFiles.SaveTemplate(saveTemplatePayload);
break; break;
case "entries.delete": case "entries.delete":
var deleteEntryPayload = DeserializePayload<EntryDeletePayload>(cmd.Payload); var deleteEntryPayload = DeserializePayload<EntryDeletePayload>(cmd.Payload);
@ -343,9 +336,10 @@ public class Entry(
var loadPayload = DeserializePayload<VaultPayload>(cmd.Payload); var loadPayload = DeserializePayload<VaultPayload>(cmd.Payload);
if (loadPayload is null) if (loadPayload is null)
return Error("Missing or invalid payload"); return Error("Missing or invalid payload");
var loaded = _vaultStorage.LoadAllVaults(loadPayload.Password, loadPayload.VaultDirectory, loadPayload.DataDirectory); var vaultStorageDirectory = ResolveVaultStorageDirectory();
var loaded = _vaultStorage.LoadAllVaults(loadPayload.Password, loadPayload.VaultDirectory, vaultStorageDirectory);
if (loaded) if (loaded)
_databaseSession.SetPassword(loadPayload.Password, loadPayload.DataDirectory); _databaseSession.SetPassword(loadPayload.Password);
result = loaded; result = loaded;
break; break;
case "vault.save_current_month": case "vault.save_current_month":
@ -355,7 +349,7 @@ public class Entry(
result = _vaultStorage.SaveCurrentMonthVault( result = _vaultStorage.SaveCurrentMonthVault(
saveCurrentPayload.Password, saveCurrentPayload.Password,
saveCurrentPayload.VaultDirectory, saveCurrentPayload.VaultDirectory,
saveCurrentPayload.DataDirectory, ResolveVaultStorageDirectory(),
ParseNowOrDefault(saveCurrentPayload.NowUtc)); ParseNowOrDefault(saveCurrentPayload.NowUtc));
break; break;
case "vault.rebuild_all": case "vault.rebuild_all":
@ -363,16 +357,16 @@ public class Entry(
if (rebuildPayload is null) if (rebuildPayload is null)
return Error("Missing or invalid payload"); return Error("Missing or invalid payload");
_databaseSession.CloseConnection(); _databaseSession.CloseConnection();
_vaultStorage.RebuildAllVaults(rebuildPayload.Password, rebuildPayload.VaultDirectory, rebuildPayload.DataDirectory); _vaultStorage.RebuildAllVaults(rebuildPayload.Password, rebuildPayload.VaultDirectory, ResolveVaultStorageDirectory());
result = true; result = true;
break; break;
case "vault.clear_data_directory": case "vault.clear_data_directory":
var clearPayload = DeserializePayload<ClearDataPayload>(cmd.Payload); var clearPayload = DeserializePayload<ClearDataPayload>(cmd.Payload);
if (clearPayload is null || string.IsNullOrWhiteSpace(clearPayload.DataDirectory)) if (clearPayload is null)
return Error("Missing or invalid payload"); return Error("Missing or invalid payload");
if (_databaseSession is IDisposable disposableSession) if (_databaseSession is IDisposable disposableSession)
disposableSession.Dispose(); disposableSession.Dispose();
_vaultStorage.ClearDataDirectory(clearPayload.DataDirectory); _vaultStorage.ClearDataDirectory(ResolveVaultStorageDirectory());
result = true; result = true;
break; break;
case "db.status": case "db.status":
@ -393,7 +387,7 @@ public class Entry(
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, dbHydratePayload.DataDirectory);
_databaseSession.SetPassword(dbHydratePayload.Password, dbHydratePayload.DataDirectory); _databaseSession.SetPassword(dbHydratePayload.Password);
break; break;
default: default:
CommandLogger.LogFailure(action, correlationId, "unknown_action"); CommandLogger.LogFailure(action, correlationId, "unknown_action");
@ -473,22 +467,27 @@ public class Entry(
if (!VaultSyncActions.Contains(action)) if (!VaultSyncActions.Contains(action))
return; return;
if (!_databaseSession.TryGetSession(out var password, out var sessionDataDirectory)) if (!_databaseSession.TryGetSession(out var password, out _))
return; return;
try try
{ {
var config = _config.Current; var config = _config.Current;
var dataDirectory = string.IsNullOrWhiteSpace(sessionDataDirectory)
? config.DataDirectory
: sessionDataDirectory;
_databaseSession.CloseConnection(); _databaseSession.CloseConnection();
_vaultStorage.RebuildAllVaults(password, config.VaultDirectory, dataDirectory); _vaultStorage.RebuildAllVaults(password, config.VaultDirectory, ResolveVaultStorageDirectory());
} }
catch (Exception ex) catch (Exception ex)
{ {
CommandLogger.LogFailure(action, correlationId, "vault_auto_sync_failed", ex.Message); CommandLogger.LogFailure(action, correlationId, "vault_auto_sync_failed", ex.Message);
} }
} }
private string ResolveVaultStorageDirectory()
{
var dbPath = _database.GetDatabasePath();
var directory = Path.GetDirectoryName(dbPath);
return string.IsNullOrWhiteSpace(directory)
? Path.GetFullPath(".")
: Path.GetFullPath(directory);
}
} }

View File

@ -1,35 +0,0 @@
namespace Journal.Core.Repositories;
public sealed class DiskEntryFileRepository : IEntryFileRepository
{
public IReadOnlyList<string> ListMarkdownFiles(string dataDirectory)
{
if (!Directory.Exists(dataDirectory))
return [];
return [.. Directory.GetFiles(dataDirectory, "*.md").OrderBy(Path.GetFileName, StringComparer.Ordinal)];
}
public string ReadFile(string filePath) => File.ReadAllText(filePath);
public void WriteFile(string filePath, string content) => File.WriteAllText(filePath, content);
public void AppendFile(string filePath, string content) => File.AppendAllText(filePath, content);
public bool FileExists(string filePath) => File.Exists(filePath);
public string GetFullPath(string filePath) => Path.GetFullPath(filePath);
public string GetFileName(string filePath) => Path.GetFileName(filePath);
public string GetFileNameWithoutExtension(string filePath) => Path.GetFileNameWithoutExtension(filePath);
public void EnsureDirectory(string path)
{
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(dir))
Directory.CreateDirectory(dir);
}
public void DeleteFile(string filePath) => File.Delete(filePath);
}

View File

@ -2,7 +2,7 @@ namespace Journal.Core.Repositories;
public interface IEntryFileRepository public interface IEntryFileRepository
{ {
IReadOnlyList<string> ListMarkdownFiles(string dataDirectory); IReadOnlyList<string> ListMarkdownFiles();
string ReadFile(string filePath); string ReadFile(string filePath);
void WriteFile(string filePath, string content); void WriteFile(string filePath, string content);
void AppendFile(string filePath, string content); void AppendFile(string filePath, string content);

View File

@ -9,7 +9,7 @@ public sealed class SqliteEntryFileRepository(IDatabaseSessionService session) :
private const string TemplatePrefix = "db://template/"; private const string TemplatePrefix = "db://template/";
private readonly IDatabaseSessionService _session = session; private readonly IDatabaseSessionService _session = session;
public IReadOnlyList<string> ListMarkdownFiles(string dataDirectory) public IReadOnlyList<string> ListMarkdownFiles()
{ {
var conn = _session.GetConnection(); var conn = _session.GetConnection();
using var cmd = conn.CreateCommand(); using var cmd = conn.CreateCommand();

View File

@ -20,9 +20,7 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
public string GetDatabasePath(string? dataDirectory = null) public string GetDatabasePath(string? dataDirectory = null)
{ {
var directory = string.IsNullOrWhiteSpace(dataDirectory) var directory = ResolveDatabaseDirectory();
? _config.Current.DataDirectory
: dataDirectory;
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
return Path.GetFullPath(Path.Combine(directory, _config.Current.DatabaseFilename)); return Path.GetFullPath(Path.Combine(directory, _config.Current.DatabaseFilename));
@ -136,9 +134,7 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
public string WriteSchemaBootstrap(string? dataDirectory = null) public string WriteSchemaBootstrap(string? dataDirectory = null)
{ {
var directory = string.IsNullOrWhiteSpace(dataDirectory) var directory = ResolveDatabaseDirectory();
? _config.Current.DataDirectory
: dataDirectory;
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
var bootstrapPath = Path.GetFullPath(Path.Combine(directory, "journal_schema.sql")); var bootstrapPath = Path.GetFullPath(Path.Combine(directory, "journal_schema.sql"));
@ -168,9 +164,7 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
public JournalDatabaseHydrationResult HydrateWorkspace(string password, string? dataDirectory = null) public JournalDatabaseHydrationResult HydrateWorkspace(string password, string? dataDirectory = null)
{ {
var directory = string.IsNullOrWhiteSpace(dataDirectory) var directory = ResolveDatabaseDirectory();
? _config.Current.DataDirectory
: dataDirectory;
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
using var connection = OpenEncryptedConnection(password, directory); using var connection = OpenEncryptedConnection(password, directory);
@ -267,4 +261,13 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
return (false, $"SQLCipher runtime check failed: {ex.Message}"); return (false, $"SQLCipher runtime check failed: {ex.Message}");
} }
} }
private string ResolveDatabaseDirectory()
{
var overrideDir = Environment.GetEnvironmentVariable("JOURNAL_DATABASE_DIR");
if (!string.IsNullOrWhiteSpace(overrideDir))
return Path.GetFullPath(overrideDir);
return Path.GetFullPath(Path.Combine(_config.Current.VaultDirectory, "db"));
}
} }

View File

@ -7,18 +7,18 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ
{ {
private readonly IEntryFileRepository _repo = repo ?? throw new ArgumentNullException(nameof(repo)); private readonly IEntryFileRepository _repo = repo ?? throw new ArgumentNullException(nameof(repo));
public IReadOnlyList<EntryListItem> ListEntries(string dataDirectory) public IReadOnlyList<EntryListItem> ListEntries()
{ {
return [.. _repo.ListMarkdownFiles(dataDirectory) return [.. _repo.ListMarkdownFiles()
.Where(path => !EntryFileNaming.IsTemplateFileName(_repo.GetFileName(path))) .Where(path => !EntryFileNaming.IsTemplateFileName(_repo.GetFileName(path)))
.Select(path => new EntryListItem( .Select(path => new EntryListItem(
FileName: _repo.GetFileName(path), FileName: _repo.GetFileName(path),
FilePath: _repo.GetFullPath(path)))]; FilePath: _repo.GetFullPath(path)))];
} }
public IReadOnlyList<EntryListItem> ListTemplates(string dataDirectory) public IReadOnlyList<EntryListItem> ListTemplates()
{ {
return [.. _repo.ListMarkdownFiles(dataDirectory) return [.. _repo.ListMarkdownFiles()
.Where(path => EntryFileNaming.IsTemplateFileName(_repo.GetFileName(path))) .Where(path => EntryFileNaming.IsTemplateFileName(_repo.GetFileName(path)))
.Select(path => new EntryListItem( .Select(path => new EntryListItem(
FileName: _repo.GetFileName(path), FileName: _repo.GetFileName(path),
@ -55,9 +55,9 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ
return new EntryTemplateLoadResult(fileName, normalizedPath, rawContent); return new EntryTemplateLoadResult(fileName, normalizedPath, rawContent);
} }
public EntrySaveResult SaveEntry(EntrySavePayload payload, string defaultDataDirectory) public EntrySaveResult SaveEntry(EntrySavePayload payload)
{ {
var targetPath = ResolveTargetPath(payload.FilePath, payload.FileName, defaultDataDirectory); var targetPath = ResolveTargetPath(payload.FilePath, payload.FileName);
var mode = string.IsNullOrWhiteSpace(payload.Mode) ? "Daily" : payload.Mode.Trim(); var mode = string.IsNullOrWhiteSpace(payload.Mode) ? "Daily" : payload.Mode.Trim();
var sanitizedContent = HtmlSanitizer.StripRichHtml(payload.Content ?? ""); var sanitizedContent = HtmlSanitizer.StripRichHtml(payload.Content ?? "");
_repo.EnsureDirectory(targetPath); _repo.EnsureDirectory(targetPath);
@ -93,16 +93,13 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ
return new EntrySaveResult(targetPath); return new EntrySaveResult(targetPath);
} }
public EntrySaveResult SaveTemplate(EntryTemplateSavePayload payload, string defaultDataDirectory) public EntrySaveResult SaveTemplate(EntryTemplateSavePayload payload)
{ {
ArgumentNullException.ThrowIfNull(payload); ArgumentNullException.ThrowIfNull(payload);
if (string.IsNullOrWhiteSpace(payload.Name)) if (string.IsNullOrWhiteSpace(payload.Name))
throw new ArgumentException("Template name is required."); throw new ArgumentException("Template name is required.");
var directory = string.IsNullOrWhiteSpace(payload.DataDirectory) var targetPath = ResolveTemplatePath(payload.FilePath, payload.Name);
? defaultDataDirectory
: payload.DataDirectory;
var targetPath = ResolveTemplatePath(payload.FilePath, payload.Name, directory);
var fileName = _repo.GetFileName(targetPath); var fileName = _repo.GetFileName(targetPath);
if (!EntryFileNaming.IsTemplateFileName(fileName)) if (!EntryFileNaming.IsTemplateFileName(fileName))
throw new ArgumentException("Template file name must end with .template.md."); throw new ArgumentException("Template file name must end with .template.md.");
@ -136,7 +133,7 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ
return true; return true;
} }
private string ResolveTargetPath(string? filePath, string? fileName, string defaultDataDirectory) private string ResolveTargetPath(string? filePath, string? fileName)
{ {
if (!string.IsNullOrWhiteSpace(filePath)) if (!string.IsNullOrWhiteSpace(filePath))
return _repo.GetFullPath(filePath); return _repo.GetFullPath(filePath);
@ -145,16 +142,16 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ
? SanitizeFileName(fileName) ? SanitizeFileName(fileName)
: $"{DateTime.Now:yyyy-MM-dd}"; : $"{DateTime.Now:yyyy-MM-dd}";
return _repo.GetFullPath(Path.Combine(defaultDataDirectory, $"{name}.md")); return _repo.GetFullPath($"{name}.md");
} }
private string ResolveTemplatePath(string? filePath, string templateName, string defaultDataDirectory) private string ResolveTemplatePath(string? filePath, string templateName)
{ {
if (!string.IsNullOrWhiteSpace(filePath)) if (!string.IsNullOrWhiteSpace(filePath))
return _repo.GetFullPath(filePath); return _repo.GetFullPath(filePath);
var name = SanitizeFileName(templateName); var name = SanitizeFileName(templateName);
return _repo.GetFullPath(Path.Combine(defaultDataDirectory, $"{name}{EntryFileNaming.TemplateSuffix}")); return _repo.GetFullPath($"{name}{EntryFileNaming.TemplateSuffix}");
} }
private static string SanitizeFileName(string name) private static string SanitizeFileName(string name)

View File

@ -15,8 +15,6 @@ public class EntrySearchService(IEntryFileRepository repo) : IEntrySearchService
public Task<IReadOnlyList<EntrySearchResultDto>> SearchEntriesAsync(EntrySearchRequestDto request) public Task<IReadOnlyList<EntrySearchResultDto>> SearchEntriesAsync(EntrySearchRequestDto request)
{ {
ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(request);
if (string.IsNullOrWhiteSpace(request.DataDirectory))
throw new ArgumentException("Data directory is required.", nameof(request.DataDirectory));
var hasQuery = !string.IsNullOrWhiteSpace(request.Query); var hasQuery = !string.IsNullOrWhiteSpace(request.Query);
var query = request.Query?.Trim() ?? ""; var query = request.Query?.Trim() ?? "";
@ -35,7 +33,7 @@ public class EntrySearchService(IEntryFileRepository repo) : IEntrySearchService
if (startDate.HasValue && endDate.HasValue && startDate.Value > endDate.Value) if (startDate.HasValue && endDate.HasValue && startDate.Value > endDate.Value)
throw new ArgumentException("startDate cannot be after endDate."); throw new ArgumentException("startDate cannot be after endDate.");
var currentFiles = _repo.ListMarkdownFiles(request.DataDirectory) var currentFiles = _repo.ListMarkdownFiles()
.OrderBy(Path.GetFileName, StringComparer.Ordinal) .OrderBy(Path.GetFileName, StringComparer.Ordinal)
.ToArray(); .ToArray();

View File

@ -4,12 +4,12 @@ namespace Journal.Core.Services.Entries;
public interface IEntryFileService public interface IEntryFileService
{ {
IReadOnlyList<EntryListItem> ListEntries(string dataDirectory); IReadOnlyList<EntryListItem> ListEntries();
IReadOnlyList<EntryListItem> ListTemplates(string dataDirectory); IReadOnlyList<EntryListItem> ListTemplates();
EntryLoadResult LoadEntry(string filePath); EntryLoadResult LoadEntry(string filePath);
EntryTemplateLoadResult LoadTemplate(string filePath); EntryTemplateLoadResult LoadTemplate(string filePath);
EntrySaveResult SaveEntry(EntrySavePayload payload, string defaultDataDirectory); EntrySaveResult SaveEntry(EntrySavePayload payload);
EntrySaveResult SaveTemplate(EntryTemplateSavePayload payload, string defaultDataDirectory); EntrySaveResult SaveTemplate(EntryTemplateSavePayload payload);
bool DeleteEntry(string filePath); bool DeleteEntry(string filePath);
bool DeleteTemplate(string filePath); bool DeleteTemplate(string filePath);
} }

View File

@ -6,11 +6,12 @@ using Journal.Core.Services.Vault;
namespace Journal.Core.Services.Sidecar; namespace Journal.Core.Services.Sidecar;
public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchService entrySearch, IJournalConfigService config) public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchService entrySearch, IJournalConfigService config, IEntryFileService entryFiles)
{ {
private readonly IVaultStorageService _vaultStorage = vaultStorage; private readonly IVaultStorageService _vaultStorage = vaultStorage;
private readonly IEntrySearchService _entrySearch = entrySearch; private readonly IEntrySearchService _entrySearch = entrySearch;
private readonly IJournalConfigService _config = config; private readonly IJournalConfigService _config = config;
private readonly IEntryFileService _entryFiles = entryFiles;
public async Task<int> RunAsync(string[] args, Entry entry) public async Task<int> RunAsync(string[] args, Entry entry)
{ {
@ -73,25 +74,25 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe
return 2; return 2;
} }
var (vaultDirectory, dataDirectory) = ResolveDirectories(options.VaultDirectory, options.DataDirectory); var vaultDirectory = ResolveVaultDirectory(options.VaultDirectory);
try try
{ {
if (action == "load") if (action == "load")
{ {
var ok = _vaultStorage.LoadAllVaults(password, vaultDirectory, dataDirectory); var ok = _vaultStorage.LoadAllVaults(password, vaultDirectory, ResolveVaultStorageDirectory());
if (!ok) if (!ok)
{ {
Console.Error.WriteLine("Incorrect password."); Console.Error.WriteLine("Incorrect password.");
return 1; return 1;
} }
Console.WriteLine($"Vault loaded. Decrypted files are in {dataDirectory}"); Console.WriteLine("Vault loaded.");
return 0; return 0;
} }
_vaultStorage.RebuildAllVaults(password, vaultDirectory, dataDirectory); _vaultStorage.RebuildAllVaults(password, vaultDirectory, ResolveVaultStorageDirectory());
Console.WriteLine($"Vault saved from decrypted files in {dataDirectory}"); Console.WriteLine("Vault saved.");
return 0; return 0;
} }
catch (Exception ex) catch (Exception ex)
@ -117,21 +118,16 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe
return 2; return 2;
} }
var (_, dataDirectory) = ResolveDirectories(vaultOverride: null, options.DataDirectory); var entryCount = _entryFiles.ListEntries().Count;
var entryCount = Directory.Exists(dataDirectory)
? Directory.GetFiles(dataDirectory, "*.md")
.Count(path => !EntryFileNaming.IsTemplateFileName(Path.GetFileName(path)))
: 0;
if (entryCount == 0) if (entryCount == 0)
{ {
Console.WriteLine("No decrypted journal entries found. Please load the vault first: journal vault load"); Console.WriteLine("No journal entries found. Please load the vault first: journal vault load");
return 0; return 0;
} }
try try
{ {
var request = new EntrySearchRequestDto( var request = new EntrySearchRequestDto(
DataDirectory: dataDirectory,
Query: options.Query, Query: options.Query,
Section: options.Section, Section: options.Section,
StartDate: options.StartDate, StartDate: options.StartDate,
@ -194,9 +190,6 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe
case "--vault-dir": case "--vault-dir":
parsed.VaultDirectory = value; parsed.VaultDirectory = value;
break; break;
case "--data-dir":
parsed.DataDirectory = value;
break;
default: default:
options = parsed; options = parsed;
error = $"Unknown option '{token}'."; error = $"Unknown option '{token}'.";
@ -247,9 +240,6 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe
var value = args[i + 1]; var value = args[i + 1];
switch (token) switch (token)
{ {
case "--data-dir":
parsed.DataDirectory = value;
break;
case "--tag": case "--tag":
case "-t": case "-t":
parsed.Tags.Add(value); parsed.Tags.Add(value);
@ -292,15 +282,22 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe
return true; return true;
} }
private (string VaultDirectory, string DataDirectory) ResolveDirectories(string? vaultOverride, string? dataOverride) private string ResolveVaultDirectory(string? vaultOverride)
{ {
var envVault = Environment.GetEnvironmentVariable("JOURNAL_VAULT_DIR"); var envVault = Environment.GetEnvironmentVariable("JOURNAL_VAULT_DIR");
var envData = Environment.GetEnvironmentVariable("JOURNAL_DATA_DIR");
var defaults = _config.Current; var defaults = _config.Current;
var vault = FirstNonEmpty(vaultOverride, envVault) ?? defaults.VaultDirectory; var vault = FirstNonEmpty(vaultOverride, envVault) ?? defaults.VaultDirectory;
var data = FirstNonEmpty(dataOverride, envData) ?? defaults.DataDirectory; return Path.GetFullPath(vault);
return (Path.GetFullPath(vault), Path.GetFullPath(data)); }
private string ResolveVaultStorageDirectory()
{
var dbDirOverride = Environment.GetEnvironmentVariable("JOURNAL_DATABASE_DIR");
if (!string.IsNullOrWhiteSpace(dbDirOverride))
return Path.GetFullPath(dbDirOverride);
return Path.GetFullPath(Path.Combine(_config.Current.VaultDirectory, "db"));
} }
private static string? FirstNonEmpty(params string?[] values) => private static string? FirstNonEmpty(params string?[] values) =>
@ -345,35 +342,33 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe
{ {
Console.WriteLine("Usage:"); Console.WriteLine("Usage:");
Console.WriteLine(" Journal.Sidecar # sidecar stdin/stdout mode"); Console.WriteLine(" Journal.Sidecar # sidecar stdin/stdout mode");
Console.WriteLine(" Journal.Sidecar vault load [--password <value>] [--vault-dir <path>] [--data-dir <path>]"); Console.WriteLine(" Journal.Sidecar vault load [--password <value>] [--vault-dir <path>]");
Console.WriteLine(" Journal.Sidecar vault save [--password <value>] [--vault-dir <path>] [--data-dir <path>]"); Console.WriteLine(" Journal.Sidecar vault save [--password <value>] [--vault-dir <path>]");
Console.WriteLine(" Journal.Sidecar search [query] [--tag <value>] [--type <value>] [--start-date <yyyy-MM-dd>] [--end-date <yyyy-MM-dd>] [--section <title>] [--checked <text>] [--unchecked <text>] [--data-dir <path>]"); Console.WriteLine(" Journal.Sidecar search [query] [--tag <value>] [--type <value>] [--start-date <yyyy-MM-dd>] [--end-date <yyyy-MM-dd>] [--section <title>] [--checked <text>] [--unchecked <text>]");
} }
private static void PrintVaultUsage() private static void PrintVaultUsage()
{ {
Console.WriteLine("Vault usage:"); Console.WriteLine("Vault usage:");
Console.WriteLine(" Journal.Sidecar vault load [--password <value>] [--vault-dir <path>] [--data-dir <path>]"); Console.WriteLine(" Journal.Sidecar vault load [--password <value>] [--vault-dir <path>]");
Console.WriteLine(" Journal.Sidecar vault save [--password <value>] [--vault-dir <path>] [--data-dir <path>]"); Console.WriteLine(" Journal.Sidecar vault save [--password <value>] [--vault-dir <path>]");
} }
private static void PrintSearchUsage() private static void PrintSearchUsage()
{ {
Console.WriteLine("Search usage:"); Console.WriteLine("Search usage:");
Console.WriteLine(" Journal.Sidecar search [query] [--tag <value>] [--type <value>] [--start-date <yyyy-MM-dd>] [--end-date <yyyy-MM-dd>] [--section <title>] [--checked <text>] [--unchecked <text>] [--data-dir <path>]"); Console.WriteLine(" Journal.Sidecar search [query] [--tag <value>] [--type <value>] [--start-date <yyyy-MM-dd>] [--end-date <yyyy-MM-dd>] [--section <title>] [--checked <text>] [--unchecked <text>]");
} }
private sealed class VaultOptions private sealed class VaultOptions
{ {
public string? Password { get; set; } public string? Password { get; set; }
public string? VaultDirectory { get; set; } public string? VaultDirectory { get; set; }
public string? DataDirectory { get; set; }
} }
private sealed class SearchOptions private sealed class SearchOptions
{ {
public string? Query { get; set; } public string? Query { get; set; }
public string? DataDirectory { get; set; }
public string? StartDate { get; set; } public string? StartDate { get; set; }
public string? EndDate { get; set; } public string? EndDate { get; set; }
public string? Section { get; set; } public string? Section { get; set; }

View File

@ -1,11 +1,13 @@
using System.Diagnostics; using System.Diagnostics;
using System.Security.Cryptography; using System.Security.Cryptography;
using Journal.Core.Services.Database;
namespace Journal.Core.Services.Vault; namespace Journal.Core.Services.Vault;
public class VaultStorageService(IVaultCryptoService crypto) : IVaultStorageService public class VaultStorageService(IVaultCryptoService crypto, IJournalDatabaseService database) : IVaultStorageService
{ {
private readonly IVaultCryptoService _crypto = crypto; private readonly IVaultCryptoService _crypto = crypto;
private readonly IJournalDatabaseService _database = database;
private readonly object _vaultIoLock = new(); private readonly object _vaultIoLock = new();
private const string DatabaseVaultPrefix = "_db_"; private const string DatabaseVaultPrefix = "_db_";
@ -19,11 +21,12 @@ public class VaultStorageService(IVaultCryptoService crypto) : IVaultStorageServ
lock (_vaultIoLock) lock (_vaultIoLock)
{ {
Directory.CreateDirectory(dataDirectory); var dbDirectory = GetDatabaseDirectory();
Directory.CreateDirectory(dbDirectory);
if (!Directory.Exists(vaultDirectory)) if (!Directory.Exists(vaultDirectory))
return true; return true;
return RestoreDatabaseVaults(password, vaultDirectory, dataDirectory); return RestoreDatabaseVaults(password, vaultDirectory, dbDirectory);
} }
} }
@ -34,10 +37,11 @@ public class VaultStorageService(IVaultCryptoService crypto) : IVaultStorageServ
lock (_vaultIoLock) lock (_vaultIoLock)
{ {
Directory.CreateDirectory(vaultDirectory); Directory.CreateDirectory(vaultDirectory);
if (!Directory.Exists(dataDirectory)) var dbDirectory = GetDatabaseDirectory();
if (!Directory.Exists(dbDirectory))
return false; return false;
SaveDatabaseVaults(password, vaultDirectory, dataDirectory); SaveDatabaseVaults(password, vaultDirectory, dbDirectory);
return true; return true;
} }
} }
@ -49,10 +53,11 @@ public class VaultStorageService(IVaultCryptoService crypto) : IVaultStorageServ
lock (_vaultIoLock) lock (_vaultIoLock)
{ {
Directory.CreateDirectory(vaultDirectory); Directory.CreateDirectory(vaultDirectory);
if (!Directory.Exists(dataDirectory)) var dbDirectory = GetDatabaseDirectory();
if (!Directory.Exists(dbDirectory))
return; return;
SaveDatabaseVaults(password, vaultDirectory, dataDirectory); SaveDatabaseVaults(password, vaultDirectory, dbDirectory);
} }
} }
@ -63,8 +68,12 @@ public class VaultStorageService(IVaultCryptoService crypto) : IVaultStorageServ
lock (_vaultIoLock) lock (_vaultIoLock)
{ {
DeleteDirectoryWithRetries(dataDirectory); var normalizedDataDir = Path.GetFullPath(dataDirectory);
Directory.CreateDirectory(dataDirectory); var dbDirectory = GetDatabaseDirectory();
if (string.Equals(normalizedDataDir, dbDirectory, StringComparison.OrdinalIgnoreCase))
return;
DeleteDirectoryWithRetries(normalizedDataDir);
} }
} }
@ -135,6 +144,15 @@ public class VaultStorageService(IVaultCryptoService crypto) : IVaultStorageServ
throw new ArgumentException("Data directory is required.", nameof(dataDirectory)); throw new ArgumentException("Data directory is required.", nameof(dataDirectory));
} }
private string GetDatabaseDirectory()
{
var dbPath = _database.GetDatabasePath();
var directory = Path.GetDirectoryName(dbPath);
return string.IsNullOrWhiteSpace(directory)
? Path.GetFullPath(".")
: Path.GetFullPath(directory);
}
private static void DeleteDirectoryWithRetries(string dataDirectory, int retries = 5, int delayMs = 200) private static void DeleteDirectoryWithRetries(string dataDirectory, int retries = 5, int delayMs = 200)
{ {
if (!Directory.Exists(dataDirectory)) if (!Directory.Exists(dataDirectory))