Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebClient.CancelAsync — File still downloading

I'm trying to create a Web API using Asp.NET Core that exposes routes to start and cancel long downloads of large files. The server should be able to handle multiple downloads at the same time.

  • The download is performed using WebClient.DownloadFileAsync in order to have a short response time and returning a downloadId for later use. The instance of the WebClient is stored as value in a static dictionary whose corresponding key is of naturally the downloadId.
  • The download should be canceled using WebClient.CancelAsync on the instance of the client retrieved by accessing the value of the dictionary corresponding to the downloadId key.

The following code works perfectly when the download reaches its completion without being canceled; the AsyncCompletedEventHandler (OnDownloadFileCompleted in that case) is properly invoked.

PROBLEM: When invoking WebClient.CancelAsync, the file keeps on downloading and OnDownloadFileCompleted is not invoked right away. The WebClient seems to wait until the download finishes before invoking the handler. In both case however, the property AsyncCompletedEventArgs.Canceled is properly set (e.g to true if WebClient.CancelAsync was indeed invoked.

  • Am I missing something?
  • Is there a better/smarter pattern to handle multiple downloads in a WebAPI?

Any help would much appreciated!

DownloadController.cs

[Route ("api/download")]
public class DownloadController {

    private readonly DownloadService service;

    public DownloadController (DownloadService service) {
        this.service = service;
    }

    [Route ("start")]
    [HttpPost]
    public string Start ([FromForm] string fileUrl) => this.service.StartDownload (fileUrl);

    [Route ("cancel")]
    [HttpPost]
    public void Cancel ([FromForm] string downloadId) => this.service.CancelDownload (downloadId);
}

DownloadService.cs

public class DownloadService {
    public string DOWNLOAD_FOLDER { get => "C:\\tmp"; }
    public static Dictionary<string, WebClient> DownloadClients = new Dictionary<string, WebClient> ();

    public string StartDownload (string fileUrl) {
        var downloadId = Guid.NewGuid ().ToString ("N");
        DownloadClients[downloadId] = new WebClient ();
        DownloadClients[downloadId].DownloadFileCompleted += OnDownloadFileCompleted;
        DownloadClients[downloadId].DownloadFileAsync (new Uri (fileUrl), Path.Combine (DOWNLOAD_FOLDER, downloadId), downloadId);
        return downloadId;
    }

    public void CancelDownload (string downloadId) {
        if (DownloadClients.TryGetValue (downloadId, out WebClient client)) {
            client.CancelAsync ();
        }
    }

    private void OnDownloadFileCompleted (object sender, AsyncCompletedEventArgs e) {
        var downloadId = e.UserState.ToString ();
        if (!e.Cancelled) {
            Debug.WriteLine ("Completed");
        } else {
            Debug.WriteLine ("Cancelled"); //will only be reached when the file finishes downloading
        }

        if (DownloadClients.ContainsKey (downloadId)) {
            DownloadClients[downloadId].Dispose ();
            DownloadClients.Remove (downloadId);
        }
    }
}
like image 582
Pierre Roudaut Avatar asked Jan 10 '18 12:01

Pierre Roudaut


1 Answers

I was able to replicate what you saw: CancelAsync does not actually cancel the download.

Using HttpClient, you can get the stream and save it to a file using CopyToAsync, accepts a CancellationToken. Cancelling the token stops the download immediately.

Here is the DownloadService class that I modified to use HttpClient.

public class DownloadService {
    public string DOWNLOAD_FOLDER {
        get => "C:\\tmp";
    }

    public static readonly ConcurrentDictionary<string, Download> Downloads = new ConcurrentDictionary<string, Download>();

    public async Task<string> StartDownload(string fileUrl) {
        var downloadId = Guid.NewGuid().ToString("N");
        Downloads[downloadId] = new Download(fileUrl);
        await Downloads[downloadId].Start(Path.Combine(DOWNLOAD_FOLDER, downloadId));

        return downloadId;
    }

    public void CancelDownload(string downloadId) {
        if (Downloads.TryRemove(downloadId, out var download)) {
            download.Cancel();
        }
    }

This uses a Download class that looks like this:

public class Download {
    private static readonly HttpClient Client = new HttpClient();
    private readonly string _fileUrl;

    private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
    private Task _copyTask;
    private Stream _responseStream;
    private Stream _fileStream;

    public Download(string fileUrl) {
        _fileUrl = fileUrl;
    }

    public async Task Start(string saveTo) {
        var response = await Client.GetAsync(_fileUrl, HttpCompletionOption.ResponseHeadersRead);
        _responseStream = await response.Content.ReadAsStreamAsync();
        _fileStream = File.Create(saveTo);
        _copyTask = _responseStream.CopyToAsync(_fileStream, 81920, _tokenSource.Token).ContinueWith(task => {
            if (task.IsCanceled) return;
            _responseStream.Dispose();
            _fileStream.Dispose();
        });
    }

    public void Cancel() {
        _tokenSource.Cancel();
        _responseStream.Dispose();
        _fileStream.Dispose();
    }
}

You will still have some work to do to remove successfully-completed downloads from your Downloads list, but I'll leave that with you.

like image 109
Gabriel Luci Avatar answered Sep 24 '22 05:09

Gabriel Luci