Open samdubey opened 3 days ago
The task requires implementing a dynamic list of download items with progress tracking, pause, and resume functionality across all platforms in .NET MAUI. Additionally, it should support background downloads. The solution involves updating the JSON file format, enhancing the DownloadItem
class, modifying the IDownloadService
interface, updating the DownloadPackage
class, and implementing the necessary changes in the DownloadService
class. Finally, the sample application in Program.cs
will be updated to demonstrate these features.
download.json
FileUpdate the download.json
file to include unique identifiers, status, and progress fields for each download item.
download.json
[
{
"Id": "1",
"FileName": "./downloads/LocalFile10GB_Raw.dat",
"Url": "http://localhost:3333/dummyfile/file/size/10737418240",
"ValidateData": true,
"Status": "pending",
"Progress": 0
},
{
"Id": "2",
"FolderPath": "./downloads/",
"Url": "http://localhost:3333/dummyfile/file/LocalFile100MB_WithContentDisposition.dat/size/104857600",
"ValidateData": true,
"Status": "pending",
"Progress": 0
},
{
"Id": "3",
"FolderPath": "./downloads/",
"Url": "http://localhost:3333/dummyfile/noheader/file/LocalFile100MB_WithoutHeader.dat?size=104857600",
"ValidateData": true,
"Status": "pending",
"Progress": 0
},
{
"Id": "4",
"FolderPath": "./downloads/",
"Url": "http://localhost:3333/dummyfile/file/LocalFile100MB.dat?size=104857600",
"ValidateData": true,
"Status": "pending",
"Progress": 0
}
]
DownloadItem
ClassEnhance the DownloadItem
class to include properties for progress tracking, methods for pause and resume, and event handlers for status and progress updates.
DownloadItem.cs
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace Downloader.Sample
{
[ExcludeFromCodeCoverage]
public class DownloadItem
{
private string _folderPath;
public string Id { get; set; }
public string FolderPath
{
get => _folderPath ?? Path.GetDirectoryName(FileName);
set => _folderPath = value;
}
public string FileName { get; set; }
public string Url { get; set; }
public bool ValidateData { get; set; }
// New properties for progress tracking
public long TotalBytes { get; set; }
public long DownloadedBytes { get; set; }
public DownloadStatus Status { get; set; }
// Event handlers for status and progress updates
public event EventHandler<DownloadProgressChangedEventArgs> ProgressChanged;
public event EventHandler<DownloadStatusChangedEventArgs> StatusChanged;
// Method to update progress
public void UpdateProgress(long downloadedBytes, long totalBytes)
{
DownloadedBytes = downloadedBytes;
TotalBytes = totalBytes;
ProgressChanged?.Invoke(this, new DownloadProgressChangedEventArgs(downloadedBytes, totalBytes));
}
// Method to update status
public void UpdateStatus(DownloadStatus status)
{
Status = status;
StatusChanged?.Invoke(this, new DownloadStatusChangedEventArgs(status));
}
// Methods for pause and resume
public void Pause()
{
// Logic to pause the download
UpdateStatus(DownloadStatus.Paused);
}
public void Resume()
{
// Logic to resume the download
UpdateStatus(DownloadStatus.Downloading);
}
}
// Enum for download status
public enum DownloadStatus
{
Pending,
Downloading,
Paused,
Completed,
Failed
}
// EventArgs for progress changed event
public class DownloadProgressChangedEventArgs : EventArgs
{
public long DownloadedBytes { get; }
public long TotalBytes { get; }
public DownloadProgressChangedEventArgs(long downloadedBytes, long totalBytes)
{
DownloadedBytes = downloadedBytes;
TotalBytes = totalBytes;
}
}
// EventArgs for status changed event
public class DownloadStatusChangedEventArgs : EventArgs
{
public DownloadStatus Status { get; }
public DownloadStatusChangedEventArgs(DownloadStatus status)
{
Status = status;
}
}
}
IDownloadService
InterfaceUpdate the IDownloadService
interface to include methods for managing download items dynamically, pausing and resuming specific downloads, and enabling background download support.
IDownloadService.cs
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Downloader
{
public interface IDownloadService
{
bool IsBusy { get; }
bool IsCancelled { get; }
DownloadPackage Package { get; }
DownloadStatus Status { get; }
event EventHandler<AsyncCompletedEventArgs> DownloadFileCompleted;
event EventHandler<DownloadProgressChangedEventArgs> DownloadProgressChanged;
event EventHandler<DownloadProgressChangedEventArgs> ChunkDownloadProgressChanged;
event EventHandler<DownloadStartedEventArgs> DownloadStarted;
Task<Stream> DownloadFileTaskAsync(DownloadPackage package, CancellationToken cancellationToken = default);
Task<Stream> DownloadFileTaskAsync(DownloadPackage package, string address, CancellationToken cancellationToken = default);
Task<Stream> DownloadFileTaskAsync(DownloadPackage package, string[] urls, CancellationToken cancellationToken = default);
Task<Stream> DownloadFileTaskAsync(string address, CancellationToken cancellationToken = default);
Task<Stream> DownloadFileTaskAsync(string[] urls, CancellationToken cancellationToken = default);
Task DownloadFileTaskAsync(string address, string fileName, CancellationToken cancellationToken = default);
Task DownloadFileTaskAsync(string[] urls, string fileName, CancellationToken cancellationToken = default);
Task DownloadFileTaskAsync(string address, DirectoryInfo folder, CancellationToken cancellationToken = default);
Task DownloadFileTaskAsync(string[] urls, DirectoryInfo folder, CancellationToken cancellationToken = default);
void CancelAsync();
Task CancelTaskAsync();
void Pause();
void Resume();
Task Clear();
void AddLogger(ILogger logger);
// New methods for dynamic management...
void AddDownloadItem(DownloadItem downloadItem);
void RemoveDownloadItem(string downloadItemId);
List<DownloadItem> GetDownloadItems();
void PauseDownloadItem(string downloadItemId);
void ResumeDownloadItem(string downloadItemId);
void EnableBackgroundDownloads();
void DisableBackgroundDownloads();
}
}
DownloadPackage
ClassUpdate the DownloadPackage
class to support dynamic management, progress tracking, pause/resume functionality, and background download support.
DownloadPackage.cs
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text.Json;
namespace Downloader;
public class DownloadPackage : IDisposable, IAsyncDisposable
{
private List<DownloadItem> downloadItems = new List<DownloadItem>();
public bool IsSaving { get; set; }
public bool IsSaveComplete { get; set; }
public double SaveProgress { get; set; }
public DownloadStatus Status { get; set; } = DownloadStatus.None;
public string[] Urls { get; set; }
public long TotalFileSize { get; set; }
public string FileName { get; set; }
public Chunk[] Chunks { get; set; }
public long ReceivedBytesSize => Chunks?.Sum(chunk => chunk.Position) ?? 0;
public bool IsSupportDownloadInRange { get; set; } = true;
public bool InMemoryStream => string.IsNullOrWhiteSpace(FileName);
public ConcurrentStream Storage { get; set; }
public double Progress => (double)ReceivedBytesSize / TotalFileSize * 100;
public void AddDownloadItem(DownloadItem item)
{
downloadItems.Add(item);
}
public void RemoveDownloadItem(DownloadItem item)
{
downloadItems.Remove(item);
}
public string SerializeState()
{
return JsonSerializer.Serialize(this);
}
public static DownloadPackage DeserializeState(string state)
{
return JsonSerializer.Deserialize<DownloadPackage>(state);
}
public void Pause()
{
// Implement pause logic here
}
public void Resume()
{
// Implement resume logic here
}
public void Clear()
{
if (Chunks != null)
{
foreach (Chunk chunk in Chunks)
chunk.Clear();
}
Chunks = null;
}
public async Task FlushAsync()
{
if (Storage?.CanWrite == true)
await Storage.FlushAsync().ConfigureAwait(false);
}
[Obsolete("This method has been deprecated. Please use FlushAsync instead.")]
public void Flush()
{
if (Storage?.CanWrite == true)
Storage?.FlushAsync().Wait();
}
public void Validate()
{
foreach (var chunk in Chunks)
{
if (chunk.IsValidPosition() == false)
{
var realLength = Storage?.Length ?? 0;
if (realLength <= chunk.Position)
{
chunk.Clear();
}
}
if (!IsSupportDownloadInRange)
chunk.Clear();
}
}
public void BuildStorage(bool reserveFileSize, long maxMemoryBufferBytes = 0, ILogger logger = null)
{
Storage = string.IsNullOrWhiteSpace(FileName)
? new ConcurrentStream(maxMemoryBufferBytes, logger)
: new ConcurrentStream(FileName, reserveFileSize ? TotalFileSize : 0, maxMemoryBufferBytes, logger);
}
public void Dispose()
{
Clear();
Storage?.Dispose();
}
public async ValueTask DisposeAsync()
{
Clear();
if (Storage is not null)
{
await Storage.DisposeAsync().ConfigureAwait(false);
}
}
}
DownloadService
ClassUpdate the DownloadService
class to manage a dynamic list of download items, track progress, handle pause/resume functionality, and support background downloads.
DownloadService.cs
using Downloader.Extensions.Helpers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Downloader
{
public class DownloadService : AbstractDownloadService
{
private readonly List<DownloadItem> _downloadItems = new List<DownloadItem>();
private readonly Dictionary<string, CancellationTokenSource> _cancellationTokenSources = new Dictionary<string, CancellationTokenSource>();
public DownloadService(DownloadConfiguration options) : base(options) { }
public DownloadService() : base(null) { }
public void AddDownloadItem(DownloadItem item)
{
_downloadItems.Add(item);
_cancellationTokenSources[item.Id] = new CancellationTokenSource();
}
public void RemoveDownloadItem(string itemId)
{
var item = _downloadItems.FirstOrDefault(i => i.Id == itemId);
if (item != null)
{
_downloadItems.Remove(item);
_cancellationTokenSources[itemId].Cancel();
_cancellationTokenSources.Remove(itemId);
}
}
public void PauseDownload(string itemId)
{
if (_cancellationTokenSources.ContainsKey(itemId))
{
_cancellationTokenSources[itemId].Cancel();
}
}
public void ResumeDownload(string itemId)
{
var item = _downloadItems.FirstOrDefault(i => i.Id == itemId);
if (item != null)
{
_cancellationTokenSources[itemId] = new CancellationTokenSource();
StartDownload(item, _cancellationTokenSources[itemId].Token);
}
}
public async Task StartAllDownloads()
{
var tasks = _downloadItems.Select(item => StartDownload(item, _cancellationTokenSources[item.Id].Token));
await Task.WhenAll(tasks);
}
private async Task StartDownload(DownloadItem item, CancellationToken cancellationToken)
{
try
{
await SingleInstanceSemaphore.WaitAsync().ConfigureAwait(false);
item.TotalFileSize = await RequestInstances.First().GetFileSize().ConfigureAwait(false);
item.IsSupportDownloadInRange = await RequestInstances.First().IsSupportDownloadInRange().ConfigureAwait(false);
item.BuildStorage(Options.ReserveStorageSpaceBeforeStartingDownload, Options.MaximumMemoryBufferBytes);
ValidateBeforeChunking(item);
ChunkHub.SetFileChunks(item);
OnDownloadStarted(new DownloadStartedEventArgs(item.FileName, item.TotalFileSize));
if (Options.ParallelDownload)
{
await ParallelDownload(item, cancellationToken).ConfigureAwait(false);
}
else
{
await SerialDownload(item, cancellationToken).ConfigureAwait(false);
}
await SendDownloadCompletionSignal(item, DownloadStatus.Completed).ConfigureAwait(false);
}
catch (OperationCanceledException exp)
{
await SendDownloadCompletionSignal(item, DownloadStatus.Stopped, exp).ConfigureAwait(false);
}
catch (Exception exp)
{
await SendDownloadCompletionSignal(item, DownloadStatus.Failed, exp).ConfigureAwait(false);
}
finally
{
SingleInstanceSemaphore.Release();
await Task.Yield();
}
}
private async Task SendDownloadCompletionSignal(DownloadItem item, DownloadStatus state, Exception error = null)
{
var isCancelled = state == DownloadStatus.Stopped;
item.IsSaveComplete = state == DownloadStatus.Completed;
Status = state;
await (item?.Storage?.FlushAsync() ?? Task.FromResult(0)).ConfigureAwait(false);
OnDownloadFileCompleted(new AsyncCompletedEventArgs(error, isCancelled, item));
}
private void ValidateBeforeChunking(DownloadItem item)
{
CheckSingleChunkDownload(item);
CheckSupportDownloadInRange(item);
SetRangedSizes(item);
CheckSizes(item);
}
private void SetRangedSizes(DownloadItem item)
{
if (Options.RangeDownload)
{
if (!item.IsSupportDownloadInRange)
{
throw new NotSupportedException("The server of your desired address does not support download in a specific range");
}
if (Options.RangeHigh < Options.RangeLow)
{
Options.RangeLow = Options.RangeHigh - 1;
}
if (Options.RangeLow < 0)
{
Options.RangeLow = 0;
}
if (Options.RangeHigh < 0)
{
Options.RangeHigh = Options.RangeLow;
}
if (item.TotalFileSize > 0)
{
Options.RangeHigh = Math.Min(item.TotalFileSize, Options.RangeHigh);
}
item.TotalFileSize = Options.RangeHigh - Options.RangeLow + 1;
}
else
{
Options.RangeHigh = Options.RangeLow = 0;
}
}
private void CheckSizes(DownloadItem item)
{
if (Options.CheckDiskSizeBeforeDownload && !item.InMemoryStream)
{
FileHelper.ThrowIfNotEnoughSpace(item.TotalFileSize, item.FileName);
}
}
private void CheckSingleChunkDownload(DownloadItem item)
{
if (item.TotalFileSize <= 1)
item.TotalFileSize = 0;
if (item.TotalFileSize <= Options.MinimumSizeOfChunking)
SetSingleChunkDownload();
}
private void CheckSupportDownloadInRange(DownloadItem item)
{
if (item.IsSupportDownloadInRange == false)
SetSingleChunkDownload();
}
private void SetSingleChunkDownload()
{
Options.ChunkCount = 1;
Options.ParallelCount = 1;
ParallelSemaphore = new SemaphoreSlim(1, 1);
}
private async Task ParallelDownload(DownloadItem item, CancellationToken cancellationToken)
{
var tasks = GetChunksTasks(item, cancellationToken);
var result = Task.WhenAll(tasks);
await result.ConfigureAwait(false);
if (result.IsFaulted)
{
throw result.Exception;
}
}
private async Task SerialDownload(DownloadItem item, CancellationToken cancellationToken)
{
var tasks = GetChunksTasks(item, cancellationToken);
foreach (var task in tasks)
await task.ConfigureAwait(false);
}
private IEnumerable<Task> GetChunksTasks(DownloadItem item, CancellationToken cancellationToken)
{
for (int i = 0; i < item.Chunks.Length; i++)
{
var request = RequestInstances[i % RequestInstances.Count];
yield return DownloadChunk(item.Chunks[i], request, cancellationToken);
}
}
private async Task<Chunk> DownloadChunk(Chunk chunk, Request request, CancellationToken cancellationToken)
{
ChunkDownloader chunkDownloader = new ChunkDownloader(chunk, Options, chunk.Storage, Logger);
chunkDownloader.DownloadProgressChanged += OnChunkDownloadProgressChanged;
await ParallelSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
cancellationToken.ThrowIfCancellationRequested();
return await chunkDownloader.Download(request, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception)
{
cancellationToken.ThrowIfCancellationRequested();
throw;
}
finally
{
ParallelSemaphore.Release();
}
}
}
}
Program.cs
Update the sample application to demonstrate adding, removing, and managing download items dynamically, as well as tracking progress and handling pause/resume operations.
Program.cs
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using ShellProgressBar;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using FileLogger = Downloader.Extensions.Logging.FileLogger;
namespace Downloader.Sample;
[ExcludeFromCodeCoverage]
public partial class Program
{
private const string DownloadListFile = "download.json";
private static List<DownloadItem> DownloadList;
private static ProgressBar ConsoleProgress;
private static ConcurrentDictionary<string, ChildProgressBar> ChildConsoleProgresses;
private static ProgressBarOptions ChildOption;
private static ProgressBarOptions ProcessBarOption;
private static IDownloadService CurrentDownloadService;
private static DownloadConfiguration CurrentDownloadConfiguration;
private static CancellationTokenSource CancelAllTokenSource;
private static ILogger Logger;
private static async Task Main()
{
try
{
DummyHttpServer.HttpServer.Run(3333);
await Task.Delay(1000);
Console.Clear();
Initial();
new Task(
[Click here to create a Pull Request with the proposed solution](https://api.codeautopilot.com/create-pr?task=32620&hash=bb46697ec7646be62eb7e3b7ac4c61333f6940036ca836bb8d51df27bc4ff8d4)
**Files used for this task:**
<details>
<summary>Changes on src/Samples/Downloader.Sample/download.json</summary>
To address the task of updating the `download.json` file format to support dynamic addition and removal of download items, we need to ensure that the file structure is flexible and can be easily modified programmatically. Here’s a concrete proposal for updating the `download.json` file:
1. **Add Unique Identifiers**: Each download item should have a unique identifier to facilitate dynamic management.
2. **Include Status and Progress Fields**: Add fields to track the status (e.g., pending, downloading, paused, completed) and progress of each download item.
3. **Support for Pause and Resume**: Include fields to store the state necessary for pausing and resuming downloads.
Here is an updated version of the `download.json` file format:
```json
[
{
"Id": "1",
"FileName": "./downloads/LocalFile10GB_Raw.dat",
"Url": "http://localhost:3333/dummyfile/file/size/10737418240",
"ValidateData": true,
"Status": "pending",
"Progress": 0
},
{
"Id": "2",
"FolderPath": "./downloads/",
"Url": "http://localhost:3333/dummyfile/file/LocalFile100MB_WithContentDisposition.dat/size/104857600",
"ValidateData": true,
"Status": "pending",
"Progress": 0
},
{
"Id": "3",
"FolderPath": "./downloads/",
"Url": "http://localhost:3333/dummyfile/noheader/file/LocalFile100MB_WithoutHeader.dat?size=104857600",
"ValidateData": true,
"Status": "pending",
"Progress": 0
},
{
"Id": "4",
"FolderPath": "./downloads/",
"Url": "http://localhost:3333/dummyfile/file/LocalFile100MB.dat?size=104857600",
"ValidateData": true,
"Status": "pending",
"Progress": 0
}
]
pending
, downloading
, paused
, completed
).download.json
file as shown above.DownloadService
, DownloadPackage
, and other relevant classes can read and write these new fields.DownloadService
and DownloadPackage
classes to dynamically add, remove, and update download items based on the new JSON structure.Status
and Progress
fields to manage the state of downloads for pausing and resuming.By following these steps, the download.json
file will be able to support dynamic addition and removal of download items, as well as track their progress and manage pause/resume functionality.
using this library how to implement dynamic files to be added to downloader manager and track the download progress and pause and resume across all platforms in .NET MAUI and it should support background download too