aboutsummaryrefslogtreecommitdiff
path: root/src/server/shared/Updater
diff options
context:
space:
mode:
authorNaios <naios-dev@live.de>2015-03-21 00:25:21 +0100
committerNayd <dnpd.dd@gmail.com>2015-03-21 14:09:38 +0000
commit352012e53173372ebc82898e1b6854c983b01b25 (patch)
treed5e89b1d42749075fe38fec76148129657e4bbaf /src/server/shared/Updater
parent127d8a2bcc5acdcc909c7812a53e40d8dc79e437 (diff)
Core/Updates: Add an automatic database update system. Automatically detects new and edited sql updates through file lists and hashing. Detects renames, deletes and is able to create and auto import full databases. * cleanups in main.cpp of world & bnetserver * refactoring in DatabaseWorkerPool.h & MySQLConnection.cpp
Make sure you re-run cmake, because boost::iostreams was added as dependency. Maybe you need to install libboost-iostreams1.55-dev on unix as well. Import every update manual until (included) those INSERT IGNORE updates for each database. Thanks DDuarte and Shauren for your amazing ideas, help and advises. In hope that nobody gets a "Your database structure is not up to date..." anymore ,-) Signed-off-by: Naios <naios-dev@live.de> Signed-off-by: Nayd <dnpd.dd@gmail.com>
Diffstat (limited to 'src/server/shared/Updater')
-rw-r--r--src/server/shared/Updater/DBUpdater.cpp408
-rw-r--r--src/server/shared/Updater/DBUpdater.h79
-rw-r--r--src/server/shared/Updater/UpdateFetcher.cpp387
-rw-r--r--src/server/shared/Updater/UpdateFetcher.h128
4 files changed, 1002 insertions, 0 deletions
diff --git a/src/server/shared/Updater/DBUpdater.cpp b/src/server/shared/Updater/DBUpdater.cpp
new file mode 100644
index 00000000000..68c8b76e3f7
--- /dev/null
+++ b/src/server/shared/Updater/DBUpdater.cpp
@@ -0,0 +1,408 @@
+/*
+ * Copyright (C) 2008-2015 TrinityCore <http://www.trinitycore.org/>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include "DBUpdater.h"
+#include "Log.h"
+#include "revision.h"
+#include "UpdateFetcher.h"
+#include "DatabaseLoader.h"
+#include "Config.h"
+
+#include <fstream>
+#include <iostream>
+#include <unordered_map>
+#include <boost/process.hpp>
+#include <boost/process/mitigate.hpp>
+#include <boost/iostreams/device/file_descriptor.hpp>
+#include <boost/system/system_error.hpp>
+
+using namespace boost::process;
+using namespace boost::process::initializers;
+using namespace boost::iostreams;
+
+template<class T>
+std::string DBUpdater<T>::GetSourceDirectory()
+{
+ std::string const entry = sConfigMgr->GetStringDefault("Updates.SourcePath", "");
+ if (!entry.empty())
+ return entry;
+ else
+ return _SOURCE_DIRECTORY;
+}
+
+template<class T>
+std::string DBUpdater<T>::GetMySqlCli()
+{
+ std::string const entry = sConfigMgr->GetStringDefault("Updates.MySqlCLIPath", "");
+ if (!entry.empty())
+ return entry;
+ else
+ return _MYSQL_EXECUTABLE;
+}
+
+// Auth Database
+template<>
+std::string DBUpdater<LoginDatabaseConnection>::GetConfigEntry()
+{
+ return "Updates.Auth";
+}
+
+template<>
+std::string DBUpdater<LoginDatabaseConnection>::GetTableName()
+{
+ return "Auth";
+}
+
+template<>
+std::string DBUpdater<LoginDatabaseConnection>::GetBaseFile()
+{
+ return DBUpdater<LoginDatabaseConnection>::GetSourceDirectory() + "/sql/base/auth_database.sql";
+}
+
+template<>
+bool DBUpdater<LoginDatabaseConnection>::IsEnabled(uint32 const updateMask)
+{
+ // This way silences warnings under msvc
+ return (updateMask & DatabaseLoader::DATABASE_LOGIN) ? true : false;
+}
+
+// World Database
+template<>
+std::string DBUpdater<WorldDatabaseConnection>::GetConfigEntry()
+{
+ return "Updates.World";
+}
+
+template<>
+std::string DBUpdater<WorldDatabaseConnection>::GetTableName()
+{
+ return "World";
+}
+
+template<>
+std::string DBUpdater<WorldDatabaseConnection>::GetBaseFile()
+{
+ return _FULL_DATABASE;
+}
+
+template<>
+bool DBUpdater<WorldDatabaseConnection>::IsEnabled(uint32 const updateMask)
+{
+ // This way silences warnings under msvc
+ return (updateMask & DatabaseLoader::DATABASE_WORLD) ? true : false;
+}
+
+template<>
+BaseLocation DBUpdater<WorldDatabaseConnection>::GetBaseLocationType()
+{
+ return LOCATION_DOWNLOAD;
+}
+
+// Character Database
+template<>
+std::string DBUpdater<CharacterDatabaseConnection>::GetConfigEntry()
+{
+ return "Updates.Character";
+}
+
+template<>
+std::string DBUpdater<CharacterDatabaseConnection>::GetTableName()
+{
+ return "Character";
+}
+
+template<>
+std::string DBUpdater<CharacterDatabaseConnection>::GetBaseFile()
+{
+ return DBUpdater<CharacterDatabaseConnection>::GetSourceDirectory() + "/sql/base/characters_database.sql";
+}
+
+template<>
+bool DBUpdater<CharacterDatabaseConnection>::IsEnabled(uint32 const updateMask)
+{
+ // This way silences warnings under msvc
+ return (updateMask & DatabaseLoader::DATABASE_CHARACTER) ? true : false;
+}
+
+// Hotfix Database
+template<>
+std::string DBUpdater<HotfixDatabaseConnection>::GetConfigEntry()
+{
+ return "Updates.Hotfix";
+}
+
+template<>
+std::string DBUpdater<HotfixDatabaseConnection>::GetTableName()
+{
+ return "Hotfixes";
+}
+
+template<>
+std::string DBUpdater<HotfixDatabaseConnection>::GetBaseFile()
+{
+ return DBUpdater<HotfixDatabaseConnection>::GetSourceDirectory() + "/sql/base/hotfixes_database.sql";
+}
+
+template<>
+bool DBUpdater<HotfixDatabaseConnection>::IsEnabled(uint32 const updateMask)
+{
+ // This way silences warnings under msvc
+ return (updateMask & DatabaseLoader::DATABASE_HOTFIX) ? true : false;
+}
+
+// All
+template<class T>
+BaseLocation DBUpdater<T>::GetBaseLocationType()
+{
+ return LOCATION_REPOSITORY;
+}
+
+template<class T>
+bool DBUpdater<T>::CheckExecutable()
+{
+ DBUpdater<T>::Path const exe(DBUpdater<T>::GetMySqlCli());
+ if (!exists(exe))
+ {
+ // Check for mysql in path
+ std::vector<std::string> args = {"--version"};
+ uint32 ret;
+ try
+ {
+ child c = execute(run_exe("mysql"), set_args(args), throw_on_error(), close_stdout());
+ ret = wait_for_exit(c);
+ }
+ catch (boost::system::system_error&)
+ {
+ ret = EXIT_FAILURE;
+ }
+
+ if (ret == EXIT_FAILURE)
+ {
+ TC_LOG_FATAL("sql.updates", "Didn't find executeable mysql binary at \'%s\', correct the path in the *.conf (\"Updates.MySqlCLIPath\").",
+ absolute(exe).generic_string().c_str());
+
+ return false;
+ }
+ }
+ return true;
+}
+
+template<class T>
+bool DBUpdater<T>::Create(DatabaseWorkerPool<T>& pool)
+{
+ TC_LOG_INFO("sql.updates", "Database \"%s\" does not exist, do you want to create it? [yes (default) / no]: ",
+ pool.GetConnectionInfo()->database.c_str());
+
+ std::string answer;
+ std::getline(std::cin, answer);
+ if (!answer.empty() && !(answer.substr(0, 1) == "y"))
+ return false;
+
+ TC_LOG_INFO("sql.updates", "Creating database \"%s\"...", pool.GetConnectionInfo()->database.c_str());
+
+ // Path of temp file
+ static Path const temp("create_table.sql");
+
+ // Create temporary query to use external mysql cli
+ std::ofstream file(temp.generic_string());
+ if (!file.is_open())
+ {
+ TC_LOG_FATAL("sql.updates", "Failed to create temporary query file \"%s\"!", temp.generic_string().c_str());
+ return false;
+ }
+
+ file << "CREATE DATABASE `" << pool.GetConnectionInfo()->database << "` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci\n\n";
+
+ file.close();
+
+ try
+ {
+ DBUpdater<T>::ApplyFile(pool, pool.GetConnectionInfo()->host, pool.GetConnectionInfo()->user, pool.GetConnectionInfo()->password,
+ pool.GetConnectionInfo()->port_or_socket, "", temp);
+ }
+ catch (UpdateException&)
+ {
+ TC_LOG_FATAL("sql.updates", "Failed to create database %s! Has the user `CREATE` priviliges?", pool.GetConnectionInfo()->database.c_str());
+ boost::filesystem::remove(temp);
+ return false;
+ }
+
+ TC_LOG_INFO("sql.updates", "Done.");
+ boost::filesystem::remove(temp);
+ return true;
+}
+
+template<class T>
+bool DBUpdater<T>::Update(DatabaseWorkerPool<T>& pool)
+{
+ if (!DBUpdater<T>::CheckExecutable())
+ return false;
+
+ TC_LOG_INFO("sql.updates", "Updating %s database...", DBUpdater<T>::GetTableName().c_str());
+
+ Path const sourceDirectory(GetSourceDirectory());
+
+ if (!is_directory(sourceDirectory))
+ {
+ TC_LOG_ERROR("sql.updates", "DBUpdater: Given source directory %s does not exist, skipped!", sourceDirectory.generic_string().c_str());
+ return false;
+ }
+
+ UpdateFetcher updateFetcher(sourceDirectory, [&](std::string const& query) { DBUpdater<T>::Apply(pool, query); },
+ [&](Path const& file) { DBUpdater<T>::ApplyFile(pool, file); },
+ [&](std::string const& query) -> QueryResult { return DBUpdater<T>::Retrieve(pool, query); });
+
+ uint32 const count = updateFetcher.Update(
+ sConfigMgr->GetBoolDefault("Updates.Redundancy", true),
+ sConfigMgr->GetBoolDefault("Updates.AllowRehash", true),
+ sConfigMgr->GetBoolDefault("Updates.ArchivedRedundancy", false),
+ sConfigMgr->GetBoolDefault("Updates.CleanDeadRef", true));
+
+ if (!count)
+ TC_LOG_INFO("sql.updates", ">> %s database is up-to-date!", DBUpdater<T>::GetTableName().c_str());
+ else
+ TC_LOG_INFO("sql.updates", ">> Applied %d %s.", count, count == 1 ? "query" : "queries");
+
+ return true;
+}
+
+template<class T>
+bool DBUpdater<T>::Populate(DatabaseWorkerPool<T>& pool)
+{
+ {
+ QueryResult const result = Retrieve(pool, "SHOW TABLES");
+ if (result && (result->GetRowCount() > 0))
+ return true;
+ }
+
+ if (!DBUpdater<T>::CheckExecutable())
+ return false;
+
+ TC_LOG_INFO("sql.updates", "Database %s is empty, auto populating it...", DBUpdater<T>::GetTableName().c_str());
+
+ std::string const p = DBUpdater<T>::GetBaseFile();
+ if (p.empty())
+ {
+ TC_LOG_INFO("sql.updates", ">> No base file provided, skipped!");
+ return true;
+ }
+
+ Path const base(p);
+ if (!exists(base))
+ {
+ switch (DBUpdater<T>::GetBaseLocationType())
+ {
+ case LOCATION_REPOSITORY:
+ {
+ TC_LOG_ERROR("sql.updates", ">> Base file \"%s\" is missing, try to clone the source again.",
+ base.generic_string().c_str(), base.filename().generic_string().c_str());
+
+ break;
+ }
+ case LOCATION_DOWNLOAD:
+ {
+ TC_LOG_ERROR("sql.updates", ">> File \"%s\" is missing, download it from \"http://www.trinitycore.org/f/files/category/1-database/\"" \
+ " and place it in your server directory.", base.filename().generic_string().c_str());
+ break;
+ }
+ }
+ return false;
+ }
+
+ // Update database
+ TC_LOG_INFO("sql.updates", ">> Applying \'%s\'...", base.generic_string().c_str());
+ ApplyFile(pool, base);
+
+ TC_LOG_INFO("sql.updates", ">> Done!");
+ return true;
+}
+
+template<class T>
+QueryResult DBUpdater<T>::Retrieve(DatabaseWorkerPool<T>& pool, std::string const& query)
+{
+ return pool.PQuery(query.c_str());
+}
+
+template<class T>
+void DBUpdater<T>::Apply(DatabaseWorkerPool<T>& pool, std::string const& query)
+{
+ pool.DirectExecute(query.c_str());
+}
+
+template<class T>
+void DBUpdater<T>::ApplyFile(DatabaseWorkerPool<T>& pool, Path const& path)
+{
+ DBUpdater<T>::ApplyFile(pool, pool.GetConnectionInfo()->host, pool.GetConnectionInfo()->user, pool.GetConnectionInfo()->password,
+ pool.GetConnectionInfo()->port_or_socket, pool.GetConnectionInfo()->database, path);
+}
+
+template<class T>
+void DBUpdater<T>::ApplyFile(DatabaseWorkerPool<T>& pool, std::string const& host, std::string const& user,
+ std::string const& password, std::string const& port_or_socket, std::string const& database, Path const& path)
+{
+ std::vector<std::string> args;
+ args.reserve(7);
+
+ // CLI Client connection info
+ args.push_back("-h" + host);
+ args.push_back("-u" + user);
+ args.push_back("-p" + password);
+ args.push_back("-P" + port_or_socket);
+
+ // Set the default charset to utf8
+ args.push_back("--default-character-set=utf8");
+
+ // Set max allowed packet to 1 GB
+ args.push_back("--max-allowed-packet=1GB");
+
+ // Database
+ if (!database.empty())
+ args.push_back(database);
+
+ // ToDo: use the existing query in memory as virtual file if possible
+ file_descriptor_source source(path);
+
+ uint32 ret;
+ try
+ {
+ child c = execute(run_exe(DBUpdater<T>::GetMySqlCli().empty() ? "mysql" :
+ boost::filesystem::absolute(DBUpdater<T>::GetMySqlCli()).generic_string()),
+ set_args(args), bind_stdin(source), throw_on_error());
+
+ ret = wait_for_exit(c);
+ }
+ catch (boost::system::system_error&)
+ {
+ ret = EXIT_FAILURE;
+ }
+
+ source.close();
+
+ if (ret != EXIT_SUCCESS)
+ {
+ TC_LOG_FATAL("sql.updates", "Applying of file \'%s\' to database \'%s\' failed!" \
+ " If you are an user pull the latest revision from the repository. If you are a developer fix your sql query.",
+ path.generic_string().c_str(), pool.GetConnectionInfo()->database.c_str());
+
+ throw UpdateException("update failed");
+ }
+}
+
+template class DBUpdater<LoginDatabaseConnection>;
+template class DBUpdater<WorldDatabaseConnection>;
+template class DBUpdater<CharacterDatabaseConnection>;
+template class DBUpdater<HotfixDatabaseConnection>;
diff --git a/src/server/shared/Updater/DBUpdater.h b/src/server/shared/Updater/DBUpdater.h
new file mode 100644
index 00000000000..0caf8a438fb
--- /dev/null
+++ b/src/server/shared/Updater/DBUpdater.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2008-2015 TrinityCore <http://www.trinitycore.org/>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DBUpdater_h__
+#define DBUpdater_h__
+
+#include "DatabaseEnv.h"
+
+#include <string>
+#include <boost/filesystem.hpp>
+
+class UpdateException : public std::exception
+{
+public:
+ UpdateException(std::string const& msg) : _msg(msg) { }
+ ~UpdateException() throw() { }
+
+ char const* what() const throw() override { return _msg.c_str(); }
+
+private:
+ std::string const _msg;
+};
+
+enum BaseLocation
+{
+ LOCATION_REPOSITORY,
+ LOCATION_DOWNLOAD
+};
+
+template <class T>
+class DBUpdater
+{
+public:
+ using Path = boost::filesystem::path;
+
+ static std::string GetSourceDirectory();
+
+ static inline std::string GetConfigEntry();
+
+ static inline std::string GetTableName();
+
+ static std::string GetBaseFile();
+
+ static bool IsEnabled(uint32 const updateMask);
+
+ static BaseLocation GetBaseLocationType();
+
+ static bool Create(DatabaseWorkerPool<T>& pool);
+
+ static bool Update(DatabaseWorkerPool<T>& pool);
+
+ static bool Populate(DatabaseWorkerPool<T>& pool);
+
+private:
+ static std::string GetMySqlCli();
+ static bool CheckExecutable();
+
+ static QueryResult Retrieve(DatabaseWorkerPool<T>& pool, std::string const& query);
+ static void Apply(DatabaseWorkerPool<T>& pool, std::string const& query);
+ static void ApplyFile(DatabaseWorkerPool<T>& pool, Path const& path);
+ static void ApplyFile(DatabaseWorkerPool<T>& pool, std::string const& host, std::string const& user,
+ std::string const& password, std::string const& port_or_socket, std::string const& database, Path const& path);
+};
+
+#endif // DBUpdater_h__
diff --git a/src/server/shared/Updater/UpdateFetcher.cpp b/src/server/shared/Updater/UpdateFetcher.cpp
new file mode 100644
index 00000000000..8084c6ba37f
--- /dev/null
+++ b/src/server/shared/Updater/UpdateFetcher.cpp
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2008-2015 TrinityCore <http://www.trinitycore.org/>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include "UpdateFetcher.h"
+#include "Log.h"
+#include "Util.h"
+
+#include <fstream>
+#include <chrono>
+#include <vector>
+#include <sstream>
+#include <exception>
+#include <unordered_map>
+#include <openssl/sha.h>
+
+using namespace boost::filesystem;
+
+UpdateFetcher::UpdateFetcher(Path const& sourceDirectory,
+ std::function<void(std::string const&)> const& apply,
+ std::function<void(Path const& path)> const& applyFile,
+ std::function<QueryResult(std::string const&)> const& retrieve) :
+ _sourceDirectory(sourceDirectory), _apply(apply), _applyFile(applyFile),
+ _retrieve(retrieve)
+{
+}
+
+UpdateFetcher::LocaleFileStorage UpdateFetcher::GetFileList() const
+{
+ LocaleFileStorage files;
+ DirectoryStorage directories = ReceiveIncludedDirectories();
+ for (auto const& entry : directories)
+ FillFileListRecursively(entry.path, files, entry.state, 1);
+
+ return files;
+}
+
+void UpdateFetcher::FillFileListRecursively(Path const& path, LocaleFileStorage& storage, State const state, uint32 const depth) const
+{
+ static uint32 const MAX_DEPTH = 10;
+ static directory_iterator const end;
+
+ for (directory_iterator itr(path); itr != end; ++itr)
+ {
+ if (is_directory(itr->path()))
+ {
+ if (depth < MAX_DEPTH)
+ FillFileListRecursively(itr->path(), storage, state, depth + 1);
+ }
+ else if (itr->path().extension() == ".sql")
+ {
+ TC_LOG_TRACE("sql.updates", "Added locale file \"%s\".", itr->path().filename().generic_string().c_str());
+
+ LocaleFileEntry const entry = { itr->path(), state };
+
+ // Check for doubled filenames
+ // Since elements are only compared through their filenames this is ok
+ if (storage.find(entry) != storage.end())
+ {
+ TC_LOG_FATAL("sql.updates", "Duplicated filename occurred \"%s\", since updates are ordered " \
+ "through its filename every name needs to be unique!", itr->path().generic_string().c_str());
+
+ throw UpdateException("Updating failed, see the log for details.");
+ }
+
+ storage.insert(entry);
+ }
+ }
+}
+
+UpdateFetcher::DirectoryStorage UpdateFetcher::ReceiveIncludedDirectories() const
+{
+ DirectoryStorage directories;
+
+ QueryResult const result = _retrieve("SELECT `path`, `state` FROM `updates_include`");
+ if (!result)
+ return directories;
+
+ do
+ {
+ Field* fields = result->Fetch();
+
+ std::string path = fields[0].GetString();
+ if (path.substr(0, 1) == "$")
+ path = _sourceDirectory.generic_string() + path.substr(1);
+
+ Path const p(path);
+
+ if (!is_directory(p))
+ {
+ TC_LOG_ERROR("sql.updates", "DBUpdater: Given update include directory \"%s\" isn't existing, skipped!", p.generic_string().c_str());
+ continue;
+ }
+
+ DirectoryEntry const entry = { p, AppliedFileEntry::StateConvert(fields[1].GetString()) };
+ directories.push_back(entry);
+
+ TC_LOG_TRACE("sql.updates", "Added applied file \"%s\" from remote.", p.filename().generic_string().c_str());
+
+ } while (result->NextRow());
+
+ return directories;
+}
+
+UpdateFetcher::AppliedFileStorage UpdateFetcher::ReceiveAppliedFiles() const
+{
+ AppliedFileStorage map;
+
+ QueryResult result = _retrieve("SELECT `name`, `hash`, `state`, `timestamp` FROM `updates` ORDER BY `name` ASC");
+ if (!result)
+ return map;
+
+ do
+ {
+ Field* fields = result->Fetch();
+
+ AppliedFileEntry const entry = { fields[0].GetString(), fields[1].GetString(),
+ AppliedFileEntry::StateConvert(fields[2].GetString()), fields[3].GetUInt32() };
+
+ map.insert(std::make_pair(entry.name, entry));
+ }
+ while (result->NextRow());
+
+ return map;
+}
+
+UpdateFetcher::SQLUpdate UpdateFetcher::ReadSQLUpdate(boost::filesystem::path const& file) const
+{
+ std::ifstream in(file.c_str());
+ WPFatal(in.is_open(), "Could not read an update file.");
+
+ auto const start_pos = in.tellg();
+ in.ignore(std::numeric_limits<std::streamsize>::max());
+ auto const char_count = in.gcount();
+ in.seekg(start_pos);
+
+ SQLUpdate const update(new std::string(char_count, char{}));
+
+ in.read(&(*update)[0], update->size());
+ in.close();
+ return update;
+}
+
+uint32 UpdateFetcher::Update(bool const redundancyChecks, bool const allowRehash, bool const archivedRedundancy, bool const cleanDeadReferences) const
+{
+ LocaleFileStorage const available = GetFileList();
+ AppliedFileStorage applied = ReceiveAppliedFiles();
+
+ // Fill hash to name cache
+ HashToFileNameStorage hashToName;
+ for (auto entry : applied)
+ hashToName.insert(std::make_pair(entry.second.hash, entry.first));
+
+ uint32 importedUpdates = 0;
+
+ for (auto const& availableQuery : available)
+ {
+ TC_LOG_DEBUG("sql.updates", "Checking update \"%s\"...", availableQuery.first.filename().generic_string().c_str());
+
+ AppliedFileStorage::const_iterator iter = applied.find(availableQuery.first.filename().string());
+ if (iter != applied.end())
+ {
+ // If redundancy is disabled skip it since the update is already applied.
+ if (!redundancyChecks)
+ {
+ TC_LOG_DEBUG("sql.updates", ">> Update is already applied, skipping redundancy checks.");
+ applied.erase(iter);
+ 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 && (iter->second.state == ARCHIVED) && (availableQuery.second == ARCHIVED))
+ {
+ TC_LOG_DEBUG("sql.updates", ">> Update is archived and marked as archived in database, skipping redundancy checks.");
+ applied.erase(iter);
+ continue;
+ }
+ }
+
+ // Read update from file
+ SQLUpdate const update = ReadSQLUpdate(availableQuery.first);
+
+ // Calculate hash
+ std::string const hash = CalculateHash(update);
+
+ UpdateMode mode = MODE_APPLY;
+
+ // Update is not in our applied list
+ if (iter == applied.end())
+ {
+ // Catch renames (different filename but same hash)
+ HashToFileNameStorage::const_iterator const hashIter = hashToName.find(hash);
+ if (hashIter != hashToName.end())
+ {
+ // Check if the original file was removed if not we've got a problem.
+ LocaleFileStorage::const_iterator localeIter;
+ // Push localeIter forward
+ for (localeIter = available.begin(); (localeIter != available.end()) &&
+ (localeIter->first.filename().string() != hashIter->second); ++localeIter);
+
+ // Conflict!
+ if (localeIter != available.end())
+ {
+ TC_LOG_WARN("sql.updates", ">> Seems like update \"%s\" \'%s\' was renamed, but the old file is still there! " \
+ "Trade it as a new file! (Probably its an unmodified copy of file \"%s\")",
+ availableQuery.first.filename().string().c_str(), hash.substr(0, 7).c_str(),
+ localeIter->first.filename().string().c_str());
+ }
+ // Its save to trade the file as renamed here
+ else
+ {
+ TC_LOG_INFO("sql.updates", ">> Renaming update \"%s\" to \"%s\" \'%s\'.",
+ hashIter->second.c_str(), availableQuery.first.filename().string().c_str(), hash.substr(0, 7).c_str());
+
+ RenameEntry(hashIter->second, availableQuery.first.filename().string());
+ applied.erase(hashIter->second);
+ continue;
+ }
+ }
+ // Apply the update if it was never seen before.
+ else
+ {
+ TC_LOG_INFO("sql.updates", ">> Applying update \"%s\" \'%s\'...",
+ availableQuery.first.filename().string().c_str(), hash.substr(0, 7).c_str());
+ }
+ }
+ // Rehash the update entry if it is contained in our database but with an empty hash.
+ else if (allowRehash && iter->second.hash.empty())
+ {
+ mode = MODE_REHASH;
+
+ TC_LOG_INFO("sql.updates", ">> Re-hashing update \"%s\" \'%s\'...", availableQuery.first.filename().string().c_str(),
+ hash.substr(0, 7).c_str());
+ }
+ else
+ {
+ // If the hash of the files differs from the one stored in our database reapply the update (because it was changed).
+ if (iter->second.hash != hash)
+ {
+ TC_LOG_INFO("sql.updates", ">> Reapplying update \"%s\" \'%s\' -> \'%s\' (it changed)...", availableQuery.first.filename().string().c_str(),
+ iter->second.hash.substr(0, 7).c_str(), hash.substr(0, 7).c_str());
+ }
+ else
+ {
+ // If the file wasn't changed and just moved update its state if necessary.
+ if (iter->second.state != availableQuery.second)
+ {
+ TC_LOG_DEBUG("sql.updates", ">> Updating state of \"%s\" to \'%s\'...",
+ availableQuery.first.filename().string().c_str(), AppliedFileEntry::StateConvert(availableQuery.second).c_str());
+
+ UpdateState(availableQuery.first.filename().string(), availableQuery.second);
+ }
+
+ TC_LOG_DEBUG("sql.updates", ">> Update is already applied and is matching hash \'%s\'.", hash.substr(0, 7).c_str());
+
+ applied.erase(iter);
+ continue;
+ }
+ }
+
+ uint32 speed = 0;
+ AppliedFileEntry const file = { availableQuery.first.filename().string(), hash, availableQuery.second, 0 };
+
+ switch (mode)
+ {
+ case MODE_APPLY:
+ speed = Apply(availableQuery.first);
+ /*no break*/
+ case MODE_REHASH:
+ UpdateEntry(file, speed);
+ break;
+ }
+
+ if (iter != applied.end())
+ applied.erase(iter);
+
+ if (mode == MODE_APPLY)
+ ++importedUpdates;
+ }
+
+ for (auto const& entry : applied)
+ {
+ TC_LOG_WARN("sql.updates", ">> File \'%s\' was applied to the database but is missing in" \
+ " your update directory now!%s", entry.first.c_str(), cleanDeadReferences ? " Deleting orphaned entry..." : "");
+ }
+
+ if (cleanDeadReferences)
+ CleanUp(applied);
+
+ return importedUpdates;
+}
+
+std::string UpdateFetcher::CalculateHash(SQLUpdate const& query) const
+{
+ // Calculate a Sha1 hash based on query content.
+ unsigned char digest[SHA_DIGEST_LENGTH];
+ SHA1((unsigned char*)query->c_str(), query->length(), (unsigned char*)&digest);
+
+ return ByteArrayToHexStr(digest, SHA_DIGEST_LENGTH);
+}
+
+uint32 UpdateFetcher::Apply(Path const& path) const
+{
+ using Time = std::chrono::high_resolution_clock;
+ using ms = std::chrono::milliseconds;
+
+ // Benchmark query speed
+ auto const begin = Time::now();
+
+ // Update database
+ _applyFile(path);
+
+ // Return time the query took to apply
+ return std::chrono::duration_cast<ms>(Time::now() - begin).count();
+}
+
+void UpdateFetcher::UpdateEntry(AppliedFileEntry const& entry, uint32 const speed) const
+{
+ std::string const update = "REPLACE INTO `updates` (`name`, `hash`, `state`, `speed`) VALUES (\"" +
+ entry.name + "\", \"" + entry.hash + "\", \'" + entry.GetStateAsString() + "\', " + std::to_string(speed) + ")";
+
+ // Update database
+ _apply(update);
+}
+
+void UpdateFetcher::RenameEntry(std::string const& from, std::string const& to) const
+{
+ // Delete target if it exists
+ {
+ std::string const update = "DELETE FROM `updates` WHERE `name`=\"" + to + "\"";
+
+ // Update database
+ _apply(update);
+ }
+
+ // Rename
+ {
+ std::string const update = "UPDATE `updates` SET `name`=\"" + to + "\" WHERE `name`=\"" + from + "\"";
+
+ // Update database
+ _apply(update);
+ }
+}
+
+void UpdateFetcher::CleanUp(AppliedFileStorage const& storage) const
+{
+ if (storage.empty())
+ return;
+
+ std::stringstream update;
+ size_t remaining = storage.size();
+
+ update << "DELETE FROM `updates` WHERE `name` IN(";
+
+ for (auto const& entry : storage)
+ {
+ update << "\"" << entry.first << "\"";
+ if ((--remaining) > 0)
+ update << ", ";
+ }
+
+ update << ")";
+
+ // Update database
+ _apply(update.str());
+}
+
+void UpdateFetcher::UpdateState(std::string const& name, State const state) const
+{
+ std::string const update = "UPDATE `updates` SET `state`=\'" + AppliedFileEntry::StateConvert(state) + "\' WHERE `name`=\"" + name + "\"";
+
+ // Update database
+ _apply(update);
+}
diff --git a/src/server/shared/Updater/UpdateFetcher.h b/src/server/shared/Updater/UpdateFetcher.h
new file mode 100644
index 00000000000..b348eecf4e1
--- /dev/null
+++ b/src/server/shared/Updater/UpdateFetcher.h
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2008-2015 TrinityCore <http://www.trinitycore.org/>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef UpdateFetcher_h__
+#define UpdateFetcher_h__
+
+#include <DBUpdater.h>
+
+#include <functional>
+#include <string>
+#include <memory>
+#include <vector>
+#include <boost/container/flat_set.hpp>
+
+class UpdateFetcher
+{
+ using Path = boost::filesystem::path;
+
+public:
+ UpdateFetcher(Path const& updateDirectory,
+ std::function<void(std::string const&)> const& apply,
+ std::function<void(Path const& path)> const& applyFile,
+ std::function<QueryResult(std::string const&)> const& retrieve);
+
+ uint32 Update(bool const redundancyChecks, bool const allowRehash,
+ bool const archivedRedundancy, bool const cleanDeadReferences) const;
+
+private:
+ enum UpdateMode
+ {
+ MODE_APPLY,
+ MODE_REHASH
+ };
+
+ enum State
+ {
+ RELEASED,
+ ARCHIVED
+ };
+
+ struct AppliedFileEntry
+ {
+ std::string const name;
+
+ std::string const hash;
+
+ State const state;
+
+ uint32 const timestamp;
+
+ static inline State StateConvert(std::string const& state)
+ {
+ return (state == "RELEASED") ? RELEASED : ARCHIVED;
+ }
+
+ static inline std::string StateConvert(State const state)
+ {
+ return (state == RELEASED) ? "RELEASED" : "ARCHIVED";
+ }
+
+ std::string GetStateAsString() const
+ {
+ return StateConvert(state);
+ }
+ };
+
+ struct DirectoryEntry
+ {
+ Path const path;
+
+ State const state;
+ };
+
+ using LocaleFileEntry = std::pair<Path, State>;
+
+ struct PathCompare
+ {
+ inline bool operator() (LocaleFileEntry const& left, LocaleFileEntry const& right) const
+ {
+ return left.first.filename().string() < right.first.filename().string();
+ }
+ };
+
+ using LocaleFileStorage = boost::container::flat_set<LocaleFileEntry, PathCompare>;
+ using HashToFileNameStorage = std::unordered_map<std::string, std::string>;
+ using AppliedFileStorage = std::unordered_map<std::string, AppliedFileEntry>;
+ using DirectoryStorage = std::vector<UpdateFetcher::DirectoryEntry>;
+ using SQLUpdate = std::shared_ptr<std::string>;
+
+ LocaleFileStorage GetFileList() const;
+ void FillFileListRecursively(Path const& path, LocaleFileStorage& storage, State const state, uint32 const depth) const;
+
+ DirectoryStorage ReceiveIncludedDirectories() const;
+ AppliedFileStorage ReceiveAppliedFiles() const;
+
+ SQLUpdate ReadSQLUpdate(Path const& file) const;
+ std::string CalculateHash(SQLUpdate const& query) const;
+
+ uint32 Apply(Path const& path) const;
+
+ void UpdateEntry(AppliedFileEntry const& entry, uint32 const speed = 0) const;
+ void RenameEntry(std::string const& from, std::string const& to) const;
+ void CleanUp(AppliedFileStorage const& storage) const;
+
+ void UpdateState(std::string const& name, State const state) const;
+
+ Path const _sourceDirectory;
+
+ std::function<void(std::string const&)> const _apply;
+ std::function<void(Path const& path)> const _applyFile;
+ std::function<QueryResult(std::string const&)> const _retrieve;
+};
+
+#endif // UpdateFetcher_h__