using DeepDrftContent.FileDatabase.Models; using DeepDrftContent.FileDatabase.Utils; using Microsoft.Extensions.Logging; namespace DeepDrftContent.FileDatabase.Services; /// /// Main file database class that orchestrates multiple media vaults. /// Includes file watching for automatic index reloading when modified by external processes. /// public class FileDatabase : DirectoryIndexDirectory, IDisposable { private readonly StructuralMap _vaults; private readonly IndexWatcher _indexWatcher; private readonly IndexFactoryService _indexFactory; private readonly ILogger _logger; private bool _disposed; /// /// Factory method to create a FileDatabase instance /// public static async Task FromAsync(string rootPath, ILogger? logger = null) { var factoryService = new IndexFactoryService(); var rootIndex = await factoryService.LoadOrCreateDirectoryIndexAsync(rootPath); if (rootIndex != null) { var db = new FileDatabase(rootPath, rootIndex, factoryService, logger); await db.InitVaultsAsync(); return db; } return null; } private FileDatabase(string rootPath, IDirectoryIndex index, IndexFactoryService indexFactory, ILogger? logger = null) : base(rootPath, index) { _vaults = new StructuralMap(); _indexWatcher = new IndexWatcher(); _indexFactory = indexFactory; _logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; } /// /// Initializes all vaults found in the index /// private async Task InitVaultsAsync() { foreach (var vaultId in GetIndexEntries()) { var vaultType = await GetVaultTypeFromIndex(vaultId); if (vaultType.HasValue) { await InitVaultAsync(vaultId, vaultType.Value); } } } /// /// Initializes a specific vault and sets up file watching for its index /// private async Task InitVaultAsync(string vaultId, MediaVaultType vaultType) { var path = Path.Combine(RootPath, vaultId); var directoryVault = await MediaVaultFactory.From(path, vaultType, _indexFactory); if (directoryVault != null) { _vaults.Set(vaultId, directoryVault); // Watch the vault's index file for external modifications _indexWatcher.Watch(path, () => { // Reload the index asynchronously when file changes _ = directoryVault.ReloadIndexAsync(); }); } } /// /// Gets vault type from the vault's index file /// private async Task GetVaultTypeFromIndex(string vaultId) { try { var vaultPath = Path.Combine(RootPath, vaultId); var index = await _indexFactory.LoadIndexAsync(IndexType.Vault, vaultPath); if (index is VaultIndex vaultIndex) { return vaultIndex.VaultType; } } catch { // If we can't load the index, we can't determine the vault type // This might happen for legacy vaults or corrupted indexes } return null; } /// /// Checks if a vault exists for the given vault ID /// public bool HasVault(string vaultId) { return _vaults.Has(vaultId); } /// /// Gets a vault by vault ID /// public MediaVault? GetVault(string vaultId) { return HasVault(vaultId) ? _vaults.Get(vaultId) : null; } /// /// Creates a new vault. Propagates exceptions to the caller — vault creation failure is not /// silently swallowable because a partially-created vault would leave the index inconsistent. /// public async Task CreateVaultAsync(string vaultId, MediaVaultType vaultType) { var path = Path.Combine(RootPath, vaultId); var directoryVault = await MediaVaultFactory.From(path, vaultType, _indexFactory); if (directoryVault != null) { _vaults.Set(vaultId, directoryVault); await AddToIndexAsync(vaultId); } } /// /// Loads a resource from a specific vault (MediaVaultType inferred from T) /// public async Task LoadResourceAsync(string vaultId, string entryId) where T : FileBinary { try { var vault = _vaults.Get(vaultId); if (vault != null) { return await vault.GetEntryAsync(entryId); } } catch { // Swallow exceptions and return null, matching TypeScript behavior } return null; } /// /// Registers a resource in a specific vault (MediaVaultType inferred from media type) /// public async Task RegisterResourceAsync(string vaultId, string entryId, FileBinary media) { try { var directoryVault = _vaults.Get(vaultId); if (directoryVault != null) { await directoryVault.AddEntryAsync(entryId, media); return true; } } catch { // Swallow exceptions and return false, matching TypeScript behavior } return false; } /// /// Registers a resource by streaming its bytes into the vault, without materializing the whole /// file in a managed byte[] (the store-path OOM fix). The caller supplies the index /// and a callback that emits bytes to /// the backing stream. Swallows exceptions and returns false, matching /// 's contract — callers check the bool. /// public async Task RegisterResourceStreamingAsync( string vaultId, string entryId, MetaData metaData, Func writeContent, CancellationToken cancellationToken = default) { try { var directoryVault = _vaults.Get(vaultId); if (directoryVault != null) { var written = await directoryVault.AddEntryStreamingAsync(entryId, metaData, writeContent, cancellationToken); _logger.LogInformation( "Streamed {Bytes} bytes into vault {VaultId} entry {EntryId} (no whole-file buffer).", written, vaultId, entryId); return true; } } catch (Exception ex) { // Swallow and return false, matching RegisterResourceAsync. Log at error for real failures // only — a normal client cancel (OperationCanceledException) is not an error condition and // would spam the error log on every client disconnect during a large upload or replace. if (ex is not OperationCanceledException) { _logger.LogError(ex, "RegisterResourceStreamingAsync failed for vault {VaultId} entry {EntryId}", vaultId, entryId); } } return false; } /// /// Removes a resource from a specific vault. Returns null if the vault does not exist, /// false if the entry was not found, true if the entry was removed. Distinguishing /// "no such vault" / "no such entry" / "removed" lets the HTTP host map cleanly to /// 404 vs. 200. Follows the FileDatabase error-swallow contract: any unexpected failure /// returns null so callers can surface 5xx without try/catch at the controller layer. /// public async Task RemoveResourceAsync(string vaultId, string entryId) { try { var directoryVault = _vaults.Get(vaultId); if (directoryVault == null) return null; return await directoryVault.RemoveEntryAsync(entryId); } catch (Exception ex) { _logger.LogError(ex, "RemoveResourceAsync failed for vault {VaultName} key {Key}", vaultId, entryId); return null; } } /// /// Gets all vault IDs managed by this database /// public IReadOnlyList GetVaultIds() { return _vaults.Keys.ToList().AsReadOnly(); } /// /// Gets the total number of vaults /// public int GetVaultCount() { return _vaults.Size; } /// /// Disposes the file database and stops all file watchers /// public void Dispose() { if (_disposed) return; _disposed = true; _indexWatcher.Dispose(); GC.SuppressFinalize(this); } }