All checks were successful
Build and Deploy / deploy (push) Successful in 1m7s
历史库内若存带 q-sign 的完整 COS 链接,打印/查看会 AccessDenied;写入只保留 key,读出时重新签名。 Co-authored-by: Cursor <cursoragent@cursor.com>
131 lines
3.7 KiB
C#
131 lines
3.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));
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|