using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using UnityEngine.Networking; /// /// 云存档模块。 /// public sealed class BriskArchiveModule : BriskModuleBase { /// /// 获取当前账号的存档槽位列表。 /// public async Task> GetSlotsAsync() { return await ExecuteAsync(async context => { var data = await context.HttpClient.GetRawDataAsync("/archives/slots", null, true); return (IReadOnlyList)BriskModelMapper.ToArchiveSlots(data); }); } /// /// 获取指定槽位的元信息。 /// public async Task GetMetaAsync(int slotNo) { ValidateSlotNo(slotNo); return await ExecuteAsync(async context => { var data = await context.HttpClient.GetDataAsync($"/archives/slot/{slotNo}/meta", null, true); return BriskModelMapper.ToArchiveMeta(data); }); } /// /// 上传指定槽位的二进制存档。 /// public async Task UploadAsync(int slotNo, byte[] bytes, int? baseVersion = null, string checksum = null) { ValidateSlotNo(slotNo); if (bytes == null || bytes.Length == 0) { throw new ArgumentException("bytes is required.", nameof(bytes)); } return await ExecuteAsync(async context => { var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : NormalizeChecksum(checksum); var sections = new List { new MultipartFormDataSection("base_version", (baseVersion ?? 0).ToString()), new MultipartFormDataSection("checksum", finalChecksum), new MultipartFormFileSection("file", bytes, "archive.bin", "application/octet-stream") }; var data = await context.HttpClient.PostMultipartAsync($"/archives/slot/{slotNo}/upload", sections, true); return BriskModelMapper.ToArchiveUploadResult(data); }); } /// /// 以 UTF-8 文本形式上传指定槽位的存档。 /// public Task UploadTextAsync(int slotNo, string text, int? baseVersion = null, string checksum = null) { RequireNotNull(text, nameof(text)); return UploadAsync(slotNo, Encoding.UTF8.GetBytes(text), baseVersion, checksum); } /// /// 以 JSON 文本形式上传指定槽位的存档。 /// public Task UploadJsonAsync(int slotNo, object payload, int? baseVersion = null, string checksum = null) { RequireNotNull(payload, nameof(payload)); return UploadAsync(slotNo, Encoding.UTF8.GetBytes(BriskJson.Serialize(payload)), baseVersion, checksum); } /// /// 下载指定槽位的二进制存档。 /// public async Task DownloadAsync(int slotNo) { ValidateSlotNo(slotNo); return await ExecuteAsync(async context => { var response = await context.HttpClient.GetBytesAsync($"/archives/slot/{slotNo}/download", null, true); return new BriskArchiveDownloadResult { Bytes = response.Bytes, Version = ReadHeaderInt(response.Headers, "X-Archive-Version"), Checksum = ReadHeader(response.Headers, "X-Archive-Checksum") }; }); } /// /// 以 UTF-8 文本形式下载指定槽位的存档。 /// public async Task DownloadTextAsync(int slotNo) { var result = await DownloadAsync(slotNo); return result == null || result.Bytes == null ? string.Empty : Encoding.UTF8.GetString(result.Bytes); } /// /// 以 JSON 对象形式下载指定槽位的存档。 /// public async Task DownloadJsonAsync(int slotNo) { var text = await DownloadTextAsync(slotNo); return string.IsNullOrWhiteSpace(text) ? null : BriskJson.Deserialize(text); } private static void ValidateSlotNo(int slotNo) { RequirePositive(slotNo, nameof(slotNo), "slotNo must be greater than 0."); } private static string ComputeSha256(byte[] bytes) { using (var sha = SHA256.Create()) { var hash = sha.ComputeHash(bytes); var builder = new StringBuilder(hash.Length * 2); for (var i = 0; i < hash.Length; i++) { builder.Append(hash[i].ToString("x2")); } return builder.ToString(); } } private static string NormalizeChecksum(string checksum) { if (string.IsNullOrWhiteSpace(checksum)) { return checksum; } var value = checksum.Trim(); return value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase) ? value.Substring("sha256:".Length) : value; } private static int ReadHeaderInt(Dictionary headers, string key) { var value = ReadHeader(headers, key); return int.TryParse(value, out var result) ? result : 0; } private static string ReadHeader(Dictionary headers, string key) { if (headers == null || string.IsNullOrWhiteSpace(key)) { return null; } foreach (var pair in headers) { if (string.Equals(pair.Key, key, StringComparison.OrdinalIgnoreCase)) { return pair.Value; } } return null; } }