web-dev-qa-db-fra.com

Barre de progression avec HttpClient

j'ai une fonction de téléchargement de fichiers:

        HttpClientHandler aHandler = new HttpClientHandler();
        aHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
        HttpClient aClient = new HttpClient(aHandler);
        aClient.DefaultRequestHeaders.ExpectContinue = false;
        HttpResponseMessage response = await aClient.GetAsync(url);
        InMemoryRandomAccessStream randomAccessStream = new InMemoryRandomAccessStream();

        // To save downloaded image to local storage
        var imageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(
        filename, CreationCollisionOption.ReplaceExisting);
        var fs = await imageFile.OpenAsync(FileAccessMode.ReadWrite);
        DataWriter writer = new DataWriter(fs.GetOutputStreamAt(0));

        writer.WriteBytes(await response.Content.ReadAsByteArrayAsync());

        await writer.StoreAsync();
        //current.image.SetSource(randomAccessStream);
        writer.DetachStream();
        await fs.FlushAsync();

Comment puis-je réaliser la fonctionnalité de barre de progression? Peut-être que je peux obtenir les octets d'écriture écrits jusqu'à présent? Ou quelque chose?

P.S. Je ne peux pas utiliser DownloadOperation (transfert en arrière-plan) parce que les données du serveur demandent un certificat - et cette fonctionnalité n'existe pas dans DownloadOperations.

27
Cheese

La meilleure solution consiste à utiliser Windows.Web.Http.HttpClient au lieu de System.Net.Http.HttpClient. Le premier soutient le progrès.

Mais si pour une raison quelconque vous souhaitez vous en tenir à celui de System.Net, vous devrez implémenter vos propres progrès.

Supprimez le DataWriter, supprimez le InMemoryRandomAccessStream et ajoutez HttpCompletionOption.ResponseHeadersRead à GetAsync call donc il revient dès que les en-têtes sont reçus, pas quand toute la réponse est reçue. C'est à dire.:

// Your original code.
HttpClientHandler aHandler = new HttpClientHandler();
aHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
HttpClient aClient = new HttpClient(aHandler);
aClient.DefaultRequestHeaders.ExpectContinue = false;
HttpResponseMessage response = await aClient.GetAsync(
    url,
    HttpCompletionOption.ResponseHeadersRead); // Important! ResponseHeadersRead.

// To save downloaded image to local storage
var imageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(
    filename,
    CreationCollisionOption.ReplaceExisting);
var fs = await imageFile.OpenAsync(FileAccessMode.ReadWrite);

// New code.
Stream stream = await response.Content.ReadAsStreamAsync();
IInputStream inputStream = stream.AsInputStream();
ulong totalBytesRead = 0;
while (true)
{
    // Read from the web.
    IBuffer buffer = new Windows.Storage.Streams.Buffer(1024);
    buffer = await inputStream.ReadAsync(
        buffer,
        buffer.Capacity,
        InputStreamOptions.None);

    if (buffer.Length == 0)
    {
        // There is nothing else to read.
        break;
    }

    // Report progress.
    totalBytesRead += buffer.Length;
    System.Diagnostics.Debug.WriteLine("Bytes read: {0}", totalBytesRead);

    // Write to file.
    await fs.WriteAsync(buffer);
}
inputStream.Dispose();
fs.Dispose();
23
kiewic

Voici une classe autonome qui fera le téléchargement et rendra compte du pourcentage de progression, basé sur le code de TheBlueSky sur this = SO réponse , et eriksendc sur ce commentaire GitHub .

public class HttpClientDownloadWithProgress : IDisposable
{
    private readonly string _downloadUrl;
    private readonly string _destinationFilePath;

    private HttpClient _httpClient;

    public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);

    public event ProgressChangedHandler ProgressChanged;

    public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath)
    {
        _downloadUrl = downloadUrl;
        _destinationFilePath = destinationFilePath;
    }

    public async Task StartDownload()
    {
        _httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1) };

        using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
            await DownloadFileFromHttpResponseMessage(response);
    }

    private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
    {
        response.EnsureSuccessStatusCode();

        var totalBytes = response.Content.Headers.ContentLength;

        using (var contentStream = await response.Content.ReadAsStreamAsync())
            await ProcessContentStream(totalBytes, contentStream);
    }

    private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
    {
        var totalBytesRead = 0L;
        var readCount = 0L;
        var buffer = new byte[8192];
        var isMoreToRead = true;

        using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
        {
            do
            {
                var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
                if (bytesRead == 0)
                {
                    isMoreToRead = false;
                    TriggerProgressChanged(totalDownloadSize, totalBytesRead);
                    continue;
                }

                await fileStream.WriteAsync(buffer, 0, bytesRead);

                totalBytesRead += bytesRead;
                readCount += 1;

                if (readCount % 100 == 0)
                    TriggerProgressChanged(totalDownloadSize, totalBytesRead);
            }
            while (isMoreToRead);
        }
    }

    private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
    {
        if (ProgressChanged == null)
            return;

        double? progressPercentage = null;
        if (totalDownloadSize.HasValue)
            progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);

        ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
    }

    public void Dispose()
    {
        _httpClient?.Dispose();
    }
}

Usage:

var downloadFileUrl = "http://example.com/file.Zip";
var destinationFilePath = Path.GetFullPath("file.Zip");

using (var client = new HttpClientDownloadWithProgress(downloadFileUrl, destinationFilePath))
{
    client.ProgressChanged += (totalFileSize, totalBytesDownloaded, progressPercentage) => {
        Console.WriteLine($"{progressPercentage}% ({totalBytesDownloaded}/{totalFileSize})");
    };

    await client.StartDownload();
}

Résultat:

7.81% (26722304/342028776)
8.05% (27535016/342028776)
8.28% (28307984/342028776)
8.5% (29086548/342028776)
8.74% (29898692/342028776)
8.98% (30704184/342028776)
9.22% (31522816/342028776)
21
René Sackers

À partir de .Net 4.5: utilisez IProgress<T>

Depuis .Net 4.5, vous pouvez gérer les rapports de progression asynchrones avec IProgress<T> interface. Vous pouvez écrire une méthode d'extension pour télécharger des fichiers en utilisant le HttpClient qui peut être appelé comme ceci où progress est l'implémentation de IProgress<float> pour votre barre d'avant-projet ou d'autres éléments de l'interface utilisateur:

// Seting up the http client used to download the data
using (var client = new HttpClient()) {
    client.Timeout = TimeSpan.FromMinutes(5);

    // Create a file stream to store the downloaded data.
    // This really can be any type of writeable stream.
    using (var file = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)) {

        // Use the custom extension method below to download the data.
        // The passed progress-instance will receive the download status updates.
        await client.DownloadAsync(DownloadUrl, file, progress, cancellationToken);
    }
}

La mise en oeuvre

Le code de cette méthode d'extension ressemble à ceci. Notez que cette extension dépend d'une autre extension pour gérer la copie de flux asynchrone avec des rapports de progression.

public static class HttpClientExtensions
{
    public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress<float> progress = null, CancellationToken cancellationToken = default) {
        // Get the http headers first to examine the content length
        using (var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead)) {
            var contentLength = response.Content.Headers.ContentLength;

            using (var download = await response.Content.ReadAsStreamAsync()) {

                // Ignore progress reporting when no progress reporter was 
                // passed or when the content length is unknown
                if (progress == null || !contentLength.HasValue) {
                    await download.CopyToAsync(destination);
                    return;
                }

                // Convert absolute progress (bytes downloaded) into relative progress (0% - 100%)
                var relativeProgress = new Progress<long>(totalBytes => progress.Report((float)totalBytes / contentLength.Value));
                // Use extension method to report progress while downloading
                await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken);
                progress.Report(1);
            }
        }
    }
}

Avec extension de flux pour les rapports de progrès réels:

public static class StreamExtensions
{
    public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress = null, CancellationToken cancellationToken = default) {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        if (!source.CanRead)
            throw new ArgumentException("Has to be readable", nameof(source));
        if (destination == null)
            throw new ArgumentNullException(nameof(destination));
        if (!destination.CanWrite)
            throw new ArgumentException("Has to be writable", nameof(destination));
        if (bufferSize < 0)
            throw new ArgumentOutOfRangeException(nameof(bufferSize));

        var buffer = new byte[bufferSize];
        long totalBytesRead = 0;
        int bytesRead;
        while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) {
            await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
            totalBytesRead += bytesRead;
            progress?.Report(totalBytesRead);
        }
    }
}
17
Bruno Zell

Le code suivant montre un exemple minimal de ce qui doit être fait contre l'api HttpClient pour obtenir la progression du téléchargement.

HttpClient client = //...

// Must use ResponseHeadersRead to avoid buffering of the content
using (var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead)){
    // You must use as stream to have control over buffering and number of bytes read/received
    using (var stream = await response.Content.ReadAsStreamAsync())
    {
        // Read/process bytes from stream as appropriate

        // Calculated by you based on how many bytes you have read.  Likely incremented within a loop.
        long bytesRecieved = //...

        long? totalBytes = response.Content.Headers.ContentLength;
        double? percentComplete = (double)bytesRecieved / totalBytes;

        // Do what you want with `percentComplete`
    }
}

Ce qui précède ne vous dit pas comment traiter le flux, comment signaler le processus ou essayer de fournir une solution directe au code de la question d'origine. Cependant, cette réponse peut être plus accessible aux futurs lecteurs qui souhaitent postuler en ayant progressé dans leur code.

3
vossad01

identique à la solution @ René Sackers ci-dessus, mais a ajouté la possibilité d'annuler le téléchargement

class HttpClientDownloadWithProgress : IDisposable
{
    private readonly string _downloadUrl;
    private readonly string _destinationFilePath;
    private readonly CancellationToken? _cancellationToken;

    private HttpClient _httpClient;

    public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);

    public event ProgressChangedHandler ProgressChanged;

    public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath, CancellationToken? cancellationToken = null)
    {
        _downloadUrl = downloadUrl;
        _destinationFilePath = destinationFilePath;
        _cancellationToken = cancellationToken;
    }

    public async Task StartDownload()
    {
        _httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1) };

        using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
            await DownloadFileFromHttpResponseMessage(response);
    }

    private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
    {
        response.EnsureSuccessStatusCode();

        var totalBytes = response.Content.Headers.ContentLength;

        using (var contentStream = await response.Content.ReadAsStreamAsync())
            await ProcessContentStream(totalBytes, contentStream);
    }

    private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
    {
        var totalBytesRead = 0L;
        var readCount = 0L;
        var buffer = new byte[8192];
        var isMoreToRead = true;

        using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
        {
            do
            {
                int bytesRead;
                if (_cancellationToken.HasValue)
                {
                    bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, _cancellationToken.Value);
                }
                else
                {
                    bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
                }

                if (bytesRead == 0)
                {
                    isMoreToRead = false;
                    continue;
                }

                await fileStream.WriteAsync(buffer, 0, bytesRead);

                totalBytesRead += bytesRead;
                readCount += 1;

                if (readCount % 10 == 0)
                    TriggerProgressChanged(totalDownloadSize, totalBytesRead);
            }
            while (isMoreToRead);

        }

        //the last progress trigger should occur after the file handle has been released or you may get file locked error
        TriggerProgressChanged(totalDownloadSize, totalBytesRead);
    }

    private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
    {
        if (ProgressChanged == null)
            return;

        double? progressPercentage = null;
        if (totalDownloadSize.HasValue)
            progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);

        ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
    }

    public void Dispose()
    {
        _httpClient?.Dispose();
    }
}
2
Balage

La version René Sackers est excellente mais elle pourrait être meilleure. Plus précisément, il présente une condition de concurrence subtile provoquée par le déclenchement de TriggerProgressChanged avant la fermeture du flux. Le correctif consiste à déclencher l'événement après la suppression explicite du flux. La version ci-dessous inclut la modification ci-dessus, hérite de HttpClient et ajoute la prise en charge des jetons d'annulation.

public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);

public class HttpClientWithProgress : HttpClient
{
    private readonly string _DownloadUrl;
    private readonly string _DestinationFilePath;

    public event ProgressChangedHandler ProgressChanged;

    public HttpClientWithProgress(string downloadUrl, string destinationFilePath)
    {
        _DownloadUrl = downloadUrl;
        _DestinationFilePath = destinationFilePath;
    }

    public async Task StartDownload()
    {
        using (var response = await GetAsync(_DownloadUrl, HttpCompletionOption.ResponseHeadersRead))
            await DownloadFileFromHttpResponseMessage(response);
    }

    public async Task StartDownload(CancellationToken cancellationToken)
    {
        using (var response = await GetAsync(_DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
            await DownloadFileFromHttpResponseMessage(response);
    }

    private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
    {
        response.EnsureSuccessStatusCode();
        long? totalBytes = response.Content.Headers.ContentLength;
        using (var contentStream = await response.Content.ReadAsStreamAsync())
            await ProcessContentStream(totalBytes, contentStream);
    }

    private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
    {
        long totalBytesRead = 0L;
        long readCount = 0L;
        byte[] buffer = new byte[8192];
        bool isMoreToRead = true;

        using (FileStream fileStream = new FileStream(_DestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
        {
            do
            {
                int bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
                if (bytesRead == 0)
                {
                    isMoreToRead = false;
                    continue;
                }

                await fileStream.WriteAsync(buffer, 0, bytesRead);

                totalBytesRead += bytesRead;
                readCount += 1;

                if (readCount % 10 == 0)
                    TriggerProgressChanged(totalDownloadSize, totalBytesRead);
            }
            while (isMoreToRead);
        }
        TriggerProgressChanged(totalDownloadSize, totalBytesRead);
    }

    private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
    {
        if (ProgressChanged == null)
            return;

        double? progressPercentage = null;
        if (totalDownloadSize.HasValue)
            progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);

        ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
    }
}
0
Shaun

Hm, vous pouvez demander à un autre thread de vérifier la taille actuelle du flux en cours d'écriture (vous devez également lui transmettre la taille de fichier attendue), puis mettre à jour la barre de progression en conséquence.

0
Rodrigo Silva