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