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
48 changes: 32 additions & 16 deletions src/Extensions/FileReadExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,24 +100,24 @@ public static async Task<string> ReadTextAsync(this IFile file, Encoding encodin
/// - <c>End</c> is exclusive; the sequence yields lines in [<c>Start</c>, <c>End</c>).
/// - If EOF is reached before <c>Start</c> or during enumeration, the sequence ends early without error.
/// </remarks>
#if NETSTANDARD
#if NETSTANDARD || NETSTANDARD2_0
public static async IAsyncEnumerable<string> ReadTextAsync(this IFile sourceFile, (int Start, int End) lineRange, [EnumeratorCancellation] CancellationToken cancellationToken)
#elif NET7_OR_GREATER
#else
public static async IAsyncEnumerable<string> ReadTextAsync(this IFile sourceFile, Range lineRange, [EnumeratorCancellation] CancellationToken cancellationToken)
#endif
{
using var fileStream = await sourceFile.OpenReadAsync(cancellationToken);
using var streamReader = new StreamReader(fileStream);

#if NETSTANDARD
#if NETSTANDARD || NETSTANDARD2_0
var start = lineRange.Start;
var end = lineRange.End;

if (start < 0 || end < 0)
throw new ArgumentOutOfRangeException(nameof(lineRange), "Start and End must be non-negative.");
if (end < start)
throw new ArgumentException("End must be greater than or equal to Start.", nameof(lineRange));
#elif NET7_OR_GREATER
#else
if (lineRange.Start.IsFromEnd || lineRange.End.IsFromEnd)
throw new ArgumentException("From-end indices (^) are not supported.", nameof(lineRange));

Expand All @@ -132,22 +132,22 @@ public static async IAsyncEnumerable<string> ReadTextAsync(this IFile sourceFile

for (var i = 0; i < start; i++)
{
#if NETSTANDARD
#if NETSTANDARD || NETSTANDARD2_0
cancellationToken.ThrowIfCancellationRequested();
if (await streamReader.ReadLineAsync() is null)
yield break;
#elif NET7_OR_GREATER
#else
if (await streamReader.ReadLineAsync(cancellationToken) is null)
yield break;
#endif
}

for (var i = start; i < end; i++)
{
#if NETSTANDARD
#if NETSTANDARD || NETSTANDARD2_0
cancellationToken.ThrowIfCancellationRequested();
var line = await streamReader.ReadLineAsync();
#elif NET7_OR_GREATER
#else
var line = await streamReader.ReadLineAsync(cancellationToken);
#endif
if (line is null)
Expand All @@ -157,6 +157,14 @@ public static async IAsyncEnumerable<string> ReadTextAsync(this IFile sourceFile
}
}

#if !(NETSTANDARD || NETSTANDARD2_0)
/// <summary>
/// Tuple-based shim for TFMs that support <see cref="Range"/>. Forwards to the <see cref="Range"/> overload.
/// </summary>
public static IAsyncEnumerable<string> ReadTextAsync(this IFile sourceFile, (int Start, int End) lineRange, CancellationToken cancellationToken)
=> ReadTextAsync(sourceFile, new Range(lineRange.Start, lineRange.End), cancellationToken);
#endif

/// <summary>
/// Reads a specific column range from each line within a line range in the text file.
/// </summary>
Expand All @@ -174,16 +182,16 @@ public static async IAsyncEnumerable<string> ReadTextAsync(this IFile sourceFile
/// - If EOF is reached before <c>lineRange.Start</c> or during enumeration, the sequence ends early without error.
/// - If <c>columnRange.Start</c> is beyond the end of a line, an empty string is yielded for that line.
/// </remarks>
#if NETSTANDARD
#if NETSTANDARD || NETSTANDARD2_0
public static async IAsyncEnumerable<string> ReadTextAsync(this IFile sourceFile, (int Start, int End) lineRange, (int Start, int End) columnRange, [EnumeratorCancellation] CancellationToken cancellationToken)
#elif NET7_OR_GREATER
#else
public static async IAsyncEnumerable<string> ReadTextAsync(this IFile sourceFile, Range lineRange, Range columnRange, [EnumeratorCancellation] CancellationToken cancellationToken)
#endif
{
using var fileStream = await sourceFile.OpenReadAsync(cancellationToken);
using var streamReader = new StreamReader(fileStream);

#if NETSTANDARD
#if NETSTANDARD || NETSTANDARD2_0
var start = lineRange.Start;
var end = lineRange.End;
var colStart = columnRange.Start;
Expand All @@ -198,7 +206,7 @@ public static async IAsyncEnumerable<string> ReadTextAsync(this IFile sourceFile
throw new ArgumentOutOfRangeException(nameof(columnRange), "Start and End must be non-negative.");
if (colEnd < colStart)
throw new ArgumentException("End must be greater than or equal to Start.", nameof(columnRange));
#elif NET7_OR_GREATER
#else
if (lineRange.Start.IsFromEnd || lineRange.End.IsFromEnd)
throw new ArgumentException("From-end indices (^) are not supported.", nameof(lineRange));
if (columnRange.Start.IsFromEnd || columnRange.End.IsFromEnd)
Expand All @@ -222,22 +230,22 @@ public static async IAsyncEnumerable<string> ReadTextAsync(this IFile sourceFile

for (var i = 0; i < start; i++)
{
#if NETSTANDARD
#if NETSTANDARD || NETSTANDARD2_0
cancellationToken.ThrowIfCancellationRequested();
if (await streamReader.ReadLineAsync() is null)
yield break;
#elif NET7_OR_GREATER
#else
if (await streamReader.ReadLineAsync(cancellationToken) is null)
yield break;
#endif
}

for (var i = start; i < end; i++)
{
#if NETSTANDARD
#if NETSTANDARD || NETSTANDARD2_0
cancellationToken.ThrowIfCancellationRequested();
var line = await streamReader.ReadLineAsync();
#elif NET7_OR_GREATER
#else
var line = await streamReader.ReadLineAsync(cancellationToken);
#endif
if (line is null)
Expand All @@ -255,4 +263,12 @@ public static async IAsyncEnumerable<string> ReadTextAsync(this IFile sourceFile
}
}
}

#if !(NETSTANDARD || NETSTANDARD2_0)
/// <summary>
/// Tuple-based shim for TFMs that support <see cref="Range"/>. Forwards to the <see cref="Range"/> overload.
/// </summary>
public static IAsyncEnumerable<string> ReadTextAsync(this IFile sourceFile, (int Start, int End) lineRange, (int Start, int End) columnRange, CancellationToken cancellationToken)
=> ReadTextAsync(sourceFile, new Range(lineRange.Start, lineRange.End), new Range(columnRange.Start, columnRange.End), cancellationToken);
#endif
}
45 changes: 30 additions & 15 deletions src/Extensions/FileWriteExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -54,9 +55,9 @@ public static async Task WriteTextAsync(this IFile file, string content, Encodin
/// <param name="lineRange">The line range (inclusive) from the content to write.</param>
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
/// <returns>A Task representing the asynchronous write operation.</returns>
#if NETSTANDARD
#if NETSTANDARD || NETSTANDARD2_0
public static async Task WriteTextAsync(this IFile file, string content, (int Start, int End) lineRange, CancellationToken cancellationToken = default)
#elif NET7_OR_GREATER
#else
public static async Task WriteTextAsync(this IFile file, string content, Range lineRange, CancellationToken cancellationToken = default)
#endif
{
Expand All @@ -65,10 +66,10 @@ public static async Task WriteTextAsync(this IFile file, string content, Range l

int startLine;
int endLineInclusive;
#if NETSTANDARD
#if NETSTANDARD || NETSTANDARD2_0
startLine = lineRange.Start;
endLineInclusive = lineRange.End;
#elif NET7_OR_GREATER
#else
startLine = lineRange.Start.Value;
endLineInclusive = lineRange.End.Value;
#endif
Expand All @@ -77,13 +78,27 @@ public static async Task WriteTextAsync(this IFile file, string content, Range l
{
await writer.WriteLineAsync(line);
}
#if NET7_OR_GREATER
await writer.FlushAsync(cancellationToken);
#else
#if NETSTANDARD || NETSTANDARD2_0
await writer.FlushAsync();
#else
await writer.FlushAsync(cancellationToken);
#endif
}

#if !(NETSTANDARD || NETSTANDARD2_0)
/// <summary>
/// Tuple-based shim for TFMs that support <see cref="Range"/>. Forwards to the <see cref="Range"/> overload.
/// </summary>
public static Task WriteTextAsync(this IFile file, string content, (int Start, int End) lineRange, CancellationToken cancellationToken = default)
=> WriteTextAsync(file, content, new Range(lineRange.Start, lineRange.End), cancellationToken);

/// <summary>
/// Tuple-based shim for TFMs that support <see cref="Range"/>. Forwards to the <see cref="Range"/> overload.
/// </summary>
public static Task WriteTextAsync(this IFile file, string content, (int Start, int End) lineRange, (int Start, int End) columnRange, CancellationToken cancellationToken = default)
=> WriteTextAsync(file, content, new Range(lineRange.Start, lineRange.End), new Range(columnRange.Start, columnRange.End), cancellationToken);
#endif

/// <summary>
/// Writes only the specified column range from each line within a line range from the provided <paramref name="content"/> into <paramref name="file"/> as UTF-8 text.
/// </summary>
Expand All @@ -93,9 +108,9 @@ public static async Task WriteTextAsync(this IFile file, string content, Range l
/// <param name="columnRange">The character range within each line to write (end exclusive).</param>
/// <param name="cancellationToken">A token that can be used to cancel the ongoing operation.</param>
/// <returns>A Task representing the asynchronous write operation.</returns>
#if NETSTANDARD
#if NETSTANDARD || NETSTANDARD2_0
public static async Task WriteTextAsync(this IFile file, string content, (int Start, int End) lineRange, (int Start, int End) columnRange, CancellationToken cancellationToken = default)
#elif NET7_OR_GREATER
#else
public static async Task WriteTextAsync(this IFile file, string content, Range lineRange, Range columnRange, CancellationToken cancellationToken = default)
#endif
{
Expand All @@ -106,12 +121,12 @@ public static async Task WriteTextAsync(this IFile file, string content, Range l
int endLineInclusive;
int startCol;
int endColExclusive;
#if NETSTANDARD
#if NETSTANDARD || NETSTANDARD2_0
startLine = lineRange.Start;
endLineInclusive = lineRange.End;
startCol = columnRange.Start;
endColExclusive = columnRange.End;
#elif NET7_OR_GREATER
#else
startLine = lineRange.Start.Value;
endLineInclusive = lineRange.End.Value;
startCol = columnRange.Start.Value;
Expand All @@ -122,10 +137,10 @@ public static async Task WriteTextAsync(this IFile file, string content, Range l
{
await writer.WriteLineAsync(line);
}
#if NET7_OR_GREATER
await writer.FlushAsync(cancellationToken);
#else
#if NETSTANDARD || NETSTANDARD2_0
await writer.FlushAsync();
#else
await writer.FlushAsync(cancellationToken);
#endif
}

Expand Down
30 changes: 29 additions & 1 deletion src/OwlCore.Storage.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<DebugType>embedded</DebugType>

<Author>Arlo Godfrey</Author>
<Version>0.13.1</Version>
<Version>0.14.0</Version>
<Product>OwlCore</Product>
<Description>The most flexible file system abstraction, ever. Built in partnership with the Windows App Community.

Expand All @@ -26,6 +26,34 @@
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageIcon>logo.png</PackageIcon>
<PackageReleaseNotes>
--- 0.14.0 ---
[New]
Added DepthFirstRecursiveFolder: depth-first async traversal wrapper over any IFolder with optional MaxDepth.
Added BreadthFirstRecursiveFolder: breadth-first async traversal wrapper over any IFolder with optional MaxDepth.
Added ItemsOverrideFolder: override a folder's GetItemsAsync by projecting its async enumerable.
Added ReadOnlyCompositeFolder: present a read-only folder view materialized from a set of files.
Added ParentOverrideChildFile: wrap any IFile with a custom parent, exposing it as IChildFile.
Added TruncatedFile: wrap a file and cap the readable length of its underlying stream.
Added TruncatedFilesFolder: wrap a folder so each file is exposed with a maximum byte length.
Added TruncatedStream: stream wrapper enforcing a maximum readable length.
Added FileReadExtensions.ReadTextAsync (line + column range): iterate a specific line range and slice per-line columns.
Added FileWriteExtensions.WriteTextAsync (line range; line + column range): write sliced text content into a file (line end inclusive, column end exclusive).
Added CopyExtensions.CopyToAsync: async file-to-file copy extension over IFile.

Added CreateByRelativePathAsync (start from folder or child file): create a file or folder at a relative path (supports "." and ".."; explicit target type).
Added CreateFolderByRelativePathAsync (start from folder or child file): convenience wrapper for creating folders by relative path.
Added CreateFileByRelativePathAsync (start from folder or child file): convenience wrapper for creating files by relative path.
Added CreateFoldersAlongRelativePathAsync (start from folder or child file): async sequence yielding each folder visited/created along a relative path.
Added CreateAlongRelativePathAsync (start from folder or child file): async sequence yielding parents, then the file when targeting a file; folders only when targeting a folder.

[Improvements]
Added StorableExtensions.GetItemsAlongRelativePathAsync: navigates a relative path from any IStorable, yielding each visited node in order (supports '.', '..', and normalizes path separators).
Added FolderExtensions.GetItemsAlongRelativePathToAsync: yields items along the path from a source IFolder to a descendant IStorableChild in traversal order.

[Tests]
Added traversal tests for DepthFirstRecursiveFolder and BreadthFirstRecursiveFolder covering BFS/DFS order, type filtering, and MaxDepth semantics.
Added range read/write tests for IFile covering line range and line + column range.

--- 0.13.1 ---
[Fixes]
Fixed the MoveFromAsync rename-capable extension not calling the proper rename-capable fallback overload.
Expand Down
Loading