using System; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using SNote.Server.Data; using SNote.Server.Endpoints; using SNote.Server.Security; namespace SNote.Server; public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add Configuration & DB Context (SQLite) var dbPath = Path.Combine(AppContext.BaseDirectory, "snote_server.db"); builder.Services.AddDbContext(options => options.UseSqlite($"Data Source={dbPath}")); // Register custom security managers & HTTP utilities builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Register OpenAPI explorer builder.Services.AddOpenApi(); var app = builder.Build(); // 1. Initialize SQLite Database Schema using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureCreated(); } // Configure Development Tools if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } // 2. Map Modular API Endpoint Classes app.MapAuthEndpoints(); app.MapNodeEndpoints(); app.MapSyncEndpoints(); // Welcome / Healthcheck root app.MapGet("/", () => Results.Ok(new { service = "SNote Server", status = "Online", time = DateTime.UtcNow })); // 3. Start Background Sync Runner var hostApplicationLifetime = app.Services.GetRequiredService(); var services = app.Services; var config = app.Configuration; // Dynamic Handshake & Bootstrapping: Connect to target destination server on startup, and optionally bootstrap var destUrl = config["Sync:DestinationServerUrl"]; var localUrl = config["Sync:LocalServerUrl"]; if (!string.IsNullOrEmpty(destUrl) && !string.IsNullOrEmpty(localUrl)) { Task.Run(async () => { // Wait briefly for server startup await Task.Delay(2000); using var scope = services.CreateScope(); var rsaKeyManager = scope.ServiceProvider.GetRequiredService(); var configuration = scope.ServiceProvider.GetRequiredService(); var client = scope.ServiceProvider.GetRequiredService(); // 1. Perform handshake await SyncEndpoints.RegisterPeerWithDestinationAsync(destUrl, localUrl, rsaKeyManager, configuration, client); // 2. Check if we have successfully handshaked and obtained a token if (!string.IsNullOrEmpty(SyncEndpoints.UpstreamSessionToken)) { var bootstrapMode = configuration["Sync:BootstrapFromUpstream"] ?? "Never"; bool doBootstrap = false; if (string.Equals(bootstrapMode, "Always", StringComparison.OrdinalIgnoreCase)) { doBootstrap = true; } else if (string.Equals(bootstrapMode, "FirstTime", StringComparison.OrdinalIgnoreCase)) { var flagPath = Path.Combine(AppContext.BaseDirectory, "snote_bootstrapped.flag"); if (!File.Exists(flagPath)) { doBootstrap = true; } else { Console.WriteLine("[Bootstrap] Skipped because BootstrapFromUpstream is set to FirstTime and server has bootstrapped before."); } } if (doBootstrap) { var db = scope.ServiceProvider.GetRequiredService(); await SyncEndpoints.BootstrapFromPeerServerAsync(destUrl, localUrl, SyncEndpoints.UpstreamSessionToken, db, client); // If it's FirstTime, write the flag file if (string.Equals(bootstrapMode, "FirstTime", StringComparison.OrdinalIgnoreCase)) { try { var flagPath = Path.Combine(AppContext.BaseDirectory, "snote_bootstrapped.flag"); await File.WriteAllTextAsync(flagPath, DateTime.UtcNow.ToString("o")); Console.WriteLine("[Bootstrap] Wrote bootstrap flag file."); } catch (Exception ex) { Console.WriteLine($"[Bootstrap] Warning: Could not write bootstrap flag file: {ex.Message}"); } } } } else { Console.WriteLine("[Bootstrap] Skipped because handshake failed (no token obtained)."); } }); } // Heartbeat Loop: Periodic Downstream Heartbeat pinger pings downstream peers every 30 seconds Task.Run(async () => { var token = hostApplicationLifetime.ApplicationStopping; using var scope = services.CreateScope(); var peerCache = scope.ServiceProvider.GetRequiredService(); var rsaKeyManager = scope.ServiceProvider.GetRequiredService(); var client = scope.ServiceProvider.GetRequiredService(); await SyncEndpoints.StartHeartbeatPingerAsync(peerCache, rsaKeyManager, client, token); }); app.Run(); } }