journal/Journal.Core/Services/Vault/VaultStorageService.cs

163 lines
5.9 KiB
C#

using System.Diagnostics;
using System.Security.Cryptography;
using Journal.Core.Services.Database;
namespace Journal.Core.Services.Vault;
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_";
private const string DatabaseVaultSuffix = ".vault";
public bool LoadAllVaults(string password, string vaultDirectory, string dataDirectory)
{
EnsureRequiredArguments(password, vaultDirectory, dataDirectory);
lock (_vaultIoLock)
{
var dbDirectory = GetDatabaseDirectory();
Directory.CreateDirectory(dbDirectory);
if (!Directory.Exists(vaultDirectory))
return true;
return RestoreDatabaseVaults(password, vaultDirectory, dbDirectory);
}
}
public void RebuildAllVaults(string password, string vaultDirectory, string dataDirectory)
{
EnsureRequiredArguments(password, vaultDirectory, dataDirectory);
lock (_vaultIoLock)
{
Directory.CreateDirectory(vaultDirectory);
var dbDirectory = GetDatabaseDirectory();
if (!Directory.Exists(dbDirectory))
return;
SaveDatabaseVaults(password, vaultDirectory, dbDirectory);
}
}
public void ClearDataDirectory(string dataDirectory)
{
if (string.IsNullOrWhiteSpace(dataDirectory))
throw new ArgumentException("Data directory is required.", nameof(dataDirectory));
lock (_vaultIoLock)
{
var normalizedDataDir = Path.GetFullPath(dataDirectory);
var dbDirectory = GetDatabaseDirectory();
if (string.Equals(normalizedDataDir, dbDirectory, StringComparison.OrdinalIgnoreCase))
return;
DeleteDirectoryWithRetries(normalizedDataDir);
}
}
private void SaveDatabaseVaults(string password, string vaultDirectory, string dataDirectory)
{
var dbFiles = Directory.GetFiles(dataDirectory, "*.db");
foreach (var dbPath in dbFiles)
{
try
{
var dbFileName = Path.GetFileName(dbPath);
var vaultFileName = $"{DatabaseVaultPrefix}{dbFileName}{DatabaseVaultSuffix}";
var vaultPath = Path.Combine(vaultDirectory, vaultFileName);
var dbBytes = File.ReadAllBytes(dbPath);
var encrypted = _crypto.EncryptData(dbBytes, password);
File.WriteAllBytes(vaultPath, encrypted);
}
catch (Exception ex)
{
Debug.WriteLine($"[VaultStorageService] Failed to save database vault for {Path.GetFileName(dbPath)}: {ex.Message}");
}
}
}
private bool RestoreDatabaseVaults(string password, string vaultDirectory, string dataDirectory)
{
var dbVaultFiles = Directory.GetFiles(vaultDirectory, $"{DatabaseVaultPrefix}*{DatabaseVaultSuffix}");
if (dbVaultFiles.Length == 0)
return true;
var anyRestored = false;
foreach (var vaultFile in dbVaultFiles)
{
try
{
var vaultFileName = Path.GetFileName(vaultFile);
var dbFileName = vaultFileName[DatabaseVaultPrefix.Length..^DatabaseVaultSuffix.Length];
if (string.IsNullOrWhiteSpace(dbFileName))
continue;
var encrypted = File.ReadAllBytes(vaultFile);
var dbBytes = _crypto.DecryptData(encrypted, password);
var targetPath = Path.Combine(dataDirectory, dbFileName);
File.WriteAllBytes(targetPath, dbBytes);
anyRestored = true;
}
catch (CryptographicException)
{
Debug.WriteLine($"[VaultStorageService] Database vault decryption failed for {Path.GetFileName(vaultFile)} (likely wrong password)");
}
catch (Exception ex)
{
Debug.WriteLine($"[VaultStorageService] Failed to restore database vault {Path.GetFileName(vaultFile)}: {ex.Message}");
}
}
return anyRestored;
}
private static void EnsureRequiredArguments(string password, string vaultDirectory, string dataDirectory)
{
if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("Password cannot be empty.", nameof(password));
if (string.IsNullOrWhiteSpace(vaultDirectory))
throw new ArgumentException("Vault directory is required.", nameof(vaultDirectory));
if (string.IsNullOrWhiteSpace(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)
{
if (!Directory.Exists(dataDirectory))
return;
for (var attempt = 0; attempt < retries; attempt++)
{
try
{
Directory.Delete(dataDirectory, recursive: true);
return;
}
catch (IOException) when (attempt < retries - 1)
{
Thread.Sleep(delayMs);
}
catch (UnauthorizedAccessException) when (attempt < retries - 1)
{
Thread.Sleep(delayMs);
}
}
Directory.Delete(dataDirectory, recursive: true);
}
}