Skip to content

Commit

Permalink
Merge branch 'release/v2.4.0'
Browse files Browse the repository at this point in the history
bezzad committed Sep 16, 2022
2 parents d85b293 + 02f9b81 commit 00c5be4
Showing 8 changed files with 151 additions and 105 deletions.
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ public DummyFileController(ILogger<DummyFileController> logger)
[Route("file/size/{size}")]
public IActionResult GetFile(int size)
{
_logger.Log(LogLevel.Information, $"file/size/{size}");
_logger.LogTrace($"file/size/{size}");
var data = DummyData.GenerateOrderedBytes(size);
return File(data, "application/octet-stream", true);
}
@@ -38,7 +38,7 @@ public IActionResult GetFile(int size)
[Route("noheader/file/{fileName}")]
public IActionResult GetFileWithNameNoHeader(string fileName, [FromQuery] int size)
{
_logger.Log(LogLevel.Information, $"noheader/file/{fileName}?size={size}");
_logger.LogTrace($"noheader/file/{fileName}?size={size}");
var data = new MemoryStream(DummyData.GenerateOrderedBytes(size));
return Ok(data); // return stream without header data
}
@@ -52,7 +52,7 @@ public IActionResult GetFileWithNameNoHeader(string fileName, [FromQuery] int si
[Route("file/{fileName}")]
public IActionResult GetFileWithName(string fileName, [FromQuery] int size)
{
_logger.Log(LogLevel.Information, $"file/{fileName}?size={size}");
_logger.LogTrace($"file/{fileName}?size={size}");
byte[] fileData = DummyData.GenerateOrderedBytes(size);
return File(fileData, "application/octet-stream", true);
}
@@ -66,7 +66,7 @@ public IActionResult GetFileWithName(string fileName, [FromQuery] int size)
[Route("file/{fileName}/size/{size}")]
public IActionResult GetFileWithContentDisposition(string fileName, int size)
{
_logger.Log(LogLevel.Information, $"file/{fileName}/size/{size}");
_logger.LogTrace($"file/{fileName}/size/{size}");
byte[] fileData = DummyData.GenerateOrderedBytes(size);
return File(fileData, "application/octet-stream", fileName, true);
}
@@ -80,7 +80,7 @@ public IActionResult GetFileWithContentDisposition(string fileName, int size)
[Route("file/{fileName}/size/{size}/norange")]
public IActionResult GetFileWithNoAcceptRange(string fileName, int size)
{
_logger.Log(LogLevel.Information, $"file/{fileName}/size/{size}/norange");
_logger.LogTrace($"file/{fileName}/size/{size}/norange");
byte[] fileData = DummyData.GenerateOrderedBytes(size);
return File(fileData, "application/octet-stream", fileName, false);
}
21 changes: 20 additions & 1 deletion src/Downloader.Test/UnitTests/ChunkDownloaderTest.cs
Original file line number Diff line number Diff line change
@@ -102,7 +102,7 @@ public void ReadStreamProgressEventsTest()
}

[TestMethod]
public void ReadStreamTimeoutExceptionTest()
public void ReadStreamCanceledExceptionTest()
{
// arrange
var streamSize = DummyFileHelper.FileSize16Kb;
@@ -121,6 +121,25 @@ async Task CallReadStream() => await chunkDownloader
Assert.ThrowsExceptionAsync<OperationCanceledException>(CallReadStream);
}

[TestMethod]
public void ReadStreamTimeoutExceptionTest()
{
// arrange
var streamSize = DummyFileHelper.FileSize16Kb;
var randomlyBytes = DummyData.GenerateRandomBytes(streamSize);
var chunk = new Chunk(0, streamSize - 1) { Timeout = 0, Storage = _storage };
var chunkDownloader = new ChunkDownloader(chunk, _configuration);
using var memoryStream = new MemoryStream(randomlyBytes);

// act
async Task CallReadStream() => await chunkDownloader
.ReadStream(new MemoryStream(), new PauseTokenSource().Token, new CancellationToken())
.ConfigureAwait(false);

// assert
Assert.ThrowsExceptionAsync<TaskCanceledException>(CallReadStream);
}

[TestMethod]
public async Task CancelReadStreamTest()
{
14 changes: 12 additions & 2 deletions src/Downloader/ChunkDownloader.cs
Original file line number Diff line number Diff line change
@@ -119,8 +119,10 @@ private void SetRequestRange(HttpWebRequest request)
internal async Task ReadStream(Stream stream, PauseToken pauseToken, CancellationToken cancelToken)
{
int readSize = 1;
CancellationToken? innerToken = null;
try
{
// close stream on cancellation because, it's not work on .Net Framework
using (cancelToken.Register(stream.Close))
{
while (CanReadStream() && readSize > 0)
@@ -130,7 +132,12 @@ internal async Task ReadStream(Stream stream, PauseToken pauseToken, Cancellatio
byte[] buffer = new byte[Configuration.BufferBlockSize];
using var innerCts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken);
innerCts.CancelAfter(Chunk.Timeout);
readSize = await stream.ReadAsync(buffer, 0, buffer.Length, innerCts.Token).ConfigureAwait(false);
innerToken = innerCts.Token;
using (innerToken?.Register(stream.Close))
{
// if innerToken timeout occurs, close the stream just during the reading stream
readSize = await stream.ReadAsync(buffer, 0, buffer.Length, innerToken.Value).ConfigureAwait(false);
}
await Chunk.Storage.WriteAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false);
Chunk.Position += readSize;

@@ -143,9 +150,12 @@ internal async Task ReadStream(Stream stream, PauseToken pauseToken, Cancellatio
}
}
}
catch (ObjectDisposedException) // When closing stream manually, ObjectDisposedException will be thrown
catch (ObjectDisposedException exp) // When closing stream manually, ObjectDisposedException will be thrown
{
cancelToken.ThrowIfCancellationRequested();
if (innerToken?.IsCancellationRequested == true)
throw new TaskCanceledException("The ReadAsync function has timed out", exp);

throw; // throw origin stack trace of exception
}
}
21 changes: 11 additions & 10 deletions src/Downloader/Downloader.csproj
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.1;net452;net6.0</TargetFrameworks>
<LangVersion>latestMajor</LangVersion>
<Version>2.3.9</Version>
<AssemblyVersion>2.3.9</AssemblyVersion>
<FileVersion>2.3.9</FileVersion>
<Version>2.4.0</Version>
<AssemblyVersion>2.4.0</AssemblyVersion>
<FileVersion>2.4.0</FileVersion>
<Title>Downloader</Title>
<Authors>Behzad Khosravifar</Authors>
<Company>bezzad</Company>
<Description>Fast and reliable multipart downloader with asynchronous progress events for .NET</Description>
<PackageProjectUrl>https://github.com/bezzad/Downloader</PackageProjectUrl>
<RepositoryUrl>https://github.com/bezzad/Downloader</RepositoryUrl>
<PackageTags>download-manager, downloader, download, idm, internet, streaming, download-file, stream-downloader, multipart-download</PackageTags>
<PackageReleaseNotes>Fixed cancellation issues #94 and #104</PackageReleaseNotes>
<PackageReleaseNotes>Fixed timeout issue on .Net Framework when network disconnected #104</PackageReleaseNotes>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Downloader.snk</AssemblyOriginatorKeyFile>
<Copyright>Copyright (C) 2019-2022 Behzad Khosravifar</Copyright>
@@ -22,9 +21,14 @@
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageIcon>downloader.png</PackageIcon>
<RepositoryType>git</RepositoryType>

<PackageReadmeFile>README.md</PackageReadmeFile>
<DebugType>embedded</DebugType>

<!-- Optional: Publish the repository URL in the built .nupkg (in the NuSpec <Repository> element) -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>

<!-- Optional: Build symbol package (.snupkg) to distribute the PDB containing Source Link -->
<IncludeSymbols>true</IncludeSymbols>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
@@ -64,10 +68,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
6 changes: 6 additions & 0 deletions src/Downloader/ThrottledStream.cs
Original file line number Diff line number Diff line change
@@ -114,6 +114,12 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc
await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
}

public override void Close()
{
_baseStream.Close();
base.Close();
}

private async Task Throttle(int transmissionVolume)
{
// Make sure the buffer isn't empty.
8 changes: 4 additions & 4 deletions src/Samples/Downloader.Sample.Framework/DownloadList.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[
{
"FileName": "D:\\TestDownload\\Hello - 81606.mp4",
"Url": "https://cdn.pixabay.com/vimeo/576083058/Hello%20-%2081605.mp4?width=1920&hash=e6f56273dcd2f28fd1a9fe6e77f66d7e157b33f6&download=1"
"FileName": "D:\\TestDownload\\file_example_MP4_1920_18MG.mp4",
"Url": "https://file-examples.com/storage/fe783a5cbb6323602a28c66/2017/04/file_example_MP4_1920_18MG.mp4"
},
{
"FileName": "D:\\TestDownload\\VS.exe",
"Url": "https://c2rsetup.officeapps.live.com/c2r/downloadVS.aspx?sku=community&channel=Release&version=VS2022&source=VSLandingPage&includeRecommended=true&cid=2030:9bf2104738684908988ca7dcd5dafed1"
"FileName": "D:\\TestDownload\\file_example_PNG_3MB.png",
"Url": "https://file-examples.com/storage/fe783a5cbb6323602a28c66/2017/10/file_example_PNG_3MB.png"
}
]
3 changes: 2 additions & 1 deletion src/Samples/Downloader.Sample/Helper.cs
Original file line number Diff line number Diff line change
@@ -55,7 +55,8 @@ public static void UpdateTitleInfo(this DownloadProgressChangedEventArgs e, bool
Console.Title = $"{progressPercentage}% - " +
$"{speed}/s (avg: {avgSpeed}/s) - " +
$"{estimateTime} {timeLeftUnit} left - " +
$"[{bytesReceived} of {totalBytesToReceive}]" +
$"[{bytesReceived} of {totalBytesToReceive}] - " +
$"Active Chunks: {e.ActiveChunks}" +
(isPaused ? " - Paused" : "");
}
}
173 changes: 91 additions & 82 deletions src/Samples/Downloader.Sample/Program.cs
Original file line number Diff line number Diff line change
@@ -40,11 +40,12 @@ private static async Task Main()
}
catch (Exception e)
{
Console.Clear();
Console.Error.WriteLine(e);
Debugger.Break();
}

Console.WriteLine("\n\n END \n\n");
Console.WriteLine("END");
Console.Read();
}
private static void Initial()
@@ -72,11 +73,6 @@ private static void KeyboardHandler()
{
ConsoleKeyInfo cki;
Console.CancelKeyPress += CancelAll;
Console.WriteLine("\nPress Esc to Stop current file download");
Console.WriteLine("\nPress P to Pause and R to Resume downloading");
Console.WriteLine("\nPress Up Arrow to Increase download speed 2X");
Console.WriteLine("\nPress Down Arrow to Decrease download speed 2X");
Console.WriteLine();

while (true)
{
@@ -131,7 +127,7 @@ private static DownloadConfiguration GetDownloadConfiguration()
RangeDownload = false, // set true if you want to download just a specific range of bytes of a large file
RangeLow = 0, // floor offset of download range of a large file
RangeHigh = 0, // ceiling offset of download range of a large file
ClearPackageOnCompletionWithFailure = false, // clear package temp files when download completed with failure, default value is true
ClearPackageOnCompletionWithFailure = true, // clear package temp files when download completed with failure, default value is true
RequestConfiguration =
{
// config and customize request headers
@@ -168,97 +164,110 @@ private static List<DownloadItem> GetDownloadItems()
}

return downloadList;
}
private static async Task DownloadAll(IEnumerable<DownloadItem> downloadList, CancellationToken cancelToken)
}
private static async Task DownloadAll(IEnumerable<DownloadItem> downloadList, CancellationToken cancelToken)
{
foreach (DownloadItem downloadItem in downloadList)
{
foreach (DownloadItem downloadItem in downloadList)
{
if (cancelToken.IsCancellationRequested)
return;
if (cancelToken.IsCancellationRequested)
return;

// begin download from url
await DownloadFile(downloadItem).ConfigureAwait(false);
}
// begin download from url
await DownloadFile(downloadItem).ConfigureAwait(false);
}
private static async Task<DownloadService> DownloadFile(DownloadItem downloadItem)
{
CurrentDownloadConfiguration = GetDownloadConfiguration();
CurrentDownloadService = CreateDownloadService(CurrentDownloadConfiguration);

if (string.IsNullOrWhiteSpace(downloadItem.FileName))
{
await CurrentDownloadService.DownloadFileTaskAsync(downloadItem.Url, new DirectoryInfo(downloadItem.FolderPath)).ConfigureAwait(false);
}
else
{
await CurrentDownloadService.DownloadFileTaskAsync(downloadItem.Url, downloadItem.FileName).ConfigureAwait(false);
}
}
private static async Task<DownloadService> DownloadFile(DownloadItem downloadItem)
{
CurrentDownloadConfiguration = GetDownloadConfiguration();
CurrentDownloadService = CreateDownloadService(CurrentDownloadConfiguration);

return CurrentDownloadService;
if (string.IsNullOrWhiteSpace(downloadItem.FileName))
{
await CurrentDownloadService.DownloadFileTaskAsync(downloadItem.Url, new DirectoryInfo(downloadItem.FolderPath)).ConfigureAwait(false);
}
private static DownloadService CreateDownloadService(DownloadConfiguration config)
else
{
var downloadService = new DownloadService(config);
await CurrentDownloadService.DownloadFileTaskAsync(downloadItem.Url, downloadItem.FileName).ConfigureAwait(false);
}

return CurrentDownloadService;
}
private static void WriteKeyboardGuidLines()
{
Console.Clear();
Console.WriteLine("Press Esc to Stop current file download");
Console.WriteLine("Press P to Pause and R to Resume downloading");
Console.WriteLine("Press Up Arrow to Increase download speed 2X");
Console.WriteLine("Press Down Arrow to Decrease download speed 2X");
Console.WriteLine();
}
private static DownloadService CreateDownloadService(DownloadConfiguration config)
{
var downloadService = new DownloadService(config);

// Provide `FileName` and `TotalBytesToReceive` at the start of each downloads
downloadService.DownloadStarted += OnDownloadStarted;
// Provide `FileName` and `TotalBytesToReceive` at the start of each downloads
downloadService.DownloadStarted += OnDownloadStarted;

// Provide any information about chunker downloads,
// like progress percentage per chunk, speed,
// total received bytes and received bytes array to live streaming.
downloadService.ChunkDownloadProgressChanged += OnChunkDownloadProgressChanged;
// Provide any information about chunker downloads,
// like progress percentage per chunk, speed,
// total received bytes and received bytes array to live streaming.
downloadService.ChunkDownloadProgressChanged += OnChunkDownloadProgressChanged;

// Provide any information about download progress,
// like progress percentage of sum of chunks, total speed,
// average speed, total received bytes and received bytes array
// to live streaming.
downloadService.DownloadProgressChanged += OnDownloadProgressChanged;
// Provide any information about download progress,
// like progress percentage of sum of chunks, total speed,
// average speed, total received bytes and received bytes array
// to live streaming.
downloadService.DownloadProgressChanged += OnDownloadProgressChanged;

// Download completed event that can include occurred errors or
// cancelled or download completed successfully.
downloadService.DownloadFileCompleted += OnDownloadFileCompleted;
// Download completed event that can include occurred errors or
// cancelled or download completed successfully.
downloadService.DownloadFileCompleted += OnDownloadFileCompleted;

return downloadService;
}
return downloadService;
}

private static void OnDownloadStarted(object sender, DownloadStartedEventArgs e)
{
ConsoleProgress = new ProgressBar(10000,
$"Downloading {Path.GetFileName(e.FileName)} ...", ProcessBarOption);
ChildConsoleProgresses = new ConcurrentDictionary<string, ChildProgressBar>();
}
private static void OnDownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
{
ConsoleProgress?.Tick(10000);
Console.WriteLine();
Console.WriteLine();
private static void OnDownloadStarted(object sender, DownloadStartedEventArgs e)
{
WriteKeyboardGuidLines();
ConsoleProgress = new ProgressBar(10000, $"Downloading {Path.GetFileName(e.FileName)} ", ProcessBarOption);
ChildConsoleProgresses = new ConcurrentDictionary<string, ChildProgressBar>();
}
private static void OnDownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
{
ConsoleProgress?.Tick(10000);

if (e.Cancelled)
{
Console.WriteLine("Download canceled!");
}
else if (e.Error != null)
{
Console.Error.WriteLine(e.Error.Message);
}
else
{
Console.WriteLine("Download completed successfully.");
Console.Title = "100%";
}
if (e.Cancelled)
{
ConsoleProgress.Message += " CANCELED";
}
private static void OnChunkDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
else if (e.Error != null)
{
ChildProgressBar progress = ChildConsoleProgresses.GetOrAdd(e.ProgressId, id =>
ConsoleProgress?.Spawn(10000, $"chunk {id}", ChildOption));
progress.Tick((int)(e.ProgressPercentage * 100));
var activeChunksCount = e.ActiveChunks; // Running chunks count
ConsoleProgress.Message += " ERROR!";
}
private static void OnDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
else
{
ConsoleProgress.Tick((int)(e.ProgressPercentage * 100));
if (sender is DownloadService ds)
e.UpdateTitleInfo(ds.IsPaused);
ConsoleProgress.Message += " DONE";
Console.Title = "100%";
}

foreach (var child in ChildConsoleProgresses.Values)
child.Dispose();

ChildConsoleProgresses.Clear();
ConsoleProgress.Dispose();
}
private static void OnChunkDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
ChildProgressBar progress = ChildConsoleProgresses.GetOrAdd(e.ProgressId,
id => ConsoleProgress?.Spawn(10000, $"chunk {id}", ChildOption));
progress.Tick((int)(e.ProgressPercentage * 100));
var activeChunksCount = e.ActiveChunks; // Running chunks count
}
private static void OnDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
ConsoleProgress.Tick((int)(e.ProgressPercentage * 100));
if (sender is DownloadService ds)
e.UpdateTitleInfo(ds.IsPaused);
}
}
}
}

0 comments on commit 00c5be4

Please sign in to comment.