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 - +