Compare commits

..

No commits in common. "f8cabe19096ae61caca094e99d29c13fafde165e" and "62d423ad4d575e260fadd2cea532ac715598d424" have entirely different histories.

3 changed files with 3 additions and 112 deletions

View File

@ -39,37 +39,6 @@ public static class FileStoragePath
&& (value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && (value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|| value.StartsWith("https://", 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> /// <summary>
/// 读取对象时尝试的 key:含/不含 <c>admin/</c> 前缀(自定义域路径常带 PathBase)。 /// 读取对象时尝试的 key:含/不含 <c>admin/</c> 前缀(自定义域路径常带 PathBase)。
/// </summary> /// </summary>

View File

@ -8,7 +8,7 @@ namespace Oms.Application.Storage;
public static class OmsObjectStorageExtensions public static class OmsObjectStorageExtensions
{ {
/// <summary> /// <summary>
/// 将 key / 相对路径规范为库内 object key;本系统 COS 签名 URL 剥签后存 key;其它绝对外链原样返回;空返回 null。 /// 将 key / 相对路径规范为库内 object key;绝对外链原样返回;空返回 null。
/// </summary> /// </summary>
public static string? ToStorageKey(string? keyOrUrl) public static string? ToStorageKey(string? keyOrUrl)
{ {
@ -23,12 +23,6 @@ public static class OmsObjectStorageExtensions
return "0"; return "0";
} }
// 私有桶签名 URL 会过期,禁止写入库;只保留 object key。
if (FileStoragePath.IsManagedObjectStorageUrl(raw))
{
return FileStoragePath.NormalizeKey(raw);
}
if (FileStoragePath.IsAbsoluteUrl(raw)) if (FileStoragePath.IsAbsoluteUrl(raw))
{ {
return raw; return raw;
@ -38,7 +32,7 @@ public static class OmsObjectStorageExtensions
} }
/// <summary> /// <summary>
/// 临时访问地址。空值返回空串;真正外链原样返回;逻辑 key / 本系统 COS URL 走 <see cref="IObjectStorage.GetUrlAsync"/> 重新签名。 /// 临时访问地址。空值返回空串;外链原样返回;逻辑 key 走 <see cref="IObjectStorage.GetUrlAsync"/>。
/// Local 存储目录不可写/不可读时降级返回 key,避免拖垮下单等主流程。 /// Local 存储目录不可写/不可读时降级返回 key,避免拖垮下单等主流程。
/// </summary> /// </summary>
public static async Task<string> GetPublicUrlAsync( public static async Task<string> GetPublicUrlAsync(
@ -52,12 +46,7 @@ public static class OmsObjectStorageExtensions
} }
var raw = keyOrUrl.Trim(); var raw = keyOrUrl.Trim();
// 历史数据可能把带 q-sign 的完整 URL 存进库;直接返回会 AccessDenied/Request has expired。 if (FileStoragePath.IsAbsoluteUrl(raw))
if (FileStoragePath.IsManagedObjectStorageUrl(raw))
{
raw = FileStoragePath.NormalizeKey(raw);
}
else if (FileStoragePath.IsAbsoluteUrl(raw))
{ {
return raw; return raw;
} }

View File

@ -1,6 +1,4 @@
using Fake.ObjectStorage;
using FluentAssertions; using FluentAssertions;
using Moq;
using Oms.Application.Storage; using Oms.Application.Storage;
namespace Oms.Tests; namespace Oms.Tests;
@ -18,69 +16,4 @@ public class FileStoragePathTests
keys.Should().Contain("admin/uploads/prints/orders/IDE2610061059117673-20261006105912.pdf"); keys.Should().Contain("admin/uploads/prints/orders/IDE2610061059117673-20261006105912.pdf");
keys.Should().Contain("uploads/prints/orders/IDE2610061059117673-20261006105912.pdf"); keys.Should().Contain("uploads/prints/orders/IDE2610061059117673-20261006105912.pdf");
} }
[Theory]
[InlineData("https://cos.ideship.com/uploads/a.pdf?q-sign-algorithm=sha1", true)]
[InlineData("http://cos.test.ideship.com/admin/uploads/a.pdf", true)]
[InlineData("https://bucket-123.cos.ap-guangzhou.myqcloud.com/uploads/a.pdf", true)]
[InlineData("https://third-party.example.com/label.pdf", false)]
[InlineData("uploads/prints/orders/a.pdf", false)]
public void IsManagedObjectStorageUrl_ShouldDetectCosHosts(string value, bool expected)
{
FileStoragePath.IsManagedObjectStorageUrl(value).Should().Be(expected);
}
[Fact]
public void ToStorageKey_ShouldStripExpiredCosSignedUrlToObjectKey()
{
var expired =
"https://cos.ideship.com/uploads/prints/orders/IDE2610081704036507-20261008170404.pdf?q-sign-algorithm=sha1&q-sign-time=1;2";
OmsObjectStorageExtensions.ToStorageKey(expired)
.Should()
.Be("uploads/prints/orders/IDE2610081704036507-20261008170404.pdf");
}
[Fact]
public void ToStorageKey_ShouldKeepExternalAbsoluteUrl()
{
const string external = "https://third-party.example.com/label.pdf?token=1";
OmsObjectStorageExtensions.ToStorageKey(external).Should().Be(external);
}
[Fact]
public async Task GetPublicUrl_ShouldResignManagedCosAbsoluteUrl()
{
var expired =
"https://cos.ideship.com/uploads/prints/orders/IDE2610081704036507-20261008170404.pdf?q-sign-algorithm=sha1&q-sign-time=1;2";
const string fresh =
"https://cos.ideship.com/uploads/prints/orders/IDE2610081704036507-20261008170404.pdf?q-sign-algorithm=sha1&fresh=1";
var storage = new Mock<IObjectStorage>(MockBehavior.Strict);
storage
.Setup(x => x.GetUrlAsync(
"uploads/prints/orders/IDE2610081704036507-20261008170404.pdf",
It.IsAny<TimeSpan?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(fresh);
var url = await storage.Object.GetPublicUrlAsync(expired);
url.Should().Be(fresh);
storage.VerifyAll();
}
[Fact]
public async Task GetPublicUrl_ShouldPassThroughExternalAbsoluteUrl()
{
const string external = "https://third-party.example.com/label.pdf";
var storage = new Mock<IObjectStorage>(MockBehavior.Strict);
var url = await storage.Object.GetPublicUrlAsync(external);
url.Should().Be(external);
storage.Verify(
x => x.GetUrlAsync(It.IsAny<string>(), It.IsAny<TimeSpan?>(), It.IsAny<CancellationToken>()),
Times.Never);
}
} }