diff --git a/src/Foundatio/Storage/InMemoryFileStorage.cs b/src/Foundatio/Storage/InMemoryFileStorage.cs index 91430923..26290b71 100644 --- a/src/Foundatio/Storage/InMemoryFileStorage.cs +++ b/src/Foundatio/Storage/InMemoryFileStorage.cs @@ -12,287 +12,286 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Foundatio.Storage +namespace Foundatio.Storage; + +public class InMemoryFileStorage : IFileStorage { - public class InMemoryFileStorage : IFileStorage + private readonly ConcurrentDictionary _storage = new(StringComparer.OrdinalIgnoreCase); + private readonly ISerializer _serializer; + protected readonly ILogger _logger; + + public InMemoryFileStorage() : this(o => o) { } + + public InMemoryFileStorage(InMemoryFileStorageOptions options) { - private readonly ConcurrentDictionary _storage = new(StringComparer.OrdinalIgnoreCase); - private readonly ISerializer _serializer; - protected readonly ILogger _logger; + if (options == null) + throw new ArgumentNullException(nameof(options)); - public InMemoryFileStorage() : this(o => o) { } + MaxFileSize = options.MaxFileSize; + MaxFiles = options.MaxFiles; + _serializer = options.Serializer ?? DefaultSerializer.Instance; + _logger = options.LoggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; + } - public InMemoryFileStorage(InMemoryFileStorageOptions options) - { - if (options == null) - throw new ArgumentNullException(nameof(options)); + public InMemoryFileStorage(Builder config) + : this(config(new InMemoryFileStorageOptionsBuilder()).Build()) { } - MaxFileSize = options.MaxFileSize; - MaxFiles = options.MaxFiles; - _serializer = options.Serializer ?? DefaultSerializer.Instance; - _logger = options.LoggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; - } + public long MaxFileSize { get; set; } + public long MaxFiles { get; set; } + ISerializer IHaveSerializer.Serializer => _serializer; - public InMemoryFileStorage(Builder config) - : this(config(new InMemoryFileStorageOptionsBuilder()).Build()) { } + [Obsolete($"Use {nameof(GetFileStreamAsync)} with {nameof(FileAccess)} instead to define read or write behaviour of stream")] + public Task GetFileStreamAsync(string path, CancellationToken cancellationToken = default) => + GetFileStreamAsync(path, StreamMode.Read, cancellationToken); - public long MaxFileSize { get; set; } - public long MaxFiles { get; set; } - ISerializer IHaveSerializer.Serializer => _serializer; + public Task GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - [Obsolete($"Use {nameof(GetFileStreamAsync)} with {nameof(FileAccess)} instead to define read or write behaviour of stream")] - public Task GetFileStreamAsync(string path, CancellationToken cancellationToken = default) => - GetFileStreamAsync(path, StreamMode.Read, cancellationToken); + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Getting file stream for {Path}", normalizedPath); - public Task GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default) + switch (streamMode) { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Getting file stream for {Path}", normalizedPath); - - switch (streamMode) - { - case StreamMode.Read: - if (_storage.TryGetValue(normalizedPath, out var file)) - return Task.FromResult(new MemoryStream(file.Data)); - - _logger.LogError("Unable to get file stream for {Path}: File Not Found", normalizedPath); - return Task.FromResult(null); - case StreamMode.Write: - var stream = new MemoryStream(); - var actionableStream = new ActionableStream(stream, () => - { - stream.Position = 0; - var contents = ReadBytes(stream); - if (contents.Length > MaxFileSize) - throw new ArgumentException($"File size {contents.Length.ToFileSizeDisplay()} exceeds the maximum size of {MaxFileSize.ToFileSizeDisplay()}."); - - AddOrUpdate(normalizedPath, contents); - }); - - return Task.FromResult(actionableStream); - default: - throw new ArgumentException("Invalid stream mode", nameof(streamMode)); - } + case StreamMode.Read: + if (_storage.TryGetValue(normalizedPath, out var file)) + return Task.FromResult(new MemoryStream(file.Data)); + + _logger.LogError("Unable to get file stream for {Path}: File Not Found", normalizedPath); + return Task.FromResult(null); + case StreamMode.Write: + var stream = new MemoryStream(); + var actionableStream = new ActionableStream(stream, () => + { + stream.Position = 0; + var contents = ReadBytes(stream); + if (contents.Length > MaxFileSize) + throw new ArgumentException($"File size {contents.Length.ToFileSizeDisplay()} exceeds the maximum size of {MaxFileSize.ToFileSizeDisplay()}."); + + AddOrUpdate(normalizedPath, contents); + }); + + return Task.FromResult(actionableStream); + default: + throw new ArgumentException("Invalid stream mode", nameof(streamMode)); } + } - public async Task GetFileInfoAsync(string path) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + public async Task GetFileInfoAsync(string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Getting file info for {Path}", normalizedPath); + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Getting file info for {Path}", normalizedPath); - if (await ExistsAsync(normalizedPath).AnyContext()) - return _storage[normalizedPath].Spec.DeepClone(); + if (await ExistsAsync(normalizedPath).AnyContext()) + return _storage[normalizedPath].Spec.DeepClone(); - _logger.LogError("Unable to get file info for {Path}: File Not Found", normalizedPath); - return null; - } + _logger.LogError("Unable to get file info for {Path}: File Not Found", normalizedPath); + return null; + } - public Task ExistsAsync(string path) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + public Task ExistsAsync(string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Checking if {Path} exists", normalizedPath); + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Checking if {Path} exists", normalizedPath); - return Task.FromResult(_storage.ContainsKey(normalizedPath)); - } + return Task.FromResult(_storage.ContainsKey(normalizedPath)); + } - private static byte[] ReadBytes(Stream input) - { - using var ms = new MemoryStream(); - input.CopyTo(ms); - return ms.ToArray(); - } + private static byte[] ReadBytes(Stream input) + { + using var ms = new MemoryStream(); + input.CopyTo(ms); + return ms.ToArray(); + } - public Task SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default) - { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - if (stream == null) - throw new ArgumentNullException(nameof(stream)); + public Task SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + if (stream == null) + throw new ArgumentNullException(nameof(stream)); - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Saving {Path}", normalizedPath); + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Saving {Path}", normalizedPath); - var contents = ReadBytes(stream); - if (contents.Length > MaxFileSize) - throw new ArgumentException($"File size {contents.Length.ToFileSizeDisplay()} exceeds the maximum size of {MaxFileSize.ToFileSizeDisplay()}."); + var contents = ReadBytes(stream); + if (contents.Length > MaxFileSize) + throw new ArgumentException($"File size {contents.Length.ToFileSizeDisplay()} exceeds the maximum size of {MaxFileSize.ToFileSizeDisplay()}."); - AddOrUpdate(normalizedPath, contents); + AddOrUpdate(normalizedPath, contents); - return Task.FromResult(true); - } + return Task.FromResult(true); + } - private void AddOrUpdate(string path, byte[] contents) + private void AddOrUpdate(string path, byte[] contents) + { + string normalizedPath = path.NormalizePath(); + + _storage.AddOrUpdate(normalizedPath, (new FileSpec { - string normalizedPath = path.NormalizePath(); - - _storage.AddOrUpdate(normalizedPath, (new FileSpec - { - Created = SystemClock.UtcNow, - Modified = SystemClock.UtcNow, - Path = normalizedPath, - Size = contents.Length - }, contents), (_, file) => (new FileSpec - { - Created = file.Spec.Created, - Modified = SystemClock.UtcNow, - Path = file.Spec.Path, - Size = contents.Length - }, contents)); - - while (MaxFiles >= 0 && _storage.Count > MaxFiles) - _storage.TryRemove(_storage.OrderByDescending(kvp => kvp.Value.Spec.Created).First().Key, out _); - } + Created = SystemClock.UtcNow, + Modified = SystemClock.UtcNow, + Path = normalizedPath, + Size = contents.Length + }, contents), (_, file) => (new FileSpec + { + Created = file.Spec.Created, + Modified = SystemClock.UtcNow, + Path = file.Spec.Path, + Size = contents.Length + }, contents)); + + while (MaxFiles >= 0 && _storage.Count > MaxFiles) + _storage.TryRemove(_storage.OrderByDescending(kvp => kvp.Value.Spec.Created).First().Key, out _); + } + + public Task RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + if (String.IsNullOrEmpty(newPath)) + throw new ArgumentNullException(nameof(newPath)); + + string normalizedPath = path.NormalizePath(); + string normalizedNewPath = newPath.NormalizePath(); + _logger.LogInformation("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath); - public Task RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) + if (!_storage.TryGetValue(normalizedPath, out var file)) { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - if (String.IsNullOrEmpty(newPath)) - throw new ArgumentNullException(nameof(newPath)); + _logger.LogDebug("Error renaming {Path} to {NewPath}: File not found", normalizedPath, normalizedNewPath); + return Task.FromResult(false); + } - string normalizedPath = path.NormalizePath(); - string normalizedNewPath = newPath.NormalizePath(); - _logger.LogInformation("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath); + AddOrUpdate(normalizedNewPath, file.Data.ToArray()); + _storage.TryRemove(normalizedPath, out _); - if (!_storage.TryGetValue(normalizedPath, out var file)) - { - _logger.LogDebug("Error renaming {Path} to {NewPath}: File not found", normalizedPath, normalizedNewPath); - return Task.FromResult(false); - } + return Task.FromResult(true); + } - AddOrUpdate(normalizedNewPath, file.Data.ToArray()); - _storage.TryRemove(normalizedPath, out _); + public Task CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + if (String.IsNullOrEmpty(targetPath)) + throw new ArgumentNullException(nameof(targetPath)); + + string normalizedPath = path.NormalizePath(); + string normalizedNewPath = targetPath.NormalizePath(); + _logger.LogInformation("Copying {Path} to {TargetPath}", normalizedPath, normalizedNewPath); - return Task.FromResult(true); - } - public Task CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) + if (!_storage.TryGetValue(normalizedPath, out var file)) { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); - if (String.IsNullOrEmpty(targetPath)) - throw new ArgumentNullException(nameof(targetPath)); + _logger.LogDebug("Error copying {Path} to {NewPath}: File not found", normalizedPath, normalizedNewPath); + return Task.FromResult(false); + } - string normalizedPath = path.NormalizePath(); - string normalizedNewPath = targetPath.NormalizePath(); - _logger.LogInformation("Copying {Path} to {TargetPath}", normalizedPath, normalizedNewPath); + AddOrUpdate(normalizedNewPath, file.Data.ToArray()); + return Task.FromResult(true); + } - if (!_storage.TryGetValue(normalizedPath, out var file)) - { - _logger.LogDebug("Error copying {Path} to {NewPath}: File not found", normalizedPath, normalizedNewPath); - return Task.FromResult(false); - } + public Task DeleteFileAsync(string path, CancellationToken cancellationToken = default) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); - AddOrUpdate(normalizedNewPath, file.Data.ToArray()); + string normalizedPath = path.NormalizePath(); + _logger.LogTrace("Deleting {Path}", normalizedPath); + if (_storage.TryRemove(normalizedPath, out _)) return Task.FromResult(true); - } - public Task DeleteFileAsync(string path, CancellationToken cancellationToken = default) + _logger.LogError("Unable to delete {Path}: File not found", normalizedPath); + return Task.FromResult(false); + } + + public Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) + { + if (String.IsNullOrEmpty(searchPattern) || searchPattern == "*") { - if (String.IsNullOrEmpty(path)) - throw new ArgumentNullException(nameof(path)); + _storage.Clear(); + return Task.FromResult(0); + } - string normalizedPath = path.NormalizePath(); - _logger.LogTrace("Deleting {Path}", normalizedPath); + searchPattern = searchPattern.NormalizePath(); + int count = 0; - if (_storage.TryRemove(normalizedPath, out _)) - return Task.FromResult(true); + if (searchPattern[searchPattern.Length - 1] == Path.DirectorySeparatorChar) + searchPattern = $"{searchPattern}*"; + else if (!searchPattern.EndsWith(Path.DirectorySeparatorChar + "*") && !Path.HasExtension(searchPattern)) + searchPattern = Path.Combine(searchPattern, "*"); - _logger.LogError("Unable to delete {Path}: File not found", normalizedPath); - return Task.FromResult(false); - } - - public Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) - { - if (String.IsNullOrEmpty(searchPattern) || searchPattern == "*") - { - _storage.Clear(); - return Task.FromResult(0); - } + var regex = new Regex($"^{Regex.Escape(searchPattern).Replace("\\*", ".*?")}$"); - searchPattern = searchPattern.NormalizePath(); - int count = 0; + var keys = _storage.Keys.Where(k => regex.IsMatch(k)).Select(k => _storage[k].Spec).ToList(); - if (searchPattern[searchPattern.Length - 1] == Path.DirectorySeparatorChar) - searchPattern = $"{searchPattern}*"; - else if (!searchPattern.EndsWith(Path.DirectorySeparatorChar + "*") && !Path.HasExtension(searchPattern)) - searchPattern = Path.Combine(searchPattern, "*"); + _logger.LogInformation("Deleting {FileCount} files matching {SearchPattern} (Regex={SearchPatternRegex})", keys.Count, searchPattern, regex); + foreach (var key in keys) + { + _logger.LogTrace("Deleting {Path}", key.Path); + _storage.TryRemove(key.Path, out _); + count++; + } - var regex = new Regex($"^{Regex.Escape(searchPattern).Replace("\\*", ".*?")}$"); + _logger.LogTrace("Finished deleting {FileCount} files matching {SearchPattern}", count, searchPattern); - var keys = _storage.Keys.Where(k => regex.IsMatch(k)).Select(k => _storage[k].Spec).ToList(); + return Task.FromResult(count); + } - _logger.LogInformation("Deleting {FileCount} files matching {SearchPattern} (Regex={SearchPatternRegex})", keys.Count, searchPattern, regex); - foreach (var key in keys) - { - _logger.LogTrace("Deleting {Path}", key.Path); - _storage.TryRemove(key.Path, out _); - count++; - } + public async Task GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default) + { + if (pageSize <= 0) + return PagedFileListResult.Empty; - _logger.LogTrace("Finished deleting {FileCount} files matching {SearchPattern}", count, searchPattern); + if (String.IsNullOrEmpty(searchPattern)) + searchPattern = "*"; - return Task.FromResult(count); - } + searchPattern = searchPattern.NormalizePath(); - public async Task GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default) - { - if (pageSize <= 0) - return PagedFileListResult.Empty; + var result = new PagedFileListResult(async s => await GetFilesAsync(searchPattern, 1, pageSize, cancellationToken)); + await result.NextPageAsync().AnyContext(); + return result; + } - if (String.IsNullOrEmpty(searchPattern)) - searchPattern = "*"; + private Task GetFilesAsync(string searchPattern, int page, int pageSize, CancellationToken cancellationToken = default) + { + var list = new List(); + int pagingLimit = pageSize; + int skip = (page - 1) * pagingLimit; + if (pagingLimit < Int32.MaxValue) + pagingLimit++; - searchPattern = searchPattern.NormalizePath(); + var regex = new Regex($"^{Regex.Escape(searchPattern).Replace("\\*", ".*?")}$"); - var result = new PagedFileListResult(async s => await GetFilesAsync(searchPattern, 1, pageSize, cancellationToken)); - await result.NextPageAsync().AnyContext(); - return result; - } + _logger.LogTrace(s => s.Property("Limit", pagingLimit).Property("Skip", skip), "Getting file list matching {SearchPattern}...", regex); + list.AddRange(_storage.Keys.Where(k => regex.IsMatch(k)).Select(k => _storage[k].Spec.DeepClone()).Skip(skip).Take(pagingLimit).ToList()); - private Task GetFilesAsync(string searchPattern, int page, int pageSize, CancellationToken cancellationToken = default) + bool hasMore = false; + if (list.Count == pagingLimit) { - var list = new List(); - int pagingLimit = pageSize; - int skip = (page - 1) * pagingLimit; - if (pagingLimit < Int32.MaxValue) - pagingLimit++; - - var regex = new Regex($"^{Regex.Escape(searchPattern).Replace("\\*", ".*?")}$"); - - _logger.LogTrace(s => s.Property("Limit", pagingLimit).Property("Skip", skip), "Getting file list matching {SearchPattern}...", regex); - list.AddRange(_storage.Keys.Where(k => regex.IsMatch(k)).Select(k => _storage[k].Spec.DeepClone()).Skip(skip).Take(pagingLimit).ToList()); - - bool hasMore = false; - if (list.Count == pagingLimit) - { - hasMore = true; - list.RemoveAt(pagingLimit - 1); - } - - return Task.FromResult(new NextPageResult - { - Success = true, - HasMore = hasMore, - Files = list, - NextPageFunc = hasMore ? async _ => await GetFilesAsync(searchPattern, page + 1, pageSize, cancellationToken) : null - }); + hasMore = true; + list.RemoveAt(pagingLimit - 1); } - public void Dispose() + return Task.FromResult(new NextPageResult { - _storage?.Clear(); - } + Success = true, + HasMore = hasMore, + Files = list, + NextPageFunc = hasMore ? async _ => await GetFilesAsync(searchPattern, page + 1, pageSize, cancellationToken) : null + }); + } + + public void Dispose() + { + _storage?.Clear(); } }