Newer
Older
Sunfish / src / Sunfish.Cli / Program.cs
@Ivan Dominguez Ivan Dominguez 21 hours ago 7 KB 3.0
using System.Globalization;
using System.Net.Sockets;
using Sunfish;
using Sunfish.Cli;
using Sunfish.Cli.Tui;
using Sunfish.Config;
using Sunfish.Host;
using Sunfish.Log;
using Sunfish.Net;

// sunfish-cli: TUI interactiva si hay terminal; si no (o con "serve"), sirve sin interfaz.
// Tambien comparte una carpeta al momento (modo rapido) y gestiona shares y usuarios.

// Acentos correctos en Windows Terminal, conhost y al redirigir a otros programas.
Console.OutputEncoding = new System.Text.UTF8Encoding(false);

if (args.Length > 0 && args[0] == "user")
    return UserCommands.Run(args[1..]);
if (args.Length > 0 && args[0] == "admin")
    return AdminCommands.RunAdmin(args[1..]);
if (args.Length > 0 && args[0] == "import-v2")
    return AdminCommands.RunImport(args[1..]);
if (args.Length > 0 && args[0] == "share")
    return ShareCommands.Run(args[1..]);

string? configPath = null;
string? folder = null;
int? port = null;
bool serve = false;
bool verbose = false;

for (int i = 0; i < args.Length; i++)
{
    string arg = args[i];
    switch (arg)
    {
        case "-h" or "--help" or "/?":
            Console.WriteLine(Texts.Usage);
            return 0;
        case "--version":
            Console.WriteLine("Sunfish 3.0");
            return 0;
        case "serve":
            serve = true;
            break;
        case "-v" or "--verbose":
            verbose = true;
            break;
        case "-c" or "--config":
            configPath = Value(ref i);
            if (configPath is null) return 1;
            break;
        case "--lang":
            string? lang = Value(ref i);
            if (lang is null) return 1;
            Strings.Force(lang);
            break;
        case "-p" or "--port":
            string? text = Value(ref i);
            if (text is null) return 1;
            if (!int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out int p) || p > 65535)
            {
                Console.Error.WriteLine(Texts.BadPort(text));
                return 1;
            }
            port = p;
            break;
        default:
            if (arg.StartsWith('-') || folder is not null)
            {
                Console.Error.WriteLine(Texts.UnknownOption(arg));
                return 1;
            }
            folder = arg;
            break;
    }
}

bool interactive = !serve && !Console.IsInputRedirected && !Console.IsOutputRedirected;

SunfishConfig config;
string? found = ConfigLocator.Find(configPath);
if (folder is not null)
{
    // Modo rapido: la carpeta en "/", con listado y solo lectura. No se guarda en ningun sitio.
    string full = Path.GetFullPath(folder);
    if (!Directory.Exists(full))
    {
        Console.Error.WriteLine(Texts.NotAFolder(full));
        return 1;
    }
    config = found is not null && File.Exists(found) ? SunfishConfig.Load(found) : new SunfishConfig();
    config.FilePath = null;
    config.Shares.Clear();
    config.Shares.Add(new ShareConfig { Name = Path.GetFileName(full.TrimEnd(Path.DirectorySeparatorChar)), Location = "/", Path = full });
    if (!interactive)
        Console.WriteLine(Texts.Sharing(full));
}
else if (found is not null && File.Exists(found))
{
    config = SunfishConfig.Load(found);
}
else if (interactive && AskWhereToCreate() is { } created)
{
    config = new SunfishConfig();
    config.Save(created);
    // Si hay una configuracion de la 2.0 a mano, se ofrece traer sus shares.
    if (LegacyImport.Find() is { } legacy)
    {
        Console.Write(Texts.ImportAsk(legacy));
        var answer = Console.ReadKey(intercept: true);
        Console.WriteLine();
        if (answer.Key is ConsoleKey.Enter or ConsoleKey.Y or ConsoleKey.S && AdminCommands.Import(legacy, created, isNew: true))
            config = SunfishConfig.Load(created);
    }
}
else
{
    if (!interactive)
    {
        Console.Error.WriteLine(Texts.NoConfig(ConfigLocator.PortablePath, ConfigLocator.UserPath));
        Console.Error.WriteLine();
        Console.Error.WriteLine(Texts.Usage);
    }
    return 1;
}

if (port is int forced)
    config.Server.HttpPort = forced;

var log = new ServerLog { MinimumLevel = verbose ? LogLevel.Detail : LogLevel.Info };
foreach (var error in config.Errors)
    log.Warning(Texts.ConfigWarning(error));

await using var host = new SunfishHost(config, log);
if (port is int overridePort)
    host.Overrides = c => c.Server.HttpPort = overridePort;
if (!interactive)
{
    log.Written += entry =>
    {
        var writer = entry.Level >= LogLevel.Warning ? Console.Error : Console.Out;
        writer.WriteLine($"{entry.Time:HH:mm:ss} {entry.Message}");
    };
    foreach (var entry in log.Recent())
        Console.Error.WriteLine(entry.Message);
}

try
{
    host.Start();
}
catch (SocketException e)
{
    // El host ya lo anota en el registro. Sin ningun servicio en marcha, "serve" no tiene sentido;
    // en la TUI se puede cambiar el puerto.
    if (!interactive && !host.HttpRunning && !host.FtpRunning)
    {
        Console.Error.WriteLine(Texts.CannotStart(e.Message));
        return 2;
    }
}
host.WatchConfigFile();

if (interactive)
{
    await new TuiApp(host).RunAsync();
    await host.StopAsync();
    return 0;
}

PrintUrls(host);
var stop = new TaskCompletionSource();
Console.CancelKeyPress += (_, e) =>
{
    e.Cancel = true;
    stop.TrySetResult();
};
AppDomain.CurrentDomain.ProcessExit += (_, _) => stop.TrySetResult();
await stop.Task;
Console.WriteLine(Texts.Stopping);
await host.StopAsync();
return 0;

string? Value(ref int i)
{
    if (i + 1 >= args.Length)
    {
        Console.Error.WriteLine(Texts.MissingValue(args[i]));
        return null;
    }
    return args[++i];
}

// Sin configuracion y con terminal (p. ej. doble clic): se ofrece crearla.
static string? AskWhereToCreate()
{
    Console.Write(TuiTexts.NoConfigQuestion(ConfigLocator.PortablePath, ConfigLocator.UserPath));
    while (true)
    {
        var key = Console.ReadKey(intercept: true);
        if (key.KeyChar == '1')
        {
            Console.WriteLine("1");
            return ConfigLocator.PortablePath;
        }
        if (key.KeyChar == '2')
        {
            Console.WriteLine("2");
            return ConfigLocator.UserPath;
        }
        if (key.Key == ConsoleKey.Escape)
        {
            Console.WriteLine();
            return null;
        }
    }
}

static void PrintUrls(SunfishHost host)
{
    var shares = host.Config.Shares.Where(s => s.Enabled).ToList();
    Console.WriteLine();
    if (shares.Count == 0)
    {
        Console.WriteLine(Texts.NoShares);
    }
    else
    {
        Console.WriteLine(Texts.OpenWith);
        var addresses = LocalAddresses.Get();
        foreach (var share in shares)
        {
            foreach (var address in addresses)
            {
                if (host.HttpRunning)
                    Console.WriteLine($"  {LocalAddresses.HttpUrl(address.Address, host.HttpPort, share)}   ({address.Interface})");
                if (host.FtpRunning && share.Ftp)
                    Console.WriteLine($"  {LocalAddresses.FtpUrl(address.Address, host.FtpPort, share)}   ({address.Interface})");
            }
        }
    }
    Console.WriteLine();
    Console.WriteLine(Texts.StopHint);
    Console.WriteLine();
}