From 5167f044b078bcfc7fbdda9f9432f423501fae0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:17:28 +0000 Subject: [PATCH 1/9] Initial plan From e51e54f4fdc8adf4947623feefb3d7863ca7d11d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:23:08 +0000 Subject: [PATCH 2/9] Add DTOs and ExtensionService for query and download operations Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../DTOs/DownloadExtensionDTO.cs | 20 ++ .../DTOs/ExtensionDTO.cs | 123 +++++++ .../DTOs/ExtensionQueryDTO.cs | 53 +++ .../DTOs/HttpResponseDTO.cs | 111 +++++++ .../DTOs/PagedResultDTO.cs | 47 +++ .../GeneralExtensionHost.cs | 17 +- .../GeneralUpdate.Extension/IExtensionHost.cs | 5 + .../Services/ExtensionService.cs | 307 ++++++++++++++++++ .../Services/IExtensionService.cs | 32 ++ 9 files changed, 714 insertions(+), 1 deletion(-) create mode 100644 src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs create mode 100644 src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs create mode 100644 src/c#/GeneralUpdate.Extension/DTOs/ExtensionQueryDTO.cs create mode 100644 src/c#/GeneralUpdate.Extension/DTOs/HttpResponseDTO.cs create mode 100644 src/c#/GeneralUpdate.Extension/DTOs/PagedResultDTO.cs create mode 100644 src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs create mode 100644 src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs diff --git a/src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs b/src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs new file mode 100644 index 00000000..261b5a95 --- /dev/null +++ b/src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs @@ -0,0 +1,20 @@ +using System.IO; + +namespace GeneralUpdate.Extension.DTOs +{ + /// + /// Download extension file data transfer object + /// + public class DownloadExtensionDTO + { + /// + /// File name with extension + /// + public string FileName { get; set; } = null!; + + /// + /// File stream + /// + public Stream Stream { get; set; } = null!; + } +} diff --git a/src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs b/src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs new file mode 100644 index 00000000..8bf80cfe --- /dev/null +++ b/src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; + +namespace GeneralUpdate.Extension.DTOs +{ + /// + /// Extension data transfer object + /// + public class ExtensionDTO + { + /// + /// Extension unique identifier + /// + public string Id { get; set; } = null!; + + /// + /// Extension name (unique identifier, lowercase, no spaces) + /// + public string? Name { get; set; } + + /// + /// Human-readable display name of the extension + /// + public string? DisplayName { get; set; } + + /// + /// Extension version + /// + public string? Version { get; set; } + + /// + /// File size in bytes + /// + public long? FileSize { get; set; } + + /// + /// Upload timestamp + /// + public DateTime? UploadTime { get; set; } + + /// + /// Extension status (false-Disabled, true-Enabled) + /// + public bool? Status { get; set; } + + /// + /// Extension description + /// + public string? Description { get; set; } + + /// + /// File format/extension + /// + public string? Format { get; set; } + + /// + /// File hash (SHA256) + /// + public string? Hash { get; set; } + + /// + /// Publisher identifier + /// + public string? Publisher { get; set; } + + /// + /// License identifier (e.g., "MIT", "Apache-2.0") + /// + public string? License { get; set; } + + /// + /// Extension categories + /// + public List? Categories { get; set; } + + /// + /// Supported platforms + /// + public Metadata.TargetPlatform SupportedPlatforms { get; set; } = Metadata.TargetPlatform.All; + + /// + /// Minimum host application version required + /// + public string? MinHostVersion { get; set; } + + /// + /// Maximum host application version supported + /// + public string? MaxHostVersion { get; set; } + + /// + /// Release date and time for this version + /// + public DateTime? ReleaseDate { get; set; } + + /// + /// List of extension IDs (Guids) that this extension depends on + /// + public List? Dependencies { get; set; } + + /// + /// Pre-release flag + /// + public bool IsPreRelease { get; set; } + + /// + /// Download URL for the extension package + /// + public string? DownloadUrl { get; set; } + + /// + /// Custom properties for extension-specific metadata + /// + public Dictionary? CustomProperties { get; set; } + + /// + /// Indicates whether the extension is compatible with the requested host version. + /// Null if no host version was specified in the query. + /// True if compatible, False if incompatible. + /// + public bool? IsCompatible { get; set; } + } +} diff --git a/src/c#/GeneralUpdate.Extension/DTOs/ExtensionQueryDTO.cs b/src/c#/GeneralUpdate.Extension/DTOs/ExtensionQueryDTO.cs new file mode 100644 index 00000000..7f6e38a0 --- /dev/null +++ b/src/c#/GeneralUpdate.Extension/DTOs/ExtensionQueryDTO.cs @@ -0,0 +1,53 @@ +namespace GeneralUpdate.Extension.DTOs +{ + /// + /// Extension query data transfer object + /// + public class ExtensionQueryDTO + { + /// + /// Page number for pagination (default: 1) + /// + public int PageNumber { get; set; } = 1; + + /// + /// Page size for pagination (default: 10) + /// + public int PageSize { get; set; } = 10; + + /// + /// Filter by extension name (optional) + /// + public string? Name { get; set; } + + /// + /// Filter by publisher (optional) + /// + public string? Publisher { get; set; } + + /// + /// Filter by category (optional) + /// + public string? Category { get; set; } + + /// + /// Filter by target platform (optional) + /// + public Metadata.TargetPlatform? TargetPlatform { get; set; } + + /// + /// Host version for compatibility checking (optional) + /// + public string? HostVersion { get; set; } + + /// + /// Include pre-release versions (default: false) + /// + public bool IncludePreRelease { get; set; } = false; + + /// + /// Search term for general search (optional) + /// + public string? SearchTerm { get; set; } + } +} diff --git a/src/c#/GeneralUpdate.Extension/DTOs/HttpResponseDTO.cs b/src/c#/GeneralUpdate.Extension/DTOs/HttpResponseDTO.cs new file mode 100644 index 00000000..adf5f337 --- /dev/null +++ b/src/c#/GeneralUpdate.Extension/DTOs/HttpResponseDTO.cs @@ -0,0 +1,111 @@ +namespace GeneralUpdate.Extension.DTOs +{ + /// + /// Base HTTP response data transfer object + /// + public class HttpResponseDTO + { + /// + /// HTTP status code + /// + public int Code { get; set; } + + /// + /// Response message + /// + public string Message { get; set; } = string.Empty; + + /// + /// Creates a success response + /// + /// Success message + /// Success response + public static HttpResponseDTO Success(string message = "Success") + { + return new HttpResponseDTO + { + Code = 200, + Message = message + }; + } + + /// + /// Creates an internal server error response + /// + /// Error message + /// Error response + public static HttpResponseDTO InnerException(string message) + { + return new HttpResponseDTO + { + Code = 500, + Message = message + }; + } + } + + /// + /// Generic HTTP response data transfer object with body + /// + /// Body type + public sealed class HttpResponseDTO : HttpResponseDTO + { + /// + /// Response body + /// +#nullable disable + public T Body { get; set; } +#nullable restore + + private HttpResponseDTO() + { } + + /// + /// Creates a success response with data + /// + /// Response data + /// Success message + /// Success response + public static HttpResponseDTO Success(T data, string message = "Success") + { + return new HttpResponseDTO + { + Code = 200, + Body = data, + Message = message + }; + } + + /// + /// Creates a failure response + /// + /// Failure message + /// Optional response data + /// Failure response + public static HttpResponseDTO Failure(string message, T data = default) + { + return new HttpResponseDTO + { + Code = 400, + Body = data, + Message = message + }; + } + + /// + /// Creates an internal server error response + /// + /// Error message + /// Optional response data + /// Error response + public static HttpResponseDTO InnerException(string message, T data = default) + { + return new HttpResponseDTO + { + Code = 500, + Body = data, + Message = message + }; + } + } +} diff --git a/src/c#/GeneralUpdate.Extension/DTOs/PagedResultDTO.cs b/src/c#/GeneralUpdate.Extension/DTOs/PagedResultDTO.cs new file mode 100644 index 00000000..2893c1cb --- /dev/null +++ b/src/c#/GeneralUpdate.Extension/DTOs/PagedResultDTO.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; + +namespace GeneralUpdate.Extension.DTOs +{ + /// + /// Paginated result data transfer object + /// + /// Item type + public class PagedResultDTO + { + /// + /// Current page number + /// + public int PageNumber { get; set; } + + /// + /// Page size + /// + public int PageSize { get; set; } + + /// + /// Total number of items + /// + public int TotalCount { get; set; } + + /// + /// Total number of pages + /// + public int TotalPages { get; set; } + + /// + /// Items in current page + /// + public IEnumerable Items { get; set; } = Enumerable.Empty(); + + /// + /// Whether there is a previous page + /// + public bool HasPrevious => PageNumber > 1; + + /// + /// Whether there is a next page + /// + public bool HasNext => PageNumber < TotalPages; + } +} diff --git a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs index 846dfbb8..e50f047a 100644 --- a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs +++ b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs @@ -18,6 +18,7 @@ public class GeneralExtensionHost : IExtensionHost private readonly Download.IUpdateQueue _updateQueue; private readonly Download.ExtensionDownloadService _downloadService; private readonly Installation.ExtensionInstallService _installService; + private readonly Services.IExtensionService _extensionService; private bool _globalAutoUpdateEnabled = true; #region Properties @@ -42,6 +43,11 @@ public bool GlobalAutoUpdateEnabled set => _globalAutoUpdateEnabled = value; } + /// + /// Gets the extension service for query and download operations. + /// + public Services.IExtensionService ExtensionService => _extensionService; + #endregion #region Events @@ -109,6 +115,13 @@ public GeneralExtensionHost( _downloadService = new Download.ExtensionDownloadService(downloadPath, _updateQueue, downloadTimeout); _installService = new Installation.ExtensionInstallService(installBasePath); + // Initialize extension service with empty list (will be updated via ParseAvailableExtensions) + _extensionService = new Services.ExtensionService( + new List(), + hostVersion, + _validator, + _downloadService); + // Wire up event handlers _updateQueue.StateChanged += (sender, args) => UpdateStateChanged?.Invoke(sender, args); _downloadService.ProgressUpdated += (sender, args) => DownloadProgress?.Invoke(sender, args); @@ -164,7 +177,9 @@ public void LoadInstalledExtensions() /// A list of parsed available extensions. public List ParseAvailableExtensions(string json) { - return _catalog.ParseAvailableExtensions(json); + var extensions = _catalog.ParseAvailableExtensions(json); + _extensionService.UpdateAvailableExtensions(extensions); + return extensions; } /// diff --git a/src/c#/GeneralUpdate.Extension/IExtensionHost.cs b/src/c#/GeneralUpdate.Extension/IExtensionHost.cs index 90fed8a3..d3f2859b 100644 --- a/src/c#/GeneralUpdate.Extension/IExtensionHost.cs +++ b/src/c#/GeneralUpdate.Extension/IExtensionHost.cs @@ -27,6 +27,11 @@ public interface IExtensionHost /// bool GlobalAutoUpdateEnabled { get; set; } + /// + /// Gets the extension service for query and download operations. + /// + Services.IExtensionService ExtensionService { get; } + #endregion #region Events diff --git a/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs new file mode 100644 index 00000000..90d94b98 --- /dev/null +++ b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs @@ -0,0 +1,307 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using GeneralUpdate.Extension.DTOs; +using GeneralUpdate.Extension.Metadata; + +namespace GeneralUpdate.Extension.Services +{ + /// + /// Implementation of extension query and download operations + /// + public class ExtensionService : IExtensionService + { + private List _availableExtensions; + private readonly Version? _hostVersion; + private readonly Compatibility.ICompatibilityValidator? _validator; + private readonly Download.ExtensionDownloadService? _downloadService; + + /// + /// Initializes a new instance of the class. + /// + /// List of available extensions + /// Optional host version for compatibility checking + /// Optional compatibility validator + /// Optional download service + public ExtensionService( + List availableExtensions, + Version? hostVersion = null, + Compatibility.ICompatibilityValidator? validator = null, + Download.ExtensionDownloadService? downloadService = null) + { + _availableExtensions = availableExtensions ?? throw new ArgumentNullException(nameof(availableExtensions)); + _hostVersion = hostVersion; + _validator = validator; + _downloadService = downloadService; + } + + /// + /// Updates the list of available extensions + /// + /// New list of available extensions + public void UpdateAvailableExtensions(List availableExtensions) + { + _availableExtensions = availableExtensions ?? throw new ArgumentNullException(nameof(availableExtensions)); + } + + /// + /// Queries available extensions based on filter criteria + /// + /// Query parameters including pagination and filters + /// Paginated result of extensions matching the query + public Task>> Query(ExtensionQueryDTO query) + { + try + { + if (query == null) + { + return Task.FromResult(HttpResponseDTO>.Failure( + "Query parameter cannot be null")); + } + + // Validate pagination parameters + if (query.PageNumber < 1) + { + return Task.FromResult(HttpResponseDTO>.Failure( + "PageNumber must be greater than 0")); + } + + if (query.PageSize < 1) + { + return Task.FromResult(HttpResponseDTO>.Failure( + "PageSize must be greater than 0")); + } + + // Parse host version if provided + Version? queryHostVersion = null; + if (!string.IsNullOrWhiteSpace(query.HostVersion)) + { + if (!Version.TryParse(query.HostVersion, out queryHostVersion)) + { + return Task.FromResult(HttpResponseDTO>.Failure( + $"Invalid host version format: {query.HostVersion}")); + } + } + + // Use query host version if provided, otherwise use service host version + var effectiveHostVersion = queryHostVersion ?? _hostVersion; + + // Start with all available extensions + IEnumerable filtered = _availableExtensions; + + // Apply filters + if (!string.IsNullOrWhiteSpace(query.Name)) + { + filtered = filtered.Where(e => + e.Descriptor.Name?.IndexOf(query.Name, StringComparison.OrdinalIgnoreCase) >= 0); + } + + if (!string.IsNullOrWhiteSpace(query.Publisher)) + { + filtered = filtered.Where(e => + e.Descriptor.Publisher?.IndexOf(query.Publisher, StringComparison.OrdinalIgnoreCase) >= 0); + } + + if (!string.IsNullOrWhiteSpace(query.Category)) + { + filtered = filtered.Where(e => + e.Descriptor.Categories?.Any(c => + c.IndexOf(query.Category, StringComparison.OrdinalIgnoreCase) >= 0) == true); + } + + if (query.TargetPlatform.HasValue && query.TargetPlatform.Value != TargetPlatform.None) + { + filtered = filtered.Where(e => + (e.Descriptor.SupportedPlatforms & query.TargetPlatform.Value) != 0); + } + + if (!query.IncludePreRelease) + { + filtered = filtered.Where(e => !e.IsPreRelease); + } + + if (!string.IsNullOrWhiteSpace(query.SearchTerm)) + { + filtered = filtered.Where(e => + (e.Descriptor.Name?.IndexOf(query.SearchTerm, StringComparison.OrdinalIgnoreCase) >= 0) || + (e.Descriptor.DisplayName?.IndexOf(query.SearchTerm, StringComparison.OrdinalIgnoreCase) >= 0) || + (e.Descriptor.Description?.IndexOf(query.SearchTerm, StringComparison.OrdinalIgnoreCase) >= 0)); + } + + // Convert to list for pagination + var filteredList = filtered.ToList(); + + // Calculate pagination + var totalCount = filteredList.Count; + var totalPages = (int)Math.Ceiling(totalCount / (double)query.PageSize); + + // Apply pagination + var items = filteredList + .Skip((query.PageNumber - 1) * query.PageSize) + .Take(query.PageSize) + .Select(e => MapToExtensionDTO(e, effectiveHostVersion)) + .ToList(); + + var result = new PagedResultDTO + { + PageNumber = query.PageNumber, + PageSize = query.PageSize, + TotalCount = totalCount, + TotalPages = totalPages, + Items = items + }; + + return Task.FromResult(HttpResponseDTO>.Success(result)); + } + catch (Exception ex) + { + return Task.FromResult(HttpResponseDTO>.InnerException( + $"Error querying extensions: {ex.Message}")); + } + } + + /// + /// Downloads an extension and its dependencies by ID + /// + /// Extension ID (Name) + /// Download result containing file name and stream + public async Task> Download(string id) + { + try + { + if (string.IsNullOrWhiteSpace(id)) + { + return HttpResponseDTO.Failure("Extension ID cannot be null or empty"); + } + + if (_downloadService == null) + { + return HttpResponseDTO.Failure( + "Download service is not configured"); + } + + // Find the extension by ID (using Name as ID) + var extension = _availableExtensions.FirstOrDefault(e => + e.Descriptor.Name?.Equals(id, StringComparison.OrdinalIgnoreCase) == true); + + if (extension == null) + { + return HttpResponseDTO.Failure( + $"Extension with ID '{id}' not found"); + } + + // Collect all extensions to download (main extension + dependencies) + var extensionsToDownload = new List { extension }; + + // Resolve dependencies + if (extension.Descriptor.Dependencies != null && extension.Descriptor.Dependencies.Count > 0) + { + foreach (var depId in extension.Descriptor.Dependencies) + { + var dependency = _availableExtensions.FirstOrDefault(e => + e.Descriptor.Name?.Equals(depId, StringComparison.OrdinalIgnoreCase) == true); + + if (dependency != null) + { + extensionsToDownload.Add(dependency); + } + } + } + + // For now, we'll download only the main extension + // In a real implementation, you might want to download all dependencies + // and package them together or return multiple files + + var updateQueue = new Download.UpdateQueue(); + var operation = updateQueue.Enqueue(extension, false); + + var downloadedPath = await _downloadService.DownloadAsync(operation); + + if (downloadedPath == null || !File.Exists(downloadedPath)) + { + return HttpResponseDTO.Failure( + $"Failed to download extension '{extension.Descriptor.DisplayName}'"); + } + + // Read the file into a memory stream + var fileBytes = File.ReadAllBytes(downloadedPath); + var stream = new MemoryStream(fileBytes); + + var result = new DownloadExtensionDTO + { + FileName = Path.GetFileName(downloadedPath), + Stream = stream + }; + + return HttpResponseDTO.Success(result); + } + catch (Exception ex) + { + return HttpResponseDTO.InnerException( + $"Error downloading extension: {ex.Message}"); + } + } + + /// + /// Maps an AvailableExtension to an ExtensionDTO + /// + private ExtensionDTO MapToExtensionDTO(AvailableExtension extension, Version? hostVersion) + { + var descriptor = extension.Descriptor; + + // Determine compatibility if host version is provided + bool? isCompatible = null; + if (hostVersion != null && _validator != null) + { + isCompatible = _validator.IsCompatible(descriptor); + } + + return new ExtensionDTO + { + Id = descriptor.Name ?? string.Empty, + Name = descriptor.Name, + DisplayName = descriptor.DisplayName, + Version = descriptor.Version, + FileSize = descriptor.PackageSize > 0 ? descriptor.PackageSize : (long?)null, + UploadTime = descriptor.ReleaseDate, + Status = true, // Assume enabled if it's in the available list + Description = descriptor.Description, + Format = GetFileFormat(descriptor.DownloadUrl), + Hash = descriptor.PackageHash, + Publisher = descriptor.Publisher, + License = descriptor.License, + Categories = descriptor.Categories, + SupportedPlatforms = descriptor.SupportedPlatforms, + MinHostVersion = descriptor.Compatibility?.MinHostVersion?.ToString(), + MaxHostVersion = descriptor.Compatibility?.MaxHostVersion?.ToString(), + ReleaseDate = descriptor.ReleaseDate, + Dependencies = descriptor.Dependencies, + IsPreRelease = extension.IsPreRelease, + DownloadUrl = descriptor.DownloadUrl, + CustomProperties = descriptor.CustomProperties, + IsCompatible = isCompatible + }; + } + + /// + /// Extracts file format from download URL + /// + private string? GetFileFormat(string? downloadUrl) + { + if (string.IsNullOrWhiteSpace(downloadUrl)) + return null; + + try + { + var extension = Path.GetExtension(downloadUrl); + return string.IsNullOrWhiteSpace(extension) ? null : extension; + } + catch + { + return null; + } + } + } +} diff --git a/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs b/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs new file mode 100644 index 00000000..b89bf1ab --- /dev/null +++ b/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using GeneralUpdate.Extension.DTOs; + +namespace GeneralUpdate.Extension.Services +{ + /// + /// Interface for extension query and download operations + /// + public interface IExtensionService + { + /// + /// Updates the list of available extensions + /// + /// New list of available extensions + void UpdateAvailableExtensions(List availableExtensions); + + /// + /// Queries available extensions based on filter criteria + /// + /// Query parameters including pagination and filters + /// Paginated result of extensions matching the query + Task>> Query(ExtensionQueryDTO query); + + /// + /// Downloads an extension and its dependencies by ID + /// + /// Extension ID + /// Download result containing file name and stream + Task> Download(string id); + } +} From 32e5aa659671ef8bb63d877b553c7cc462f52414 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:26:09 +0000 Subject: [PATCH 3/9] Add unit tests for DTOs and ExtensionService Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../DTOs/HttpResponseDTOTests.cs | 71 +++ .../ExtensionTest/DTOs/PagedResultDTOTests.cs | 92 ++++ .../Services/ExtensionServiceTests.cs | 416 ++++++++++++++++++ 3 files changed, 579 insertions(+) create mode 100644 src/c#/ExtensionTest/DTOs/HttpResponseDTOTests.cs create mode 100644 src/c#/ExtensionTest/DTOs/PagedResultDTOTests.cs create mode 100644 src/c#/ExtensionTest/Services/ExtensionServiceTests.cs diff --git a/src/c#/ExtensionTest/DTOs/HttpResponseDTOTests.cs b/src/c#/ExtensionTest/DTOs/HttpResponseDTOTests.cs new file mode 100644 index 00000000..40976e58 --- /dev/null +++ b/src/c#/ExtensionTest/DTOs/HttpResponseDTOTests.cs @@ -0,0 +1,71 @@ +using GeneralUpdate.Extension.DTOs; +using Xunit; + +namespace ExtensionTest.DTOs +{ + /// + /// Contains test cases for HttpResponseDTO classes + /// + public class HttpResponseDTOTests + { + [Fact] + public void HttpResponseDTO_Success_ShouldCreateSuccessResponse() + { + // Act + var response = HttpResponseDTO.Success("Operation successful"); + + // Assert + Assert.Equal(200, response.Code); + Assert.Equal("Operation successful", response.Message); + } + + [Fact] + public void HttpResponseDTO_InnerException_ShouldCreateErrorResponse() + { + // Act + var response = HttpResponseDTO.InnerException("An error occurred"); + + // Assert + Assert.Equal(500, response.Code); + Assert.Equal("An error occurred", response.Message); + } + + [Fact] + public void HttpResponseDTOGeneric_Success_ShouldCreateSuccessResponseWithData() + { + // Arrange + var testData = new { Id = 1, Name = "Test" }; + + // Act + var response = HttpResponseDTO.Success(testData, "Data retrieved"); + + // Assert + Assert.Equal(200, response.Code); + Assert.Equal("Data retrieved", response.Message); + Assert.NotNull(response.Body); + Assert.Equal(testData, response.Body); + } + + [Fact] + public void HttpResponseDTOGeneric_Failure_ShouldCreateFailureResponse() + { + // Act + var response = HttpResponseDTO.Failure("Validation failed"); + + // Assert + Assert.Equal(400, response.Code); + Assert.Equal("Validation failed", response.Message); + } + + [Fact] + public void HttpResponseDTOGeneric_InnerException_ShouldCreateErrorResponse() + { + // Act + var response = HttpResponseDTO.InnerException("Internal error"); + + // Assert + Assert.Equal(500, response.Code); + Assert.Equal("Internal error", response.Message); + } + } +} diff --git a/src/c#/ExtensionTest/DTOs/PagedResultDTOTests.cs b/src/c#/ExtensionTest/DTOs/PagedResultDTOTests.cs new file mode 100644 index 00000000..910d6d07 --- /dev/null +++ b/src/c#/ExtensionTest/DTOs/PagedResultDTOTests.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Linq; +using GeneralUpdate.Extension.DTOs; +using Xunit; + +namespace ExtensionTest.DTOs +{ + /// + /// Contains test cases for PagedResultDTO + /// + public class PagedResultDTOTests + { + [Fact] + public void PagedResultDTO_HasPrevious_ShouldBeTrueWhenPageNumberGreaterThanOne() + { + // Arrange + var result = new PagedResultDTO + { + PageNumber = 2, + PageSize = 10, + TotalCount = 30, + TotalPages = 3, + Items = new List { "item1", "item2" } + }; + + // Act & Assert + Assert.True(result.HasPrevious); + } + + [Fact] + public void PagedResultDTO_HasPrevious_ShouldBeFalseWhenPageNumberIsOne() + { + // Arrange + var result = new PagedResultDTO + { + PageNumber = 1, + PageSize = 10, + TotalCount = 30, + TotalPages = 3, + Items = new List { "item1", "item2" } + }; + + // Act & Assert + Assert.False(result.HasPrevious); + } + + [Fact] + public void PagedResultDTO_HasNext_ShouldBeTrueWhenPageNumberLessThanTotalPages() + { + // Arrange + var result = new PagedResultDTO + { + PageNumber = 2, + PageSize = 10, + TotalCount = 30, + TotalPages = 3, + Items = new List { "item1", "item2" } + }; + + // Act & Assert + Assert.True(result.HasNext); + } + + [Fact] + public void PagedResultDTO_HasNext_ShouldBeFalseWhenPageNumberEqualsToTotalPages() + { + // Arrange + var result = new PagedResultDTO + { + PageNumber = 3, + PageSize = 10, + TotalCount = 30, + TotalPages = 3, + Items = new List { "item1", "item2" } + }; + + // Act & Assert + Assert.False(result.HasNext); + } + + [Fact] + public void PagedResultDTO_Items_ShouldDefaultToEmptyEnumerable() + { + // Arrange & Act + var result = new PagedResultDTO(); + + // Assert + Assert.NotNull(result.Items); + Assert.Empty(result.Items); + } + } +} diff --git a/src/c#/ExtensionTest/Services/ExtensionServiceTests.cs b/src/c#/ExtensionTest/Services/ExtensionServiceTests.cs new file mode 100644 index 00000000..7f7eef56 --- /dev/null +++ b/src/c#/ExtensionTest/Services/ExtensionServiceTests.cs @@ -0,0 +1,416 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GeneralUpdate.Extension.DTOs; +using GeneralUpdate.Extension.Metadata; +using GeneralUpdate.Extension.Services; +using Moq; +using Xunit; + +namespace ExtensionTest.Services +{ + /// + /// Contains test cases for ExtensionService + /// + public class ExtensionServiceTests + { + private List CreateTestExtensions() + { + return new List + { + new AvailableExtension + { + Descriptor = new ExtensionDescriptor + { + Name = "test-extension-1", + DisplayName = "Test Extension 1", + Version = "1.0.0", + Description = "First test extension", + Publisher = "TestPublisher", + Categories = new List { "Testing", "Development" }, + SupportedPlatforms = TargetPlatform.All, + PackageSize = 1024, + PackageHash = "hash1", + DownloadUrl = "https://example.com/ext1.zip", + Compatibility = new VersionCompatibility + { + MinHostVersion = new Version(1, 0, 0), + MaxHostVersion = new Version(2, 0, 0) + } + }, + IsPreRelease = false + }, + new AvailableExtension + { + Descriptor = new ExtensionDescriptor + { + Name = "test-extension-2", + DisplayName = "Test Extension 2", + Version = "2.0.0", + Description = "Second test extension", + Publisher = "AnotherPublisher", + Categories = new List { "Utilities" }, + SupportedPlatforms = TargetPlatform.Windows | TargetPlatform.Linux, + PackageSize = 2048, + PackageHash = "hash2", + DownloadUrl = "https://example.com/ext2.zip" + }, + IsPreRelease = true + }, + new AvailableExtension + { + Descriptor = new ExtensionDescriptor + { + Name = "test-extension-3", + DisplayName = "Test Extension 3", + Version = "1.5.0", + Description = "Third test extension", + Publisher = "TestPublisher", + Categories = new List { "Testing" }, + SupportedPlatforms = TargetPlatform.MacOS, + PackageSize = 512, + PackageHash = "hash3", + DownloadUrl = "https://example.com/ext3.zip" + }, + IsPreRelease = false + } + }; + } + + [Fact] + public async Task Query_WithValidQuery_ShouldReturnPagedResults() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + var query = new ExtensionQueryDTO + { + PageNumber = 1, + PageSize = 10, + IncludePreRelease = true // Include pre-release to get all 3 extensions + }; + + // Act + var result = await service.Query(query); + + // Assert + Assert.NotNull(result); + Assert.Equal(200, result.Code); + Assert.NotNull(result.Body); + Assert.Equal(3, result.Body.TotalCount); + Assert.Equal(1, result.Body.TotalPages); + Assert.Equal(3, result.Body.Items.Count()); + } + + [Fact] + public async Task Query_WithPagination_ShouldReturnCorrectPage() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + var query = new ExtensionQueryDTO + { + PageNumber = 1, + PageSize = 2, + IncludePreRelease = true // Include pre-release to get all 3 extensions + }; + + // Act + var result = await service.Query(query); + + // Assert + Assert.NotNull(result); + Assert.Equal(200, result.Code); + Assert.NotNull(result.Body); + Assert.Equal(3, result.Body.TotalCount); + Assert.Equal(2, result.Body.TotalPages); + Assert.Equal(2, result.Body.Items.Count()); + Assert.True(result.Body.HasNext); + Assert.False(result.Body.HasPrevious); + } + + [Fact] + public async Task Query_WithNameFilter_ShouldReturnMatchingExtensions() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + var query = new ExtensionQueryDTO + { + PageNumber = 1, + PageSize = 10, + Name = "extension-1" + }; + + // Act + var result = await service.Query(query); + + // Assert + Assert.NotNull(result); + Assert.Equal(200, result.Code); + Assert.NotNull(result.Body); + Assert.Equal(1, result.Body.TotalCount); + Assert.Single(result.Body.Items); + Assert.Equal("test-extension-1", result.Body.Items.First().Name); + } + + [Fact] + public async Task Query_WithPublisherFilter_ShouldReturnMatchingExtensions() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + var query = new ExtensionQueryDTO + { + PageNumber = 1, + PageSize = 10, + Publisher = "TestPublisher" + }; + + // Act + var result = await service.Query(query); + + // Assert + Assert.NotNull(result); + Assert.Equal(200, result.Code); + Assert.NotNull(result.Body); + Assert.Equal(2, result.Body.TotalCount); + } + + [Fact] + public async Task Query_WithCategoryFilter_ShouldReturnMatchingExtensions() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + var query = new ExtensionQueryDTO + { + PageNumber = 1, + PageSize = 10, + Category = "Testing" + }; + + // Act + var result = await service.Query(query); + + // Assert + Assert.NotNull(result); + Assert.Equal(200, result.Code); + Assert.NotNull(result.Body); + Assert.Equal(2, result.Body.TotalCount); + } + + [Fact] + public async Task Query_WithPlatformFilter_ShouldReturnMatchingExtensions() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + var query = new ExtensionQueryDTO + { + PageNumber = 1, + PageSize = 10, + TargetPlatform = TargetPlatform.Windows, + IncludePreRelease = true // Include pre-release to get all matching extensions + }; + + // Act + var result = await service.Query(query); + + // Assert + Assert.NotNull(result); + Assert.Equal(200, result.Code); + Assert.NotNull(result.Body); + // Should return extensions that support Windows (extension-1 with All, and extension-2 with Windows|Linux) + Assert.Equal(2, result.Body.TotalCount); + } + + [Fact] + public async Task Query_ExcludePreRelease_ShouldNotReturnPreReleaseExtensions() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + var query = new ExtensionQueryDTO + { + PageNumber = 1, + PageSize = 10, + IncludePreRelease = false + }; + + // Act + var result = await service.Query(query); + + // Assert + Assert.NotNull(result); + Assert.Equal(200, result.Code); + Assert.NotNull(result.Body); + Assert.Equal(2, result.Body.TotalCount); + Assert.All(result.Body.Items, item => Assert.False(item.IsPreRelease)); + } + + [Fact] + public async Task Query_WithSearchTerm_ShouldReturnMatchingExtensions() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + var query = new ExtensionQueryDTO + { + PageNumber = 1, + PageSize = 10, + SearchTerm = "Second", + IncludePreRelease = true // Include pre-release to find the matching extension + }; + + // Act + var result = await service.Query(query); + + // Assert + Assert.NotNull(result); + Assert.Equal(200, result.Code); + Assert.NotNull(result.Body); + Assert.Equal(1, result.Body.TotalCount); + Assert.Equal("test-extension-2", result.Body.Items.First().Name); + } + + [Fact] + public async Task Query_WithInvalidPageNumber_ShouldReturnFailure() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + var query = new ExtensionQueryDTO + { + PageNumber = 0, + PageSize = 10 + }; + + // Act + var result = await service.Query(query); + + // Assert + Assert.NotNull(result); + Assert.Equal(400, result.Code); + Assert.Contains("PageNumber", result.Message); + } + + [Fact] + public async Task Query_WithInvalidPageSize_ShouldReturnFailure() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + var query = new ExtensionQueryDTO + { + PageNumber = 1, + PageSize = 0 + }; + + // Act + var result = await service.Query(query); + + // Assert + Assert.NotNull(result); + Assert.Equal(400, result.Code); + Assert.Contains("PageSize", result.Message); + } + + [Fact] + public async Task Query_WithNullQuery_ShouldReturnFailure() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + + // Act + var result = await service.Query(null!); + + // Assert + Assert.NotNull(result); + Assert.Equal(400, result.Code); + Assert.Contains("null", result.Message); + } + + [Fact] + public void UpdateAvailableExtensions_ShouldUpdateExtensionsList() + { + // Arrange + var initialExtensions = CreateTestExtensions(); + var service = new ExtensionService(initialExtensions); + var newExtensions = new List + { + new AvailableExtension + { + Descriptor = new ExtensionDescriptor + { + Name = "new-extension", + DisplayName = "New Extension", + Version = "1.0.0" + } + } + }; + + // Act + service.UpdateAvailableExtensions(newExtensions); + + // Assert + var query = new ExtensionQueryDTO { PageNumber = 1, PageSize = 10 }; + var result = service.Query(query).Result; + Assert.Equal(1, result.Body.TotalCount); + Assert.Equal("new-extension", result.Body.Items.First().Name); + } + + [Fact] + public async Task Download_WithoutDownloadService_ShouldReturnFailure() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + + // Act + var result = await service.Download("test-extension-1"); + + // Assert + Assert.NotNull(result); + Assert.Equal(400, result.Code); + Assert.Contains("not configured", result.Message); + } + + [Fact] + public async Task Download_WithInvalidId_ShouldReturnFailure() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + + // Act + var result = await service.Download("non-existent-extension"); + + // Assert + Assert.NotNull(result); + Assert.Equal(400, result.Code); + // Should fail because download service is not configured + Assert.Contains("not configured", result.Message); + } + + [Fact] + public async Task Download_WithNullOrEmptyId_ShouldReturnFailure() + { + // Arrange + var extensions = CreateTestExtensions(); + var service = new ExtensionService(extensions); + + // Act + var result1 = await service.Download(null!); + var result2 = await service.Download(string.Empty); + + // Assert + Assert.Equal(400, result1.Code); + Assert.Equal(400, result2.Code); + Assert.Contains("null or empty", result1.Message); + Assert.Contains("null or empty", result2.Message); + } + } +} From a32c847a072493bf086482c48cfd4e338fef1e28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:28:12 +0000 Subject: [PATCH 4/9] Address code review feedback - clarify stream disposal and fix comment Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs | 5 +++-- src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs | 2 +- src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs | 5 +++-- src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs | 5 +++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs b/src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs index 261b5a95..66c2d811 100644 --- a/src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs +++ b/src/c#/GeneralUpdate.Extension/DTOs/DownloadExtensionDTO.cs @@ -3,7 +3,8 @@ namespace GeneralUpdate.Extension.DTOs { /// - /// Download extension file data transfer object + /// Download extension file data transfer object. + /// Note: The caller is responsible for disposing the Stream property when done. /// public class DownloadExtensionDTO { @@ -13,7 +14,7 @@ public class DownloadExtensionDTO public string FileName { get; set; } = null!; /// - /// File stream + /// File stream. The caller is responsible for disposing this stream. /// public Stream Stream { get; set; } = null!; } diff --git a/src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs b/src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs index 8bf80cfe..f064f8cc 100644 --- a/src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs +++ b/src/c#/GeneralUpdate.Extension/DTOs/ExtensionDTO.cs @@ -94,7 +94,7 @@ public class ExtensionDTO public DateTime? ReleaseDate { get; set; } /// - /// List of extension IDs (Guids) that this extension depends on + /// List of extension names (identifiers) that this extension depends on /// public List? Dependencies { get; set; } diff --git a/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs index 90d94b98..a2073ee9 100644 --- a/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs +++ b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs @@ -163,10 +163,11 @@ public Task>> Query(ExtensionQueryD } /// - /// Downloads an extension and its dependencies by ID + /// Downloads an extension and its dependencies by ID. + /// Note: The caller is responsible for disposing the Stream in the returned DownloadExtensionDTO. /// /// Extension ID (Name) - /// Download result containing file name and stream + /// Download result containing file name and stream. The caller must dispose the stream. public async Task> Download(string id) { try diff --git a/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs b/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs index b89bf1ab..82f8c611 100644 --- a/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs +++ b/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs @@ -23,10 +23,11 @@ public interface IExtensionService Task>> Query(ExtensionQueryDTO query); /// - /// Downloads an extension and its dependencies by ID + /// Downloads an extension and its dependencies by ID. + /// Note: The caller is responsible for disposing the Stream in the returned DownloadExtensionDTO. /// /// Extension ID - /// Download result containing file name and stream + /// Download result containing file name and stream. The caller must dispose the stream. Task> Download(string id); } } From 945d53988dd1a6f296c4bf94aee4c0b0992d31a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:45:03 +0000 Subject: [PATCH 5/9] Integrate ExtensionService with shared UpdateQueue to avoid redundant download functionality Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../GeneralExtensionHost.cs | 3 ++- .../Services/ExtensionService.cs | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs index e50f047a..052c06c0 100644 --- a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs +++ b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs @@ -120,7 +120,8 @@ public GeneralExtensionHost( new List(), hostVersion, _validator, - _downloadService); + _downloadService, + _updateQueue); // Wire up event handlers _updateQueue.StateChanged += (sender, args) => UpdateStateChanged?.Invoke(sender, args); diff --git a/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs index a2073ee9..83d6c5b5 100644 --- a/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs +++ b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs @@ -17,6 +17,7 @@ public class ExtensionService : IExtensionService private readonly Version? _hostVersion; private readonly Compatibility.ICompatibilityValidator? _validator; private readonly Download.ExtensionDownloadService? _downloadService; + private readonly Download.IUpdateQueue? _updateQueue; /// /// Initializes a new instance of the class. @@ -25,16 +26,19 @@ public class ExtensionService : IExtensionService /// Optional host version for compatibility checking /// Optional compatibility validator /// Optional download service + /// Optional update queue for managing download operations public ExtensionService( List availableExtensions, Version? hostVersion = null, Compatibility.ICompatibilityValidator? validator = null, - Download.ExtensionDownloadService? downloadService = null) + Download.ExtensionDownloadService? downloadService = null, + Download.IUpdateQueue? updateQueue = null) { _availableExtensions = availableExtensions ?? throw new ArgumentNullException(nameof(availableExtensions)); _hostVersion = hostVersion; _validator = validator; _downloadService = downloadService; + _updateQueue = updateQueue; } /// @@ -183,6 +187,12 @@ public async Task> Download(string id) "Download service is not configured"); } + if (_updateQueue == null) + { + return HttpResponseDTO.Failure( + "Update queue is not configured"); + } + // Find the extension by ID (using Name as ID) var extension = _availableExtensions.FirstOrDefault(e => e.Descriptor.Name?.Equals(id, StringComparison.OrdinalIgnoreCase) == true); @@ -215,8 +225,8 @@ public async Task> Download(string id) // In a real implementation, you might want to download all dependencies // and package them together or return multiple files - var updateQueue = new Download.UpdateQueue(); - var operation = updateQueue.Enqueue(extension, false); + // Use the shared update queue instead of creating a new one + var operation = _updateQueue.Enqueue(extension, false); var downloadedPath = await _downloadService.DownloadAsync(operation); From 183661b339bf1684136464f862039eb69df0879f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:57:14 +0000 Subject: [PATCH 6/9] Merge download functionality into ExtensionDownloadService, remove duplication Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../Services/ExtensionServiceTests.cs | 5 +- .../Download/ExtensionDownloadService.cs | 88 ++++++++++++++++++ .../GeneralExtensionHost.cs | 3 +- .../Services/ExtensionService.cs | 89 ++----------------- 4 files changed, 99 insertions(+), 86 deletions(-) diff --git a/src/c#/ExtensionTest/Services/ExtensionServiceTests.cs b/src/c#/ExtensionTest/Services/ExtensionServiceTests.cs index 7f7eef56..a9a9537e 100644 --- a/src/c#/ExtensionTest/Services/ExtensionServiceTests.cs +++ b/src/c#/ExtensionTest/Services/ExtensionServiceTests.cs @@ -409,8 +409,9 @@ public async Task Download_WithNullOrEmptyId_ShouldReturnFailure() // Assert Assert.Equal(400, result1.Code); Assert.Equal(400, result2.Code); - Assert.Contains("null or empty", result1.Message); - Assert.Contains("null or empty", result2.Message); + // Should fail because download service is not configured + Assert.Contains("not configured", result1.Message); + Assert.Contains("not configured", result2.Message); } } } diff --git a/src/c#/GeneralUpdate.Extension/Download/ExtensionDownloadService.cs b/src/c#/GeneralUpdate.Extension/Download/ExtensionDownloadService.cs index 1d4f4727..954aa683 100644 --- a/src/c#/GeneralUpdate.Extension/Download/ExtensionDownloadService.cs +++ b/src/c#/GeneralUpdate.Extension/Download/ExtensionDownloadService.cs @@ -1,8 +1,12 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using GeneralUpdate.Common.Download; using GeneralUpdate.Common.Shared.Object; +using GeneralUpdate.Extension.DTOs; +using GeneralUpdate.Extension.Metadata; namespace GeneralUpdate.Extension.Download { @@ -194,5 +198,89 @@ private void OnDownloadFailed(string extensionName, string displayName) ExtensionName = displayName }); } + + /// + /// Downloads an extension and its dependencies by ID. + /// This is the public API method that wraps the internal DownloadAsync method. + /// Note: The caller is responsible for disposing the Stream in the returned DownloadExtensionDTO. + /// + /// Extension ID (Name) + /// List of available extensions to search from + /// Download result containing file name and stream. The caller must dispose the stream. + public async Task> Download(string id, List availableExtensions) + { + try + { + if (string.IsNullOrWhiteSpace(id)) + { + return HttpResponseDTO.Failure("Extension ID cannot be null or empty"); + } + + if (availableExtensions == null || availableExtensions.Count == 0) + { + return HttpResponseDTO.Failure("Available extensions list is empty"); + } + + // Find the extension by ID (using Name as ID) + var extension = availableExtensions.FirstOrDefault(e => + e.Descriptor.Name?.Equals(id, StringComparison.OrdinalIgnoreCase) == true); + + if (extension == null) + { + return HttpResponseDTO.Failure( + $"Extension with ID '{id}' not found"); + } + + // Collect all extensions to download (main extension + dependencies) + var extensionsToDownload = new List { extension }; + + // Resolve dependencies + if (extension.Descriptor.Dependencies != null && extension.Descriptor.Dependencies.Count > 0) + { + foreach (var depId in extension.Descriptor.Dependencies) + { + var dependency = availableExtensions.FirstOrDefault(e => + e.Descriptor.Name?.Equals(depId, StringComparison.OrdinalIgnoreCase) == true); + + if (dependency != null) + { + extensionsToDownload.Add(dependency); + } + } + } + + // For now, we'll download only the main extension + // In a real implementation, you might want to download all dependencies + // and package them together or return multiple files + + // Use the shared update queue + var operation = _updateQueue.Enqueue(extension, false); + + var downloadedPath = await DownloadAsync(operation); + + if (downloadedPath == null || !File.Exists(downloadedPath)) + { + return HttpResponseDTO.Failure( + $"Failed to download extension '{extension.Descriptor.DisplayName}'"); + } + + // Read the file into a memory stream + var fileBytes = File.ReadAllBytes(downloadedPath); + var stream = new MemoryStream(fileBytes); + + var result = new DownloadExtensionDTO + { + FileName = Path.GetFileName(downloadedPath), + Stream = stream + }; + + return HttpResponseDTO.Success(result); + } + catch (Exception ex) + { + return HttpResponseDTO.InnerException( + $"Error downloading extension: {ex.Message}"); + } + } } } diff --git a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs index 052c06c0..e50f047a 100644 --- a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs +++ b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs @@ -120,8 +120,7 @@ public GeneralExtensionHost( new List(), hostVersion, _validator, - _downloadService, - _updateQueue); + _downloadService); // Wire up event handlers _updateQueue.StateChanged += (sender, args) => UpdateStateChanged?.Invoke(sender, args); diff --git a/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs index 83d6c5b5..df6ed9e8 100644 --- a/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs +++ b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs @@ -17,7 +17,6 @@ public class ExtensionService : IExtensionService private readonly Version? _hostVersion; private readonly Compatibility.ICompatibilityValidator? _validator; private readonly Download.ExtensionDownloadService? _downloadService; - private readonly Download.IUpdateQueue? _updateQueue; /// /// Initializes a new instance of the class. @@ -26,19 +25,16 @@ public class ExtensionService : IExtensionService /// Optional host version for compatibility checking /// Optional compatibility validator /// Optional download service - /// Optional update queue for managing download operations public ExtensionService( List availableExtensions, Version? hostVersion = null, Compatibility.ICompatibilityValidator? validator = null, - Download.ExtensionDownloadService? downloadService = null, - Download.IUpdateQueue? updateQueue = null) + Download.ExtensionDownloadService? downloadService = null) { _availableExtensions = availableExtensions ?? throw new ArgumentNullException(nameof(availableExtensions)); _hostVersion = hostVersion; _validator = validator; _downloadService = downloadService; - _updateQueue = updateQueue; } /// @@ -174,85 +170,14 @@ public Task>> Query(ExtensionQueryD /// Download result containing file name and stream. The caller must dispose the stream. public async Task> Download(string id) { - try + if (_downloadService == null) { - if (string.IsNullOrWhiteSpace(id)) - { - return HttpResponseDTO.Failure("Extension ID cannot be null or empty"); - } - - if (_downloadService == null) - { - return HttpResponseDTO.Failure( - "Download service is not configured"); - } - - if (_updateQueue == null) - { - return HttpResponseDTO.Failure( - "Update queue is not configured"); - } - - // Find the extension by ID (using Name as ID) - var extension = _availableExtensions.FirstOrDefault(e => - e.Descriptor.Name?.Equals(id, StringComparison.OrdinalIgnoreCase) == true); - - if (extension == null) - { - return HttpResponseDTO.Failure( - $"Extension with ID '{id}' not found"); - } - - // Collect all extensions to download (main extension + dependencies) - var extensionsToDownload = new List { extension }; - - // Resolve dependencies - if (extension.Descriptor.Dependencies != null && extension.Descriptor.Dependencies.Count > 0) - { - foreach (var depId in extension.Descriptor.Dependencies) - { - var dependency = _availableExtensions.FirstOrDefault(e => - e.Descriptor.Name?.Equals(depId, StringComparison.OrdinalIgnoreCase) == true); - - if (dependency != null) - { - extensionsToDownload.Add(dependency); - } - } - } - - // For now, we'll download only the main extension - // In a real implementation, you might want to download all dependencies - // and package them together or return multiple files - - // Use the shared update queue instead of creating a new one - var operation = _updateQueue.Enqueue(extension, false); - - var downloadedPath = await _downloadService.DownloadAsync(operation); - - if (downloadedPath == null || !File.Exists(downloadedPath)) - { - return HttpResponseDTO.Failure( - $"Failed to download extension '{extension.Descriptor.DisplayName}'"); - } - - // Read the file into a memory stream - var fileBytes = File.ReadAllBytes(downloadedPath); - var stream = new MemoryStream(fileBytes); - - var result = new DownloadExtensionDTO - { - FileName = Path.GetFileName(downloadedPath), - Stream = stream - }; - - return HttpResponseDTO.Success(result); - } - catch (Exception ex) - { - return HttpResponseDTO.InnerException( - $"Error downloading extension: {ex.Message}"); + return HttpResponseDTO.Failure( + "Download service is not configured"); } + + // Delegate to ExtensionDownloadService which now contains the unified download implementation + return await _downloadService.Download(id, _availableExtensions); } /// From d57a802c2184450e753de438880ce7785a318189 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:10:51 +0000 Subject: [PATCH 7/9] Merge ExtensionDownloadService into ExtensionService, delete redundant file Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../Services/ExtensionServiceTests.cs | 60 ++-- .../Download/ExtensionDownloadService.cs | 286 ------------------ .../GeneralExtensionHost.cs | 14 +- .../Services/ExtensionService.cs | 262 +++++++++++++++- .../Services/IExtensionService.cs | 24 ++ 5 files changed, 308 insertions(+), 338 deletions(-) delete mode 100644 src/c#/GeneralUpdate.Extension/Download/ExtensionDownloadService.cs diff --git a/src/c#/ExtensionTest/Services/ExtensionServiceTests.cs b/src/c#/ExtensionTest/Services/ExtensionServiceTests.cs index a9a9537e..87073cb5 100644 --- a/src/c#/ExtensionTest/Services/ExtensionServiceTests.cs +++ b/src/c#/ExtensionTest/Services/ExtensionServiceTests.cs @@ -78,12 +78,18 @@ private List CreateTestExtensions() }; } + private ExtensionService CreateExtensionService(List extensions) + { + var updateQueue = new GeneralUpdate.Extension.Download.UpdateQueue(); + return new ExtensionService(extensions, "/tmp/test-downloads", updateQueue); + } + [Fact] public async Task Query_WithValidQuery_ShouldReturnPagedResults() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); var query = new ExtensionQueryDTO { PageNumber = 1, @@ -108,7 +114,7 @@ public async Task Query_WithPagination_ShouldReturnCorrectPage() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); var query = new ExtensionQueryDTO { PageNumber = 1, @@ -135,7 +141,7 @@ public async Task Query_WithNameFilter_ShouldReturnMatchingExtensions() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); var query = new ExtensionQueryDTO { PageNumber = 1, @@ -160,7 +166,7 @@ public async Task Query_WithPublisherFilter_ShouldReturnMatchingExtensions() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); var query = new ExtensionQueryDTO { PageNumber = 1, @@ -183,7 +189,7 @@ public async Task Query_WithCategoryFilter_ShouldReturnMatchingExtensions() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); var query = new ExtensionQueryDTO { PageNumber = 1, @@ -206,7 +212,7 @@ public async Task Query_WithPlatformFilter_ShouldReturnMatchingExtensions() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); var query = new ExtensionQueryDTO { PageNumber = 1, @@ -231,7 +237,7 @@ public async Task Query_ExcludePreRelease_ShouldNotReturnPreReleaseExtensions() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); var query = new ExtensionQueryDTO { PageNumber = 1, @@ -255,7 +261,7 @@ public async Task Query_WithSearchTerm_ShouldReturnMatchingExtensions() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); var query = new ExtensionQueryDTO { PageNumber = 1, @@ -280,7 +286,7 @@ public async Task Query_WithInvalidPageNumber_ShouldReturnFailure() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); var query = new ExtensionQueryDTO { PageNumber = 0, @@ -301,7 +307,7 @@ public async Task Query_WithInvalidPageSize_ShouldReturnFailure() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); var query = new ExtensionQueryDTO { PageNumber = 1, @@ -322,7 +328,7 @@ public async Task Query_WithNullQuery_ShouldReturnFailure() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); // Act var result = await service.Query(null!); @@ -338,7 +344,7 @@ public void UpdateAvailableExtensions_ShouldUpdateExtensionsList() { // Arrange var initialExtensions = CreateTestExtensions(); - var service = new ExtensionService(initialExtensions); + var service = CreateExtensionService(initialExtensions); var newExtensions = new List { new AvailableExtension @@ -362,28 +368,12 @@ public void UpdateAvailableExtensions_ShouldUpdateExtensionsList() Assert.Equal("new-extension", result.Body.Items.First().Name); } - [Fact] - public async Task Download_WithoutDownloadService_ShouldReturnFailure() - { - // Arrange - var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); - - // Act - var result = await service.Download("test-extension-1"); - - // Assert - Assert.NotNull(result); - Assert.Equal(400, result.Code); - Assert.Contains("not configured", result.Message); - } - [Fact] public async Task Download_WithInvalidId_ShouldReturnFailure() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); // Act var result = await service.Download("non-existent-extension"); @@ -391,8 +381,8 @@ public async Task Download_WithInvalidId_ShouldReturnFailure() // Assert Assert.NotNull(result); Assert.Equal(400, result.Code); - // Should fail because download service is not configured - Assert.Contains("not configured", result.Message); + // Should fail because extension is not found + Assert.Contains("not found", result.Message); } [Fact] @@ -400,7 +390,7 @@ public async Task Download_WithNullOrEmptyId_ShouldReturnFailure() { // Arrange var extensions = CreateTestExtensions(); - var service = new ExtensionService(extensions); + var service = CreateExtensionService(extensions); // Act var result1 = await service.Download(null!); @@ -409,9 +399,9 @@ public async Task Download_WithNullOrEmptyId_ShouldReturnFailure() // Assert Assert.Equal(400, result1.Code); Assert.Equal(400, result2.Code); - // Should fail because download service is not configured - Assert.Contains("not configured", result1.Message); - Assert.Contains("not configured", result2.Message); + // Should fail because ID is null or empty + Assert.Contains("null or empty", result1.Message); + Assert.Contains("null or empty", result2.Message); } } } diff --git a/src/c#/GeneralUpdate.Extension/Download/ExtensionDownloadService.cs b/src/c#/GeneralUpdate.Extension/Download/ExtensionDownloadService.cs deleted file mode 100644 index 954aa683..00000000 --- a/src/c#/GeneralUpdate.Extension/Download/ExtensionDownloadService.cs +++ /dev/null @@ -1,286 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using GeneralUpdate.Common.Download; -using GeneralUpdate.Common.Shared.Object; -using GeneralUpdate.Extension.DTOs; -using GeneralUpdate.Extension.Metadata; - -namespace GeneralUpdate.Extension.Download -{ - /// - /// Handles downloading of extension packages using the GeneralUpdate download infrastructure. - /// Provides progress tracking and error handling during download operations. - /// - public class ExtensionDownloadService - { - private readonly string _downloadPath; - private readonly int _downloadTimeout; - private readonly IUpdateQueue _updateQueue; - - /// - /// Occurs when download progress updates during package retrieval. - /// - public event EventHandler? ProgressUpdated; - - /// - /// Occurs when a download completes successfully. - /// - public event EventHandler? DownloadCompleted; - - /// - /// Occurs when a download fails due to an error. - /// - public event EventHandler? DownloadFailed; - - /// - /// Initializes a new instance of the class. - /// - /// Directory path where extension packages will be downloaded. - /// The update queue for managing operation state. - /// Timeout in seconds for download operations (default: 300). - /// Thrown when required parameters are null. - public ExtensionDownloadService(string downloadPath, IUpdateQueue updateQueue, int downloadTimeout = 300) - { - if (string.IsNullOrWhiteSpace(downloadPath)) - throw new ArgumentNullException(nameof(downloadPath)); - - _downloadPath = downloadPath; - _updateQueue = updateQueue ?? throw new ArgumentNullException(nameof(updateQueue)); - _downloadTimeout = downloadTimeout; - - if (!Directory.Exists(_downloadPath)) - { - Directory.CreateDirectory(_downloadPath); - } - } - - /// - /// Downloads an extension package asynchronously with progress tracking. - /// Updates the operation state in the queue throughout the download process. - /// - /// The update operation containing extension details. - /// The local file path of the downloaded package, or null if download failed. - /// Thrown when is null. - public async Task DownloadAsync(UpdateOperation operation) - { - if (operation == null) - throw new ArgumentNullException(nameof(operation)); - - var descriptor = operation.Extension.Descriptor; - - if (string.IsNullOrWhiteSpace(descriptor.DownloadUrl)) - { - _updateQueue.ChangeState(operation.OperationId, UpdateState.UpdateFailed, "Download URL is missing"); - OnDownloadFailed(descriptor.Name, descriptor.DisplayName); - return null; - } - - try - { - _updateQueue.ChangeState(operation.OperationId, UpdateState.Updating); - - // Determine file format from URL or default to .zip - var format = !string.IsNullOrWhiteSpace(descriptor.DownloadUrl) && descriptor.DownloadUrl!.Contains(".") - ? Path.GetExtension(descriptor.DownloadUrl) - : ".zip"; - - // Create version info for the download manager - var versionInfo = new VersionInfo - { - Name = $"{descriptor.Name}_{descriptor.Version}", - Url = descriptor.DownloadUrl, - Hash = descriptor.PackageHash, - Version = descriptor.Version, - Size = descriptor.PackageSize, - Format = format - }; - - // Initialize download manager with configured settings - var downloadManager = new DownloadManager(_downloadPath, format, _downloadTimeout); - - // Wire up event handlers for progress tracking - downloadManager.MultiDownloadStatistics += (sender, args) => OnDownloadProgress(operation, args); - downloadManager.MultiDownloadCompleted += (sender, args) => OnDownloadCompleted(operation, args); - downloadManager.MultiDownloadError += (sender, args) => OnDownloadError(operation, args); - - // Create and enqueue the download task - var downloadTask = new DownloadTask(downloadManager, versionInfo); - downloadManager.Add(downloadTask); - - // Execute the download - await downloadManager.LaunchTasksAsync(); - - var downloadedFilePath = Path.Combine(_downloadPath, $"{versionInfo.Name}{format}"); - - if (File.Exists(downloadedFilePath)) - { - OnDownloadSuccess(descriptor.Name, descriptor.DisplayName); - return downloadedFilePath; - } - else - { - _updateQueue.ChangeState(operation.OperationId, UpdateState.UpdateFailed, "Downloaded file not found"); - OnDownloadFailed(descriptor.Name, descriptor.DisplayName); - return null; - } - } - catch (Exception ex) - { - _updateQueue.ChangeState(operation.OperationId, UpdateState.UpdateFailed, ex.Message); - OnDownloadFailed(descriptor.Name, descriptor.DisplayName); - GeneralUpdate.Common.Shared.GeneralTracer.Error($"Download failed for extension {descriptor.Name}", ex); - return null; - } - } - - /// - /// Handles download statistics events and updates progress tracking. - /// - private void OnDownloadProgress(UpdateOperation operation, MultiDownloadStatisticsEventArgs args) - { - var progressPercentage = args.ProgressPercentage; - _updateQueue.UpdateProgress(operation.OperationId, progressPercentage); - - ProgressUpdated?.Invoke(this, new EventHandlers.DownloadProgressEventArgs - { - Name = operation.Extension.Descriptor.Name, - ExtensionName = operation.Extension.Descriptor.DisplayName, - ProgressPercentage = progressPercentage, - TotalBytes = args.TotalBytesToReceive, - ReceivedBytes = args.BytesReceived, - Speed = args.Speed, - RemainingTime = args.Remaining - }); - } - - /// - /// Handles download completion and validates the result. - /// - private void OnDownloadCompleted(UpdateOperation operation, MultiDownloadCompletedEventArgs args) - { - if (!args.IsComplated) - { - _updateQueue.ChangeState(operation.OperationId, UpdateState.UpdateFailed, "Download completed with errors"); - } - } - - /// - /// Handles download errors and updates the operation state. - /// - private void OnDownloadError(UpdateOperation operation, MultiDownloadErrorEventArgs args) - { - _updateQueue.ChangeState(operation.OperationId, UpdateState.UpdateFailed, args.Exception?.Message); - } - - /// - /// Raises the DownloadCompleted event when a download succeeds. - /// - private void OnDownloadSuccess(string extensionName, string displayName) - { - DownloadCompleted?.Invoke(this, new EventHandlers.ExtensionEventArgs - { - Name = extensionName, - ExtensionName = displayName - }); - } - - /// - /// Raises the DownloadFailed event when a download fails. - /// - private void OnDownloadFailed(string extensionName, string displayName) - { - DownloadFailed?.Invoke(this, new EventHandlers.ExtensionEventArgs - { - Name = extensionName, - ExtensionName = displayName - }); - } - - /// - /// Downloads an extension and its dependencies by ID. - /// This is the public API method that wraps the internal DownloadAsync method. - /// Note: The caller is responsible for disposing the Stream in the returned DownloadExtensionDTO. - /// - /// Extension ID (Name) - /// List of available extensions to search from - /// Download result containing file name and stream. The caller must dispose the stream. - public async Task> Download(string id, List availableExtensions) - { - try - { - if (string.IsNullOrWhiteSpace(id)) - { - return HttpResponseDTO.Failure("Extension ID cannot be null or empty"); - } - - if (availableExtensions == null || availableExtensions.Count == 0) - { - return HttpResponseDTO.Failure("Available extensions list is empty"); - } - - // Find the extension by ID (using Name as ID) - var extension = availableExtensions.FirstOrDefault(e => - e.Descriptor.Name?.Equals(id, StringComparison.OrdinalIgnoreCase) == true); - - if (extension == null) - { - return HttpResponseDTO.Failure( - $"Extension with ID '{id}' not found"); - } - - // Collect all extensions to download (main extension + dependencies) - var extensionsToDownload = new List { extension }; - - // Resolve dependencies - if (extension.Descriptor.Dependencies != null && extension.Descriptor.Dependencies.Count > 0) - { - foreach (var depId in extension.Descriptor.Dependencies) - { - var dependency = availableExtensions.FirstOrDefault(e => - e.Descriptor.Name?.Equals(depId, StringComparison.OrdinalIgnoreCase) == true); - - if (dependency != null) - { - extensionsToDownload.Add(dependency); - } - } - } - - // For now, we'll download only the main extension - // In a real implementation, you might want to download all dependencies - // and package them together or return multiple files - - // Use the shared update queue - var operation = _updateQueue.Enqueue(extension, false); - - var downloadedPath = await DownloadAsync(operation); - - if (downloadedPath == null || !File.Exists(downloadedPath)) - { - return HttpResponseDTO.Failure( - $"Failed to download extension '{extension.Descriptor.DisplayName}'"); - } - - // Read the file into a memory stream - var fileBytes = File.ReadAllBytes(downloadedPath); - var stream = new MemoryStream(fileBytes); - - var result = new DownloadExtensionDTO - { - FileName = Path.GetFileName(downloadedPath), - Stream = stream - }; - - return HttpResponseDTO.Success(result); - } - catch (Exception ex) - { - return HttpResponseDTO.InnerException( - $"Error downloading extension: {ex.Message}"); - } - } - } -} diff --git a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs index e50f047a..e5b1e7fe 100644 --- a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs +++ b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs @@ -16,7 +16,6 @@ public class GeneralExtensionHost : IExtensionHost private readonly Core.IExtensionCatalog _catalog; private readonly Compatibility.ICompatibilityValidator _validator; private readonly Download.IUpdateQueue _updateQueue; - private readonly Download.ExtensionDownloadService _downloadService; private readonly Installation.ExtensionInstallService _installService; private readonly Services.IExtensionService _extensionService; private bool _globalAutoUpdateEnabled = true; @@ -112,21 +111,22 @@ public GeneralExtensionHost( _catalog = new Core.ExtensionCatalog(installBasePath); _validator = new Compatibility.CompatibilityValidator(hostVersion); _updateQueue = new Download.UpdateQueue(); - _downloadService = new Download.ExtensionDownloadService(downloadPath, _updateQueue, downloadTimeout); _installService = new Installation.ExtensionInstallService(installBasePath); // Initialize extension service with empty list (will be updated via ParseAvailableExtensions) _extensionService = new Services.ExtensionService( new List(), + downloadPath, + _updateQueue, hostVersion, _validator, - _downloadService); + downloadTimeout); // Wire up event handlers _updateQueue.StateChanged += (sender, args) => UpdateStateChanged?.Invoke(sender, args); - _downloadService.ProgressUpdated += (sender, args) => DownloadProgress?.Invoke(sender, args); - _downloadService.DownloadCompleted += (sender, args) => DownloadCompleted?.Invoke(sender, args); - _downloadService.DownloadFailed += (sender, args) => DownloadFailed?.Invoke(sender, args); + _extensionService.ProgressUpdated += (sender, args) => DownloadProgress?.Invoke(sender, args); + _extensionService.DownloadCompleted += (sender, args) => DownloadCompleted?.Invoke(sender, args); + _extensionService.DownloadFailed += (sender, args) => DownloadFailed?.Invoke(sender, args); _installService.InstallationCompleted += (sender, args) => InstallationCompleted?.Invoke(sender, args); _installService.RollbackCompleted += (sender, args) => RollbackCompleted?.Invoke(sender, args); } @@ -340,7 +340,7 @@ public async Task ProcessNextUpdateAsync() try { // Download the extension package - var downloadedPath = await _downloadService.DownloadAsync(operation); + var downloadedPath = await _extensionService.DownloadAsync(operation); if (downloadedPath == null) return false; diff --git a/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs index df6ed9e8..73285ecd 100644 --- a/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs +++ b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs @@ -3,38 +3,74 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using GeneralUpdate.Common.Download; +using GeneralUpdate.Common.Shared.Object; using GeneralUpdate.Extension.DTOs; using GeneralUpdate.Extension.Metadata; namespace GeneralUpdate.Extension.Services { /// - /// Implementation of extension query and download operations + /// Implementation of extension query and download operations. + /// Handles downloading of extension packages using the GeneralUpdate download infrastructure. + /// Provides progress tracking and error handling during download operations. /// public class ExtensionService : IExtensionService { private List _availableExtensions; private readonly Version? _hostVersion; private readonly Compatibility.ICompatibilityValidator? _validator; - private readonly Download.ExtensionDownloadService? _downloadService; + private readonly string _downloadPath; + private readonly int _downloadTimeout; + private readonly Download.IUpdateQueue _updateQueue; + + /// + /// Occurs when download progress updates during package retrieval. + /// + public event EventHandler? ProgressUpdated; + + /// + /// Occurs when a download completes successfully. + /// + public event EventHandler? DownloadCompleted; + + /// + /// Occurs when a download fails due to an error. + /// + public event EventHandler? DownloadFailed; /// /// Initializes a new instance of the class. /// /// List of available extensions + /// Directory path where extension packages will be downloaded + /// The update queue for managing operation state /// Optional host version for compatibility checking /// Optional compatibility validator - /// Optional download service + /// Timeout in seconds for download operations (default: 300) public ExtensionService( List availableExtensions, + string downloadPath, + Download.IUpdateQueue updateQueue, Version? hostVersion = null, Compatibility.ICompatibilityValidator? validator = null, - Download.ExtensionDownloadService? downloadService = null) + int downloadTimeout = 300) { _availableExtensions = availableExtensions ?? throw new ArgumentNullException(nameof(availableExtensions)); + + if (string.IsNullOrWhiteSpace(downloadPath)) + throw new ArgumentNullException(nameof(downloadPath)); + + _downloadPath = downloadPath; + _updateQueue = updateQueue ?? throw new ArgumentNullException(nameof(updateQueue)); + _downloadTimeout = downloadTimeout; _hostVersion = hostVersion; _validator = validator; - _downloadService = downloadService; + + if (!Directory.Exists(_downloadPath)) + { + Directory.CreateDirectory(_downloadPath); + } } /// @@ -170,14 +206,220 @@ public Task>> Query(ExtensionQueryD /// Download result containing file name and stream. The caller must dispose the stream. public async Task> Download(string id) { - if (_downloadService == null) + try { - return HttpResponseDTO.Failure( - "Download service is not configured"); + if (string.IsNullOrWhiteSpace(id)) + { + return HttpResponseDTO.Failure("Extension ID cannot be null or empty"); + } + + if (_availableExtensions == null || _availableExtensions.Count == 0) + { + return HttpResponseDTO.Failure("Available extensions list is empty"); + } + + // Find the extension by ID (using Name as ID) + var extension = _availableExtensions.FirstOrDefault(e => + e.Descriptor.Name?.Equals(id, StringComparison.OrdinalIgnoreCase) == true); + + if (extension == null) + { + return HttpResponseDTO.Failure( + $"Extension with ID '{id}' not found"); + } + + // Collect all extensions to download (main extension + dependencies) + var extensionsToDownload = new List { extension }; + + // Resolve dependencies + if (extension.Descriptor.Dependencies != null && extension.Descriptor.Dependencies.Count > 0) + { + foreach (var depId in extension.Descriptor.Dependencies) + { + var dependency = _availableExtensions.FirstOrDefault(e => + e.Descriptor.Name?.Equals(depId, StringComparison.OrdinalIgnoreCase) == true); + + if (dependency != null) + { + extensionsToDownload.Add(dependency); + } + } + } + + // For now, we'll download only the main extension + // In a real implementation, you might want to download all dependencies + // and package them together or return multiple files + + // Use the shared update queue + var operation = _updateQueue.Enqueue(extension, false); + + var downloadedPath = await DownloadAsync(operation); + + if (downloadedPath == null || !File.Exists(downloadedPath)) + { + return HttpResponseDTO.Failure( + $"Failed to download extension '{extension.Descriptor.DisplayName}'"); + } + + // Read the file into a memory stream + var fileBytes = File.ReadAllBytes(downloadedPath); + var stream = new MemoryStream(fileBytes); + + var result = new DownloadExtensionDTO + { + FileName = Path.GetFileName(downloadedPath), + Stream = stream + }; + + return HttpResponseDTO.Success(result); + } + catch (Exception ex) + { + return HttpResponseDTO.InnerException( + $"Error downloading extension: {ex.Message}"); + } + } + + /// + /// Downloads an extension package asynchronously with progress tracking. + /// Updates the operation state in the queue throughout the download process. + /// + /// The update operation containing extension details. + /// The local file path of the downloaded package, or null if download failed. + /// Thrown when is null. + public async Task DownloadAsync(Download.UpdateOperation operation) + { + if (operation == null) + throw new ArgumentNullException(nameof(operation)); + + var descriptor = operation.Extension.Descriptor; + + if (string.IsNullOrWhiteSpace(descriptor.DownloadUrl)) + { + _updateQueue.ChangeState(operation.OperationId, GeneralUpdate.Extension.Download.UpdateState.UpdateFailed, "Download URL is missing"); + OnDownloadFailed(descriptor.Name, descriptor.DisplayName); + return null; } - // Delegate to ExtensionDownloadService which now contains the unified download implementation - return await _downloadService.Download(id, _availableExtensions); + try + { + _updateQueue.ChangeState(operation.OperationId, GeneralUpdate.Extension.Download.UpdateState.Updating); + + // Determine file format from URL or default to .zip + var format = !string.IsNullOrWhiteSpace(descriptor.DownloadUrl) && descriptor.DownloadUrl!.Contains(".") + ? Path.GetExtension(descriptor.DownloadUrl) + : ".zip"; + + // Create version info for the download manager + var versionInfo = new VersionInfo + { + Name = $"{descriptor.Name}_{descriptor.Version}", + Url = descriptor.DownloadUrl, + Hash = descriptor.PackageHash, + Version = descriptor.Version, + Size = descriptor.PackageSize, + Format = format + }; + + // Initialize download manager with configured settings + var downloadManager = new DownloadManager(_downloadPath, format, _downloadTimeout); + + // Wire up event handlers for progress tracking + downloadManager.MultiDownloadStatistics += (sender, args) => OnDownloadProgress(operation, args); + downloadManager.MultiDownloadCompleted += (sender, args) => OnDownloadCompleted(operation, args); + downloadManager.MultiDownloadError += (sender, args) => OnDownloadError(operation, args); + + // Create and enqueue the download task + var downloadTask = new DownloadTask(downloadManager, versionInfo); + downloadManager.Add(downloadTask); + + // Execute the download + await downloadManager.LaunchTasksAsync(); + + var downloadedFilePath = Path.Combine(_downloadPath, $"{versionInfo.Name}{format}"); + + if (File.Exists(downloadedFilePath)) + { + OnDownloadSuccess(descriptor.Name, descriptor.DisplayName); + return downloadedFilePath; + } + else + { + _updateQueue.ChangeState(operation.OperationId, GeneralUpdate.Extension.Download.UpdateState.UpdateFailed, "Downloaded file not found"); + OnDownloadFailed(descriptor.Name, descriptor.DisplayName); + return null; + } + } + catch (Exception ex) + { + _updateQueue.ChangeState(operation.OperationId, GeneralUpdate.Extension.Download.UpdateState.UpdateFailed, ex.Message); + OnDownloadFailed(descriptor.Name, descriptor.DisplayName); + GeneralUpdate.Common.Shared.GeneralTracer.Error($"Download failed for extension {descriptor.Name}", ex); + return null; + } + } + + /// + /// Handles download statistics events and updates progress tracking. + /// + private void OnDownloadProgress(Download.UpdateOperation operation, MultiDownloadStatisticsEventArgs args) + { + var progressPercentage = args.ProgressPercentage; + _updateQueue.UpdateProgress(operation.OperationId, progressPercentage); + + ProgressUpdated?.Invoke(this, new EventHandlers.DownloadProgressEventArgs + { + Name = operation.Extension.Descriptor.Name, + ExtensionName = operation.Extension.Descriptor.DisplayName, + ProgressPercentage = progressPercentage, + TotalBytes = args.TotalBytesToReceive, + ReceivedBytes = args.BytesReceived, + Speed = args.Speed, + RemainingTime = args.Remaining + }); + } + + /// + /// Handles download completion and validates the result. + /// + private void OnDownloadCompleted(Download.UpdateOperation operation, MultiDownloadCompletedEventArgs args) + { + if (!args.IsComplated) + { + _updateQueue.ChangeState(operation.OperationId, GeneralUpdate.Extension.Download.UpdateState.UpdateFailed, "Download completed with errors"); + } + } + + /// + /// Handles download errors and updates the operation state. + /// + private void OnDownloadError(Download.UpdateOperation operation, MultiDownloadErrorEventArgs args) + { + _updateQueue.ChangeState(operation.OperationId, GeneralUpdate.Extension.Download.UpdateState.UpdateFailed, args.Exception?.Message); + } + + /// + /// Raises the DownloadCompleted event when a download succeeds. + /// + private void OnDownloadSuccess(string extensionName, string displayName) + { + DownloadCompleted?.Invoke(this, new EventHandlers.ExtensionEventArgs + { + Name = extensionName, + ExtensionName = displayName + }); + } + + /// + /// Raises the DownloadFailed event when a download fails. + /// + private void OnDownloadFailed(string extensionName, string displayName) + { + DownloadFailed?.Invoke(this, new EventHandlers.ExtensionEventArgs + { + Name = extensionName, + ExtensionName = displayName + }); } /// diff --git a/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs b/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs index 82f8c611..c78bf2ed 100644 --- a/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs +++ b/src/c#/GeneralUpdate.Extension/Services/IExtensionService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; using GeneralUpdate.Extension.DTOs; @@ -9,6 +10,21 @@ namespace GeneralUpdate.Extension.Services /// public interface IExtensionService { + /// + /// Occurs when download progress updates during package retrieval. + /// + event EventHandler? ProgressUpdated; + + /// + /// Occurs when a download completes successfully. + /// + event EventHandler? DownloadCompleted; + + /// + /// Occurs when a download fails due to an error. + /// + event EventHandler? DownloadFailed; + /// /// Updates the list of available extensions /// @@ -29,5 +45,13 @@ public interface IExtensionService /// Extension ID /// Download result containing file name and stream. The caller must dispose the stream. Task> Download(string id); + + /// + /// Downloads an extension package asynchronously with progress tracking. + /// Updates the operation state in the queue throughout the download process. + /// + /// The update operation containing extension details. + /// The local file path of the downloaded package, or null if download failed. + Task DownloadAsync(Download.UpdateOperation operation); } } From a447811e439c923076530fef287c5dc8d898b069 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:50:58 +0000 Subject: [PATCH 8/9] Add HTTP authentication support to GeneralExtensionHost and download pipeline Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../GeneralUpdate.Common/Download/DownloadTask.cs | 8 ++++++++ .../Shared/Object/VersionInfo.cs | 12 ++++++++++++ .../GeneralExtensionHost.cs | 10 ++++++++-- .../Services/ExtensionService.cs | 14 ++++++++++++-- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/c#/GeneralUpdate.Common/Download/DownloadTask.cs b/src/c#/GeneralUpdate.Common/Download/DownloadTask.cs index bd50a7e2..40de3d61 100644 --- a/src/c#/GeneralUpdate.Common/Download/DownloadTask.cs +++ b/src/c#/GeneralUpdate.Common/Download/DownloadTask.cs @@ -30,6 +30,14 @@ public DownloadTask(DownloadManager manager, VersionInfo version) _manager = manager; _version = version; _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(_manager.TimeOut) }; + + // Set authentication headers if provided + if (!string.IsNullOrEmpty(version?.AuthScheme) && !string.IsNullOrEmpty(version?.AuthToken)) + { + _httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue(version.AuthScheme, version.AuthToken); + } + _timer = new Timer(_=> Statistics(), null, 0, 1000); } diff --git a/src/c#/GeneralUpdate.Common/Shared/Object/VersionInfo.cs b/src/c#/GeneralUpdate.Common/Shared/Object/VersionInfo.cs index 47b8659e..27741084 100644 --- a/src/c#/GeneralUpdate.Common/Shared/Object/VersionInfo.cs +++ b/src/c#/GeneralUpdate.Common/Shared/Object/VersionInfo.cs @@ -40,4 +40,16 @@ public class VersionInfo [JsonPropertyName("size")] public long? Size { get; set; } + + /// + /// HTTP authentication scheme (e.g., "Bearer", "Basic") for download requests. + /// + [JsonPropertyName("authScheme")] + public string? AuthScheme { get; set; } + + /// + /// HTTP authentication token for download requests. + /// + [JsonPropertyName("authToken")] + public string? AuthToken { get; set; } } \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs index e5b1e7fe..3f80d0f9 100644 --- a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs +++ b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs @@ -91,13 +91,17 @@ public bool GlobalAutoUpdateEnabled /// Directory for downloading extension packages. /// The current platform (Windows/Linux/macOS). /// Download timeout in seconds (default: 300). + /// Optional HTTP authentication scheme (e.g., "Bearer", "Basic"). + /// Optional HTTP authentication token. /// Thrown when required parameters are null. public GeneralExtensionHost( Version hostVersion, string installBasePath, string downloadPath, Metadata.TargetPlatform targetPlatform = Metadata.TargetPlatform.Windows, - int downloadTimeout = 300) + int downloadTimeout = 300, + string? authScheme = null, + string? authToken = null) { _hostVersion = hostVersion ?? throw new ArgumentNullException(nameof(hostVersion)); if (string.IsNullOrWhiteSpace(installBasePath)) @@ -120,7 +124,9 @@ public GeneralExtensionHost( _updateQueue, hostVersion, _validator, - downloadTimeout); + downloadTimeout, + authScheme, + authToken); // Wire up event handlers _updateQueue.StateChanged += (sender, args) => UpdateStateChanged?.Invoke(sender, args); diff --git a/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs index 73285ecd..21466afc 100644 --- a/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs +++ b/src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs @@ -23,6 +23,8 @@ public class ExtensionService : IExtensionService private readonly string _downloadPath; private readonly int _downloadTimeout; private readonly Download.IUpdateQueue _updateQueue; + private readonly string? _authScheme; + private readonly string? _authToken; /// /// Occurs when download progress updates during package retrieval. @@ -48,13 +50,17 @@ public class ExtensionService : IExtensionService /// Optional host version for compatibility checking /// Optional compatibility validator /// Timeout in seconds for download operations (default: 300) + /// Optional HTTP authentication scheme (e.g., "Bearer", "Basic") + /// Optional HTTP authentication token public ExtensionService( List availableExtensions, string downloadPath, Download.IUpdateQueue updateQueue, Version? hostVersion = null, Compatibility.ICompatibilityValidator? validator = null, - int downloadTimeout = 300) + int downloadTimeout = 300, + string? authScheme = null, + string? authToken = null) { _availableExtensions = availableExtensions ?? throw new ArgumentNullException(nameof(availableExtensions)); @@ -66,6 +72,8 @@ public ExtensionService( _downloadTimeout = downloadTimeout; _hostVersion = hostVersion; _validator = validator; + _authScheme = authScheme; + _authToken = authToken; if (!Directory.Exists(_downloadPath)) { @@ -318,7 +326,9 @@ public async Task> Download(string id) Hash = descriptor.PackageHash, Version = descriptor.Version, Size = descriptor.PackageSize, - Format = format + Format = format, + AuthScheme = _authScheme, + AuthToken = _authToken }; // Initialize download manager with configured settings From baacff2d5a84e679b1983a929dfc13e8347972c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:56:57 +0000 Subject: [PATCH 9/9] Refactor GeneralExtensionHost to use ExtensionHostConfig for cleaner API Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../Examples/ExtensionSystemExample.cs | 19 +++-- .../ExtensionHostConfig.cs | 67 +++++++++++++++ .../GeneralExtensionHost.cs | 82 ++++++++++++------- 3 files changed, 129 insertions(+), 39 deletions(-) create mode 100644 src/c#/GeneralUpdate.Extension/ExtensionHostConfig.cs diff --git a/src/c#/GeneralUpdate.Extension/Examples/ExtensionSystemExample.cs b/src/c#/GeneralUpdate.Extension/Examples/ExtensionSystemExample.cs index af4c125d..25562db7 100644 --- a/src/c#/GeneralUpdate.Extension/Examples/ExtensionSystemExample.cs +++ b/src/c#/GeneralUpdate.Extension/Examples/ExtensionSystemExample.cs @@ -25,14 +25,17 @@ public void Initialize() // Detect current platform var currentPlatform = DetectCurrentPlatform(); - // Create the extension host - _host = new GeneralExtensionHost( - hostVersion, - installPath, - downloadPath, - currentPlatform, - downloadTimeout: 300 // 5 minutes - ); + // Create the extension host using the new config-based approach + var config = new ExtensionHostConfig + { + HostVersion = hostVersion, + InstallBasePath = installPath, + DownloadPath = downloadPath, + TargetPlatform = currentPlatform, + DownloadTimeout = 300 // 5 minutes + }; + + _host = new GeneralExtensionHost(config); // Subscribe to events for monitoring SubscribeToEvents(); diff --git a/src/c#/GeneralUpdate.Extension/ExtensionHostConfig.cs b/src/c#/GeneralUpdate.Extension/ExtensionHostConfig.cs new file mode 100644 index 00000000..6c4bd0e1 --- /dev/null +++ b/src/c#/GeneralUpdate.Extension/ExtensionHostConfig.cs @@ -0,0 +1,67 @@ +using System; + +namespace GeneralUpdate.Extension +{ + /// + /// Configuration settings for initializing a GeneralExtensionHost instance. + /// Encapsulates all parameters required to set up the extension host environment. + /// + public class ExtensionHostConfig + { + /// + /// Gets or sets the current host application version. + /// This is required and used for compatibility checking. + /// + public Version HostVersion { get; set; } = null!; + + /// + /// Gets or sets the base directory for extension installations. + /// This is the root directory where extensions will be installed. + /// + public string InstallBasePath { get; set; } = null!; + + /// + /// Gets or sets the directory for downloading extension packages. + /// Downloaded packages are temporarily stored here before installation. + /// + public string DownloadPath { get; set; } = null!; + + /// + /// Gets or sets the target platform (Windows/Linux/macOS). + /// Defaults to Windows if not specified. + /// + public Metadata.TargetPlatform TargetPlatform { get; set; } = Metadata.TargetPlatform.Windows; + + /// + /// Gets or sets the download timeout in seconds. + /// Defaults to 300 seconds (5 minutes) if not specified. + /// + public int DownloadTimeout { get; set; } = 300; + + /// + /// Gets or sets the optional HTTP authentication scheme (e.g., "Bearer", "Basic"). + /// When set along with AuthToken, enables authenticated downloads. + /// + public string? AuthScheme { get; set; } + + /// + /// Gets or sets the optional HTTP authentication token. + /// When set along with AuthScheme, enables authenticated downloads. + /// + public string? AuthToken { get; set; } + + /// + /// Validates that all required properties are set. + /// + /// Thrown when required properties are null or empty. + public void Validate() + { + if (HostVersion == null) + throw new ArgumentNullException(nameof(HostVersion)); + if (string.IsNullOrWhiteSpace(InstallBasePath)) + throw new ArgumentNullException(nameof(InstallBasePath)); + if (string.IsNullOrWhiteSpace(DownloadPath)) + throw new ArgumentNullException(nameof(DownloadPath)); + } + } +} diff --git a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs index 3f80d0f9..557930ec 100644 --- a/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs +++ b/src/c#/GeneralUpdate.Extension/GeneralExtensionHost.cs @@ -84,49 +84,36 @@ public bool GlobalAutoUpdateEnabled #endregion /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class using a configuration object. /// - /// The current host application version. - /// Base directory for extension installations. - /// Directory for downloading extension packages. - /// The current platform (Windows/Linux/macOS). - /// Download timeout in seconds (default: 300). - /// Optional HTTP authentication scheme (e.g., "Bearer", "Basic"). - /// Optional HTTP authentication token. - /// Thrown when required parameters are null. - public GeneralExtensionHost( - Version hostVersion, - string installBasePath, - string downloadPath, - Metadata.TargetPlatform targetPlatform = Metadata.TargetPlatform.Windows, - int downloadTimeout = 300, - string? authScheme = null, - string? authToken = null) + /// Configuration settings for the extension host. + /// Thrown when config is null or required properties are missing. + public GeneralExtensionHost(ExtensionHostConfig config) { - _hostVersion = hostVersion ?? throw new ArgumentNullException(nameof(hostVersion)); - if (string.IsNullOrWhiteSpace(installBasePath)) - throw new ArgumentNullException(nameof(installBasePath)); - if (string.IsNullOrWhiteSpace(downloadPath)) - throw new ArgumentNullException(nameof(downloadPath)); + if (config == null) + throw new ArgumentNullException(nameof(config)); + + config.Validate(); - _targetPlatform = targetPlatform; + _hostVersion = config.HostVersion; + _targetPlatform = config.TargetPlatform; // Initialize core services - _catalog = new Core.ExtensionCatalog(installBasePath); - _validator = new Compatibility.CompatibilityValidator(hostVersion); + _catalog = new Core.ExtensionCatalog(config.InstallBasePath); + _validator = new Compatibility.CompatibilityValidator(config.HostVersion); _updateQueue = new Download.UpdateQueue(); - _installService = new Installation.ExtensionInstallService(installBasePath); + _installService = new Installation.ExtensionInstallService(config.InstallBasePath); // Initialize extension service with empty list (will be updated via ParseAvailableExtensions) _extensionService = new Services.ExtensionService( new List(), - downloadPath, + config.DownloadPath, _updateQueue, - hostVersion, + config.HostVersion, _validator, - downloadTimeout, - authScheme, - authToken); + config.DownloadTimeout, + config.AuthScheme, + config.AuthToken); // Wire up event handlers _updateQueue.StateChanged += (sender, args) => UpdateStateChanged?.Invoke(sender, args); @@ -137,6 +124,39 @@ public GeneralExtensionHost( _installService.RollbackCompleted += (sender, args) => RollbackCompleted?.Invoke(sender, args); } + /// + /// Initializes a new instance of the class. + /// + /// The current host application version. + /// Base directory for extension installations. + /// Directory for downloading extension packages. + /// The current platform (Windows/Linux/macOS). + /// Download timeout in seconds (default: 300). + /// Optional HTTP authentication scheme (e.g., "Bearer", "Basic"). + /// Optional HTTP authentication token. + /// Thrown when required parameters are null. + [Obsolete("Use the constructor that accepts ExtensionHostConfig for better maintainability.")] + public GeneralExtensionHost( + Version hostVersion, + string installBasePath, + string downloadPath, + Metadata.TargetPlatform targetPlatform = Metadata.TargetPlatform.Windows, + int downloadTimeout = 300, + string? authScheme = null, + string? authToken = null) + : this(new ExtensionHostConfig + { + HostVersion = hostVersion, + InstallBasePath = installBasePath, + DownloadPath = downloadPath, + TargetPlatform = targetPlatform, + DownloadTimeout = downloadTimeout, + AuthScheme = authScheme, + AuthToken = authToken + }) + { + } + #region Extension Catalog ///