diff --git a/builtin/game/features.lua b/builtin/game/features.lua index d3f1716801fa..68005811ff62 100644 --- a/builtin/game/features.lua +++ b/builtin/game/features.lua @@ -36,6 +36,7 @@ core.features = { item_specific_pointabilities = true, blocking_pointability_type = true, dynamic_add_media_startup = true, + dynamic_add_media_filepath = true, } function core.has_feature(arg) diff --git a/doc/lua_api.md b/doc/lua_api.md index 1688193b0944..589967a68ee1 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -5306,6 +5306,8 @@ Utilities blocking_pointability_type = true, -- dynamic_add_media can be called at startup when leaving callback as `nil` (5.9.0) dynamic_add_media_startup = true, + -- dynamic_add_media supports `filename` and `filedata` parameters (5.9.0) + dynamic_add_media_filepath = true, } ``` @@ -6602,12 +6604,14 @@ Server * `minetest.dynamic_add_media(options, callback)` * `options`: table containing the following parameters * `filename`: name the media file will be usable as - (optional, default taken from path) - * `filepath`: path to the file on the filesystem + (optional if `filepath` present) + * `filepath`: path to the file on the filesystem [*] + * `filedata`: the data of the file to be sent [*] * `to_player`: name of the player the media should be sent to instead of all players (optional) * `ephemeral`: boolean that marks the media as ephemeral, it will not be cached on the client (optional, default false) + * Exactly one of the paramters marked [*] must be specified. * `callback`: function with arguments `name`, which is a player name * Pushes the specified media file to client(s). (details below) The file must be a supported image, sound or model format. diff --git a/games/devtest/mods/testnodes/textures.lua b/games/devtest/mods/testnodes/textures.lua index 82e8b2d30517..8c7f198c30d0 100644 --- a/games/devtest/mods/testnodes/textures.lua +++ b/games/devtest/mods/testnodes/textures.lua @@ -151,7 +151,7 @@ checker = nil do -- we used to write the textures to our mod folder. in order to avoid - -- duplicate errors delete them if they still exist. + -- duplication errors delete them if they still exist. local path = core.get_modpath(core.get_current_modname()) .. "/textures/" os.remove(path .. "testnodes_generated_mb.png") os.remove(path .. "testnodes_generated_ck.png") @@ -162,15 +162,15 @@ core.safe_file_write( textures_path .. "testnodes1.png", encode_and_check(512, 512, "rgb", data_mb) ) -core.safe_file_write( - textures_path .. "testnodes_generated_ck.png", - encode_and_check(512, 512, "gray", data_ck) -) +local png_ck = encode_and_check(512, 512, "gray", data_ck) core.dynamic_add_media({ filename = "testnodes_generated_mb.png", filepath = textures_path .. "testnodes1.png" }) -core.dynamic_add_media(textures_path .. "testnodes_generated_ck.png") +core.dynamic_add_media({ + filename = "testnodes_generated_ck.png", + filedata = png_ck, +}) minetest.register_node("testnodes:generated_png_mb", { description = S("Generated Mandelbrot PNG Test Node"), @@ -213,6 +213,8 @@ minetest.register_node("testnodes:generated_png_dst_emb", { groups = { dig_immediate = 2 }, }) +png_ck = nil +png_emb = nil data_emb = nil data_mb = nil data_ck = nil diff --git a/src/script/common/c_converter.cpp b/src/script/common/c_converter.cpp index f3a335d7a96c..a7b18365a929 100644 --- a/src/script/common/c_converter.cpp +++ b/src/script/common/c_converter.cpp @@ -483,6 +483,17 @@ bool check_field_or_nil(lua_State *L, int index, int type, const char *fieldname bool getstringfield(lua_State *L, int table, const char *fieldname, std::string &result) +{ + std::string_view sv; + if (getstringfield(L, table, fieldname, sv)) { + result = sv; + return true; + } + return false; +} + +bool getstringfield(lua_State *L, int table, + const char *fieldname, std::string_view &result) { lua_getfield(L, table, fieldname); bool got = false; @@ -491,7 +502,7 @@ bool getstringfield(lua_State *L, int table, size_t len = 0; const char *ptr = lua_tolstring(L, -1, &len); if (ptr) { - result.assign(ptr, len); + result = std::string_view(ptr, len); got = true; } } diff --git a/src/script/common/c_converter.h b/src/script/common/c_converter.h index 2235cb45a1cb..05d43953e8c8 100644 --- a/src/script/common/c_converter.h +++ b/src/script/common/c_converter.h @@ -27,7 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include -#include +#include #include "irrlichttypes_bloated.h" #include "common/c_types.h" @@ -67,11 +67,11 @@ v3s16 getv3s16field_default(lua_State *L, int table, bool getstringfield(lua_State *L, int table, const char *fieldname, std::string &result); +bool getstringfield(lua_State *L, int table, + const char *fieldname, std::string_view &result); size_t getstringlistfield(lua_State *L, int table, const char *fieldname, std::vector *result); -void read_groups(lua_State *L, int index, - std::unordered_map &result); bool getboolfield(lua_State *L, int table, const char *fieldname, bool &result); bool getfloatfield(lua_State *L, int table, diff --git a/src/script/lua_api/l_server.cpp b/src/script/lua_api/l_server.cpp index a2b0e859e368..860b05d49595 100644 --- a/src/script/lua_api/l_server.cpp +++ b/src/script/lua_api/l_server.cpp @@ -548,19 +548,22 @@ int ModApiServer::l_dynamic_add_media(lua_State *L) Server *server = getServer(L); const bool at_startup = !getEnv(L); - std::string filename, filepath, to_player; - bool ephemeral = false; + std::string tmp; + Server::DynamicMediaArgs args; if (lua_istable(L, 1)) { - getstringfield(L, 1, "filename", filename); - getstringfield(L, 1, "filepath", filepath); - getstringfield(L, 1, "to_player", to_player); - getboolfield(L, 1, "ephemeral", ephemeral); + getstringfield(L, 1, "filename", args.filename); + if (getstringfield(L, 1, "filepath", tmp)) + args.filepath = tmp; + args.data.emplace(); + if (!getstringfield(L, 1, "filedata", *args.data)) + args.data.reset(); + getstringfield(L, 1, "to_player", args.to_player); + getboolfield(L, 1, "ephemeral", args.ephemeral); } else { - filepath = readParam(L, 1); + tmp = readParam(L, 1); + args.filepath = tmp; } - if (filepath.empty()) - luaL_typerror(L, 1, "non-empty string"); if (at_startup) { if (!lua_isnoneornil(L, 2)) throw LuaError("must be called without callback at load-time"); @@ -572,13 +575,27 @@ int ModApiServer::l_dynamic_add_media(lua_State *L) luaL_checktype(L, 2, LUA_TFUNCTION); } - CHECK_SECURE_PATH(L, filepath.c_str(), false); + // validate + if (args.filepath) { + if (args.filepath->empty()) + throw LuaError("filepath must be non-empty"); + if (args.data) + throw LuaError("cannot provide both filepath and filedata"); + } else if (args.data) { + if (args.filename.empty()) + throw LuaError("filename required"); + } else { + throw LuaError("either filepath or filedata must be provided"); + } + + if (args.filepath) + CHECK_SECURE_PATH(L, args.filepath->c_str(), false); - u32 token = server->getScriptIface()->allocateDynamicMediaCallback(L, 2); + args.token = server->getScriptIface()->allocateDynamicMediaCallback(L, 2); - bool ok = server->dynamicAddMedia(filename, filepath, token, to_player, ephemeral); + bool ok = server->dynamicAddMedia(args); if (!ok) - server->getScriptIface()->freeDynamicMediaCallback(token); + server->getScriptIface()->freeDynamicMediaCallback(args.token); lua_pushboolean(L, ok); return 1; diff --git a/src/server.cpp b/src/server.cpp index 91c42a51f27e..7458956850b2 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -381,6 +381,13 @@ Server::~Server() if (m_mod_storage_database) m_mod_storage_database->endSave(); + // Clean up files + for (auto &it : m_media) { + if (it.second.delete_at_shutdown) { + fs::DeleteSingleFileOrEmptyDirectory(it.second.path); + } + } + // Delete things in the reverse order of creation delete m_emerge; delete m_env; @@ -3568,22 +3575,65 @@ void Server::deleteParticleSpawner(const std::string &playername, u32 id) SendDeleteParticleSpawner(peer_id, id); } -bool Server::dynamicAddMedia(std::string filename, std::string filepath, - const u32 token, const std::string &to_player, bool ephemeral) +namespace { + std::string writeToTempFile(std::string_view content) + { + auto filepath = fs::CreateTempFile(); + if (filepath.empty()) + return ""; + std::ofstream os(filepath, std::ios::binary); + if (!os.good()) + return ""; + os << content; + os.close(); + if (os.fail()) { + fs::DeleteSingleFileOrEmptyDirectory(filepath); + return ""; + } + return filepath; + } +} + +bool Server::dynamicAddMedia(const DynamicMediaArgs &a) { - if (filename.empty()) - filename = fs::GetFilenameFromPath(filepath.c_str()); + std::string filename = a.filename; + std::string filepath; + + // Deal with file -or- data, as provided + // (Note: caller must ensure validity, so sanity_check is okay) + if (a.filepath) { + sanity_check(!a.data); + filepath = *a.filepath; + if (filename.empty()) + filename = fs::GetFilenameFromPath(filepath.c_str()); + } else { + sanity_check(a.data); + sanity_check(!filename.empty()); + + // Write the file to disk. addMediaFile() will read it right back but this + // is the best way without turning the media loading code into spaghetti. + filepath = writeToTempFile(*a.data); + if (filepath.empty()) { + errorstream << "Server: failed writing media file \"" + << filename << "\" to disk" << std::endl; + return false; + } + verbosestream << "Server: \"" << filename << "\" temporarily written to " + << filepath << std::endl; + } + + // Do some checks auto it = m_media.find(filename); if (it != m_media.end()) { // Allow the same path to be "added" again in certain conditions - if (ephemeral || it->second.path != filepath) { + if (a.ephemeral || it->second.path != filepath) { errorstream << "Server::dynamicAddMedia(): file \"" << filename << "\" already exists in media cache" << std::endl; return false; } } - if (!m_env && (!to_player.empty() || ephemeral)) { + if (!m_env && (!a.to_player.empty() || a.ephemeral)) { errorstream << "Server::dynamicAddMedia(): " "adding ephemeral or player-specific media at startup is nonsense" << std::endl; @@ -3595,35 +3645,37 @@ bool Server::dynamicAddMedia(std::string filename, std::string filepath, bool ok = addMediaFile(filename, filepath, &filedata, &raw_hash); if (!ok) return false; - - if (ephemeral) { - // Create a copy of the file and swap out the path, this removes the - // requirement that mods keep the file accessible at the original path. - filepath = fs::CreateTempFile(); - bool ok = ([&] () -> bool { - if (filepath.empty()) + assert(!filedata.empty()); + + const auto &media_it = m_media.find(filename); + assert(media_it != m_media.end()); + + if (a.ephemeral) { + if (!a.data) { + // Create a copy of the file and swap out the path, this removes the + // requirement that mods keep the file accessible at the original path. + filepath = writeToTempFile(filedata); + if (filepath.empty()) { + errorstream << "Server: failed creating a copy of media file \"" + << filename << "\"" << std::endl; + m_media.erase(filename); return false; - std::ofstream os(filepath.c_str(), std::ios::binary); - if (!os.good()) - return false; - os << filedata; - os.close(); - return !os.fail(); - })(); - if (!ok) { - errorstream << "Server: failed to create a copy of media file " - << "\"" << filename << "\"" << std::endl; - m_media.erase(filename); - return false; + } + verbosestream << "Server: \"" << filename << "\" temporarily copied to " + << filepath << std::endl; + media_it->second.path = filepath; } - verbosestream << "Server: \"" << filename << "\" temporarily copied to " - << filepath << std::endl; - m_media[filename].path = filepath; - m_media[filename].no_announce = true; - // stepPendingDynMediaCallbacks will clean this up later. - } else if (!to_player.empty()) { - m_media[filename].no_announce = true; + media_it->second.no_announce = true; + // stepPendingDynMediaCallbacks will clean the file up later + } else if (a.data) { + // data is in a temporary file but not ephemeral, so the cleanup point + // is different. + media_it->second.delete_at_shutdown = true; + } + if (!a.to_player.empty()) { + // only sent to one player (who must be online), so shouldn't announce. + media_it->second.no_announce = true; } std::unordered_set delivered, waiting; @@ -3631,18 +3683,18 @@ bool Server::dynamicAddMedia(std::string filename, std::string filepath, // Push file to existing clients if (m_env) { NetworkPacket pkt(TOCLIENT_MEDIA_PUSH, 0); - pkt << raw_hash << filename << static_cast(ephemeral); + pkt << raw_hash << filename << static_cast(a.ephemeral); NetworkPacket legacy_pkt = pkt; // Newer clients get asked to fetch the file (asynchronous) - pkt << token; + pkt << a.token; // Older clients have an awful hack that just throws the data at them legacy_pkt.putLongString(filedata); ClientInterface::AutoLock clientlock(m_clients); for (auto &pair : m_clients.getClientList()) { - if (pair.second->getState() == CS_DefinitionsSent && !ephemeral) { + if (pair.second->getState() == CS_DefinitionsSent && !a.ephemeral) { /* If a client is in the DefinitionsSent state it is too late to transfer the file via sendMediaAnnouncement() but at the same @@ -3663,7 +3715,7 @@ bool Server::dynamicAddMedia(std::string filename, std::string filepath, continue; const session_t peer_id = pair.second->peer_id; - if (!to_player.empty() && getPlayerName(peer_id) != to_player) + if (!a.to_player.empty() && getPlayerName(peer_id) != a.to_player) continue; if (proto_ver < 40) { @@ -3686,15 +3738,15 @@ bool Server::dynamicAddMedia(std::string filename, std::string filepath, // Run callback for players that already had the file delivered (legacy-only) for (session_t peer_id : delivered) { if (auto player = m_env->getPlayer(peer_id)) - getScriptIface()->on_dynamic_media_added(token, player->getName()); + getScriptIface()->on_dynamic_media_added(a.token, player->getName()); } // Save all others in our pending state - auto &state = m_pending_dyn_media[token]; + auto &state = m_pending_dyn_media[a.token]; state.waiting_players = std::move(waiting); // regardless of success throw away the callback after a while state.expiry_timer = 60.0f; - if (ephemeral) + if (a.ephemeral) state.filename = filename; return true; diff --git a/src/server.h b/src/server.h index 988c5937ed8d..cc7bdc2f7cc7 100644 --- a/src/server.h +++ b/src/server.h @@ -44,6 +44,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include +#include class ChatEvent; struct ChatEventChat; @@ -87,13 +89,17 @@ struct MediaInfo { std::string path; std::string sha1_digest; // base64-encoded - bool no_announce; // true: not announced in TOCLIENT_ANNOUNCE_MEDIA (at player join) + // true = not announced in TOCLIENT_ANNOUNCE_MEDIA (at player join) + bool no_announce; + // does what it says. used by some cases of dynamic media. + bool delete_at_shutdown; MediaInfo(const std::string &path_="", const std::string &sha1_digest_=""): path(path_), sha1_digest(sha1_digest_), - no_announce(false) + no_announce(false), + delete_at_shutdown(false) { } }; @@ -258,8 +264,15 @@ class Server : public con::PeerHandler, public MapEventReceiver, void deleteParticleSpawner(const std::string &playername, u32 id); - bool dynamicAddMedia(std::string filename, std::string filepath, u32 token, - const std::string &to_player, bool ephemeral); + struct DynamicMediaArgs { + std::string filename; + std::optional filepath; + std::optional data; + u32 token; + std::string to_player; + bool ephemeral = false; + }; + bool dynamicAddMedia(const DynamicMediaArgs &args); ServerInventoryManager *getInventoryMgr() const { return m_inventory_mgr.get(); } void sendDetachedInventory(Inventory *inventory, const std::string &name, session_t peer_id);