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<string, object>: 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,
};
}