namespace Oms.Application.Storage;
///
/// 文件存储 key / URL 规范化。
///
public static class FileStoragePath
{
///
/// 规范化为库内 object key:uploads/a.jpg(无前导 /,完整 URL 取 AbsolutePath)。
///
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);
}
///
/// 去掉公开 URL 上的 PathBase(可重复的 admin/),保留 uploads/... 等逻辑 key。
///
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;
}
///
/// 是否已是绝对 URL(含微信头像等外链)。
///
public static bool IsAbsoluteUrl(string? value) =>
!string.IsNullOrWhiteSpace(value)
&& (value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|| value.StartsWith("https://", StringComparison.OrdinalIgnoreCase));
///
/// 是否为本系统对象存储域名(自定义域 cos.* / 腾讯云默认域)。
/// 这类地址带临时签名,库内应存 object key,读出时重新签名;不可当外链原样返回。
///
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;
}
}
///
/// 读取对象时尝试的 key:含/不含 admin/ 前缀(自定义域路径常带 PathBase)。
///
public static IReadOnlyList ExpandReadKeys(string? pathOrUrl)
{
var keys = new List();
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;
}
}