diff --git a/DeepDrftContent/FileDatabase/FileDatabase.csproj b/DeepDrftContent/FileDatabase/FileDatabase.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/DeepDrftContent/FileDatabase/FileDatabase.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/DeepDrftContent/FileDatabase/Models/EntryKey.cs b/DeepDrftContent/FileDatabase/Models/EntryKey.cs new file mode 100644 index 0000000..71040e5 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Models/EntryKey.cs @@ -0,0 +1,9 @@ +namespace DeepDrftContent.FileDatabase.Models; + +/// +/// Represents a key for entries in the file database system. +/// Combines a string key with a media vault type for type-safe operations. +/// +/// The string identifier for the entry +/// The media vault type this entry belongs to +public record EntryKey(string Key, MediaVaultType Type); diff --git a/DeepDrftContent/FileDatabase/Models/IIndex.cs b/DeepDrftContent/FileDatabase/Models/IIndex.cs new file mode 100644 index 0000000..867f3e1 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Models/IIndex.cs @@ -0,0 +1,27 @@ +namespace DeepDrftContent.FileDatabase.Models; + +/// +/// Base interface for all index types +/// +public interface IIndex +{ + /// + /// Gets the key identifier for this index + /// + string GetKey(); + + /// + /// Gets all entry keys in this index + /// + IReadOnlyList GetEntries(); + + /// + /// Gets the number of entries in this index + /// + int GetEntriesSize(); + + /// + /// Checks if the index contains the specified entry key + /// + bool HasEntry(EntryKey entryKey); +} diff --git a/DeepDrftContent/FileDatabase/Models/IndexData.cs b/DeepDrftContent/FileDatabase/Models/IndexData.cs new file mode 100644 index 0000000..c5f0fe4 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Models/IndexData.cs @@ -0,0 +1,112 @@ +using DeepDrftContent.FileDatabase.Utils; + +namespace DeepDrftContent.FileDatabase.Models; + +/// +/// Base class for index data used in serialization +/// +public abstract class IndexData +{ + public string IndexKey { get; } + + protected IndexData(string indexKey) + { + IndexKey = indexKey; + } +} + +/// +/// Serializable data for directory indexes +/// +public class DirectoryIndexData : IndexData +{ + public List 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; + } +} + +/// +/// Serializable data for vault indexes +/// +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; + } +} + +/// +/// Directory index implementation using StructuralSet for entries +/// +public class DirectoryIndex : IndexData, IIndex +{ + public StructuralSet Entries { get; } + + public DirectoryIndex(DirectoryIndexData indexData) : base(indexData.IndexKey) + { + Entries = new StructuralSet(); + // Load entries from data + foreach (var entry in indexData.Entries) + { + Entries.Add(entry); + } + } + + public string GetKey() => IndexKey; + + public IReadOnlyList 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); +} + +/// +/// Vault index implementation using StructuralMap for entries with metadata +/// +public class VaultIndex : IndexData, IIndex +{ + public StructuralMap Entries { get; } + + public VaultIndex(VaultIndexData indexData) : base(indexData.IndexKey) + { + Entries = new StructuralMap(); + // Load entries from data + foreach (var (key, value) in indexData.Entries) + { + Entries.Set(key, value); + } + } + + public string GetKey() => IndexKey; + + public IReadOnlyList 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); +} diff --git a/DeepDrftContent/FileDatabase/Models/MediaFactories.cs b/DeepDrftContent/FileDatabase/Models/MediaFactories.cs new file mode 100644 index 0000000..1db85eb --- /dev/null +++ b/DeepDrftContent/FileDatabase/Models/MediaFactories.cs @@ -0,0 +1,147 @@ +namespace DeepDrftContent.FileDatabase.Models; + +/// +/// Type mappings for media vault types to their corresponding classes +/// +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}") + }; +} + +/// +/// Factory for creating metadata objects based on vault type +/// +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(MediaVaultType type, string entryKey, string extension, double aspectRatio = 1.0) + where T : MetaData + { + var metaData = Create(type, entryKey, extension, aspectRatio); + return (T)metaData; + } +} + +/// +/// Factory for creating media parameter objects +/// +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(MediaVaultType type, FileBinary fileBinary, MetaData metaData) + { + var parameters = Create(type, fileBinary, metaData); + return (T)parameters; + } +} + +/// +/// Factory for creating media binary objects from parameters +/// +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(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(MediaVaultType type, object mediaBinaryDto) where T : FileBinary + { + var binary = From(type, mediaBinaryDto); + return (T)binary; + } +} + +/// +/// Factory for creating DTO objects from media binaries +/// +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(MediaVaultType type, object mediaBinary) + { + var dto = From(type, mediaBinary); + return (T)dto; + } +} diff --git a/DeepDrftContent/FileDatabase/Models/MediaModels.cs b/DeepDrftContent/FileDatabase/Models/MediaModels.cs new file mode 100644 index 0000000..a54d278 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Models/MediaModels.cs @@ -0,0 +1,175 @@ +namespace DeepDrftContent.FileDatabase.Models; + +/// +/// Parameters for creating a FileBinary +/// +/// The binary data +/// The size of the data in bytes +public record FileBinaryParams(byte[] Buffer, int Size); + +/// +/// Base class for file binary data +/// +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)); + } +} + +/// +/// DTO for FileBinary serialization +/// +/// Base64 encoded binary data +/// Size of the original data +public record FileBinaryDto(string Base64, int Size) +{ + public FileBinaryDto(FileBinary fileBinary) : this( + Convert.ToBase64String(fileBinary.Buffer), + fileBinary.Size) { } +} + +/// +/// Parameters for creating a MediaBinary +/// +/// The binary data +/// The size of the data in bytes +/// The file extension +public record MediaBinaryParams(byte[] Buffer, int Size, string Extension) + : FileBinaryParams(Buffer, Size); + +/// +/// Media binary with extension information +/// +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); + } +} + +/// +/// DTO for MediaBinary serialization +/// +/// Base64 encoded binary data +/// Size of the original data +/// MIME type of the media +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)) { } +} + +/// +/// Parameters for creating an ImageBinary +/// +/// The binary data +/// The size of the data in bytes +/// The file extension +/// The aspect ratio of the image +public record ImageBinaryParams(byte[] Buffer, int Size, string Extension, double AspectRatio) + : MediaBinaryParams(Buffer, Size, Extension); + +/// +/// Image binary with aspect ratio information +/// +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); + } +} + +/// +/// DTO for ImageBinary serialization +/// +/// Base64 encoded binary data +/// Size of the original data +/// MIME type of the media +/// The aspect ratio of the image +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) { } +} + +/// +/// Utility class for MIME type and extension conversions +/// +public static class MimeTypeExtensions +{ + private static readonly Dictionary 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 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"; + } +} diff --git a/DeepDrftContent/FileDatabase/Models/MediaVaultType.cs b/DeepDrftContent/FileDatabase/Models/MediaVaultType.cs new file mode 100644 index 0000000..3ed2263 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Models/MediaVaultType.cs @@ -0,0 +1,10 @@ +namespace DeepDrftContent.FileDatabase.Models; + +/// +/// Enum representing different types of media vaults +/// +public enum MediaVaultType +{ + Media, + Image +} diff --git a/DeepDrftContent/FileDatabase/Models/MetaData.cs b/DeepDrftContent/FileDatabase/Models/MetaData.cs new file mode 100644 index 0000000..12d84d5 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Models/MetaData.cs @@ -0,0 +1,17 @@ +namespace DeepDrftContent.FileDatabase.Models; + +/// +/// Base metadata for media entries +/// +/// The key used to identify the media file +/// The file extension of the media +public record MetaData(string MediaKey, string Extension); + +/// +/// Extended metadata for image entries, including aspect ratio +/// +/// The key used to identify the media file +/// The file extension of the media +/// The aspect ratio of the image +public record ImageMetaData(string MediaKey, string Extension, double AspectRatio) + : MetaData(MediaKey, Extension); diff --git a/DeepDrftContent/FileDatabase/README.md b/DeepDrftContent/FileDatabase/README.md new file mode 100644 index 0000000..26ca9b2 --- /dev/null +++ b/DeepDrftContent/FileDatabase/README.md @@ -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` + - 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`**: JSON-based structural equality for complex keys +- **`StructuralSet`**: 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` and `ValueTask` +- **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( + 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. diff --git a/DeepDrftContent/FileDatabase/Services/FileDatabase.cs b/DeepDrftContent/FileDatabase/Services/FileDatabase.cs new file mode 100644 index 0000000..09613d3 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Services/FileDatabase.cs @@ -0,0 +1,159 @@ +using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.FileDatabase.Utils; + +namespace DeepDrftContent.FileDatabase.Services; + +/// +/// Main file database class that orchestrates multiple media vaults +/// +public class FileDatabase : DirectoryIndexDirectory +{ + private readonly StructuralMap _vaults; + + /// + /// Factory method to create a FileDatabase instance + /// + public static async Task 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(); + } + + /// + /// Initializes all vaults found in the index + /// + private async Task InitVaultsAsync() + { + foreach (var vaultKey in GetIndexEntries()) + { + await InitVaultAsync(vaultKey); + } + } + + /// + /// Initializes a specific vault + /// + 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); + } + } + + /// + /// Checks if a vault exists for the given key + /// + public bool HasVault(EntryKey vaultKey) + { + return _vaults.Has(vaultKey); + } + + /// + /// Gets a vault by key + /// + public MediaVault? GetVault(EntryKey vaultKey) + { + return HasVault(vaultKey) ? _vaults.Get(vaultKey) : null; + } + + /// + /// Creates a new vault + /// + 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; + } + } + + /// + /// Loads a resource from a specific vault + /// + public async Task LoadResourceAsync(MediaVaultType vaultType, EntryKey vaultKey, EntryKey entryKey) + where T : FileBinary + { + try + { + var vault = _vaults.Get(vaultKey); + if (vault != null) + { + return await vault.GetEntryAsync(vaultType, entryKey); + } + } + catch + { + // Swallow exceptions and return null, matching TypeScript behavior + } + + return null; + } + + /// + /// Registers a resource in a specific vault + /// + public async Task 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; + } + + /// + /// Gets all vault keys managed by this database + /// + public IReadOnlyList GetVaultKeys() + { + return _vaults.Keys.ToList().AsReadOnly(); + } + + /// + /// Gets the total number of vaults + /// + public int GetVaultCount() + { + return _vaults.Size; + } +} diff --git a/DeepDrftContent/FileDatabase/Services/IndexSystem.cs b/DeepDrftContent/FileDatabase/Services/IndexSystem.cs new file mode 100644 index 0000000..76fc656 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Services/IndexSystem.cs @@ -0,0 +1,162 @@ +using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.FileDatabase.Utils; + +namespace DeepDrftContent.FileDatabase.Services; + +/// +/// Enum representing different types of indexes +/// +public enum IndexType +{ + Directory, + Vault +} + +/// +/// Abstract base class for index containers +/// +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 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); + } +} + +/// +/// Factory for creating and loading indexes +/// +public class IndexFactory : AbstractIndexContainer +{ + public IndexFactory(string path, IndexType type) : base(path, type) { } + + /// + /// Builds an index by loading existing or creating new + /// + public async Task BuildIndexAsync() + { + try + { + return await LoadOrCreateIndexAsync(); + } + catch + { + return null; + } + } + + private async Task LoadOrCreateIndexAsync() + { + try + { + return await LoadIndexAsync(); + } + catch + { + return await CreateIndexAsync(); + } + } + + private async Task LoadIndexAsync() + { + var indexPath = Path.Combine(RootPath, "index"); + + IIndex result = Type switch + { + IndexType.Directory => new DirectoryIndex(await FileUtils.FetchObjectAsync(indexPath)), + IndexType.Vault => new VaultIndex(await FileUtils.FetchObjectAsync(indexPath)), + _ => throw new ArgumentException($"Unknown index type: {Type}") + }; + return result; + } + + private async Task 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; + } +} + +/// +/// Abstract base class for directory containers that manage indexes +/// +public abstract class IndexDirectory : AbstractIndexContainer +{ + protected IIndex Index { get; } + + protected IndexDirectory(string rootPath, IndexType type, IIndex index) : base(rootPath, type) + { + Index = index; + } + + protected IReadOnlyList GetIndexEntries() => Index.GetEntries(); + + public int GetIndexSize() => Index.GetEntriesSize(); + + public bool HasIndexEntry(EntryKey entryKey) => Index.HasEntry(entryKey); +} + +/// +/// Directory index directory implementation +/// +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); + } + } +} + +/// +/// Vault index directory implementation +/// +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); + } + } +} diff --git a/DeepDrftContent/FileDatabase/Services/MediaVault.cs b/DeepDrftContent/FileDatabase/Services/MediaVault.cs new file mode 100644 index 0000000..54027c9 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Services/MediaVault.cs @@ -0,0 +1,127 @@ +using System.Text.RegularExpressions; +using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.FileDatabase.Utils; + +namespace DeepDrftContent.FileDatabase.Services; + +/// +/// Abstract base class for media vaults that store and manage media files +/// +public abstract class MediaVault : VaultIndexDirectory +{ + protected MediaVault(string rootPath, VaultIndex index) : base(rootPath, index) { } + + /// + /// Generates a media key from an entry key by sanitizing special characters + /// + protected string GetMediaKey(string entryKey, string extension) + { + var sanitized = Regex.Replace(entryKey, @"[^a-zA-Z0-9]", "-"); + return $"{sanitized}{extension}"; + } + + /// + /// Gets the full file path for a media file from an entry key + /// + protected string GetMediaPathFromEntryKey(string entryKey, string extension) + { + return Path.Combine(RootPath, GetMediaKey(entryKey, extension)); + } + + /// + /// Gets the full file path for a media file from a media key + /// + protected string GetMediaPathFromMediaKey(string mediaKey) + { + return Path.Combine(RootPath, mediaKey); + } + + /// + /// Adds a new entry to the vault with the specified media data + /// + 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); + } + + /// + /// Retrieves an entry from the vault + /// + public async Task GetEntryAsync(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; + } + + /// + /// Extracts buffer and extension from a media object + /// + 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()}") + }; + } + + /// + /// Extracts aspect ratio from media object if it's an image + /// + private static double GetAspectRatio(object media) + { + return media is ImageBinary imageBinary ? imageBinary.AspectRatio : 1.0; + } +} + +/// +/// Concrete implementation of MediaVault for image storage +/// +public class ImageDirectoryVault : MediaVault +{ + private ImageDirectoryVault(string rootPath, VaultIndex index) : base(rootPath, index) { } + + /// + /// Factory method to create an ImageDirectoryVault instance + /// + public static async Task 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; + } + + +} diff --git a/DeepDrftContent/FileDatabase/Utils/FileUtils.cs b/DeepDrftContent/FileDatabase/Utils/FileUtils.cs new file mode 100644 index 0000000..dbbe6ce --- /dev/null +++ b/DeepDrftContent/FileDatabase/Utils/FileUtils.cs @@ -0,0 +1,118 @@ +using System.Text.Json; +using DeepDrftContent.FileDatabase.Models; + +namespace DeepDrftContent.FileDatabase.Utils; + +/// +/// Utility class for file I/O operations, matching the TypeScript file utilities +/// +public static class FileUtils +{ + /// + /// Reads a file and returns it as a FileBinary object + /// + /// Path to the media file + /// FileBinary containing the file data + public static async Task 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)); + } + + /// + /// Writes binary data to a file + /// + /// Path where to write the file + /// Binary data to write + 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(); + } + + /// + /// Serializes an object to a file using JSON + /// + /// Path to the file + /// Object to serialize + public static async Task PutObjectAsync(string filePath, T obj) + { + var options = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var json = JsonSerializer.Serialize(obj, options); + await File.WriteAllTextAsync(filePath, json); + } + + /// + /// Deserializes an object from a JSON file + /// + /// Path to the file + /// Deserialized object + public static async Task FetchObjectAsync(string filePath) + { + var json = await File.ReadAllTextAsync(filePath); + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var result = JsonSerializer.Deserialize(json, options); + return result ?? throw new InvalidOperationException($"Failed to deserialize object from {filePath}"); + } + + /// + /// Creates a directory if it doesn't exist + /// + /// Path to the directory + public static Task MakeVaultDirectoryAsync(string directoryPath) + { + Directory.CreateDirectory(directoryPath); + return Task.CompletedTask; + } + + /// + /// Checks if a file exists + /// + /// Path to check + /// True if file exists + public static bool FileExists(string filePath) + { + return File.Exists(filePath); + } + + /// + /// Checks if a directory exists + /// + /// Path to check + /// True if directory exists + public static bool DirectoryExists(string directoryPath) + { + return Directory.Exists(directoryPath); + } +} diff --git a/DeepDrftContent/FileDatabase/Utils/StructuralMap.cs b/DeepDrftContent/FileDatabase/Utils/StructuralMap.cs new file mode 100644 index 0000000..67ab43f --- /dev/null +++ b/DeepDrftContent/FileDatabase/Utils/StructuralMap.cs @@ -0,0 +1,107 @@ +using System.Collections; +using System.Text.Json; + +namespace DeepDrftContent.FileDatabase.Utils; + +/// +/// A map implementation that uses structural equality for keys by serializing them to JSON. +/// This provides the same behavior as the TypeScript StructuralMap. +/// +/// The key type +/// The value type +public class StructuralMap : IEnumerable> +{ + private readonly Dictionary> _innerMap = new(); + + /// + /// Converts a key to its string representation for structural comparison + /// + 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) + }; + } + + /// + /// Sets a key-value pair in the map + /// + public StructuralMap Set(TKey key, TValue value) + { + var keyString = GetKeyString(key); + _innerMap[keyString] = new KeyValuePair(key, value); + return this; + } + + /// + /// Gets a value by key, or default if not found + /// + public TValue? Get(TKey key) + { + var keyString = GetKeyString(key); + return _innerMap.TryGetValue(keyString, out var pair) ? pair.Value : default; + } + + /// + /// Checks if the map contains the specified key + /// + public bool Has(TKey key) + { + var keyString = GetKeyString(key); + return _innerMap.ContainsKey(keyString); + } + + /// + /// Removes a key-value pair from the map + /// + public bool Delete(TKey key) + { + var keyString = GetKeyString(key); + return _innerMap.Remove(keyString); + } + + /// + /// Clears all entries from the map + /// + public void Clear() => _innerMap.Clear(); + + /// + /// Gets the number of entries in the map + /// + public int Size => _innerMap.Count; + + /// + /// Enumerates all key-value pairs + /// + public IEnumerator> GetEnumerator() + { + return _innerMap.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets all keys in the map + /// + public IEnumerable Keys => _innerMap.Values.Select(pair => pair.Key); + + /// + /// Gets all values in the map + /// + public IEnumerable Values => _innerMap.Values.Select(pair => pair.Value); + + /// + /// Executes a callback for each key-value pair + /// + public void ForEach(Action> callback) + { + foreach (var (key, value) in this) + { + callback(value, key, this); + } + } +} diff --git a/DeepDrftContent/FileDatabase/Utils/StructuralSet.cs b/DeepDrftContent/FileDatabase/Utils/StructuralSet.cs new file mode 100644 index 0000000..4bdc000 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Utils/StructuralSet.cs @@ -0,0 +1,84 @@ +using System.Collections; +using System.Text.Json; + +namespace DeepDrftContent.FileDatabase.Utils; + +/// +/// A set implementation that uses structural equality for values by serializing them to JSON. +/// This provides the same behavior as the TypeScript StructuralSet. +/// +/// The value type +public class StructuralSet : IEnumerable +{ + private readonly Dictionary _innerMap = new(); + + /// + /// Converts a value to its string representation for structural comparison + /// + 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) + }; + } + + /// + /// Adds a value to the set + /// + public StructuralSet Add(T value) + { + var valueString = GetValueString(value); + if (!_innerMap.ContainsKey(valueString)) + { + _innerMap[valueString] = value; + } + return this; + } + + /// + /// Checks if the set contains the specified value + /// + public bool Has(T value) + { + var valueString = GetValueString(value); + return _innerMap.ContainsKey(valueString); + } + + /// + /// Removes a value from the set + /// + public bool Delete(T value) + { + var valueString = GetValueString(value); + return _innerMap.Remove(valueString); + } + + /// + /// Clears all values from the set + /// + public void Clear() => _innerMap.Clear(); + + /// + /// Gets the number of values in the set + /// + public int Size => _innerMap.Count; + + /// + /// Enumerates all values in the set + /// + public IEnumerator GetEnumerator() + { + return _innerMap.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets all values in the set + /// + public IEnumerable Values => _innerMap.Values; +}