using FSO.Server.Database.DA; using System; using System.Collections.Generic; using System.Linq; using System.Text; using Dapper; using System.IO; using System.Security.Cryptography; using System.Text.RegularExpressions; using FSO.Server.Database.DA.DbChanges; using FSO.Server.Common; using MySql.Data.MySqlClient; namespace FSO.Server.Database.Management { public class DbChangeTool { private ISqlContext Context; public DbChangeTool(ISqlContext context) { this.Context = context; } public List GetChanges() { List changes = new List(); var connection = Context.Connection; var hasChangesTable = connection.Query("SHOW TABLES LIKE 'fso_db_changes'").FirstOrDefault() != null; var scriptsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "DatabaseScripts"); var manifest = Newtonsoft.Json.JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(scriptsDirectory, "manifest.json"))); foreach(var script in manifest.Changes) { var scriptData = File.ReadAllText(Path.Combine(scriptsDirectory, script.Script)); changes.Add(new DbChangeScript { ScriptFilename = script.Script, Idempotent = script.Idempotent, ScriptID = script.ID, ScriptData = scriptData, Status = DbChangeScriptStatus.NOT_INSTALLED, Hash = hashScriptFile(scriptData) }); } if (hasChangesTable){ var changeHistory = connection.Query("SELECT * FROM fso_db_changes"); foreach(var changeHistoryItem in changeHistory) { var match = changes.FirstOrDefault(x => x.ScriptID == changeHistoryItem.id); if(match == null) { continue; } if(match.Hash != changeHistoryItem.hash) { match.Status = DbChangeScriptStatus.MODIFIED; } else { match.Status = DbChangeScriptStatus.INSTALLED; } } } return changes; } public void ApplyChange(DbChangeScript change, bool repair) { var connection = Context.Connection; using (var transaction = connection.BeginTransaction(System.Data.IsolationLevel.RepeatableRead)) { if (!repair) { var cmd = transaction.Connection.CreateCommand(); cmd.CommandText = change.ScriptData; try { cmd.ExecuteNonQuery(); } catch (MySqlException e) { throw new DbMigrateException(e.ToString()); } } connection.Execute("INSERT INTO fso_db_changes VALUES (@id, @filename, @date, @hash) ON DUPLICATE KEY UPDATE hash=@hash, date = @date, filename = @filename", new DbChange { id = change.ScriptID, date = Epoch.Now, filename = change.ScriptFilename, hash = change.Hash }); transaction.Commit(); } } private string hashScriptFile(string sqlFileData) { //People often mess up whitespace in sql files in their IDE. Normalize whitespace before hashing //to try and avoid changes sqlFileData = Regex.Replace(sqlFileData, @"\s+", " ").Trim(); MD5 md5 = System.Security.Cryptography.MD5.Create(); byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(sqlFileData); byte[] hash = md5.ComputeHash(inputBytes); // step 2, convert byte array to hex string StringBuilder sb = new StringBuilder(); for (int i = 0; i < hash.Length; i++) { sb.Append(hash[i].ToString("X2")); } return sb.ToString(); } } public class DbMigrateException : Exception { public DbMigrateException(string message) : base(message) { } } public class DbChangeScript { public DbChangeScriptStatus Status; public string ScriptID; public string ScriptFilename; public string ScriptData; public string Hash; public bool Idempotent; public List Requires; } public enum DbChangeScriptStatus { INSTALLED, MODIFIED, NOT_INSTALLED, FORCE_REINSTALL } public class DbChangeManifest { public List Changes; } public class DbChangeManifestScript { public string ID; public string Script; public bool Idempotent; } }