using System; using System.Collections.Generic; using System.IO; namespace SyncEngine.Configuration { public static class SyncOptionsValidator { private static readonly string[] PlaceholderPathFragments = { @"c:\path", @"/path/", "your_", "yoursite.com", "changeme", "<", ">" }; public static void Validate(SyncOptions options, string mode, bool envFileFound) { var errors = new List(); var normalized = (mode ?? "all").Trim().ToLowerInvariant(); if (!envFileFound) { errors.Add("No .env file found. Copy .env.example to .env beside SyncEngine.exe (or the repo root) and fill in your connection details."); } if (normalized != "all" && normalized != "access" && normalized != "mysql" && normalized != "schema-only") { errors.Add("Unknown mode '" + mode + "'. Use: all, access, mysql, schema-only"); throw new ConfigurationException(errors); } if (!options.SyncAccessToSqlExpress && !options.SyncSqlExpressToMySql) { errors.Add("Both sync legs are disabled. Set SYNC_ACCESS_TO_SQL_EXPRESS and/or SYNC_SQL_EXPRESS_TO_MYSQL to true."); } if (options.BatchSize <= 0) { errors.Add("SYNC_BATCH_SIZE must be greater than 0."); } if (options.MaxRetries < 0) { errors.Add("SYNC_MAX_RETRIES cannot be negative."); } var runAccess = options.SyncAccessToSqlExpress && (normalized == "all" || normalized == "access" || normalized == "schema-only"); var runMySql = options.SyncSqlExpressToMySql && (normalized == "all" || normalized == "mysql"); if (runAccess) { ValidateAccessLeg(options, errors); } if (runMySql) { ValidateMySqlLeg(options, errors); } if (errors.Count > 0) { throw new ConfigurationException(errors); } } private static void ValidateAccessLeg(SyncOptions options, IList errors) { if (string.IsNullOrWhiteSpace(options.AccessDbPath)) { errors.Add("ACCESS_DB_PATH is required for Access → SQL Express sync."); return; } if (LooksLikePlaceholder(options.AccessDbPath)) { errors.Add("ACCESS_DB_PATH still looks like a placeholder (" + options.AccessDbPath + "). Set the real path to your .mdb file."); } else if (!File.Exists(options.AccessDbPath)) { errors.Add("ACCESS_DB_PATH file not found: " + options.AccessDbPath); } if (string.IsNullOrWhiteSpace(options.SqlExpressDatabase)) { errors.Add("SQL_EXPRESS_DATABASE is required for Access → SQL Express sync."); } if (string.IsNullOrWhiteSpace(options.SqlExpressConnectionString)) { errors.Add("SQL_EXPRESS_CONNECTION_STRING is required for Access → SQL Express sync."); } else if (LooksLikePlaceholder(options.SqlExpressConnectionString)) { errors.Add("SQL_EXPRESS_CONNECTION_STRING still looks like a placeholder. Set your SQL Express server and credentials."); } if (string.IsNullOrWhiteSpace(options.SqlExpressSchema)) { errors.Add("SQL_EXPRESS_SCHEMA is required (default: dbo)."); } } private static void ValidateMySqlLeg(SyncOptions options, IList errors) { if (string.IsNullOrWhiteSpace(options.SqlExpressConnectionString)) { errors.Add("SQL_EXPRESS_CONNECTION_STRING is required for SQL Express → MySQL sync (source read)."); } if (string.IsNullOrWhiteSpace(options.MySqlHost)) { errors.Add("MYSQL_HOST is required for SQL Express → MySQL sync."); } if (string.IsNullOrWhiteSpace(options.MySqlUser)) { errors.Add("MYSQL_USER is required for SQL Express → MySQL sync."); } if (string.IsNullOrWhiteSpace(options.MySqlDatabase)) { errors.Add("MYSQL_DATABASE is required for SQL Express → MySQL sync."); } if (options.MySqlPort <= 0 || options.MySqlPort > 65535) { errors.Add("MYSQL_PORT must be between 1 and 65535."); } ValidateShopSyncApi(options, errors); } private static void ValidateShopSyncApi(SyncOptions options, IList errors) { var hasUrl = !string.IsNullOrWhiteSpace(options.ShopSyncApiUrl); var hasKey = !string.IsNullOrWhiteSpace(options.ShopSyncApiKey); if (hasUrl != hasKey) { errors.Add("SHOP_SYNC_API_URL and SHOP_SYNC_API_KEY must both be set for API log fallback, or both left empty."); return; } if (!hasUrl) { return; } if (LooksLikePlaceholder(options.ShopSyncApiUrl)) { errors.Add("SHOP_SYNC_API_URL still looks like a placeholder (" + options.ShopSyncApiUrl + ")."); } Uri uri; if (!Uri.TryCreate(options.ShopSyncApiUrl.Trim(), UriKind.Absolute, out uri) || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) { errors.Add("SHOP_SYNC_API_URL must be an absolute http or https URL."); } } public static bool LooksLikePlaceholder(string value) { if (string.IsNullOrWhiteSpace(value)) { return false; } var lower = value.Trim().ToLowerInvariant(); foreach (var fragment in PlaceholderPathFragments) { if (lower.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } } }