diff --git a/.gitignore b/.gitignore index a73a8c3022c..4bd08dd8908 100644 --- a/.gitignore +++ b/.gitignore @@ -395,5 +395,8 @@ canary.old # VCPKG vcpkg_installed +# DB Backups +database_backup + # CLION cmake-build-* diff --git a/config.lua.dist b/config.lua.dist index 7d0360d9360..1ed6c59f367 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -399,6 +399,7 @@ mysqlHost = "127.0.0.1" mysqlUser = "root" mysqlPass = "root" mysqlDatabase = "otservbr-global" +mysqlDatabaseBackup = false mysqlPort = 3306 mysqlSock = "" passwordType = "sha1" diff --git a/src/canary_server.cpp b/src/canary_server.cpp index 687fa51b6cd..3c282e4f197 100644 --- a/src/canary_server.cpp +++ b/src/canary_server.cpp @@ -308,6 +308,8 @@ void CanaryServer::initializeDatabase() { } logger.debug("MySQL Version: {}", Database::getClientVersion()); + g_database().createDatabaseBackup(); + logger.debug("Running database manager..."); if (!DatabaseManager::isDatabaseSetup()) { throw FailedToInitializeCanary(fmt::format( diff --git a/src/config/config_enums.hpp b/src/config/config_enums.hpp index 559045fdb9b..7e417c2dfb5 100644 --- a/src/config/config_enums.hpp +++ b/src/config/config_enums.hpp @@ -165,6 +165,7 @@ enum ConfigKey_t : uint16_t { MONTH_KILLS_TO_RED, MULTIPLIER_ATTACKONFIST, MYSQL_DB, + MYSQL_DB_BACKUP, MYSQL_HOST, MYSQL_PASS, MYSQL_SOCK, diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index 1c00df76c2f..d52096f123d 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -71,6 +71,7 @@ bool ConfigManager::load() { loadStringConfig(L, MAP_DOWNLOAD_URL, "mapDownloadUrl", ""); loadStringConfig(L, MAP_NAME, "mapName", "canary"); loadStringConfig(L, MYSQL_DB, "mysqlDatabase", "canary"); + loadBoolConfig(L, MYSQL_DB_BACKUP, "mysqlDatabaseBackup", false); loadStringConfig(L, MYSQL_HOST, "mysqlHost", "127.0.0.1"); loadStringConfig(L, MYSQL_PASS, "mysqlPass", ""); loadStringConfig(L, MYSQL_SOCK, "mysqlSock", ""); diff --git a/src/database/database.cpp b/src/database/database.cpp index fcb7371708d..31e113b3f93 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -60,6 +60,120 @@ bool Database::connect(const std::string* host, const std::string* user, const s return true; } +void Database::createDatabaseBackup() const { + if (!g_configManager().getBoolean(MYSQL_DB_BACKUP)) { + return; + } + + std::time_t now = getTimeNow(); + std::string formattedTime = fmt::format("{:%Y-%m-%d_%H-%M-%S}", fmt::localtime(now)); + + if (formattedTime.empty()) { + g_logger().error("Failed to format time for database backup."); + return; + } + + // Create backup directory and define backup file name + std::string backupDir = "database_backup/"; + std::filesystem::create_directories(backupDir); + std::string backupFileName = fmt::format("{}/backup_{}.sql", backupDir, formattedTime); + + std::ofstream backupFile(backupFileName, std::ios::binary); + if (!backupFile.is_open()) { + g_logger().error("Failed to open backup file: {}", backupFileName); + return; + } + + if (!handle) { + g_logger().error("Database handle not initialized."); + return; + } + + // Retrieve the list of tables + if (mysql_query(handle, "SHOW TABLES") != 0) { + g_logger().error("Failed to retrieve table list: {}", mysql_error(handle)); + return; + } + + MYSQL_RES* tablesResult = mysql_store_result(handle); + if (!tablesResult) { + g_logger().error("Failed to store table list result: {}", mysql_error(handle)); + return; + } + + g_logger().info("Creating database backup..."); + MYSQL_ROW tableRow; + while ((tableRow = mysql_fetch_row(tablesResult))) { + std::string tableName = tableRow[0]; + + // Retrieve CREATE TABLE statement for each table + std::string createTableQuery = fmt::format("SHOW CREATE TABLE `{}`", tableName); + if (mysql_query(handle, createTableQuery.c_str()) == 0) { + MYSQL_RES* createTableResult = mysql_store_result(handle); + if (createTableResult) { + MYSQL_ROW createRow = mysql_fetch_row(createTableResult); + if (createRow && createRow[1]) { + backupFile << createRow[1] << ";\n\n"; + } + mysql_free_result(createTableResult); + } + } else { + g_logger().error("Failed to retrieve create statement for table {}: {}", tableName, mysql_error(handle)); + continue; + } + + // Retrieve table data + std::string selectQuery = fmt::format("SELECT * FROM `{}`", tableName); + if (mysql_query(handle, selectQuery.c_str()) != 0) { + g_logger().error("Failed to retrieve data from table {}: {}", tableName, mysql_error(handle)); + continue; + } + + MYSQL_RES* tableData = mysql_store_result(handle); + if (!tableData) { + g_logger().error("Failed to store data result for table {}: {}", tableName, mysql_error(handle)); + continue; + } + + int numFields = mysql_num_fields(tableData); + MYSQL_FIELD* fields = mysql_fetch_fields(tableData); + MYSQL_ROW rowData; + unsigned long* lengths; + + while ((rowData = mysql_fetch_row(tableData))) { + lengths = mysql_fetch_lengths(tableData); + backupFile << "INSERT INTO " << tableName << " VALUES("; + + for (int i = 0; i < numFields; ++i) { + if (i > 0) { + backupFile << ", "; + } + + if (rowData[i]) { + if (IS_BLOB(fields[i].type)) { + backupFile << escapeBlob(rowData[i], lengths[i]); + } else if (IS_NUM(fields[i].type)) { + backupFile << rowData[i]; + } else { + backupFile << escapeString(std::string(rowData[i], lengths[i])); + } + } else { + backupFile << "NULL"; + } + } + backupFile << ");\n"; + } + + backupFile << "\n"; + mysql_free_result(tableData); + } + + mysql_free_result(tablesResult); + backupFile.close(); + + g_logger().info("Database backup successfully created at: {}", backupFileName); +} + bool Database::beginTransaction() { if (!executeQuery("BEGIN")) { return false; diff --git a/src/database/database.hpp b/src/database/database.hpp index 69c47d324ad..1aa2943aa3c 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -36,6 +36,7 @@ class Database { bool connect(); bool connect(const std::string* host, const std::string* user, const std::string* password, const std::string* database, uint32_t port, const std::string* sock); + void createDatabaseBackup() const; bool retryQuery(std::string_view query, int retries); bool executeQuery(std::string_view query);