using System.Text.Json; using System.Text.Json.Serialization; namespace YMhut.Box.Core.Downloads; public enum DownloadState { Queued, Running, Paused, Completed, Failed, Canceled } public sealed record DownloadSource( string Url, string DisplayName, string FileName, string SourceKind = "Official", string? Sha256 = null, string SourceLabel = "", string MirrorRegion = "", long? SizeBytes = null, string OriginalUrl = "", string ResolvedUrl = "", string ValidatedContentType = "", string PackageId = "") { public string EffectiveLabel => string.IsNullOrWhiteSpace(SourceLabel) ? SourceKind : SourceLabel; public string EffectiveUrl => string.IsNullOrWhiteSpace(ResolvedUrl) ? Url : ResolvedUrl; } public sealed record DownloadOptions( string? TargetDirectory = null, string? TargetPath = null, string? InstallCommand = null, string? InstallArguments = null, bool IsInstaller = false, bool DeleteAfterInstall = false); public sealed record DownloadSettings( string DefaultDirectory, int MaxConcurrentDownloads = 5) { public int EffectiveMaxConcurrentDownloads => Math.Clamp(MaxConcurrentDownloads, 1, 5); } public sealed record DownloadItem( string Id, DownloadSource Source, string TargetPath, DownloadState State, long ReceivedBytes = 0, long? TotalBytes = null, double BytesPerSecond = 0, DateTimeOffset CreatedAt = default, DateTimeOffset UpdatedAt = default, string? Error = null, string? InstallCommand = null, string? InstallArguments = null, bool IsInstaller = false, bool DeleteAfterInstall = false, bool InstallLaunched = false, bool InstallCleanupCompleted = false, string PartialPath = "", string ETag = "", string LastModified = "", string AcceptRanges = "", long? ContentLength = null, string FinalUrl = "", bool ResumeSupported = false) { [JsonIgnore] public string EffectivePartialPath => string.IsNullOrWhiteSpace(PartialPath) ? TargetPath + ".partial" : PartialPath; public static DownloadItem Create( DownloadSource source, string targetPath, string? installCommand = null, string? installArguments = null, bool isInstaller = false, bool deleteAfterInstall = false) { var now = DateTimeOffset.UtcNow; return new DownloadItem( Guid.NewGuid().ToString("N"), source, targetPath, DownloadState.Queued, CreatedAt: now, UpdatedAt: now, InstallCommand: installCommand, InstallArguments: installArguments, IsInstaller: isInstaller, DeleteAfterInstall: deleteAfterInstall, PartialPath: targetPath + ".partial"); } public DownloadItem WithProgress(DownloadState state, long receivedBytes, long? totalBytes, double bytesPerSecond, string? error = null) { return this with { State = state, ReceivedBytes = Math.Max(0, receivedBytes), TotalBytes = totalBytes is > 0 ? totalBytes : null, BytesPerSecond = Math.Max(0, bytesPerSecond), UpdatedAt = DateTimeOffset.UtcNow, Error = error }; } public DownloadItem WithResumeMetadata( string? eTag = null, string? lastModified = null, string? acceptRanges = null, long? contentLength = null, string? finalUrl = null, bool? resumeSupported = null) { return this with { PartialPath = string.IsNullOrWhiteSpace(PartialPath) ? TargetPath + ".partial" : PartialPath, ETag = string.IsNullOrWhiteSpace(eTag) ? ETag : eTag, LastModified = string.IsNullOrWhiteSpace(lastModified) ? LastModified : lastModified, AcceptRanges = string.IsNullOrWhiteSpace(acceptRanges) ? AcceptRanges : acceptRanges, ContentLength = contentLength is > 0 ? contentLength : ContentLength, FinalUrl = string.IsNullOrWhiteSpace(finalUrl) ? FinalUrl : finalUrl, ResumeSupported = resumeSupported ?? ResumeSupported, UpdatedAt = DateTimeOffset.UtcNow }; } public DownloadItem WithInstallState(bool launched, bool cleanupCompleted = false) { return this with { InstallLaunched = launched, InstallCleanupCompleted = cleanupCompleted, UpdatedAt = DateTimeOffset.UtcNow }; } } public sealed record DownloadProgressSnapshot( string Id, DownloadState State, long ReceivedBytes, long? TotalBytes, double BytesPerSecond, string? Error = null, string ETag = "", string LastModified = "", string AcceptRanges = "", long? ContentLength = null, string FinalUrl = "", bool RestartedFromZero = false) { public double Progress { get { if (TotalBytes is not > 0) { return 0; } return Math.Clamp(ReceivedBytes / (double)TotalBytes.Value, 0, 1); } } } public static class DownloadFormat { public static string FormatBytes(long bytes) { string[] units = ["B", "KB", "MB", "GB", "TB"]; var value = Math.Max(0, bytes); var unit = 0; var scaled = (double)value; while (scaled >= 1024 && unit < units.Length - 1) { scaled /= 1024; unit++; } return unit == 0 ? $"{value} {units[unit]}" : $"{scaled:0.#} {units[unit]}"; } public static string FormatSpeed(double bytesPerSecond) { return $"{FormatBytes((long)Math.Max(0, bytesPerSecond))}/s"; } public static string FormatEta(long receivedBytes, long? totalBytes, double bytesPerSecond) { if (totalBytes is not > 0 || receivedBytes <= 0 || receivedBytes >= totalBytes.Value || bytesPerSecond <= 1) { return "-"; } var remaining = Math.Max(0, totalBytes.Value - receivedBytes); var eta = TimeSpan.FromSeconds(remaining / bytesPerSecond); if (eta.TotalHours >= 1) { return $"{(int)eta.TotalHours:0}h {eta.Minutes:00}m"; } return eta.TotalMinutes >= 1 ? $"{eta.Minutes:0}m {eta.Seconds:00}s" : $"{Math.Max(1, eta.Seconds):0}s"; } } public static class DownloadHostProtocol { public const string Version = "1"; public const string Ready = "ready"; public const string Start = "start"; public const string Pause = "pause"; public const string Resume = "resume"; public const string Cancel = "cancel"; public const string Progress = "progress"; public const string Completed = "completed"; public const string Failed = "failed"; public const string Paused = "paused"; public const string Canceled = "canceled"; public const string Shutdown = "shutdown"; public const string Ping = "ping"; public const string Pong = "pong"; private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { Converters = { new JsonStringEnumConverter() } }; public static string Serialize(DownloadHostMessage message) { return JsonSerializer.Serialize(message, JsonOptions); } public static DownloadHostMessage? Deserialize(string line) { return JsonSerializer.Deserialize(line, JsonOptions); } } public sealed record DownloadHostMessage( string Type, string RequestId = "", string Version = "", DownloadItem? Item = null, DownloadProgressSnapshot? Progress = null, string? Error = null);