Server pushing media at runtime (#9961)
authorsfan5 <sfan5@live.de>
Sat, 13 Jun 2020 17:03:26 +0000 (19:03 +0200)
committerGitHub <noreply@github.com>
Sat, 13 Jun 2020 17:03:26 +0000 (19:03 +0200)
16 files changed:
doc/lua_api.txt
src/client/client.cpp
src/client/client.h
src/client/clientmedia.cpp
src/client/clientmedia.h
src/client/filecache.cpp
src/client/filecache.h
src/filesys.cpp
src/network/clientopcodes.cpp
src/network/clientpackethandler.cpp
src/network/networkprotocol.h
src/network/serveropcodes.cpp
src/script/lua_api/l_server.cpp
src/script/lua_api/l_server.h
src/server.cpp
src/server.h

index ed060c4ad9dde4c89ff4f79e2c16ef485c234b7e..cb968958ff9d2209c507561680061e57036540f9 100644 (file)
@@ -5217,6 +5217,20 @@ Server
     * Returns a code (0: successful, 1: no such player, 2: player is connected)
 * `minetest.remove_player_auth(name)`: remove player authentication data
     * Returns boolean indicating success (false if player nonexistant)
+* `minetest.dynamic_add_media(filepath)`
+    * Adds the file at the given path to the media sent to clients by the server
+      on startup and also pushes this file to already connected clients.
+      The file must be a supported image, sound or model format. It must not be
+      modified, deleted, moved or renamed after calling this function.
+      The list of dynamically added media is not persisted.
+    * Returns boolean indicating success (duplicate files count as error)
+    * The media will be ready to use (in e.g. entity textures, sound_play)
+      immediately after calling this function.
+      Old clients that lack support for this feature will not see the media
+      unless they reconnect to the server.
+    * Since media transferred this way does not use client caching or HTTP
+      transfers, dynamic media should not be used with big files or performance
+      will suffer.
 
 Bans
 ----
index c03c062c6635a95803c721c48a4cd214c8b2c06e..34f97a9de356fb7abbd9b5b77aa067461e9deca1 100644 (file)
@@ -670,11 +670,9 @@ void Client::step(float dtime)
        }
 }
 
-bool Client::loadMedia(const std::string &data, const std::string &filename)
+bool Client::loadMedia(const std::string &data, const std::string &filename,
+       bool from_media_push)
 {
-       // Silly irrlicht's const-incorrectness
-       Buffer<char> data_rw(data.c_str(), data.size());
-
        std::string name;
 
        const char *image_ext[] = {
@@ -690,6 +688,9 @@ bool Client::loadMedia(const std::string &data, const std::string &filename)
                io::IFileSystem *irrfs = RenderingEngine::get_filesystem();
                video::IVideoDriver *vdrv = RenderingEngine::get_video_driver();
 
+               // Silly irrlicht's const-incorrectness
+               Buffer<char> data_rw(data.c_str(), data.size());
+
                // Create an irrlicht memory file
                io::IReadFile *rfile = irrfs->createMemoryReadFile(
                                *data_rw, data_rw.getSize(), "_tempreadfile");
@@ -727,7 +728,6 @@ bool Client::loadMedia(const std::string &data, const std::string &filename)
                ".x", ".b3d", ".md2", ".obj",
                NULL
        };
-
        name = removeStringEnd(filename, model_ext);
        if (!name.empty()) {
                verbosestream<<"Client: Storing model into memory: "
@@ -744,6 +744,8 @@ bool Client::loadMedia(const std::string &data, const std::string &filename)
        };
        name = removeStringEnd(filename, translate_ext);
        if (!name.empty()) {
+               if (from_media_push)
+                       return false;
                TRACESTREAM(<< "Client: Loading translation: "
                                << "\"" << filename << "\"" << std::endl);
                g_client_translations->loadTranslation(data);
index 3b1095ac29af48709c4a5414ae8c1aaa63630e17..733634db1e280b6bdff149c0bfe2592f7f83c74e 100644 (file)
@@ -222,6 +222,7 @@ public:
        void handleCommand_FormspecPrepend(NetworkPacket *pkt);
        void handleCommand_CSMRestrictionFlags(NetworkPacket *pkt);
        void handleCommand_PlayerSpeed(NetworkPacket *pkt);
+       void handleCommand_MediaPush(NetworkPacket *pkt);
 
        void ProcessData(NetworkPacket *pkt);
 
@@ -376,7 +377,8 @@ public:
 
        // The following set of functions is used by ClientMediaDownloader
        // Insert a media file appropriately into the appropriate manager
-       bool loadMedia(const std::string &data, const std::string &filename);
+       bool loadMedia(const std::string &data, const std::string &filename,
+               bool from_media_push = false);
        // Send a request for conventional media transfer
        void request_media(const std::vector<std::string> &file_requests);
 
@@ -488,6 +490,7 @@ private:
        Camera *m_camera = nullptr;
        Minimap *m_minimap = nullptr;
        bool m_minimap_disabled_by_server = false;
+
        // Server serialization version
        u8 m_server_ser_ver;
 
@@ -529,7 +532,6 @@ private:
        AuthMechanism m_chosen_auth_mech;
        void *m_auth_data = nullptr;
 
-
        bool m_access_denied = false;
        bool m_access_denied_reconnect = false;
        std::string m_access_denied_reason = "";
@@ -538,7 +540,10 @@ private:
        bool m_nodedef_received = false;
        bool m_activeobjects_received = false;
        bool m_mods_loaded = false;
+
        ClientMediaDownloader *m_media_downloader;
+       // Set of media filenames pushed by server at runtime
+       std::unordered_set<std::string> m_media_pushed_files;
 
        // time_of_day speed approximation for old protocol
        bool m_time_of_day_set = false;
index 6da99bbbf8120c94cb041a0b5d6b4a533e445d80..8cd3b6bcc8b2d8da314e2412834e1e700ea03bb2 100644 (file)
@@ -35,6 +35,15 @@ static std::string getMediaCacheDir()
        return porting::path_cache + DIR_DELIM + "media";
 }
 
+bool clientMediaUpdateCache(const std::string &raw_hash, const std::string &filedata)
+{
+       FileCache media_cache(getMediaCacheDir());
+       std::string sha1_hex = hex_encode(raw_hash);
+       if (!media_cache.exists(sha1_hex))
+               return media_cache.update(sha1_hex, filedata);
+       return true;
+}
+
 /*
        ClientMediaDownloader
 */
@@ -559,7 +568,6 @@ bool ClientMediaDownloader::checkAndLoad(
        return true;
 }
 
-
 /*
        Minetest Hashset File Format
 
index 92831082c3fa1d011df27de7ccc0f6752030441b..5a918535b9339500c08e18106eb26a79546cc8bd 100644 (file)
@@ -33,6 +33,11 @@ struct HTTPFetchResult;
 #define MTHASHSET_FILE_SIGNATURE 0x4d544853 // 'MTHS'
 #define MTHASHSET_FILE_NAME "index.mth"
 
+// Store file into media cache (unless it exists already)
+// Validating the hash is responsibility of the caller
+bool clientMediaUpdateCache(const std::string &raw_hash,
+       const std::string &filedata);
+
 class ClientMediaDownloader
 {
 public:
index 3d1b302a8cc59e8a5c4cbe9eef2061fac8ba18f2..46bbe40595edab8107e9030e8f16853554e204bc 100644 (file)
@@ -82,8 +82,16 @@ bool FileCache::update(const std::string &name, const std::string &data)
        std::string path = m_dir + DIR_DELIM + name;
        return updateByPath(path, data);
 }
+
 bool FileCache::load(const std::string &name, std::ostream &os)
 {
        std::string path = m_dir + DIR_DELIM + name;
        return loadByPath(path, os);
 }
+
+bool FileCache::exists(const std::string &name)
+{
+       std::string path = m_dir + DIR_DELIM + name;
+       std::ifstream fis(path.c_str(), std::ios_base::binary);
+       return fis.good();
+}
index 96e4c8ba1b6c14a6f3d0ae8dad1304390755d01b..ea6afc4b2a9aeb5215c83b6c3d59624539cc2c3b 100644 (file)
@@ -33,6 +33,7 @@ public:
 
        bool update(const std::string &name, const std::string &data);
        bool load(const std::string &name, std::ostream &os);
+       bool exists(const std::string &name);
 
 private:
        std::string m_dir;
index f61b39b9465b191badf70befdb27da41e27b13f5..0bc351669d0e744f5255648c0dd0e37ec94fcb31 100644 (file)
@@ -691,6 +691,12 @@ std::string AbsolutePath(const std::string &path)
 const char *GetFilenameFromPath(const char *path)
 {
        const char *filename = strrchr(path, DIR_DELIM_CHAR);
+       // Consistent with IsDirDelimiter this function handles '/' too
+       if (DIR_DELIM_CHAR != '/') {
+               const char *tmp = strrchr(path, '/');
+               if (tmp && tmp > filename)
+                       filename = tmp;
+       }
        return filename ? filename + 1 : path;
 }
 
index 0f20047c048856d67f111e734c1bf04b9c6d9cd0..f812a08a103218556df25cb18788a09a083813e7 100644 (file)
@@ -68,7 +68,7 @@ const ToClientCommandHandler toClientCommandTable[TOCLIENT_NUM_MSG_TYPES] =
        { "TOCLIENT_TIME_OF_DAY",              TOCLIENT_STATE_CONNECTED, &Client::handleCommand_TimeOfDay }, // 0x29
        { "TOCLIENT_CSM_RESTRICTION_FLAGS",    TOCLIENT_STATE_CONNECTED, &Client::handleCommand_CSMRestrictionFlags }, // 0x2A
        { "TOCLIENT_PLAYER_SPEED",             TOCLIENT_STATE_CONNECTED, &Client::handleCommand_PlayerSpeed }, // 0x2B
-       null_command_handler,
+       { "TOCLIENT_MEDIA_PUSH",               TOCLIENT_STATE_CONNECTED, &Client::handleCommand_MediaPush }, // 0x2C
        null_command_handler,
        null_command_handler,
        { "TOCLIENT_CHAT_MESSAGE",             TOCLIENT_STATE_CONNECTED, &Client::handleCommand_ChatMessage }, // 0x2F
index e000acc928d767a56d6c27386a5bf522a14e30a2..5934eaf8cf91ae446cefe41042a029031d2819bf 100644 (file)
@@ -39,6 +39,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "script/scripting_client.h"
 #include "util/serialize.h"
 #include "util/srp.h"
+#include "util/sha1.h"
 #include "tileanimation.h"
 #include "gettext.h"
 #include "skyparams.h"
@@ -1471,6 +1472,51 @@ void Client::handleCommand_PlayerSpeed(NetworkPacket *pkt)
        player->addVelocity(added_vel);
 }
 
+void Client::handleCommand_MediaPush(NetworkPacket *pkt)
+{
+       std::string raw_hash, filename, filedata;
+       bool cached;
+
+       *pkt >> raw_hash >> filename >> cached;
+       filedata = pkt->readLongString();
+
+       if (raw_hash.size() != 20 || filedata.empty() || filename.empty() ||
+                       !string_allowed(filename, TEXTURENAME_ALLOWED_CHARS)) {
+               throw PacketError("Illegal filename, data or hash");
+       }
+
+       verbosestream << "Server pushes media file \"" << filename << "\" with "
+               << filedata.size() << " bytes of data (cached=" << cached
+               << ")" << std::endl;
+
+       if (m_media_pushed_files.count(filename) != 0) {
+               // Silently ignore for synchronization purposes
+               return;
+       }
+
+       // Compute and check checksum of data
+       std::string computed_hash;
+       {
+               SHA1 ctx;
+               ctx.addBytes(filedata.c_str(), filedata.size());
+               unsigned char *buf = ctx.getDigest();
+               computed_hash.assign((char*) buf, 20);
+               free(buf);
+       }
+       if (raw_hash != computed_hash) {
+               verbosestream << "Hash of file data mismatches, ignoring." << std::endl;
+               return;
+       }
+
+       // Actually load media
+       loadMedia(filedata, filename, true);
+       m_media_pushed_files.insert(filename);
+
+       // Cache file for the next time when this client joins the same server
+       if (cached)
+               clientMediaUpdateCache(raw_hash, filedata);
+}
+
 /*
  * Mod channels
  */
index ab924f1dbd7dedb2fd512db94f7369b1f991529a..fd683eac9f97bdfb5b9c1477ee7988268b331aad 100644 (file)
@@ -323,6 +323,15 @@ enum ToClientCommand
                v3f added_vel
         */
 
+       TOCLIENT_MEDIA_PUSH = 0x2C,
+       /*
+               std::string raw_hash
+               std::string filename
+               bool should_be_cached
+               u32 len
+               char filedata[len]
+       */
+
        // (oops, there is some gap here)
 
        TOCLIENT_CHAT_MESSAGE = 0x2F,
index 6ee4ff25685e6583001840fd94daa8c327a881d2..2fc3197c2fc818ba1777c04b44bb099c65910b10 100644 (file)
@@ -167,7 +167,7 @@ const ClientCommandFactory clientCommandFactoryTable[TOCLIENT_NUM_MSG_TYPES] =
        { "TOCLIENT_TIME_OF_DAY",              0, true }, // 0x29
        { "TOCLIENT_CSM_RESTRICTION_FLAGS",    0, true }, // 0x2A
        { "TOCLIENT_PLAYER_SPEED",             0, true }, // 0x2B
-       null_command_factory, // 0x2C
+       { "TOCLIENT_MEDIA_PUSH",               0, true }, // 0x2C (sent over channel 1 too)
        null_command_factory, // 0x2D
        null_command_factory, // 0x2E
        { "TOCLIENT_CHAT_MESSAGE",             0, true }, // 0x2F
index b6754938e8ac92e7778c48098c61e7e8bfa48150..6f934bb9d0b50c8e7c9d6b655b5b4c6e9f255263 100644 (file)
@@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "common/c_converter.h"
 #include "common/c_content.h"
 #include "cpp_api/s_base.h"
+#include "cpp_api/s_security.h"
 #include "server.h"
 #include "environment.h"
 #include "remoteplayer.h"
@@ -412,9 +413,6 @@ int ModApiServer::l_get_modnames(lua_State *L)
        std::vector<std::string> modlist;
        getServer(L)->getModNames(modlist);
 
-       // Take unsorted items from mods_unsorted and sort them into
-       // mods_sorted; not great performance but the number of mods on a
-       // server will likely be small.
        std::sort(modlist.begin(), modlist.end());
 
        // Package them up for Lua
@@ -474,6 +472,23 @@ int ModApiServer::l_sound_fade(lua_State *L)
        return 0;
 }
 
+// dynamic_add_media(filepath)
+int ModApiServer::l_dynamic_add_media(lua_State *L)
+{
+       NO_MAP_LOCK_REQUIRED;
+
+       // Reject adding media before the server has started up
+       if (!getEnv(L))
+               throw LuaError("Dynamic media cannot be added before server has started up");
+
+       std::string filepath = readParam<std::string>(L, 1);
+       CHECK_SECURE_PATH(L, filepath.c_str(), false);
+
+       bool ok = getServer(L)->dynamicAddMedia(filepath);
+       lua_pushboolean(L, ok);
+       return 1;
+}
+
 // is_singleplayer()
 int ModApiServer::l_is_singleplayer(lua_State *L)
 {
@@ -538,6 +553,7 @@ void ModApiServer::Initialize(lua_State *L, int top)
        API_FCT(sound_play);
        API_FCT(sound_stop);
        API_FCT(sound_fade);
+       API_FCT(dynamic_add_media);
 
        API_FCT(get_player_information);
        API_FCT(get_player_privs);
index 3aa1785a248295bed8c81dc544f57c8a21701caa..938bfa8ef24fc68426285b341a5a494925ba7b33 100644 (file)
@@ -70,6 +70,9 @@ private:
        // sound_fade(handle, step, gain)
        static int l_sound_fade(lua_State *L);
 
+       // dynamic_add_media(filepath)
+       static int l_dynamic_add_media(lua_State *L);
+
        // get_player_privs(name, text)
        static int l_get_player_privs(lua_State *L);
 
index 6ecbd70973d58b5163b0336f895584bd356343de..fe2bb3840280d24e3194e001518cf123e0f0a276 100644 (file)
@@ -2405,9 +2405,87 @@ bool Server::SendBlock(session_t peer_id, const v3s16 &blockpos)
        return true;
 }
 
+bool Server::addMediaFile(const std::string &filename,
+       const std::string &filepath, std::string *filedata_to,
+       std::string *digest_to)
+{
+       // If name contains illegal characters, ignore the file
+       if (!string_allowed(filename, TEXTURENAME_ALLOWED_CHARS)) {
+               infostream << "Server: ignoring illegal file name: \""
+                               << filename << "\"" << std::endl;
+               return false;
+       }
+       // If name is not in a supported format, ignore it
+       const char *supported_ext[] = {
+               ".png", ".jpg", ".bmp", ".tga",
+               ".pcx", ".ppm", ".psd", ".wal", ".rgb",
+               ".ogg",
+               ".x", ".b3d", ".md2", ".obj",
+               // Custom translation file format
+               ".tr",
+               NULL
+       };
+       if (removeStringEnd(filename, supported_ext).empty()) {
+               infostream << "Server: ignoring unsupported file extension: \""
+                               << filename << "\"" << std::endl;
+               return false;
+       }
+       // Ok, attempt to load the file and add to cache
+
+       // Read data
+       std::ifstream fis(filepath.c_str(), std::ios_base::binary);
+       if (!fis.good()) {
+               errorstream << "Server::addMediaFile(): Could not open \""
+                               << filename << "\" for reading" << std::endl;
+               return false;
+       }
+       std::string filedata;
+       bool bad = false;
+       for (;;) {
+               char buf[1024];
+               fis.read(buf, sizeof(buf));
+               std::streamsize len = fis.gcount();
+               filedata.append(buf, len);
+               if (fis.eof())
+                       break;
+               if (!fis.good()) {
+                       bad = true;
+                       break;
+               }
+       }
+       if (bad) {
+               errorstream << "Server::addMediaFile(): Failed to read \""
+                               << filename << "\"" << std::endl;
+               return false;
+       } else if (filedata.empty()) {
+               errorstream << "Server::addMediaFile(): Empty file \""
+                               << filepath << "\"" << std::endl;
+               return false;
+       }
+
+       SHA1 sha1;
+       sha1.addBytes(filedata.c_str(), filedata.length());
+
+       unsigned char *digest = sha1.getDigest();
+       std::string sha1_base64 = base64_encode(digest, 20);
+       std::string sha1_hex = hex_encode((char*) digest, 20);
+       if (digest_to)
+               *digest_to = std::string((char*) digest, 20);
+       free(digest);
+
+       // Put in list
+       m_media[filename] = MediaInfo(filepath, sha1_base64);
+       verbosestream << "Server: " << sha1_hex << " is " << filename
+                       << std::endl;
+
+       if (filedata_to)
+               *filedata_to = std::move(filedata);
+       return true;
+}
+
 void Server::fillMediaCache()
 {
-       infostream<<"Server: Calculating media file checksums"<<std::endl;
+       infostream << "Server: Calculating media file checksums" << std::endl;
 
        // Collect all media file paths
        std::vector<std::string> paths;
@@ -2419,80 +2497,15 @@ void Server::fillMediaCache()
        for (const std::string &mediapath : paths) {
                std::vector<fs::DirListNode> dirlist = fs::GetDirListing(mediapath);
                for (const fs::DirListNode &dln : dirlist) {
-                       if (dln.dir) // Ignode dirs
-                               continue;
-                       std::string filename = dln.name;
-                       // If name contains illegal characters, ignore the file
-                       if (!string_allowed(filename, TEXTURENAME_ALLOWED_CHARS)) {
-                               infostream<<"Server: ignoring illegal file name: \""
-                                               << filename << "\"" << std::endl;
-                               continue;
-                       }
-                       // If name is not in a supported format, ignore it
-                       const char *supported_ext[] = {
-                               ".png", ".jpg", ".bmp", ".tga",
-                               ".pcx", ".ppm", ".psd", ".wal", ".rgb",
-                               ".ogg",
-                               ".x", ".b3d", ".md2", ".obj",
-                               // Custom translation file format
-                               ".tr",
-                               NULL
-                       };
-                       if (removeStringEnd(filename, supported_ext).empty()){
-                               infostream << "Server: ignoring unsupported file extension: \""
-                                               << filename << "\"" << std::endl;
+                       if (dln.dir) // Ignore dirs
                                continue;
-                       }
-                       // Ok, attempt to load the file and add to cache
-                       std::string filepath;
-                       filepath.append(mediapath).append(DIR_DELIM).append(filename);
-
-                       // Read data
-                       std::ifstream fis(filepath.c_str(), std::ios_base::binary);
-                       if (!fis.good()) {
-                               errorstream << "Server::fillMediaCache(): Could not open \""
-                                               << filename << "\" for reading" << std::endl;
-                               continue;
-                       }
-                       std::ostringstream tmp_os(std::ios_base::binary);
-                       bool bad = false;
-                       for(;;) {
-                               char buf[1024];
-                               fis.read(buf, 1024);
-                               std::streamsize len = fis.gcount();
-                               tmp_os.write(buf, len);
-                               if (fis.eof())
-                                       break;
-                               if (!fis.good()) {
-                                       bad = true;
-                                       break;
-                               }
-                       }
-                       if(bad) {
-                               errorstream<<"Server::fillMediaCache(): Failed to read \""
-                                               << filename << "\"" << std::endl;
-                               continue;
-                       }
-                       if(tmp_os.str().length() == 0) {
-                               errorstream << "Server::fillMediaCache(): Empty file \""
-                                               << filepath << "\"" << std::endl;
-                               continue;
-                       }
-
-                       SHA1 sha1;
-                       sha1.addBytes(tmp_os.str().c_str(), tmp_os.str().length());
-
-                       unsigned char *digest = sha1.getDigest();
-                       std::string sha1_base64 = base64_encode(digest, 20);
-                       std::string sha1_hex = hex_encode((char*)digest, 20);
-                       free(digest);
-
-                       // Put in list
-                       m_media[filename] = MediaInfo(filepath, sha1_base64);
-                       verbosestream << "Server: " << sha1_hex << " is " << filename
-                                       << std::endl;
+                       std::string filepath = mediapath;
+                       filepath.append(DIR_DELIM).append(dln.name);
+                       addMediaFile(dln.name, filepath);
                }
        }
+
+       infostream << "Server: " << m_media.size() << " media files collected" << std::endl;
 }
 
 void Server::sendMediaAnnouncement(session_t peer_id, const std::string &lang_code)
@@ -3428,6 +3441,44 @@ void Server::deleteParticleSpawner(const std::string &playername, u32 id)
        SendDeleteParticleSpawner(peer_id, id);
 }
 
+bool Server::dynamicAddMedia(const std::string &filepath)
+{
+       std::string filename = fs::GetFilenameFromPath(filepath.c_str());
+       if (m_media.find(filename) != m_media.end()) {
+               errorstream << "Server::dynamicAddMedia(): file \"" << filename
+                       << "\" already exists in media cache" << std::endl;
+               return false;
+       }
+
+       // Load the file and add it to our media cache
+       std::string filedata, raw_hash;
+       bool ok = addMediaFile(filename, filepath, &filedata, &raw_hash);
+       if (!ok)
+               return false;
+
+       // Push file to existing clients
+       NetworkPacket pkt(TOCLIENT_MEDIA_PUSH, 0);
+       pkt << raw_hash << filename << (bool) true;
+       pkt.putLongString(filedata);
+
+       auto client_ids = m_clients.getClientIDs(CS_DefinitionsSent);
+       for (session_t client_id : client_ids) {
+               /*
+                       The network layer only guarantees ordered delivery inside a channel.
+                       Since the very next packet could be one that uses the media, we have
+                       to push the media over ALL channels to ensure it is processed before
+                       it is used.
+                       In practice this means we have to send it twice:
+                       - channel 1 (HUD)
+                       - channel 0 (everything else: e.g. play_sound, object messages)
+               */
+               m_clients.send(client_id, 1, &pkt, true);
+               m_clients.send(client_id, 0, &pkt, true);
+       }
+
+       return true;
+}
+
 // actions: time-reversed list
 // Return value: success/failure
 bool Server::rollbackRevertActions(const std::list<RollbackAction> &actions,
index 27943cc296cf43f25e71d32935e8408636af4b72..f44716531f2d3c6cf49576f6ee2f64ed8bc42ac7 100644 (file)
@@ -236,6 +236,8 @@ public:
 
        void deleteParticleSpawner(const std::string &playername, u32 id);
 
+       bool dynamicAddMedia(const std::string &filepath);
+
        ServerInventoryManager *getInventoryMgr() const { return m_inventory_mgr.get(); }
        void sendDetachedInventory(Inventory *inventory, const std::string &name, session_t peer_id);
 
@@ -435,6 +437,8 @@ private:
        // Sends blocks to clients (locks env and con on its own)
        void SendBlocks(float dtime);
 
+       bool addMediaFile(const std::string &filename, const std::string &filepath,
+                       std::string *filedata = nullptr, std::string *digest = nullptr);
        void fillMediaCache();
        void sendMediaAnnouncement(session_t peer_id, const std::string &lang_code);
        void sendRequestedMedia(session_t peer_id,