TH1/Unity/Assets/Scripts/TH1_Logic/Editor/OssDownloadService.cs
2026-04-17 18:53:21 +08:00

169 lines
6.7 KiB
C#
Raw 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.

/*
* @Author: Copilot
* @Description: OSS文件下载服务Editor Only从阿里云OSS下载Collect数据文件
* @Date: 2026年04月17日
* @Modify:
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using UnityEngine;
namespace Logic.Editor
{
/// <summary>
/// Editor-only service for downloading files from Aliyun OSS.
/// Uses OSS REST API with V1 HMAC-SHA1 signature.
/// </summary>
public class OssDownloadService
{
private readonly string _accessKeyId;
private readonly string _accessKeySecret;
private readonly string _endpoint;
private readonly string _bucket;
private static readonly HttpClient SharedHttpClient = new() { Timeout = TimeSpan.FromMinutes(10) };
public OssDownloadService(string accessKeyId, string accessKeySecret,
string endpoint = "oss-cn-shanghai.aliyuncs.com", string bucket = "th1-oss")
{
_accessKeyId = accessKeyId?.Trim();
_accessKeySecret = accessKeySecret?.Trim();
_endpoint = endpoint?.Trim();
_bucket = bucket?.Trim();
// 打印密钥信息帮助排查问题
if (!string.IsNullOrEmpty(_accessKeySecret))
{
var secretLen = _accessKeySecret.Length;
var maskedSecret = secretLen > 4
? $"{_accessKeySecret.Substring(0, 2)}***{_accessKeySecret.Substring(secretLen - 2)}"
: "***";
Debug.Log($"[OSS配置检查] AccessKeyId: {_accessKeyId}, Secret长度: {secretLen}, Secret首尾: {maskedSecret}");
}
}
/// <summary>
/// 生成 OSS V1 签名
/// StringToSign = VERB + "\n" + "\n" + "\n" + Date + "\n" + CanonicalizedResource
/// </summary>
private string Sign(string verb, string date, string canonicalizedResource)
{
var stringToSign = $"{verb}\n\n\n{date}\n{canonicalizedResource}";
using var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(_accessKeySecret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign));
return Convert.ToBase64String(hash);
}
/// <summary>
/// 列出指定前缀下的所有对象(自动分页)
/// </summary>
public async Task<List<string>> ListObjectsAsync(string prefix)
{
var keys = new List<string>();
string continuationToken = null;
bool isTruncated = true;
while (isTruncated)
{
var queryParams = $"list-type=2&prefix={Uri.EscapeDataString(prefix)}&max-keys=1000";
var canonicalizedResource = $"/{_bucket}/";
if (!string.IsNullOrEmpty(continuationToken))
{
var encodedToken = Uri.EscapeDataString(continuationToken);
queryParams += $"&continuation-token={encodedToken}";
canonicalizedResource += $"?continuation-token={encodedToken}";
}
var url = $"https://{_bucket}.{_endpoint}/?{queryParams}";
var now = DateTimeOffset.UtcNow;
var date = now.ToString("R");
var signature = Sign("GET", date, canonicalizedResource);
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Date = now; // 强制使用完全一致的时间对象,避免 HttpClient 覆盖 Date
request.Headers.TryAddWithoutValidation("Authorization", $"OSS {_accessKeyId}:{signature}");
var response = await SharedHttpClient.SendAsync(request);
var xml = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
Debug.LogError($"<color=red>OSS ListObjects 失败: {response.StatusCode}</color>\n{xml}");
break;
}
var doc = XDocument.Parse(xml);
var ns = doc.Root?.GetDefaultNamespace() ?? XNamespace.None;
foreach (var content in doc.Root!.Elements(ns + "Contents"))
{
var key = content.Element(ns + "Key")?.Value;
if (!string.IsNullOrEmpty(key) && !key.EndsWith("/"))
keys.Add(key);
}
isTruncated = bool.TryParse(doc.Root.Element(ns + "IsTruncated")?.Value, out var t) && t;
continuationToken = doc.Root.Element(ns + "NextContinuationToken")?.Value;
}
return keys;
}
/// <summary>
/// 下载单个 OSS 对象到本地文件
/// </summary>
public async Task<bool> DownloadObjectAsync(string objectKey, string localPath)
{
try
{
var now = DateTimeOffset.UtcNow;
var date = now.ToString("R");
var canonicalizedResource = $"/{_bucket}/{objectKey}";
var signature = Sign("GET", date, canonicalizedResource);
// 对 objectKey 中的每个路径段分别编码,保留 '/'
var encodedKey = string.Join("/",
objectKey.Split('/').Select(seg => Uri.EscapeDataString(seg)));
var url = $"https://{_bucket}.{_endpoint}/{encodedKey}";
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Date = now; // 强制使用完全一致的时间对象
request.Headers.TryAddWithoutValidation("Authorization", $"OSS {_accessKeyId}:{signature}");
var response = await SharedHttpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
Debug.LogError($"<color=red>OSS 下载失败 {objectKey}: {response.StatusCode}</color>\n{error}");
return false;
}
var dir = Path.GetDirectoryName(localPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
var data = await response.Content.ReadAsByteArrayAsync();
File.WriteAllBytes(localPath, data);
return true;
}
catch (Exception ex)
{
Debug.LogError($"<color=red>OSS 下载异常 {objectKey}: {ex.Message}</color>");
return false;
}
}
}
}