All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s
自定义域 URL 带 /admin 前缀,归一化为 uploads/... 再 GetUrl,防止双重 PathBase。 Co-authored-by: Cursor <cursoragent@cursor.com>
156 lines
4.7 KiB
C#
156 lines
4.7 KiB
C#
namespace Oms.Application.Storage;
|
||
|
||
/// <summary>
|
||
/// 文件存储 key / URL 规范化。
|
||
/// </summary>
|
||
public static class FileStoragePath
|
||
{
|
||
/// <summary>
|
||
/// 规范化为库内 object key:uploads/a.jpg(无前导 /,完整 URL 取 AbsolutePath)。
|
||
/// </summary>
|
||
public static string NormalizeKey(string? pathOrUrl)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(pathOrUrl))
|
||
{
|
||
throw new ArgumentException("路径不能为空", nameof(pathOrUrl));
|
||
}
|
||
|
||
var key = pathOrUrl.Trim().Replace('\\', '/');
|
||
if (key.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||
|| key.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
key = new Uri(key).AbsolutePath;
|
||
}
|
||
|
||
key = key.TrimStart('/');
|
||
if (string.IsNullOrWhiteSpace(key))
|
||
{
|
||
throw new ArgumentException("object key 不能为空", nameof(pathOrUrl));
|
||
}
|
||
|
||
// 自定义域公开 URL 常带 PathBase「admin/」;库内/COS 逻辑 key 为 uploads/...。
|
||
// 若不剥离,GetUrl 再拼 KeyPrefix/PathBase 会变成 admin/admin/uploads → NoSuchKey。
|
||
return StripPublicPathBase(key);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 去掉公开 URL 上的 PathBase(可重复的 admin/),保留 uploads/... 等逻辑 key。
|
||
/// </summary>
|
||
public static string StripPublicPathBase(string key)
|
||
{
|
||
var value = key.Replace('\\', '/').Trim().TrimStart('/');
|
||
while (value.StartsWith("admin/", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var rest = value["admin/".Length..];
|
||
// 仅剥「PathBase + 业务前缀」;避免误伤 admin 业务目录(若有)
|
||
if (rest.StartsWith("uploads/", StringComparison.OrdinalIgnoreCase)
|
||
|| rest.StartsWith("admin/", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
value = rest;
|
||
continue;
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否已是绝对 URL(含微信头像等外链)。
|
||
/// </summary>
|
||
public static bool IsAbsoluteUrl(string? value) =>
|
||
!string.IsNullOrWhiteSpace(value)
|
||
&& (value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||
|| value.StartsWith("https://", StringComparison.OrdinalIgnoreCase));
|
||
|
||
/// <summary>
|
||
/// 是否为本系统对象存储域名(自定义域 cos.* / 腾讯云默认域)。
|
||
/// 这类地址带临时签名,库内应存 object key,读出时重新签名;不可当外链原样返回。
|
||
/// </summary>
|
||
public static bool IsManagedObjectStorageUrl(string? value)
|
||
{
|
||
if (!IsAbsoluteUrl(value))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
var host = new Uri(value!).Host;
|
||
if (string.IsNullOrWhiteSpace(host))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
host = host.ToLowerInvariant();
|
||
return host.StartsWith("cos.", StringComparison.Ordinal)
|
||
|| host.Contains("myqcloud.com", StringComparison.Ordinal)
|
||
|| host.Contains("tencentcos.cn", StringComparison.Ordinal)
|
||
|| host.Contains("tencentcos.com", StringComparison.Ordinal);
|
||
}
|
||
catch (UriFormatException)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 读取对象时尝试的 key:含/不含 <c>admin/</c> 前缀(自定义域路径常带 PathBase)。
|
||
/// </summary>
|
||
public static IReadOnlyList<string> ExpandReadKeys(string? pathOrUrl)
|
||
{
|
||
var keys = new List<string>();
|
||
void Push(string? raw)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(raw))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var key = raw.Replace('\\', '/').Trim().TrimStart('/');
|
||
if (key.Length == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (keys.Exists(x => string.Equals(x, key, StringComparison.OrdinalIgnoreCase)))
|
||
{
|
||
return;
|
||
}
|
||
|
||
keys.Add(key);
|
||
}
|
||
|
||
if (IsAbsoluteUrl(pathOrUrl))
|
||
{
|
||
try
|
||
{
|
||
Push(NormalizeKey(pathOrUrl));
|
||
}
|
||
catch (ArgumentException)
|
||
{
|
||
// ignore
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Push(pathOrUrl);
|
||
}
|
||
|
||
foreach (var existing in keys.ToArray())
|
||
{
|
||
if (existing.StartsWith("admin/", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
Push(existing["admin/".Length..]);
|
||
}
|
||
else
|
||
{
|
||
Push("admin/" + existing);
|
||
}
|
||
}
|
||
|
||
return keys;
|
||
}
|
||
}
|