259 lines
7.7 KiB
C#
259 lines
7.7 KiB
C#
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<DownloadState>() }
|
|
};
|
|
|
|
public static string Serialize(DownloadHostMessage message)
|
|
{
|
|
return JsonSerializer.Serialize(message, JsonOptions);
|
|
}
|
|
|
|
public static DownloadHostMessage? Deserialize(string line)
|
|
{
|
|
return JsonSerializer.Deserialize<DownloadHostMessage>(line, JsonOptions);
|
|
}
|
|
}
|
|
|
|
public sealed record DownloadHostMessage(
|
|
string Type,
|
|
string RequestId = "",
|
|
string Version = "",
|
|
DownloadItem? Item = null,
|
|
DownloadProgressSnapshot? Progress = null,
|
|
string? Error = null);
|