Skip to content

Commit 4bd6855

Browse files
committed
fix: fix downloading different URLs to same destination
1 parent 8f60b4d commit 4bd6855

File tree

2 files changed

+42
-15
lines changed

2 files changed

+42
-15
lines changed

Tests.Vpn.Service/DownloaderTest.cs

+31-3
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,34 @@ public async Task Download(CancellationToken ct)
284284
Assert.That(await File.ReadAllTextAsync(destPath, ct), Is.EqualTo("test"));
285285
}
286286

287+
[Test(Description = "Perform 2 downloads with the same destination")]
288+
[CancelAfter(30_000)]
289+
public async Task DownloadSameDest(CancellationToken ct)
290+
{
291+
using var httpServer = EchoServer();
292+
var url0 = new Uri(httpServer.BaseUrl + "/test0");
293+
var url1 = new Uri(httpServer.BaseUrl + "/test1");
294+
var destPath = Path.Combine(_tempDir, "test");
295+
296+
var manager = new Downloader(NullLogger<Downloader>.Instance);
297+
var startTask0 = manager.StartDownloadAsync(new HttpRequestMessage(HttpMethod.Get, url0), destPath,
298+
NullDownloadValidator.Instance, ct);
299+
var startTask1 = manager.StartDownloadAsync(new HttpRequestMessage(HttpMethod.Get, url1), destPath,
300+
NullDownloadValidator.Instance, ct);
301+
var dlTask0 = await startTask0;
302+
await dlTask0.Task;
303+
Assert.That(dlTask0.TotalBytes, Is.EqualTo(5));
304+
Assert.That(dlTask0.BytesRead, Is.EqualTo(5));
305+
Assert.That(dlTask0.Progress, Is.EqualTo(1));
306+
Assert.That(dlTask0.IsCompleted, Is.True);
307+
var dlTask1 = await startTask1;
308+
await dlTask1.Task;
309+
Assert.That(dlTask1.TotalBytes, Is.EqualTo(5));
310+
Assert.That(dlTask1.BytesRead, Is.EqualTo(5));
311+
Assert.That(dlTask1.Progress, Is.EqualTo(1));
312+
Assert.That(dlTask1.IsCompleted, Is.True);
313+
}
314+
287315
[Test(Description = "Download with custom headers")]
288316
[CancelAfter(30_000)]
289317
public async Task WithHeaders(CancellationToken ct)
@@ -395,9 +423,9 @@ public void CancelledOuter(CancellationToken ct)
395423
var manager = new Downloader(NullLogger<Downloader>.Instance);
396424
// The "outer" Task should fail.
397425
var smallerCt = new CancellationTokenSource(TimeSpan.FromSeconds(1)).Token;
398-
Assert.ThrowsAsync<TaskCanceledException>(
399-
async () => await manager.StartDownloadAsync(new HttpRequestMessage(HttpMethod.Get, url), destPath,
400-
NullDownloadValidator.Instance, smallerCt));
426+
Assert.ThrowsAsync<TaskCanceledException>(async () => await manager.StartDownloadAsync(
427+
new HttpRequestMessage(HttpMethod.Get, url), destPath,
428+
NullDownloadValidator.Instance, smallerCt));
401429
}
402430

403431
[Test(Description = "Timeout on response body")]

Vpn.Service/Downloader.cs

+11-12
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,9 @@ public async Task<DownloadTask> StartDownloadAsync(HttpRequestMessage req, strin
288288
{
289289
var task = _downloads.GetOrAdd(destinationPath,
290290
_ => new DownloadTask(_logger, req, destinationPath, validator));
291-
await task.EnsureStartedAsync(ct);
291+
// EnsureStarted is a no-op if we didn't create a new DownloadTask.
292+
// So, we will only remove the destination once for each time we start a new task.
293+
task.EnsureStarted(tsk => _downloads.TryRemove(destinationPath, out _), ct);
292294

293295
// If the existing (or new) task is for the same URL, return it.
294296
if (task.Request.RequestUri == req.RequestUri)
@@ -357,21 +359,19 @@ internal DownloadTask(ILogger logger, HttpRequestMessage req, string destination
357359
".download-" + Path.GetRandomFileName());
358360
}
359361

360-
internal async Task<Task> EnsureStartedAsync(CancellationToken ct = default)
362+
internal void EnsureStarted(Action<Task> continuation, CancellationToken ct = default)
361363
{
362-
using var _ = await _semaphore.LockAsync(ct);
364+
using var _ = _semaphore.Lock();
363365
if (Task == null!)
364-
Task = await StartDownloadAsync(ct);
365-
366-
return Task;
366+
Task = Start(ct).ContinueWith(continuation, CancellationToken.None);
367367
}
368368

369369
/// <summary>
370370
/// Starts downloading the file. The request will be performed in this task, but once started, the task will complete
371371
/// and the download will continue in the background. The provided CancellationToken can be used to cancel the
372372
/// download.
373373
/// </summary>
374-
private async Task<Task> StartDownloadAsync(CancellationToken ct = default)
374+
private async Task Start(CancellationToken ct = default)
375375
{
376376
Directory.CreateDirectory(_destinationDirectory);
377377

@@ -398,8 +398,7 @@ private async Task<Task> StartDownloadAsync(CancellationToken ct = default)
398398
throw new Exception("Existing file failed validation after 304 Not Modified", e);
399399
}
400400

401-
Task = Task.CompletedTask;
402-
return Task;
401+
return;
403402
}
404403

405404
if (res.StatusCode != HttpStatusCode.OK)
@@ -432,11 +431,11 @@ private async Task<Task> StartDownloadAsync(CancellationToken ct = default)
432431
throw;
433432
}
434433

435-
Task = DownloadAsync(res, tempFile, ct);
436-
return Task;
434+
await Download(res, tempFile, ct);
435+
return;
437436
}
438437

439-
private async Task DownloadAsync(HttpResponseMessage res, FileStream tempFile, CancellationToken ct)
438+
private async Task Download(HttpResponseMessage res, FileStream tempFile, CancellationToken ct)
440439
{
441440
try
442441
{

0 commit comments

Comments
 (0)