diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/ISecureStorageProvider.cs b/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/ISecureStorageProvider.cs index 8b5e0db..9c45878 100644 --- a/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/ISecureStorageProvider.cs +++ b/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/ISecureStorageProvider.cs @@ -3,8 +3,13 @@ namespace StevanFreeborn.Extensions.Configuration.Secure.Storage; /// /// Defines a contract for a provider that stores and retrieves secure configuration data. /// -public interface ISecureStorageProvider +public interface ISecureStorageProvider : IDisposable { + /// + /// + /// + event EventHandler StorageChanged; + /// /// Reads the value associated with the specified key asynchronously. /// diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/JsonFileStorageProvider.cs b/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/JsonFileStorageProvider.cs index 309abb2..4884c3a 100644 --- a/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/JsonFileStorageProvider.cs +++ b/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/JsonFileStorageProvider.cs @@ -1,16 +1,52 @@ using System.Text.Json; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; + namespace StevanFreeborn.Extensions.Configuration.Secure.Storage; /// /// Provides a mechanism to store and retrieve secure configuration data in a JSON file. /// -/// The configuring the storage provider, including file paths. -public sealed class JsonFileStorageProvider(JsonStorageOptions options) : ISecureStorageProvider +public sealed class JsonFileStorageProvider : ISecureStorageProvider { private static readonly SemaphoreSlim FileLock = new(1, 1); - private readonly JsonStorageOptions _options = options - ?? throw new ArgumentNullException(nameof(options)); + private readonly PhysicalFileProvider? _fileProvider; + private readonly IDisposable? _changeTokenRegistration; + private readonly JsonStorageOptions _options; + + /// + public event EventHandler? StorageChanged; + + /// + /// Provides a mechanism to store and retrieve secure configuration data in a JSON file. + /// + /// The configuring the storage provider, including file paths. + public JsonFileStorageProvider(JsonStorageOptions options) + { + _options = options + ?? throw new ArgumentNullException(nameof(options)); + + var directory = Path.GetDirectoryName(_options.FullPath); + + if (string.IsNullOrWhiteSpace(directory) is false && Directory.Exists(directory)) + { + _fileProvider = new PhysicalFileProvider(directory) + { + UseActivePolling = true, + UsePollingFileWatcher = true, + }; + + _changeTokenRegistration = ChangeToken.OnChange( + () => _fileProvider.Watch(_options.FileName), + () => + { + Thread.Sleep(250); + StorageChanged?.Invoke(this, EventArgs.Empty); + } + ); + } + } /// /// Reads the value associated with the specified key from the JSON file asynchronously. @@ -87,10 +123,14 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options) : ISecur } /// - /// Acquires an exclusive lock and loads the configuration data from the JSON file. + /// /// - /// A cancellation token to observe while waiting for the lock or during the load operation. - /// A dictionary containing the loaded configuration data. + public void Dispose() + { + _changeTokenRegistration?.Dispose(); + _fileProvider?.Dispose(); + } + private async Task> AcquireLockAndLoadAsync(CancellationToken ct) { await FileLock.WaitAsync(ct).ConfigureAwait(false); @@ -105,11 +145,6 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options) : ISecur } } - /// - /// Loads the configuration data from the JSON file. - /// - /// A cancellation token to observe while loading the data. - /// A dictionary containing the loaded configuration data, or an empty dictionary if the file does not exist or is empty. private async Task> LoadAsync(CancellationToken ct) { if (File.Exists(_options.FullPath) is false) @@ -117,7 +152,16 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options) : ISecur return []; } - using var stream = new FileStream(_options.FullPath, FileMode.Open, FileAccess.Read); + var stream = new FileStream( + _options.FullPath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + bufferSize: 4096, + FileOptions.Asynchronous | FileOptions.SequentialScan + ); + + await using var _ = stream.ConfigureAwait(false); if (stream.Length is 0) { @@ -130,24 +174,22 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options) : ISecur return data ?? []; } - /// - /// Saves the configuration data to the JSON file. - /// - /// The configuration data to save. - /// A cancellation token to observe while saving the data. - /// A task that represents the asynchronous save operation. private async Task SaveAsync(Dictionary data, CancellationToken ct) { Directory.CreateDirectory(_options.DirectoryPath); - using var stream = new FileStream( + var stream = new FileStream( _options.FullPath, FileMode.Create, FileAccess.Write, - FileShare.None + FileShare.None, + bufferSize: 4096, + FileOptions.Asynchronous ); + await using var _ = stream.ConfigureAwait(false); + await JsonSerializer.SerializeAsync(stream, data, SecureConfigJsonContext.Default.DictionaryStringString, ct) .ConfigureAwait(false); } -} +} \ No newline at end of file