using System.Diagnostics; using Beam.Abstractions; using Beam.Models; using Microsoft.Extensions.Logging; using File = System.IO.File; namespace Beam.Stealth.Strategies; public class WaitingDownloadStrategy : IDownloadStrategy { public async Task DownloadToStream(string url, int bufferSize, Stream destinationStream, IProgress progress, StealthConfig config, ILogger? logger, CancellationToken ct) { await using var stream = await WaitForDownloadAsync(url, progress, Stopwatch.StartNew(), config, logger, ct); await (stream?.CopyToAsync(destinationStream, ct) ?? Task.CompletedTask); } private async Task WaitForDownloadAsync( string link, IProgress progress, Stopwatch sw, StealthConfig config, ILogger? logger, CancellationToken ct) { const int PollDelayMs = 250; // how often we look const int StableDelayMs = 1000; // size-unchanged window string dir = config.DownloadsDirectory; string? finalPath = null; long lastSize = -1; DateTime lastChange = DateTime.UtcNow; bool IsTemp(string p) => p.EndsWith(".crdownload", StringComparison.OrdinalIgnoreCase) || p.EndsWith(".part", StringComparison.OrdinalIgnoreCase); logger?.LogDebug("Polling {Dir} for download files", dir); while (sw.Elapsed < config.TimeOut && !ct.IsCancellationRequested) { // current files in the directory var files = Directory.EnumerateFiles(dir, "*", SearchOption.TopDirectoryOnly).ToArray(); // ignore temp names; pick (or re-pick) the first real candidate finalPath ??= files.FirstOrDefault(f => !IsTemp(f)); // still nothing but temps – keep waiting if (finalPath is null) { await Task.Delay(PollDelayMs, ct); continue; } // track growth long size = new FileInfo(finalPath).Length; if (size == 0 || size != lastSize) { progress?.Report(new DownloadReport() { BytesDownloaded = size - lastSize, }); lastSize = size; lastChange = DateTime.UtcNow; await Task.Delay(PollDelayMs, ct); continue; } // size stable long enough *and* no temp files left? bool tempsRemain = files.Any(IsTemp); if ((DateTime.UtcNow - lastChange).TotalMilliseconds < StableDelayMs || tempsRemain) { await Task.Delay(PollDelayMs, ct); continue; } // wait until writer releases lock while (true) { try { using FileStream _ = File.Open(finalPath, FileMode.Open, FileAccess.Read, FileShare.None); break; } catch (IOException) { await Task.Delay(200, ct); } } return File.OpenRead(finalPath); } logger?.LogWarning("Download timed out after {Elapsed}", sw.Elapsed); return null; } }