From b74ebff068b630e77619c912d1d756d6b125909f Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sat, 30 May 2026 01:00:21 +0800 Subject: [PATCH 1/2] fix(Server): AppType filtering, WebRootPath, and new patch package - Use IWebHostEnvironment.WebRootPath instead of AppDomain.BaseDirectory to correctly locate wwwroot in both dev and publish scenarios - Filter patches by request.AppType to avoid returning mismatched updates - Remove hardcoded call counter to allow repeated test runs - Add patch_20260529221936.zip with matching SHA256 hash - Update csproj to copy patch_*.zip to output directory --- src/Server/Program.cs | 30 ++++++++++++------ src/Server/ServerSample.csproj | 5 +-- .../packet_20250102230201638_1.0.0.1.zip | Bin 338 -> 0 bytes .../wwwroot/packages/patch_20260529221936.zip | Bin 0 -> 722 bytes src/Server/wwwroot/packages/versions.json | 6 ++-- 5 files changed, 24 insertions(+), 17 deletions(-) delete mode 100644 src/Server/wwwroot/packages/packet_20250102230201638_1.0.0.1.zip create mode 100644 src/Server/wwwroot/packages/patch_20260529221936.zip diff --git a/src/Server/Program.cs b/src/Server/Program.cs index 433a8b0..348ade9 100644 --- a/src/Server/Program.cs +++ b/src/Server/Program.cs @@ -3,35 +3,45 @@ var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); -int count = 0; -string packageName = "packet_20250102230201638_1.0.0.1"; +string packageName = "patch_20260529221936"; app.MapPost("/Upgrade/Report", (ReportDTO request) => { return HttpResponseDTO.Success(true,"has update."); }); -app.MapPost("/Upgrade/Verification", (VerifyDTO request) => +app.MapPost("/Upgrade/Verification", (VerifyDTO request, IWebHostEnvironment env) => { - count++; - if (count > 2) + // Use WebRootPath to correctly locate wwwroot in both dev (project dir) and publish scenarios. + var filePath = Path.Combine(env.WebRootPath, "packages", $"{packageName}.zip"); + var packet = new FileInfo(filePath); + if (!packet.Exists) { - return HttpResponseDTO>.Success(Enumerable.Empty(), "Upgrade completed."); + return HttpResponseDTO>.InnerException( + $"Package file not found: {filePath}"); + } + + // Only return patches whose AppType matches the request. + // Client convention: Client=1, Upgrade=2 (matches GeneralUpdate.Core.AppType enum). + // This patch is a main application update (AppType=Client). + const int patchAppType = 2; // Client + if (request.AppType != null && request.AppType != patchAppType) + { + return HttpResponseDTO>.Success( + Enumerable.Empty(), "No matching update for this app type."); } - var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot", "packages", $"{packageName}.zip"); - var packet = new FileInfo(filePath); var result = new List { new VerificationResultDTO { RecordId = 1, Name = packageName, - Hash = "ad1a85a9169ca0083ab54ba390e085c56b9059efc3ca8aa1ec9ed857683cc4b1", + Hash = "0ad66de07179921e7ab5d2f38d39e92ca73969136d42536e0381b9f86082b4e5", ReleaseDate = DateTime.Now, Url = $"http://localhost:5000/packages/{packageName}.zip", Version = "1.0.0.1", - AppType = 1, + AppType = patchAppType, Platform = 1, ProductId = "2d974e2a-31e6-4887-9bb1-b4689e98c77a", IsForcibly = false, diff --git a/src/Server/ServerSample.csproj b/src/Server/ServerSample.csproj index 6f9e4c0..e36abdd 100644 --- a/src/Server/ServerSample.csproj +++ b/src/Server/ServerSample.csproj @@ -11,7 +11,7 @@ PreserveNewest - + PreserveNewest @@ -22,9 +22,6 @@ Always - - Always - 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 d8ad11680ae7be698069ce498df88661c6fcef4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 338 zcmWIWW@Zs#VBp|j=v!PHd4}=+YIz0*23a6h1Y+m>y!4{PlG2>SlFav9sdg`1!b6z{_#Q9*2lOCZOx*nc7o~#T3aMK%rrl;I* zi53T%Dg?x0K>Vz6?bB&18%1R%|ikN j9gb`qHn$k)D|@$3FVLB1&(t`}_iVY+ z`rz%0Gu@^=wQ8x6DNk5@;Fr>dAJUDl_bK*QEc@&9u=5MQ>b!rajyJY%*AGAmB+DGH zj#^;g%mHE^AkNR$E2${aD@ZI!&M4+8`&d$JYi;Q%5WtFcsDVKgb zA9wb^fp-rAPChuW;6Oq_L+Z~iVfFk1_UGmALuM_0s3=zdEsNP{=kYho{`2tg@Ch8X zRTA2*o++1=S+rC6AM?pGU#pWPB_u2)ZpdpGXqbI4SpC|z>iNbw-W{c{zF*~LKseKD zR4JPc$K=q7FA`G}=AE*fl5J(H?hM|WN n!X#Bj)G$NViOt~%oo9eLk-QP$&B_LnWCp?mK>80*nt=fTWgq|v literal 0 HcmV?d00001 diff --git a/src/Server/wwwroot/packages/versions.json b/src/Server/wwwroot/packages/versions.json index 4e1ffa7..7f66b02 100644 --- a/src/Server/wwwroot/packages/versions.json +++ b/src/Server/wwwroot/packages/versions.json @@ -1,9 +1,9 @@ [ { - "PacketName": "packet_20250102230201638_1.0.0.1", - "Hash": "ad1a85a9169ca0083ab54ba390e085c56b9059efc3ca8aa1ec9ed857683cc4b1", + "PacketName": "patch_20260529221936", + "Hash": "0ad66de07179921e7ab5d2f38d39e92ca73969136d42536e0381b9f86082b4e5", "Version": "1.0.0.1", - "Url": "http://localhost:5000/packages/packet_20250102230201638_1.0.0.1.zip", + "Url": "http://localhost:5000/packages/patch_20260529221936.zip", "PubTime": "2025-01-02T23:48:21" } ] \ No newline at end of file From ab0f5545edb637dc128b5d3885a203f3839f9fb9 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sun, 31 May 2026 19:11:11 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(Server):=20=E5=AE=8C=E5=96=84=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E6=9C=8D=E5=8A=A1=E5=99=A8=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=85=A8=E9=87=8F/=E5=B7=AE=E5=88=86=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ISSUE: src/Server 仅是一个硬编码模拟服务器,不支持: - versions.json 动态加载 - AppType/Platform/ProductId 过滤 - VersionChain / CrossVersion 升级模式 - 基于 SHA256 的文件下载(断点续传) - ClientTest/UpgradeTest 端到端升级流程 CHANGES: Server: 重写 Program.cs,从 versions.json 动态加载包元数据 新增 /Upgrade/ 和 /Update/ 双路由兼容 新增 GET /File/Download/{hash} 下载端点(支持 Range) 新增请求日志中间件 新增 8 个升级包(Client/Upgrade × 全量/差分 × 多版本) DTOs: VerifyDTO 新增 UpgradeMode 字段 VerificationResultDTO 新增 UrlExpireTimeUtc/UpgradeMode/ IsCrossVersion/FromVersion/ToVersion 字段 脚本: generate_packages.ps1 — 从 content 目录生成升级包 finalize_packages.ps1 — 整合差分/模拟包并生成 versions.json create_versions_json.ps1 — 重建 versions.json Content: 从 content_client/content_upgrade 移除 GeneralUpdate.*.dll, 避免升级包覆盖运行时 DLL 导致 IPC 解密失败 Co-Authored-By: Claude Opus 4.8 --- src/Server/DTOs/VerificationResultDTO.cs | 50 ++++- src/Server/DTOs/VerifyDTO.cs | 15 +- src/Server/Program.cs | 260 +++++++++++++++++++--- src/Server/ServerSample.csproj | 30 +-- src/Server/create_versions_json.ps1 | 69 ++++++ src/Server/finalize_packages.ps1 | 99 ++++++++ src/Server/generate_packages.ps1 | 238 ++++++++++++++++++++ src/Server/wwwroot/packages/versions.json | 75 ++++++- 8 files changed, 748 insertions(+), 88 deletions(-) create mode 100644 src/Server/create_versions_json.ps1 create mode 100644 src/Server/finalize_packages.ps1 create mode 100644 src/Server/generate_packages.ps1 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 348ade9..4dcb399 100644 --- a/src/Server/Program.cs +++ b/src/Server/Program.cs @@ -1,57 +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(); -string packageName = "patch_20260529221936"; +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, IWebHostEnvironment env) => +// ── Verification handler (shared across URL variants) ────────── +var handleVerification = (VerifyDTO request) => { - // Use WebRootPath to correctly locate wwwroot in both dev (project dir) and publish scenarios. - var filePath = Path.Combine(env.WebRootPath, "packages", $"{packageName}.zip"); - var packet = new FileInfo(filePath); - if (!packet.Exists) + 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>.InnerException( - $"Package file not found: {filePath}"); + 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 + }); } - // Only return patches whose AppType matches the request. - // Client convention: Client=1, Upgrade=2 (matches GeneralUpdate.Core.AppType enum). - // This patch is a main application update (AppType=Client). - const int patchAppType = 2; // Client - if (request.AppType != null && request.AppType != patchAppType) + 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)) { - return HttpResponseDTO>.Success( - Enumerable.Empty(), "No matching update for this app type."); + // Try finding by hash as filename + var candidates = Directory.GetFiles(Path.Combine(contentRoot, "packages"), "*.zip"); + var matched = candidates.FirstOrDefault(f => + { + 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 result = new List + 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(); + +// ── 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) { - new VerificationResultDTO + var zipPath = Path.Combine(packagesDir, $"{e.PacketName}.zip"); + if (File.Exists(zipPath)) { - RecordId = 1, - Name = packageName, - Hash = "0ad66de07179921e7ab5d2f38d39e92ca73969136d42536e0381b9f86082b4e5", - ReleaseDate = DateTime.Now, - Url = $"http://localhost:5000/packages/{packageName}.zip", - Version = "1.0.0.1", - AppType = patchAppType, - Platform = 1, - ProductId = "2d974e2a-31e6-4887-9bb1-b4689e98c77a", - IsForcibly = false, - Format = ".zip", - Size = packet.Length, - IsFreeze = false + 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 HttpResponseDTO>.Success(result,"has update."); -}); + } -app.UseStaticFiles(); -app.Run(); \ No newline at end of file + 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 e36abdd..c1962fc 100644 --- a/src/Server/ServerSample.csproj +++ b/src/Server/ServerSample.csproj @@ -1,44 +1,18 @@ - net8.0 + net10.0 enable enable - PreserveNewest - + PreserveNewest - - - - - 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/versions.json b/src/Server/wwwroot/packages/versions.json index 7f66b02..c9877cf 100644 --- a/src/Server/wwwroot/packages/versions.json +++ b/src/Server/wwwroot/packages/versions.json @@ -1,9 +1,70 @@ [ - { - "PacketName": "patch_20260529221936", - "Hash": "0ad66de07179921e7ab5d2f38d39e92ca73969136d42536e0381b9f86082b4e5", - "Version": "1.0.0.1", - "Url": "http://localhost:5000/packages/patch_20260529221936.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