@@ -234,18 +234,133 @@ public async Task<HttpResponseDTO<PagedResultDTO<ExtensionDTO>>> Query(Extension
234234 }
235235
236236 /// <summary>
237- /// Downloads an extension package by ID via HTTP GET request to the server.
237+ /// Downloads an extension package by ID via HTTP GET request to the server with automatic resume support.
238+ /// Automatically detects partial downloads and resumes from where it left off.
238239 /// Note: The caller is responsible for disposing the Stream in the returned DownloadExtensionDTO.
239240 /// </summary>
240241 /// <param name="id">Extension ID (Name)</param>
241242 /// <returns>Download result containing file name and stream. The caller must dispose the stream.</returns>
242243 public async Task < HttpResponseDTO < DownloadExtensionDTO > > Download ( string id )
243244 {
244- return await Download ( id , 0 ) ;
245+ try
246+ {
247+ if ( string . IsNullOrWhiteSpace ( id ) )
248+ {
249+ return HttpResponseDTO < DownloadExtensionDTO > . Failure ( "Extension ID cannot be null or empty" ) ;
250+ }
251+
252+ // Construct download URL with encoded extension name
253+ var encodedExtensionName = Uri . EscapeDataString ( id ) ;
254+ var downloadUrl = $ "{ _serverUrl } /Download/{ encodedExtensionName } ";
255+
256+ // Determine the temporary file path for partial downloads
257+ var tempFileName = $ "{ id } .partial";
258+ var tempFilePath = Path . Combine ( _downloadPath , tempFileName ) ;
259+
260+ // Check if a partial download exists and get its size
261+ long startPosition = 0 ;
262+ if ( File . Exists ( tempFilePath ) )
263+ {
264+ var fileInfo = new FileInfo ( tempFilePath ) ;
265+ startPosition = fileInfo . Length ;
266+ }
267+
268+ // Create request message to support Range header
269+ var request = new HttpRequestMessage ( HttpMethod . Get , downloadUrl ) ;
270+
271+ // Add Range header if resuming from a specific position
272+ if ( startPosition > 0 )
273+ {
274+ request . Headers . Range = new System . Net . Http . Headers . RangeHeaderValue ( startPosition , null ) ;
275+ }
276+
277+ // Make HTTP GET request to download the file
278+ var response = await _httpClient . SendAsync ( request , HttpCompletionOption . ResponseHeadersRead ) ;
279+
280+ // Check for success status codes (200 for full content, 206 for partial content)
281+ if ( response . StatusCode != System . Net . HttpStatusCode . OK &&
282+ response . StatusCode != System . Net . HttpStatusCode . PartialContent )
283+ {
284+ var errorContent = await response . Content . ReadAsStringAsync ( ) ;
285+ return HttpResponseDTO < DownloadExtensionDTO > . Failure (
286+ $ "Server returned error { response . StatusCode } : { errorContent } ") ;
287+ }
288+
289+ // If we received a 200 (full content) response but we had a partial file, delete it and start fresh
290+ if ( response . StatusCode == System . Net . HttpStatusCode . OK && File . Exists ( tempFilePath ) )
291+ {
292+ File . Delete ( tempFilePath ) ;
293+ startPosition = 0 ;
294+ }
295+
296+ // Download and append to the partial file
297+ using ( var responseStream = await response . Content . ReadAsStreamAsync ( ) )
298+ {
299+ using ( var fileStream = new FileStream ( tempFilePath ,
300+ startPosition > 0 ? FileMode . Append : FileMode . Create ,
301+ FileAccess . Write ,
302+ FileShare . None ) )
303+ {
304+ await responseStream . CopyToAsync ( fileStream ) ;
305+ }
306+ }
307+
308+ // Try to get filename from content-disposition header
309+ var fileName = $ "{ id } .zip";
310+ if ( response . Content . Headers . ContentDisposition ? . FileName != null )
311+ {
312+ fileName = response . Content . Headers . ContentDisposition . FileName . Trim ( '"' ) ;
313+ }
314+ // URL decode the filename if it was URL encoded
315+ fileName = System . Net . WebUtility . UrlDecode ( fileName ) ;
316+
317+ // Read the complete file into a memory stream
318+ var memoryStream = new MemoryStream ( ) ;
319+ using ( var fileStream = new FileStream ( tempFilePath , FileMode . Open , FileAccess . Read , FileShare . Read ) )
320+ {
321+ await fileStream . CopyToAsync ( memoryStream ) ;
322+ }
323+ memoryStream . Position = 0 ;
324+
325+ // Delete the temporary file now that we have it in memory
326+ if ( File . Exists ( tempFilePath ) )
327+ {
328+ File . Delete ( tempFilePath ) ;
329+ }
330+
331+ var result = new DownloadExtensionDTO
332+ {
333+ FileName = fileName ,
334+ Stream = memoryStream
335+ } ;
336+
337+ return HttpResponseDTO < DownloadExtensionDTO > . Success ( result ) ;
338+ }
339+ catch ( HttpRequestException ex )
340+ {
341+ return HttpResponseDTO < DownloadExtensionDTO > . InnerException (
342+ $ "HTTP request error: { ex . Message } ") ;
343+ }
344+ catch ( TaskCanceledException ex )
345+ {
346+ return HttpResponseDTO < DownloadExtensionDTO > . InnerException (
347+ $ "Request timeout: { ex . Message } ") ;
348+ }
349+ catch ( IOException ex )
350+ {
351+ return HttpResponseDTO < DownloadExtensionDTO > . InnerException (
352+ $ "File I/O error: { ex . Message } ") ;
353+ }
354+ catch ( Exception ex )
355+ {
356+ return HttpResponseDTO < DownloadExtensionDTO > . InnerException (
357+ $ "Error downloading extension: { ex . Message } ") ;
358+ }
245359 }
246360
247361 /// <summary>
248362 /// Downloads an extension package by ID via HTTP GET request with support for resumable downloads.
363+ /// This overload allows manual control of the resume position.
249364 /// Note: The caller is responsible for disposing the Stream in the returned DownloadExtensionDTO.
250365 /// </summary>
251366 /// <param name="id">Extension ID (Name)</param>
0 commit comments