diff --git a/src/Archive/ZipArchiveFolder.cs b/src/Archive/ZipArchiveFolder.cs
index f96162e..82d03bc 100644
--- a/src/Archive/ZipArchiveFolder.cs
+++ b/src/Archive/ZipArchiveFolder.cs
@@ -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
@@ -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)
{
diff --git a/src/HttpFile.cs b/src/HttpFile.cs
index ad88080..dc39b23 100644
--- a/src/HttpFile.cs
+++ b/src/HttpFile.cs
@@ -70,7 +70,7 @@ public HttpFile(string uri, HttpClient httpClient)
public string Name { get; init; }
///
- public Task OpenStreamAsync(FileAccess accessMode = FileAccess.Read, CancellationToken cancellationToken = default)
+ public async Task OpenStreamAsync(FileAccess accessMode = FileAccess.Read, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -80,6 +80,19 @@ public Task 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);
}
}
\ No newline at end of file
diff --git a/src/Internal/LazySeekStream.cs b/src/Internal/LazySeekStream.cs
new file mode 100644
index 0000000..1112765
--- /dev/null
+++ b/src/Internal/LazySeekStream.cs
@@ -0,0 +1,188 @@
+using System;
+using System.IO;
+
+namespace OwlCore.Storage;
+
+///
+/// Wraps around a non-seekable stream to enable seeking functionality with lazy loading of the source.
+///
+internal class LazySeekStream : Stream
+{
+ private Stream _originalStream;
+ private MemoryStream _memoryStream;
+
+ ///
+ /// Creates a new instance of .
+ ///
+ ///
+ public LazySeekStream(Stream stream)
+ {
+ _originalStream = stream;
+
+ _memoryStream = new MemoryStream()
+ {
+ Capacity = (int)Length,
+ };
+ }
+
+ ///
+ public override bool CanRead => _memoryStream.CanRead;
+
+ ///
+ public override bool CanSeek => _memoryStream.CanSeek;
+
+ ///
+ public override bool CanWrite => false;
+
+ ///
+ public override long Length => _originalStream.Length;
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ public override void Flush() => _memoryStream.Flush();
+
+ ///
+ 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;
+ }
+
+ ///
+ 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;
+ }
+
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException($"Writing not supported by {nameof(LazySeekStream)}");
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ _memoryStream.Dispose();
+ _originalStream.Dispose();
+ }
+}
diff --git a/src/Internal/LengthOverrideStream.cs b/src/Internal/LengthOverrideStream.cs
new file mode 100644
index 0000000..5df0e73
--- /dev/null
+++ b/src/Internal/LengthOverrideStream.cs
@@ -0,0 +1,71 @@
+using System;
+using System.IO;
+
+namespace OwlCore.Storage;
+
+///
+/// A stream wrapper that allows overriding the Length property.
+///
+internal class LengthOverrideStream : Stream
+{
+ private readonly long _overriddenLength;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The underlying stream to wrap.
+ /// The length value to be returned by the Length property.
+ public LengthOverrideStream(Stream sourceStream, long overriddenLength)
+ {
+ SourceStream = sourceStream ?? throw new ArgumentNullException(nameof(sourceStream));
+ _overriddenLength = overriddenLength;
+ }
+
+ ///
+ /// The underlying source stream being wrapped around.
+ ///
+ public Stream SourceStream { get; }
+
+ ///
+ public override bool CanRead => SourceStream.CanRead;
+
+ ///
+ public override bool CanSeek => SourceStream.CanSeek;
+
+ ///
+ public override bool CanWrite => SourceStream.CanWrite;
+
+ ///
+ public override long Length => _overriddenLength;
+
+ ///
+ public override long Position
+ {
+ get => SourceStream.Position;
+ set => SourceStream.Position = value;
+ }
+
+ ///
+ public override void Flush() => SourceStream.Flush();
+
+ ///
+ public override int Read(byte[] buffer, int offset, int count) => SourceStream.Read(buffer, offset, count);
+
+ ///
+ public override long Seek(long offset, SeekOrigin origin) => SourceStream.Seek(offset, origin);
+
+ ///
+ public override void SetLength(long value) => SourceStream.SetLength(value);
+
+ ///
+ public override void Write(byte[] buffer, int offset, int count) => SourceStream.Write(buffer, offset, count);
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ SourceStream.Dispose();
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/OwlCore.Storage.csproj b/src/OwlCore.Storage.csproj
index 5709d3e..4b9c89f 100644
--- a/src/OwlCore.Storage.csproj
+++ b/src/OwlCore.Storage.csproj
@@ -14,7 +14,7 @@
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
Arlo Godfrey
- 0.9.1
+ 0.9.2
OwlCore
The most flexible file system abstraction, ever. Built in partnership with the UWP Community.
@@ -23,6 +23,14 @@
LICENSE.txt
logo.png
+--- 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).
diff --git a/tests/OwlCore.Storage.Tests/Archive/ZipArchive/InMemIFolderTests.cs b/tests/OwlCore.Storage.Tests/Archive/ZipArchive/InMemIFolderTests.cs
index cde301c..025441d 100644
--- a/tests/OwlCore.Storage.Tests/Archive/ZipArchive/InMemIFolderTests.cs
+++ b/tests/OwlCore.Storage.Tests/Archive/ZipArchive/InMemIFolderTests.cs
@@ -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(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(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(fileCEx);
- Assert.AreEqual("subB/subC/fileC", fileCEx.Id);
+ Assert.AreEqual("fileC", fileCEx.Name);
}
[TestMethod]
diff --git a/tests/OwlCore.Storage.Tests/OwlCore.Storage.Tests.csproj b/tests/OwlCore.Storage.Tests/OwlCore.Storage.Tests.csproj
index 75e7453..3c6fb3f 100644
--- a/tests/OwlCore.Storage.Tests/OwlCore.Storage.Tests.csproj
+++ b/tests/OwlCore.Storage.Tests/OwlCore.Storage.Tests.csproj
@@ -9,15 +9,15 @@
-
-
-
-
-
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+