//////////////////////////////////////////////////////////////////////// // Crystal Server - an opensource roleplaying game //////////////////////////////////////////////////////////////////////// // 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 . //////////////////////////////////////////////////////////////////////// #include "database/database.hpp" #include "config/configmanager.hpp" #include "lib/di/container.hpp" #include "lib/metrics/metrics.hpp" #include "utils/tools.hpp" #include Database::~Database() { if (handle != nullptr) { mysql_close(handle); } } Database &Database::getInstance() { return inject(); } bool Database::connect() { return connect(&g_configManager().getString(MYSQL_HOST), &g_configManager().getString(MYSQL_USER), &g_configManager().getString(MYSQL_PASS), &g_configManager().getString(MYSQL_DB), g_configManager().getNumber(SQL_PORT), &g_configManager().getString(MYSQL_SOCK)); } bool Database::connect(const std::string* host, const std::string* user, const std::string* password, const std::string* database, uint32_t port, const std::string* sock) { // connection handle initialization handle = mysql_init(nullptr); if (!handle) { g_logger().error("Failed to initialize MySQL connection handle."); return false; } if (host->empty() || user->empty() || password->empty() || database->empty() || port <= 0) { g_logger().warn("MySQL host, user, password, database or port not provided"); } // automatic reconnect bool reconnect = true; mysql_options(handle, MYSQL_OPT_RECONNECT, &reconnect); // Remove ssl verification bool ssl_enabled = false; mysql_options(handle, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, &ssl_enabled); // connects to database if (!mysql_real_connect(handle, host->c_str(), user->c_str(), password->c_str(), database->c_str(), port, sock->c_str(), 0)) { g_logger().error("MySQL Error Message: {}", mysql_error(handle)); return false; } DBResult_ptr result = storeQuery("SHOW VARIABLES LIKE 'max_allowed_packet'"); if (result) { maxPacketSize = result->getNumber("Value"); } return true; } void Database::createDatabaseBackup(bool compress) const { if (!g_configManager().getBoolean(MYSQL_DB_BACKUP)) { return; } // Get current time for formatting auto now = std::chrono::system_clock::now(); std::time_t now_c = std::chrono::system_clock::to_time_t(now); std::tm tm_buf {}; #if defined(_WIN32) || defined(_WIN64) localtime_s(&tm_buf, &now_c); #else localtime_r(&now_c, &tm_buf); #endif char date_buf[16]; std::strftime(date_buf, sizeof(date_buf), "%Y-%m-%d", &tm_buf); char time_buf[16]; std::strftime(time_buf, sizeof(time_buf), "%H-%M-%S", &tm_buf); std::string formattedDate = date_buf; std::string formattedTime = time_buf; // Create a backup directory based on the current date std::string backupDir = fmt::format("database_backup/{}/", formattedDate); std::filesystem::create_directories(backupDir); std::string backupFileName = fmt::format("{}backup_{}.sql", backupDir, formattedTime); // Create a temporary configuration file for MySQL credentials std::string tempConfigFile = "database_backup.cnf"; std::ofstream configFile(tempConfigFile); if (configFile.is_open()) { configFile << "[client]\n"; configFile << "user=\"" << g_configManager().getString(MYSQL_USER) << "\"\n"; configFile << "password=\"" << g_configManager().getString(MYSQL_PASS) << "\"\n"; configFile << "host=" << g_configManager().getString(MYSQL_HOST) << "\n"; configFile << "port=" << g_configManager().getNumber(SQL_PORT) << "\n"; configFile.close(); } else { g_logger().error("Failed to create temporary MySQL configuration file."); return; } // Execute mysqldump command to create backup file std::string command = fmt::format( "mysqldump --defaults-extra-file={} {} > {}", tempConfigFile, g_configManager().getString(MYSQL_DB), backupFileName ); int result = std::system(command.c_str()); std::filesystem::remove(tempConfigFile); if (result != 0) { g_logger().error("Failed to create database backup using mysqldump."); return; } // Compress the backup file if requested std::string compressedFileName; compressedFileName = backupFileName + ".gz"; gzFile gzFile = gzopen(compressedFileName.c_str(), "wb9"); if (!gzFile) { g_logger().error("Failed to open gzip file for compression."); return; } std::ifstream backupFile(backupFileName, std::ios::binary); if (!backupFile.is_open()) { g_logger().error("Failed to open backup file for compression: {}", backupFileName); gzclose(gzFile); return; } std::string buffer(8192, '\0'); while (backupFile.read(&buffer[0], buffer.size()) || backupFile.gcount() > 0) { gzwrite(gzFile, buffer.data(), backupFile.gcount()); } backupFile.close(); gzclose(gzFile); std::filesystem::remove(backupFileName); g_logger().info("Database backup successfully compressed to: {}", compressedFileName); // Delete backups older than 7 days auto nowTime = std::chrono::system_clock::now(); auto sevenDaysAgo = nowTime - std::chrono::hours(7 * 24); // 7 days in hours for (const auto &entry : std::filesystem::directory_iterator("database_backup")) { if (entry.is_directory()) { try { for (const auto &file : std::filesystem::directory_iterator(entry)) { if (file.path().extension() == ".gz") { auto fileTime = std::filesystem::last_write_time(file); auto sctp = std::chrono::time_point_cast(fileTime - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); if (sctp < sevenDaysAgo) { std::filesystem::remove(file); g_logger().info("Deleted old backup file: {}", file.path().string()); } } } } catch (const std::filesystem::filesystem_error &e) { g_logger().error("Failed to check or delete files in backup directory: {}. Error: {}", entry.path().string(), e.what()); } } } } bool Database::beginTransaction() { if (!executeQuery("BEGIN")) { return false; } metrics::lock_latency measureLock("database"); databaseLock.lock(); measureLock.stop(); return true; } bool Database::rollback() { if (!handle) { g_logger().error("Database not initialized!"); return false; } if (mysql_rollback(handle) != 0) { g_logger().error("Message: {}", mysql_error(handle)); databaseLock.unlock(); return false; } databaseLock.unlock(); return true; } bool Database::commit() { if (!handle) { g_logger().error("Database not initialized!"); return false; } if (mysql_commit(handle) != 0) { g_logger().error("Message: {}", mysql_error(handle)); databaseLock.unlock(); return false; } databaseLock.unlock(); return true; } bool Database::isRecoverableError(unsigned int error) { return error == CR_SERVER_LOST || error == CR_SERVER_GONE_ERROR || error == CR_CONN_HOST_ERROR || error == 1053 /*ER_SERVER_SHUTDOWN*/ || error == CR_CONNECTION_ERROR; } bool Database::retryQuery(std::string_view query, int retries) { while (retries > 0 && mysql_query(handle, query.data()) != 0) { g_logger().error("Query: {}", query.substr(0, 256)); g_logger().error("MySQL error [{}]: {}", mysql_errno(handle), mysql_error(handle)); if (!isRecoverableError(mysql_errno(handle))) { return false; } std::this_thread::sleep_for(std::chrono::seconds(1)); retries--; } if (retries == 0) { g_logger().error("Query {} failed after {} retries.", query, 10); return false; } return true; } bool Database::executeQuery(std::string_view query) { if (!handle) { g_logger().error("Database not initialized!"); return false; } g_logger().trace("Executing Query: {}", query); metrics::lock_latency measureLock("database"); std::scoped_lock lock { databaseLock }; measureLock.stop(); metrics::query_latency measure(query.substr(0, 50)); bool success = retryQuery(query, 10); mysql_free_result(mysql_store_result(handle)); return success; } DBResult_ptr Database::storeQuery(std::string_view query) { if (!handle) { g_logger().error("Database not initialized!"); return nullptr; } g_logger().trace("Storing Query: {}", query); metrics::lock_latency measureLock("database"); std::scoped_lock lock { databaseLock }; measureLock.stop(); metrics::query_latency measure(query.substr(0, 50)); retry: if (mysql_query(handle, query.data()) != 0) { g_logger().error("Query: {}", query); g_logger().error("Message: {}", mysql_error(handle)); if (!isRecoverableError(mysql_errno(handle))) { return nullptr; } std::this_thread::sleep_for(std::chrono::seconds(1)); goto retry; } // Retrieving results of query MYSQL_RES* res = mysql_store_result(handle); if (res != nullptr) { DBResult_ptr result = std::make_shared(res); if (!result->hasNext()) { return nullptr; } return result; } return nullptr; } std::string Database::escapeString(const std::string &s) const { std::string::size_type len = s.length(); auto length = static_cast(len); std::string escaped = escapeBlob(s.c_str(), length); if (escaped.empty()) { g_logger().warn("Error escaping string"); } return escaped; } std::string Database::escapeBlob(const char* s, uint32_t length) const { size_t maxLength = (length * 2) + 1; std::string escaped; escaped.reserve(maxLength + 2); escaped.push_back('\''); if (length != 0) { std::string output(maxLength, '\0'); size_t escapedLength = mysql_real_escape_string(handle, &output[0], s, length); output.resize(escapedLength); escaped.append(output); } escaped.push_back('\''); return escaped; } DBResult::DBResult(MYSQL_RES* res) { handle = res; int num_fields = mysql_num_fields(handle); const MYSQL_FIELD* fields = mysql_fetch_fields(handle); for (size_t i = 0; i < num_fields; i++) { listNames[fields[i].name] = i; } row = mysql_fetch_row(handle); } DBResult::~DBResult() { mysql_free_result(handle); } std::string DBResult::getString(const std::string &s) const { auto it = listNames.find(s); if (it == listNames.end()) { g_logger().error("Column '{}' does not exist in result set", s); return {}; } if (row[it->second] == nullptr) { return {}; } return std::string(row[it->second]); } const char* DBResult::getStream(const std::string &s, unsigned long &size) const { auto it = listNames.find(s); if (it == listNames.end()) { g_logger().error("Column '{}' doesn't exist in the result set", s); size = 0; return nullptr; } if (row[it->second] == nullptr) { size = 0; return nullptr; } size = mysql_fetch_lengths(handle)[it->second]; return row[it->second]; } uint8_t DBResult::getU8FromString(const std::string &string, const std::string &function) { auto result = static_cast(std::atoi(string.c_str())); if (result > std::numeric_limits::max()) { g_logger().error("[{}] Failed to get number value {} for tier table result, on function call: {}", __FUNCTION__, result, function); return 0; } return result; } int8_t DBResult::getInt8FromString(const std::string &string, const std::string &function) { auto result = static_cast(std::atoi(string.c_str())); if (result > std::numeric_limits::max()) { g_logger().error("[{}] Failed to get number value {} for tier table result, on function call: {}", __FUNCTION__, result, function); return 0; } return result; } size_t DBResult::countResults() const { return static_cast(mysql_num_rows(handle)); } bool DBResult::hasNext() const { return row != nullptr; } bool DBResult::next() { if (!handle) { g_logger().error("Database not initialized!"); return false; } row = mysql_fetch_row(handle); return row != nullptr; } DBInsert::DBInsert(std::string insertQuery) : query(std::move(insertQuery)) { this->length = this->query.length(); } bool DBInsert::addRow(std::string_view row) { const size_t rowLength = row.length(); length += rowLength; auto max_packet_size = Database::getInstance().getMaxPacketSize(); if (length > max_packet_size && !execute()) { return false; } if (values.empty()) { values.reserve(rowLength + 2); values.push_back('('); values.append(row); values.push_back(')'); } else { values.reserve(values.length() + rowLength + 3); values.push_back(','); values.push_back('('); values.append(row); values.push_back(')'); } return true; } bool DBInsert::addRow(std::ostringstream &row) { bool ret = addRow(row.str()); row.str(std::string()); return ret; } void DBInsert::upsert(const std::vector &columns) { upsertColumns = columns; } bool DBInsert::execute() { if (values.empty()) { return true; } std::string baseQuery = this->query; std::string upsertQuery; if (!upsertColumns.empty()) { std::ostringstream upsertStream; upsertStream << " ON DUPLICATE KEY UPDATE "; for (size_t i = 0; i < upsertColumns.size(); ++i) { upsertStream << "`" << upsertColumns[i] << "` = VALUES(`" << upsertColumns[i] << "`)"; if (i < upsertColumns.size() - 1) { upsertStream << ", "; } } upsertQuery = upsertStream.str(); } std::string currentBatch = values; while (!currentBatch.empty()) { size_t cutPos = Database::MAX_QUERY_SIZE - baseQuery.size() - upsertQuery.size(); if (cutPos < currentBatch.size()) { cutPos = currentBatch.rfind("),(", cutPos); if (cutPos == std::string::npos) { return false; } cutPos += 2; } else { cutPos = currentBatch.size(); } std::string batchValues = currentBatch.substr(0, cutPos); if (batchValues.back() == ',') { batchValues.pop_back(); } currentBatch = currentBatch.substr(cutPos); std::ostringstream query; query << baseQuery << " " << batchValues << upsertQuery; if (!Database::getInstance().executeQuery(query.str())) { return false; } } return true; }