Newer
Older
Sunfish / src / Sunfish.Core / Shares / ShareTree.cs
@Ivan Dominguez Ivan Dominguez 21 hours ago 3 KB 3.0
using Sunfish.Config;

namespace Sunfish.Shares;

/// <summary>Resultado de resolver una ruta contra los shares.</summary>
public sealed class Resolution
{
    /// <summary>Share que atiende la ruta (location de prefijo mas largo); null si ninguno.</summary>
    public ShareConfig? Share { get; init; }

    /// <summary>Componentes de la ruta dentro del share.</summary>
    public IReadOnlyList<string> Relative { get; init; } = [];

    /// <summary>
    /// Locations mas especificas que cuelgan de esta ruta, como carpetas virtuales
    /// (nombre del siguiente componente). Tapan a las carpetas reales del mismo nombre.
    /// </summary>
    public IReadOnlyList<string> VirtualChildren { get; init; } = [];
}

/// <summary>
/// Arbol de locations. Gana el prefijo mas largo, comparando por componentes completos y sin
/// distinguir mayusculas (como las locations de prefijo de nginx).
/// </summary>
public sealed class ShareTree
{
    private readonly List<(ShareConfig Share, string[] Segments)> _entries;

    public ShareTree(IEnumerable<ShareConfig> shares)
    {
        _entries = shares
            .Where(s => s.Enabled)
            .Select(s => (s, LocationPath.Segments(s.Location)))
            .OrderByDescending(e => e.Item2.Length)
            .ToList();
    }

    public IEnumerable<ShareConfig> Shares => _entries.Select(e => e.Share);

    public Resolution Resolve(IReadOnlyList<string> segments)
    {
        ShareConfig? share = null;
        int matched = 0;
        foreach (var (candidate, location) in _entries)
        {
            if (IsPrefix(location, segments))
            {
                share = candidate;
                matched = location.Length;
                break; // ordenadas de mas a menos componentes
            }
        }
        return new Resolution
        {
            Share = share,
            Relative = segments.Skip(matched).ToArray(),
            VirtualChildren = ChildrenOf(segments),
        };
    }

    /// <summary>Nombres de las locations que cuelgan directamente o indirectamente de la ruta.</summary>
    public IReadOnlyList<string> ChildrenOf(IReadOnlyList<string> segments)
    {
        var names = new List<string>();
        foreach (var (_, location) in _entries)
        {
            if (location.Length > segments.Count && IsPrefix(segments, location))
            {
                string child = location[segments.Count];
                if (!names.Contains(child, StringComparer.OrdinalIgnoreCase))
                    names.Add(child);
            }
        }
        names.Sort(StringComparer.OrdinalIgnoreCase);
        return names;
    }

    /// <summary>La location del share, para construir URLs.</summary>
    public static string UrlOf(ShareConfig share) => share.Location == "/" ? "/" : share.Location + "/";

    private static bool IsPrefix(IReadOnlyList<string> prefix, IReadOnlyList<string> path)
    {
        if (prefix.Count > path.Count)
            return false;
        for (int i = 0; i < prefix.Count; i++)
        {
            if (!string.Equals(prefix[i], path[i], StringComparison.OrdinalIgnoreCase))
                return false;
        }
        return true;
    }
}