Enable dynamic_add_media to take the file data instead of a path

This commit is contained in:
sfan5 2024-01-23 21:15:09 +01:00
parent c90ebad46b
commit d4b107e2e8
8 changed files with 167 additions and 67 deletions

@ -36,6 +36,7 @@ core.features = {
item_specific_pointabilities = true, item_specific_pointabilities = true,
blocking_pointability_type = true, blocking_pointability_type = true,
dynamic_add_media_startup = true, dynamic_add_media_startup = true,
dynamic_add_media_filepath = true,
} }
function core.has_feature(arg) function core.has_feature(arg)

@ -5306,6 +5306,8 @@ Utilities
blocking_pointability_type = true, blocking_pointability_type = true,
-- dynamic_add_media can be called at startup when leaving callback as `nil` (5.9.0) -- dynamic_add_media can be called at startup when leaving callback as `nil` (5.9.0)
dynamic_add_media_startup = true, 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)` * `minetest.dynamic_add_media(options, callback)`
* `options`: table containing the following parameters * `options`: table containing the following parameters
* `filename`: name the media file will be usable as * `filename`: name the media file will be usable as
(optional, default taken from path) (optional if `filepath` present)
* `filepath`: path to the file on the filesystem * `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 * `to_player`: name of the player the media should be sent to instead of
all players (optional) all players (optional)
* `ephemeral`: boolean that marks the media as ephemeral, * `ephemeral`: boolean that marks the media as ephemeral,
it will not be cached on the client (optional, default false) 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 * `callback`: function with arguments `name`, which is a player name
* Pushes the specified media file to client(s). (details below) * Pushes the specified media file to client(s). (details below)
The file must be a supported image, sound or model format. The file must be a supported image, sound or model format.

@ -151,7 +151,7 @@ checker = nil
do do
-- we used to write the textures to our mod folder. in order to avoid -- 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/" local path = core.get_modpath(core.get_current_modname()) .. "/textures/"
os.remove(path .. "testnodes_generated_mb.png") os.remove(path .. "testnodes_generated_mb.png")
os.remove(path .. "testnodes_generated_ck.png") os.remove(path .. "testnodes_generated_ck.png")
@ -162,15 +162,15 @@ core.safe_file_write(
textures_path .. "testnodes1.png", textures_path .. "testnodes1.png",
encode_and_check(512, 512, "rgb", data_mb) encode_and_check(512, 512, "rgb", data_mb)
) )
core.safe_file_write( local png_ck = encode_and_check(512, 512, "gray", data_ck)
textures_path .. "testnodes_generated_ck.png",
encode_and_check(512, 512, "gray", data_ck)
)
core.dynamic_add_media({ core.dynamic_add_media({
filename = "testnodes_generated_mb.png", filename = "testnodes_generated_mb.png",
filepath = textures_path .. "testnodes1.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", { minetest.register_node("testnodes:generated_png_mb", {
description = S("Generated Mandelbrot PNG Test Node"), description = S("Generated Mandelbrot PNG Test Node"),
@ -213,6 +213,8 @@ minetest.register_node("testnodes:generated_png_dst_emb", {
groups = { dig_immediate = 2 }, groups = { dig_immediate = 2 },
}) })
png_ck = nil
png_emb = nil
data_emb = nil data_emb = nil
data_mb = nil data_mb = nil
data_ck = nil data_ck = nil

@ -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, bool getstringfield(lua_State *L, int table,
const char *fieldname, std::string &result) 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); lua_getfield(L, table, fieldname);
bool got = false; bool got = false;
@ -491,7 +502,7 @@ bool getstringfield(lua_State *L, int table,
size_t len = 0; size_t len = 0;
const char *ptr = lua_tolstring(L, -1, &len); const char *ptr = lua_tolstring(L, -1, &len);
if (ptr) { if (ptr) {
result.assign(ptr, len); result = std::string_view(ptr, len);
got = true; got = true;
} }
} }

@ -27,7 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#pragma once #pragma once
#include <vector> #include <vector>
#include <unordered_map> #include <string_view>
#include "irrlichttypes_bloated.h" #include "irrlichttypes_bloated.h"
#include "common/c_types.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, bool getstringfield(lua_State *L, int table,
const char *fieldname, std::string &result); 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, size_t getstringlistfield(lua_State *L, int table,
const char *fieldname, const char *fieldname,
std::vector<std::string> *result); std::vector<std::string> *result);
void read_groups(lua_State *L, int index,
std::unordered_map<std::string, int> &result);
bool getboolfield(lua_State *L, int table, bool getboolfield(lua_State *L, int table,
const char *fieldname, bool &result); const char *fieldname, bool &result);
bool getfloatfield(lua_State *L, int table, bool getfloatfield(lua_State *L, int table,

@ -548,19 +548,22 @@ int ModApiServer::l_dynamic_add_media(lua_State *L)
Server *server = getServer(L); Server *server = getServer(L);
const bool at_startup = !getEnv(L); const bool at_startup = !getEnv(L);
std::string filename, filepath, to_player; std::string tmp;
bool ephemeral = false; Server::DynamicMediaArgs args;
if (lua_istable(L, 1)) { if (lua_istable(L, 1)) {
getstringfield(L, 1, "filename", filename); getstringfield(L, 1, "filename", args.filename);
getstringfield(L, 1, "filepath", filepath); if (getstringfield(L, 1, "filepath", tmp))
getstringfield(L, 1, "to_player", to_player); args.filepath = tmp;
getboolfield(L, 1, "ephemeral", ephemeral); 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 { } else {
filepath = readParam<std::string>(L, 1); tmp = readParam<std::string>(L, 1);
args.filepath = tmp;
} }
if (filepath.empty())
luaL_typerror(L, 1, "non-empty string");
if (at_startup) { if (at_startup) {
if (!lua_isnoneornil(L, 2)) if (!lua_isnoneornil(L, 2))
throw LuaError("must be called without callback at load-time"); 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); 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");
}
u32 token = server->getScriptIface()->allocateDynamicMediaCallback(L, 2); if (args.filepath)
CHECK_SECURE_PATH(L, args.filepath->c_str(), false);
bool ok = server->dynamicAddMedia(filename, filepath, token, to_player, ephemeral); args.token = server->getScriptIface()->allocateDynamicMediaCallback(L, 2);
bool ok = server->dynamicAddMedia(args);
if (!ok) if (!ok)
server->getScriptIface()->freeDynamicMediaCallback(token); server->getScriptIface()->freeDynamicMediaCallback(args.token);
lua_pushboolean(L, ok); lua_pushboolean(L, ok);
return 1; return 1;

@ -381,6 +381,13 @@ Server::~Server()
if (m_mod_storage_database) if (m_mod_storage_database)
m_mod_storage_database->endSave(); 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 things in the reverse order of creation
delete m_emerge; delete m_emerge;
delete m_env; delete m_env;
@ -3568,22 +3575,65 @@ void Server::deleteParticleSpawner(const std::string &playername, u32 id)
SendDeleteParticleSpawner(peer_id, id); SendDeleteParticleSpawner(peer_id, id);
} }
bool Server::dynamicAddMedia(std::string filename, std::string filepath, namespace {
const u32 token, const std::string &to_player, bool ephemeral) 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()) std::string filename = a.filename;
filename = fs::GetFilenameFromPath(filepath.c_str()); 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); auto it = m_media.find(filename);
if (it != m_media.end()) { if (it != m_media.end()) {
// Allow the same path to be "added" again in certain conditions // 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 errorstream << "Server::dynamicAddMedia(): file \"" << filename
<< "\" already exists in media cache" << std::endl; << "\" already exists in media cache" << std::endl;
return false; return false;
} }
} }
if (!m_env && (!to_player.empty() || ephemeral)) { if (!m_env && (!a.to_player.empty() || a.ephemeral)) {
errorstream << "Server::dynamicAddMedia(): " errorstream << "Server::dynamicAddMedia(): "
"adding ephemeral or player-specific media at startup is nonsense" "adding ephemeral or player-specific media at startup is nonsense"
<< std::endl; << std::endl;
@ -3595,35 +3645,37 @@ bool Server::dynamicAddMedia(std::string filename, std::string filepath,
bool ok = addMediaFile(filename, filepath, &filedata, &raw_hash); bool ok = addMediaFile(filename, filepath, &filedata, &raw_hash);
if (!ok) if (!ok)
return false; return false;
assert(!filedata.empty());
if (ephemeral) { const auto &media_it = m_media.find(filename);
// Create a copy of the file and swap out the path, this removes the assert(media_it != m_media.end());
// requirement that mods keep the file accessible at the original path.
filepath = fs::CreateTempFile(); if (a.ephemeral) {
bool ok = ([&] () -> bool { if (!a.data) {
if (filepath.empty()) // 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; return false;
std::ofstream os(filepath.c_str(), std::ios::binary); }
if (!os.good()) verbosestream << "Server: \"" << filename << "\" temporarily copied to "
return false; << filepath << std::endl;
os << filedata; media_it->second.path = filepath;
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;
m_media[filename].path = filepath; media_it->second.no_announce = true;
m_media[filename].no_announce = true; // stepPendingDynMediaCallbacks will clean the file up later
// stepPendingDynMediaCallbacks will clean this up later. } else if (a.data) {
} else if (!to_player.empty()) { // data is in a temporary file but not ephemeral, so the cleanup point
m_media[filename].no_announce = true; // 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<session_t> delivered, waiting; std::unordered_set<session_t> delivered, waiting;
@ -3631,18 +3683,18 @@ bool Server::dynamicAddMedia(std::string filename, std::string filepath,
// Push file to existing clients // Push file to existing clients
if (m_env) { if (m_env) {
NetworkPacket pkt(TOCLIENT_MEDIA_PUSH, 0); NetworkPacket pkt(TOCLIENT_MEDIA_PUSH, 0);
pkt << raw_hash << filename << static_cast<bool>(ephemeral); pkt << raw_hash << filename << static_cast<bool>(a.ephemeral);
NetworkPacket legacy_pkt = pkt; NetworkPacket legacy_pkt = pkt;
// Newer clients get asked to fetch the file (asynchronous) // 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 // Older clients have an awful hack that just throws the data at them
legacy_pkt.putLongString(filedata); legacy_pkt.putLongString(filedata);
ClientInterface::AutoLock clientlock(m_clients); ClientInterface::AutoLock clientlock(m_clients);
for (auto &pair : m_clients.getClientList()) { 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 If a client is in the DefinitionsSent state it is too late to
transfer the file via sendMediaAnnouncement() but at the same transfer the file via sendMediaAnnouncement() but at the same
@ -3663,7 +3715,7 @@ bool Server::dynamicAddMedia(std::string filename, std::string filepath,
continue; continue;
const session_t peer_id = pair.second->peer_id; 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; continue;
if (proto_ver < 40) { 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) // Run callback for players that already had the file delivered (legacy-only)
for (session_t peer_id : delivered) { for (session_t peer_id : delivered) {
if (auto player = m_env->getPlayer(peer_id)) 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 // 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); state.waiting_players = std::move(waiting);
// regardless of success throw away the callback after a while // regardless of success throw away the callback after a while
state.expiry_timer = 60.0f; state.expiry_timer = 60.0f;
if (ephemeral) if (a.ephemeral)
state.filename = filename; state.filename = filename;
return true; return true;

@ -44,6 +44,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <map> #include <map>
#include <vector> #include <vector>
#include <unordered_set> #include <unordered_set>
#include <optional>
#include <string_view>
class ChatEvent; class ChatEvent;
struct ChatEventChat; struct ChatEventChat;
@ -87,13 +89,17 @@ struct MediaInfo
{ {
std::string path; std::string path;
std::string sha1_digest; // base64-encoded 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_="", MediaInfo(const std::string &path_="",
const std::string &sha1_digest_=""): const std::string &sha1_digest_=""):
path(path_), path(path_),
sha1_digest(sha1_digest_), sha1_digest(sha1_digest_),
no_announce(false) no_announce(false),
delete_at_shutdown(false)
{ {
} }
}; };
@ -258,8 +264,15 @@ public:
void deleteParticleSpawner(const std::string &playername, u32 id); void deleteParticleSpawner(const std::string &playername, u32 id);
bool dynamicAddMedia(std::string filename, std::string filepath, u32 token, struct DynamicMediaArgs {
const std::string &to_player, bool ephemeral); std::string filename;
std::optional<std::string> filepath;
std::optional<std::string_view> data;
u32 token;
std::string to_player;
bool ephemeral = false;
};
bool dynamicAddMedia(const DynamicMediaArgs &args);
ServerInventoryManager *getInventoryMgr() const { return m_inventory_mgr.get(); } ServerInventoryManager *getInventoryMgr() const { return m_inventory_mgr.get(); }
void sendDetachedInventory(Inventory *inventory, const std::string &name, session_t peer_id); void sendDetachedInventory(Inventory *inventory, const std::string &name, session_t peer_id);