136 changes: 135 additions & 1 deletion src/database/database-files.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
*/

#include <cassert>
#include <json/json.h>
#include "convert_json.h"
#include "database-files.h"
#include "remoteplayer.h"
Expand Down Expand Up @@ -376,3 +375,138 @@ bool AuthDatabaseFiles::writeAuthFile()
}
return true;
}

ModMetadataDatabaseFiles::ModMetadataDatabaseFiles(const std::string &savedir):
m_storage_dir(savedir + DIR_DELIM + "mod_storage")
{
}

bool ModMetadataDatabaseFiles::getModEntries(const std::string &modname, StringMap *storage)
{
Json::Value *meta = getOrCreateJson(modname);
if (!meta)
return false;

const Json::Value::Members attr_list = meta->getMemberNames();
for (const auto &it : attr_list) {
Json::Value attr_value = (*meta)[it];
(*storage)[it] = attr_value.asString();
}

return true;
}

bool ModMetadataDatabaseFiles::setModEntry(const std::string &modname,
const std::string &key, const std::string &value)
{
Json::Value *meta = getOrCreateJson(modname);
if (!meta)
return false;

(*meta)[key] = Json::Value(value);
m_modified.insert(modname);

return true;
}

bool ModMetadataDatabaseFiles::removeModEntry(const std::string &modname,
const std::string &key)
{
Json::Value *meta = getOrCreateJson(modname);
if (!meta)
return false;

Json::Value removed;
if (meta->removeMember(key, &removed)) {
m_modified.insert(modname);
return true;
}
return false;
}

void ModMetadataDatabaseFiles::beginSave()
{
}

void ModMetadataDatabaseFiles::endSave()
{
if (!fs::CreateAllDirs(m_storage_dir)) {
errorstream << "ModMetadataDatabaseFiles: Unable to save. '" << m_storage_dir
<< "' tree cannot be created." << std::endl;
return;
}

for (auto it = m_modified.begin(); it != m_modified.end();) {
const std::string &modname = *it;

if (!fs::PathExists(m_storage_dir)) {
if (!fs::CreateAllDirs(m_storage_dir)) {
errorstream << "ModMetadataDatabaseFiles[" << modname
<< "]: Unable to save. '" << m_storage_dir
<< "' tree cannot be created." << std::endl;
++it;
continue;
}
} else if (!fs::IsDir(m_storage_dir)) {
errorstream << "ModMetadataDatabaseFiles[" << modname << "]: Unable to save. '"
<< m_storage_dir << "' is not a directory." << std::endl;
++it;
continue;
}

const Json::Value &json = m_mod_meta[modname];

if (!fs::safeWriteToFile(m_storage_dir + DIR_DELIM + modname, fastWriteJson(json))) {
errorstream << "ModMetadataDatabaseFiles[" << modname
<< "]: failed write file." << std::endl;
++it;
continue;
}

it = m_modified.erase(it);
}
}

void ModMetadataDatabaseFiles::listMods(std::vector<std::string> *res)
{
// List in-memory metadata first.
for (const auto &pair : m_mod_meta) {
res->push_back(pair.first);
}

// List other metadata present in the filesystem.
for (const auto &entry : fs::GetDirListing(m_storage_dir)) {
if (!entry.dir && m_mod_meta.count(entry.name) == 0)
res->push_back(entry.name);
}
}

Json::Value *ModMetadataDatabaseFiles::getOrCreateJson(const std::string &modname)
{
auto found = m_mod_meta.find(modname);
if (found == m_mod_meta.end()) {
fs::CreateAllDirs(m_storage_dir);

Json::Value meta(Json::objectValue);

std::string path = m_storage_dir + DIR_DELIM + modname;
if (fs::PathExists(path)) {
std::ifstream is(path.c_str(), std::ios_base::binary);

Json::CharReaderBuilder builder;
builder.settings_["collectComments"] = false;
std::string errs;

if (!Json::parseFromStream(builder, is, &meta, &errs)) {
errorstream << "ModMetadataDatabaseFiles[" << modname
<< "]: failed read data (Json decoding failure). Message: "
<< errs << std::endl;
return nullptr;
}
}

return &(m_mod_meta[modname] = meta);
} else {
return &found->second;
}
}
26 changes: 26 additions & 0 deletions src/database/database-files.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,

#include "database.h"
#include <unordered_map>
#include <unordered_set>
#include <json/json.h>

class PlayerDatabaseFiles : public PlayerDatabase
{
Expand Down Expand Up @@ -69,3 +71,27 @@ class AuthDatabaseFiles : public AuthDatabase
bool readAuthFile();
bool writeAuthFile();
};

class ModMetadataDatabaseFiles : public ModMetadataDatabase
{
public:
ModMetadataDatabaseFiles(const std::string &savedir);
virtual ~ModMetadataDatabaseFiles() = default;

virtual bool getModEntries(const std::string &modname, StringMap *storage);
virtual bool setModEntry(const std::string &modname,
const std::string &key, const std::string &value);
virtual bool removeModEntry(const std::string &modname, const std::string &key);
virtual void listMods(std::vector<std::string> *res);

virtual void beginSave();
virtual void endSave();

private:
Json::Value *getOrCreateJson(const std::string &modname);
bool writeJson(const std::string &modname, const Json::Value &json);

std::string m_storage_dir;
std::unordered_map<std::string, Json::Value> m_mod_meta;
std::unordered_set<std::string> m_modified;
};
105 changes: 105 additions & 0 deletions src/database/database-sqlite3.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -779,3 +779,108 @@ void AuthDatabaseSQLite3::writePrivileges(const AuthEntry &authEntry)
sqlite3_reset(m_stmt_write_privs);
}
}

ModMetadataDatabaseSQLite3::ModMetadataDatabaseSQLite3(const std::string &savedir):
Database_SQLite3(savedir, "mod_storage"), ModMetadataDatabase()
{
}

ModMetadataDatabaseSQLite3::~ModMetadataDatabaseSQLite3()
{
FINALIZE_STATEMENT(m_stmt_remove)
FINALIZE_STATEMENT(m_stmt_set)
FINALIZE_STATEMENT(m_stmt_get)
}

void ModMetadataDatabaseSQLite3::createDatabase()
{
assert(m_database); // Pre-condition

SQLOK(sqlite3_exec(m_database,
"CREATE TABLE IF NOT EXISTS `entries` (\n"
" `modname` TEXT NOT NULL,\n"
TurkeyMcMac marked this conversation as resolved.
Show resolved Hide resolved
" `key` BLOB NOT NULL,\n"
TurkeyMcMac marked this conversation as resolved.
Show resolved Hide resolved
TurkeyMcMac marked this conversation as resolved.
Show resolved Hide resolved
" `value` BLOB NOT NULL,\n"
" PRIMARY KEY (`modname`, `key`)\n"
");\n",
NULL, NULL, NULL),
"Failed to create database table");
}

void ModMetadataDatabaseSQLite3::initStatements()
{
PREPARE_STATEMENT(get, "SELECT `key`, `value` FROM `entries` WHERE `modname` = ?");
PREPARE_STATEMENT(set,
"REPLACE INTO `entries` (`modname`, `key`, `value`) VALUES (?, ?, ?)");
PREPARE_STATEMENT(remove, "DELETE FROM `entries` WHERE `modname` = ? AND `key` = ?");
}

bool ModMetadataDatabaseSQLite3::getModEntries(const std::string &modname, StringMap *storage)
{
verifyDatabase();

str_to_sqlite(m_stmt_get, 1, modname);
while (sqlite3_step(m_stmt_get) == SQLITE_ROW) {
const char *key_data = (const char *) sqlite3_column_blob(m_stmt_get, 0);
size_t key_len = sqlite3_column_bytes(m_stmt_get, 0);
const char *value_data = (const char *) sqlite3_column_blob(m_stmt_get, 1);
size_t value_len = sqlite3_column_bytes(m_stmt_get, 1);
(*storage)[std::string(key_data, key_len)] = std::string(value_data, value_len);
}
sqlite3_vrfy(sqlite3_errcode(m_database), SQLITE_DONE);

sqlite3_reset(m_stmt_get);

return true;
}

bool ModMetadataDatabaseSQLite3::setModEntry(const std::string &modname,
const std::string &key, const std::string &value)
{
verifyDatabase();

str_to_sqlite(m_stmt_set, 1, modname);
SQLOK(sqlite3_bind_blob(m_stmt_set, 2, key.data(), key.size(), NULL),
"Internal error: failed to bind query at " __FILE__ ":" TOSTRING(__LINE__));
SQLOK(sqlite3_bind_blob(m_stmt_set, 3, value.data(), value.size(), NULL),
"Internal error: failed to bind query at " __FILE__ ":" TOSTRING(__LINE__));
SQLRES(sqlite3_step(m_stmt_set), SQLITE_DONE, "Failed to set mod entry")

sqlite3_reset(m_stmt_set);

return true;
}

bool ModMetadataDatabaseSQLite3::removeModEntry(const std::string &modname,
const std::string &key)
{
verifyDatabase();

str_to_sqlite(m_stmt_remove, 1, modname);
SQLOK(sqlite3_bind_blob(m_stmt_remove, 2, key.data(), key.size(), NULL),
"Internal error: failed to bind query at " __FILE__ ":" TOSTRING(__LINE__));
sqlite3_vrfy(sqlite3_step(m_stmt_remove), SQLITE_DONE);
int changes = sqlite3_changes(m_database);

sqlite3_reset(m_stmt_remove);

return changes > 0;
}

void ModMetadataDatabaseSQLite3::listMods(std::vector<std::string> *res)
{
verifyDatabase();

char *errmsg;
int status = sqlite3_exec(m_database,
"SELECT `modname` FROM `entries` GROUP BY `modname`;",
[](void *res_vp, int n_col, char **cols, char **col_names) -> int {
((decltype(res)) res_vp)->emplace_back(cols[0]);
return 0;
}, (void *) res, &errmsg);
Comment on lines +875 to +880
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be more efficient to use the mod name as table name, rather than a separate column?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be. I haven't tested. The currently implementation is simpler, especially within the existing framework of Database_SQLite3. IMO the important thing is that the cost of changing a metadata entry is now dependent on the size of the entry rather than the combined size of all the mod's entries.

if (status != SQLITE_OK) {
DatabaseException e(std::string("Error trying to list mods with metadata: ") + errmsg);
sqlite3_free(errmsg);
throw e;
}
}
25 changes: 25 additions & 0 deletions src/database/database-sqlite3.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,28 @@ class AuthDatabaseSQLite3 : private Database_SQLite3, public AuthDatabase
sqlite3_stmt *m_stmt_delete_privs = nullptr;
sqlite3_stmt *m_stmt_last_insert_rowid = nullptr;
};

class ModMetadataDatabaseSQLite3 : private Database_SQLite3, public ModMetadataDatabase
{
public:
ModMetadataDatabaseSQLite3(const std::string &savedir);
virtual ~ModMetadataDatabaseSQLite3();

virtual bool getModEntries(const std::string &modname, StringMap *storage);
virtual bool setModEntry(const std::string &modname,
const std::string &key, const std::string &value);
virtual bool removeModEntry(const std::string &modname, const std::string &key);
virtual void listMods(std::vector<std::string> *res);

virtual void beginSave() { Database_SQLite3::beginSave(); }
virtual void endSave() { Database_SQLite3::endSave(); }

protected:
virtual void createDatabase();
virtual void initStatements();

private:
sqlite3_stmt *m_stmt_get = nullptr;
sqlite3_stmt *m_stmt_set = nullptr;
sqlite3_stmt *m_stmt_remove = nullptr;
};
13 changes: 13 additions & 0 deletions src/database/database.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "irr_v3d.h"
#include "irrlichttypes.h"
#include "util/basic_macros.h"
#include "util/string.h"
TurkeyMcMac marked this conversation as resolved.
Show resolved Hide resolved

class Database
{
Expand Down Expand Up @@ -84,3 +85,15 @@ class AuthDatabase
virtual void listNames(std::vector<std::string> &res) = 0;
virtual void reload() = 0;
};

class ModMetadataDatabase : public Database
{
public:
virtual ~ModMetadataDatabase() = default;

virtual bool getModEntries(const std::string &modname, StringMap *storage) = 0;
virtual bool setModEntry(const std::string &modname,
const std::string &key, const std::string &value) = 0;
virtual bool removeModEntry(const std::string &modname, const std::string &key) = 0;
virtual void listMods(std::vector<std::string> *res) = 0;
};
3 changes: 2 additions & 1 deletion src/gamedef.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class EmergeManager;
class Camera;
class ModChannel;
class ModMetadata;
class ModMetadataDatabase;

namespace irr { namespace scene {
class IAnimatedMesh;
Expand Down Expand Up @@ -70,9 +71,9 @@ class IGameDef
virtual const std::vector<ModSpec> &getMods() const = 0;
virtual const ModSpec* getModSpec(const std::string &modname) const = 0;
virtual std::string getWorldPath() const { return ""; }
virtual std::string getModStoragePath() const = 0;
virtual bool registerModStorage(ModMetadata *storage) = 0;
virtual void unregisterModStorage(const std::string &name) = 0;
virtual ModMetadataDatabase *getModStorageDatabase() = 0;

virtual bool joinModChannel(const std::string &channel) = 0;
virtual bool leaveModChannel(const std::string &channel) = 0;
Expand Down
5 changes: 5 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,8 @@ static void set_allowed_options(OptionList *allowed_options)
_("Migrate from current players backend to another (Only works when using minetestserver or with --server)"))));
allowed_options->insert(std::make_pair("migrate-auth", ValueSpec(VALUETYPE_STRING,
_("Migrate from current auth backend to another (Only works when using minetestserver or with --server)"))));
allowed_options->insert(std::make_pair("migrate-mod-storage", ValueSpec(VALUETYPE_STRING,
_("Migrate from current mod storage backend to another (Only works when using minetestserver or with --server)"))));
allowed_options->insert(std::make_pair("terminal", ValueSpec(VALUETYPE_FLAG,
_("Feature an interactive terminal (Only works when using minetestserver or with --server)"))));
allowed_options->insert(std::make_pair("recompress", ValueSpec(VALUETYPE_FLAG,
Expand Down Expand Up @@ -886,6 +888,9 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
if (cmd_args.exists("migrate-auth"))
return ServerEnvironment::migrateAuthDatabase(game_params, cmd_args);

if (cmd_args.exists("migrate-mod-storage"))
return Server::migrateModStorageDatabase(game_params, cmd_args);

if (cmd_args.getFlag("recompress"))
return recompress_map_database(game_params, cmd_args, bind_addr);

Expand Down
18 changes: 11 additions & 7 deletions src/script/lua_api/l_storage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,23 @@ int ModApiStorage::l_get_mod_storage(lua_State *L)

std::string mod_name = readParam<std::string>(L, -1);

ModMetadata *store = new ModMetadata(mod_name);
ModMetadata *store = nullptr;

if (IGameDef *gamedef = getGameDef(L)) {
store->load(gamedef->getModStoragePath());
gamedef->registerModStorage(store);
store = new ModMetadata(mod_name, gamedef->getModStorageDatabase());
if (gamedef->registerModStorage(store)) {
StorageRef::create(L, store);
int object = lua_gettop(L);
lua_pushvalue(L, object);
return 1;
}
} else {
delete store;
assert(false); // this should not happen
}

StorageRef::create(L, store);
int object = lua_gettop(L);
delete store;

lua_pushvalue(L, object);
lua_pushnil(L);
return 1;
}

Expand Down
140 changes: 120 additions & 20 deletions src/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "server/player_sao.h"
#include "server/serverinventorymgr.h"
#include "translation.h"
#include "database/database-sqlite3.h"
#include "database/database-files.h"
#include "database/database-dummy.h"
#include "gameparams.h"

class ClientNotFoundException : public BaseException
{
Expand Down Expand Up @@ -344,10 +348,15 @@ Server::~Server()
delete m_thread;
}

// Write any changes before deletion.
if (m_mod_storage_database)
m_mod_storage_database->endSave();

// Delete things in the reverse order of creation
delete m_emerge;
delete m_env;
delete m_rollback;
delete m_mod_storage_database;
delete m_banmanager;
delete m_itemdef;
delete m_nodedef;
Expand Down Expand Up @@ -393,6 +402,10 @@ void Server::init()
std::string ban_path = m_path_world + DIR_DELIM "ipban.txt";
m_banmanager = new BanManager(ban_path);

// Create mod storage database and begin a save for later
m_mod_storage_database = openModStorageDatabase(m_path_world);
m_mod_storage_database->beginSave();

m_modmgr = std::unique_ptr<ServerModManager>(new ServerModManager(m_path_world));
std::vector<ModSpec> unsatisfied_mods = m_modmgr->getUnsatisfiedMods();
// complain about mods with unsatisfied dependencies
Expand Down Expand Up @@ -734,20 +747,12 @@ void Server::AsyncRunStep(bool initial_step)
}
m_clients.unlock();

// Save mod storages if modified
// Write changes to the mod storage
m_mod_storage_save_timer -= dtime;
if (m_mod_storage_save_timer <= 0.0f) {
m_mod_storage_save_timer = g_settings->getFloat("server_map_save_interval");
int n = 0;
for (std::unordered_map<std::string, ModMetadata *>::const_iterator
it = m_mod_storages.begin(); it != m_mod_storages.end(); ++it) {
if (it->second->isModified()) {
it->second->save(getModStoragePath());
n++;
}
}
if (n > 0)
infostream << "Saved " << n << " modified mod storages." << std::endl;
m_mod_storage_database->endSave();
m_mod_storage_database->beginSave();
}
}

Expand Down Expand Up @@ -3690,11 +3695,6 @@ std::string Server::getBuiltinLuaPath()
return porting::path_share + DIR_DELIM + "builtin";
}

std::string Server::getModStoragePath() const
{
return m_path_world + DIR_DELIM + "mod_storage";
}

v3f Server::findSpawnPos()
{
ServerMap &map = m_env->getServerMap();
Expand Down Expand Up @@ -3858,11 +3858,8 @@ bool Server::registerModStorage(ModMetadata *storage)
void Server::unregisterModStorage(const std::string &name)
{
std::unordered_map<std::string, ModMetadata *>::const_iterator it = m_mod_storages.find(name);
if (it != m_mod_storages.end()) {
// Save unconditionaly on unregistration
it->second->save(getModStoragePath());
if (it != m_mod_storages.end())
m_mod_storages.erase(name);
}
}

void dedicated_server_loop(Server &server, bool &kill)
Expand Down Expand Up @@ -4000,3 +3997,106 @@ Translations *Server::getTranslationLanguage(const std::string &lang_code)

return translations;
}

ModMetadataDatabase *Server::openModStorageDatabase(const std::string &world_path)
{
std::string world_mt_path = world_path + DIR_DELIM + "world.mt";
Settings world_mt;
if (!world_mt.readConfigFile(world_mt_path.c_str()))
throw BaseException("Cannot read world.mt!");

std::string backend = world_mt.exists("mod_storage_backend") ?
world_mt.get("mod_storage_backend") : "files";
if (backend == "files")
warningstream << "/!\\ You are using the old mod storage files backend. "
<< "This backend is deprecated and may be removed in a future release /!\\"
<< std::endl << "Switching to SQLite3 is advised, "
<< "please read http://wiki.minetest.net/Database_backends." << std::endl;

return openModStorageDatabase(backend, world_path, world_mt);
}

ModMetadataDatabase *Server::openModStorageDatabase(const std::string &backend,
const std::string &world_path, const Settings &world_mt)
{
if (backend == "sqlite3")
return new ModMetadataDatabaseSQLite3(world_path);

if (backend == "files")
return new ModMetadataDatabaseFiles(world_path);

if (backend == "dummy")
return new Database_Dummy();

throw BaseException("Mod storage database backend " + backend + " not supported");
}

bool Server::migrateModStorageDatabase(const GameParams &game_params, const Settings &cmd_args)
{
std::string migrate_to = cmd_args.get("migrate-mod-storage");
Settings world_mt;
std::string world_mt_path = game_params.world_path + DIR_DELIM + "world.mt";
if (!world_mt.readConfigFile(world_mt_path.c_str())) {
errorstream << "Cannot read world.mt!" << std::endl;
return false;
}

std::string backend = world_mt.exists("mod_storage_backend") ?
world_mt.get("mod_storage_backend") : "files";
if (backend == migrate_to) {
errorstream << "Cannot migrate: new backend is same"
<< " as the old one" << std::endl;
return false;
}

ModMetadataDatabase *srcdb = nullptr;
ModMetadataDatabase *dstdb = nullptr;

bool succeeded = false;

try {
srcdb = Server::openModStorageDatabase(backend, game_params.world_path, world_mt);
dstdb = Server::openModStorageDatabase(migrate_to, game_params.world_path, world_mt);

dstdb->beginSave();

std::vector<std::string> mod_list;
srcdb->listMods(&mod_list);
for (const std::string &modname : mod_list) {
StringMap meta;
srcdb->getModEntries(modname, &meta);
for (const auto &pair : meta) {
dstdb->setModEntry(modname, pair.first, pair.second);
}
}

dstdb->endSave();

succeeded = true;

actionstream << "Successfully migrated the metadata of "
<< mod_list.size() << " mods" << std::endl;
world_mt.set("mod_storage_backend", migrate_to);
if (!world_mt.updateConfigFile(world_mt_path.c_str()))
errorstream << "Failed to update world.mt!" << std::endl;
else
actionstream << "world.mt updated" << std::endl;

} catch (BaseException &e) {
errorstream << "An error occurred during migration: " << e.what() << std::endl;
}

delete srcdb;
delete dstdb;

if (succeeded && backend == "files") {
// Back up files
const std::string storage_path = game_params.world_path + DIR_DELIM + "mod_storage";
const std::string backup_path = game_params.world_path + DIR_DELIM + "mod_storage.bak";
if (!fs::Rename(storage_path, backup_path))
warningstream << "After migration, " << storage_path
<< " could not be renamed to " << backup_path << std::endl;
}

return succeeded;
}
11 changes: 10 additions & 1 deletion src/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ class Server : public con::PeerHandler, public MapEventReceiver,
virtual u16 allocateUnknownNodeId(const std::string &name);
IRollbackManager *getRollbackManager() { return m_rollback; }
virtual EmergeManager *getEmergeManager() { return m_emerge; }
virtual ModMetadataDatabase *getModStorageDatabase() { return m_mod_storage_database; }

IWritableItemDefManager* getWritableItemDefManager();
NodeDefManager* getWritableNodeDefManager();
Expand All @@ -293,7 +294,6 @@ class Server : public con::PeerHandler, public MapEventReceiver,
void getModNames(std::vector<std::string> &modlist);
std::string getBuiltinLuaPath();
virtual std::string getWorldPath() const { return m_path_world; }
virtual std::string getModStoragePath() const;

inline bool isSingleplayer()
{ return m_simple_singleplayer_mode; }
Expand Down Expand Up @@ -377,6 +377,14 @@ class Server : public con::PeerHandler, public MapEventReceiver,
// Get or load translations for a language
Translations *getTranslationLanguage(const std::string &lang_code);

static ModMetadataDatabase *openModStorageDatabase(const std::string &world_path);

static ModMetadataDatabase *openModStorageDatabase(const std::string &backend,
const std::string &world_path, const Settings &world_mt);

static bool migrateModStorageDatabase(const GameParams &game_params,
const Settings &cmd_args);

// Bind address
Address m_bind_addr;

Expand Down Expand Up @@ -678,6 +686,7 @@ class Server : public con::PeerHandler, public MapEventReceiver,
s32 nextSoundId();

std::unordered_map<std::string, ModMetadata *> m_mod_storages;
ModMetadataDatabase *m_mod_storage_database = nullptr;
float m_mod_storage_save_timer = 10.0f;

// CSM restrictions byteflag
Expand Down
1 change: 1 addition & 0 deletions src/unittest/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ set (UNITTEST_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/test_map_settings_manager.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_mapnode.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_modchannels.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_modmetadatadatabase.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_nodedef.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_noderesolver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_noise.cpp
Expand Down
6 changes: 5 additions & 1 deletion src/unittest/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "gamedef.h"
#include "modchannels.h"
#include "content/mods.h"
#include "database/database-dummy.h"
#include "util/numeric.h"
#include "porting.h"

Expand Down Expand Up @@ -55,6 +56,7 @@ class TestGameDef : public IGameDef {
scene::ISceneManager *getSceneManager() { return m_scenemgr; }
IRollbackManager *getRollbackManager() { return m_rollbackmgr; }
EmergeManager *getEmergeManager() { return m_emergemgr; }
ModMetadataDatabase *getModStorageDatabase() { return m_mod_storage_database; }

scene::IAnimatedMesh *getMesh(const std::string &filename) { return NULL; }
bool checkLocalPrivilege(const std::string &priv) { return false; }
Expand All @@ -68,7 +70,6 @@ class TestGameDef : public IGameDef {
return testmodspec;
}
virtual const ModSpec* getModSpec(const std::string &modname) const { return NULL; }
virtual std::string getModStoragePath() const { return "."; }
virtual bool registerModStorage(ModMetadata *meta) { return true; }
virtual void unregisterModStorage(const std::string &name) {}
bool joinModChannel(const std::string &channel);
Expand All @@ -89,11 +90,13 @@ class TestGameDef : public IGameDef {
scene::ISceneManager *m_scenemgr = nullptr;
IRollbackManager *m_rollbackmgr = nullptr;
EmergeManager *m_emergemgr = nullptr;
ModMetadataDatabase *m_mod_storage_database = nullptr;
std::unique_ptr<ModChannelMgr> m_modchannel_mgr;
};


TestGameDef::TestGameDef() :
m_mod_storage_database(new Database_Dummy()),
m_modchannel_mgr(new ModChannelMgr())
{
m_itemdef = createItemDefManager();
Expand All @@ -107,6 +110,7 @@ TestGameDef::~TestGameDef()
{
delete m_itemdef;
delete m_nodedef;
delete m_mod_storage_database;
}


Expand Down
253 changes: 253 additions & 0 deletions src/unittest/test_modmetadatadatabase.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
/*
Minetest
Copyright (C) 2018 bendeutsch, Ben Deutsch <ben@bendeutsch.de>
Copyright (C) 2021 TurkeyMcMac, Jude Melton-Houghton <jwmhjwmh@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

// This file is an edited copy of test_authdatabase.cpp

#include "test.h"

#include <algorithm>
#include "database/database-files.h"
#include "database/database-sqlite3.h"
#include "filesys.h"

namespace
{
// Anonymous namespace to create classes that are only
// visible to this file
//
// These are helpers that return a *ModMetadataDatabase and
// allow us to run the same tests on different databases and
// database acquisition strategies.

class ModMetadataDatabaseProvider
{
public:
virtual ~ModMetadataDatabaseProvider() = default;
virtual ModMetadataDatabase *getModMetadataDatabase() = 0;
};

class FixedProvider : public ModMetadataDatabaseProvider
{
public:
FixedProvider(ModMetadataDatabase *mod_meta_db) : mod_meta_db(mod_meta_db){};
virtual ~FixedProvider(){};
virtual ModMetadataDatabase *getModMetadataDatabase() { return mod_meta_db; };

private:
ModMetadataDatabase *mod_meta_db;
};

class FilesProvider : public ModMetadataDatabaseProvider
{
public:
FilesProvider(const std::string &dir) : dir(dir){};
virtual ~FilesProvider()
{
if (mod_meta_db)
mod_meta_db->endSave();
delete mod_meta_db;
}
virtual ModMetadataDatabase *getModMetadataDatabase()
{
if (mod_meta_db)
mod_meta_db->endSave();
delete mod_meta_db;
mod_meta_db = new ModMetadataDatabaseFiles(dir);
mod_meta_db->beginSave();
return mod_meta_db;
};

private:
std::string dir;
ModMetadataDatabase *mod_meta_db = nullptr;
};

class SQLite3Provider : public ModMetadataDatabaseProvider
{
public:
SQLite3Provider(const std::string &dir) : dir(dir){};
virtual ~SQLite3Provider()
{
if (mod_meta_db)
mod_meta_db->endSave();
delete mod_meta_db;
}
virtual ModMetadataDatabase *getModMetadataDatabase()
{
if (mod_meta_db)
mod_meta_db->endSave();
delete mod_meta_db;
mod_meta_db = new ModMetadataDatabaseSQLite3(dir);
mod_meta_db->beginSave();
return mod_meta_db;
};

private:
std::string dir;
ModMetadataDatabase *mod_meta_db = nullptr;
};
}

class TestModMetadataDatabase : public TestBase
{
public:
TestModMetadataDatabase() { TestManager::registerTestModule(this); }
const char *getName() { return "TestModMetadataDatabase"; }

void runTests(IGameDef *gamedef);
void runTestsForCurrentDB();

void testRecallFail();
void testCreate();
void testRecall();
void testChange();
void testRecallChanged();
void testListMods();
void testRemove();

private:
ModMetadataDatabaseProvider *mod_meta_provider;
};

static TestModMetadataDatabase g_test_instance;

void TestModMetadataDatabase::runTests(IGameDef *gamedef)
{
// fixed directory, for persistence
thread_local const std::string test_dir = getTestTempDirectory();

// Each set of tests is run twice for each database type:
// one where we reuse the same ModMetadataDatabase object (to test local caching),
// and one where we create a new ModMetadataDatabase object for each call
// (to test actual persistence).

rawstream << "-------- Files database (same object)" << std::endl;

ModMetadataDatabase *mod_meta_db = new ModMetadataDatabaseFiles(test_dir);
mod_meta_provider = new FixedProvider(mod_meta_db);

runTestsForCurrentDB();

delete mod_meta_db;
delete mod_meta_provider;

// reset database
fs::RecursiveDelete(test_dir + DIR_DELIM + "mod_storage");

rawstream << "-------- Files database (new objects)" << std::endl;

mod_meta_provider = new FilesProvider(test_dir);

runTestsForCurrentDB();

delete mod_meta_provider;

rawstream << "-------- SQLite3 database (same object)" << std::endl;

mod_meta_db = new ModMetadataDatabaseSQLite3(test_dir);
mod_meta_provider = new FixedProvider(mod_meta_db);

runTestsForCurrentDB();

delete mod_meta_db;
delete mod_meta_provider;

// reset database
fs::DeleteSingleFileOrEmptyDirectory(test_dir + DIR_DELIM + "mod_storage.sqlite");

rawstream << "-------- SQLite3 database (new objects)" << std::endl;

mod_meta_provider = new SQLite3Provider(test_dir);

runTestsForCurrentDB();

delete mod_meta_provider;
}

////////////////////////////////////////////////////////////////////////////////

void TestModMetadataDatabase::runTestsForCurrentDB()
{
TEST(testRecallFail);
TEST(testCreate);
TEST(testRecall);
TEST(testChange);
TEST(testRecallChanged);
TEST(testListMods);
TEST(testRemove);
TEST(testRecallFail);
}

void TestModMetadataDatabase::testRecallFail()
{
ModMetadataDatabase *mod_meta_db = mod_meta_provider->getModMetadataDatabase();
StringMap recalled;
mod_meta_db->getModEntries("mod1", &recalled);
UASSERT(recalled.empty());
}

void TestModMetadataDatabase::testCreate()
{
ModMetadataDatabase *mod_meta_db = mod_meta_provider->getModMetadataDatabase();
StringMap recalled;
UASSERT(mod_meta_db->setModEntry("mod1", "key1", "value1"));
}

void TestModMetadataDatabase::testRecall()
{
ModMetadataDatabase *mod_meta_db = mod_meta_provider->getModMetadataDatabase();
StringMap recalled;
mod_meta_db->getModEntries("mod1", &recalled);
UASSERT(recalled.size() == 1);
UASSERT(recalled["key1"] == "value1");
}

void TestModMetadataDatabase::testChange()
{
ModMetadataDatabase *mod_meta_db = mod_meta_provider->getModMetadataDatabase();
StringMap recalled;
UASSERT(mod_meta_db->setModEntry("mod1", "key1", "value2"));
}

void TestModMetadataDatabase::testRecallChanged()
{
ModMetadataDatabase *mod_meta_db = mod_meta_provider->getModMetadataDatabase();
StringMap recalled;
mod_meta_db->getModEntries("mod1", &recalled);
UASSERT(recalled.size() == 1);
UASSERT(recalled["key1"] == "value2");
}

void TestModMetadataDatabase::testListMods()
{
ModMetadataDatabase *mod_meta_db = mod_meta_provider->getModMetadataDatabase();
UASSERT(mod_meta_db->setModEntry("mod2", "key1", "value1"));
std::vector<std::string> mod_list;
mod_meta_db->listMods(&mod_list);
UASSERT(mod_list.size() == 2);
UASSERT(std::find(mod_list.cbegin(), mod_list.cend(), "mod1") != mod_list.cend());
UASSERT(std::find(mod_list.cbegin(), mod_list.cend(), "mod2") != mod_list.cend());
}

void TestModMetadataDatabase::testRemove()
{
ModMetadataDatabase *mod_meta_db = mod_meta_provider->getModMetadataDatabase();
UASSERT(mod_meta_db->removeModEntry("mod1", "key1"));
}