using System; using System.Diagnostics; using System.IO; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; using static Sozsoft.Settings.SettingsConsts; namespace Sozsoft.Platform; /// /// Veritabanı henüz hazır değilken çalışan minimal kurulum uygulaması. /// Tam ABP stack yüklemez; sadece /api/setup/* endpointlerini sunar. /// internal static class SetupAppRunner { // Veritabanı Hazırlık Kontrolü /// /// DB var mı ve AbpRoles tablosu oluşmuş mu diye kontrol eder. /// Boş DB veya bağlantı hatası durumunda false döner. /// public static bool DatabaseIsReady(IConfiguration configuration) { var connectionString = configuration.GetConnectionString(DefaultDatabaseProvider); if (string.IsNullOrWhiteSpace(connectionString)) return false; try { if (DefaultDatabaseProvider == DatabaseProvider.SqlServer) return SqlServerIsReady(connectionString); #pragma warning disable CS0162 return true; // Diğer sağlayıcılar için geçici — ileride PostgreSQL desteği eklenecek #pragma warning restore CS0162 } catch (Exception ex) { Log.Warning("Veritabanı hazırlık kontrolü başarısız: {Error}", ex.Message); return false; } } private static bool SqlServerIsReady(string connectionString) { var csb = new SqlConnectionStringBuilder(connectionString); var dbName = csb.InitialCatalog; if (string.IsNullOrEmpty(dbName)) return false; // 1) master'a bağlan — DB varlığını kontrol et var masterCsb = new SqlConnectionStringBuilder(connectionString) { InitialCatalog = "master", ConnectTimeout = 8 }; using var masterConn = new SqlConnection(masterCsb.ConnectionString); masterConn.Open(); using var dbCheck = new SqlCommand( "SELECT COUNT(1) FROM sys.databases WHERE name = @n", masterConn); dbCheck.Parameters.AddWithValue("@n", dbName); if ((int)dbCheck.ExecuteScalar() == 0) return false; // 2) Hedef DB'ye bağlan — AbpRoles tablosunun varlığını kontrol et csb.ConnectTimeout = 8; using var dbConn = new SqlConnection(csb.ConnectionString); dbConn.Open(); using var tableCheck = new SqlCommand( "SELECT COUNT(1) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'AbpRoles'", dbConn); return (int)tableCheck.ExecuteScalar() > 0; } // Minimal Kurulum Uygulaması public static async Task RunAsync(string[] args, IConfiguration configuration) { Log.Warning("Veritabanı hazır değil — kurulum modu başlatılıyor."); var builder = WebApplication.CreateBuilder(args); var extraOrigins = (configuration["App:CorsOrigins"] ?? "") .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var baseDomain = configuration["App:BaseDomain"]?.Trim(); builder.Services.AddCors(o => o.AddPolicy("Setup", policy => policy.AllowAnyHeader() .AllowAnyMethod() .AllowCredentials() .SetIsOriginAllowed(origin => { if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri)) return false; var host = uri.Host.ToLowerInvariant(); if (host is "localhost" or "127.0.0.1" or "[::1]") return true; if (!string.IsNullOrWhiteSpace(baseDomain)) { var bd = baseDomain.ToLowerInvariant(); if (host == bd || host.EndsWith("." + bd)) return true; } foreach (var o in extraOrigins) if (Uri.TryCreate(o, UriKind.Absolute, out var eo) && eo.Host.Equals(host, StringComparison.OrdinalIgnoreCase)) return true; return false; }))); builder.Host.UseSerilog(); var app = builder.Build(); app.UseCors("Setup"); app.MapGet("/api/setup/status", (IConfiguration cfg) => Results.Ok(new { dbExists = DatabaseIsReady(cfg) })); app.MapGet("/api/setup/migrate", async (IConfiguration cfg, IHostEnvironment env, IHostApplicationLifetime lifetime, HttpContext ctx, CancellationToken ct) => { ctx.Response.ContentType = "text/event-stream; charset=utf-8"; ctx.Response.Headers["Cache-Control"] = "no-cache, no-store"; ctx.Response.Headers["X-Accel-Buffering"] = "no"; await ctx.Response.Body.FlushAsync(ct); async Task Send(string level, string message) { try { var payload = JsonSerializer.Serialize(new { level, message }); await ctx.Response.WriteAsync($"data: {payload}\n\n", ct); await ctx.Response.Body.FlushAsync(ct); } catch { } } if (DatabaseIsReady(cfg)) { await Send("warn", "Veritabanı zaten hazır. Migration atlanıyor."); await Send("done", "Tamamlandı."); return; } var migratorPath = cfg["Setup:MigratorPath"] ?? Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "Sozsoft.Platform.DbMigrator")); await Send("info", "Veritabanı migration ve seed başlatılıyor..."); await Send("info", $"Migrator yolu: {migratorPath}"); var fileName = "dotnet"; var arguments = migratorPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ? $"\"{migratorPath}\"" : $"run --project \"{migratorPath}\""; // seed=true her zaman geçirilir — başlangıç kurulumu için zorunlu. // appsettings Setup:MigratorArgs ile override edilebilir. var extraArgs = cfg["Setup:MigratorArgs"] ?? "seed=true"; arguments += $" -- {extraArgs}"; await Send("info", $"Çalıştırılıyor: {fileName} {arguments}"); Process? process = null; try { process = new Process { StartInfo = new ProcessStartInfo { FileName = fileName, Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, WorkingDirectory = migratorPath, } }; process.Start(); async Task ReadStream(StreamReader reader, string level) { try { while (!reader.EndOfStream) { var line = await reader.ReadLineAsync(ct); if (line != null) await Send(level, line); } } catch (OperationCanceledException) { } } await Task.WhenAll( ReadStream(process.StandardOutput, "info"), ReadStream(process.StandardError, "warn")); await process.WaitForExitAsync(ct); if (process.ExitCode == 0) { await Send("success", "Migration ve seed başarıyla tamamlandı."); await Send("restart", "Uygulama sunucusu yeniden başlatılıyor..."); await Send("done", "Tamamlandı."); _ = Task.Delay(1500).ContinueWith(_ => lifetime.StopApplication()); } else { await Send("error", $"Migration başarısız. Çıkış kodu: {process.ExitCode}"); await Send("done", "Hata ile sonlandı."); } } catch (OperationCanceledException) { await Send("warn", "Migration isteği iptal edildi."); } catch (Exception ex) { await Send("error", $"Migration hatası: {ex.Message}"); await Send("done", "Hata ile sonlandı."); } finally { process?.Dispose(); } }); app.MapFallback(() => Results.StatusCode(503)); await app.RunAsync(); return 0; } }