FileDatabase engine port from snailbird-content TS/Node program

This commit is contained in:
2025-09-01 16:55:28 -04:00
parent f0d60190cc
commit 9124e82e24
15 changed files with 1395 additions and 0 deletions
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -0,0 +1,9 @@
namespace DeepDrftContent.FileDatabase.Models;
/// <summary>
/// Represents a key for entries in the file database system.
/// Combines a string key with a media vault type for type-safe operations.
/// </summary>
/// <param name="Key">The string identifier for the entry</param>
/// <param name="Type">The media vault type this entry belongs to</param>
public record EntryKey(string Key, MediaVaultType Type);
@@ -0,0 +1,27 @@
namespace DeepDrftContent.FileDatabase.Models;
/// <summary>
/// Base interface for all index types
/// </summary>
public interface IIndex
{
/// <summary>
/// Gets the key identifier for this index
/// </summary>
string GetKey();
/// <summary>
/// Gets all entry keys in this index
/// </summary>
IReadOnlyList<EntryKey> GetEntries();
/// <summary>
/// Gets the number of entries in this index
/// </summary>
int GetEntriesSize();
/// <summary>
/// Checks if the index contains the specified entry key
/// </summary>
bool HasEntry(EntryKey entryKey);
}
@@ -0,0 +1,112 @@
using DeepDrftContent.FileDatabase.Utils;
namespace DeepDrftContent.FileDatabase.Models;
/// <summary>
/// Base class for index data used in serialization
/// </summary>
public abstract class IndexData
{
public string IndexKey { get; }
protected IndexData(string indexKey)
{
IndexKey = indexKey;
}
}
/// <summary>
/// Serializable data for directory indexes
/// </summary>
public class DirectoryIndexData : IndexData
{
public List<EntryKey> Entries { get; set; } = new();
public DirectoryIndexData(string indexKey) : base(indexKey) { }
public static DirectoryIndexData FromIndex(DirectoryIndex index)
{
var data = new DirectoryIndexData(index.GetKey())
{
Entries = index.GetEntries().ToList()
};
return data;
}
}
/// <summary>
/// Serializable data for vault indexes
/// </summary>
public class VaultIndexData : IndexData
{
public List<(EntryKey Key, MetaData Value)> Entries { get; set; } = new();
public VaultIndexData(string indexKey) : base(indexKey) { }
public static VaultIndexData FromIndex(VaultIndex index)
{
var data = new VaultIndexData(index.GetKey())
{
Entries = index.Entries.Select(kvp => (kvp.Key, kvp.Value)).ToList()
};
return data;
}
}
/// <summary>
/// Directory index implementation using StructuralSet for entries
/// </summary>
public class DirectoryIndex : IndexData, IIndex
{
public StructuralSet<EntryKey> Entries { get; }
public DirectoryIndex(DirectoryIndexData indexData) : base(indexData.IndexKey)
{
Entries = new StructuralSet<EntryKey>();
// Load entries from data
foreach (var entry in indexData.Entries)
{
Entries.Add(entry);
}
}
public string GetKey() => IndexKey;
public IReadOnlyList<EntryKey> GetEntries() => Entries.ToList().AsReadOnly();
public int GetEntriesSize() => Entries.Size;
public bool HasEntry(EntryKey entryKey) => Entries.Has(entryKey);
public void PutEntry(EntryKey entryKey) => Entries.Add(entryKey);
}
/// <summary>
/// Vault index implementation using StructuralMap for entries with metadata
/// </summary>
public class VaultIndex : IndexData, IIndex
{
public StructuralMap<EntryKey, MetaData> Entries { get; }
public VaultIndex(VaultIndexData indexData) : base(indexData.IndexKey)
{
Entries = new StructuralMap<EntryKey, MetaData>();
// Load entries from data
foreach (var (key, value) in indexData.Entries)
{
Entries.Set(key, value);
}
}
public string GetKey() => IndexKey;
public IReadOnlyList<EntryKey> GetEntries() => Entries.Keys.ToList().AsReadOnly();
public int GetEntriesSize() => Entries.Size;
public bool HasEntry(EntryKey entryKey) => Entries.Has(entryKey);
public MetaData? GetEntry(EntryKey entryKey) => Entries.Get(entryKey);
public void PutEntry(EntryKey entryKey, MetaData metaData) => Entries.Set(entryKey, metaData);
}
@@ -0,0 +1,147 @@
namespace DeepDrftContent.FileDatabase.Models;
/// <summary>
/// Type mappings for media vault types to their corresponding classes
/// </summary>
public static class MediaVaultTypeMap
{
public static Type GetBinaryType(MediaVaultType vaultType) => vaultType switch
{
MediaVaultType.Media => typeof(MediaBinary),
MediaVaultType.Image => typeof(ImageBinary),
_ => throw new ArgumentException($"Unknown vault type: {vaultType}")
};
public static Type GetDtoType(MediaVaultType vaultType) => vaultType switch
{
MediaVaultType.Media => typeof(MediaBinaryDto),
MediaVaultType.Image => typeof(ImageBinaryDto),
_ => throw new ArgumentException($"Unknown vault type: {vaultType}")
};
public static Type GetParamsType(MediaVaultType vaultType) => vaultType switch
{
MediaVaultType.Media => typeof(MediaBinaryParams),
MediaVaultType.Image => typeof(ImageBinaryParams),
_ => throw new ArgumentException($"Unknown vault type: {vaultType}")
};
public static Type GetMetaDataType(MediaVaultType vaultType) => vaultType switch
{
MediaVaultType.Media => typeof(MetaData),
MediaVaultType.Image => typeof(ImageMetaData),
_ => throw new ArgumentException($"Unknown vault type: {vaultType}")
};
}
/// <summary>
/// Factory for creating metadata objects based on vault type
/// </summary>
public static class MetaDataFactory
{
public static MetaData Create(MediaVaultType type, string entryKey, string extension, double aspectRatio = 1.0)
{
return type switch
{
MediaVaultType.Media => new MetaData(entryKey, extension),
MediaVaultType.Image => new ImageMetaData(entryKey, extension, aspectRatio),
_ => throw new ArgumentException($"Unknown vault type: {type}")
};
}
public static T Create<T>(MediaVaultType type, string entryKey, string extension, double aspectRatio = 1.0)
where T : MetaData
{
var metaData = Create(type, entryKey, extension, aspectRatio);
return (T)metaData;
}
}
/// <summary>
/// Factory for creating media parameter objects
/// </summary>
public static class MediaParamsFactory
{
public static object Create(MediaVaultType type, FileBinary fileBinary, MetaData metaData)
{
return type switch
{
MediaVaultType.Media => new MediaBinaryParams(fileBinary.Buffer, fileBinary.Size, metaData.Extension),
MediaVaultType.Image when metaData is ImageMetaData imageMetaData =>
new ImageBinaryParams(fileBinary.Buffer, fileBinary.Size, metaData.Extension, imageMetaData.AspectRatio),
_ => throw new ArgumentException($"Invalid vault type {type} or metadata type mismatch")
};
}
public static T Create<T>(MediaVaultType type, FileBinary fileBinary, MetaData metaData)
{
var parameters = Create(type, fileBinary, metaData);
return (T)parameters;
}
}
/// <summary>
/// Factory for creating media binary objects from parameters
/// </summary>
public static class FileBinaryFactory
{
public static object Create(MediaVaultType vaultType, object parameters)
{
return vaultType switch
{
MediaVaultType.Media when parameters is MediaBinaryParams mediaParams =>
new MediaBinary(mediaParams),
MediaVaultType.Image when parameters is ImageBinaryParams imageParams =>
new ImageBinary(imageParams),
_ => throw new ArgumentException($"Invalid vault type {vaultType} or parameter type mismatch")
};
}
public static T Create<T>(MediaVaultType vaultType, object parameters) where T : FileBinary
{
var binary = Create(vaultType, parameters);
return (T)binary;
}
public static object From(MediaVaultType type, object mediaBinaryDto)
{
return type switch
{
MediaVaultType.Media when mediaBinaryDto is MediaBinaryDto mediaDto =>
MediaBinary.From(mediaDto),
MediaVaultType.Image when mediaBinaryDto is ImageBinaryDto imageDto =>
ImageBinary.From(imageDto),
_ => throw new ArgumentException($"Invalid type {type} or DTO type mismatch")
};
}
public static T From<T>(MediaVaultType type, object mediaBinaryDto) where T : FileBinary
{
var binary = From(type, mediaBinaryDto);
return (T)binary;
}
}
/// <summary>
/// Factory for creating DTO objects from media binaries
/// </summary>
public static class FileBinaryDtoFactory
{
public static object From(MediaVaultType type, object mediaBinary)
{
return type switch
{
MediaVaultType.Media when mediaBinary is MediaBinary media =>
new MediaBinaryDto(media),
MediaVaultType.Image when mediaBinary is ImageBinary image =>
new ImageBinaryDto(image),
_ => throw new ArgumentException($"Invalid type {type} or binary type mismatch")
};
}
public static T From<T>(MediaVaultType type, object mediaBinary)
{
var dto = From(type, mediaBinary);
return (T)dto;
}
}
@@ -0,0 +1,175 @@
namespace DeepDrftContent.FileDatabase.Models;
/// <summary>
/// Parameters for creating a FileBinary
/// </summary>
/// <param name="Buffer">The binary data</param>
/// <param name="Size">The size of the data in bytes</param>
public record FileBinaryParams(byte[] Buffer, int Size);
/// <summary>
/// Base class for file binary data
/// </summary>
public class FileBinary
{
public byte[] Buffer { get; }
public int Size { get; }
public FileBinary(FileBinaryParams parameters)
{
Buffer = parameters.Buffer;
Size = parameters.Size;
}
public static FileBinary From(FileBinaryDto dto)
{
var buffer = Convert.FromBase64String(dto.Base64);
return new FileBinary(new FileBinaryParams(buffer, dto.Size));
}
}
/// <summary>
/// DTO for FileBinary serialization
/// </summary>
/// <param name="Base64">Base64 encoded binary data</param>
/// <param name="Size">Size of the original data</param>
public record FileBinaryDto(string Base64, int Size)
{
public FileBinaryDto(FileBinary fileBinary) : this(
Convert.ToBase64String(fileBinary.Buffer),
fileBinary.Size) { }
}
/// <summary>
/// Parameters for creating a MediaBinary
/// </summary>
/// <param name="Buffer">The binary data</param>
/// <param name="Size">The size of the data in bytes</param>
/// <param name="Extension">The file extension</param>
public record MediaBinaryParams(byte[] Buffer, int Size, string Extension)
: FileBinaryParams(Buffer, Size);
/// <summary>
/// Media binary with extension information
/// </summary>
public class MediaBinary : FileBinary
{
public string Extension { get; }
public MediaBinary(MediaBinaryParams parameters) : base(parameters)
{
Extension = parameters.Extension;
}
public static MediaBinary From(MediaBinaryDto dto)
{
var buffer = Convert.FromBase64String(dto.Base64);
var extension = GetExtensionType(dto.Mime);
return new MediaBinary(new MediaBinaryParams(buffer, dto.Size, extension));
}
private static string GetExtensionType(string mime)
{
return MimeTypeExtensions.GetExtension(mime);
}
}
/// <summary>
/// DTO for MediaBinary serialization
/// </summary>
/// <param name="Base64">Base64 encoded binary data</param>
/// <param name="Size">Size of the original data</param>
/// <param name="Mime">MIME type of the media</param>
public record MediaBinaryDto(string Base64, int Size, string Mime) : FileBinaryDto(Base64, Size)
{
public MediaBinaryDto(MediaBinary mediaBinary) : this(
Convert.ToBase64String(mediaBinary.Buffer),
mediaBinary.Size,
MimeTypeExtensions.GetMimeType(mediaBinary.Extension)) { }
}
/// <summary>
/// Parameters for creating an ImageBinary
/// </summary>
/// <param name="Buffer">The binary data</param>
/// <param name="Size">The size of the data in bytes</param>
/// <param name="Extension">The file extension</param>
/// <param name="AspectRatio">The aspect ratio of the image</param>
public record ImageBinaryParams(byte[] Buffer, int Size, string Extension, double AspectRatio)
: MediaBinaryParams(Buffer, Size, Extension);
/// <summary>
/// Image binary with aspect ratio information
/// </summary>
public class ImageBinary : MediaBinary
{
public double AspectRatio { get; }
public ImageBinary(ImageBinaryParams parameters) : base(parameters)
{
AspectRatio = parameters.AspectRatio;
}
public static ImageBinary From(ImageBinaryDto dto)
{
var buffer = Convert.FromBase64String(dto.Base64);
var extension = GetExtensionType(dto.Mime);
return new ImageBinary(new ImageBinaryParams(buffer, dto.Size, extension, dto.AspectRatio));
}
private static string GetExtensionType(string mime)
{
return MimeTypeExtensions.GetExtension(mime);
}
}
/// <summary>
/// DTO for ImageBinary serialization
/// </summary>
/// <param name="Base64">Base64 encoded binary data</param>
/// <param name="Size">Size of the original data</param>
/// <param name="Mime">MIME type of the media</param>
/// <param name="AspectRatio">The aspect ratio of the image</param>
public record ImageBinaryDto(string Base64, int Size, string Mime, double AspectRatio)
: MediaBinaryDto(Base64, Size, Mime)
{
public ImageBinaryDto(ImageBinary imageBinary) : this(
Convert.ToBase64String(imageBinary.Buffer),
imageBinary.Size,
MimeTypeExtensions.GetMimeType(imageBinary.Extension),
imageBinary.AspectRatio) { }
}
/// <summary>
/// Utility class for MIME type and extension conversions
/// </summary>
public static class MimeTypeExtensions
{
private static readonly Dictionary<string, string> MimeTypes = new()
{
{ ".jpg", "image/jpeg" },
{ ".jpeg", "image/jpeg" },
{ ".png", "image/png" },
{ ".gif", "image/gif" },
{ ".webp", "image/webp" },
{ ".svg", "image/svg+xml" },
{ ".bmp", "image/bmp" }
};
private static readonly Dictionary<string, string> Extensions =
MimeTypes.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
public static string GetMimeType(string extension)
{
return MimeTypes.TryGetValue(extension.ToLowerInvariant(), out var mime)
? mime
: "application/octet-stream";
}
public static string GetExtension(string mime)
{
return Extensions.TryGetValue(mime.ToLowerInvariant(), out var extension)
? extension
: ".bin";
}
}
@@ -0,0 +1,10 @@
namespace DeepDrftContent.FileDatabase.Models;
/// <summary>
/// Enum representing different types of media vaults
/// </summary>
public enum MediaVaultType
{
Media,
Image
}
@@ -0,0 +1,17 @@
namespace DeepDrftContent.FileDatabase.Models;
/// <summary>
/// Base metadata for media entries
/// </summary>
/// <param name="MediaKey">The key used to identify the media file</param>
/// <param name="Extension">The file extension of the media</param>
public record MetaData(string MediaKey, string Extension);
/// <summary>
/// Extended metadata for image entries, including aspect ratio
/// </summary>
/// <param name="MediaKey">The key used to identify the media file</param>
/// <param name="Extension">The file extension of the media</param>
/// <param name="AspectRatio">The aspect ratio of the image</param>
public record ImageMetaData(string MediaKey, string Extension, double AspectRatio)
: MetaData(MediaKey, Extension);
+132
View File
@@ -0,0 +1,132 @@
# FileDatabase C# Port
This is a C# port of the TypeScript file database system, maintaining architectural integrity while leveraging C# language features.
## Architecture Overview
The C# port preserves the original three-layer architecture:
### 1. **FileDatabase** (Main Orchestrator)
- **Location**: `Services/FileDatabase.cs`
- **Purpose**: Root-level manager coordinating multiple media vaults
- **Key Features**:
- Manages collection of `MediaVault` instances using `StructuralMap<EntryKey, MediaVault>`
- Provides async factory method `FromAsync()` for initialization
- Handles vault creation, resource loading, and resource registration
### 2. **MediaVault System** (Storage Containers)
- **Location**: `Services/MediaVault.cs`
- **Components**:
- `MediaVault` (Abstract base class)
- `ImageDirectoryVault` (Concrete implementation for images)
- **Key Features**:
- File path normalization and media key generation
- Generic type-safe operations using `MediaVaultType` enum
- Metadata association with stored files
- Async factory pattern for initialization
### 3. **Index Management** (Metadata & Organization)
- **Location**: `Services/IndexSystem.cs`
- **Two-tier indexing**:
- `DirectoryIndex`: Manages vault entries (what vaults exist)
- `VaultIndex`: Manages media entries within vaults (what files exist + metadata)
- **Features**:
- `IndexFactory` handles index creation/loading
- Automatic JSON serialization to filesystem
- Strong typing with generic constraints
## Key Components
### Models (`Models/` directory)
- **`EntryKey`**: Composite key structure with string key + MediaVaultType
- **`MetaData` hierarchy**: Base metadata → `ImageMetaData` with aspect ratio
- **Media Types**: `FileBinary``MediaBinary``ImageBinary`
- **Factory Classes**: Type-safe creation of media objects and metadata
### Utilities (`Utils/` directory)
- **`StructuralMap<TKey, TValue>`**: JSON-based structural equality for complex keys
- **`StructuralSet<T>`**: Set with structural equality semantics
- **`FileUtils`**: Async file I/O operations with chunked reading/writing
## C# Design Improvements
### SOLID Principles Applied
1. **Single Responsibility**: Each class handles one concern
2. **Open/Closed**: Extensible media types via generic constraints
3. **Liskov Substitution**: Proper inheritance hierarchies
4. **Interface Segregation**: Focused interfaces (`IIndex`)
5. **Dependency Inversion**: Abstract base classes and interfaces
### C# Language Features
- **Records**: Immutable data structures for `EntryKey`, `MetaData`
- **Pattern Matching**: Switch expressions for type-safe factory methods
- **Nullable Reference Types**: Explicit nullability handling
- **Async/Await**: Full async support with `Task<T>` and `ValueTask<T>`
- **Generic Constraints**: Strong typing with `where` clauses
### DRY Implementation
- **Factory Pattern**: Centralized object creation logic
- **Generic Type Maps**: Reusable type mappings for different media types
- **Template Method Pattern**: Common functionality in base classes
## Usage Example
```csharp
// Initialize the database
var database = await FileDatabase.FromAsync("/path/to/database");
// Create a vault
var vaultKey = new EntryKey("images", MediaVaultType.Image);
await database.CreateVaultAsync(vaultKey);
// Store an image
var entryKey = new EntryKey("photo1", MediaVaultType.Image);
var imageData = new ImageBinary(new ImageBinaryParams(buffer, size, ".jpg", 1.5));
await database.RegisterResourceAsync(MediaVaultType.Image, vaultKey, entryKey, imageData);
// Load an image
var loadedImage = await database.LoadResourceAsync<ImageBinary>(
MediaVaultType.Image, vaultKey, entryKey);
```
## Project Structure
```
FileDatabase/
├── Models/
│ ├── EntryKey.cs # Composite key structure
│ ├── MetaData.cs # Metadata hierarchy
│ ├── MediaModels.cs # Binary data classes
│ ├── MediaFactories.cs # Factory pattern implementations
│ ├── MediaVaultType.cs # Enum for vault types
│ ├── IIndex.cs # Index interface
│ └── IndexData.cs # Index implementations
├── Services/
│ ├── FileDatabase.cs # Main orchestrator
│ ├── MediaVault.cs # Vault system
│ └── IndexSystem.cs # Index management
├── Utils/
│ ├── StructuralMap.cs # Structural equality map
│ ├── StructuralSet.cs # Structural equality set
│ └── FileUtils.cs # File I/O utilities
└── FileDatabase.csproj # Project file (.NET 9.0)
```
## Key Architectural Decisions
1. **Async-First Design**: All I/O operations are asynchronous
2. **Strong Type Safety**: Extensive use of generics and constraints
3. **Structural Equality**: JSON-based equality for composite keys
4. **Separation of Concerns**: Clear boundaries between indexing, storage, and media handling
5. **Factory-Based Initialization**: Handles complex async setup patterns
6. **Metadata-Driven**: Rich metadata system supporting extensible media types
## Differences from TypeScript Version
1. **Explicit Type Safety**: C# compiler enforces type constraints at compile time
2. **Memory Management**: Automatic garbage collection vs manual buffer management
3. **Serialization**: System.Text.Json instead of V8 serialization
4. **Error Handling**: Exceptions vs try/catch patterns (maintained original behavior)
5. **Nullability**: Explicit nullable reference types for better null safety
This port maintains the architectural integrity of the original TypeScript design while leveraging C#'s type system and language features for improved maintainability and performance.
@@ -0,0 +1,159 @@
using DeepDrftContent.FileDatabase.Models;
using DeepDrftContent.FileDatabase.Utils;
namespace DeepDrftContent.FileDatabase.Services;
/// <summary>
/// Main file database class that orchestrates multiple media vaults
/// </summary>
public class FileDatabase : DirectoryIndexDirectory
{
private readonly StructuralMap<EntryKey, MediaVault> _vaults;
/// <summary>
/// Factory method to create a FileDatabase instance
/// </summary>
public static async Task<FileDatabase?> FromAsync(string rootPath)
{
var factory = new IndexFactory(rootPath, IndexType.Directory);
var rootIndex = await factory.BuildIndexAsync();
if (rootIndex is DirectoryIndex directoryIndex)
{
var db = new FileDatabase(rootPath, directoryIndex);
await db.InitVaultsAsync();
return db;
}
return null;
}
private FileDatabase(string rootPath, DirectoryIndex index) : base(rootPath, index)
{
_vaults = new StructuralMap<EntryKey, MediaVault>();
}
/// <summary>
/// Initializes all vaults found in the index
/// </summary>
private async Task InitVaultsAsync()
{
foreach (var vaultKey in GetIndexEntries())
{
await InitVaultAsync(vaultKey);
}
}
/// <summary>
/// Initializes a specific vault
/// </summary>
private async Task InitVaultAsync(EntryKey vaultKey)
{
var path = Path.Combine(RootPath, vaultKey.Key);
var directoryVault = await ImageDirectoryVault.FromAsync(path);
if (directoryVault != null)
{
_vaults.Set(vaultKey, directoryVault);
}
}
/// <summary>
/// Checks if a vault exists for the given key
/// </summary>
public bool HasVault(EntryKey vaultKey)
{
return _vaults.Has(vaultKey);
}
/// <summary>
/// Gets a vault by key
/// </summary>
public MediaVault? GetVault(EntryKey vaultKey)
{
return HasVault(vaultKey) ? _vaults.Get(vaultKey) : null;
}
/// <summary>
/// Creates a new vault
/// </summary>
public async Task CreateVaultAsync(EntryKey vaultKey)
{
try
{
var path = Path.Combine(RootPath, vaultKey.Key);
var directoryVault = await ImageDirectoryVault.FromAsync(path);
if (directoryVault != null)
{
_vaults.Set(vaultKey, directoryVault);
await AddToIndexAsync(vaultKey);
}
}
catch
{
// Re-throw to maintain the same error behavior as TypeScript version
throw;
}
}
/// <summary>
/// Loads a resource from a specific vault
/// </summary>
public async Task<T?> LoadResourceAsync<T>(MediaVaultType vaultType, EntryKey vaultKey, EntryKey entryKey)
where T : FileBinary
{
try
{
var vault = _vaults.Get(vaultKey);
if (vault != null)
{
return await vault.GetEntryAsync<T>(vaultType, entryKey);
}
}
catch
{
// Swallow exceptions and return null, matching TypeScript behavior
}
return null;
}
/// <summary>
/// Registers a resource in a specific vault
/// </summary>
public async Task<bool> RegisterResourceAsync(MediaVaultType vaultType, EntryKey vaultKey, EntryKey entryKey, object media)
{
try
{
var directoryVault = _vaults.Get(vaultKey);
if (directoryVault != null)
{
await directoryVault.AddEntryAsync(vaultType, entryKey, media);
return true;
}
}
catch
{
// Swallow exceptions and return false, matching TypeScript behavior
}
return false;
}
/// <summary>
/// Gets all vault keys managed by this database
/// </summary>
public IReadOnlyList<EntryKey> GetVaultKeys()
{
return _vaults.Keys.ToList().AsReadOnly();
}
/// <summary>
/// Gets the total number of vaults
/// </summary>
public int GetVaultCount()
{
return _vaults.Size;
}
}
@@ -0,0 +1,162 @@
using DeepDrftContent.FileDatabase.Models;
using DeepDrftContent.FileDatabase.Utils;
namespace DeepDrftContent.FileDatabase.Services;
/// <summary>
/// Enum representing different types of indexes
/// </summary>
public enum IndexType
{
Directory,
Vault
}
/// <summary>
/// Abstract base class for index containers
/// </summary>
public abstract class AbstractIndexContainer
{
protected IndexType Type { get; }
public string RootPath { get; }
protected AbstractIndexContainer(string path, IndexType type)
{
RootPath = path;
Type = type;
}
public string GetKey() => Path.GetFileName(RootPath);
protected async Task SaveIndexAsync<T>(T index) where T : IIndex
{
var indexPath = Path.Combine(RootPath, "index");
object indexData = Type switch
{
IndexType.Directory when index is DirectoryIndex dirIndex => DirectoryIndexData.FromIndex(dirIndex),
IndexType.Vault when index is VaultIndex vaultIndex => VaultIndexData.FromIndex(vaultIndex),
_ => throw new ArgumentException($"Invalid index type {Type} for index {typeof(T)}")
};
await FileUtils.PutObjectAsync(indexPath, indexData);
}
}
/// <summary>
/// Factory for creating and loading indexes
/// </summary>
public class IndexFactory : AbstractIndexContainer
{
public IndexFactory(string path, IndexType type) : base(path, type) { }
/// <summary>
/// Builds an index by loading existing or creating new
/// </summary>
public async Task<IIndex?> BuildIndexAsync()
{
try
{
return await LoadOrCreateIndexAsync();
}
catch
{
return null;
}
}
private async Task<IIndex?> LoadOrCreateIndexAsync()
{
try
{
return await LoadIndexAsync();
}
catch
{
return await CreateIndexAsync();
}
}
private async Task<IIndex?> LoadIndexAsync()
{
var indexPath = Path.Combine(RootPath, "index");
IIndex result = Type switch
{
IndexType.Directory => new DirectoryIndex(await FileUtils.FetchObjectAsync<DirectoryIndexData>(indexPath)),
IndexType.Vault => new VaultIndex(await FileUtils.FetchObjectAsync<VaultIndexData>(indexPath)),
_ => throw new ArgumentException($"Unknown index type: {Type}")
};
return result;
}
private async Task<IIndex?> CreateIndexAsync()
{
IIndex index = Type switch
{
IndexType.Directory => new DirectoryIndex(new DirectoryIndexData(RootPath)),
IndexType.Vault => new VaultIndex(new VaultIndexData(RootPath)),
_ => throw new ArgumentException($"Unknown index type: {Type}")
};
await FileUtils.MakeVaultDirectoryAsync(RootPath);
await SaveIndexAsync(index);
return index;
}
}
/// <summary>
/// Abstract base class for directory containers that manage indexes
/// </summary>
public abstract class IndexDirectory : AbstractIndexContainer
{
protected IIndex Index { get; }
protected IndexDirectory(string rootPath, IndexType type, IIndex index) : base(rootPath, type)
{
Index = index;
}
protected IReadOnlyList<EntryKey> GetIndexEntries() => Index.GetEntries();
public int GetIndexSize() => Index.GetEntriesSize();
public bool HasIndexEntry(EntryKey entryKey) => Index.HasEntry(entryKey);
}
/// <summary>
/// Directory index directory implementation
/// </summary>
public class DirectoryIndexDirectory : IndexDirectory
{
public DirectoryIndexDirectory(string rootPath, DirectoryIndex index)
: base(rootPath, IndexType.Directory, index) { }
protected async Task AddToIndexAsync(EntryKey entryKey)
{
if (Index is DirectoryIndex dirIndex)
{
dirIndex.PutEntry(entryKey);
await SaveIndexAsync(dirIndex);
}
}
}
/// <summary>
/// Vault index directory implementation
/// </summary>
public class VaultIndexDirectory : IndexDirectory
{
public VaultIndexDirectory(string rootPath, VaultIndex index)
: base(rootPath, IndexType.Vault, index) { }
protected async Task AddToIndexAsync(EntryKey entryKey, MetaData metaData)
{
if (Index is VaultIndex vaultIndex)
{
vaultIndex.PutEntry(entryKey, metaData);
await SaveIndexAsync(vaultIndex);
}
}
}
@@ -0,0 +1,127 @@
using System.Text.RegularExpressions;
using DeepDrftContent.FileDatabase.Models;
using DeepDrftContent.FileDatabase.Utils;
namespace DeepDrftContent.FileDatabase.Services;
/// <summary>
/// Abstract base class for media vaults that store and manage media files
/// </summary>
public abstract class MediaVault : VaultIndexDirectory
{
protected MediaVault(string rootPath, VaultIndex index) : base(rootPath, index) { }
/// <summary>
/// Generates a media key from an entry key by sanitizing special characters
/// </summary>
protected string GetMediaKey(string entryKey, string extension)
{
var sanitized = Regex.Replace(entryKey, @"[^a-zA-Z0-9]", "-");
return $"{sanitized}{extension}";
}
/// <summary>
/// Gets the full file path for a media file from an entry key
/// </summary>
protected string GetMediaPathFromEntryKey(string entryKey, string extension)
{
return Path.Combine(RootPath, GetMediaKey(entryKey, extension));
}
/// <summary>
/// Gets the full file path for a media file from a media key
/// </summary>
protected string GetMediaPathFromMediaKey(string mediaKey)
{
return Path.Combine(RootPath, mediaKey);
}
/// <summary>
/// Adds a new entry to the vault with the specified media data
/// </summary>
public async Task AddEntryAsync(MediaVaultType vaultType, EntryKey entryKey, object media)
{
// Extract properties from media object based on type
var (buffer, extension) = ExtractMediaProperties(media);
var mediaPath = GetMediaPathFromEntryKey(entryKey.Key, extension);
var metaData = MetaDataFactory.Create(vaultType, entryKey.Key, extension, GetAspectRatio(media));
await AddToIndexAsync(entryKey, metaData);
await FileUtils.PutFileAsync(mediaPath, buffer);
}
/// <summary>
/// Retrieves an entry from the vault
/// </summary>
public async Task<T?> GetEntryAsync<T>(MediaVaultType vaultType, EntryKey entryKey) where T : FileBinary
{
if (!HasIndexEntry(entryKey))
return null;
if (Index is not VaultIndex vaultIndex)
return null;
var metaData = vaultIndex.GetEntry(entryKey);
if (metaData == null)
return null;
var mediaPath = GetMediaPathFromEntryKey(metaData.MediaKey, metaData.Extension);
if (!FileUtils.FileExists(mediaPath))
return null;
var fileBinary = await FileUtils.FetchFileAsync(mediaPath);
var parameters = MediaParamsFactory.Create(vaultType, fileBinary, metaData);
var result = FileBinaryFactory.Create(vaultType, parameters);
return (T)result;
}
/// <summary>
/// Extracts buffer and extension from a media object
/// </summary>
private static (byte[] buffer, string extension) ExtractMediaProperties(object media)
{
return media switch
{
ImageBinary imageBinary => (imageBinary.Buffer, imageBinary.Extension),
MediaBinary mediaBinary => (mediaBinary.Buffer, mediaBinary.Extension),
_ => throw new ArgumentException($"Unsupported media type: {media.GetType()}")
};
}
/// <summary>
/// Extracts aspect ratio from media object if it's an image
/// </summary>
private static double GetAspectRatio(object media)
{
return media is ImageBinary imageBinary ? imageBinary.AspectRatio : 1.0;
}
}
/// <summary>
/// Concrete implementation of MediaVault for image storage
/// </summary>
public class ImageDirectoryVault : MediaVault
{
private ImageDirectoryVault(string rootPath, VaultIndex index) : base(rootPath, index) { }
/// <summary>
/// Factory method to create an ImageDirectoryVault instance
/// </summary>
public static async Task<ImageDirectoryVault?> FromAsync(string rootPath)
{
var factory = new IndexFactory(rootPath, IndexType.Vault);
var index = await factory.BuildIndexAsync();
if (index is VaultIndex vaultIndex)
{
return new ImageDirectoryVault(rootPath, vaultIndex);
}
return null;
}
}
@@ -0,0 +1,118 @@
using System.Text.Json;
using DeepDrftContent.FileDatabase.Models;
namespace DeepDrftContent.FileDatabase.Utils;
/// <summary>
/// Utility class for file I/O operations, matching the TypeScript file utilities
/// </summary>
public static class FileUtils
{
/// <summary>
/// Reads a file and returns it as a FileBinary object
/// </summary>
/// <param name="mediaPath">Path to the media file</param>
/// <returns>FileBinary containing the file data</returns>
public static async Task<FileBinary> FetchFileAsync(string mediaPath)
{
using var fileStream = new FileStream(mediaPath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 64 * 1024);
var buffer = new byte[fileStream.Length];
var totalBytesRead = 0;
while (totalBytesRead < fileStream.Length)
{
var bytesRead = await fileStream.ReadAsync(buffer.AsMemory(totalBytesRead));
if (bytesRead == 0)
throw new EndOfStreamException($"Unexpected end of stream while reading {mediaPath}");
totalBytesRead += bytesRead;
}
return new FileBinary(new FileBinaryParams(buffer, buffer.Length));
}
/// <summary>
/// Writes binary data to a file
/// </summary>
/// <param name="mediaPath">Path where to write the file</param>
/// <param name="buffer">Binary data to write</param>
public static async Task PutFileAsync(string mediaPath, byte[] buffer)
{
const int chunkSize = 64 * 1024;
using var fileStream = new FileStream(mediaPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: chunkSize);
for (int offset = 0; offset < buffer.Length; offset += chunkSize)
{
var length = Math.Min(chunkSize, buffer.Length - offset);
await fileStream.WriteAsync(buffer.AsMemory(offset, length));
}
await fileStream.FlushAsync();
}
/// <summary>
/// Serializes an object to a file using JSON
/// </summary>
/// <param name="filePath">Path to the file</param>
/// <param name="obj">Object to serialize</param>
public static async Task PutObjectAsync<T>(string filePath, T obj)
{
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(obj, options);
await File.WriteAllTextAsync(filePath, json);
}
/// <summary>
/// Deserializes an object from a JSON file
/// </summary>
/// <param name="filePath">Path to the file</param>
/// <returns>Deserialized object</returns>
public static async Task<T> FetchObjectAsync<T>(string filePath)
{
var json = await File.ReadAllTextAsync(filePath);
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var result = JsonSerializer.Deserialize<T>(json, options);
return result ?? throw new InvalidOperationException($"Failed to deserialize object from {filePath}");
}
/// <summary>
/// Creates a directory if it doesn't exist
/// </summary>
/// <param name="directoryPath">Path to the directory</param>
public static Task MakeVaultDirectoryAsync(string directoryPath)
{
Directory.CreateDirectory(directoryPath);
return Task.CompletedTask;
}
/// <summary>
/// Checks if a file exists
/// </summary>
/// <param name="filePath">Path to check</param>
/// <returns>True if file exists</returns>
public static bool FileExists(string filePath)
{
return File.Exists(filePath);
}
/// <summary>
/// Checks if a directory exists
/// </summary>
/// <param name="directoryPath">Path to check</param>
/// <returns>True if directory exists</returns>
public static bool DirectoryExists(string directoryPath)
{
return Directory.Exists(directoryPath);
}
}
@@ -0,0 +1,107 @@
using System.Collections;
using System.Text.Json;
namespace DeepDrftContent.FileDatabase.Utils;
/// <summary>
/// A map implementation that uses structural equality for keys by serializing them to JSON.
/// This provides the same behavior as the TypeScript StructuralMap.
/// </summary>
/// <typeparam name="TKey">The key type</typeparam>
/// <typeparam name="TValue">The value type</typeparam>
public class StructuralMap<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>>
{
private readonly Dictionary<string, KeyValuePair<TKey, TValue>> _innerMap = new();
/// <summary>
/// Converts a key to its string representation for structural comparison
/// </summary>
private string GetKeyString(TKey key)
{
return key switch
{
null => "null",
string s => s,
int or long or float or double or decimal => key.ToString()!,
_ => JsonSerializer.Serialize(key)
};
}
/// <summary>
/// Sets a key-value pair in the map
/// </summary>
public StructuralMap<TKey, TValue> Set(TKey key, TValue value)
{
var keyString = GetKeyString(key);
_innerMap[keyString] = new KeyValuePair<TKey, TValue>(key, value);
return this;
}
/// <summary>
/// Gets a value by key, or default if not found
/// </summary>
public TValue? Get(TKey key)
{
var keyString = GetKeyString(key);
return _innerMap.TryGetValue(keyString, out var pair) ? pair.Value : default;
}
/// <summary>
/// Checks if the map contains the specified key
/// </summary>
public bool Has(TKey key)
{
var keyString = GetKeyString(key);
return _innerMap.ContainsKey(keyString);
}
/// <summary>
/// Removes a key-value pair from the map
/// </summary>
public bool Delete(TKey key)
{
var keyString = GetKeyString(key);
return _innerMap.Remove(keyString);
}
/// <summary>
/// Clears all entries from the map
/// </summary>
public void Clear() => _innerMap.Clear();
/// <summary>
/// Gets the number of entries in the map
/// </summary>
public int Size => _innerMap.Count;
/// <summary>
/// Enumerates all key-value pairs
/// </summary>
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
return _innerMap.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Gets all keys in the map
/// </summary>
public IEnumerable<TKey> Keys => _innerMap.Values.Select(pair => pair.Key);
/// <summary>
/// Gets all values in the map
/// </summary>
public IEnumerable<TValue> Values => _innerMap.Values.Select(pair => pair.Value);
/// <summary>
/// Executes a callback for each key-value pair
/// </summary>
public void ForEach(Action<TValue, TKey, StructuralMap<TKey, TValue>> callback)
{
foreach (var (key, value) in this)
{
callback(value, key, this);
}
}
}
@@ -0,0 +1,84 @@
using System.Collections;
using System.Text.Json;
namespace DeepDrftContent.FileDatabase.Utils;
/// <summary>
/// A set implementation that uses structural equality for values by serializing them to JSON.
/// This provides the same behavior as the TypeScript StructuralSet.
/// </summary>
/// <typeparam name="T">The value type</typeparam>
public class StructuralSet<T> : IEnumerable<T>
{
private readonly Dictionary<string, T> _innerMap = new();
/// <summary>
/// Converts a value to its string representation for structural comparison
/// </summary>
private string GetValueString(T value)
{
return value switch
{
null => "null",
string s => s,
int or long or float or double or decimal => value.ToString()!,
_ => JsonSerializer.Serialize(value)
};
}
/// <summary>
/// Adds a value to the set
/// </summary>
public StructuralSet<T> Add(T value)
{
var valueString = GetValueString(value);
if (!_innerMap.ContainsKey(valueString))
{
_innerMap[valueString] = value;
}
return this;
}
/// <summary>
/// Checks if the set contains the specified value
/// </summary>
public bool Has(T value)
{
var valueString = GetValueString(value);
return _innerMap.ContainsKey(valueString);
}
/// <summary>
/// Removes a value from the set
/// </summary>
public bool Delete(T value)
{
var valueString = GetValueString(value);
return _innerMap.Remove(valueString);
}
/// <summary>
/// Clears all values from the set
/// </summary>
public void Clear() => _innerMap.Clear();
/// <summary>
/// Gets the number of values in the set
/// </summary>
public int Size => _innerMap.Count;
/// <summary>
/// Enumerates all values in the set
/// </summary>
public IEnumerator<T> GetEnumerator()
{
return _innerMap.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Gets all values in the set
/// </summary>
public IEnumerable<T> Values => _innerMap.Values;
}