Files
2026-07-10 10:24:29 +08:00

169 lines
7.2 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
namespace XGame.MiniGame
{
// 下载并校验某小游戏版本的全部文件;产出本地目录 + 解析好的 manifest
public sealed class MiniGameDownloader
{
public string LocalDir { get; private set; }
public MiniGameManifest Manifest { get; private set; }
public string Error { get; private set; }
private readonly string _cdnBase; // 例如 http://.../minigame/
private readonly string _gameId;
private readonly int _version;
public MiniGameDownloader(string cdnBase, string gameId, int version)
{
_cdnBase = NormalizeCdnBase(cdnBase);
_gameId = gameId; _version = version;
LocalDir = $"{Application.persistentDataPath}/minigame/{gameId}/{version}/";
}
private string RemoteBase => $"{_cdnBase}{_gameId}/{_version}/{MiniGamePlatform.Name}/";
public IEnumerator Run(Action<bool> onDone)
{
Error = null;
Directory.CreateDirectory(LocalDir);
// 1) game.json(每次拉取,文件小)
string gameJson = null;
yield return GetText($"{RemoteBase}game.json", t => gameJson = t);
if (gameJson == null) { Fail("下载 game.json 失败"); onDone(false); yield break; }
try { Manifest = MiniGameManifest.ParseGameJson(gameJson); }
catch (Exception e) { Fail("game.json 解析失败: " + e.Message); onDone(false); yield break; }
if (Manifest.GameId != _gameId || Manifest.Version != _version)
{ Fail("game.json 与请求的 gameId/version 不符"); onDone(false); yield break; }
if (Manifest.MinFrameworkVersion > XWorld.Framework.FrameworkInfo.Version)
{ Fail($"小游戏要求最小框架版本 {Manifest.MinFrameworkVersion},当前框架 {XWorld.Framework.FrameworkInfo.Version}"); onDone(false); yield break; }
// 2) files.txtmd5 清单)
string filesTxt = null;
yield return GetText($"{RemoteBase}files.txt", t => filesTxt = t);
if (filesTxt == null) { Fail("下载 files.txt 失败"); onDone(false); yield break; }
Manifest.LoadFilesList(filesTxt);
// 验签(设计 §7):下载 files.txt.sig,校验清单签名后才信任 md5 列表
byte[] sig = null;
yield return GetBytes($"{RemoteBase}files.txt.sig", b => sig = b);
if (ClientManifestVerifier.IsConfigured)
{
if (sig == null) { Fail("缺少签名文件 files.txt.sig"); onDone(false); yield break; }
if (!ClientManifestVerifier.Verify(Manifest.Digest(), sig))
{ Fail("清单签名验证失败,拒绝加载"); onDone(false); yield break; }
}
// 3) 按清单逐个增量下载(md5 命名;本地已存在且 md5 匹配则跳过)
foreach (var kv in Manifest.Files)
{
string name = kv.Key; string md5 = kv.Value.Md5;
string localPath = LocalDir + name;
if (File.Exists(localPath) && Md5OfFile(localPath) == md5) continue;
string remote = $"{RemoteBase}{Md5Name(name, md5)}";
byte[] data = null;
yield return GetBytes(remote, b => data = b);
if (data == null) { Fail("下载失败: " + remote); onDone(false); yield break; }
if (Md5OfBytes(data) != md5) { Fail("md5 不符: " + name); onDone(false); yield break; }
Directory.CreateDirectory(Path.GetDirectoryName(localPath));
File.WriteAllBytes(localPath, data);
}
onDone(true);
}
private void Fail(string msg) { Error = msg; Debug.LogError("[MiniGameDownloader] " + msg); }
private static string NormalizeCdnBase(string cdnBase)
{
string url = string.IsNullOrWhiteSpace(cdnBase) ? "http://127.0.0.1:15081/" : cdnBase.Trim();
if (url.StartsWith("http://file://", StringComparison.OrdinalIgnoreCase))
{
url = url.Substring("http://".Length);
}
else if (url.StartsWith("https://file://", StringComparison.OrdinalIgnoreCase))
{
url = url.Substring("https://".Length);
}
if (url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
url = NormalizeFileUrl(url);
}
else if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
url = "http://" + url;
}
return url.EndsWith("/") ? url : url + "/";
}
private static string NormalizeFileUrl(string url)
{
string rawPath = url.Substring("file://".Length).Replace('\\', '/');
while (rawPath.StartsWith("/") && rawPath.Length > 2 && rawPath[2] == ':')
{
rawPath = rawPath.Substring(1);
}
string localPath = Uri.UnescapeDataString(rawPath).Replace('/', Path.DirectorySeparatorChar);
try
{
return new Uri(localPath).AbsoluteUri;
}
catch
{
return "file:///" + rawPath.Replace("#", "%23");
}
}
// 与 LoadDll.GetFileMd5Name 同款:name.ext -> name_md5.ext
private static string Md5Name(string filename, string md5)
{
int pos = filename.LastIndexOf('.');
return pos >= 0 ? $"{filename.Substring(0, pos)}_{md5}{filename.Substring(pos)}"
: $"{filename}_{md5}";
}
private static string Md5OfBytes(byte[] data)
{
using (var md5 = System.Security.Cryptography.MD5.Create())
{
var hash = md5.ComputeHash(data);
var sb = new System.Text.StringBuilder(hash.Length * 2);
foreach (var b in hash) sb.Append(b.ToString("x2"));
return sb.ToString();
}
}
private static string Md5OfFile(string path) => Md5OfBytes(File.ReadAllBytes(path));
private IEnumerator GetText(string url, Action<string> onText)
{
using (var uwr = UnityWebRequest.Get(url))
{
yield return uwr.SendWebRequest();
onText(uwr.result == UnityWebRequest.Result.Success ? uwr.downloadHandler.text : null);
if (uwr.result != UnityWebRequest.Result.Success)
Debug.LogError($"[MiniGameDownloader] GET 失败 {url}: {uwr.error}");
}
}
private IEnumerator GetBytes(string url, Action<byte[]> onBytes)
{
using (var uwr = UnityWebRequest.Get(url))
{
yield return uwr.SendWebRequest();
onBytes(uwr.result == UnityWebRequest.Result.Success ? uwr.downloadHandler.data : null);
if (uwr.result != UnityWebRequest.Result.Success)
Debug.LogError($"[MiniGameDownloader] GET 失败 {url}: {uwr.error}");
}
}
}
}