using NLog; using NLog.Targets; using SQLite; using System; // Use different command line parser for more standardized output // (like help text) using System.CommandLine; using System.CommandLine.Parsing; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; namespace DBMerger { public class Program { private static readonly Logger logger = LogManager.GetLogger("DBMerger"); // TODO: Consider config class? public static SQLiteConnection DBConn { get; private set; } public static SQLiteConnection OldDBConn { get; private set; } public static Config Config { get; private set; } public static void Main(string[] args) { ProcessArgs(args); ConfigureLogger(); if (Config.Debug) { // Needed? mostly just covering my ass logger.Warn(new string('=', 100)); logger.Warn("WARNING:".PadLeft(46)); logger.Warn("Debug mode will output some sensitive information (friends list, friend history, etc.)"); logger.Warn("Only use this mode for debug purposes. Enter `y` to confirm or anything else to exit."); logger.Warn(new string('=', 100)); if (Console.ReadLine() != "y") { return; } } var asm = Assembly.GetExecutingAssembly(); var versionInfo = FileVersionInfo.GetVersionInfo(asm.Location); logger.Info($"{versionInfo.ProductName}-{versionInfo.ProductVersion}"); logger.Info($"by {versionInfo.LegalCopyright}\n"); if (Path.GetFullPath(Config.NewDBPath) == Path.GetFullPath(Config.OldDBPath)) { logger.Fatal("Database pathes cannot be the same!"); return; } try { logger.Debug("Creating connection to old DB"); try { DBConn = new SQLiteConnection(Config.OldDBPath) { Tracer = logger.Trace, Trace = true }; } catch (SQLiteException) { logger.Fatal("Could not connect to old DB. Perhaps passed in db is corrupt or not a valid sqlite db?"); return; } logger.Debug("Creating connection to new DB"); try { DBConn.Execute("ATTACH DATABASE ? AS new_db", Config.NewDBPath); } catch (SQLiteException) { logger.Fatal("Could not connect to new DB. Perhaps passed in db is corrupt or not a valid sqlite db?"); return; } logger.Info("Database connections created successfully!"); CreateBackup(); try { new Merger(DBConn, "main", "new_db", Config).Merge(); } catch (Exception ex) { logger.Fatal(ex, "Merge process failed with error:\n"); } } finally { logger.Debug("Closing database connection..."); DBConn.Close(); } } private static void ProcessArgs(string[] args) { static string validateDBPath(ArgumentResult arg) { string path = arg.Tokens[0].Value; Option option = arg.Argument.Parents.Single() as Option; string extension = Path.GetExtension(path); if (extension == "") { path += Path.ChangeExtension(path, "sqlite3"); } else if (extension != ".sqlite3") { arg.ErrorMessage = $"File given to option `{option.Aliases.First()}` is not a sqlite database!"; return null; } if (!File.Exists(path)) { arg.ErrorMessage = $"File given to option `{option.Aliases.First()}` does not exist!"; return null; } return path; } var rootCommand = new RootCommand("Merge an old and new VRCX sqlite database into one."); var newDBOption = new Option( ["-n", "--new-db-path"], description: "The path of the new DB to merge the old onto.", parseArgument: validateDBPath ) { IsRequired = true }; rootCommand.AddOption(newDBOption); var oldDBOption = new Option( ["-o", "--old-db-path"], description: "The path of the old DB to merge into the new.", parseArgument: validateDBPath ) { IsRequired = true }; rootCommand.AddOption(oldDBOption); // Add `debug` option to be consistent with args from the main exe var debugOption = new Option(["-v", "--verbose", "-d", "--debug"], () => false, "Add debug information to the output."); rootCommand.AddOption(debugOption); var importConfigOption = new Option(["--import-config"], () => false, "Imports the config values from the old database. This will override the config in the new database."); rootCommand.AddOption(importConfigOption); rootCommand.SetHandler((newDBPath, oldDBPath, debug, importConfig) => { Config = new Config(newDBPath, oldDBPath, debug, importConfig); }, newDBOption, oldDBOption, debugOption, importConfigOption); // If the args weren't parsable or verifiable, exit if (rootCommand.Invoke(args) != 0) { Environment.Exit(0); } } private static void ConfigureLogger() { LogManager.Setup().LoadConfiguration(builder => { var fileTarget = new FileTarget("fileTarget") { FileName = "DBMerger.log", Layout = "${longdate} [${level:uppercase=true:padding=-5}] ${logger:padding=-20} - ${message} ${exception:format=tostring}", ArchiveSuffixFormat = "{1:yyyy-MM-dd.HH-mm-ss}", ArchiveOldFileOnStartup = true, KeepFileOpen = true, AutoFlush = true, Encoding = System.Text.Encoding.UTF8 }; var consoleTarget = new ColoredConsoleTarget() { Layout = "[${level:uppercase=true:padding=-5}] ${message} ${exception:format=tostring}", AutoFlush = true, Encoding = System.Text.Encoding.UTF8 }; builder.ForLogger().FilterMinLevel(Config.Debug ? LogLevel.Trace : LogLevel.Debug).WriteTo(fileTarget); builder.ForLogger().FilterMinLevel(Config.Debug ? LogLevel.Trace : LogLevel.Info).WriteTo(consoleTarget); }); } private static void CreateBackup() { // Get unique name for backup. Format matches the log file name format string date = DateTime.Now.ToString("yyyyMMdd"); int counter = 0; string backupPath; do { backupPath = Path.Combine(Path.GetDirectoryName(Config.NewDBPath), $"VRCX.back.{date}.{counter}.sqlite3"); counter++; } while (File.Exists(backupPath)); File.Copy(Config.NewDBPath, backupPath); logger.Info($"Created backup of new DB at {backupPath}"); } } }