Skip to content

Commit

Permalink
Release 0.9.2
Browse files Browse the repository at this point in the history
- The stream returned by HttpFile.OpenStreamAsync() now includes the value of the Content-Length header as the stream Length.
- The stream returned by HttpFile.OpenStreamAsync() is now wrapped in a LazySeekStream, allowing for seeking and only advancing the underlying stream when strictly needed.
- Fixed an issue where ZipArchiveFolder.DeleteAsync wasn't correctly identifying the zip entry given the storage id.
- Fixed a redundant Id check in OwlCore.Storage.Tests.Archive.ZipArchive.CreateNewFolderAsyncTest_FolderWithNestedItems, substituting with a name check instead.
  • Loading branch information
Arlodotexe committed Nov 15, 2023
1 parent 0bbc8c0 commit 8baab6c
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 18 deletions.
6 changes: 4 additions & 2 deletions src/Archive/ZipArchiveFolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ public async Task DeleteAsync(IStorableChild item, CancellationToken cancellatio

if (item is ZipArchiveFolder folder)
{
if (!GetVirtualFolders().ContainsKey(folder.Id))
var itemEntryId = folder.Id.Replace(Id, "");

if (!GetVirtualFolders().ContainsKey(itemEntryId))
throw new FileNotFoundException("The item was not found in the folder.");

// Recursively remove any sub-entries
Expand All @@ -109,7 +111,7 @@ public async Task DeleteAsync(IStorableChild item, CancellationToken cancellatio
foreach (var entry in childEntries)
entry.Delete();

GetVirtualFolders().Remove(folder.Id);
GetVirtualFolders().Remove(itemEntryId);
}
else if (item is ReadOnlyZipArchiveFolder readOnlyFolder)
{
Expand Down
17 changes: 15 additions & 2 deletions src/HttpFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public HttpFile(string uri, HttpClient httpClient)
public string Name { get; init; }

/// <inheritdoc />
public Task<Stream> OpenStreamAsync(FileAccess accessMode = FileAccess.Read, CancellationToken cancellationToken = default)
public async Task<Stream> OpenStreamAsync(FileAccess accessMode = FileAccess.Read, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

Expand All @@ -80,6 +80,19 @@ public Task<Stream> OpenStreamAsync(FileAccess accessMode = FileAccess.Read, Can
if (accessMode == FileAccess.Write)
throw new NotSupportedException($"{nameof(FileAccess)}.{accessMode} is not supported over Http.");

return Client.GetStreamAsync(Uri);
var request = new HttpRequestMessage(HttpMethod.Get, Uri);
var response = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();

var contentStream = await response.Content.ReadAsStreamAsync();

// Extract the content length if available
long? length = response.Content.Headers.ContentLength;

if (length is long notNullLength)
contentStream = new LengthOverrideStream(contentStream, notNullLength);

// Return in a lazy seek-able wrapper.
return new LazySeekStream(contentStream);
}
}
188 changes: 188 additions & 0 deletions src/Internal/LazySeekStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using System;
using System.IO;

namespace OwlCore.Storage;

/// <summary>
/// Wraps around a non-seekable stream to enable seeking functionality with lazy loading of the source.
/// </summary>
internal class LazySeekStream : Stream
{
private Stream _originalStream;
private MemoryStream _memoryStream;

/// <summary>
/// Creates a new instance of <see cref="LazySeekStream"/>.
/// </summary>
/// <param name="stream"></param>
public LazySeekStream(Stream stream)
{
_originalStream = stream;

_memoryStream = new MemoryStream()
{
Capacity = (int)Length,
};
}

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

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

/// <inheritdoc />
public override bool CanWrite => false;

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

/// <inheritdoc />
public override long Position
{
get => _memoryStream.Position;
set
{
if (value < 0)
throw new IOException("An attempt was made to move the position before the beginning of the stream.");

// Check if the requested position is beyond the current length of the memory stream
if (value > _memoryStream.Length)
{
long additionalBytesNeeded = value - _memoryStream.Length;
var buffer = new byte[additionalBytesNeeded];
long totalBytesRead = 0;

while (totalBytesRead < additionalBytesNeeded)
{
int bytesRead = _originalStream.Read(buffer, (int)totalBytesRead, (int)(additionalBytesNeeded - totalBytesRead));
if (bytesRead == 0)
break; // End of the original stream reached

totalBytesRead += bytesRead;
}

// Write the newly read bytes to the end of the memory stream
_memoryStream.Seek(0, SeekOrigin.End);
_memoryStream.Write(buffer, 0, (int)totalBytesRead);
}

// Set the new position of the memory stream
_memoryStream.Position = value;
}
}

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

/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count)
{
int totalBytesRead = 0;

// Read from memory stream first
if (_memoryStream.Position < _memoryStream.Length)
{
totalBytesRead = _memoryStream.Read(buffer, offset, count);
if (totalBytesRead == count)
{
return totalBytesRead; // Complete read from memory stream
}

// Prepare to read the remaining data from the original stream
offset += totalBytesRead;
count -= totalBytesRead;
}

// Read the remaining data directly into the provided buffer
while (count > 0)
{
int bytesReadFromOriginalStream = _originalStream.Read(buffer, offset, count);
if (bytesReadFromOriginalStream == 0)
{
break; // End of the original stream reached
}

// Write the new data from the original stream into the memory stream
_memoryStream.Seek(0, SeekOrigin.End);
_memoryStream.Write(buffer, offset, bytesReadFromOriginalStream);

totalBytesRead += bytesReadFromOriginalStream;
offset += bytesReadFromOriginalStream;
count -= bytesReadFromOriginalStream;
}

return totalBytesRead;
}

/// <inheritdoc />
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Begin:
Position = offset;
break;
case SeekOrigin.Current:
Position = _memoryStream.Position + offset;
break;
case SeekOrigin.End:
Position = _originalStream.Length + offset;
break;
default:
throw new ArgumentOutOfRangeException(nameof(origin), "Invalid seek origin.");
}

return Position;
}

/// <inheritdoc />
public override void SetLength(long value)
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "Length must be non-negative.");

if (value < _memoryStream.Length)
{
// Truncate the memory stream
_memoryStream.SetLength(value);
}
else if (value > _memoryStream.Length)
{
long additionalBytesNeeded = value - _memoryStream.Length;

// Extend the memory stream with zeros or additional data from the original stream
if (_originalStream.CanRead && additionalBytesNeeded > 0)
{
var buffer = new byte[additionalBytesNeeded];
int bytesRead = _originalStream.Read(buffer, 0, buffer.Length);

_memoryStream.Seek(0, SeekOrigin.End);
_memoryStream.Write(buffer, 0, bytesRead);

if (bytesRead < additionalBytesNeeded)
{
// Fill the rest with zeros if the original stream didn't have enough data
var zeroFill = new byte[additionalBytesNeeded - bytesRead];
_memoryStream.Write(zeroFill, 0, zeroFill.Length);
}
}
else
{
// Fill with zeros if the original stream can't be read or no additional bytes are needed
_memoryStream.SetLength(value);
}
}
}

/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException($"Writing not supported by {nameof(LazySeekStream)}");

/// <inheritdoc />
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_memoryStream.Dispose();
_originalStream.Dispose();
}
}
71 changes: 71 additions & 0 deletions src/Internal/LengthOverrideStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.IO;

namespace OwlCore.Storage;

/// <summary>
/// A stream wrapper that allows overriding the Length property.
/// </summary>
internal class LengthOverrideStream : Stream
{
private readonly long _overriddenLength;

/// <summary>
/// Initializes a new instance of the <see cref="LengthOverrideStream"/> class.
/// </summary>
/// <param name="sourceStream">The underlying stream to wrap.</param>
/// <param name="overriddenLength">The length value to be returned by the Length property.</param>
public LengthOverrideStream(Stream sourceStream, long overriddenLength)
{
SourceStream = sourceStream ?? throw new ArgumentNullException(nameof(sourceStream));
_overriddenLength = overriddenLength;
}

/// <summary>
/// The underlying source stream being wrapped around.
/// </summary>
public Stream SourceStream { get; }

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

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

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

/// <inheritdoc />
public override long Length => _overriddenLength;

/// <inheritdoc />
public override long Position
{
get => SourceStream.Position;
set => SourceStream.Position = value;
}

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

/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count) => SourceStream.Read(buffer, offset, count);

/// <inheritdoc />
public override long Seek(long offset, SeekOrigin origin) => SourceStream.Seek(offset, origin);

/// <inheritdoc />
public override void SetLength(long value) => SourceStream.SetLength(value);

/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count) => SourceStream.Write(buffer, offset, count);

/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (disposing)
SourceStream.Dispose();

base.Dispose(disposing);
}
}
10 changes: 9 additions & 1 deletion src/OwlCore.Storage.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>

<Author>Arlo Godfrey</Author>
<Version>0.9.1</Version>
<Version>0.9.2</Version>
<Product>OwlCore</Product>
<Description>The most flexible file system abstraction, ever. Built in partnership with the UWP Community.

Expand All @@ -23,6 +23,14 @@
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageIcon>logo.png</PackageIcon>
<PackageReleaseNotes>
--- 0.9.2 ---
[Improvements]
The stream returned by HttpFile.OpenStreamAsync() now includes the value of the Content-Length header as the stream Length.
The stream returned by HttpFile.OpenStreamAsync() is now wrapped in a LazySeekStream, allowing for seeking and only advancing the underlying stream when strictly needed.

[Fixes]
Fixed an issue where ZipArchiveFolder.DeleteAsync wasn't correctly identifying the zip entry given the storage id.

--- 0.9.1 ---
[New]
HttpFile was given a new constructor accepting an HttpClient: HttpFile(string uri, HttpClient httpClient).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,31 +85,31 @@ public async Task CreateNewFolderAsyncTest_FolderWithNestedItems()

// Check each path
var fileRootEx = await root.GetItemAsync($"{root.Id}fileRoot");
Assert.AreEqual("fileRoot", fileRootEx.Id);
Assert.AreEqual("fileRoot", fileRootEx.Name);

var subAEx = await root.GetItemAsync($"{root.Id}subA/") as IChildFolder;
Assert.IsNotNull(subAEx);
Assert.AreEqual("subA/", subAEx.Id);
Assert.AreEqual("subA", subAEx.Name);

var fileAEx = await subAEx.GetItemAsync($"{root.Id}subA/fileA");
Assert.IsInstanceOfType<IFile>(fileAEx);
Assert.AreEqual("subA/fileA", fileAEx.Id);
Assert.AreEqual("fileA", fileAEx.Name);

var subBEx = await root.GetItemAsync($"{root.Id}subB/") as IChildFolder;
Assert.IsNotNull(subBEx);
Assert.AreEqual("subB/", subBEx.Id);
Assert.AreEqual("subB", subBEx.Name);

var fileBEx = await subBEx.GetItemAsync($"{root.Id}subB/fileB");
Assert.IsInstanceOfType<IFile>(fileBEx);
Assert.AreEqual("subB/fileB", fileBEx.Id);
Assert.AreEqual("fileB", fileBEx.Name);

var subCEx = await subBEx.GetItemAsync($"{root.Id}subB/subC/") as IChildFolder;
Assert.IsNotNull(subCEx);
Assert.AreEqual("subB/subC/", subCEx.Id);
Assert.AreEqual("subC", subCEx.Name);

var fileCEx = await subCEx.GetItemAsync($"{root.Id}subB/subC/fileC");
Assert.IsInstanceOfType<IFile>(fileCEx);
Assert.AreEqual("subB/subC/fileC", fileCEx.Id);
Assert.AreEqual("fileC", fileCEx.Name);
}

[TestMethod]
Expand Down
12 changes: 6 additions & 6 deletions tests/OwlCore.Storage.Tests/OwlCore.Storage.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Microsoft.TestPlatform" Version="17.5.0-preview-20221221-03" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Microsoft.TestPlatform" Version="17.8.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="OwlCore.Storage.CommonTests" Version="0.3.0" />
<PackageReference Include="OwlCore.Storage.CommonTests" Version="0.4.1" />
</ItemGroup>

<ItemGroup>
Expand Down

0 comments on commit 8baab6c

Please sign in to comment.