162 lines
6.4 KiB
C#
162 lines
6.4 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using SVSim.Bootstrap.Importers;
|
|
using SVSim.Database;
|
|
|
|
namespace SVSim.Bootstrap;
|
|
|
|
public static class Program
|
|
{
|
|
private const string DefaultConnectionString =
|
|
"Host=localhost;Database=svsim;Username=postgres;password=postgres";
|
|
|
|
public static async Task<int> Main(string[] args)
|
|
{
|
|
if (args.Length > 0 && (args[0] is "--help" or "-h"))
|
|
{
|
|
PrintUsage();
|
|
return 1;
|
|
}
|
|
|
|
var opts = ParseArgs(args);
|
|
if (opts is null)
|
|
{
|
|
PrintUsage();
|
|
return 1;
|
|
}
|
|
|
|
if (opts.SkipCards && opts.SkipGlobals)
|
|
{
|
|
Console.Error.WriteLine("Both --skip-cards and --skip-globals set; nothing to do.");
|
|
return 1;
|
|
}
|
|
|
|
Console.WriteLine($"[Bootstrap] Connection: {RedactPassword(opts.ConnectionString)}");
|
|
Console.WriteLine($"[Bootstrap] Cards file: {opts.CardsFile}");
|
|
Console.WriteLine($"[Bootstrap] Captures: {opts.CapturesDir}");
|
|
|
|
var dbOptions = new DbContextOptionsBuilder<SVSimDbContext>()
|
|
.UseNpgsql(opts.ConnectionString)
|
|
.Options;
|
|
|
|
await using var context = new SVSimDbContext(NullLogger<SVSimDbContext>.Instance, dbOptions);
|
|
|
|
// Bootstrap applies pending migrations first so it can be the very first thing run after
|
|
// `dotnet ef migrations add` — no need to start the server too.
|
|
Console.WriteLine("[Bootstrap] Applying pending migrations...");
|
|
await context.Database.MigrateAsync();
|
|
|
|
if (!opts.SkipCards)
|
|
{
|
|
await new CardImporter().ImportAsync(context, opts.CardsFile);
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("[Bootstrap] --skip-cards set; skipping card import.");
|
|
}
|
|
|
|
if (!opts.SkipGlobals)
|
|
{
|
|
await new GlobalsImporter().ImportAllAsync(context, opts.CapturesDir);
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("[Bootstrap] --skip-globals set; skipping globals import.");
|
|
}
|
|
|
|
Console.WriteLine("[Bootstrap] Complete.");
|
|
return 0;
|
|
}
|
|
|
|
private static BootstrapOptions? ParseArgs(string[] args)
|
|
{
|
|
string? dataDir = null;
|
|
string? cards = null;
|
|
string? captures = null;
|
|
string? connection = null;
|
|
bool skipCards = false;
|
|
bool skipGlobals = false;
|
|
string? positionalCards = null;
|
|
|
|
for (int i = 0; i < args.Length; i++)
|
|
{
|
|
string a = args[i];
|
|
switch (a)
|
|
{
|
|
case "--data-dir": dataDir = NextArg(args, ref i); break;
|
|
case "--cards": cards = NextArg(args, ref i); break;
|
|
case "--captures": captures = NextArg(args, ref i); break;
|
|
case "--connection-string": connection = NextArg(args, ref i); break;
|
|
case "--skip-cards": skipCards = true; break;
|
|
case "--skip-globals": skipGlobals = true; break;
|
|
default:
|
|
// Back-compat: legacy positional form `svsim-card-import <cards.json> [connection]`.
|
|
if (positionalCards is null && !a.StartsWith('-')) positionalCards = a;
|
|
else if (connection is null && !a.StartsWith('-')) connection = a;
|
|
else { Console.Error.WriteLine($"Unknown argument: {a}"); return null; }
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Resolution order:
|
|
// --cards beats --data-dir/cards.json beats legacy positional;
|
|
// --captures beats --data-dir/prod-captures beats Bootstrap/Data/prod-captures (shipped default).
|
|
string baseDir = AppContext.BaseDirectory;
|
|
string shippedCaptures = Path.Combine(baseDir, "Data", "prod-captures");
|
|
|
|
string cardsFile = cards
|
|
?? (dataDir is not null ? Path.Combine(dataDir, "cards.json") : null)
|
|
?? positionalCards
|
|
?? "data_dumps/cards.json";
|
|
|
|
// Resolve captures dir, falling back to the shipped copy if the data-dir path is unset
|
|
// OR points at a missing folder. (Common case: user has cards.json in data_dumps/ but
|
|
// hasn't copied prod-captures/ there — the shipped snapshot is the source of truth.)
|
|
string? capturesCandidate = captures
|
|
?? (dataDir is not null ? Path.Combine(dataDir, "prod-captures") : null);
|
|
string capturesDir = capturesCandidate is not null && Directory.Exists(capturesCandidate)
|
|
? capturesCandidate
|
|
: shippedCaptures;
|
|
|
|
string connStr = connection
|
|
?? Environment.GetEnvironmentVariable("NPGSQL_CONNECTION")
|
|
?? DefaultConnectionString;
|
|
|
|
return new BootstrapOptions(cardsFile, capturesDir, connStr, skipCards, skipGlobals);
|
|
}
|
|
|
|
private static string NextArg(string[] args, ref int i)
|
|
{
|
|
if (i + 1 >= args.Length) throw new ArgumentException($"Missing value for {args[i]}");
|
|
return args[++i];
|
|
}
|
|
|
|
private static string RedactPassword(string conn) =>
|
|
System.Text.RegularExpressions.Regex.Replace(conn, "(?i)(password=)[^;]+", "$1***");
|
|
|
|
private static void PrintUsage()
|
|
{
|
|
Console.Error.WriteLine(
|
|
"Usage: svsim-bootstrap [options]\n" +
|
|
"\n" +
|
|
" --data-dir <path> Directory containing cards.json and prod-captures/\n" +
|
|
" (default: ./data_dumps relative to working dir)\n" +
|
|
" --cards <file> Override path to cards.json\n" +
|
|
" --captures <dir> Override path to prod-captures directory\n" +
|
|
" (default: shipped Data/prod-captures next to the binary)\n" +
|
|
" --connection-string <conn> Postgres connection (or NPGSQL_CONNECTION env var,\n" +
|
|
$" then \"{DefaultConnectionString}\")\n" +
|
|
" --skip-cards Skip card import (re-run globals only)\n" +
|
|
" --skip-globals Skip globals import (cards only — legacy behavior)\n" +
|
|
"\n" +
|
|
"Back-compat: `svsim-bootstrap <cards.json> [connection]` still works (positional).");
|
|
}
|
|
|
|
private sealed record BootstrapOptions(
|
|
string CardsFile,
|
|
string CapturesDir,
|
|
string ConnectionString,
|
|
bool SkipCards,
|
|
bool SkipGlobals);
|
|
}
|