From 387e6e7ffa747a27412c66c7efca6fde4abc6169 Mon Sep 17 00:00:00 2001 From: Dorian Wouters Date: Sat, 3 Jun 2017 22:58:42 -0400 Subject: [PATCH] Initial commit --- .gitmodules | 3 + README.md | 108 ++++++++++++++++++++++++++++++ init.lua | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++ mysql | 1 + 4 files changed, 298 insertions(+) create mode 100644 .gitmodules create mode 100644 README.md create mode 100644 init.lua create mode 160000 mysql diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..065a19b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "mysql"] + path = mysql + url = https://github.com/luapower/mysql.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..56cd50b --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# mysql_base + +Base Minetest mod to connect to a MySQL database. Used by other mods to read/write data. + +# Installing + +Get this repository's contents using `git`, and make sure to fetch submodules +(`git submodule update --init`). + +# Configuration + +First, if mod security is enabled (`secure.enable_security = true`), this mod must be added as +a trusted mod (in the `secure.trusted_mods` config entry). There is **no** other solution to +make it work under mod security. + +By default `mysql_base` doesn't run in singleplayer. This can be overriden by setting +`mysql_base.enable_singleplayer` to `true`. + +Configuration may be done as regular Minetest settings entries, or using a config file, allowing +for more configuration options; to do so specify the path as `mysql_base.cfgfile`. This config +must contain a Lua table that can be read by `minetest.deserialize`, i.e. a regular table +definition follwing a `return` statement (see the example below). + +When using flat Minetest configuation entries, all the following option names must be prefixed +with `mysql_base.`. When using a config file, entries are to be hierarchised as per the dot +separator. + +Values written next to option names are default values. + +## Database connection + +### Minetest flat config file + +Values after the "`=`" are the default values used if unspecified. +``` +mysql_base.db.host = 'localhost' +mysql_base.db.user = nil -- MySQL connector defaults to current username +mysql_base.db.pass = nil -- Using password: NO +mysql_base.db.port = nil -- MySQL connector defaults to either 3306, or no port if using localhost/unix socket +mysql_base.db.db = nil -- <== Setting this is required +``` + +### Lua table config file + +Connection options are passed as a table through the `db.connopts` entry. +Its format must be the same as [LuaPower's MySQL module `mysql.connect(options_t)` function][mycn], +that is (all members are optional); + +```lua +return { + db = { + connopts = { + host = ..., + user = ..., + pass = ..., + db = ..., + port = ..., + unix_socket = ..., + flags = { ... }, + options = { ... }, + attrs = { ... }, + -- Also key, cert, ca, cpath, cipher + } + } +} +``` + +## Examples + +### Example 1 + +#### Using a Lua config file + +`minetest.conf`: +``` +mysql_auth.cfgfile = /srv/minetest/skyblock/mysql_auth_config +``` + +`/srv/minetest/skyblock/mysql_auth_config`: +```lua +return { + db = { + connopts = { + user = 'minetest', + pass = 'BQy77wK$Um6es3Bi($iZ*w3N', + db = 'minetest' + }, + } +} +``` + +#### Using only Minetest config entries + +`minetest.conf`: +``` +mysql_auth.db.user = minetest +mysql_auth.db.pass = BQy77wK$Um6es3Bi($iZ*w3N +mysql_auth.db.db = minetest +``` + +# License + +`mysql_base` is licensed under [LGPLv3](https://www.gnu.org/licenses/lgpl.html). + +Using the Public Domain-licensed LuaPower `mysql` module. + + +[mycn]: https://luapower.com/mysql#mysql.connectoptions_t---conn diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..c0f0591 --- /dev/null +++ b/init.lua @@ -0,0 +1,186 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) + +local thismod = { + enabled = false, +} +_G[modname] = thismod + +local singleplayer = minetest.is_singleplayer() -- Caching is OK since you can't open a game to +-- multiplayer unless you restart it. +if not minetest.setting_get(modname .. '.enable_singleplayer') and singleplayer then + minetest.log('action', modname .. ": Not enabling because of singleplayer game") + return +end + +thismod.enabled = true + +local function setoverlay(tab, orig) + local mt = getmetatable(tab) or {} + mt.__index = function (tab, key) + if rawget(tab, key) ~= nil then + return rawget(tab, key) + else + return orig[key] + end + end + setmetatable(tab, mt) +end + +local function string_splitdots(s) + local temp = {} + local index = 0 + local last_index = string.len(s) + while true do + local i, e = string.find(s, '%.', index) + if i and e then + local next_index = e + 1 + local word_bound = i - 1 + table.insert(temp, string.sub(s, index, word_bound)) + index = next_index + else + if index > 0 and index <= last_index then + table.insert(temp, string.sub(s, index, last_index)) + elseif index == 0 then + temp = nil + end + break + end + end + return temp +end + +local mysql +do -- MySQL module loading + local env = { + require = function (module) + if module == 'mysql_h' then + return dofile(modpath .. '/mysql/mysql_h.lua') + else + return require(module) + end + end + } + setoverlay(env, _G) + local fn, msg = loadfile(modpath .. '/mysql/mysql.lua') + if not fn then error(msg) end + setfenv(fn, env) + local status + status, mysql = pcall(fn, {}) + if not status then + error(modname .. ' failed to load MySQL FFI interface: ' .. tostring(mysql)) + end + thismod.mysql = mysql +end + +function thismod.mkget(modname) + local get = function (name) return minetest.setting_get(modname .. '.' .. name) end + local cfgfile = get('cfgfile') + if type(cfgfile) == 'string' and cfgfile ~= '' then + local file = io.open(cfgfile, 'rb') + if not file then + error(modname .. ' failed to load specified config file at ' .. cfgfile) + end + local cfg, msg = minetest.deserialize(file:read('*a')) + file:close() + if not cfg then + error(modname .. ' failed to parse specified config file at ' .. cfgfile .. ': ' .. msg) + end + get = function (name) + if type(name) ~= 'string' or name == '' then + return nil + end + local parts = string_splitdots(name) + if not parts then + return cfg[name] + end + local tbl = cfg[parts[1]] + for n = 2, #parts do + if tbl == nil then + return nil + end + tbl = tbl[parts[n]] + end + return tbl + end + end + return get +end + +local get = thismod.mkget(modname) +do + local conn, dbname + -- MySQL API backend + mysql.config(get('db.api')) + + local connopts = get('db.connopts') + if (get('db.db') == nil) and (type(connopts) == 'table' and connopts.db == nil) then + error(modname .. ": missing database name parameter") + end + if type(connopts) ~= 'table' then + connopts = {} + -- Traditional connection parameters + connopts.host, connopts.user, connopts.port, connopts.pass, connopts.db = + get('db.host') or 'localhost', get('db.user'), get('db.port'), get('db.pass'), get('db.db') + end + connopts.charset = 'utf8' + connopts.options = connopts.options or {} + connopts.options.MYSQL_OPT_RECONNECT = true + conn = mysql.connect(connopts) + dbname = connopts.db + minetest.log('action', modname .. ": Connected to MySQL database " .. dbname) + thismod.conn = conn + thismod.dbname = dbname + + -- LuaPower's MySQL interface throws an error when the connection fails, no need to check if + -- it succeeded. + + -- Ensure UTF-8 is in use. + -- If you use another encoding, kill yourself (unless it's UTF-32). + conn:query("SET NAMES 'utf8'") + conn:query("SET CHARACTER SET utf8") + conn:query("SET character_set_results = 'utf8', character_set_client = 'utf8'," .. + "character_set_connection = 'utf8', character_set_database = 'utf8'," .. + "character_set_server = 'utf8'") + + local set = function(setting, val) conn:query('SET ' .. setting .. '=' .. val) end + pcall(set, 'wait_timeout', 3600) + pcall(set, 'autocommit', 1) + pcall(set, 'max_allowed_packet', 67108864) +end + +local function ping() + if thismod.conn then + if not thismod.conn:ping() then + minetest.log('error', modname .. ": failed to ping database") + end + end + minetest.after(1800, ping) +end +minetest.after(10, ping) + +local shutdown_callbacks = {} +function thismod.register_on_shutdown(func) + table.insert(shutdown_callbacks, func) +end + +minetest.register_on_shutdown(function() + if thismod.conn then + minetest.log('action', modname .. ": Shutting down, running callbacks") + for _, func in ipairs(shutdown_callbacks) do + func() + end + thismod.conn:close() + thismod.conn = nil + minetest.log('action', modname .. ": Cosed database connection") + end +end) + +function thismod.table_exists(name) + thismod.conn:query("SHOW TABLES LIKE '" .. name .. "'") + local res = thismod.conn:store_result() + local exists = (res:row_count() ~= 0) + res:free() + return exists +end + diff --git a/mysql b/mysql new file mode 160000 index 0000000..4bc8277 --- /dev/null +++ b/mysql @@ -0,0 +1 @@ +Subproject commit 4bc82774707695bf934682eb401ed44ab06d63d5