Skip to content

Commit 63cb95f

Browse files
CopilotJusterZhu
andcommitted
Implement HTTP GET requests for Query and Download methods with authentication support
Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com>
1 parent 9dc11ca commit 63cb95f

1 file changed

Lines changed: 173 additions & 122 deletions

File tree

src/c#/GeneralUpdate.Extension/Services/ExtensionService.cs

Lines changed: 173 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5+
using System.Net.Http;
6+
using System.Net.Http.Headers;
7+
using System.Text;
8+
using System.Text.Json;
59
using System.Threading.Tasks;
610
using GeneralUpdate.Common.Download;
711
using GeneralUpdate.Common.Shared.Object;
@@ -26,6 +30,7 @@ public class ExtensionService : IExtensionService
2630
private readonly string? _authScheme;
2731
private readonly string? _authToken;
2832
private readonly string _serverUrl;
33+
private readonly HttpClient _httpClient;
2934

3035
/// <summary>
3136
/// Occurs when download progress updates during package retrieval.
@@ -83,6 +88,19 @@ public ExtensionService(
8388
_authScheme = authScheme;
8489
_authToken = authToken;
8590

91+
// Initialize HttpClient with timeout
92+
_httpClient = new HttpClient
93+
{
94+
Timeout = TimeSpan.FromSeconds(_downloadTimeout)
95+
};
96+
97+
// Set authentication headers if provided
98+
if (!string.IsNullOrWhiteSpace(_authScheme) && !string.IsNullOrWhiteSpace(_authToken))
99+
{
100+
_httpClient.DefaultRequestHeaders.Authorization =
101+
new AuthenticationHeaderValue(_authScheme, _authToken);
102+
}
103+
86104
if (!Directory.Exists(_downloadPath))
87105
{
88106
Directory.CreateDirectory(_downloadPath);
@@ -99,123 +117,124 @@ public void UpdateAvailableExtensions(List<AvailableExtension> availableExtensio
99117
}
100118

101119
/// <summary>
102-
/// Queries available extensions based on filter criteria
120+
/// Queries available extensions based on filter criteria via HTTP request to the server
103121
/// </summary>
104122
/// <param name="query">Query parameters including pagination and filters</param>
105123
/// <returns>Paginated result of extensions matching the query</returns>
106-
public Task<HttpResponseDTO<PagedResultDTO<ExtensionDTO>>> Query(ExtensionQueryDTO query)
124+
public async Task<HttpResponseDTO<PagedResultDTO<ExtensionDTO>>> Query(ExtensionQueryDTO query)
107125
{
108126
try
109127
{
110128
if (query == null)
111129
{
112-
return Task.FromResult(HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.Failure(
113-
"Query parameter cannot be null"));
130+
return HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.Failure(
131+
"Query parameter cannot be null");
114132
}
115133

116134
// Validate pagination parameters
117135
if (query.PageNumber < 1)
118136
{
119-
return Task.FromResult(HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.Failure(
120-
"PageNumber must be greater than 0"));
137+
return HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.Failure(
138+
"PageNumber must be greater than 0");
121139
}
122140

123141
if (query.PageSize < 1)
124142
{
125-
return Task.FromResult(HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.Failure(
126-
"PageSize must be greater than 0"));
127-
}
128-
129-
// Parse host version if provided
130-
Version? queryHostVersion = null;
131-
if (!string.IsNullOrWhiteSpace(query.HostVersion))
132-
{
133-
if (!Version.TryParse(query.HostVersion, out queryHostVersion))
134-
{
135-
return Task.FromResult(HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.Failure(
136-
$"Invalid host version format: {query.HostVersion}"));
137-
}
143+
return HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.Failure(
144+
"PageSize must be greater than 0");
138145
}
139146

140-
// Use query host version if provided, otherwise use service host version
141-
var effectiveHostVersion = queryHostVersion ?? _hostVersion;
142-
143-
// Start with all available extensions
144-
IEnumerable<AvailableExtension> filtered = _availableExtensions;
145-
146-
// Apply filters
147+
// Build query string from parameters
148+
var queryParams = new List<string>();
149+
queryParams.Add($"PageNumber={query.PageNumber}");
150+
queryParams.Add($"PageSize={query.PageSize}");
151+
147152
if (!string.IsNullOrWhiteSpace(query.Name))
148-
{
149-
filtered = filtered.Where(e =>
150-
e.Descriptor.Name?.IndexOf(query.Name, StringComparison.OrdinalIgnoreCase) >= 0);
151-
}
152-
153+
queryParams.Add($"Name={Uri.EscapeDataString(query.Name)}");
154+
153155
if (!string.IsNullOrWhiteSpace(query.Publisher))
154-
{
155-
filtered = filtered.Where(e =>
156-
e.Descriptor.Publisher?.IndexOf(query.Publisher, StringComparison.OrdinalIgnoreCase) >= 0);
157-
}
158-
156+
queryParams.Add($"Publisher={Uri.EscapeDataString(query.Publisher)}");
157+
159158
if (!string.IsNullOrWhiteSpace(query.Category))
160-
{
161-
filtered = filtered.Where(e =>
162-
e.Descriptor.Categories?.Any(c =>
163-
c.IndexOf(query.Category, StringComparison.OrdinalIgnoreCase) >= 0) == true);
164-
}
159+
queryParams.Add($"Category={Uri.EscapeDataString(query.Category)}");
160+
161+
if (query.TargetPlatform.HasValue)
162+
queryParams.Add($"TargetPlatform={(int)query.TargetPlatform.Value}");
163+
164+
if (!string.IsNullOrWhiteSpace(query.HostVersion))
165+
queryParams.Add($"HostVersion={Uri.EscapeDataString(query.HostVersion)}");
166+
167+
queryParams.Add($"IncludePreRelease={query.IncludePreRelease}");
168+
169+
if (!string.IsNullOrWhiteSpace(query.SearchTerm))
170+
queryParams.Add($"SearchTerm={Uri.EscapeDataString(query.SearchTerm)}");
165171

166-
if (query.TargetPlatform.HasValue && query.TargetPlatform.Value != TargetPlatform.None)
167-
{
168-
filtered = filtered.Where(e =>
169-
(e.Descriptor.SupportedPlatforms & query.TargetPlatform.Value) != 0);
170-
}
172+
var queryString = string.Join("&", queryParams);
173+
var requestUrl = $"{_serverUrl}/Query?{queryString}";
171174

172-
if (!query.IncludePreRelease)
175+
// Make HTTP GET request
176+
var response = await _httpClient.GetAsync(requestUrl);
177+
178+
if (!response.IsSuccessStatusCode)
173179
{
174-
filtered = filtered.Where(e => !e.IsPreRelease);
180+
var errorContent = await response.Content.ReadAsStringAsync();
181+
return HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.Failure(
182+
$"Server returned error {response.StatusCode}: {errorContent}");
175183
}
176184

177-
if (!string.IsNullOrWhiteSpace(query.SearchTerm))
185+
var jsonContent = await response.Content.ReadAsStringAsync();
186+
var result = JsonSerializer.Deserialize<PagedResultDTO<ExtensionDTO>>(jsonContent,
187+
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
188+
189+
if (result == null)
178190
{
179-
filtered = filtered.Where(e =>
180-
(e.Descriptor.Name?.IndexOf(query.SearchTerm, StringComparison.OrdinalIgnoreCase) >= 0) ||
181-
(e.Descriptor.DisplayName?.IndexOf(query.SearchTerm, StringComparison.OrdinalIgnoreCase) >= 0) ||
182-
(e.Descriptor.Description?.IndexOf(query.SearchTerm, StringComparison.OrdinalIgnoreCase) >= 0));
191+
return HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.Failure(
192+
"Failed to deserialize server response");
183193
}
184194

185-
// Convert to list for pagination
186-
var filteredList = filtered.ToList();
187-
188-
// Calculate pagination
189-
var totalCount = filteredList.Count;
190-
var totalPages = (int)Math.Ceiling(totalCount / (double)query.PageSize);
191-
192-
// Apply pagination
193-
var items = filteredList
194-
.Skip((query.PageNumber - 1) * query.PageSize)
195-
.Take(query.PageSize)
196-
.Select(e => MapToExtensionDTO(e, effectiveHostVersion))
197-
.ToList();
198-
199-
var result = new PagedResultDTO<ExtensionDTO>
195+
// Update local cache with results
196+
if (result.Items != null && result.Items.Any())
200197
{
201-
PageNumber = query.PageNumber,
202-
PageSize = query.PageSize,
203-
TotalCount = totalCount,
204-
TotalPages = totalPages,
205-
Items = items
206-
};
198+
var availableExtensions = result.Items
199+
.Select(dto => MapFromExtensionDTO(dto))
200+
.Where(ext => ext != null)
201+
.Cast<AvailableExtension>()
202+
.ToList();
203+
204+
// Merge with existing extensions
205+
foreach (var ext in availableExtensions)
206+
{
207+
var existing = _availableExtensions.FirstOrDefault(e =>
208+
e.Descriptor.Name?.Equals(ext.Descriptor.Name, StringComparison.OrdinalIgnoreCase) == true);
209+
210+
if (existing == null)
211+
{
212+
_availableExtensions.Add(ext);
213+
}
214+
}
215+
}
207216

208-
return Task.FromResult(HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.Success(result));
217+
return HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.Success(result);
218+
}
219+
catch (HttpRequestException ex)
220+
{
221+
return HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.InnerException(
222+
$"HTTP request error: {ex.Message}");
223+
}
224+
catch (TaskCanceledException ex)
225+
{
226+
return HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.InnerException(
227+
$"Request timeout: {ex.Message}");
209228
}
210229
catch (Exception ex)
211230
{
212-
return Task.FromResult(HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.InnerException(
213-
$"Error querying extensions: {ex.Message}"));
231+
return HttpResponseDTO<PagedResultDTO<ExtensionDTO>>.InnerException(
232+
$"Error querying extensions: {ex.Message}");
214233
}
215234
}
216235

217236
/// <summary>
218-
/// Downloads an extension and its dependencies by ID.
237+
/// Downloads an extension package by ID via HTTP GET request to the server.
219238
/// Note: The caller is responsible for disposing the Stream in the returned DownloadExtensionDTO.
220239
/// </summary>
221240
/// <param name="id">Extension ID (Name)</param>
@@ -229,66 +248,51 @@ public async Task<HttpResponseDTO<DownloadExtensionDTO>> Download(string id)
229248
return HttpResponseDTO<DownloadExtensionDTO>.Failure("Extension ID cannot be null or empty");
230249
}
231250

232-
if (_availableExtensions == null || _availableExtensions.Count == 0)
233-
{
234-
return HttpResponseDTO<DownloadExtensionDTO>.Failure("Available extensions list is empty");
235-
}
251+
// Construct download URL with encoded extension name
252+
var encodedExtensionName = Uri.EscapeDataString(id);
253+
var downloadUrl = $"{_serverUrl}/Download/{encodedExtensionName}";
236254

237-
// Find the extension by ID (using Name as ID)
238-
var extension = _availableExtensions.FirstOrDefault(e =>
239-
e.Descriptor.Name?.Equals(id, StringComparison.OrdinalIgnoreCase) == true);
255+
// Make HTTP GET request to download the file
256+
var response = await _httpClient.GetAsync(downloadUrl);
240257

241-
if (extension == null)
258+
if (!response.IsSuccessStatusCode)
242259
{
260+
var errorContent = await response.Content.ReadAsStringAsync();
243261
return HttpResponseDTO<DownloadExtensionDTO>.Failure(
244-
$"Extension with ID '{id}' not found");
245-
}
246-
247-
// Collect all extensions to download (main extension + dependencies)
248-
var extensionsToDownload = new List<AvailableExtension> { extension };
249-
250-
// Resolve dependencies
251-
if (extension.Descriptor.Dependencies != null && extension.Descriptor.Dependencies.Count > 0)
252-
{
253-
foreach (var depId in extension.Descriptor.Dependencies)
254-
{
255-
var dependency = _availableExtensions.FirstOrDefault(e =>
256-
e.Descriptor.Name?.Equals(depId, StringComparison.OrdinalIgnoreCase) == true);
257-
258-
if (dependency != null)
259-
{
260-
extensionsToDownload.Add(dependency);
261-
}
262-
}
262+
$"Server returned error {response.StatusCode}: {errorContent}");
263263
}
264264

265-
// For now, we'll download only the main extension
266-
// In a real implementation, you might want to download all dependencies
267-
// and package them together or return multiple files
268-
269-
// Use the shared update queue
270-
var operation = _updateQueue.Enqueue(extension, false);
271-
272-
var downloadedPath = await DownloadAsync(operation);
265+
// Read the file content as stream
266+
var stream = await response.Content.ReadAsStreamAsync();
267+
var memoryStream = new MemoryStream();
268+
await stream.CopyToAsync(memoryStream);
269+
memoryStream.Position = 0;
273270

274-
if (downloadedPath == null || !File.Exists(downloadedPath))
271+
// Try to get filename from content-disposition header
272+
var fileName = $"{id}.zip";
273+
if (response.Content.Headers.ContentDisposition?.FileName != null)
275274
{
276-
return HttpResponseDTO<DownloadExtensionDTO>.Failure(
277-
$"Failed to download extension '{extension.Descriptor.DisplayName}'");
275+
fileName = response.Content.Headers.ContentDisposition.FileName.Trim('"');
278276
}
279277

280-
// Read the file into a memory stream
281-
var fileBytes = File.ReadAllBytes(downloadedPath);
282-
var stream = new MemoryStream(fileBytes);
283-
284278
var result = new DownloadExtensionDTO
285279
{
286-
FileName = Path.GetFileName(downloadedPath),
287-
Stream = stream
280+
FileName = fileName,
281+
Stream = memoryStream
288282
};
289283

290284
return HttpResponseDTO<DownloadExtensionDTO>.Success(result);
291285
}
286+
catch (HttpRequestException ex)
287+
{
288+
return HttpResponseDTO<DownloadExtensionDTO>.InnerException(
289+
$"HTTP request error: {ex.Message}");
290+
}
291+
catch (TaskCanceledException ex)
292+
{
293+
return HttpResponseDTO<DownloadExtensionDTO>.InnerException(
294+
$"Request timeout: {ex.Message}");
295+
}
292296
catch (Exception ex)
293297
{
294298
return HttpResponseDTO<DownloadExtensionDTO>.InnerException(
@@ -479,5 +483,52 @@ private ExtensionDTO MapToExtensionDTO(AvailableExtension extension, Version? ho
479483
IsCompatible = isCompatible
480484
};
481485
}
486+
487+
/// <summary>
488+
/// Maps an ExtensionDTO to an AvailableExtension
489+
/// </summary>
490+
private AvailableExtension? MapFromExtensionDTO(ExtensionDTO dto)
491+
{
492+
if (dto == null || string.IsNullOrWhiteSpace(dto.Name))
493+
return null;
494+
495+
Version? minVersion = null;
496+
Version? maxVersion = null;
497+
498+
if (!string.IsNullOrWhiteSpace(dto.MinHostVersion))
499+
Version.TryParse(dto.MinHostVersion, out minVersion);
500+
501+
if (!string.IsNullOrWhiteSpace(dto.MaxHostVersion))
502+
Version.TryParse(dto.MaxHostVersion, out maxVersion);
503+
504+
var descriptor = new ExtensionDescriptor
505+
{
506+
Name = dto.Name,
507+
DisplayName = dto.DisplayName ?? dto.Name,
508+
Version = dto.Version ?? "1.0.0",
509+
Description = dto.Description,
510+
Publisher = dto.Publisher,
511+
License = dto.License,
512+
Categories = dto.Categories,
513+
SupportedPlatforms = dto.SupportedPlatforms,
514+
Compatibility = new VersionCompatibility
515+
{
516+
MinHostVersion = minVersion,
517+
MaxHostVersion = maxVersion
518+
},
519+
DownloadUrl = dto.DownloadUrl,
520+
PackageHash = dto.Hash,
521+
PackageSize = dto.FileSize ?? 0,
522+
ReleaseDate = dto.ReleaseDate,
523+
Dependencies = dto.Dependencies,
524+
CustomProperties = dto.CustomProperties
525+
};
526+
527+
return new AvailableExtension
528+
{
529+
Descriptor = descriptor,
530+
IsPreRelease = dto.IsPreRelease
531+
};
532+
}
482533
}
483534
}

0 commit comments

Comments
 (0)