22using System . Collections . Generic ;
33using System . IO ;
44using System . Linq ;
5+ using System . Net . Http ;
6+ using System . Net . Http . Headers ;
7+ using System . Text ;
8+ using System . Text . Json ;
59using System . Threading . Tasks ;
610using GeneralUpdate . Common . Download ;
711using 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