admin/src/Oms.Application/Storage/FileStoragePath.cs
zzh 88c89bd85e
All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s
fix: COS access-url 重签剥掉 PathBase,避免 admin/admin NoSuchKey
自定义域 URL 带 /admin 前缀,归一化为 uploads/... 再 GetUrl,防止双重 PathBase。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-23 14:16:13 +08:00

156 lines
4.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.

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;
}
}