Skip to content

Commit

Permalink
Add write file stream (#290)
Browse files Browse the repository at this point in the history
  • Loading branch information
garcipat authored Sep 5, 2023
1 parent 0eea68f commit 871be38
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 17 deletions.
27 changes: 25 additions & 2 deletions src/Foundatio.TestHarness/Storage/FileStorageTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
using System.Threading.Tasks;
using System.Xml.Linq;
using Exceptionless;
using Foundatio.Xunit;
using Foundatio.Storage;
using Foundatio.Tests.Utility;
using Xunit;
using Foundatio.Utility;
using Foundatio.Xunit;
using Microsoft.Extensions.Logging;
using Xunit;
using Xunit.Abstractions;

namespace Foundatio.Tests.Storage {
Expand Down Expand Up @@ -455,6 +455,29 @@ public virtual async Task WillRespectStreamOffsetAsync() {
}
}

public virtual async Task WillWriteStreamContentAsync() {

const string testContent = "test";
const string path = "created.txt";

var storage = GetStorage();
if (storage == null)
return;

await ResetAsync(storage);

using (storage) {

using (var writer = new StreamWriter(await storage.GetFileStreamAsync(path, FileAccess.ReadWrite), Encoding.UTF8, 1024, false)) {
await writer.WriteAsync(testContent);
}

var content = await storage.GetFileContentsAsync(path);

Assert.Equal(testContent, content);
}
}

protected virtual async Task ResetAsync(IFileStorage storage) {
if (storage == null)
return;
Expand Down
40 changes: 28 additions & 12 deletions src/Foundatio/Storage/FolderFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,47 @@ public FolderFileStorage(FolderFileStorageOptions options) {
folder += Path.DirectorySeparatorChar;

Folder = folder;

_logger.LogInformation("Creating {Directory} directory", folder);
Directory.CreateDirectory(folder);
}

public FolderFileStorage(Builder<FolderFileStorageOptionsBuilder, FolderFileStorageOptions> config)
public FolderFileStorage(Builder<FolderFileStorageOptionsBuilder, FolderFileStorageOptions> config)
: this(config(new FolderFileStorageOptionsBuilder()).Build()) { }

public string Folder { get; set; }
ISerializer IHaveSerializer.Serializer => _serializer;

public Task<Stream> GetFileStreamAsync(string path, CancellationToken cancellationToken = default) {
public Task<Stream> GetFileStreamAsync(string path, CancellationToken cancellationToken = default)
=> GetFileStreamAsync(path, FileAccess.Read, cancellationToken);

public Task<Stream> GetFileStreamAsync(string path, FileAccess fileAccess, CancellationToken cancellationToken = default) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));

string normalizedPath = path.NormalizePath();
_logger.LogTrace("Getting file stream for {Path}", normalizedPath);
var fullPath = Path.Combine(Folder, normalizedPath);
if (fileAccess != FileAccess.Read) {
CreateFileStream(fullPath).Dispose();
}

var fileMode = GetFileModeForFileAccess(fileAccess);

try {
return Task.FromResult<Stream>(File.OpenRead(Path.Combine(Folder, normalizedPath)));
return Task.FromResult<Stream>(File.Open(fullPath, fileMode, fileAccess));
} catch (IOException ex) when (ex is FileNotFoundException or DirectoryNotFoundException) {
_logger.LogError(ex, "Unable to get file stream for {Path}: {Message}", normalizedPath, ex.Message);
return Task.FromResult<Stream>(null);
}
}
private FileMode GetFileModeForFileAccess(FileAccess fileAccess) {
return fileAccess switch {
FileAccess.Read => FileMode.Open,
FileAccess.Write => FileMode.Create,
FileAccess.ReadWrite => FileMode.OpenOrCreate,
_ => throw new ArgumentOutOfRangeException(nameof(fileAccess), fileAccess, null)
};
}

public Task<FileSpec> GetFileInfoAsync(string path) {
if (String.IsNullOrEmpty(path))
Expand Down Expand Up @@ -147,7 +163,7 @@ public Task<bool> RenameFileAsync(string path, string newPath, CancellationToken
} catch (IOException ex) {
_logger.LogDebug(ex, "Error renaming {Path} to {NewPath}: Deleting {NewFullPath}", normalizedPath, normalizedNewPath, newFullPath);
File.Delete(newFullPath);

_logger.LogTrace("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath);
File.Move(oldFullPath, newFullPath);
}
Expand Down Expand Up @@ -207,7 +223,7 @@ public Task<bool> DeleteFileAsync(string path, CancellationToken cancellationTok

public Task<int> DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) {
int count = 0;

if (String.IsNullOrEmpty(searchPattern) || searchPattern == "*") {
if (Directory.Exists(Folder)) {
_logger.LogInformation("Deleting {Directory} directory", Folder);
Expand Down Expand Up @@ -251,7 +267,7 @@ public Task<int> DeleteFilesAsync(string searchPattern = null, CancellationToken

_logger.LogTrace("Finished deleting {FileCount} files matching {SearchPattern}", count, searchPattern);
return Task.FromResult(count);

}

public async Task<PagedFileListResult> GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default) {
Expand Down Expand Up @@ -293,18 +309,18 @@ private NextPageResult GetFiles(string searchPattern, int page, int pageSize) {
Size = info.Length
});
}

bool hasMore = false;
if (list.Count == pagingLimit) {
hasMore = true;
list.RemoveAt(pagingLimit - 1);
}

return new NextPageResult {
Success = true,
HasMore = hasMore,
Success = true,
HasMore = hasMore,
Files = list,
NextPageFunc = hasMore ? _ => Task.FromResult(GetFiles(searchPattern, page + 1, pageSize)) : null
NextPageFunc = hasMore ? _ => Task.FromResult(GetFiles(searchPattern, page + 1, pageSize)) : null
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/Foundatio/Storage/IFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

namespace Foundatio.Storage {
public interface IFileStorage : IHaveSerializer, IDisposable {
[Obsolete($"Use {nameof(GetFileStreamAsync)} with {nameof(FileAccess)} instead to define read or write behaviour of stream.")]
Task<Stream> GetFileStreamAsync(string path, CancellationToken cancellationToken = default);
Task<Stream> GetFileStreamAsync(string path, FileAccess fileAccess, CancellationToken cancellationToken = default);
Task<FileSpec> GetFileInfoAsync(string path);
Task<bool> ExistsAsync(string path);
Task<bool> SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default);
Expand Down
7 changes: 5 additions & 2 deletions src/Foundatio/Storage/InMemoryFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@ public InMemoryFileStorage(Builder<InMemoryFileStorageOptionsBuilder, InMemoryFi
public long MaxFiles { get; set; }
ISerializer IHaveSerializer.Serializer => _serializer;

public Task<Stream> GetFileStreamAsync(string path, CancellationToken cancellationToken = default) {
public Task<Stream> GetFileStreamAsync(string path, CancellationToken cancellationToken = default) =>
GetFileStreamAsync(path, FileAccess.Read, cancellationToken);

public Task<Stream> GetFileStreamAsync(string path, FileAccess fileAccess, CancellationToken cancellationToken = default) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));

string normalizedPath = path.NormalizePath();
_logger.LogTrace("Getting file stream for {Path}", normalizedPath);

lock (_lock) {
if (!_storage.ContainsKey(normalizedPath)) {
_logger.LogError("Unable to get file stream for {Path}: File Not Found", normalizedPath);
Expand Down
5 changes: 4 additions & 1 deletion src/Foundatio/Storage/ScopedFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ public ScopedFileStorage(IFileStorage storage, string scope) {
public string Scope { get; private set; }
ISerializer IHaveSerializer.Serializer => UnscopedStorage.Serializer;

public Task<Stream> GetFileStreamAsync(string path, CancellationToken cancellationToken = default) {
public Task<Stream> GetFileStreamAsync(string path, CancellationToken cancellationToken = default)
=> GetFileStreamAsync(path, FileAccess.Read, cancellationToken);

public Task<Stream> GetFileStreamAsync(string path, FileAccess fileAccess, CancellationToken cancellationToken = default) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));

Expand Down
5 changes: 5 additions & 0 deletions tests/Foundatio.Tests/Storage/FolderFileStorageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,10 @@ public override Task CanRoundTripSeekableStreamAsync() {
public override Task WillRespectStreamOffsetAsync() {
return base.WillRespectStreamOffsetAsync();
}

[Fact]
public override Task WillWriteStreamContentAsync() {
return base.WillWriteStreamContentAsync();
}
}
}

0 comments on commit 871be38

Please sign in to comment.