Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
# Mono auto generated files
mono_crash.*

# Temporary install scripts
dotnet-install.sh

# Build results
[Dd]ebug/
[Dd]ebugPublic/
Expand Down
2 changes: 1 addition & 1 deletion src/Extensions/CreateRelativeStorageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ public static async Task<IChildFile> CreateFileByRelativePathAsync(this IChildFi

throw new InvalidOperationException("Resolved item is not a file.");
}

/// <summary>
/// Traverses/creates folders along a relative path and yields each folder in order as it is visited/created.
/// Supports "." and ".." segments. If the last segment looks like a file (no trailing slash and contains '.'),
Expand Down
4 changes: 2 additions & 2 deletions src/Extensions/FileOpenExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ public static partial class FileExtensions
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
/// <returns>A task containing the requested stream.</returns>
public static Task<Stream> OpenReadAsync(this IFile file, CancellationToken cancellationToken = default) => file.OpenStreamAsync(FileAccess.Read, cancellationToken);

/// <summary>
/// Opens the file for writing.
/// </summary>
/// <param name="file">The file to open.</param>
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
/// <returns>A task containing the requested stream.</returns>
public static Task<Stream> OpenWriteAsync(this IFile file, CancellationToken cancellationToken = default) => file.OpenStreamAsync(FileAccess.Write, cancellationToken);

/// <summary>
/// Opens the file for reading and writing.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Extensions/GetItemByRelativePathExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public static async IAsyncEnumerable<IStorable> GetItemsAlongRelativePathAsync(t
var normalized = (relativePath ?? string.Empty).Replace('\\', '/');
// Split path into parts (use API available on target framework)
#if NETSTANDARD2_0
var parts = normalized.Split(['/'], StringSplitOptions.RemoveEmptyEntries);
var parts = normalized.Split(['/'], StringSplitOptions.RemoveEmptyEntries);
#else
var parts = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries);
#endif
Expand Down
2 changes: 1 addition & 1 deletion src/Extensions/GetRelativePathToExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static async Task<string> GetRelativePathToAsync(this IFolder from, IStor
{
to.Name,
};

cancellationToken.ThrowIfCancellationRequested();
await RecursiveAddParentToPathAsync(to);

Expand Down
4 changes: 2 additions & 2 deletions src/IModifiableFolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace OwlCore.Storage;
/// <summary>
/// Represents a folder that can be modified.
/// </summary>
public interface IModifiableFolder : IMutableFolder
public interface IModifiableFolder : IMutableFolder
{
/// <summary>
/// Deletes the provided storable item from this folder.
Expand All @@ -26,7 +26,7 @@ public interface IModifiableFolder : IMutableFolder
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
/// <returns>The newly created (or opened if existing) folder.</returns>
Task<IChildFolder> CreateFolderAsync(string name, bool overwrite = default, CancellationToken cancellationToken = default);

/// <summary>
/// Creates a new file with the desired name inside this folder.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/IStorable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ public interface IStorable
/// <summary>
/// Gets the name of the item, with the extension (if any).
/// </summary>
string Name { get; }
string Name { get; }
}
2 changes: 1 addition & 1 deletion src/IStorableChild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ public interface IStorableChild : IStorable
/// </summary>
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
/// <returns>The containing parent folder, if any.</returns>
Task<IFolder?> GetParentAsync(CancellationToken cancellationToken = default);
Task<IFolder?> GetParentAsync(CancellationToken cancellationToken = default);
}
12 changes: 6 additions & 6 deletions src/Memory/NonDisposableStreamWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ public NonDisposableStreamWrapper(Stream stream)
{
_stream = stream;
}

/// <inheritdoc />
protected override void Dispose(bool disposing)
{
}

/// <inheritdoc />
public override void Flush() => _stream.Flush();

Expand All @@ -50,16 +50,16 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati

/// <inheritdoc />
public override bool CanRead => _stream.CanRead;

/// <inheritdoc />
public override bool CanSeek => _stream.CanSeek;

/// <inheritdoc />
public override bool CanWrite => _stream.CanWrite;

/// <inheritdoc />
public override long Length => _stream.Length;

/// <inheritdoc />
public override long Position
{
Expand Down
15 changes: 14 additions & 1 deletion src/System/IO/StreamFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
namespace OwlCore.Storage.System.IO;

/// <summary>
/// A file implementation which holds a reference to the provided <see cref="Stream"/> and returns it in a non-disposable wrapper for <see cref="OpenStreamAsync"/>.
/// A file implementation which holds a reference to the provided <see cref="Stream"/> and returns it either wrapped in a non-disposable wrapper or directly, based on the <see cref="ShouldDispose"/> property.
/// </summary>
public class StreamFile : IFile
{
Expand All @@ -16,6 +16,12 @@ public class StreamFile : IFile
/// </summary>
public Stream Stream { get; }

/// <summary>
/// Gets or sets a value indicating whether the underlying stream should be disposed when the returned stream from <see cref="OpenStreamAsync"/> is disposed.
/// When true, the underlying stream is returned directly. When false, the stream is wrapped in a non-disposable wrapper.
/// </summary>
public bool ShouldDispose { get; set; }

/// <summary>
/// Creates a new instance of <see cref="StreamFile"/>.
/// </summary>
Expand All @@ -25,6 +31,7 @@ public StreamFile(Stream stream)
{
}


/// <summary>
/// Creates a new instance of <see cref="StreamFile"/>.
/// </summary>
Expand All @@ -36,8 +43,11 @@ public StreamFile(Stream stream, string id, string name)
Stream = stream;
Id = id;
Name = name;
ShouldDispose = false; // Default to false for backward compatibility
}



/// <inheritdoc />
public string Id { get; }

Expand All @@ -52,6 +62,9 @@ public Task<Stream> OpenStreamAsync(FileAccess accessMode = FileAccess.Read, Can
if (accessMode == 0)
throw new ArgumentOutOfRangeException(nameof(accessMode), $"{nameof(FileAccess)}.{accessMode} is not valid here.");

if (ShouldDispose)
return Task.FromResult(Stream);

return Task.FromResult<Stream>(new NonDisposableStreamWrapper(Stream));
}
}
2 changes: 1 addition & 1 deletion src/TruncatedStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
{
// For non-seekable streams, we track how many bytes have been consumed to enforce the window.
private long _consumed;

// For seekable streams, capture the starting offset to define the truncation window.
private readonly long _startOffset = Stream.CanSeek ? Stream.Position : 0;

Expand Down Expand Up @@ -167,7 +167,7 @@
}

/// <inheritdoc/>
public async ValueTask DisposeAsync()

Check warning on line 170 in src/TruncatedStream.cs

View workflow job for this annotation

GitHub Actions / build

'TruncatedStream.DisposeAsync()' hides inherited member 'Stream.DisposeAsync()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.

Check warning on line 170 in src/TruncatedStream.cs

View workflow job for this annotation

GitHub Actions / build

'TruncatedStream.DisposeAsync()' hides inherited member 'Stream.DisposeAsync()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
{
// Dispose the source stream asynchronously if it supports it
if (Stream is IAsyncDisposable asyncDisposableStream)
Expand Down
4 changes: 2 additions & 2 deletions tests/OwlCore.Storage.Tests/Memory/MemoryFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ public override async Task<IFile> CreateFileAsync()
{
var randomData = GenerateRandomData(256_000);
using var tempStr = new MemoryStream(randomData);

var memoryStream = new MemoryStream();
await tempStr.CopyToAsync(memoryStream);
memoryStream.Position = 0;

return new MemoryFile(memoryStream);

static byte[] GenerateRandomData(int length)
Expand Down
2 changes: 1 addition & 1 deletion tests/OwlCore.Storage.Tests/RelativePathExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public async Task GetItemsAlongRelativePath_YieldsExpectedSequence(string relati
.ToArray();

var yielded = new List<IStorable>();
await foreach (var item in _rootFolder.GetItemsAlongRelativePathAsync(relativePath ?? string.Empty))
await foreach (var item in _rootFolder.GetItemsAlongRelativePathAsync(relativePath ?? string.Empty))
{
yielded.Add(item);
}
Expand Down
153 changes: 152 additions & 1 deletion tests/OwlCore.Storage.Tests/StreamFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public override async Task<IFile> CreateFileAsync()
{
var randomData = GenerateRandomData(256_000);
using var tempStr = new MemoryStream(randomData);

var memoryStream = new MemoryStream();
await tempStr.CopyToAsync(memoryStream);
memoryStream.Position = 0;
Expand All @@ -27,5 +27,156 @@ static byte[] GenerateRandomData(int length)
return b;
}
}

[TestMethod]
public void ShouldDispose_DefaultValue_IsFalse()
{
// Arrange
var memoryStream = new MemoryStream();

// Act
var streamFile = new StreamFile(memoryStream);

// Assert
Assert.IsFalse(streamFile.ShouldDispose);
}

[TestMethod]
public void ShouldDispose_Property_CanBeSetAndGet()
{
// Arrange
var memoryStream = new MemoryStream();
var streamFile = new StreamFile(memoryStream);

// Act & Assert - Default value should be false
Assert.IsFalse(streamFile.ShouldDispose);

// Act & Assert - Should be able to set to true
streamFile.ShouldDispose = true;
Assert.IsTrue(streamFile.ShouldDispose);

// Act & Assert - Should be able to set to false
streamFile.ShouldDispose = false;
Assert.IsFalse(streamFile.ShouldDispose);
}

[TestMethod]
public void ShouldDispose_ConstructorWithIdAndName_SetsDefaultValue()
{
// Arrange
var memoryStream = new MemoryStream();
var id = "test-id";
var name = "test-name";

// Act
var streamFile = new StreamFile(memoryStream, id, name);

// Assert - Should default to false
Assert.IsFalse(streamFile.ShouldDispose);

// Test setting the property
streamFile.ShouldDispose = true;
Assert.IsTrue(streamFile.ShouldDispose);
}

[TestMethod]
public async Task OpenStreamAsync_ShouldDisposeTrue_ReturnsUnderlyingStream()
{
// Arrange
var memoryStream = new MemoryStream();
var streamFile = new StreamFile(memoryStream);
streamFile.ShouldDispose = true;

// Act
var resultStream = await streamFile.OpenStreamAsync();

// Assert
Assert.AreSame(memoryStream, resultStream);
}

[TestMethod]
public async Task OpenStreamAsync_ShouldDisposeFalse_ReturnsWrappedStream()
{
// Arrange
var memoryStream = new MemoryStream();
var streamFile = new StreamFile(memoryStream);
streamFile.ShouldDispose = false;

// Act
var resultStream = await streamFile.OpenStreamAsync();

// Assert
Assert.AreNotSame(memoryStream, resultStream);
Assert.AreEqual(typeof(OwlCore.Storage.Memory.NonDisposableStreamWrapper), resultStream.GetType());
}

[TestMethod]
public async Task OpenStreamAsync_ShouldDisposeTrue_DisposingReturnedStreamDisposesUnderlying()
{
// Arrange
var memoryStream = new MemoryStream();
var streamFile = new StreamFile(memoryStream);
streamFile.ShouldDispose = true;

// Act
var resultStream = await streamFile.OpenStreamAsync();
resultStream.Dispose();

// Assert
Assert.ThrowsException<ObjectDisposedException>(() => memoryStream.ReadByte());
}

[TestMethod]
public async Task OpenStreamAsync_ShouldDisposeFalse_DisposingReturnedStreamDoesNotDisposeUnderlying()
{
// Arrange
var data = new byte[] { 1, 2, 3, 4, 5 };
var memoryStream = new MemoryStream(data);
var streamFile = new StreamFile(memoryStream);
streamFile.ShouldDispose = false;

// Act
var resultStream = await streamFile.OpenStreamAsync();
resultStream.Dispose();

// Assert - underlying stream should still be usable
memoryStream.Position = 0;
Assert.AreEqual(1, memoryStream.ReadByte());
}

[TestMethod]
public async Task OpenStreamAsync_ShouldDisposeTrue_MultipleOpensReturnSameStream()
{
// Arrange
var memoryStream = new MemoryStream();
var streamFile = new StreamFile(memoryStream);
streamFile.ShouldDispose = true;

// Act
var stream1 = await streamFile.OpenStreamAsync();
var stream2 = await streamFile.OpenStreamAsync();

// Assert
Assert.AreSame(stream1, stream2);
Assert.AreSame(memoryStream, stream1);
}

[TestMethod]
public async Task OpenStreamAsync_ShouldDisposeFalse_MultipleOpensReturnDifferentWrappers()
{
// Arrange
var memoryStream = new MemoryStream();
var streamFile = new StreamFile(memoryStream);
streamFile.ShouldDispose = false;

// Act
var stream1 = await streamFile.OpenStreamAsync();
var stream2 = await streamFile.OpenStreamAsync();

// Assert
Assert.AreNotSame(stream1, stream2);
Assert.AreEqual(typeof(OwlCore.Storage.Memory.NonDisposableStreamWrapper), stream1.GetType());
Assert.AreEqual(typeof(OwlCore.Storage.Memory.NonDisposableStreamWrapper), stream2.GetType());
}
}
}
Loading