Add CMS track delete: DeepDrftContent DELETE endpoint, DeepDrftWeb SQL-first orchestration, DeepDrftCms confirmation dialog (W3-T3)

This commit is contained in:
Daniel Harvey
2026-05-18 15:20:08 -04:00
parent f46c2557c8
commit 4a59df6baa
11 changed files with 325 additions and 4 deletions
@@ -57,4 +57,10 @@ public interface IVaultIndex : IEntryQueryable
/// Adds an entry with metadata to the vault index
/// </summary>
void PutEntry(string entryId, MetaData metaData);
/// <summary>
/// Removes an entry (and its metadata) from the vault index.
/// Returns true if an entry was removed, false if it was not present.
/// </summary>
bool RemoveEntry(string entryId);
}
@@ -131,4 +131,6 @@ public class VaultIndex : IndexData, IVaultIndex
public MetaData? GetEntry(string entryId) => Entries.Get(entryId);
public void PutEntry(string entryId, MetaData metaData) => Entries.Set(entryId, metaData);
public bool RemoveEntry(string entryId) => Entries.Delete(entryId);
}
@@ -171,10 +171,34 @@ public class FileDatabase : DirectoryIndexDirectory, IDisposable
{
// Swallow exceptions and return false, matching TypeScript behavior
}
return false;
}
/// <summary>
/// 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.
/// </summary>
public async Task<bool?> RemoveResourceAsync(string vaultId, string entryId)
{
try
{
var directoryVault = _vaults.Get(vaultId);
if (directoryVault == null)
return null;
return await directoryVault.RemoveEntryAsync(entryId);
}
catch
{
// Swallow exceptions and return null, matching the FileDatabase error contract.
return null;
}
}
/// <summary>
/// Gets all vault IDs managed by this database
/// </summary>
@@ -122,6 +122,32 @@ public class VaultIndexDirectory : IndexDirectory
}
}
/// <summary>
/// Removes an entry from the index under the index lock, persisting on success.
/// Returns the removed entry's metadata, or null if the entry did not exist.
/// Caller is responsible for any backing-file cleanup using the returned metadata.
/// </summary>
protected async Task<MetaData?> RemoveFromIndexAsync(string entryId)
{
await _indexLock.WaitAsync();
try
{
var metaData = _vaultIndex.GetEntry(entryId);
if (metaData == null)
return null;
if (!_vaultIndex.RemoveEntry(entryId))
return null;
await SaveIndexAsync(_vaultIndex);
return metaData;
}
finally
{
_indexLock.Release();
}
}
/// <summary>
/// Reloads the index from disk. Called when the index file is modified externally.
/// </summary>
@@ -132,6 +132,33 @@ public abstract class MediaVault : VaultIndexDirectory
}
}
/// <summary>
/// Removes an entry from the vault: drops it from the index (persisting the change)
/// and deletes the backing file from disk. Returns true if an entry was removed,
/// false if the entry was not present. Follows the FileDatabase error-swallow contract
/// for read failures; index/file write failures propagate so the caller can map them
/// to a 5xx.
/// </summary>
public async Task<bool> RemoveEntryAsync(string entryId)
{
var metaData = await RemoveFromIndexAsync(entryId);
if (metaData == null)
return false;
// Index already persisted; if the file is missing or fails to delete, the entry
// is still gone from the catalogue. Treat a missing file as success (callers asked
// for the entry to go away, and it has). A failure deleting an existing file leaves
// an orphan on disk; surface it to the caller via exception so the host can log,
// matching the AddEntryAsync error-propagation shape.
var mediaPath = GetMediaPathFromEntryKey(metaData.MediaKey, metaData.Extension);
if (FileUtils.FileExists(mediaPath))
{
File.Delete(mediaPath);
}
return true;
}
/// <summary>
/// Extracts buffer and extension from a media binary
/// </summary>