Newer
Older
Sunfish / src / Sunfish.Core / Config / LegacyImport.cs
@Ivan Dominguez Ivan Dominguez 21 hours ago 7 KB 3.0
using System.Text.Json;

namespace Sunfish.Config;

/// <summary>
/// Importacion de la configuracion de Sunfish 2.0 (JSON "sunfish2", ver DOC/SHARES-Y-RUTAS.md).
/// Usa JsonDocument (sin reflexion, compatible con AOT) y lee los nombres sin distinguir
/// mayusculas. Lo que ya no existe se ignora y se explica en las notas.
/// </summary>
public static class LegacyImport
{
    public const string FileName = "sunfish2";

    /// <summary>"sunfish2" junto al ejecutable o en la carpeta actual (donde lo dejaba la 2.0), o null.</summary>
    public static string? Find()
    {
        foreach (var dir in new[] { AppContext.BaseDirectory, Environment.CurrentDirectory })
        {
            string path = Path.Combine(dir, FileName);
            if (File.Exists(path))
                return path;
        }
        return null;
    }

    /// <summary>Convierte el JSON de la 2.0. Devuelve null si no es un JSON valido de la 2.0.</summary>
    public static SunfishConfig? FromV2(string json, List<string> notes)
    {
        JsonDocument doc;
        try
        {
            doc = JsonDocument.Parse(json, new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip });
        }
        catch (JsonException)
        {
            return null;
        }
        using (doc)
        {
            var root = doc.RootElement;
            if (root.ValueKind != JsonValueKind.Object)
                return null;

            var config = new SunfishConfig();
            if (Get(root, "Port") is { } portElement && Int(portElement) is int port)
            {
                // Sin administrador no se abren puertos por debajo de 1024 (la 2.0 usaba 90 por defecto).
                if (port is >= 1024 and <= 65535)
                    config.Server.HttpPort = port;
                else
                    notes.Add(Strings.ImportPortChanged(port, config.Server.HttpPort));
            }
            if (Get(root, "SunfishRoot") is { } menu && Bool(menu) is bool rootMenu)
                config.Server.RootMenu = rootMenu;
            if (Get(root, "AdminPanel") is { } admin && Bool(admin) == true)
                notes.Add(Strings.ImportAdminIgnored);

            if (Get(root, "Services") is { ValueKind: JsonValueKind.Array } services)
            {
                foreach (var service in services.EnumerateArray())
                {
                    if (service.ValueKind != JsonValueKind.Object)
                        continue;
                    string name = Str(Get(service, "Name")) ?? "share";
                    string? type = Str(Get(service, "Type"));
                    if (type is not null && !type.EndsWith("WebService", StringComparison.OrdinalIgnoreCase))
                    {
                        notes.Add(Strings.ImportServiceSkipped(name, type));
                        continue;
                    }
                    var settings = Get(service, "Settings");
                    var share = new ShareConfig
                    {
                        Name = ShareSettings.SuggestName(config, name),
                        Enabled = Get(service, "Enabled") is { } en && Bool(en) is bool enabled ? enabled : true,
                        Path = Str(Setting(settings, "path")) ?? "",
                        Index = Str(Setting(settings, "defaultFile")) ?? "",
                        Listing = Setting(settings, "folderShare") is { } fs && Bool(fs) == true,
                    };
                    string location = Str(Get(service, "Location")) ?? "";
                    share.Location = Shares.LocationPath.TryNormalize(location.StartsWith('/') ? location : "/" + location, out var normalized)
                        ? normalized
                        : ShareSettings.SuggestLocation(config, share.Name);
                    // readOnly ausente en la 2.0 equivalia a false (escritura permitida).
                    bool readOnly = Setting(settings, "readOnly") is { } ro && Bool(ro) == true;
                    if (!readOnly)
                        share.Upload = share.Overwrite = share.Rename = true;
                    share.Delete = !readOnly && Setting(settings, "allowDeletion") is { } del && Bool(del) == true;
                    if (Setting(settings, "allowServerExecution") is { } exec && Bool(exec) == true)
                        notes.Add(Strings.ImportExecIgnored(share.Name));
                    if (share.Path.Length == 0 || !Directory.Exists(share.Path))
                        notes.Add(Strings.ImportMissingFolder(share.Name, share.Path));
                    config.Shares.Add(share);
                }
            }
            return config;
        }
    }

    /// <summary>
    /// Anade a <paramref name="target"/> los shares importados (con nombre y location libres si
    /// chocan). El puerto y el menu raiz solo se copian si la configuracion es nueva. Devuelve
    /// cuantos shares se anadieron.
    /// </summary>
    public static int Merge(SunfishConfig target, SunfishConfig imported, bool isNew)
    {
        if (isNew)
        {
            target.Server.HttpPort = imported.Server.HttpPort;
            target.Server.RootMenu = imported.Server.RootMenu;
        }
        foreach (var share in imported.Shares)
        {
            share.Name = ShareSettings.SuggestName(target, share.Name);
            if (target.Shares.Any(s => s.Location.Equals(share.Location, StringComparison.OrdinalIgnoreCase)))
                share.Location = ShareSettings.SuggestLocation(target, share.Location.Trim('/'));
            target.Shares.Add(share);
        }
        return imported.Shares.Count;
    }

    private static JsonElement? Get(JsonElement? obj, string name)
    {
        if (obj is not { ValueKind: JsonValueKind.Object } o)
            return null;
        foreach (var p in o.EnumerateObject())
            if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
                return p.Value;
        return null;
    }

    /// <summary>Settings era un Dictionary&lt;string, object&gt;: objeto o lista de pares Key/Value.</summary>
    private static JsonElement? Setting(JsonElement? settings, string key)
    {
        if (settings is { ValueKind: JsonValueKind.Array } list)
        {
            foreach (var pair in list.EnumerateArray())
                if (Str(Get(pair, "Key")) is { } k && k.Equals(key, StringComparison.OrdinalIgnoreCase))
                    return Get(pair, "Value");
            return null;
        }
        return Get(settings, key);
    }

    private static string? Str(JsonElement? e) => e switch
    {
        { ValueKind: JsonValueKind.String } s => s.GetString(),
        { ValueKind: JsonValueKind.Number } n => n.GetRawText(),
        _ => null,
    };

    private static bool? Bool(JsonElement e) => e.ValueKind switch
    {
        JsonValueKind.True => true,
        JsonValueKind.False => false,
        JsonValueKind.String when ShareSettings.TryParseBool(e.GetString() ?? "", out bool b) => b,
        JsonValueKind.Number when e.TryGetInt32(out int n) => n != 0,
        _ => null,
    };

    private static int? Int(JsonElement e) => e.ValueKind switch
    {
        JsonValueKind.Number when e.TryGetInt32(out int n) => n,
        JsonValueKind.String when int.TryParse(e.GetString(), out int n) => n,
        _ => null,
    };
}