Реализация докачивания файла HttpClient
Разрабатываю приложение, которое должно скачать файл с web адреса на компьютер и дальше его обработать.
Задачу вроде реализовал, все работает и тут ко мне обратился клиент и говорит:
У меня интернет медленный и имеет частые обрывы соединения, не можешь дать прямую ссылку, а то программа качает файл по новой.
И вот собственно я пытаюсь исправить эту проблему, а точнее надо сделать следующие 2 вещи:
- Возобновление загрузки файла, после обрыва соединения. - Частично решил при помощи буфера, кратковременные обрывы вроде как говорят гасит.
- Докачать уже имеющийся "Огрызок", который остался после предыдущего процесса неудачного скачивания.
Сейчас процесс скачивания файла выглядит примерно так:
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
{
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = new byte[4096];
var isMoreToRead = true;
using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, 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);
}
}
То есть мы с сервера берем кусок (буфер) и записываем его в FileStream до тех пор, пока сервер не отдаст нам весь файл.
Я пробовал:
- При запуске сдвигать позицию
Stream'ов доfileStream.Length(текущего размера файла), ноHttpClientотдает обычныйStream, который имеетCanSeek = false, то есть я не могу с сервера начать получать данные с определенной точки, только с самого начала. Я пробовал пропускать запись в файл (
await fileStream.WriteAsync(buffer, 0, bytesRead);) до тех пор, пока кол-во полученных с сервера данных не будет равно текущему размеру файла, то есть делаю нечто такое (набросок):if (totalBytesRead >= fileStream.Length) await fileStream.WriteAsync(buffer, 0, bytesRead);Но по ощущениям, из за
await contentStream.ReadAsync(buffer, 0, buffer.Length);я данные все равно качаю с сервера, но только не записываю, а в результате получаю битый файл, который имеет разрыв ровно в том месте, где остановилась загрузка. Тут скорей всего происходит подобное из за того, что буфер был не до конца записан при закрытие приложения и при повторной загрузке мне надо файл начать записывать на 1 "буфер" раньше, но как это сделать так и не понял.
Короче, я в тупике и без понятия как докачать уже существующий файл на диске, посла обрыва/закрытия приложения. Было бы супер, если бы вы научили меня брать с сервера байты с определенной позиции, не затрагивая начало (Seek в Stream), тогда можно наверно после разрыва запросить эти байты и продолжить закачку, или есть другие варианты?
Ответы (1 шт):
В общем да, спасибо всем, что помогли и натолкнули на Заголовок Range.
Сделал метод, который возвращает текущий размер файла с "откатом" на размер буфера:
private long GetFileSize() { long? fileSize = null; if (_isExist) { fileSize = new FileInfo(_destinationFilePath)?.Length; var tail = fileSize % _bufferSize; if (tail != 0) fileSize -= _bufferSize; } return fileSize ?? 0L; }Результатом он выдаст либо 0, либо
размер файла - размер буфера.Дальше переписал отправку запроса на такое:
var request = new HttpRequestMessage { RequestUri = new Uri(_downloadUrl) }; request.Headers.Range = new RangeHeaderValue(_fileSize, null); using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);RangeHeaderValue(_fileSize, null);- отправляем серверу размер ранее полученного файла и берем весь остаток. Если 0 - сервер отдает все._httpClient.SendAsync(...)- отправляем на сервер GET запрос сRangeзаголовком.
У
FileStreamустановилFileMode.OpenOrCreate, чтоб файл не перезписывало.Перед чтением данных с сервера задаю позицию
FileStreamна размер файла, который ранее получили:fileStream.Seek(_fileSize, SeekOrigin.Begin);
Собственно и все, что необходимо для продолжения скачивания файла. Сам код сейчас примерно такой (черновик):
public class DownloadManager : IDisposable
{
private readonly HttpClient _httpClient;
private readonly string _downloadUrl;
private readonly string _destinationFilePath;
private long _fileSize;
private long? _totalFileSize;
private bool _isExist;
private const int _bufferSize = 8192;
private int _connections;
public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public event ProgressChangedHandler ProgressChanged;
public DownloadManager(string downloadUrl, string destinationFilePath)
=> (_downloadUrl, _destinationFilePath, _httpClient)
= (downloadUrl, destinationFilePath, new HttpClient());
public async Task StartDownload()
{
bool isConnect = false;
do
{
try
{
using var sizeRequest = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead);
_totalFileSize = sizeRequest.Content.Headers.ContentLength;
isConnect = true;
_connections = 0;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
_connections++;
await Task.Delay(TimeSpan.FromSeconds(5));
}
} while (!isConnect && _connections <= 15);
if (!isConnect) return;
_isExist = File.Exists(_destinationFilePath);
_fileSize = GetFileSize();
var request = new HttpRequestMessage { RequestUri = new Uri(_downloadUrl) };
request.Headers.Range = new RangeHeaderValue(_fileSize, null);
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
await DownloadFileFromHttpResponseMessage(response);
}
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
{
Console.WriteLine($"[{response.StatusCode}]: {response.RequestMessage}");
if (response.IsSuccessStatusCode)
{
using var contentStream = await response.Content.ReadAsStreamAsync();
await ProcessContentStream(_totalFileSize, contentStream);
}
else
{
if (_fileSize != _totalFileSize)
response.EnsureSuccessStatusCode();
}
}
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
{
var buffer = new byte[_bufferSize];
var totalBytesRead = _fileSize;
var isMoreToRead = true;
var readCount = 0L;
using (var fileStream = new FileStream(_destinationFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, _bufferSize, true))
{
fileStream.Seek(_fileSize, SeekOrigin.Begin);
do
{
int bytesRead = 0;
try
{
bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
await fileStream.FlushAsync();
fileStream.Close();
fileStream.Dispose();
await StartDownload();
return;
}
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);
}
Console.WriteLine(_destinationFilePath.GetMD5());
}
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);
}
private long GetFileSize()
{
long? fileSize = null;
if (_isExist)
{
fileSize = new FileInfo(_destinationFilePath)?.Length;
var tail = fileSize % _bufferSize;
if (tail != 0) fileSize -= _bufferSize;
}
return fileSize ?? 0L;
}
public void Dispose() => _httpClient?.Dispose();
}
Издевательства по типу:
- Открытие/закрытие окна каждые 3-5%.
- Отключение интернета.
- Заморозки процесса на длительное время.
код успешно прошел, файл всегда докачивался и имел всегда верный Md5 и размер.
Осталось подчистить, привести все в порядок, пару проверок добавить, событий и можно использовать)