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();
}