diff --git a/Journal.Core/Dtos/CommandDtos.cs b/Journal.Core/Dtos/CommandDtos.cs index e84f832..9ca24fb 100644 --- a/Journal.Core/Dtos/CommandDtos.cs +++ b/Journal.Core/Dtos/CommandDtos.cs @@ -1,20 +1,20 @@ namespace Journal.Core.Dtos; internal sealed record VaultInitializePayload(string Password, string VaultDirectory); -internal sealed record VaultPayload(string Password, string VaultDirectory, string DataDirectory, string? NowUtc = null); -internal sealed record ClearDataPayload(string DataDirectory); -internal sealed record EntryListPayload(string? DataDirectory = null); +internal sealed record VaultPayload(string Password, string VaultDirectory, string? NowUtc = null); +internal sealed record ClearDataPayload(); +internal sealed record EntryListPayload(); internal sealed record EntryLoadPayload(string FilePath); 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 EntryLoadResult(string FileName, string FilePath, JournalEntryDto Entry); public sealed record EntrySaveResult(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 EntryTemplateDeletePayload(string FilePath); 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 AiSummarizeEntryPayload(string Content, string? FileStem = null); internal sealed record AiSummarizeAllPayload(List? Entries); @@ -30,7 +30,6 @@ internal sealed record SpeechTranscribePayload( int? SimulateDelayMs = null, int? Simulate_Delay_Ms = null); internal sealed record SearchEntriesPayload( - string DataDirectory, string? Query = null, string? Section = null, string? StartDate = null, diff --git a/Journal.Core/Dtos/EntrySearchDtos.cs b/Journal.Core/Dtos/EntrySearchDtos.cs index 7330901..dbc8047 100644 --- a/Journal.Core/Dtos/EntrySearchDtos.cs +++ b/Journal.Core/Dtos/EntrySearchDtos.cs @@ -1,7 +1,6 @@ namespace Journal.Core.Dtos; public sealed record EntrySearchRequestDto( - string DataDirectory, string? Query = null, string? Section = null, string? StartDate = null, diff --git a/Journal.Core/Entry.cs b/Journal.Core/Entry.cs index 6430c2f..34482f8 100644 --- a/Journal.Core/Entry.cs +++ b/Journal.Core/Entry.cs @@ -214,10 +214,9 @@ public class Entry( break; case "search.entries": var searchPayload = DeserializePayload(cmd.Payload); - if (searchPayload is null || string.IsNullOrWhiteSpace(searchPayload.DataDirectory)) + if (searchPayload is null) return Error("Missing or invalid payload"); var searchRequest = new EntrySearchRequestDto( - DataDirectory: searchPayload.DataDirectory, Query: searchPayload.Query, Section: searchPayload.Section, StartDate: searchPayload.StartDate, @@ -229,18 +228,12 @@ public class Entry( result = await _entrySearch.SearchEntriesAsync(searchRequest); break; case "entries.list": - var listPayload = DeserializePayload(cmd.Payload); - var listDataDirectory = !string.IsNullOrWhiteSpace(listPayload?.DataDirectory) - ? listPayload.DataDirectory - : _config.Current.DataDirectory; - result = _entryFiles.ListEntries(listDataDirectory); + _ = DeserializePayload(cmd.Payload); + result = _entryFiles.ListEntries(); break; case "templates.list": - var templateListPayload = DeserializePayload(cmd.Payload); - var templateListDirectory = !string.IsNullOrWhiteSpace(templateListPayload?.DataDirectory) - ? templateListPayload.DataDirectory - : _config.Current.DataDirectory; - result = _entryFiles.ListTemplates(templateListDirectory); + _ = DeserializePayload(cmd.Payload); + result = _entryFiles.ListTemplates(); break; case "entries.load": var loadEntryPayload = DeserializePayload(cmd.Payload); @@ -258,13 +251,13 @@ public class Entry( var saveEntryPayload = DeserializePayload(cmd.Payload); if (saveEntryPayload is null || string.IsNullOrWhiteSpace(saveEntryPayload.Content)) return Error("Missing or invalid payload"); - result = _entryFiles.SaveEntry(saveEntryPayload, _config.Current.DataDirectory); + result = _entryFiles.SaveEntry(saveEntryPayload); break; case "templates.save": var saveTemplatePayload = DeserializePayload(cmd.Payload); if (saveTemplatePayload is null || string.IsNullOrWhiteSpace(saveTemplatePayload.Name)) return Error("Missing or invalid payload"); - result = _entryFiles.SaveTemplate(saveTemplatePayload, _config.Current.DataDirectory); + result = _entryFiles.SaveTemplate(saveTemplatePayload); break; case "entries.delete": var deleteEntryPayload = DeserializePayload(cmd.Payload); @@ -343,9 +336,10 @@ public class Entry( var loadPayload = DeserializePayload(cmd.Payload); if (loadPayload is null) 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) - _databaseSession.SetPassword(loadPayload.Password, loadPayload.DataDirectory); + _databaseSession.SetPassword(loadPayload.Password); result = loaded; break; case "vault.save_current_month": @@ -355,7 +349,7 @@ public class Entry( result = _vaultStorage.SaveCurrentMonthVault( saveCurrentPayload.Password, saveCurrentPayload.VaultDirectory, - saveCurrentPayload.DataDirectory, + ResolveVaultStorageDirectory(), ParseNowOrDefault(saveCurrentPayload.NowUtc)); break; case "vault.rebuild_all": @@ -363,16 +357,16 @@ public class Entry( if (rebuildPayload is null) return Error("Missing or invalid payload"); _databaseSession.CloseConnection(); - _vaultStorage.RebuildAllVaults(rebuildPayload.Password, rebuildPayload.VaultDirectory, rebuildPayload.DataDirectory); + _vaultStorage.RebuildAllVaults(rebuildPayload.Password, rebuildPayload.VaultDirectory, ResolveVaultStorageDirectory()); result = true; break; case "vault.clear_data_directory": var clearPayload = DeserializePayload(cmd.Payload); - if (clearPayload is null || string.IsNullOrWhiteSpace(clearPayload.DataDirectory)) + if (clearPayload is null) return Error("Missing or invalid payload"); if (_databaseSession is IDisposable disposableSession) disposableSession.Dispose(); - _vaultStorage.ClearDataDirectory(clearPayload.DataDirectory); + _vaultStorage.ClearDataDirectory(ResolveVaultStorageDirectory()); result = true; break; case "db.status": @@ -393,7 +387,7 @@ public class Entry( if (dbHydratePayload is null || string.IsNullOrWhiteSpace(dbHydratePayload.Password)) return Error("Missing or invalid payload"); result = _database.HydrateWorkspace(dbHydratePayload.Password, dbHydratePayload.DataDirectory); - _databaseSession.SetPassword(dbHydratePayload.Password, dbHydratePayload.DataDirectory); + _databaseSession.SetPassword(dbHydratePayload.Password); break; default: CommandLogger.LogFailure(action, correlationId, "unknown_action"); @@ -473,22 +467,27 @@ public class Entry( if (!VaultSyncActions.Contains(action)) return; - if (!_databaseSession.TryGetSession(out var password, out var sessionDataDirectory)) + if (!_databaseSession.TryGetSession(out var password, out _)) return; try { var config = _config.Current; - var dataDirectory = string.IsNullOrWhiteSpace(sessionDataDirectory) - ? config.DataDirectory - : sessionDataDirectory; - _databaseSession.CloseConnection(); - _vaultStorage.RebuildAllVaults(password, config.VaultDirectory, dataDirectory); + _vaultStorage.RebuildAllVaults(password, config.VaultDirectory, ResolveVaultStorageDirectory()); } catch (Exception ex) { 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); + } } diff --git a/Journal.Core/Repositories/DiskEntryFileRepository.cs b/Journal.Core/Repositories/DiskEntryFileRepository.cs deleted file mode 100644 index d24c4d8..0000000 --- a/Journal.Core/Repositories/DiskEntryFileRepository.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Journal.Core.Repositories; - -public sealed class DiskEntryFileRepository : IEntryFileRepository -{ - public IReadOnlyList 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); -} diff --git a/Journal.Core/Repositories/IEntryFileRepository.cs b/Journal.Core/Repositories/IEntryFileRepository.cs index ac93437..6057cea 100644 --- a/Journal.Core/Repositories/IEntryFileRepository.cs +++ b/Journal.Core/Repositories/IEntryFileRepository.cs @@ -2,7 +2,7 @@ namespace Journal.Core.Repositories; public interface IEntryFileRepository { - IReadOnlyList ListMarkdownFiles(string dataDirectory); + IReadOnlyList ListMarkdownFiles(); string ReadFile(string filePath); void WriteFile(string filePath, string content); void AppendFile(string filePath, string content); diff --git a/Journal.Core/Repositories/SqliteEntryFileRepository.cs b/Journal.Core/Repositories/SqliteEntryFileRepository.cs index f6d8ae6..25067d0 100644 --- a/Journal.Core/Repositories/SqliteEntryFileRepository.cs +++ b/Journal.Core/Repositories/SqliteEntryFileRepository.cs @@ -9,7 +9,7 @@ public sealed class SqliteEntryFileRepository(IDatabaseSessionService session) : private const string TemplatePrefix = "db://template/"; private readonly IDatabaseSessionService _session = session; - public IReadOnlyList ListMarkdownFiles(string dataDirectory) + public IReadOnlyList ListMarkdownFiles() { var conn = _session.GetConnection(); using var cmd = conn.CreateCommand(); diff --git a/Journal.Core/Services/Database/JournalDatabaseService.cs b/Journal.Core/Services/Database/JournalDatabaseService.cs index a2c33dc..218410d 100644 --- a/Journal.Core/Services/Database/JournalDatabaseService.cs +++ b/Journal.Core/Services/Database/JournalDatabaseService.cs @@ -20,9 +20,7 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour public string GetDatabasePath(string? dataDirectory = null) { - var directory = string.IsNullOrWhiteSpace(dataDirectory) - ? _config.Current.DataDirectory - : dataDirectory; + var directory = ResolveDatabaseDirectory(); Directory.CreateDirectory(directory); 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) { - var directory = string.IsNullOrWhiteSpace(dataDirectory) - ? _config.Current.DataDirectory - : dataDirectory; + var directory = ResolveDatabaseDirectory(); Directory.CreateDirectory(directory); 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) { - var directory = string.IsNullOrWhiteSpace(dataDirectory) - ? _config.Current.DataDirectory - : dataDirectory; + var directory = ResolveDatabaseDirectory(); Directory.CreateDirectory(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}"); } } + + 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")); + } } diff --git a/Journal.Core/Services/Entries/EntryFileService.cs b/Journal.Core/Services/Entries/EntryFileService.cs index 450e859..6ac0b37 100644 --- a/Journal.Core/Services/Entries/EntryFileService.cs +++ b/Journal.Core/Services/Entries/EntryFileService.cs @@ -7,18 +7,18 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ { private readonly IEntryFileRepository _repo = repo ?? throw new ArgumentNullException(nameof(repo)); - public IReadOnlyList ListEntries(string dataDirectory) + public IReadOnlyList ListEntries() { - return [.. _repo.ListMarkdownFiles(dataDirectory) + return [.. _repo.ListMarkdownFiles() .Where(path => !EntryFileNaming.IsTemplateFileName(_repo.GetFileName(path))) .Select(path => new EntryListItem( FileName: _repo.GetFileName(path), FilePath: _repo.GetFullPath(path)))]; } - public IReadOnlyList ListTemplates(string dataDirectory) + public IReadOnlyList ListTemplates() { - return [.. _repo.ListMarkdownFiles(dataDirectory) + return [.. _repo.ListMarkdownFiles() .Where(path => EntryFileNaming.IsTemplateFileName(_repo.GetFileName(path))) .Select(path => new EntryListItem( FileName: _repo.GetFileName(path), @@ -55,9 +55,9 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ 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 sanitizedContent = HtmlSanitizer.StripRichHtml(payload.Content ?? ""); _repo.EnsureDirectory(targetPath); @@ -93,16 +93,13 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ return new EntrySaveResult(targetPath); } - public EntrySaveResult SaveTemplate(EntryTemplateSavePayload payload, string defaultDataDirectory) + public EntrySaveResult SaveTemplate(EntryTemplateSavePayload payload) { ArgumentNullException.ThrowIfNull(payload); if (string.IsNullOrWhiteSpace(payload.Name)) throw new ArgumentException("Template name is required."); - var directory = string.IsNullOrWhiteSpace(payload.DataDirectory) - ? defaultDataDirectory - : payload.DataDirectory; - var targetPath = ResolveTemplatePath(payload.FilePath, payload.Name, directory); + var targetPath = ResolveTemplatePath(payload.FilePath, payload.Name); var fileName = _repo.GetFileName(targetPath); if (!EntryFileNaming.IsTemplateFileName(fileName)) throw new ArgumentException("Template file name must end with .template.md."); @@ -136,7 +133,7 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ return true; } - private string ResolveTargetPath(string? filePath, string? fileName, string defaultDataDirectory) + private string ResolveTargetPath(string? filePath, string? fileName) { if (!string.IsNullOrWhiteSpace(filePath)) return _repo.GetFullPath(filePath); @@ -145,16 +142,16 @@ public sealed class EntryFileService(IEntryFileRepository repo) : IEntryFileServ ? SanitizeFileName(fileName) : $"{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)) return _repo.GetFullPath(filePath); 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) diff --git a/Journal.Core/Services/Entries/EntrySearchService.cs b/Journal.Core/Services/Entries/EntrySearchService.cs index b32762e..1422765 100644 --- a/Journal.Core/Services/Entries/EntrySearchService.cs +++ b/Journal.Core/Services/Entries/EntrySearchService.cs @@ -15,8 +15,6 @@ public class EntrySearchService(IEntryFileRepository repo) : IEntrySearchService public Task> SearchEntriesAsync(EntrySearchRequestDto 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 query = request.Query?.Trim() ?? ""; @@ -35,7 +33,7 @@ public class EntrySearchService(IEntryFileRepository repo) : IEntrySearchService if (startDate.HasValue && endDate.HasValue && startDate.Value > endDate.Value) throw new ArgumentException("startDate cannot be after endDate."); - var currentFiles = _repo.ListMarkdownFiles(request.DataDirectory) + var currentFiles = _repo.ListMarkdownFiles() .OrderBy(Path.GetFileName, StringComparer.Ordinal) .ToArray(); diff --git a/Journal.Core/Services/Entries/IEntryFileService.cs b/Journal.Core/Services/Entries/IEntryFileService.cs index 3983272..cbf5645 100644 --- a/Journal.Core/Services/Entries/IEntryFileService.cs +++ b/Journal.Core/Services/Entries/IEntryFileService.cs @@ -4,12 +4,12 @@ namespace Journal.Core.Services.Entries; public interface IEntryFileService { - IReadOnlyList ListEntries(string dataDirectory); - IReadOnlyList ListTemplates(string dataDirectory); + IReadOnlyList ListEntries(); + IReadOnlyList ListTemplates(); EntryLoadResult LoadEntry(string filePath); EntryTemplateLoadResult LoadTemplate(string filePath); - EntrySaveResult SaveEntry(EntrySavePayload payload, string defaultDataDirectory); - EntrySaveResult SaveTemplate(EntryTemplateSavePayload payload, string defaultDataDirectory); + EntrySaveResult SaveEntry(EntrySavePayload payload); + EntrySaveResult SaveTemplate(EntryTemplateSavePayload payload); bool DeleteEntry(string filePath); bool DeleteTemplate(string filePath); } diff --git a/Journal.Core/Services/Sidecar/SidecarCli.cs b/Journal.Core/Services/Sidecar/SidecarCli.cs index f7d82fd..517fc06 100644 --- a/Journal.Core/Services/Sidecar/SidecarCli.cs +++ b/Journal.Core/Services/Sidecar/SidecarCli.cs @@ -6,11 +6,12 @@ using Journal.Core.Services.Vault; 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 IEntrySearchService _entrySearch = entrySearch; private readonly IJournalConfigService _config = config; + private readonly IEntryFileService _entryFiles = entryFiles; public async Task RunAsync(string[] args, Entry entry) { @@ -73,25 +74,25 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe return 2; } - var (vaultDirectory, dataDirectory) = ResolveDirectories(options.VaultDirectory, options.DataDirectory); + var vaultDirectory = ResolveVaultDirectory(options.VaultDirectory); try { if (action == "load") { - var ok = _vaultStorage.LoadAllVaults(password, vaultDirectory, dataDirectory); + var ok = _vaultStorage.LoadAllVaults(password, vaultDirectory, ResolveVaultStorageDirectory()); if (!ok) { Console.Error.WriteLine("Incorrect password."); return 1; } - Console.WriteLine($"Vault loaded. Decrypted files are in {dataDirectory}"); + Console.WriteLine("Vault loaded."); return 0; } - _vaultStorage.RebuildAllVaults(password, vaultDirectory, dataDirectory); - Console.WriteLine($"Vault saved from decrypted files in {dataDirectory}"); + _vaultStorage.RebuildAllVaults(password, vaultDirectory, ResolveVaultStorageDirectory()); + Console.WriteLine("Vault saved."); return 0; } catch (Exception ex) @@ -117,21 +118,16 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe return 2; } - var (_, dataDirectory) = ResolveDirectories(vaultOverride: null, options.DataDirectory); - var entryCount = Directory.Exists(dataDirectory) - ? Directory.GetFiles(dataDirectory, "*.md") - .Count(path => !EntryFileNaming.IsTemplateFileName(Path.GetFileName(path))) - : 0; + var entryCount = _entryFiles.ListEntries().Count; 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; } try { var request = new EntrySearchRequestDto( - DataDirectory: dataDirectory, Query: options.Query, Section: options.Section, StartDate: options.StartDate, @@ -194,9 +190,6 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe case "--vault-dir": parsed.VaultDirectory = value; break; - case "--data-dir": - parsed.DataDirectory = value; - break; default: options = parsed; error = $"Unknown option '{token}'."; @@ -247,9 +240,6 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe var value = args[i + 1]; switch (token) { - case "--data-dir": - parsed.DataDirectory = value; - break; case "--tag": case "-t": parsed.Tags.Add(value); @@ -292,15 +282,22 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe 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 envData = Environment.GetEnvironmentVariable("JOURNAL_DATA_DIR"); var defaults = _config.Current; var vault = FirstNonEmpty(vaultOverride, envVault) ?? defaults.VaultDirectory; - var data = FirstNonEmpty(dataOverride, envData) ?? defaults.DataDirectory; - return (Path.GetFullPath(vault), Path.GetFullPath(data)); + return Path.GetFullPath(vault); + } + + 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) => @@ -345,35 +342,33 @@ public sealed class SidecarCli(IVaultStorageService vaultStorage, IEntrySearchSe { Console.WriteLine("Usage:"); Console.WriteLine(" Journal.Sidecar # sidecar stdin/stdout mode"); - Console.WriteLine(" Journal.Sidecar vault load [--password ] [--vault-dir ] [--data-dir ]"); - Console.WriteLine(" Journal.Sidecar vault save [--password ] [--vault-dir ] [--data-dir ]"); - Console.WriteLine(" Journal.Sidecar search [query] [--tag ] [--type ] [--start-date ] [--end-date ] [--section ] [--checked <text>] [--unchecked <text>] [--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>]"); + 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() { Console.WriteLine("Vault usage:"); - Console.WriteLine(" Journal.Sidecar vault load [--password <value>] [--vault-dir <path>] [--data-dir <path>]"); - Console.WriteLine(" Journal.Sidecar vault save [--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>]"); } private static void PrintSearchUsage() { 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 { public string? Password { get; set; } public string? VaultDirectory { get; set; } - public string? DataDirectory { get; set; } } private sealed class SearchOptions { public string? Query { get; set; } - public string? DataDirectory { get; set; } public string? StartDate { get; set; } public string? EndDate { get; set; } public string? Section { get; set; } diff --git a/Journal.Core/Services/Vault/VaultStorageService.cs b/Journal.Core/Services/Vault/VaultStorageService.cs index 36a091f..b220dc0 100644 --- a/Journal.Core/Services/Vault/VaultStorageService.cs +++ b/Journal.Core/Services/Vault/VaultStorageService.cs @@ -1,11 +1,13 @@ using System.Diagnostics; using System.Security.Cryptography; +using Journal.Core.Services.Database; 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 IJournalDatabaseService _database = database; private readonly object _vaultIoLock = new(); private const string DatabaseVaultPrefix = "_db_"; @@ -19,11 +21,12 @@ public class VaultStorageService(IVaultCryptoService crypto) : IVaultStorageServ lock (_vaultIoLock) { - Directory.CreateDirectory(dataDirectory); + var dbDirectory = GetDatabaseDirectory(); + Directory.CreateDirectory(dbDirectory); if (!Directory.Exists(vaultDirectory)) return true; - return RestoreDatabaseVaults(password, vaultDirectory, dataDirectory); + return RestoreDatabaseVaults(password, vaultDirectory, dbDirectory); } } @@ -34,10 +37,11 @@ public class VaultStorageService(IVaultCryptoService crypto) : IVaultStorageServ lock (_vaultIoLock) { Directory.CreateDirectory(vaultDirectory); - if (!Directory.Exists(dataDirectory)) + var dbDirectory = GetDatabaseDirectory(); + if (!Directory.Exists(dbDirectory)) return false; - SaveDatabaseVaults(password, vaultDirectory, dataDirectory); + SaveDatabaseVaults(password, vaultDirectory, dbDirectory); return true; } } @@ -49,10 +53,11 @@ public class VaultStorageService(IVaultCryptoService crypto) : IVaultStorageServ lock (_vaultIoLock) { Directory.CreateDirectory(vaultDirectory); - if (!Directory.Exists(dataDirectory)) + var dbDirectory = GetDatabaseDirectory(); + if (!Directory.Exists(dbDirectory)) return; - SaveDatabaseVaults(password, vaultDirectory, dataDirectory); + SaveDatabaseVaults(password, vaultDirectory, dbDirectory); } } @@ -63,8 +68,12 @@ public class VaultStorageService(IVaultCryptoService crypto) : IVaultStorageServ lock (_vaultIoLock) { - DeleteDirectoryWithRetries(dataDirectory); - Directory.CreateDirectory(dataDirectory); + var normalizedDataDir = Path.GetFullPath(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)); } + 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) { if (!Directory.Exists(dataDirectory))