diff --git a/src/Downloader.Test/UnitTests/ChunkDownloaderOnFileTest.cs b/src/Downloader.Test/UnitTests/ChunkDownloaderOnFileTest.cs index 59c8e65..af45829 100644 --- a/src/Downloader.Test/UnitTests/ChunkDownloaderOnFileTest.cs +++ b/src/Downloader.Test/UnitTests/ChunkDownloaderOnFileTest.cs @@ -17,6 +17,6 @@ public ChunkDownloaderOnFileTest() MinimumSizeOfChunking = 16, Timeout = 100, }; - Storage = new ConcurrentStream(path, DummyFileHelper.FileSize16Kb); + Storage = new ConcurrentStream(path, 0); } } diff --git a/src/Downloader.Test/UnitTests/ChunkDownloaderTest.cs b/src/Downloader.Test/UnitTests/ChunkDownloaderTest.cs index eaa0fee..a473239 100644 --- a/src/Downloader.Test/UnitTests/ChunkDownloaderTest.cs +++ b/src/Downloader.Test/UnitTests/ChunkDownloaderTest.cs @@ -169,4 +169,27 @@ public async Task CancelReadStreamTest() chunkDownloader.Chunk.Clear(); } + + [Fact] + public async Task OverflowWhenReadStreamTest() + { + // arrange + var randomlyBytes = DummyData.GenerateRandomBytes(Size); + var chunk = new Chunk(0, Size / 2 - 1); + var chunkDownloader = new ChunkDownloader(chunk, Configuration, Storage); + using var memoryStream = new MemoryStream(randomlyBytes); + + // act + await chunkDownloader.ReadStream(memoryStream, new PauseTokenSource().Token, new CancellationToken()); + Storage.Flush(); + + // assert + Assert.Equal(expected: Size / 2, actual: chunk.Length); + Assert.Equal(expected: chunk.Length, actual: chunk.Position); + Assert.Equal(expected: 0, actual: chunk.EmptyLength); + Assert.Equal(expected: memoryStream.Position, actual: chunk.Position); + Assert.Equal(expected: chunk.Length, actual: Storage.Length); + + Storage.Dispose(); + } } \ No newline at end of file diff --git a/src/Downloader/Chunk.cs b/src/Downloader/Chunk.cs index bd15f1a..2eb2c14 100644 --- a/src/Downloader/Chunk.cs +++ b/src/Downloader/Chunk.cs @@ -3,29 +3,76 @@ namespace Downloader { /// - /// Chunk data structure + /// Chunk data structure /// public class Chunk { + /// + /// Chunk unique identity name + /// + public string Id { get; set; } + + /// + /// Start offset of the chunk in the file bytes + /// + public long Start { get; set; } + + /// + /// End offset of the chunk in the file bytes + /// + public long End { get; set; } + + /// + /// Current write offset of the chunk + /// + public long Position { get; set; } + + /// + /// How many times to try again after the error + /// + public int MaxTryAgainOnFailover { get; set; } + + /// + /// How many milliseconds to wait for a response from the server? + /// + public int Timeout { get; set; } + + /// + /// How many times has downloading the chunk failed? + /// + public int FailoverCount { get; private set; } + + /// + /// Length of current chunk. + /// When the chunk length is zero, the file is open to receive new bytes + /// until no more bytes are received from the server. + /// + public long Length => End - Start + 1; + + /// + /// Unused length of current chunk. + /// When the chunk length is zero, the file is open to receive new bytes + /// until no more bytes are received from the server. + /// + public long EmptyLength => Length > 0 ? Length - Position : long.MaxValue; + + /// + /// Can write more data on this chunk according to the chunk situations? + /// + public bool CanWrite => Length > 0 ? Start + Position < End : true; + + public Chunk() { Id = Guid.NewGuid().ToString("N"); } + public Chunk(long start, long end) : this() { Start = start; End = end; } - public string Id { get; set; } - public long Start { get; set; } - public long End { get; set; } - public long Position { get; set; } - public int MaxTryAgainOnFailover { get; set; } - public int Timeout { get; set; } - public int FailoverCount { get; private set; } - public long Length => End - Start + 1; - public bool CanTryAgainOnFailover() { return FailoverCount++ < MaxTryAgainOnFailover; diff --git a/src/Downloader/ChunkDownloader.cs b/src/Downloader/ChunkDownloader.cs index 233be0e..cfe9d30 100644 --- a/src/Downloader/ChunkDownloader.cs +++ b/src/Downloader/ChunkDownloader.cs @@ -54,6 +54,11 @@ public async Task Download(Request downloadRequest, PauseToken pause, Can { return await ContinueWithDelay(downloadRequest, pause, cancelToken).ConfigureAwait(false); } + catch(Exception error) + { + // Can't handle this exception + throw; + } finally { await Task.Yield(); @@ -125,7 +130,7 @@ internal async Task ReadStream(Stream stream, PauseToken pauseToken, Cancellatio { // close stream on cancellation because, it's not work on .Net Framework using var _ = cancelToken.Register(stream.Close); - while (readSize > 0) + while (readSize > 0 && Chunk.CanWrite) { cancelToken.ThrowIfCancellationRequested(); await pauseToken.WaitWhilePausedAsync().ConfigureAwait(false); @@ -139,6 +144,7 @@ internal async Task ReadStream(Stream stream, PauseToken pauseToken, Cancellatio readSize = await stream.ReadAsync(buffer, 0, buffer.Length, innerToken.Value).ConfigureAwait(false); } + readSize = (int)Math.Min(Chunk.EmptyLength, readSize); if (readSize > 0) { await _storage.WriteAsync(Chunk.Start + Chunk.Position - _configuration.RangeLow, buffer, readSize).ConfigureAwait(false);