/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Affero 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 Affero 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 .
*/
#include "DBUpdater.h"
#include "BuiltInConfig.h"
#include "Config.h"
#include "DatabaseEnv.h"
#include "DatabaseLoader.h"
#include "Log.h"
#include "StartProcess.h"
#include "UpdateFetcher.h"
#include "QueryResult.h"
#include
#include
#include
std::string DBUpdaterUtil::GetCorrectedMySQLExecutable()
{
if (!corrected_path().empty())
return corrected_path();
else
return BuiltInConfig::GetMySQLExecutable();
}
bool DBUpdaterUtil::CheckExecutable()
{
std::filesystem::path exe(GetCorrectedMySQLExecutable());
if (!is_regular_file(exe))
{
exe = Acore::SearchExecutableInPath("mysql");
if (!exe.empty() && is_regular_file(exe))
{
// Correct the path to the cli
corrected_path() = absolute(exe).generic_string();
return true;
}
LOG_FATAL("sql.updates", "Didn't find any executable MySQL binary at \'{}\' or in path, correct the path in the *.conf (\"MySQLExecutable\").",
absolute(exe).generic_string());
return false;
}
return true;
}
std::string& DBUpdaterUtil::corrected_path()
{
static std::string path;
return path;
}
// Auth Database
template<>
std::string DBUpdater::GetConfigEntry()
{
return "Updates.Auth";
}
template<>
std::string DBUpdater::GetTableName()
{
return "Auth";
}
template<>
std::string DBUpdater::GetBaseFilesDirectory()
{
return BuiltInConfig::GetSourceDirectory() + "/data/sql/base/db_auth/";
}
template<>
bool DBUpdater::IsEnabled(uint32 const updateMask)
{
// This way silences warnings under msvc
return (updateMask & DatabaseLoader::DATABASE_LOGIN) ? true : false;
}
template<>
std::string DBUpdater::GetDBModuleName()
{
return "db-auth";
}
// World Database
template<>
std::string DBUpdater::GetConfigEntry()
{
return "Updates.World";
}
template<>
std::string DBUpdater::GetTableName()
{
return "World";
}
template<>
std::string DBUpdater::GetBaseFilesDirectory()
{
return BuiltInConfig::GetSourceDirectory() + "/data/sql/base/db_world/";
}
template<>
bool DBUpdater::IsEnabled(uint32 const updateMask)
{
// This way silences warnings under msvc
return (updateMask & DatabaseLoader::DATABASE_WORLD) ? true : false;
}
template<>
std::string DBUpdater::GetDBModuleName()
{
return "db-world";
}
// Character Database
template<>
std::string DBUpdater::GetConfigEntry()
{
return "Updates.Character";
}
template<>
std::string DBUpdater::GetTableName()
{
return "Character";
}
template<>
std::string DBUpdater::GetBaseFilesDirectory()
{
return BuiltInConfig::GetSourceDirectory() + "/data/sql/base/db_characters/";
}
template<>
bool DBUpdater::IsEnabled(uint32 const updateMask)
{
// This way silences warnings under msvc
return (updateMask & DatabaseLoader::DATABASE_CHARACTER) ? true : false;
}
template<>
std::string DBUpdater::GetDBModuleName()
{
return "db-characters";
}
// All
template
BaseLocation DBUpdater::GetBaseLocationType()
{
return LOCATION_REPOSITORY;
}
template
bool DBUpdater::Create(DatabaseWorkerPool& pool)
{
LOG_WARN("sql.updates", "Database \"{}\" does not exist", pool.GetConnectionInfo()->database);
const char* disableInteractive = std::getenv("AC_DISABLE_INTERACTIVE");
if (!sConfigMgr->isDryRun() && (disableInteractive == nullptr || std::strcmp(disableInteractive, "1") != 0))
{
std::cout << "Do you want to create it? [yes (default) / no]:" << std::endl;
std::string answer;
std::getline(std::cin, answer);
if (!answer.empty() && !(answer.substr(0, 1) == "y"))
return false;
}
LOG_INFO("sql.updates", "Creating database \"{}\"...", pool.GetConnectionInfo()->database);
// 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())
{
LOG_FATAL("sql.updates", "Failed to create temporary query file \"{}\"!", temp.generic_string());
return false;
}
file << "CREATE DATABASE `" << pool.GetConnectionInfo()->database << "` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci;\n\n";
file.close();
try
{
DBUpdater::ApplyFile(pool, pool.GetConnectionInfo()->host, pool.GetConnectionInfo()->user, pool.GetConnectionInfo()->password,
pool.GetConnectionInfo()->port_or_socket, "", pool.GetConnectionInfo()->ssl, temp);
}
catch (UpdateException&)
{
LOG_FATAL("sql.updates", "Failed to create database {}! Does the user (named in *.conf) have `CREATE`, `ALTER`, `DROP`, `INSERT` and `DELETE` privileges on the MySQL server?", pool.GetConnectionInfo()->database);
std::filesystem::remove(temp);
return false;
}
LOG_INFO("sql.updates", "Done.");
LOG_INFO("sql.updates", " ");
std::filesystem::remove(temp);
return true;
}
template
bool DBUpdater::Update(DatabaseWorkerPool& pool, std::string_view modulesList /*= {}*/)
{
if (!DBUpdaterUtil::CheckExecutable())
return false;
LOG_INFO("sql.updates", "Updating {} database...", DBUpdater::GetTableName());
Path const sourceDirectory(BuiltInConfig::GetSourceDirectory());
if (!is_directory(sourceDirectory))
{
LOG_ERROR("sql.updates", "DBUpdater: The given source directory {} does not exist, change the path to the directory where your sql directory exists (for example c:\\source\\azerothcore). Shutting down.",
sourceDirectory.generic_string());
return false;
}
auto CheckUpdateTable = [&](std::string const& tableName)
{
auto checkTable = DBUpdater::Retrieve(pool, Acore::StringFormat("SHOW TABLES LIKE '{}'", tableName));
if (!checkTable)
{
LOG_WARN("sql.updates", "> Table '{}' not exist! Try add based table", tableName);
Path const temp(GetBaseFilesDirectory() + tableName + ".sql");
try
{
DBUpdater::ApplyFile(pool, temp);
}
catch (UpdateException&)
{
LOG_FATAL("sql.updates", "Failed apply file to database {}! Does the user (named in *.conf) have `INSERT` and `DELETE` privileges on the MySQL server?", pool.GetConnectionInfo()->database);
return false;
}
return true;
}
return true;
};
if (!CheckUpdateTable("updates") || !CheckUpdateTable("updates_include"))
return false;
UpdateFetcher updateFetcher(sourceDirectory, [&](std::string const & query) { DBUpdater::Apply(pool, query); },
[&](Path const & file) { DBUpdater::ApplyFile(pool, file); },
[&](std::string const & query) -> QueryResult { return DBUpdater::Retrieve(pool, query); }, DBUpdater::GetDBModuleName(), modulesList);
UpdateResult result;
try
{
result = updateFetcher.Update(
sConfigMgr->GetOption("Updates.Redundancy", true),
sConfigMgr->GetOption("Updates.AllowRehash", true),
sConfigMgr->GetOption("Updates.ArchivedRedundancy", false),
sConfigMgr->GetOption("Updates.CleanDeadRefMaxCount", 3));
}
catch (UpdateException&)
{
return false;
}
std::string const info = Acore::StringFormat("Containing {} new and {} archived updates.", result.recent, result.archived);
if (!result.updated)
LOG_INFO("sql.updates", ">> {} database is up-to-date! {}", DBUpdater::GetTableName(), info);
else
LOG_INFO("sql.updates", ">> Applied {} {}. {}", result.updated, result.updated == 1 ? "query" : "queries", info);
LOG_INFO("sql.updates", " ");
return true;
}
template
bool DBUpdater::Update(DatabaseWorkerPool& pool, std::vector const* setDirectories)
{
if (!DBUpdaterUtil::CheckExecutable())
{
return false;
}
Path const sourceDirectory(BuiltInConfig::GetSourceDirectory());
if (!is_directory(sourceDirectory))
{
return false;
}
auto CheckUpdateTable = [&](std::string const& tableName)
{
auto checkTable = DBUpdater::Retrieve(pool, Acore::StringFormat("SHOW TABLES LIKE '{}'", tableName));
if (!checkTable)
{
Path const temp(GetBaseFilesDirectory() + tableName + ".sql");
try
{
DBUpdater::ApplyFile(pool, temp);
}
catch (UpdateException&)
{
return false;
}
return true;
}
return true;
};
if (!CheckUpdateTable("updates") || !CheckUpdateTable("updates_include"))
{
return false;
}
UpdateFetcher updateFetcher(sourceDirectory, [&](std::string const & query) { DBUpdater::Apply(pool, query); },
[&](Path const & file) { DBUpdater::ApplyFile(pool, file); },
[&](std::string const & query) -> QueryResult { return DBUpdater::Retrieve(pool, query); }, DBUpdater::GetDBModuleName(), setDirectories);
UpdateResult result;
try
{
result = updateFetcher.Update(
sConfigMgr->GetOption("Updates.Redundancy", true),
sConfigMgr->GetOption("Updates.AllowRehash", true),
sConfigMgr->GetOption("Updates.ArchivedRedundancy", false),
sConfigMgr->GetOption("Updates.CleanDeadRefMaxCount", 3));
}
catch (UpdateException&)
{
return false;
}
return true;
}
template
bool DBUpdater::Populate(DatabaseWorkerPool& pool)
{
{
QueryResult const result = Retrieve(pool, "SHOW TABLES");
if (result && (result->GetRowCount() > 0))
return true;
}
if (!DBUpdaterUtil::CheckExecutable())
return false;
LOG_INFO("sql.updates", "Database {} is empty, auto populating it...", DBUpdater::GetTableName());
std::string const DirPathStr = DBUpdater::GetBaseFilesDirectory();
Path const DirPath(DirPathStr);
if (!std::filesystem::is_directory(DirPath))
{
LOG_ERROR("sql.updates", ">> Directory \"{}\" not exist", DirPath.generic_string());
return false;
}
if (DirPath.empty())
{
LOG_ERROR("sql.updates", ">> Directory \"{}\" is empty", DirPath.generic_string());
return false;
}
std::filesystem::directory_iterator const DirItr;
uint32 FilesCount = 0;
for (std::filesystem::directory_iterator itr(DirPath); itr != DirItr; ++itr)
{
if (itr->path().extension() == ".sql")
FilesCount++;
}
if (!FilesCount)
{
LOG_ERROR("sql.updates", ">> In directory \"{}\" not exist '*.sql' files", DirPath.generic_string());
return false;
}
for (std::filesystem::directory_iterator itr(DirPath); itr != DirItr; ++itr)
{
if (itr->path().extension() != ".sql")
continue;
LOG_INFO("sql.updates", ">> Applying \'{}\'...", itr->path().filename().generic_string());
try
{
ApplyFile(pool, itr->path());
}
catch (UpdateException&)
{
return false;
}
}
LOG_INFO("sql.updates", ">> Done!");
LOG_INFO("sql.updates", " ");
return true;
}
template
QueryResult DBUpdater::Retrieve(DatabaseWorkerPool& pool, std::string const& query)
{
return pool.Query(query.c_str());
}
template
void DBUpdater::Apply(DatabaseWorkerPool& pool, std::string const& query)
{
pool.DirectExecute(query.c_str());
}
template
void DBUpdater::ApplyFile(DatabaseWorkerPool& pool, Path const& path)
{
DBUpdater::ApplyFile(pool, pool.GetConnectionInfo()->host, pool.GetConnectionInfo()->user, pool.GetConnectionInfo()->password,
pool.GetConnectionInfo()->port_or_socket, pool.GetConnectionInfo()->database, pool.GetConnectionInfo()->ssl, path);
}
template
void DBUpdater::ApplyFile(DatabaseWorkerPool& pool, std::string const& host, std::string const& user,
std::string const& password, std::string const& port_or_socket, std::string const& database, std::string const& ssl, Path const& path)
{
std::string configTempDir = sConfigMgr->GetOption("TempDir", "");
auto tempDir = configTempDir.empty() ? std::filesystem::temp_directory_path().string() : configTempDir;
tempDir = Acore::String::AddSuffixIfNotExists(tempDir, std::filesystem::path::preferred_separator);
std::string confFileName = "mysql_ac.conf";
std::ofstream outfile (tempDir + confFileName);
outfile << "[client]\npassword = \"" << password << '"' << std::endl;
outfile.close();
std::vector args;
args.reserve(9);
args.emplace_back("--defaults-extra-file="+tempDir + confFileName+"");
// CLI Client connection info
args.emplace_back("-h" + host);
args.emplace_back("-u" + user);
// Check if we want to connect through ip or socket (Unix only)
#ifdef _WIN32
if (host == ".")
args.emplace_back("--protocol=PIPE");
else
args.emplace_back("-P" + port_or_socket);
#else
if (!std::isdigit(port_or_socket[0]))
{
// We can't check if host == "." here, because it is named localhost if socket option is enabled
args.emplace_back("-P0");
args.emplace_back("--protocol=SOCKET");
args.emplace_back("-S" + port_or_socket);
}
else
// generic case
args.emplace_back("-P" + port_or_socket);
#endif
// Set the default charset to utf8
args.emplace_back("--default-character-set=utf8");
// Set max allowed packet to 1 GB
args.emplace_back("--max-allowed-packet=1GB");
if (ssl == "ssl")
args.emplace_back("--ssl-mode=REQUIRED");
// Execute sql file
args.emplace_back("-e");
args.emplace_back(Acore::StringFormat("BEGIN; SOURCE {}; COMMIT;", path.generic_string()));
// Database
if (!database.empty())
args.emplace_back(database);
// Invokes a mysql process which doesn't leak credentials to logs
int const ret = Acore::StartProcess(DBUpdaterUtil::GetCorrectedMySQLExecutable(), args,
"sql.updates", "", true);
if (ret != EXIT_SUCCESS)
{
LOG_FATAL("sql.updates", "Applying of file \'{}\' to database \'{}\' failed!" \
" If you are a user, please pull the latest revision from the repository. "
"Also make sure you have not applied any of the databases with your sql client. "
"You cannot use auto-update system and import sql files from AzerothCore repository with your sql client. "
"If you are a developer, please fix your sql query.",
path.generic_string(), pool.GetConnectionInfo()->database);
if (!sConfigMgr->isDryRun())
throw UpdateException("update failed");
}
}
template class AC_DATABASE_API DBUpdater;
template class AC_DATABASE_API DBUpdater;
template class AC_DATABASE_API DBUpdater;