diff --git a/src/Downloader/ConcurrentStream.cs b/src/Downloader/ConcurrentStream.cs index 520f587..1a1eec9 100644 --- a/src/Downloader/ConcurrentStream.cs +++ b/src/Downloader/ConcurrentStream.cs @@ -6,7 +6,10 @@ namespace Downloader; -public class ConcurrentStream : TaskStateManagement, IDisposable +/// +/// Represents a stream that supports concurrent read and write operations with optional memory buffering. +/// +public class ConcurrentStream : TaskStateManagement, IDisposable, IAsyncDisposable { private ConcurrentPacketBuffer _inputBuffer; private volatile bool _disposed; @@ -14,6 +17,9 @@ public class ConcurrentStream : TaskStateManagement, IDisposable private string _path; private CancellationTokenSource _watcherCancelSource; + /// + /// Gets or sets the path of the file associated with the stream. + /// public string Path { get => _path; @@ -26,6 +32,10 @@ public string Path } } } + + /// + /// Gets the data of the stream as a byte array if the stream is a MemoryStream. + /// public byte[] Data { get @@ -49,38 +59,86 @@ public byte[] Data } } } + + /// + /// Gets a value indicating whether the stream supports reading. + /// public bool CanRead => _stream?.CanRead == true; + + /// + /// Gets a value indicating whether the stream supports seeking. + /// public bool CanSeek => _stream?.CanSeek == true; + + /// + /// Gets a value indicating whether the stream supports writing. + /// public bool CanWrite => _stream?.CanWrite == true; + + /// + /// Gets the length of the stream in bytes. + /// public long Length => _stream?.Length ?? 0; + + /// + /// Gets or sets the current position within the stream. + /// public long Position { get => _stream?.Position ?? 0; set => _stream.Position = value; } + + /// + /// Gets or sets the maximum amount of memory, in bytes, that the stream is allowed to allocate for buffering. + /// public long MaxMemoryBufferBytes { get => _inputBuffer.BufferSize; set => _inputBuffer.BufferSize = value; } - // parameterless constructor for deserialization - public ConcurrentStream() : this(0, null) { } + /// + /// Initializes a new instance of the class with default settings. + /// + public ConcurrentStream() : this(0) { } + /// + /// Initializes a new instance of the class with the specified logger. + /// + /// The logger to use for logging. public ConcurrentStream(ILogger logger) : this(0, logger) { } + /// + /// Initializes a new instance of the class with the specified maximum memory buffer size and logger. + /// + /// The maximum amount of memory, in bytes, that the stream is allowed to allocate for buffering. + /// The logger to use for logging. public ConcurrentStream(long maxMemoryBufferBytes = 0, ILogger logger = null) : base(logger) { _stream = new MemoryStream(); Initial(maxMemoryBufferBytes); } + /// + /// Initializes a new instance of the class with the specified stream and maximum memory buffer size. + /// + /// The stream to use. + /// The maximum amount of memory, in bytes, that the stream is allowed to allocate for buffering. + /// The logger to use for logging. public ConcurrentStream(Stream stream, long maxMemoryBufferBytes = 0, ILogger logger = null) : base(logger) { _stream = stream; Initial(maxMemoryBufferBytes); } + /// + /// Initializes a new instance of the class with the specified file path, initial size, and maximum memory buffer size. + /// + /// The path of the file to use. + /// The initial size of the file. + /// The maximum amount of memory, in bytes, that the stream is allowed to allocate for buffering. + /// The logger to use for logging. public ConcurrentStream(string filename, long initSize, long maxMemoryBufferBytes = 0, ILogger logger = null) : base(logger) { _path = filename; @@ -92,6 +150,11 @@ public ConcurrentStream(string filename, long initSize, long maxMemoryBufferByte Initial(maxMemoryBufferBytes); } + /// + /// Initializes the stream with the specified maximum memory buffer size. + /// + /// The maximum amount of memory, in bytes, that the stream is allowed to allocate for buffering. + /// The logger to use for logging. private void Initial(long maxMemoryBufferBytes, ILogger logger = null) { _inputBuffer = new ConcurrentPacketBuffer(maxMemoryBufferBytes, logger); @@ -106,18 +169,37 @@ private void Initial(long maxMemoryBufferBytes, ILogger logger = null) task.Unwrap(); } + /// + /// Opens the stream for reading. + /// + /// The stream for reading. public Stream OpenRead() { Seek(0, SeekOrigin.Begin); return _stream; } + /// + /// Reads a sequence of bytes from the stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes to store the read data. + /// The zero-based byte offset in buffer at which to begin storing the data read from the stream. + /// The maximum number of bytes to be read from the stream. + /// The total number of bytes read into the buffer. public int Read(byte[] buffer, int offset, int count) { var stream = OpenRead(); return stream.Read(buffer, offset, count); } + /// + /// Writes a sequence of bytes to the stream asynchronously at the specified position. + /// + /// The position within the stream to write the data. + /// The data to write to the stream. + /// The number of bytes to write. + /// A value indicating whether to wait for the write operation to complete. + /// A task that represents the asynchronous write operation. public async Task WriteAsync(long position, byte[] bytes, int length, bool fireAndForget = true) { if (bytes.Length < length) @@ -135,6 +217,10 @@ public async Task WriteAsync(long position, byte[] bytes, int length, bool fireA } } + /// + /// Watches for incoming packets and writes them to the stream. + /// + /// A task that represents the asynchronous watch operation. private async Task Watcher() { try @@ -161,6 +247,12 @@ private async Task Watcher() } } + /// + /// Sets the position within the current stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type SeekOrigin indicating the reference point used to obtain the new position. + /// The new position within the current stream. public long Seek(long offset, SeekOrigin origin) { if (offset != Position && CanSeek) @@ -171,11 +263,20 @@ public long Seek(long offset, SeekOrigin origin) return Position; } + /// + /// Sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. public void SetLength(long value) { _stream.SetLength(value); } + /// + /// Writes a packet to the stream. + /// + /// The packet to write. + /// A task that represents the asynchronous write operation. private async Task WritePacketOnFile(Packet packet) { // seek with SeekOrigin.Begin is so faster than SeekOrigin.Current @@ -184,6 +285,10 @@ private async Task WritePacketOnFile(Packet packet) packet.Dispose(); } + /// + /// Flushes the stream asynchronously. + /// + /// A task that represents the asynchronous flush operation. public async Task FlushAsync() { await _inputBuffer.WaitToComplete().ConfigureAwait(false); @@ -196,9 +301,12 @@ public async Task FlushAsync() GC.Collect(); } + /// + /// Releases the unmanaged resources used by the ConcurrentStream and optionally releases the managed resources. + /// public void Dispose() { - if (_disposed == false) + if (!_disposed) { _disposed = true; _watcherCancelSource.Cancel(); // request the cancellation @@ -206,4 +314,19 @@ public void Dispose() _inputBuffer.Dispose(); } } -} + + /// + /// Asynchronously releases the unmanaged resources used by the ConcurrentStream. + /// + /// A task that represents the asynchronous dispose operation. + public async ValueTask DisposeAsync() + { + if (!_disposed) + { + _disposed = true; + await _watcherCancelSource.CancelAsync().ConfigureAwait(false); // request the cancellation + await _stream.DisposeAsync().ConfigureAwait(false); + _inputBuffer.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Downloader/Download.cs b/src/Downloader/Download.cs index e9a1f1e..4afbc69 100644 --- a/src/Downloader/Download.cs +++ b/src/Downloader/Download.cs @@ -20,26 +20,26 @@ internal class Download : IDownload public event EventHandler ChunkDownloadProgressChanged { - add { _downloadService.ChunkDownloadProgressChanged += value; } - remove { _downloadService.ChunkDownloadProgressChanged -= value; } + add => _downloadService.ChunkDownloadProgressChanged += value; + remove => _downloadService.ChunkDownloadProgressChanged -= value; } public event EventHandler DownloadFileCompleted { - add { _downloadService.DownloadFileCompleted += value; } - remove { _downloadService.DownloadFileCompleted -= value; } + add => _downloadService.DownloadFileCompleted += value; + remove => _downloadService.DownloadFileCompleted -= value; } public event EventHandler DownloadProgressChanged { - add { _downloadService.DownloadProgressChanged += value; } - remove { _downloadService.DownloadProgressChanged -= value; } + add => _downloadService.DownloadProgressChanged += value; + remove => _downloadService.DownloadProgressChanged -= value; } public event EventHandler DownloadStarted { - add { _downloadService.DownloadStarted += value; } - remove { _downloadService.DownloadStarted -= value; } + add => _downloadService.DownloadStarted += value; + remove => _downloadService.DownloadStarted -= value; } public Download(string url, string path, string filename, DownloadConfiguration configuration) @@ -72,26 +72,26 @@ public async Task StartAsync(CancellationToken cancellationToken = defau { return await _downloadService.DownloadFileTaskAsync(Url, cancellationToken).ConfigureAwait(false); } - else if (string.IsNullOrWhiteSpace(Filename)) - { - await _downloadService.DownloadFileTaskAsync(Url, new DirectoryInfo(Folder), cancellationToken).ConfigureAwait(false); - return null; - } - else + + if (string.IsNullOrWhiteSpace(Filename)) { - // with Folder and Filename - await _downloadService.DownloadFileTaskAsync(Url, Path.Combine(Folder, Filename), cancellationToken).ConfigureAwait(false); + await _downloadService.DownloadFileTaskAsync(Url, new DirectoryInfo(Folder!), cancellationToken) + .ConfigureAwait(false); return null; } + + // with Folder and Filename + await _downloadService.DownloadFileTaskAsync(Url, Path.Combine(Folder!, Filename), cancellationToken) + .ConfigureAwait(false); + return null; } - else if(string.IsNullOrWhiteSpace(Url)) + + if (string.IsNullOrWhiteSpace(Url)) { return await _downloadService.DownloadFileTaskAsync(Package, cancellationToken).ConfigureAwait(false); } - else - { - return await _downloadService.DownloadFileTaskAsync(Package, Url, cancellationToken).ConfigureAwait(false); - } + + return await _downloadService.DownloadFileTaskAsync(Package, Url, cancellationToken).ConfigureAwait(false); } public void Stop() @@ -123,9 +123,15 @@ public override int GetHashCode() return hashCode; } - public async void Dispose() + public async ValueTask DisposeAsync() { await _downloadService.Clear().ConfigureAwait(false); Package = null; } -} + + public void Dispose() + { + _downloadService.Clear().Wait(); + Package = null; + } +} \ No newline at end of file diff --git a/src/Downloader/DownloadPackage.cs b/src/Downloader/DownloadPackage.cs index 415891d..0041c5d 100644 --- a/src/Downloader/DownloadPackage.cs +++ b/src/Downloader/DownloadPackage.cs @@ -5,21 +5,74 @@ namespace Downloader; -public class DownloadPackage : IDisposable +/// +/// Represents a package containing information about a download operation. +/// +public class DownloadPackage : IDisposable, IAsyncDisposable { + /// + /// Gets or sets a value indicating whether the package is currently being saved. + /// public bool IsSaving { get; set; } + + /// + /// Gets or sets a value indicating whether the save operation is complete. + /// public bool IsSaveComplete { get; set; } + + /// + /// Gets or sets the progress of the save operation. + /// public double SaveProgress { get; set; } + + /// + /// Gets or sets the status of the download operation. + /// public DownloadStatus Status { get; set; } = DownloadStatus.None; + + /// + /// Gets or sets the URLs from which the file is being downloaded. + /// public string[] Urls { get; set; } + + /// + /// Gets or sets the total size of the file to be downloaded. + /// public long TotalFileSize { get; set; } + + /// + /// Gets or sets the name of the file to be saved. + /// public string FileName { get; set; } + + /// + /// Gets or sets the chunks of the file being downloaded. + /// public Chunk[] Chunks { get; set; } + + /// + /// Gets the total size of the received bytes. + /// public long ReceivedBytesSize => Chunks?.Sum(chunk => chunk.Position) ?? 0; + + /// + /// Gets or sets a value indicating whether the download supports range requests. + /// public bool IsSupportDownloadInRange { get; set; } = true; + + /// + /// Gets a value indicating whether the download is being stored in memory. + /// public bool InMemoryStream => string.IsNullOrWhiteSpace(FileName); + + /// + /// Gets or sets the storage for the download. + /// public ConcurrentStream Storage { get; set; } + /// + /// Clears the chunks and resets the package. + /// public void Clear() { if (Chunks != null) @@ -27,15 +80,23 @@ public void Clear() foreach (Chunk chunk in Chunks) chunk.Clear(); } + Chunks = null; } + /// + /// Flushes the storage asynchronously. + /// + /// A task that represents the asynchronous flush operation. public async Task FlushAsync() { if (Storage?.CanWrite == true) await Storage.FlushAsync().ConfigureAwait(false); } + /// + /// Flushes the storage synchronously. + /// [Obsolete("This method has been deprecated. Please use FlushAsync instead.")] public void Flush() { @@ -43,6 +104,9 @@ public void Flush() Storage?.FlushAsync().Wait(); } + /// + /// Validates the chunks and ensures they are in the correct position. + /// public void Validate() { foreach (var chunk in Chunks) @@ -61,17 +125,37 @@ public void Validate() } } + /// + /// Builds the storage for the download package. + /// + /// Indicates whether to reserve the file size. + /// The maximum size of the memory buffer in bytes. + /// The logger to use for logging. public void BuildStorage(bool reserveFileSize, long maxMemoryBufferBytes = 0, ILogger logger = null) { - if (string.IsNullOrWhiteSpace(FileName)) - Storage = new ConcurrentStream(maxMemoryBufferBytes, logger); - else - Storage = new ConcurrentStream(FileName, reserveFileSize ? TotalFileSize : 0, maxMemoryBufferBytes, logger); + Storage = string.IsNullOrWhiteSpace(FileName) + ? new ConcurrentStream(maxMemoryBufferBytes, logger) + : new ConcurrentStream(FileName, reserveFileSize ? TotalFileSize : 0, maxMemoryBufferBytes, logger); } + /// + /// Disposes of the download package, clearing the chunks and disposing of the storage. + /// public void Dispose() { Clear(); Storage?.Dispose(); } + + /// + /// Disposes of the download package, clearing the chunks and disposing of the storage. + /// + public async ValueTask DisposeAsync() + { + Clear(); + if (Storage is not null) + { + await Storage.DisposeAsync().ConfigureAwait(false); + } + } } \ No newline at end of file diff --git a/src/Downloader/IDownload.cs b/src/Downloader/IDownload.cs index 742d60f..3306547 100644 --- a/src/Downloader/IDownload.cs +++ b/src/Downloader/IDownload.cs @@ -1,28 +1,90 @@ -using System.ComponentModel; -using System; -using System.Threading.Tasks; +using System; +using System.ComponentModel; using System.IO; using System.Threading; +using System.Threading.Tasks; namespace Downloader; -public interface IDownload : IDisposable +/// +/// Represents an interface for managing file downloads. +/// +public interface IDownload : IDisposable, IAsyncDisposable { + /// + /// Gets the URL of the file to be downloaded. + /// public string Url { get; } + + /// + /// Gets the folder where the downloaded file will be saved. + /// public string Folder { get; } + + /// + /// Gets the name of the file to be saved. + /// public string Filename { get; } + + /// + /// Gets the size of the downloaded portion of the file. + /// public long DownloadedFileSize { get; } + + /// + /// Gets the total size of the file to be downloaded. + /// public long TotalFileSize { get; } + + /// + /// Gets the download package containing information about the download. + /// public DownloadPackage Package { get; } + + /// + /// Gets the current status of the download. + /// public DownloadStatus Status { get; } + /// + /// Occurs when the progress of a chunk download changes. + /// public event EventHandler ChunkDownloadProgressChanged; + + /// + /// Occurs when the download file operation is completed. + /// public event EventHandler DownloadFileCompleted; + + /// + /// Occurs when the overall download progress changes. + /// public event EventHandler DownloadProgressChanged; + + /// + /// Occurs when the download operation starts. + /// public event EventHandler DownloadStarted; + /// + /// Starts the download operation asynchronously. + /// + /// A cancellation token that can be used to cancel the download. + /// A task that represents the asynchronous download operation. The task result contains the downloaded stream. public Task StartAsync(CancellationToken cancellationToken = default); + + /// + /// Stops the download operation. + /// public void Stop(); + + /// + /// Pauses the download operation. + /// public void Pause(); + + /// + /// Resumes the paused download operation. + /// public void Resume(); -} +} \ No newline at end of file