Skip to content

Commit 30df99a

Browse files
CopilotJusterZhu
andcommitted
Implement automatic resume detection for downloads based on partial file existence
Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com>
1 parent 73075ed commit 30df99a

1 file changed

Lines changed: 117 additions & 2 deletions

File tree

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

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)