]> git.lizzy.rs Git - dragonfireclient.git/commitdiff
Dynamic_Add_Media v2 (#11550)
authorsfan5 <sfan5@live.de>
Thu, 9 Sep 2021 14:51:35 +0000 (16:51 +0200)
committerGitHub <noreply@github.com>
Thu, 9 Sep 2021 14:51:35 +0000 (16:51 +0200)
19 files changed:
builtin/game/misc.lua
doc/lua_api.txt
src/client/client.cpp
src/client/client.h
src/client/clientmedia.cpp
src/client/clientmedia.h
src/filesys.cpp
src/filesys.h
src/network/clientopcodes.cpp
src/network/clientpackethandler.cpp
src/network/networkprotocol.h
src/network/serveropcodes.cpp
src/network/serverpackethandler.cpp
src/script/cpp_api/s_server.cpp
src/script/cpp_api/s_server.h
src/script/lua_api/l_server.cpp
src/script/lua_api/l_server.h
src/server.cpp
src/server.h

index aac6c2d18696984232fbb827c41c7d2c1c0500bb..63d64817c7579b671a1e54eddf49451486bede1c 100644 (file)
@@ -269,27 +269,8 @@ function core.cancel_shutdown_requests()
 end
 
 
--- Callback handling for dynamic_add_media
-
-local dynamic_add_media_raw = core.dynamic_add_media_raw
-core.dynamic_add_media_raw = nil
-function core.dynamic_add_media(filepath, callback)
-       local ret = dynamic_add_media_raw(filepath)
-       if ret == false then
-               return ret
-       end
-       if callback == nil then
-               core.log("deprecated", "Calling minetest.dynamic_add_media without "..
-                       "a callback is deprecated and will stop working in future versions.")
-       else
-               -- At the moment async loading is not actually implemented, so we
-               -- immediately call the callback ourselves
-               for _, name in ipairs(ret) do
-                       callback(name)
-               end
-       end
-       return true
-end
+-- Used for callback handling with dynamic_add_media
+core.dynamic_media_callbacks = {}
 
 
 -- PNG encoder safety wrapper
index e99c1d1e65c049e6823fd3223b5eab0045a7a977..3a1a3f02fec1b6524660f268b43dcc2b57b55f26 100644 (file)
@@ -5649,22 +5649,33 @@ 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, callback)`
-    * `filepath`: path to a media file on the filesystem
-    * `callback`: function with arguments `name`, where name is a player name
-      (previously there was no callback argument; omitting it is deprecated)
-    * Adds the file 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.
+* `minetest.dynamic_add_media(options, callback)`
+    * `options`: table containing the following parameters
+        * `filepath`: path to a media file on the filesystem
+        * `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)
+    * `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.
+      Dynamically added media is not persisted between server restarts.
     * Returns false on error, true if the request was accepted
     * The given callback will be called for every player as soon as the
       media is available on the client.
-      Old clients that lack support for this feature will not see the media
-      unless they reconnect to the server. (callback won't be called)
-    * Since media transferred this way currently does not use client caching
-       or HTTP transfers, dynamic media should not be used with big files.
+    * Details/Notes:
+      * If `ephemeral`=false and `to_player` is unset the file is added to the media
+        sent to clients on startup, this means the media will appear even on
+        old clients if they rejoin the server.
+      * If `ephemeral`=false the file must not be modified, deleted, moved or
+        renamed after calling this function.
+      * Regardless of any use of `ephemeral`, adding media files with the same
+        name twice is not possible/guaranteed to work. An exception to this is the
+        use of `to_player` to send the same, already existent file to multiple
+        chosen players.
+    * Clients will attempt to fetch files added this way via remote media,
+      this can make transfer of bigger files painless (if set up). Nevertheless
+      it is advised not to use dynamic media for big media files.
 
 Bans
 ----
index 3c5559fcae0d4d51b1ed09daa41a85597511fb06..13ff22e8e5491c75970b0a078f015b86cdcc5760 100644 (file)
@@ -555,6 +555,29 @@ void Client::step(float dtime)
                        m_media_downloader = NULL;
                }
        }
+       {
+               // Acknowledge dynamic media downloads to server
+               std::vector<u32> done;
+               for (auto it = m_pending_media_downloads.begin();
+                               it != m_pending_media_downloads.end();) {
+                       assert(it->second->isStarted());
+                       it->second->step(this);
+                       if (it->second->isDone()) {
+                               done.emplace_back(it->first);
+
+                               it = m_pending_media_downloads.erase(it);
+                       } else {
+                               it++;
+                       }
+
+                       if (done.size() == 255) { // maximum in one packet
+                               sendHaveMedia(done);
+                               done.clear();
+                       }
+               }
+               if (!done.empty())
+                       sendHaveMedia(done);
+       }
 
        /*
                If the server didn't update the inventory in a while, revert
@@ -770,7 +793,8 @@ void Client::request_media(const std::vector<std::string> &file_requests)
        Send(&pkt);
 
        infostream << "Client: Sending media request list to server ("
-                       << file_requests.size() << " files. packet size)" << std::endl;
+                       << file_requests.size() << " files, packet size "
+                       << pkt.getSize() << ")" << std::endl;
 }
 
 void Client::initLocalMapSaving(const Address &address,
@@ -1295,6 +1319,19 @@ void Client::sendPlayerPos()
        Send(&pkt);
 }
 
+void Client::sendHaveMedia(const std::vector<u32> &tokens)
+{
+       NetworkPacket pkt(TOSERVER_HAVE_MEDIA, 1 + tokens.size() * 4);
+
+       sanity_check(tokens.size() < 256);
+
+       pkt << static_cast<u8>(tokens.size());
+       for (u32 token : tokens)
+               pkt << token;
+
+       Send(&pkt);
+}
+
 void Client::removeNode(v3s16 p)
 {
        std::map<v3s16, MapBlock*> modified_blocks;
index 85ca2404994583d07faea0a8f0a5f92731709f2f..c1a38ba48f65facbfdf6809d994a9f659b949f21 100644 (file)
@@ -53,6 +53,7 @@ class ISoundManager;
 class NodeDefManager;
 //class IWritableCraftDefManager;
 class ClientMediaDownloader;
+class SingleMediaDownloader;
 struct MapDrawControl;
 class ModChannelMgr;
 class MtEventManager;
@@ -245,6 +246,7 @@ class Client : public con::PeerHandler, public InventoryManager, public IGameDef
        void sendDamage(u16 damage);
        void sendRespawn();
        void sendReady();
+       void sendHaveMedia(const std::vector<u32> &tokens);
 
        ClientEnvironment& getEnv() { return m_env; }
        ITextureSource *tsrc() { return getTextureSource(); }
@@ -536,9 +538,13 @@ class Client : public con::PeerHandler, public InventoryManager, public IGameDef
        bool m_activeobjects_received = false;
        bool m_mods_loaded = false;
 
+       std::vector<std::string> m_remote_media_servers;
+       // Media downloader, only exists during init
        ClientMediaDownloader *m_media_downloader;
        // Set of media filenames pushed by server at runtime
        std::unordered_set<std::string> m_media_pushed_files;
+       // Pending downloads of dynamic media (key: token)
+       std::vector<std::pair<u32, std::unique_ptr<SingleMediaDownloader>>> m_pending_media_downloads;
 
        // time_of_day speed approximation for old protocol
        bool m_time_of_day_set = false;
index 0f9ba53566be30e43bad5e5ade974250f90f590d..6c5d4a8bf9ac09936d7dfaab5a06d2ab4e2feb74 100644 (file)
@@ -49,7 +49,6 @@ bool clientMediaUpdateCache(const std::string &raw_hash, const std::string &file
 */
 
 ClientMediaDownloader::ClientMediaDownloader():
-       m_media_cache(getMediaCacheDir()),
        m_httpfetch_caller(HTTPFETCH_DISCARD)
 {
 }
@@ -66,6 +65,12 @@ ClientMediaDownloader::~ClientMediaDownloader()
                delete remote;
 }
 
+bool ClientMediaDownloader::loadMedia(Client *client, const std::string &data,
+               const std::string &name)
+{
+       return client->loadMedia(data, name);
+}
+
 void ClientMediaDownloader::addFile(const std::string &name, const std::string &sha1)
 {
        assert(!m_initial_step_done); // pre-condition
@@ -105,7 +110,7 @@ void ClientMediaDownloader::addRemoteServer(const std::string &baseurl)
 {
        assert(!m_initial_step_done);   // pre-condition
 
-       #ifdef USE_CURL
+#ifdef USE_CURL
 
        if (g_settings->getBool("enable_remote_media_server")) {
                infostream << "Client: Adding remote server \""
@@ -117,13 +122,13 @@ void ClientMediaDownloader::addRemoteServer(const std::string &baseurl)
                m_remotes.push_back(remote);
        }
 
-       #else
+#else
 
        infostream << "Client: Ignoring remote server \""
                << baseurl << "\" because cURL support is not compiled in"
                << std::endl;
 
-       #endif
+#endif
 }
 
 void ClientMediaDownloader::step(Client *client)
@@ -172,36 +177,21 @@ void ClientMediaDownloader::initialStep(Client *client)
        // Check media cache
        m_uncached_count = m_files.size();
        for (auto &file_it : m_files) {
-               std::string name = file_it.first;
+               const std::string &name = file_it.first;
                FileStatus *filestatus = file_it.second;
                const std::string &sha1 = filestatus->sha1;
 
-               std::ostringstream tmp_os(std::ios_base::binary);
-               bool found_in_cache = m_media_cache.load(hex_encode(sha1), tmp_os);
-
-               // If found in cache, try to load it from there
-               if (found_in_cache) {
-                       bool success = checkAndLoad(name, sha1,
-                                       tmp_os.str(), true, client);
-                       if (success) {
-                               filestatus->received = true;
-                               m_uncached_count--;
-                       }
+               if (tryLoadFromCache(name, sha1, client)) {
+                       filestatus->received = true;
+                       m_uncached_count--;
                }
        }
 
        assert(m_uncached_received_count == 0);
 
        // Create the media cache dir if we are likely to write to it
-       if (m_uncached_count != 0) {
-               bool did = fs::CreateAllDirs(getMediaCacheDir());
-               if (!did) {
-                       errorstream << "Client: "
-                               << "Could not create media cache directory: "
-                               << getMediaCacheDir()
-                               << std::endl;
-               }
-       }
+       if (m_uncached_count != 0)
+               createCacheDirs();
 
        // If we found all files in the cache, report this fact to the server.
        // If the server reported no remote servers, immediately start
@@ -301,8 +291,7 @@ void ClientMediaDownloader::remoteHashSetReceived(
                        // available on this server, add this server
                        // to the available_remotes array
 
-                       for(std::map<std::string, FileStatus*>::iterator
-                                       it = m_files.upper_bound(m_name_bound);
+                       for(auto it = m_files.upper_bound(m_name_bound);
                                        it != m_files.end(); ++it) {
                                FileStatus *f = it->second;
                                if (!f->received && sha1_set.count(f->sha1))
@@ -328,8 +317,7 @@ void ClientMediaDownloader::remoteMediaReceived(
 
        std::string name;
        {
-               std::unordered_map<unsigned long, std::string>::iterator it =
-                       m_remote_file_transfers.find(fetch_result.request_id);
+               auto it = m_remote_file_transfers.find(fetch_result.request_id);
                assert(it != m_remote_file_transfers.end());
                name = it->second;
                m_remote_file_transfers.erase(it);
@@ -398,8 +386,7 @@ void ClientMediaDownloader::startRemoteMediaTransfers()
 {
        bool changing_name_bound = true;
 
-       for (std::map<std::string, FileStatus*>::iterator
-                       files_iter = m_files.upper_bound(m_name_bound);
+       for (auto files_iter = m_files.upper_bound(m_name_bound);
                        files_iter != m_files.end(); ++files_iter) {
 
                // Abort if active fetch limit is exceeded
@@ -477,19 +464,18 @@ void ClientMediaDownloader::startConventionalTransfers(Client *client)
        }
 }
 
-void ClientMediaDownloader::conventionalTransferDone(
+bool ClientMediaDownloader::conventionalTransferDone(
                const std::string &name,
                const std::string &data,
                Client *client)
 {
        // Check that file was announced
-       std::map<std::string, FileStatus*>::iterator
-               file_iter = m_files.find(name);
+       auto file_iter = m_files.find(name);
        if (file_iter == m_files.end()) {
                errorstream << "Client: server sent media file that was"
                        << "not announced, ignoring it: \"" << name << "\""
                        << std::endl;
-               return;
+               return false;
        }
        FileStatus *filestatus = file_iter->second;
        assert(filestatus != NULL);
@@ -499,7 +485,7 @@ void ClientMediaDownloader::conventionalTransferDone(
                errorstream << "Client: server sent media file that we already"
                        << "received, ignoring it: \"" << name << "\""
                        << std::endl;
-               return;
+               return true;
        }
 
        // Mark file as received, regardless of whether loading it works and
@@ -512,9 +498,45 @@ void ClientMediaDownloader::conventionalTransferDone(
        // Check that received file matches announced checksum
        // If so, load it
        checkAndLoad(name, filestatus->sha1, data, false, client);
+
+       return true;
+}
+
+/*
+       IClientMediaDownloader
+*/
+
+IClientMediaDownloader::IClientMediaDownloader():
+       m_media_cache(getMediaCacheDir()), m_write_to_cache(true)
+{
 }
 
-bool ClientMediaDownloader::checkAndLoad(
+void IClientMediaDownloader::createCacheDirs()
+{
+       if (!m_write_to_cache)
+               return;
+
+       std::string path = getMediaCacheDir();
+       if (!fs::CreateAllDirs(path)) {
+               errorstream << "Client: Could not create media cache directory: "
+                       << path << std::endl;
+       }
+}
+
+bool IClientMediaDownloader::tryLoadFromCache(const std::string &name,
+       const std::string &sha1, Client *client)
+{
+       std::ostringstream tmp_os(std::ios_base::binary);
+       bool found_in_cache = m_media_cache.load(hex_encode(sha1), tmp_os);
+
+       // If found in cache, try to load it from there
+       if (found_in_cache)
+               return checkAndLoad(name, sha1, tmp_os.str(), true, client);
+
+       return false;
+}
+
+bool IClientMediaDownloader::checkAndLoad(
                const std::string &name, const std::string &sha1,
                const std::string &data, bool is_from_cache, Client *client)
 {
@@ -544,7 +566,7 @@ bool ClientMediaDownloader::checkAndLoad(
        }
 
        // Checksum is ok, try loading the file
-       bool success = client->loadMedia(data, name);
+       bool success = loadMedia(client, data, name);
        if (!success) {
                infostream << "Client: "
                        << "Failed to load " << cached_or_received << " media: "
@@ -559,7 +581,7 @@ bool ClientMediaDownloader::checkAndLoad(
                << std::endl;
 
        // Update cache (unless we just loaded the file from the cache)
-       if (!is_from_cache)
+       if (!is_from_cache && m_write_to_cache)
                m_media_cache.update(sha1_hex, data);
 
        return true;
@@ -587,12 +609,10 @@ std::string ClientMediaDownloader::serializeRequiredHashSet()
 
        // Write list of hashes of files that have not been
        // received (found in cache) yet
-       for (std::map<std::string, FileStatus*>::iterator
-                       it = m_files.begin();
-                       it != m_files.end(); ++it) {
-               if (!it->second->received) {
-                       FATAL_ERROR_IF(it->second->sha1.size() != 20, "Invalid SHA1 size");
-                       os << it->second->sha1;
+       for (const auto &it : m_files) {
+               if (!it.second->received) {
+                       FATAL_ERROR_IF(it.second->sha1.size() != 20, "Invalid SHA1 size");
+                       os << it.second->sha1;
                }
        }
 
@@ -628,3 +648,145 @@ void ClientMediaDownloader::deSerializeHashSet(const std::string &data,
                result.insert(data.substr(pos, 20));
        }
 }
+
+/*
+       SingleMediaDownloader
+*/
+
+SingleMediaDownloader::SingleMediaDownloader(bool write_to_cache):
+       m_httpfetch_caller(HTTPFETCH_DISCARD)
+{
+       m_write_to_cache = write_to_cache;
+}
+
+SingleMediaDownloader::~SingleMediaDownloader()
+{
+       if (m_httpfetch_caller != HTTPFETCH_DISCARD)
+               httpfetch_caller_free(m_httpfetch_caller);
+}
+
+bool SingleMediaDownloader::loadMedia(Client *client, const std::string &data,
+               const std::string &name)
+{
+       return client->loadMedia(data, name, true);
+}
+
+void SingleMediaDownloader::addFile(const std::string &name, const std::string &sha1)
+{
+       assert(m_stage == STAGE_INIT); // pre-condition
+
+       assert(!name.empty());
+       assert(sha1.size() == 20);
+
+       FATAL_ERROR_IF(!m_file_name.empty(), "Cannot add a second file");
+       m_file_name = name;
+       m_file_sha1 = sha1;
+}
+
+void SingleMediaDownloader::addRemoteServer(const std::string &baseurl)
+{
+       assert(m_stage == STAGE_INIT); // pre-condition
+
+       if (g_settings->getBool("enable_remote_media_server"))
+               m_remotes.emplace_back(baseurl);
+}
+
+void SingleMediaDownloader::step(Client *client)
+{
+       if (m_stage == STAGE_INIT) {
+               m_stage = STAGE_CACHE_CHECKED;
+               initialStep(client);
+       }
+
+       // Remote media: check for completion of fetches
+       if (m_httpfetch_caller != HTTPFETCH_DISCARD) {
+               HTTPFetchResult fetch_result;
+               while (httpfetch_async_get(m_httpfetch_caller, fetch_result)) {
+                       remoteMediaReceived(fetch_result, client);
+               }
+       }
+}
+
+bool SingleMediaDownloader::conventionalTransferDone(const std::string &name,
+               const std::string &data, Client *client)
+{
+       if (name != m_file_name)
+               return false;
+
+       // Mark file as received unconditionally and try to load it
+       m_stage = STAGE_DONE;
+       checkAndLoad(name, m_file_sha1, data, false, client);
+       return true;
+}
+
+void SingleMediaDownloader::initialStep(Client *client)
+{
+       if (tryLoadFromCache(m_file_name, m_file_sha1, client))
+               m_stage = STAGE_DONE;
+       if (isDone())
+               return;
+
+       createCacheDirs();
+
+       // If the server reported no remote servers, immediately fall back to
+       // conventional transfer.
+       if (!USE_CURL || m_remotes.empty()) {
+               startConventionalTransfer(client);
+       } else {
+               // Otherwise start by requesting the file from the first remote media server
+               m_httpfetch_caller = httpfetch_caller_alloc();
+               m_current_remote = 0;
+               startRemoteMediaTransfer();
+       }
+}
+
+void SingleMediaDownloader::remoteMediaReceived(
+               const HTTPFetchResult &fetch_result, Client *client)
+{
+       sanity_check(!isDone());
+       sanity_check(m_current_remote >= 0);
+
+       // If fetch succeeded, try to load it
+       if (fetch_result.succeeded) {
+               bool success = checkAndLoad(m_file_name, m_file_sha1,
+                               fetch_result.data, false, client);
+               if (success) {
+                       m_stage = STAGE_DONE;
+                       return;
+               }
+       }
+
+       // Otherwise try the next remote server or fall back to conventional transfer
+       m_current_remote++;
+       if (m_current_remote >= (int)m_remotes.size()) {
+               infostream << "Client: Failed to remote-fetch \"" << m_file_name
+                               << "\". Requesting it the usual way." << std::endl;
+               m_current_remote = -1;
+               startConventionalTransfer(client);
+       } else {
+               startRemoteMediaTransfer();
+       }
+}
+
+void SingleMediaDownloader::startRemoteMediaTransfer()
+{
+       std::string url = m_remotes.at(m_current_remote) + hex_encode(m_file_sha1);
+       verbosestream << "Client: Requesting remote media file "
+               << "\"" << m_file_name << "\" " << "\"" << url << "\"" << std::endl;
+
+       HTTPFetchRequest fetch_request;
+       fetch_request.url = url;
+       fetch_request.caller = m_httpfetch_caller;
+       fetch_request.request_id = m_httpfetch_next_id;
+       fetch_request.timeout = g_settings->getS32("curl_file_download_timeout");
+       httpfetch_async(fetch_request);
+
+       m_httpfetch_next_id++;
+}
+
+void SingleMediaDownloader::startConventionalTransfer(Client *client)
+{
+       std::vector<std::string> requests;
+       requests.emplace_back(m_file_name);
+       client->request_media(requests);
+}
index e97a0f24bef87997daa5b0b953604d1735a88986..aa7b0f3985cd9d688bc5e2274a13e7b04c0888f5 100644 (file)
@@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #include "irrlichttypes.h"
 #include "filecache.h"
+#include "util/basic_macros.h"
 #include <ostream>
 #include <map>
 #include <set>
@@ -38,7 +39,62 @@ struct HTTPFetchResult;
 bool clientMediaUpdateCache(const std::string &raw_hash,
        const std::string &filedata);
 
-class ClientMediaDownloader
+// more of a base class than an interface but this name was most convenient...
+class IClientMediaDownloader
+{
+public:
+       DISABLE_CLASS_COPY(IClientMediaDownloader)
+
+       virtual bool isStarted() const = 0;
+
+       // If this returns true, the downloader is done and can be deleted
+       virtual bool isDone() const = 0;
+
+       // Add a file to the list of required file (but don't fetch it yet)
+       virtual void addFile(const std::string &name, const std::string &sha1) = 0;
+
+       // Add a remote server to the list; ignored if not built with cURL
+       virtual void addRemoteServer(const std::string &baseurl) = 0;
+
+       // Steps the media downloader:
+       // - May load media into client by calling client->loadMedia()
+       // - May check media cache for files
+       // - May add files to media cache
+       // - May start remote transfers by calling httpfetch_async
+       // - May check for completion of current remote transfers
+       // - May start conventional transfers by calling client->request_media()
+       // - May inform server that all media has been loaded
+       //   by calling client->received_media()
+       // After step has been called once, don't call addFile/addRemoteServer.
+       virtual void step(Client *client) = 0;
+
+       // Must be called for each file received through TOCLIENT_MEDIA
+       // returns true if this file belongs to this downloader
+       virtual bool conventionalTransferDone(const std::string &name,
+                       const std::string &data, Client *client) = 0;
+
+protected:
+       IClientMediaDownloader();
+       virtual ~IClientMediaDownloader() = default;
+
+       // Forwards the call to the appropriate Client method
+       virtual bool loadMedia(Client *client, const std::string &data,
+               const std::string &name) = 0;
+
+       void createCacheDirs();
+
+       bool tryLoadFromCache(const std::string &name, const std::string &sha1,
+                       Client *client);
+
+       bool checkAndLoad(const std::string &name, const std::string &sha1,
+                       const std::string &data, bool is_from_cache, Client *client);
+
+       // Filesystem-based media cache
+       FileCache m_media_cache;
+       bool m_write_to_cache;
+};
+
+class ClientMediaDownloader : public IClientMediaDownloader
 {
 public:
        ClientMediaDownloader();
@@ -52,39 +108,29 @@ class ClientMediaDownloader
                return 0.0f;
        }
 
-       bool isStarted() const {
+       bool isStarted() const override {
                return m_initial_step_done;
        }
 
-       // If this returns true, the downloader is done and can be deleted
-       bool isDone() const {
+       bool isDone() const override {
                return m_initial_step_done &&
                        m_uncached_received_count == m_uncached_count;
        }
 
-       // Add a file to the list of required file (but don't fetch it yet)
-       void addFile(const std::string &name, const std::string &sha1);
+       void addFile(const std::string &name, const std::string &sha1) override;
 
-       // Add a remote server to the list; ignored if not built with cURL
-       void addRemoteServer(const std::string &baseurl);
+       void addRemoteServer(const std::string &baseurl) override;
 
-       // Steps the media downloader:
-       // - May load media into client by calling client->loadMedia()
-       // - May check media cache for files
-       // - May add files to media cache
-       // - May start remote transfers by calling httpfetch_async
-       // - May check for completion of current remote transfers
-       // - May start conventional transfers by calling client->request_media()
-       // - May inform server that all media has been loaded
-       //   by calling client->received_media()
-       // After step has been called once, don't call addFile/addRemoteServer.
-       void step(Client *client);
+       void step(Client *client) override;
 
-       // Must be called for each file received through TOCLIENT_MEDIA
-       void conventionalTransferDone(
+       bool conventionalTransferDone(
                        const std::string &name,
                        const std::string &data,
-                       Client *client);
+                       Client *client) override;
+
+protected:
+       bool loadMedia(Client *client, const std::string &data,
+                       const std::string &name) override;
 
 private:
        struct FileStatus {
@@ -107,13 +153,9 @@ class ClientMediaDownloader
        void startRemoteMediaTransfers();
        void startConventionalTransfers(Client *client);
 
-       bool checkAndLoad(const std::string &name, const std::string &sha1,
-                       const std::string &data, bool is_from_cache,
-                       Client *client);
-
-       std::string serializeRequiredHashSet();
        static void deSerializeHashSet(const std::string &data,
                        std::set<std::string> &result);
+       std::string serializeRequiredHashSet();
 
        // Maps filename to file status
        std::map<std::string, FileStatus*> m_files;
@@ -121,9 +163,6 @@ class ClientMediaDownloader
        // Array of remote media servers
        std::vector<RemoteServerStatus*> m_remotes;
 
-       // Filesystem-based media cache
-       FileCache m_media_cache;
-
        // Has an attempt been made to load media files from the file cache?
        // Have hash sets been requested from remote servers?
        bool m_initial_step_done = false;
@@ -149,3 +188,63 @@ class ClientMediaDownloader
        std::string m_name_bound = "";
 
 };
+
+// A media downloader that only downloads a single file.
+// It does/doesn't do several things the normal downloader does:
+// - won't fetch hash sets from remote servers
+// - will mark loaded media as coming from file push
+// - writing to file cache is optional
+class SingleMediaDownloader : public IClientMediaDownloader
+{
+public:
+       SingleMediaDownloader(bool write_to_cache);
+       ~SingleMediaDownloader();
+
+       bool isStarted() const override {
+               return m_stage > STAGE_INIT;
+       }
+
+       bool isDone() const override {
+               return m_stage >= STAGE_DONE;
+       }
+
+       void addFile(const std::string &name, const std::string &sha1) override;
+
+       void addRemoteServer(const std::string &baseurl) override;
+
+       void step(Client *client) override;
+
+       bool conventionalTransferDone(const std::string &name,
+                       const std::string &data, Client *client) override;
+
+protected:
+       bool loadMedia(Client *client, const std::string &data,
+                       const std::string &name) override;
+
+private:
+       void initialStep(Client *client);
+       void remoteMediaReceived(const HTTPFetchResult &fetch_result, Client *client);
+       void startRemoteMediaTransfer();
+       void startConventionalTransfer(Client *client);
+
+       enum Stage {
+               STAGE_INIT,
+               STAGE_CACHE_CHECKED, // we have tried to load the file from cache
+               STAGE_DONE
+       };
+
+       // Information about the one file we want to fetch
+       std::string m_file_name;
+       std::string m_file_sha1;
+       s32 m_current_remote;
+
+       // Array of remote media servers
+       std::vector<std::string> m_remotes;
+
+       enum Stage m_stage = STAGE_INIT;
+
+       // Status of remote transfers
+       unsigned long m_httpfetch_caller;
+       unsigned long m_httpfetch_next_id = 0;
+
+};
index 99b0306240c0ad21520d950661b77eedf357daf2..0941739b8c28f8c3aab8514639b8c0fd9d375c97 100644 (file)
@@ -21,8 +21,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "util/string.h"
 #include <iostream>
 #include <cstdio>
+#include <cstdlib>
 #include <cstring>
 #include <cerrno>
+#include <unistd.h>
 #include <fstream>
 #include "log.h"
 #include "config.h"
@@ -811,5 +813,15 @@ bool Rename(const std::string &from, const std::string &to)
        return rename(from.c_str(), to.c_str()) == 0;
 }
 
+std::string CreateTempFile()
+{
+       std::string path = TempPath() + DIR_DELIM "MT_XXXXXX";
+       int fd = mkstemp(&path[0]); // modifies path
+       if (fd == -1)
+               return "";
+       close(fd);
+       return path;
+}
+
 } // namespace fs
 
index a9584b036e176a6c6d173f6bfeaa1a82af3b4eec..f72cb0ba2822a8e88e5ded2ec5d666b2c11acd27 100644 (file)
@@ -71,6 +71,10 @@ bool DeleteSingleFileOrEmptyDirectory(const std::string &path);
 // Returns path to temp directory, can return "" on error
 std::string TempPath();
 
+// Returns path to securely-created temporary file (will already exist when this function returns)
+// can return "" on error
+std::string CreateTempFile();
+
 /* Returns a list of subdirectories, including the path itself, but excluding
        hidden directories (whose names start with . or _)
 */
index 55cfdd4dc728c0c962152ea7f2fad2b049ea011a..a98a5e7d187b00b0bc31d5d6aeec5534862c3a1e 100644 (file)
@@ -204,7 +204,7 @@ const ServerCommandFactory serverCommandFactoryTable[TOSERVER_NUM_MSG_TYPES] =
        null_command_factory, // 0x3e
        null_command_factory, // 0x3f
        { "TOSERVER_REQUEST_MEDIA",      1, true }, // 0x40
-       null_command_factory, // 0x41
+       { "TOSERVER_HAVE_MEDIA",         2, true }, // 0x41
        null_command_factory, // 0x42
        { "TOSERVER_CLIENT_READY",       1, true }, // 0x43
        null_command_factory, // 0x44
index a631a3178dce62ef90ade3e8d3dc87d410221ae7..9c9c59d13b6bbda143c30740ee7873da72733465 100644 (file)
@@ -670,21 +670,19 @@ void Client::handleCommand_AnnounceMedia(NetworkPacket* pkt)
                m_media_downloader->addFile(name, sha1_raw);
        }
 
-       try {
+       {
                std::string str;
-
                *pkt >> str;
 
                Strfnd sf(str);
-               while(!sf.at_end()) {
+               while (!sf.at_end()) {
                        std::string baseurl = trim(sf.next(","));
-                       if (!baseurl.empty())
+                       if (!baseurl.empty()) {
+                               m_remote_media_servers.emplace_back(baseurl);
                                m_media_downloader->addRemoteServer(baseurl);
+                       }
                }
        }
-       catch(SerializationError& e) {
-               // not supported by server or turned off
-       }
 
        m_media_downloader->step(this);
 }
@@ -716,31 +714,38 @@ void Client::handleCommand_Media(NetworkPacket* pkt)
        if (num_files == 0)
                return;
 
-       if (!m_media_downloader || !m_media_downloader->isStarted()) {
-               const char *problem = m_media_downloader ?
-                       "media has not been requested" :
-                       "all media has been received already";
-               errorstream << "Client: Received media but "
-                       << problem << "! "
-                       << " bunch " << bunch_i << "/" << num_bunches
-                       << " files=" << num_files
-                       << " size=" << pkt->getSize() << std::endl;
-               return;
-       }
+       bool init_phase = m_media_downloader && m_media_downloader->isStarted();
 
-       // Mesh update thread must be stopped while
-       // updating content definitions
-       sanity_check(!m_mesh_update_thread.isRunning());
+       if (init_phase) {
+               // Mesh update thread must be stopped while
+               // updating content definitions
+               sanity_check(!m_mesh_update_thread.isRunning());
+       }
 
-       for (u32 i=0; i < num_files; i++) {
-               std::string name;
+       for (u32 i = 0; i < num_files; i++) {
+               std::string name, data;
 
                *pkt >> name;
+               data = pkt->readLongString();
 
-               std::string data = pkt->readLongString();
-
-               m_media_downloader->conventionalTransferDone(
-                               name, data, this);
+               bool ok = false;
+               if (init_phase) {
+                       ok = m_media_downloader->conventionalTransferDone(name, data, this);
+               } else {
+                       // Check pending dynamic transfers, one of them must be it
+                       for (const auto &it : m_pending_media_downloads) {
+                               if (it.second->conventionalTransferDone(name, data, this)) {
+                                       ok = true;
+                                       break;
+                               }
+                       }
+               }
+               if (!ok) {
+                       errorstream << "Client: Received media \"" << name
+                               << "\" but no downloads pending. " << num_bunches << " bunches, "
+                               << num_files << " in this one. (init_phase=" << init_phase
+                               << ")" << std::endl;
+               }
        }
 }
 
@@ -1497,46 +1502,72 @@ void Client::handleCommand_PlayerSpeed(NetworkPacket *pkt)
 void Client::handleCommand_MediaPush(NetworkPacket *pkt)
 {
        std::string raw_hash, filename, filedata;
+       u32 token;
        bool cached;
 
        *pkt >> raw_hash >> filename >> cached;
-       filedata = pkt->readLongString();
+       if (m_proto_ver >= 40)
+               *pkt >> token;
+       else
+               filedata = pkt->readLongString();
 
-       if (raw_hash.size() != 20 || filedata.empty() || filename.empty() ||
+       if (raw_hash.size() != 20 || filename.empty() ||
+                       (m_proto_ver < 40 && filedata.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;
+       verbosestream << "Server pushes media file \"" << filename << "\" ";
+       if (filedata.empty())
+               verbosestream << "to be fetched ";
+       else
+               verbosestream << "with " << filedata.size() << " bytes ";
+       verbosestream << "(cached=" << cached << ")" << std::endl;
 
        if (m_media_pushed_files.count(filename) != 0) {
-               // Silently ignore for synchronization purposes
+               // Ignore (but acknowledge). Previously this was for sync purposes,
+               // but even in new versions media cannot be replaced at runtime.
+               if (m_proto_ver >= 40)
+                       sendHaveMedia({ token });
                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;
+       if (!filedata.empty()) {
+               // LEGACY CODEPATH
+               // 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);
                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);
+       // create a downloader for this file
+       auto downloader = new SingleMediaDownloader(cached);
+       m_pending_media_downloads.emplace_back(token, downloader);
+       downloader->addFile(filename, raw_hash);
+       for (const auto &baseurl : m_remote_media_servers)
+               downloader->addRemoteServer(baseurl);
+
+       downloader->step(this);
 }
 
 /*
index b647aab1a9874c10d48c49de2ff7b7db0c494ea1..8214cc5b1433157428b59dc8a5496c60b96002f9 100644 (file)
@@ -207,6 +207,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
                Minimap modes
        PROTOCOL VERSION 40:
                Added 'basic_debug' privilege
+               TOCLIENT_MEDIA_PUSH changed, TOSERVER_HAVE_MEDIA added
 */
 
 #define LATEST_PROTOCOL_VERSION 40
@@ -317,9 +318,8 @@ enum ToClientCommand
        /*
                std::string raw_hash
                std::string filename
+               u32 callback_token
                bool should_be_cached
-               u32 len
-               char filedata[len]
        */
 
        // (oops, there is some gap here)
@@ -938,7 +938,13 @@ enum ToServerCommand
                }
        */
 
-       TOSERVER_RECEIVED_MEDIA = 0x41, // Obsolete
+       TOSERVER_HAVE_MEDIA = 0x41,
+       /*
+               u8 number of callback tokens
+               for each:
+                       u32 token
+       */
+
        TOSERVER_BREATH = 0x42, // Obsolete
 
        TOSERVER_CLIENT_READY = 0x43,
index aea5d71749f19a87dc9ce0d488383217bda57bb3..44b65e8daa2821933cbcefb28ce172dd152d10f0 100644 (file)
@@ -89,7 +89,7 @@ const ToServerCommandHandler toServerCommandTable[TOSERVER_NUM_MSG_TYPES] =
        null_command_handler, // 0x3e
        null_command_handler, // 0x3f
        { "TOSERVER_REQUEST_MEDIA",            TOSERVER_STATE_STARTUP, &Server::handleCommand_RequestMedia }, // 0x40
-       null_command_handler, // 0x41
+       { "TOSERVER_HAVE_MEDIA",               TOSERVER_STATE_INGAME, &Server::handleCommand_HaveMedia }, // 0x41
        null_command_handler, // 0x42
        { "TOSERVER_CLIENT_READY",             TOSERVER_STATE_STARTUP, &Server::handleCommand_ClientReady }, // 0x43
        null_command_handler, // 0x44
@@ -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
-       { "TOCLIENT_MEDIA_PUSH",               0, true }, // 0x2C (sent over channel 1 too)
+       { "TOCLIENT_MEDIA_PUSH",               0, true }, // 0x2C (sent over channel 1 too if legacy)
        null_command_factory, // 0x2D
        null_command_factory, // 0x2E
        { "TOCLIENT_CHAT_MESSAGE",             0, true }, // 0x2F
index 77fde2a6637427ea04e5d4a59e3a9bd3e697d941..4c609644fa17c6cb6ceb6ef34904c2869f9ed85c 100644 (file)
@@ -362,16 +362,15 @@ void Server::handleCommand_RequestMedia(NetworkPacket* pkt)
        session_t peer_id = pkt->getPeerId();
        infostream << "Sending " << numfiles << " files to " <<
                getPlayerName(peer_id) << std::endl;
-       verbosestream << "TOSERVER_REQUEST_MEDIA: " << std::endl;
+       verbosestream << "TOSERVER_REQUEST_MEDIA: requested file(s)" << std::endl;
 
        for (u16 i = 0; i < numfiles; i++) {
                std::string name;
 
                *pkt >> name;
 
-               tosend.push_back(name);
-               verbosestream << "TOSERVER_REQUEST_MEDIA: requested file "
-                               << name << std::endl;
+               tosend.emplace_back(name);
+               verbosestream << "  " << name << std::endl;
        }
 
        sendRequestedMedia(peer_id, tosend);
@@ -1801,3 +1800,30 @@ void Server::handleCommand_ModChannelMsg(NetworkPacket *pkt)
 
        broadcastModChannelMessage(channel_name, channel_msg, peer_id);
 }
+
+void Server::handleCommand_HaveMedia(NetworkPacket *pkt)
+{
+       std::vector<u32> tokens;
+       u8 numtokens;
+
+       *pkt >> numtokens;
+       for (u16 i = 0; i < numtokens; i++) {
+               u32 n;
+               *pkt >> n;
+               tokens.emplace_back(n);
+       }
+
+       const session_t peer_id = pkt->getPeerId();
+       auto player = m_env->getPlayer(peer_id);
+
+       for (const u32 token : tokens) {
+               auto it = m_pending_dyn_media.find(token);
+               if (it == m_pending_dyn_media.end())
+                       continue;
+               if (it->second.waiting_players.count(peer_id)) {
+                       it->second.waiting_players.erase(peer_id);
+                       if (player)
+                               getScriptIface()->on_dynamic_media_added(token, player->getName());
+               }
+       }
+}
index 96cb28b28936e3922997042f8badf6735541492f..6ddb2630d45048aa1b64b9e718066497454f283f 100644 (file)
@@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "cpp_api/s_server.h"
 #include "cpp_api/s_internal.h"
 #include "common/c_converter.h"
+#include "util/numeric.h" // myrand
 
 bool ScriptApiServer::getAuth(const std::string &playername,
                std::string *dst_password,
@@ -196,3 +197,68 @@ std::string ScriptApiServer::formatChatMessage(const std::string &name,
 
        return ret;
 }
+
+u32 ScriptApiServer::allocateDynamicMediaCallback(int f_idx)
+{
+       lua_State *L = getStack();
+
+       if (f_idx < 0)
+               f_idx = lua_gettop(L) + f_idx + 1;
+
+       lua_getglobal(L, "core");
+       lua_getfield(L, -1, "dynamic_media_callbacks");
+       luaL_checktype(L, -1, LUA_TTABLE);
+
+       // Find a randomly generated token that doesn't exist yet
+       int tries = 100;
+       u32 token;
+       while (1) {
+               token = myrand();
+               lua_rawgeti(L, -2, token);
+               bool is_free = lua_isnil(L, -1);
+               lua_pop(L, 1);
+               if (is_free)
+                       break;
+               if (--tries < 0)
+                       FATAL_ERROR("Ran out of callbacks IDs?!");
+       }
+
+       // core.dynamic_media_callbacks[token] = callback_func
+       lua_pushvalue(L, f_idx);
+       lua_rawseti(L, -2, token);
+
+       lua_pop(L, 2);
+
+       verbosestream << "allocateDynamicMediaCallback() = " << token << std::endl;
+       return token;
+}
+
+void ScriptApiServer::freeDynamicMediaCallback(u32 token)
+{
+       lua_State *L = getStack();
+
+       verbosestream << "freeDynamicMediaCallback(" << token << ")" << std::endl;
+
+       // core.dynamic_media_callbacks[token] = nil
+       lua_getglobal(L, "core");
+       lua_getfield(L, -1, "dynamic_media_callbacks");
+       luaL_checktype(L, -1, LUA_TTABLE);
+       lua_pushnil(L);
+       lua_rawseti(L, -2, token);
+       lua_pop(L, 2);
+}
+
+void ScriptApiServer::on_dynamic_media_added(u32 token, const char *playername)
+{
+       SCRIPTAPI_PRECHECKHEADER
+
+       int error_handler = PUSH_ERROR_HANDLER(L);
+       lua_getglobal(L, "core");
+       lua_getfield(L, -1, "dynamic_media_callbacks");
+       luaL_checktype(L, -1, LUA_TTABLE);
+       lua_rawgeti(L, -1, token);
+       luaL_checktype(L, -1, LUA_TFUNCTION);
+
+       lua_pushstring(L, playername);
+       PCALL_RES(lua_pcall(L, 1, 0, error_handler));
+}
index d8639cba789e87f766db577cd0db18661077db7e..c5c3d559606723006e5f1a2b9e9e64c6e2d39fd1 100644 (file)
@@ -49,6 +49,12 @@ class ScriptApiServer
                const std::string &password);
        bool setPassword(const std::string &playername,
                const std::string &password);
+
+       /* dynamic media handling */
+       u32 allocateDynamicMediaCallback(int f_idx);
+       void freeDynamicMediaCallback(u32 token);
+       void on_dynamic_media_added(u32 token, const char *playername);
+
 private:
        void getAuthHandler();
        void readPrivileges(int index, std::set<std::string> &result);
index 9866e0bc860581135338b518eb6db0710e653840..473faaa14430d7ea2484e12c7253e3311001bbd8 100644 (file)
@@ -453,29 +453,37 @@ int ModApiServer::l_sound_fade(lua_State *L)
 }
 
 // dynamic_add_media(filepath)
-int ModApiServer::l_dynamic_add_media_raw(lua_State *L)
+int ModApiServer::l_dynamic_add_media(lua_State *L)
 {
        NO_MAP_LOCK_REQUIRED;
 
        if (!getEnv(L))
                throw LuaError("Dynamic media cannot be added before server has started up");
+       Server *server = getServer(L);
 
-       std::string filepath = readParam<std::string>(L, 1);
-       CHECK_SECURE_PATH(L, filepath.c_str(), false);
+       std::string filepath;
+       std::string to_player;
+       bool ephemeral = false;
 
-       std::vector<RemotePlayer*> sent_to;
-       bool ok = getServer(L)->dynamicAddMedia(filepath, sent_to);
-       if (ok) {
-               // (see wrapper code in builtin)
-               lua_createtable(L, sent_to.size(), 0);
-               int i = 0;
-               for (RemotePlayer *player : sent_to) {
-                       lua_pushstring(L, player->getName());
-                       lua_rawseti(L, -2, ++i);
-               }
+       if (lua_istable(L, 1)) {
+               getstringfield(L, 1, "filepath", filepath);
+               getstringfield(L, 1, "to_player", to_player);
+               getboolfield(L, 1, "ephemeral", ephemeral);
        } else {
-               lua_pushboolean(L, false);
+               filepath = readParam<std::string>(L, 1);
        }
+       if (filepath.empty())
+               luaL_typerror(L, 1, "non-empty string");
+       luaL_checktype(L, 2, LUA_TFUNCTION);
+
+       CHECK_SECURE_PATH(L, filepath.c_str(), false);
+
+       u32 token = server->getScriptIface()->allocateDynamicMediaCallback(2);
+
+       bool ok = server->dynamicAddMedia(filepath, token, to_player, ephemeral);
+       if (!ok)
+               server->getScriptIface()->freeDynamicMediaCallback(token);
+       lua_pushboolean(L, ok);
 
        return 1;
 }
@@ -519,7 +527,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_raw);
+       API_FCT(dynamic_add_media);
 
        API_FCT(get_player_information);
        API_FCT(get_player_privs);
index fb7a851f46ac1f4b47bde80c23c9b2309e570c09..c688e494b9be0393c7765711bd5ebac18f4d7cbb 100644 (file)
@@ -71,7 +71,7 @@ class ModApiServer : public ModApiBase
        static int l_sound_fade(lua_State *L);
 
        // dynamic_add_media(filepath)
-       static int l_dynamic_add_media_raw(lua_State *L);
+       static int l_dynamic_add_media(lua_State *L);
 
        // get_player_privs(name, text)
        static int l_get_player_privs(lua_State *L);
index b96db120995d725c63f55064168e2dc40f81765a..1b5cbe395e1b6e9165f133dc417d288f18971d93 100644 (file)
@@ -665,6 +665,17 @@ void Server::AsyncRunStep(bool initial_step)
        } else {
                m_lag_gauge->increment(dtime/100);
        }
+
+       {
+               float &counter = m_step_pending_dyn_media_timer;
+               counter += dtime;
+               if (counter >= 5.0f) {
+                       stepPendingDynMediaCallbacks(counter);
+                       counter = 0;
+               }
+       }
+
+
 #if USE_CURL
        // send masterserver announce
        {
@@ -2527,6 +2538,8 @@ void Server::sendMediaAnnouncement(session_t peer_id, const std::string &lang_co
        std::string lang_suffix;
        lang_suffix.append(".").append(lang_code).append(".tr");
        for (const auto &i : m_media) {
+               if (i.second.no_announce)
+                       continue;
                if (str_ends_with(i.first, ".tr") && !str_ends_with(i.first, lang_suffix))
                        continue;
                media_sent++;
@@ -2535,6 +2548,8 @@ void Server::sendMediaAnnouncement(session_t peer_id, const std::string &lang_co
        pkt << media_sent;
 
        for (const auto &i : m_media) {
+               if (i.second.no_announce)
+                       continue;
                if (str_ends_with(i.first, ".tr") && !str_ends_with(i.first, lang_suffix))
                        continue;
                pkt << i.first << i.second.sha1_digest;
@@ -2553,11 +2568,9 @@ struct SendableMedia
        std::string path;
        std::string data;
 
-       SendableMedia(const std::string &name_="", const std::string &path_="",
-                     const std::string &data_=""):
-               name(name_),
-               path(path_),
-               data(data_)
+       SendableMedia(const std::string &name, const std::string &path,
+                       std::string &&data):
+               name(name), path(path), data(std::move(data))
        {}
 };
 
@@ -2584,40 +2597,19 @@ void Server::sendRequestedMedia(session_t peer_id,
                        continue;
                }
 
-               //TODO get path + name
-               std::string tpath = m_media[name].path;
+               const auto &m = m_media[name];
 
                // Read data
-               std::ifstream fis(tpath.c_str(), std::ios_base::binary);
-               if(!fis.good()){
-                       errorstream<<"Server::sendRequestedMedia(): Could not open \""
-                                       <<tpath<<"\" 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);
-                       file_size_bunch_total += len;
-                       if(fis.eof())
-                               break;
-                       if(!fis.good()) {
-                               bad = true;
-                               break;
-                       }
-               }
-               if (bad) {
-                       errorstream<<"Server::sendRequestedMedia(): Failed to read \""
-                                       <<name<<"\""<<std::endl;
+               std::string data;
+               if (!fs::ReadFile(m.path, data)) {
+                       errorstream << "Server::sendRequestedMedia(): Failed to read \""
+                                       << name << "\"" << std::endl;
                        continue;
                }
-               /*infostream<<"Server::sendRequestedMedia(): Loaded \""
-                               <<tname<<"\""<<std::endl;*/
+               file_size_bunch_total += data.size();
+
                // Put in list
-               file_bunches[file_bunches.size()-1].emplace_back(name, tpath, tmp_os.str());
+               file_bunches.back().emplace_back(name, m.path, std::move(data));
 
                // Start next bunch if got enough data
                if(file_size_bunch_total >= bytes_per_bunch) {
@@ -2660,6 +2652,33 @@ void Server::sendRequestedMedia(session_t peer_id,
        }
 }
 
+void Server::stepPendingDynMediaCallbacks(float dtime)
+{
+       MutexAutoLock lock(m_env_mutex);
+
+       for (auto it = m_pending_dyn_media.begin(); it != m_pending_dyn_media.end();) {
+               it->second.expiry_timer -= dtime;
+               bool del = it->second.waiting_players.empty() || it->second.expiry_timer < 0;
+
+               if (!del) {
+                       it++;
+                       continue;
+               }
+
+               const auto &name = it->second.filename;
+               if (!name.empty()) {
+                       assert(m_media.count(name));
+                       // if no_announce isn't set we're definitely deleting the wrong file!
+                       sanity_check(m_media[name].no_announce);
+
+                       fs::DeleteSingleFileOrEmptyDirectory(m_media[name].path);
+                       m_media.erase(name);
+               }
+               getScriptIface()->freeDynamicMediaCallback(it->first);
+               it = m_pending_dyn_media.erase(it);
+       }
+}
+
 void Server::SendMinimapModes(session_t peer_id,
                std::vector<MinimapMode> &modes, size_t wanted_mode)
 {
@@ -3457,14 +3476,18 @@ void Server::deleteParticleSpawner(const std::string &playername, u32 id)
        SendDeleteParticleSpawner(peer_id, id);
 }
 
-bool Server::dynamicAddMedia(const std::string &filepath,
-       std::vector<RemotePlayer*> &sent_to)
+bool Server::dynamicAddMedia(std::string filepath,
+       const u32 token, const std::string &to_player, bool ephemeral)
 {
        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;
+       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) {
+                       errorstream << "Server::dynamicAddMedia(): file \"" << filename
+                               << "\" already exists in media cache" << std::endl;
+                       return false;
+               }
        }
 
        // Load the file and add it to our media cache
@@ -3473,35 +3496,91 @@ bool Server::dynamicAddMedia(const std::string &filepath,
        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())
+                               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;
+
+               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;
+       }
+
        // Push file to existing clients
        NetworkPacket pkt(TOCLIENT_MEDIA_PUSH, 0);
-       pkt << raw_hash << filename << (bool) true;
-       pkt.putLongString(filedata);
+       pkt << raw_hash << filename << (bool)ephemeral;
+
+       NetworkPacket legacy_pkt = pkt;
 
+       // Newer clients get asked to fetch the file (asynchronous)
+       pkt << token;
+       // Older clients have an awful hack that just throws the data at them
+       legacy_pkt.putLongString(filedata);
+
+       std::unordered_set<session_t> delivered, waiting;
        m_clients.lock();
        for (auto &pair : m_clients.getClientList()) {
                if (pair.second->getState() < CS_DefinitionsSent)
                        continue;
-               if (pair.second->net_proto_version < 39)
+               const auto proto_ver = pair.second->net_proto_version;
+               if (proto_ver < 39)
                        continue;
 
-               if (auto player = m_env->getPlayer(pair.second->peer_id))
-                       sent_to.emplace_back(player);
-               /*
-                       FIXME: this is a very awful hack
-                       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(pair.second->peer_id, 1, &pkt, true);
-               m_clients.send(pair.second->peer_id, 0, &pkt, true);
+               const session_t peer_id = pair.second->peer_id;
+               if (!to_player.empty() && getPlayerName(peer_id) != to_player)
+                       continue;
+
+               if (proto_ver < 40) {
+                       delivered.emplace(peer_id);
+                       /*
+                               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 channels 1 and 0.
+                       */
+                       m_clients.send(peer_id, 1, &legacy_pkt, true);
+                       m_clients.send(peer_id, 0, &legacy_pkt, true);
+               } else {
+                       waiting.emplace(peer_id);
+                       Send(peer_id, &pkt);
+               }
        }
        m_clients.unlock();
 
+       // 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());
+       }
+
+       // Save all others in our pending state
+       auto &state = m_pending_dyn_media[token];
+       state.waiting_players = std::move(waiting);
+       // regardless of success throw away the callback after a while
+       state.expiry_timer = 60.0f;
+       if (ephemeral)
+               state.filename = filename;
+
        return true;
 }
 
index 9857215d09e98664772058ecfd51596b42672f91..7b16845afb0d5fcdbea8bcd8cc1de4dd9f21a464 100644 (file)
@@ -43,6 +43,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include <list>
 #include <map>
 #include <vector>
+#include <unordered_set>
 
 class ChatEvent;
 struct ChatEventChat;
@@ -81,12 +82,14 @@ enum ClientDeletionReason {
 struct MediaInfo
 {
        std::string path;
-       std::string sha1_digest;
+       std::string sha1_digest; // base64-encoded
+       bool no_announce; // true: not announced in TOCLIENT_ANNOUNCE_MEDIA (at player join)
 
        MediaInfo(const std::string &path_="",
                  const std::string &sha1_digest_=""):
                path(path_),
-               sha1_digest(sha1_digest_)
+               sha1_digest(sha1_digest_),
+               no_announce(false)
        {
        }
 };
@@ -197,6 +200,7 @@ class Server : public con::PeerHandler, public MapEventReceiver,
        void handleCommand_FirstSrp(NetworkPacket* pkt);
        void handleCommand_SrpBytesA(NetworkPacket* pkt);
        void handleCommand_SrpBytesM(NetworkPacket* pkt);
+       void handleCommand_HaveMedia(NetworkPacket *pkt);
 
        void ProcessData(NetworkPacket *pkt);
 
@@ -257,7 +261,8 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 
        void deleteParticleSpawner(const std::string &playername, u32 id);
 
-       bool dynamicAddMedia(const std::string &filepath, std::vector<RemotePlayer*> &sent_to);
+       bool dynamicAddMedia(std::string filepath, u32 token,
+               const std::string &to_player, bool ephemeral);
 
        ServerInventoryManager *getInventoryMgr() const { return m_inventory_mgr.get(); }
        void sendDetachedInventory(Inventory *inventory, const std::string &name, session_t peer_id);
@@ -395,6 +400,12 @@ class Server : public con::PeerHandler, public MapEventReceiver,
                        float m_timer = 0.0f;
        };
 
+       struct PendingDynamicMediaCallback {
+               std::string filename; // only set if media entry and file is to be deleted
+               float expiry_timer;
+               std::unordered_set<session_t> waiting_players;
+       };
+
        void init();
 
        void SendMovement(session_t peer_id);
@@ -466,6 +477,7 @@ class Server : public con::PeerHandler, public MapEventReceiver,
        void sendMediaAnnouncement(session_t peer_id, const std::string &lang_code);
        void sendRequestedMedia(session_t peer_id,
                        const std::vector<std::string> &tosend);
+       void stepPendingDynMediaCallbacks(float dtime);
 
        // Adds a ParticleSpawner on peer with peer_id (PEER_ID_INEXISTENT == all)
        void SendAddParticleSpawner(session_t peer_id, u16 protocol_version,
@@ -650,6 +662,10 @@ class Server : public con::PeerHandler, public MapEventReceiver,
        // media files known to server
        std::unordered_map<std::string, MediaInfo> m_media;
 
+       // pending dynamic media callbacks, clients inform the server when they have a file fetched
+       std::unordered_map<u32, PendingDynamicMediaCallback> m_pending_dyn_media;
+       float m_step_pending_dyn_media_timer = 0.0f;
+
        /*
                Sounds
        */