using System.Net.Http.Headers; namespace ClinicBackupSync; sealed class BackupUploader : IDisposable { readonly HttpClient _http = new() { Timeout = TimeSpan.FromHours(6), }; public async Task UploadAsync( string filePath, string uploadUrl, IProgress? progress, CancellationToken cancellationToken) { await using var stream = new FileStream( filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 256 * 1024, FileOptions.Asynchronous | FileOptions.SequentialScan); await using var counted = new ProgressStream(stream, progress); using var fileContent = new StreamContent(counted); fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); using var form = new MultipartFormDataContent(); form.Add(fileContent, "file", Path.GetFileName(filePath)); using var response = await _http.PostAsync(uploadUrl, form, cancellationToken); var body = (await response.Content.ReadAsStringAsync(cancellationToken)).Trim(); if (!response.IsSuccessStatusCode) { var detail = string.IsNullOrWhiteSpace(body) ? response.ReasonPhrase : body; throw new IOException($"Server returned {(int)response.StatusCode}: {detail}"); } progress?.Report(stream.Length); return string.IsNullOrWhiteSpace(body) ? "OK" : body; } public void Dispose() { _http.Dispose(); } } sealed class ProgressStream : Stream { readonly Stream _inner; readonly IProgress? _progress; long _sent; long _lastReportTick = Environment.TickCount64 - 200; public ProgressStream(Stream inner, IProgress? progress) { _inner = inner; _progress = progress; } public override bool CanRead => _inner.CanRead; public override bool CanSeek => _inner.CanSeek; public override bool CanWrite => false; public override long Length => _inner.Length; public override long Position { get => _inner.Position; set => _inner.Position = value; } public override void Flush() { _inner.Flush(); } public override int Read(byte[] buffer, int offset, int count) { var read = _inner.Read(buffer, offset, count); Count(read); return read; } public override int Read(Span buffer) { var read = _inner.Read(buffer); Count(read); return read; } public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { var read = await _inner.ReadAsync(buffer, cancellationToken); Count(read); return read; } public override long Seek(long offset, SeekOrigin origin) { return _inner.Seek(offset, origin); } public override void SetLength(long value) { throw new NotSupportedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } protected override void Dispose(bool disposing) { base.Dispose(disposing); } void Count(int read) { if (read <= 0 || _progress is null) { return; } _sent += read; var now = Environment.TickCount64; if (_sent < Length && now - _lastReportTick < 80) { return; } _lastReportTick = now; _progress.Report(_sent); } }