FileDatabase engine port from snailbird-content TS/Node program
This commit is contained in:
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user