/* * Copyright (C) 2012-2017 CypherCore * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ using Framework.Configuration; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; namespace Framework.Database { public class DatabaseUpdater { public DatabaseUpdater(MySqlBase database) { _database = database; } public bool Populate() { SQLResult result = _database.Query("SHOW TABLES"); if (!result.IsEmpty() && result.GetRowCount() > 0) return true; Log.outInfo(LogFilter.SqlUpdates, "Database {0} is empty, auto populating it...", _database.GetDatabaseName()); string path = GetSourceDirectory(); string fileName = "Unknown"; switch (_database.GetType().Name) { case "LoginDatabase": fileName = @"\sql\base\auth_database.sql"; break; case "CharacterDatabase": fileName = @"\sql\base\characters_database.sql"; break; case "WorldDatabase": fileName = @"\sql\base\TDB_world_703.00_2016_10_17.sql"; break; case "HotfixDatabase": fileName = @"\sql\base\TDB_hotfixes_703.00_2016_10_17.sql"; break; } if (!File.Exists(path + fileName)) { Log.outError(LogFilter.SqlUpdates, "File \"{0}\" is missing, download it from \"http://www.trinitycore.org/f/files/category/1-database/\"" + " and place it in your worldserver directory.", fileName); return false; } // Update database Log.outInfo(LogFilter.SqlUpdates, "Applying \'{0}\'...", fileName); _database.ApplyFile(path + fileName); Log.outInfo(LogFilter.SqlUpdates, "Done Applying \'{0}\'", fileName); return true; } public bool Update() { Log.outInfo(LogFilter.SqlUpdates, "Updating {0} database...", _database.GetDatabaseName()); string sourceDirectory = GetSourceDirectory(); if (!Directory.Exists(sourceDirectory)) { Log.outError(LogFilter.SqlUpdates, "DBUpdater: Given source directory {0} does not exist, skipped!", sourceDirectory); return false; } var availableFiles = GetFileList(); var appliedFiles = ReceiveAppliedFiles(); bool redundancyChecks = ConfigMgr.GetDefaultValue("Updates.Redundancy", true); bool archivedRedundancy = ConfigMgr.GetDefaultValue("Updates.Redundancy", true); UpdateResult result = new UpdateResult(); // Count updates foreach (var entry in appliedFiles) { if (entry.Value.State == State.RELEASED) ++result.recent; else ++result.archived; } foreach (var availableQuery in availableFiles) { Log.outDebug(LogFilter.SqlUpdates, "Checking update \"{0}\"...", availableQuery.GetFileName()); var applied = appliedFiles.LookupByKey(availableQuery.GetFileName()); if (applied != null) { // If redundancy is disabled skip it since the update is already applied. if (!redundancyChecks) { Log.outDebug(LogFilter.SqlUpdates, "Update is already applied, skipping redundancy checks."); appliedFiles.Remove(availableQuery.GetFileName()); continue; } // If the update is in an archived directory and is marked as archived in our database skip redundancy checks (archived updates never change). if (!archivedRedundancy && (applied.State == State.ARCHIVED) && (availableQuery.state == State.ARCHIVED)) { Log.outDebug(LogFilter.SqlUpdates, "Update is archived and marked as archived in database, skipping redundancy checks."); appliedFiles.Remove(availableQuery.GetFileName()); continue; } } // Calculate hash string hash = CalculateHash(availableQuery.path); UpdateMode mode = UpdateMode.Apply; // Update is not in our applied list if (applied == null) { // Catch renames (different filename but same hash) var hashIter = appliedFiles.Values.FirstOrDefault(p => p.Hash == hash); if (hashIter != null) { // Check if the original file was removed if not we've got a problem. var renameFile = availableFiles.Find(p => p.GetFileName() == hashIter.Name); if (renameFile != null) { Log.outWarn(LogFilter.SqlUpdates, "Seems like update \"{0}\" \'{1}\' was renamed, but the old file is still there! " + "Trade it as a new file! (Probably its an unmodified copy of file \"{2}\")", availableQuery.GetFileName(), hash.Substring(0, 7), renameFile.GetFileName()); } // Its save to trade the file as renamed here else { Log.outInfo(LogFilter.SqlUpdates, "Renaming update \"{0}\" to \"{1}\" \'{2}\'.", hashIter.Name, availableQuery.GetFileName(), hash.Substring(0, 7)); RenameEntry(hashIter.Name, availableQuery.GetFileName()); appliedFiles.Remove(hashIter.Name); continue; } } // Apply the update if it was never seen before. else { Log.outInfo(LogFilter.SqlUpdates, "Applying update \"{0}\" \'{1}\'...", availableQuery.GetFileName(), hash.Substring(0, 7)); } } // Rehash the update entry if it is contained in our database but with an empty hash. else if (ConfigMgr.GetDefaultValue("Updates.AllowRehash", true) && string.IsNullOrEmpty(applied.Hash)) { mode = UpdateMode.Rehash; Log.outInfo(LogFilter.SqlUpdates, "Re-hashing update \"{0}\" \'{1}\'...", availableQuery.GetFileName(), hash.Substring(0, 7)); } else { // If the hash of the files differs from the one stored in our database reapply the update (because it was changed). if (applied.Hash != hash) { Log.outInfo(LogFilter.SqlUpdates, "Reapplying update \"{0}\" \'{1}\' . \'{2}\' (it changed)...", availableQuery.GetFileName(), applied.Hash.Substring(0, 7), hash.Substring(0, 7)); } else { // If the file wasn't changed and just moved update its state if necessary. if (applied.State != availableQuery.state) { Log.outDebug(LogFilter.SqlUpdates, "Updating state of \"{0}\" to \'{1}\'...", availableQuery.GetFileName(), availableQuery.state); UpdateState(availableQuery.GetFileName(), availableQuery.state); } Log.outDebug(LogFilter.SqlUpdates, "Update is already applied and is matching hash \'{0}\'.", hash.Substring(0, 7)); appliedFiles.Remove(applied.Name); continue; } } uint speed = 0; AppliedFileEntry file = new AppliedFileEntry(availableQuery.GetFileName(), hash, availableQuery.state, 0); switch (mode) { case UpdateMode.Apply: speed = ApplyTimedFile(availableQuery.path); goto case UpdateMode.Rehash; case UpdateMode.Rehash: UpdateEntry(file, speed); break; } if (applied != null) appliedFiles.Remove(applied.Name); if (mode == UpdateMode.Apply) ++result.updated; } // Cleanup up orphaned entries if enabled if (!appliedFiles.Empty()) { int cleanDeadReferencesMaxCount = ConfigMgr.GetDefaultValue("Updates.CleanDeadRefMaxCount", 3); bool doCleanup = (cleanDeadReferencesMaxCount < 0) || (appliedFiles.Count <= cleanDeadReferencesMaxCount); foreach (var entry in appliedFiles) { Log.outWarn(LogFilter.SqlUpdates, "File \'{0}\' was applied to the database but is missing in your update directory now!", entry.Key); if (doCleanup) Log.outInfo(LogFilter.SqlUpdates, "Deleting orphaned entry \'{0}\'...", entry.Key); } if (doCleanup) CleanUp(appliedFiles); else { Log.outError(LogFilter.SqlUpdates, "Cleanup is disabled! There are {0} dirty files that were applied to your database but are now missing in your source directory!", appliedFiles.Count); } } string info = string.Format("Containing {0} new and {1} archived updates.", result.recent, result.archived); if (result.updated == 0) Log.outInfo(LogFilter.SqlUpdates, "{0} database is up-to-date! {1}", _database.GetDatabaseName(), info); else Log.outInfo(LogFilter.SqlUpdates, "Applied {0} {1}. {2}", result.updated, result.updated == 1 ? "query" : "queries", info); return true; } string GetSourceDirectory() { string entry = ConfigMgr.GetDefaultValue("Updates.SourcePath", Environment.CurrentDirectory); if (!string.IsNullOrEmpty(entry)) return entry; else return Directory.GetParent(Directory.GetCurrentDirectory()).Parent.FullName; } uint ApplyTimedFile(string path) { // Benchmark query speed uint oldMSTime = Time.GetMSTime(); // Update database _database.ApplyFile(path); // Return time the query took to apply return Time.GetMSTimeDiffToNow(oldMSTime); } void UpdateEntry(AppliedFileEntry entry, uint speed) { string update = "REPLACE INTO `updates` (`name`, `hash`, `state`, `speed`) VALUES (\"" + entry.Name + "\", \"" + entry.Hash + "\", \'" + entry.State + "\', " + speed + ")"; // Update database _database.Execute(update); } void RenameEntry(string from, string to) { // Delete target if it exists { string update = "DELETE FROM `updates` WHERE `name`=\"" + to + "\""; // Update database _database.Execute(update); } // Rename { string update = "UPDATE `updates` SET `name`=\"" + to + "\" WHERE `name`=\"" + from + "\""; // Update database _database.Execute(update); } } void CleanUp(Dictionary storage) { if (storage.Empty()) return; int remaining = storage.Count; string update = "DELETE FROM `updates` WHERE `name` IN("; foreach (var entry in storage) { update += "\"" + entry.Key + "\""; if ((--remaining) > 0) update += ", "; } update += ")"; // Update database _database.Execute(update); } void UpdateState(string name, State state) { string update = "UPDATE `updates` SET `state`=\'" + state + "\' WHERE `name`=\"" + name + "\""; // Update database _database.Execute(update); } public List GetFileList() { List fileList = new List(); SQLResult result = _database.Query("SELECT `path`, `state` FROM `updates_include`"); if (result.IsEmpty()) return fileList; do { string path = result.Read(0); if (path[0] == '$') path = GetSourceDirectory() + path.Substring(1); if (!Directory.Exists(path)) { Log.outWarn(LogFilter.SqlUpdates, "DBUpdater: Given update include directory \"{0}\" isn't existing, skipped!", path); continue; } State state = result.Read(1).ToEnum(); fileList.AddRange(GetFilesFromDirectory(path, state)); Log.outDebug(LogFilter.SqlUpdates, "Added applied file \"{0}\" from remote.", path); } while (result.NextRow()); return fileList; } public Dictionary ReceiveAppliedFiles() { Dictionary map = new Dictionary(); SQLResult result = _database.Query("SELECT `name`, `hash`, `state`, UNIX_TIMESTAMP(`timestamp`) FROM `updates` ORDER BY `name` ASC"); if (result.IsEmpty()) return map; do { AppliedFileEntry entry = new AppliedFileEntry(result.Read(0), result.Read(1), result.Read(2).ToEnum(), result.Read(3)); map.Add(entry.Name, entry); } while (result.NextRow()); return map; } IEnumerable GetFilesFromDirectory(string directory, State state) { Queue queue = new Queue(); queue.Enqueue(directory); while (queue.Count > 0) { directory = queue.Dequeue(); try { foreach (string subDir in Directory.GetDirectories(directory)) { queue.Enqueue(subDir); } } catch (Exception ex) { Console.Error.WriteLine(ex); } string[] files = Directory.GetFiles(directory, "*.sql"); for (int i = 0; i < files.Length; i++) { yield return new FileEntry(files[i], state); } } } string CalculateHash(string fileName) { using (SHA1 sha1 = new SHA1Managed()) { string text = File.ReadAllText(fileName).Replace("\r", ""); return sha1.ComputeHash(Encoding.UTF8.GetBytes(text)).ToHexString(); } } protected MySqlBase _database; } public class AppliedFileEntry { public AppliedFileEntry(string name, string hash, State state, ulong timestamp) { Name = name; Hash = hash; State = state; Timestamp = timestamp; } public string Name; public string Hash; public State State; public ulong Timestamp; } public class FileEntry { public FileEntry(string _path, State _state) { path = _path; state = _state; } public string GetFileName() { return Path.GetFileName(path); } public string path; public State state; } struct UpdateResult { public int updated; public int recent; public int archived; } public enum State { RELEASED, ARCHIVED } enum UpdateMode { Apply, Rehash } }