diff --git a/src/Server/DTOs/VerificationResultDTO.cs b/src/Server/DTOs/VerificationResultDTO.cs index 0c129ce..7362bdf 100644 --- a/src/Server/DTOs/VerificationResultDTO.cs +++ b/src/Server/DTOs/VerificationResultDTO.cs @@ -3,17 +3,17 @@ namespace ServerSample.DTOs; public record VerificationResultDTO { /// - /// 用于更新结果上报 + /// 记录 ID,用于更新结果上报 /// public int RecordId { get; set; } - + /// /// 包名称 /// public string? Name { get; set; } /// - /// 补丁包hash值 + /// 补丁包 SHA256 哈希值 /// public string? Hash { get; set; } @@ -27,23 +27,28 @@ public record VerificationResultDTO /// public string? Url { get; set; } + /// + /// 签名 URL 过期时间 (UTC) + /// + public DateTime? UrlExpireTimeUtc { get; set; } + /// /// 版本号 /// public string? Version { get; set; } /// - /// 应用类型 + /// 应用类型: 1=Client(主应用), 2=Upgrade(升级器) /// public int? AppType { get; set; } /// - /// 平台 + /// 系统平台: 1=Windows, 2=Linux, 3=macOS /// public int? Platform { get; set; } /// - /// 产品id + /// 产品 ID /// public string? ProductId { get; set; } @@ -51,16 +56,39 @@ public record VerificationResultDTO /// 是否强制更新 /// public bool? IsForcibly { get; set; } - + /// - /// 文件格式 + /// 文件格式(如 .zip) /// - public string Format { get; set; } = string.Empty; + public string? Format { get; set; } /// - /// 文件大小 + /// 文件大小(字节) /// public long? Size { get; set; } - + + /// + /// 是否冻结(冻结的包不参与更新) + /// public bool? IsFreeze { get; set; } + + /// + /// 升级模式: 1=VersionChain, 2=CrossVersion + /// + public int? UpgradeMode { get; set; } + + /// + /// 是否为跨版本升级包 + /// + public bool? IsCrossVersion { get; set; } + + /// + /// 跨版本升级的源版本号 + /// + public string? FromVersion { get; set; } + + /// + /// 跨版本升级的目标版本号 + /// + public string? ToVersion { get; set; } } \ No newline at end of file diff --git a/src/Server/DTOs/VerifyDTO.cs b/src/Server/DTOs/VerifyDTO.cs index 09826f2..6656707 100644 --- a/src/Server/DTOs/VerifyDTO.cs +++ b/src/Server/DTOs/VerifyDTO.cs @@ -3,27 +3,32 @@ namespace ServerSample.DTOs; public class VerifyDTO { /// - /// 版本号 + /// 客户端当前版本号 /// public string? Version { get; set; } /// - /// 应用类型 + /// 应用类型: 1=Client(主应用), 2=Upgrade(升级器) /// public int? AppType { get; set; } /// - /// 应用密钥 + /// 应用密钥,用于服务端验证 /// public string? AppKey { get; set; } /// - /// 系统平台 + /// 系统平台: 1=Windows, 2=Linux, 3=macOS /// public int? Platform { get; set; } /// - /// 所属产品分支id + /// 所属产品分支 ID /// public string? ProductId { get; set; } + + /// + /// 升级模式: 1=VersionChain(逐版本链式升级), 2=CrossVersion(跨版本升级) + /// + public int? UpgradeMode { get; set; } } \ No newline at end of file diff --git a/src/Server/Program.cs b/src/Server/Program.cs index 433a8b0..4dcb399 100644 --- a/src/Server/Program.cs +++ b/src/Server/Program.cs @@ -1,47 +1,243 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Text.Json; +using System.Web; using ServerSample.DTOs; +// ================================================================ +// GeneralUpdate Sample Server — Minimal API for update delivery +// +// Endpoints: +// POST /Upgrade/Verification — version validation & update discovery +// POST /Upgrade/Report — update status reporting +// GET /File/Download/{hash} — package download with Range support +// +// The server reads package metadata from wwwroot/packages/versions.json +// and serves the corresponding .zip files. +// ================================================================ + var builder = WebApplication.CreateBuilder(args); +builder.WebHost.UseUrls(builder.Configuration.GetValue("Urls") ?? "http://0.0.0.0:5000"); + var app = builder.Build(); -int count = 0; -string packageName = "packet_20250102230201638_1.0.0.1"; +var versionStore = LoadVersionStore(app.Environment); +var reportStore = new ConcurrentDictionary(); +var recordIdCounter = 0; + +var contentRoot = Path.Combine(app.Environment.ContentRootPath, "wwwroot"); +if (!Directory.Exists(contentRoot)) + Directory.CreateDirectory(contentRoot); -app.MapPost("/Upgrade/Report", (ReportDTO request) => +// ── Request logging ──────────────────────────────────────────── +app.Use(async (ctx, next) => { - return HttpResponseDTO.Success(true,"has update."); + var start = DateTime.UtcNow; + Console.WriteLine($"[{start:O}] {ctx.Request.Method} {ctx.Request.Path}{ctx.Request.QueryString}"); + await next(); + Console.WriteLine($"[{DateTime.UtcNow:O}] {ctx.Request.Method} {ctx.Request.Path} -> {ctx.Response.StatusCode} ({(DateTime.UtcNow - start).TotalMilliseconds:F0}ms)"); }); -app.MapPost("/Upgrade/Verification", (VerifyDTO request) => +// ── Verification handler (shared across URL variants) ────────── +var handleVerification = (VerifyDTO request) => { - count++; - if (count > 2) + Console.WriteLine($"[Verification] AppKey={request.AppKey}, Version={request.Version}, AppType={request.AppType}, Platform={request.Platform}, ProductId={request.ProductId}, UpgradeMode={request.UpgradeMode}"); + + if (!Version.TryParse(request.Version ?? "0.0.0.0", out var currentVersion)) + return Results.Ok(HttpResponseDTO>.Success( + Array.Empty(), "Invalid version format.")); + + var requestUpgradeMode = request.UpgradeMode ?? 1; + + var available = versionStore + .Where(v => + { + if (!Version.TryParse(v.Version, out var storeVersion)) return false; + if (storeVersion <= currentVersion) return false; + if (request.AppType.HasValue && v.AppType != request.AppType) return false; + if (request.Platform.HasValue && v.Platform != request.Platform) return false; + if (!string.IsNullOrEmpty(request.ProductId) && !string.IsNullOrEmpty(v.ProductId) && + !string.Equals(v.ProductId, request.ProductId, StringComparison.OrdinalIgnoreCase)) + return false; + if (requestUpgradeMode == 1 && v.IsCrossVersion == true) return false; + if (requestUpgradeMode == 2) + { + if (v.IsCrossVersion != true) return false; + if (!string.Equals(v.FromVersion, request.Version, StringComparison.OrdinalIgnoreCase)) + return false; + } + return true; + }) + .OrderByDescending(v => new Version(v.Version!)) + .ThenBy(v => v.IsCrossVersion == true ? 1 : 0) + .ToList(); + + if (available.Count == 0) + return Results.Ok(HttpResponseDTO>.Success( + Array.Empty(), "Already up to date.")); + + var results = new List(); + foreach (var v in available) { - return HttpResponseDTO>.Success(Enumerable.Empty(), "Upgrade completed."); + var recordId = Interlocked.Increment(ref recordIdCounter); + var downloadUrl = $"{GetBaseUrl(app)}File/Download/{HttpUtility.UrlEncode(v.Hash!)}"; + results.Add(new VerificationResultDTO + { + RecordId = recordId, Name = v.PacketName, Hash = v.Hash, + ReleaseDate = v.PubTime, Url = downloadUrl, + UrlExpireTimeUtc = DateTime.UtcNow.AddHours(24), + Version = v.Version, AppType = v.AppType, Platform = v.Platform, + ProductId = v.ProductId, IsForcibly = v.IsForcibly, + Format = v.Format ?? ".zip", Size = v.Size, IsFreeze = v.IsFreeze, + UpgradeMode = requestUpgradeMode, + IsCrossVersion = v.IsCrossVersion ?? false, + FromVersion = v.FromVersion, ToVersion = v.ToVersion + }); } - var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot", "packages", $"{packageName}.zip"); - var packet = new FileInfo(filePath); - var result = new List + var modeLabel = requestUpgradeMode == 2 ? "CrossVersion" : "VersionChain"; + Console.WriteLine($"[Verification] Returning {results.Count} packages (Mode: {modeLabel})"); + foreach (var r in results) + Console.WriteLine($" {r.Version} — {r.Name} ({(r.IsCrossVersion == true ? $"Cross {r.FromVersion}→{r.ToVersion}" : "Full")})"); + + return Results.Ok(HttpResponseDTO>.Success(results, + $"Found {results.Count} update(s).")); +}; + +// Register both URL variants (different clients use different paths) +app.MapPost("/Upgrade/Verification", handleVerification); +app.MapPost("/Update/Verification", handleVerification); + +// ── Report handler (shared across URL variants) ───────────────── +var handleReport = (ReportDTO request) => +{ + reportStore[request.RecordId] = request; + var statusText = request.Status switch { 1 => "updating", 2 => "success", 3 => "failed", _ => "unknown" }; + var typeText = request.Type switch { 1 => "upgrade", 2 => "push", _ => "unknown" }; + Console.WriteLine($"[Report] Record {request.RecordId} -> {statusText} (type={typeText})"); + return Results.Ok(HttpResponseDTO.Success(true, $"Report accepted: {statusText}.")); +}; + +app.MapPost("/Update/Report", handleReport); + +// ── GET /File/Download/{hash} ─────────────────────────────────── +// Serves package files by their SHA256 hash. Supports HTTP Range +// requests for download resume. +app.MapGet("/File/Download/{hash}", (string hash, HttpContext http) => +{ + var entry = versionStore.FirstOrDefault(v => + string.Equals(v.Hash, hash, StringComparison.OrdinalIgnoreCase)); + + var fileName = entry != null ? $"{entry.PacketName}.zip" : $"{hash}.zip"; + var filePath = Path.Combine(contentRoot, "packages", fileName); + + if (!File.Exists(filePath)) { - new VerificationResultDTO + // Try finding by hash as filename + var candidates = Directory.GetFiles(Path.Combine(contentRoot, "packages"), "*.zip"); + var matched = candidates.FirstOrDefault(f => { - RecordId = 1, - Name = packageName, - Hash = "ad1a85a9169ca0083ab54ba390e085c56b9059efc3ca8aa1ec9ed857683cc4b1", - ReleaseDate = DateTime.Now, - Url = $"http://localhost:5000/packages/{packageName}.zip", - Version = "1.0.0.1", - AppType = 1, - Platform = 1, - ProductId = "2d974e2a-31e6-4887-9bb1-b4689e98c77a", - IsForcibly = false, - Format = ".zip", - Size = packet.Length, - IsFreeze = false - } - }; - return HttpResponseDTO>.Success(result,"has update."); + using var sha = SHA256.Create(); + using var fs = File.OpenRead(f); + var computed = Convert.ToHexStringLower(sha.ComputeHash(fs)); + return string.Equals(computed, hash, StringComparison.OrdinalIgnoreCase); + }); + if (matched != null) filePath = matched; + else + return Results.NotFound(new { Code = 404, Message = $"File not found for hash: {hash}" }); + } + + var fileInfo = new FileInfo(filePath); + Console.WriteLine($"[Download] {hash}: {fileInfo.Name} ({fileInfo.Length} bytes)"); + return Results.File(filePath, contentType: "application/octet-stream", + enableRangeProcessing: true, fileDownloadName: Path.GetFileName(filePath)); }); +// ── Static files fallback ────────────────────────────────────── app.UseStaticFiles(); -app.Run(); \ No newline at end of file + +// ── Startup banner ───────────────────────────────────────────── +Console.WriteLine("╔══════════════════════════════════════════════════╗"); +Console.WriteLine("║ GeneralUpdate Sample Upgrade Server ║"); +Console.WriteLine("╠══════════════════════════════════════════════════╣"); +Console.WriteLine($"║ Verification: {GetBaseUrl(app)}Upgrade/Verification".PadRight(51) + "║"); +Console.WriteLine($"║ Report: {GetBaseUrl(app)}Upgrade/Report".PadRight(51) + "║"); +Console.WriteLine($"║ Download: {GetBaseUrl(app)}File/Download/{{hash}}".PadRight(51) + "║"); +Console.WriteLine($"║ Packages: {versionStore.Count} version(s) loaded".PadRight(51) + "║"); +Console.WriteLine("╚══════════════════════════════════════════════════╝"); +if (versionStore.Count > 0) +{ + foreach (var v in versionStore.OrderBy(v => new Version(v.Version!))) + Console.WriteLine($" {v.Version,-12} AppType={v.AppType} {(v.IsCrossVersion == true ? $"Cross {v.FromVersion}→{v.ToVersion}" : "Full")} {v.PacketName}"); +} +else +{ + Console.WriteLine(" [Warn] No packages loaded. Run generate_packages.ps1 to create sample packages."); +} + +app.Run(); + +// ═══════════════════════════════════════════════════════════════════ +// Helpers +// ═══════════════════════════════════════════════════════════════════ + +static string GetBaseUrl(IHost host) +{ + var config = host.Services.GetRequiredService(); + var url = config.GetValue("BaseUrl"); + if (!string.IsNullOrEmpty(url)) return url.EndsWith('/') ? url : url + "/"; + return "http://localhost:5000/"; +} + +static List LoadVersionStore(IWebHostEnvironment env) +{ + var jsonPath = Path.Combine(env.ContentRootPath, "wwwroot", "packages", "versions.json"); + if (!File.Exists(jsonPath)) + { + Console.WriteLine($"[Warn] versions.json not found at {jsonPath}, using empty store."); + return new List(); + } + + var json = File.ReadAllText(jsonPath); + var entries = JsonSerializer.Deserialize>(json) ?? new List(); + + var packagesDir = Path.Combine(env.ContentRootPath, "wwwroot", "packages"); + foreach (var e in entries) + { + var zipPath = Path.Combine(packagesDir, $"{e.PacketName}.zip"); + if (File.Exists(zipPath)) + { + e.Size ??= new FileInfo(zipPath).Length; + if (string.IsNullOrEmpty(e.Hash)) + { + using var sha256 = SHA256.Create(); + using var stream = File.OpenRead(zipPath); + e.Hash = Convert.ToHexStringLower(sha256.ComputeHash(stream)); + } + } + } + + return entries; +} + +// ═══════════════════════════════════════════════════════════════════ +// Internal model for versions.json deserialization +// ═══════════════════════════════════════════════════════════════════ +record VersionEntry +{ + public string? PacketName { get; set; } + public string? Hash { get; set; } + public string? Version { get; set; } + public string? Url { get; set; } + public DateTime? PubTime { get; set; } + public int? AppType { get; set; } + public int? Platform { get; set; } + public string? ProductId { get; set; } + public bool? IsForcibly { get; set; } + public string? Format { get; set; } + public long? Size { get; set; } + public bool? IsFreeze { get; set; } + public bool? IsCrossVersion { get; set; } + public string? FromVersion { get; set; } + public string? ToVersion { get; set; } +} diff --git a/src/Server/ServerSample.csproj b/src/Server/ServerSample.csproj index 6f9e4c0..c1962fc 100644 --- a/src/Server/ServerSample.csproj +++ b/src/Server/ServerSample.csproj @@ -1,47 +1,18 @@ - net8.0 + net10.0 enable enable - PreserveNewest - + PreserveNewest - - - - - Always - - - Always - - - - - - - - - - - - - <_ContentIncludedByDefault Remove="ServerSample\obj\project.assets.json" /> - <_ContentIncludedByDefault Remove="ServerSample\obj\ServerSample.csproj.nuget.dgspec.json" /> - - - - - - diff --git a/src/Server/create_versions_json.ps1 b/src/Server/create_versions_json.ps1 new file mode 100644 index 0000000..f1755b2 --- /dev/null +++ b/src/Server/create_versions_json.ps1 @@ -0,0 +1,69 @@ +# Creates versions.json from the .zip files in wwwroot/packages +$packagesDir = Join-Path $PSScriptRoot "wwwroot\packages" +$baseUrl = "http://localhost:5000/" +$productId = "2d974e2a-31e6-4887-9bb1-b4689e98c77a" + +# Map filenames to metadata +$packages = @( + @{Name="packet_*_full_client_2.0.0.0.zip"; Ver="2.0.0.0"; AppType=1; IsCross=$false; From=$null; To=$null}, + @{Name="packet_*_full_client_1.0.0.2.zip"; Ver="1.0.0.2"; AppType=1; IsCross=$false; From=$null; To=$null}, + @{Name="packet_*_full_client_1.0.0.1.zip"; Ver="1.0.0.1"; AppType=1; IsCross=$false; From=$null; To=$null}, + @{Name="packet_*_full_upgrade_2.0.0.0.zip"; Ver="2.0.0.0"; AppType=2; IsCross=$false; From=$null; To=$null}, + @{Name="packet_*_full_upgrade_1.0.0.2.zip"; Ver="1.0.0.2"; AppType=2; IsCross=$false; From=$null; To=$null}, + @{Name="packet_*_full_upgrade_1.0.0.1.zip"; Ver="1.0.0.1"; AppType=2; IsCross=$false; From=$null; To=$null}, + @{Name="packet_*_cross_client_*.zip"; Ver="2.0.0.0"; AppType=1; IsCross=$true; From="1.0.0"; To="2.0.0.0"}, + @{Name="packet_*_cross_upgrade_*.zip"; Ver="2.0.0.0"; AppType=2; IsCross=$true; From="1.0.0.1"; To="2.0.0.0"} +) + +$entries = @() +foreach ($p in $packages) { + $pattern = $p.Name + $files = Get-ChildItem -Path $packagesDir -Filter $pattern -ErrorAction SilentlyContinue + foreach ($file in $files) { + # Skip non-.zip or the old patch_*.zip + if ($file.Extension -ne ".zip") { continue } + if ($file.Name -like "patch_*") { continue } + + $hash = (Get-FileHash -Path $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + $size = $file.Length + $name = [System.IO.Path]::GetFileNameWithoutExtension($file.Name) + + # Extract FromVersion/ToVersion from cross-version filenames + $fromVer = $p.From + $toVer = $p.To + if ($p.IsCross -and $file.Name -match "_([^_]+)_to_") { + $fromVer = $Matches[1] + } + + $entry = [PSCustomObject]@{ + PacketName = $name + Hash = $hash + Version = $p.Ver + Url = "$baseUrl`File/Download/$hash" + PubTime = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ss") + AppType = $p.AppType + Platform = 1 + ProductId = $productId + IsForcibly = $false + Format = ".zip" + Size = $size + IsFreeze = $false + IsCrossVersion = $p.IsCross + FromVersion = $fromVer + ToVersion = $toVer + } + $entries += $entry + $crossLabel = if ($p.IsCross) { "Cross $fromVer -> $toVer" } else { "Full" } + Write-Host " AppType=$($p.AppType) v$($p.Ver) [$crossLabel] $($file.Name) ($size bytes)" + } +} + +if ($entries.Count -eq 0) { + Write-Host "[Error] No .zip packages found in $packagesDir" -ForegroundColor Red + exit 1 +} + +$json = ConvertTo-Json -InputObject @($entries) -Depth 5 +$versionsPath = Join-Path $packagesDir "versions.json" +[System.IO.File]::WriteAllText($versionsPath, $json, [System.Text.UTF8Encoding]::new($false)) +Write-Host "`nGenerated $($entries.Count) entries -> $versionsPath" -ForegroundColor Green diff --git a/src/Server/finalize_packages.ps1 b/src/Server/finalize_packages.ps1 new file mode 100644 index 0000000..d54d32b --- /dev/null +++ b/src/Server/finalize_packages.ps1 @@ -0,0 +1,99 @@ +# Finalize all upgrade packages in wwwroot/packages +# - Adds mock content packages alongside PatchGenerator output +# - Generates a single comprehensive versions.json + +Add-Type -AssemblyName System.IO.Compression.FileSystem +$packagesDir = Join-Path $PSScriptRoot "wwwroot\packages" +$contentDir = Join-Path (Split-Path -Parent $PSScriptRoot) "content" +$baseUrl = "http://localhost:5000/" +$productId = "2d974e2a-31e6-4887-9bb1-b4689e98c77a" +$timestamp = Get-Date -Format "yyyyMMddHHmmssfff" + +function Get-Hash($path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + $sha = [System.Security.Cryptography.SHA256]::Create() + return [System.BitConverter]::ToString($sha.ComputeHash($bytes)).Replace("-", "").ToLowerInvariant() +} + +# Step 1: Add mock content packages (don't delete existing ones!) +Write-Host "=== Adding mock content packages ===" -ForegroundColor Cyan +foreach ($ver in @("1.0.0.1", "1.0.0.2")) { + $srcPath = Join-Path $contentDir "v$ver" + if (-not (Test-Path $srcPath)) { continue } + foreach ($appType in @(1, 2)) { + $label = if ($appType -eq 1) { "client" } else { "upgrade" } + $pkgName = "packet_${timestamp}_full_${label}_$ver" + $zipPath = Join-Path $packagesDir "$pkgName.zip" + if (-not (Test-Path $zipPath)) { + [System.IO.Compression.ZipFile]::CreateFromDirectory($srcPath, $zipPath, + [System.IO.Compression.CompressionLevel]::Optimal, $false) + } + Write-Host " $pkgName.zip" + } +} + +# Step 2: Build comprehensive versions.json from ALL packet_*.zip files +Write-Host "" +Write-Host "=== Building versions.json ===" -ForegroundColor Cyan +$zipFiles = Get-ChildItem -Path $packagesDir -Filter "packet_*.zip" | Sort-Object Name +$entries = @() + +foreach ($f in $zipFiles) { + $hash = Get-Hash $f.FullName + $size = $f.Length + $name = [System.IO.Path]::GetFileNameWithoutExtension($f.Name) + + # Determine if cross-version + $isCross = $f.Name -match "_cross_" + + # Determine AppType + $appType = if ($f.Name -match "_client_") { 1 } else { 2 } + + # Extract version from filename + $verMatch = [regex]::Match($f.Name, "_(\d+\.\d+\.\d+\.\d+)\.zip$") + if (-not $verMatch.Success) { + $verMatch = [regex]::Match($f.Name, "_(\d+\.\d+\.\d+)\.zip$") + } + $version = if ($verMatch.Success) { $verMatch.Groups[1].Value } else { "0.0.0.0" } + + # Extract cross-version range + $fromVer = $null + $toVer = $null + if ($isCross) { + $crossMatch = [regex]::Match($f.Name, "_cross_.*?_(\d+\.\d+\.\d+\.\d+|\d+\.\d+\.\d+)_to_(\d+\.\d+\.\d+\.\d+|\d+\.\d+\.\d+)\.zip$") + if (-not $crossMatch.Success) { + $crossMatch = [regex]::Match($f.Name, "_cross_.*?_(\d+\.\d+\.\d+)_to_(\d+\.\d+\.\d+)\.zip$") + } + if ($crossMatch.Success) { + $fromVer = $crossMatch.Groups[1].Value + $toVer = $crossMatch.Groups[2].Value + } + } + + $entry = [PSCustomObject]@{ + PacketName = $name + Hash = $hash + Version = $version + Url = "$baseUrl" + "File/Download/$hash" + PubTime = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ss") + AppType = $appType + Platform = 1 + ProductId = $productId + IsForcibly = $false + Format = ".zip" + Size = $size + IsFreeze = $false + IsCrossVersion = $isCross + FromVersion = $fromVer + ToVersion = $toVer + } + $entries += $entry + $crossLabel = if ($isCross) { "Cross $fromVer -> $toVer" } else { "Full" } + Write-Host " AppType=$appType v$version [$crossLabel] $($f.Name) ($size bytes)" +} + +$json = ConvertTo-Json -InputObject @($entries) -Depth 5 +$versionsPath = Join-Path $packagesDir "versions.json" +[System.IO.File]::WriteAllText($versionsPath, $json, [System.Text.UTF8Encoding]::new($false)) +Write-Host "" +Write-Host "Generated $($entries.Count) entries -> $versionsPath" -ForegroundColor Green diff --git a/src/Server/generate_packages.ps1 b/src/Server/generate_packages.ps1 new file mode 100644 index 0000000..037854e --- /dev/null +++ b/src/Server/generate_packages.ps1 @@ -0,0 +1,238 @@ +# ================================================================ +# GeneralUpdate Sample Server — Upgrade Package Generator +# +# Generates full (VersionChain) and differential (CrossVersion) +# upgrade packages from the sample content directories. +# +# Output: wwwroot/packages/*.zip + wwwroot/packages/versions.json +# +# For differential packages, this script invokes the PatchGenerator +# C# project when available; otherwise generates only full packages. +# ================================================================ +param( + [switch]$FullOnly # Skip differential packages (fast mode) +) + +$ErrorActionPreference = "Stop" + +$srcDir = Split-Path -Parent $PSScriptRoot +$contentClientDir = Join-Path $srcDir "content_client" +$contentUpgradeDir = Join-Path $srcDir "content_upgrade" +$packagesDir = Join-Path $PSScriptRoot "wwwroot\packages" +$timestamp = Get-Date -Format "yyyyMMddHHmmssfff" +$baseUrl = "http://localhost:5000/" +$productId = "2d974e2a-31e6-4887-9bb1-b4689e98c77a" + +# Ensure output directory exists +New-Item -ItemType Directory -Force -Path $packagesDir | Out-Null + +# Clean old generated packages +Get-ChildItem -Path $packagesDir -Filter "packet_*.zip" | Remove-Item -Force +Write-Host "Cleaned old packages." -ForegroundColor Cyan + +$allVersions = @() + +# =================================================================== +# Helper functions +# =================================================================== + +function Get-SHA256Hash($filePath) { + $stream = [System.IO.File]::OpenRead($filePath) + $sha = [System.Security.Cryptography.SHA256]::Create() + $hash = [System.BitConverter]::ToString($sha.ComputeHash($stream)).Replace("-", "").ToLowerInvariant() + $stream.Close() + return $hash +} + +function New-VersionEntry { + param( + [string]$PacketName, [string]$Version, [int]$AppType, + [int]$Platform = 1, [string]$ProductId, + [bool]$IsForcibly = $false, [string]$Format = ".zip", + [bool]$IsCrossVersion = $false, + [string]$FromVersion = $null, [string]$ToVersion = $null, + [bool]$IsFreeze = $false + ) + $zipPath = Join-Path $packagesDir "$PacketName.zip" + $hash = Get-SHA256Hash $zipPath + $size = (Get-Item $zipPath).Length + + return @{ + PacketName = $PacketName + Hash = $hash + Version = $Version + Url = "$baseUrl`File/Download/$hash" + PubTime = (Get-Date).ToString("o") + AppType = $AppType + Platform = $Platform + ProductId = $ProductId + IsForcibly = $IsForcibly + Format = $Format + Size = $size + IsFreeze = $IsFreeze + IsCrossVersion = $(if ($IsCrossVersion) { $true } else { $false }) + FromVersion = $FromVersion + ToVersion = $ToVersion + } +} + +function Add-ZipPackage { + param( + [string]$SourceDir, [string]$PacketName, + [string]$Version, [int]$AppType, + [bool]$IsCrossVersion = $false, + [string]$FromVersion = $null, [string]$ToVersion = $null + ) + $zipPath = Join-Path $packagesDir "$PacketName.zip" + Write-Host " Creating: $PacketName.zip" -ForegroundColor Yellow + + if (Test-Path $zipPath) { Remove-Item $zipPath -Force } + + # Use .NET for compression (available in PowerShell 5+) + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::CreateFromDirectory( + $SourceDir, $zipPath, + [System.IO.Compression.CompressionLevel]::Optimal, + $false # includeBaseDirectory = false + ) + + $size = (Get-Item $zipPath).Length + Write-Host " Size: $size bytes" -ForegroundColor Gray + + $entry = New-VersionEntry -PacketName $PacketName -Version $Version ` + -AppType $AppType -ProductId $productId ` + -IsCrossVersion $IsCrossVersion -FromVersion $FromVersion -ToVersion $ToVersion + $script:allVersions += $entry +} + +# =================================================================== +# CLIENT packages (AppType=1) +# Current version: 1.0.0 → Target version: 2.0.0.0 +# =================================================================== +Write-Host "" +Write-Host "===== CLIENT Packages (AppType=1) =====" -ForegroundColor Green + +$clientV1 = Join-Path $contentClientDir "v1.0.0.0" +$clientV2 = Join-Path $contentClientDir "v2.0.0.0" + +if ((Test-Path $clientV2) -and (Test-Path $clientV1)) { + # Full (VersionChain) package + $pkgName = "packet_${timestamp}_full_client_2.0.0.0" + Add-ZipPackage -SourceDir $clientV2 -PacketName $pkgName ` + -Version "2.0.0.0" -AppType 1 -IsCrossVersion $false + + if (-not $FullOnly) { + # Differential (CrossVersion) package via PatchGenerator + Write-Host " [CrossVersion] Generating differential patches via PatchGenerator..." -ForegroundColor Yellow + $patchGeneratorProj = Join-Path $srcDir "PatchGenerator\PatchGenerator.csproj" + if (Test-Path $patchGeneratorProj) { + Push-Location (Join-Path $srcDir "PatchGenerator") + try { + dotnet run --project $patchGeneratorProj + Write-Host " PatchGenerator completed." -ForegroundColor Gray + } + catch { + Write-Host " [Warn] PatchGenerator failed: $_" -ForegroundColor Magenta + Write-Host " Run 'dotnet run --project $patchGeneratorProj' manually for differential packages." -ForegroundColor Magenta + } + finally { Pop-Location } + + # PatchGenerator writes directly to Server/wwwroot/packages/ + # Also write its output to versions.json + $patchVersionsJson = Join-Path $packagesDir "versions.json" + if (Test-Path $patchVersionsJson) { + $patchEntries = Get-Content $patchVersionsJson -Raw | ConvertFrom-Json + foreach ($pe in $patchEntries) { + $script:allVersions += @{ + PacketName = $pe.PacketName + Hash = $pe.Hash + Version = $pe.Version + Url = $pe.Url + PubTime = $pe.PubTime + AppType = $pe.AppType + Platform = $pe.Platform + ProductId = $pe.ProductId + IsForcibly = $pe.IsForcibly + Format = $pe.Format + Size = $pe.Size + IsFreeze = $pe.IsFreeze + IsCrossVersion = $(if ($pe.IsCrossVersion) { $true } else { $false }) + FromVersion = $pe.FromVersion + ToVersion = $pe.ToVersion + } + } + } + } + else { + Write-Host " [Skip] PatchGenerator project not found at $patchGeneratorProj" -ForegroundColor Yellow + } + } +} +else { + Write-Host " [Skip] content_client directories not found: $clientV1 or $clientV2" -ForegroundColor Yellow +} + +# =================================================================== +# UPGRADE packages (AppType=2) +# Current version: 1.0.0.1 → Target version: 2.0.0.0 +# =================================================================== +Write-Host "" +Write-Host "===== UPGRADE Packages (AppType=2) =====" -ForegroundColor Green + +$upgradeV1 = Join-Path $contentUpgradeDir "v1.0.0.0" +$upgradeV2 = Join-Path $contentUpgradeDir "v2.0.0.0" + +if ((Test-Path $upgradeV2) -and (Test-Path $upgradeV1)) { + # Full (VersionChain) package + $pkgName = "packet_${timestamp}_full_upgrade_2.0.0.0" + Add-ZipPackage -SourceDir $upgradeV2 -PacketName $pkgName ` + -Version "2.0.0.0" -AppType 2 -IsCrossVersion $false +} +else { + Write-Host " [Skip] content_upgrade directories not found: $upgradeV1 or $upgradeV2" -ForegroundColor Yellow +} + +# =================================================================== +# Also generate smaller mock packages from content/ dirs +# (for quick testing without large binary packages) +# =================================================================== +Write-Host "" +Write-Host "===== MOCK Content Packages =====" -ForegroundColor Green + +$contentDir = Join-Path $srcDir "content" + +foreach ($ver in @("1.0.0.1", "1.0.0.2")) { + $srcPath = Join-Path $contentDir "v$ver" + if (Test-Path $srcPath) { + # Client full package + $pkgName = "packet_${timestamp}_full_client_$ver" + Add-ZipPackage -SourceDir $srcPath -PacketName $pkgName ` + -Version $ver -AppType 1 -IsCrossVersion $false + + # Upgrade full package + $pkgName = "packet_${timestamp}_full_upgrade_$ver" + Add-ZipPackage -SourceDir $srcPath -PacketName $pkgName ` + -Version $ver -AppType 2 -IsCrossVersion $false + } +} + +# =================================================================== +# Write versions.json +# =================================================================== +# Deduplicate entries (PatchGenerator may have added some already) +$uniqueVersions = $allVersions | Sort-Object PacketName -Unique +$versionsJsonPath = Join-Path $packagesDir "versions.json" +$json = $uniqueVersions | ConvertTo-Json -Depth 10 +[System.IO.File]::WriteAllText($versionsJsonPath, $json, [System.Text.Encoding]::UTF8) + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Generated $($uniqueVersions.Count) package(s)" -ForegroundColor Cyan +Write-Host " Output: $packagesDir" -ForegroundColor Cyan +Write-Host " Metadata: $versionsJsonPath" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan + +foreach ($v in $uniqueVersions) { + $cross = if ($v.IsCrossVersion) { "Cross $($v.FromVersion) → $($v.ToVersion)" } else { "Full" } + Write-Host " AppType=$($v.AppType) v$($v.Version) [$cross] $($v.PacketName)" -ForegroundColor Gray +} diff --git a/src/Server/wwwroot/packages/packet_20250102230201638_1.0.0.1.zip b/src/Server/wwwroot/packages/packet_20250102230201638_1.0.0.1.zip deleted file mode 100644 index d8ad116..0000000 Binary files a/src/Server/wwwroot/packages/packet_20250102230201638_1.0.0.1.zip and /dev/null differ diff --git a/src/Server/wwwroot/packages/patch_20260529221936.zip b/src/Server/wwwroot/packages/patch_20260529221936.zip new file mode 100644 index 0000000..09f9342 Binary files /dev/null and b/src/Server/wwwroot/packages/patch_20260529221936.zip differ diff --git a/src/Server/wwwroot/packages/versions.json b/src/Server/wwwroot/packages/versions.json index 4e1ffa7..c9877cf 100644 --- a/src/Server/wwwroot/packages/versions.json +++ b/src/Server/wwwroot/packages/versions.json @@ -1,9 +1,70 @@ [ - { - "PacketName": "packet_20250102230201638_1.0.0.1", - "Hash": "ad1a85a9169ca0083ab54ba390e085c56b9059efc3ca8aa1ec9ed857683cc4b1", - "Version": "1.0.0.1", - "Url": "http://localhost:5000/packages/packet_20250102230201638_1.0.0.1.zip", - "PubTime": "2025-01-02T23:48:21" - } + { + "PacketName": "packet_20260531180913400_full_client_2.0.0.0", + "Hash": "eeb4ce8c90a8da765a1a9e00e6d02a258f368654a621b7d1991b02cdd5f27090", + "Version": "2.0.0.0", + "Url": "http://localhost:5000/File/Download/eeb4ce8c90a8da765a1a9e00e6d02a258f368654a621b7d1991b02cdd5f27090", + "PubTime": "2026-05-31T18:09:18", + "AppType": 1, + "Platform": 1, + "ProductId": "2d974e2a-31e6-4887-9bb1-b4689e98c77a", + "IsForcibly": false, + "Format": ".zip", + "Size": 516566, + "IsFreeze": false, + "IsCrossVersion": false, + "FromVersion": null, + "ToVersion": null + }, + { + "PacketName": "packet_20260531180913400_full_upgrade_2.0.0.0", + "Hash": "8f4ff42d68804f9a105b5c3cd59ee859ce8f8d2c32e60b38567984d6ae8ae893", + "Version": "2.0.0.0", + "Url": "http://localhost:5000/File/Download/8f4ff42d68804f9a105b5c3cd59ee859ce8f8d2c32e60b38567984d6ae8ae893", + "PubTime": "2026-05-31T18:09:18", + "AppType": 2, + "Platform": 1, + "ProductId": "2d974e2a-31e6-4887-9bb1-b4689e98c77a", + "IsForcibly": false, + "Format": ".zip", + "Size": 512475, + "IsFreeze": false, + "IsCrossVersion": false, + "FromVersion": null, + "ToVersion": null + }, + { + "PacketName": "packet_20260531180913400_cross_client_1.0.0.0_to_2.0.0.0", + "Hash": "18a66651e76689894d0c4053da0c710584d319b0e374fddf52e6755b6f8ee524", + "Version": "2.0.0.0", + "Url": "http://localhost:5000/File/Download/18a66651e76689894d0c4053da0c710584d319b0e374fddf52e6755b6f8ee524", + "PubTime": "2026-05-31T18:09:18", + "AppType": 1, + "Platform": 1, + "ProductId": "2d974e2a-31e6-4887-9bb1-b4689e98c77a", + "IsForcibly": false, + "Format": ".zip", + "Size": 84828, + "IsFreeze": false, + "IsCrossVersion": true, + "FromVersion": "1.0.0.0", + "ToVersion": "2.0.0.0" + }, + { + "PacketName": "packet_20260531180913400_cross_upgrade_1.0.0.0_to_2.0.0.0", + "Hash": "fb24b5065bd54f88fd3a131cecc34eef6f58d95cd20da180de12fe297f2a94d7", + "Version": "2.0.0.0", + "Url": "http://localhost:5000/File/Download/fb24b5065bd54f88fd3a131cecc34eef6f58d95cd20da180de12fe297f2a94d7", + "PubTime": "2026-05-31T18:09:18", + "AppType": 2, + "Platform": 1, + "ProductId": "2d974e2a-31e6-4887-9bb1-b4689e98c77a", + "IsForcibly": false, + "Format": ".zip", + "Size": 80735, + "IsFreeze": false, + "IsCrossVersion": true, + "FromVersion": "1.0.0.0", + "ToVersion": "2.0.0.0" + } ] \ No newline at end of file