namespace Dpz.Core.Service.ObjectStorage;

/// <summary>
/// COS 对象 Key 与公开访问 URL 的统一转换工具
/// </summary>
internal static class S3ObjectKeyHelper
{
    /// <summary>
    /// 根据目录段和文件名生成对象 Key
    /// </summary>
    public static string BuildKey(IEnumerable<string> path, string filename)
    {
        if (string.IsNullOrWhiteSpace(filename))
        {
            throw new ArgumentNullException(nameof(filename));
        }

        var segments = path.SelectMany(SplitPath).ToList();
        segments.AddRange(SplitPath(filename));
        return BuildKey(segments);
    }

    /// <summary>
    /// 根据已经包含文件名的路径段生成对象 Key
    /// </summary>
    public static string BuildKey(IEnumerable<string> pathToFile)
    {
        var segments = pathToFile.SelectMany(SplitPath).ToList();
        return segments.Count == 0
            ? throw new ArgumentException("对象路径不能为空", nameof(pathToFile))
            : string.Join("/", segments);
    }

    /// <summary>
    /// 从相对路径或完整 URL 中还原 COS 对象 Key
    /// </summary>
    public static string NormalizeKey(string pathToFile)
    {
        if (string.IsNullOrWhiteSpace(pathToFile))
        {
            throw new ArgumentNullException(nameof(pathToFile));
        }

        var path = pathToFile.Trim();
        if (Uri.TryCreate(path, UriKind.Absolute, out var uri))
        {
            path = uri.AbsolutePath;
        }

        path = path.TrimStart('/');
        path = Uri.UnescapeDataString(path);
        return BuildKey([path]);
    }

    /// <summary>
    /// 把目录路径转换成 ListObjectsV2 可用的 Prefix
    /// </summary>
    public static string NormalizeDirectoryPrefix(string path)
    {
        ArgumentNullException.ThrowIfNull(path);

        var value = path.Trim();
        if (value is "" or "/")
        {
            return string.Empty;
        }

        value = Uri.TryCreate(value, UriKind.Absolute, out var uri)
            ? Uri.UnescapeDataString(uri.AbsolutePath)
            : Uri.UnescapeDataString(value);

        value = value.Trim('/');
        return string.IsNullOrWhiteSpace(value) ? string.Empty : value + "/";
    }

    /// <summary>
    /// 生成对象的公开访问 URL
    /// </summary>
    public static string BuildAccessUrl(string cdnHost, string key)
    {
        if (string.IsNullOrWhiteSpace(cdnHost))
        {
            throw new ArgumentNullException(nameof(cdnHost));
        }

        var encodedKey = string.Join(
            "/",
            NormalizeKey(key).Split('/').Select(Uri.EscapeDataString)
        );
        return $"{cdnHost.TrimEnd('/')}/{encodedKey}";
    }

    /// <summary>
    /// 取指定 Prefix 下一级目录或文件的显示名称
    /// </summary>
    public static string GetDisplayName(string keyOrPrefix, string parentPrefix)
    {
        var value = keyOrPrefix;
        if (!string.IsNullOrEmpty(parentPrefix) && value.StartsWith(parentPrefix))
        {
            value = value[parentPrefix.Length..];
        }

        return value.Trim('/').Split('/').FirstOrDefault() ?? string.Empty;
    }

    /// <summary>
    /// 组合远程目录和相对文件路径
    /// </summary>
    public static string CombineRemotePath(string remoteDirectory, string relativeFilePath)
    {
        var prefix = NormalizeDirectoryPrefix(remoteDirectory);
        var key = NormalizeKey(relativeFilePath.Replace(Path.DirectorySeparatorChar, '/'));
        return prefix + key;
    }

    private static IEnumerable<string> SplitPath(string path)
    {
        if (string.IsNullOrWhiteSpace(path))
        {
            yield break;
        }

        foreach (
            var segment in path.Split(
                ['/', '\\'],
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
            )
        )
        {
            yield return Uri.UnescapeDataString(segment);
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个用于处理 S3/COS 对象存储 的工具类,主要负责对象键(Key)与公开访问 URL 之间的转换和规范化。

主要功能

1. BuildKey - 构建对象键

两个重载方法用于生成标准的对象存储键:

// 方法1: 从目录段和文件名生成
BuildKey(IEnumerable<string> path, string filename)
// 示例: path=["folder1", "folder2"], filename="file.txt" 
// 返回: "folder1/folder2/file.txt"

// 方法2: 从完整路径段生成
BuildKey(IEnumerable<string> pathToFile)

2. NormalizeKey - 规范化键

从各种格式的路径还原成标准的对象键:

// 输入可以是:
// - 相对路径: "folder/file.txt"
// - 完整URL: "https://cdn.com/folder/file.txt"
// - URL编码路径: "folder%2Ffile.txt"
// 都会输出: "folder/file.txt"

处理步骤:

  • 如果是完整URL,提取路径部分
  • 移除开头的 /
  • 解码 URL 编码字符
  • 通过分割重组确保格式统一

3. NormalizeDirectoryPrefix - 规范化目录前缀

生成用于 ListObjectsV2 API 的目录前缀:

// 输入: "folder/subfolder" 或 "/folder/subfolder/" 或 URL
// 输出: "folder/subfolder/"  (始终以/结尾)
// 特殊: "" 或 "/" 输入会返回空字符串

4. BuildAccessUrl - 生成公开访问 URL

结合 CDN 域名和对象键生成完整访问链接:

// cdnHost = "https://cdn.example.com"
// key = "folder/文件.txt"
// 返回: "https://cdn.example.com/folder/%E6%96%87%E4%BB%B6.txt"

注意: 对路径的每个段分别进行 URL 编码,确保特殊字符正确处理。

5. GetDisplayName - 获取显示名称

提取相对于父目录的下一级名称:

// keyOrPrefix = "parent/child/file.txt"
// parentPrefix = "parent/"
// 返回: "child"

6. CombineRemotePath - 组合远程路径

合并远程目录和本地相对路径:

// remoteDirectory = "upload/images"
// relativeFilePath = "2024\01\photo.jpg"  (Windows路径)
// 返回: "upload/images/2024/01/photo.jpg"

核心辅助方法

SplitPath - 路径分割

私有方法,负责智能分割路径:

// 输入: "folder1/folder2\\file%20name.txt"
// 输出: ["folder1", "folder2", "file name.txt"]

特点:

  • 同时支持 /\ 分隔符
  • 自动去除空段和两端空格
  • URL 解码每个段

设计特点

  1. 容错性强:支持多种输入格式(相对路径、绝对URL、不同分隔符)
  2. 统一标准:所有输出都是规范的 S3 对象键格式(使用 / 分隔)
  3. 编码处理:正确处理 URL 编码/解码,避免特殊字符问题
  4. 参数验证:对空值和无效输入有完善的检查

使用场景

  • 上传文件时生成对象键
  • 从URL还原对象键以进行操作
  • 列举目录内容(生成查询前缀)
  • 生成文件的公开访问链接
  • 跨平台路径处理(Windows/Linux)
评论加载中...