admin/src/Oms.Application/Storage/FileStoragePath.cs
zzh e48f8ce926
All checks were successful
Build and Deploy / deploy (push) Successful in 1m7s
fix: COS 签名 URL 过期时按 object key 重新签发访问地址
历史库内若存带 q-sign 的完整 COS 链接,打印/查看会 AccessDenied;写入只保留 key,读出时重新签名。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-23 10:12:07 +08:00

131 lines
3.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));
}
return key;
}
/// <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;
}
}