You've already forked CC-Framework.BriskGameServer
178 lines
5.8 KiB
C#
178 lines
5.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using UnityEngine.Networking;
|
|
|
|
/// <summary>
|
|
/// 云存档模块。
|
|
/// </summary>
|
|
public sealed class BriskArchiveModule
|
|
: BriskModuleBase
|
|
{
|
|
/// <summary>
|
|
/// 获取当前账号的存档槽位列表。
|
|
/// </summary>
|
|
public async Task<IReadOnlyList<BriskArchiveSlot>> GetSlotsAsync()
|
|
{
|
|
return await ExecuteAsync(async context =>
|
|
{
|
|
var data = await context.HttpClient.GetRawDataAsync("/archives/slots", null, true);
|
|
return (IReadOnlyList<BriskArchiveSlot>)BriskModelMapper.ToArchiveSlots(data);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取指定槽位的元信息。
|
|
/// </summary>
|
|
public async Task<BriskArchiveMeta> 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);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 上传指定槽位的二进制存档。
|
|
/// </summary>
|
|
public async Task<BriskArchiveUploadResult> 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<IMultipartFormSection>
|
|
{
|
|
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);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 以 UTF-8 文本形式上传指定槽位的存档。
|
|
/// </summary>
|
|
public Task<BriskArchiveUploadResult> UploadTextAsync(int slotNo, string text, int? baseVersion = null, string checksum = null)
|
|
{
|
|
RequireNotNull(text, nameof(text));
|
|
return UploadAsync(slotNo, Encoding.UTF8.GetBytes(text), baseVersion, checksum);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 以 JSON 文本形式上传指定槽位的存档。
|
|
/// </summary>
|
|
public Task<BriskArchiveUploadResult> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 下载指定槽位的二进制存档。
|
|
/// </summary>
|
|
public async Task<BriskArchiveDownloadResult> 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")
|
|
};
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 以 UTF-8 文本形式下载指定槽位的存档。
|
|
/// </summary>
|
|
public async Task<string> DownloadTextAsync(int slotNo)
|
|
{
|
|
var result = await DownloadAsync(slotNo);
|
|
return result == null || result.Bytes == null ? string.Empty : Encoding.UTF8.GetString(result.Bytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 以 JSON 对象形式下载指定槽位的存档。
|
|
/// </summary>
|
|
public async Task<object> 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<string, string> headers, string key)
|
|
{
|
|
var value = ReadHeader(headers, key);
|
|
return int.TryParse(value, out var result) ? result : 0;
|
|
}
|
|
|
|
private static string ReadHeader(Dictionary<string, string> 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;
|
|
}
|
|
}
|