diff libervia/cli/cmd_file.py @ 4233:d01b8d002619

cli (call, file), frontends: implement webRTC data channel transfer: - file send/receive commands now supports webRTC transfer. In `send` command, the `--webrtc` flags is currenty used to activate it. - WebRTC related code have been factorized and moved to `libervia.frontends.tools.webrtc*` modules. rel 442
author Goffi <goffi@goffi.org>
date Sat, 06 Apr 2024 13:43:09 +0200
parents cd889f4771cb
children 79c8a70e1813
line wrap: on
line diff
--- a/libervia/cli/cmd_file.py	Sat Apr 06 12:59:50 2024 +0200
+++ b/libervia/cli/cmd_file.py	Sat Apr 06 13:43:09 2024 +0200
@@ -18,6 +18,11 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
+import asyncio
+from functools import partial
+import importlib
+import logging
+from typing import IO
 from . import base
 from . import xmlui_manager
 import sys
@@ -28,7 +33,7 @@
 from libervia.backend.tools.common import data_format
 from libervia.cli.constants import Const as C
 from libervia.cli import common
-from libervia.frontends.tools import jid
+from libervia.frontends.tools import aio, jid
 from libervia.backend.tools.common.ansi import ANSI as A
 from libervia.backend.tools.common import utils
 from urllib.parse import urlparse
@@ -81,6 +86,11 @@
             action="store_true",
             help=_("end-to-end encrypt the file transfer")
         )
+        self.parser.add_argument(
+            "--webrtc",
+            action="store_true",
+            help=_("Use WebRTC Data Channel transport.")
+        )
 
     async def on_progress_started(self, metadata):
         self.disp(_("File copy started"), 2)
@@ -94,12 +104,8 @@
         else:
             self.disp(_("Error while sending file: {}").format(error_msg), error=True)
 
-    async def got_id(self, data, file_):
-        """Called when a progress id has been received
-
-        @param pid(unicode): progress id
-        @param file_(str): file path
-        """
+    async def got_id(self, data: dict):
+        """Called when a progress id has been received"""
         # FIXME: this show progress only for last progress_id
         self.disp(_("File request sent to {jid}".format(jid=self.args.jid)), 1)
         try:
@@ -109,7 +115,9 @@
             self.disp(_("Can't send file to {jid}".format(jid=self.args.jid)), error=True)
             self.host.quit(2)
 
+
     async def start(self):
+        file_ = None
         for file_ in self.args.files:
             if not os.path.exists(file_):
                 self.disp(
@@ -148,28 +156,41 @@
                     bz2.add(file_)
                 bz2.close()
                 self.disp(_("Done !"), 1)
+                self.args.files = [buf.name]
+                if not self.args.name:
+                    self.args.name = archive_name
 
+        for file_ in self.args.files:
+            file_path = Path(file_)
+            if self.args.webrtc:
+                root_logger = logging.getLogger()
+                # we don't want any formatting for messages from webrtc
+                for handler in root_logger.handlers:
+                    handler.setFormatter(None)
+                if self.verbosity == 0:
+                    root_logger.setLevel(logging.ERROR)
+                if self.verbosity >= 1:
+                    root_logger.setLevel(logging.WARNING)
+                if self.verbosity >= 2:
+                    root_logger.setLevel(logging.DEBUG)
+                from libervia.frontends.tools.webrtc_file import WebRTCFileSender
+                aio.install_glib_asyncio_iteration()
+                file_sender = WebRTCFileSender(
+                    self.host.bridge,
+                    self.profile,
+                    on_call_start_cb=self.got_id,
+                    end_call_cb=self.host.a_quit
+                )
+                await file_sender.send_file_webrtc(
+                    file_path,
+                    self.args.jid,
+                    self.args.name
+                )
+            else:
                 try:
-                    send_data = await self.host.bridge.file_send(
+                    send_data_raw = await self.host.bridge.file_send(
                         self.args.jid,
-                        buf.name,
-                        self.args.name or archive_name,
-                        "",
-                        data_format.serialise(extra),
-                        self.profile,
-                    )
-                except Exception as e:
-                    self.disp(f"can't send file: {e}", error=True)
-                    self.host.quit(C.EXIT_BRIDGE_ERRBACK)
-                else:
-                    await self.got_id(send_data, file_)
-        else:
-            for file_ in self.args.files:
-                path = os.path.abspath(file_)
-                try:
-                    send_data = await self.host.bridge.file_send(
-                        self.args.jid,
-                        path,
+                        str(file_path.absolute()),
                         self.args.name,
                         "",
                         data_format.serialise(extra),
@@ -179,7 +200,8 @@
                     self.disp(f"can't send file {file_!r}: {e}", error=True)
                     self.host.quit(C.EXIT_BRIDGE_ERRBACK)
                 else:
-                    await self.got_id(send_data, file_)
+                    send_data = data_format.deserialise(send_data_raw)
+                    await self.got_id(send_data)
 
 
 class Request(base.CommandBase):
@@ -301,7 +323,7 @@
             use_verbose=True,
             help=_("wait for a file to be sent by a contact"),
         )
-        self._overwrite_refused = False  # True when one overwrite as already been refused
+        self._overwrite_refused = False  # True when one overwrite has already been refused
         self.action_callbacks = {
             C.META_TYPE_CONFIRM: self.on_confirm_action,
             C.META_TYPE_FILE: self.on_file_action,
@@ -326,7 +348,7 @@
             "--force",
             action="store_true",
             help=_(
-                "force overwritting of existing files (/!\\ name is choosed by sender)"
+                "force overwriting of existing files (/!\\ name is choosed by sender)"
             ),
         )
         self.parser.add_argument(
@@ -354,6 +376,58 @@
     async def on_progress_error(self, e):
         self.disp(_("Error while receiving file: {e}").format(e=e), error=True)
 
+    async def _on_webrtc_close(self) -> None:
+        if not self.args.multiple:
+            await self.host.a_quit()
+
+    async def on_webrtc_file(
+        self,
+        from_jid: jid.JID,
+        session_id: str,
+        file_data: dict
+    ) -> None:
+        from libervia.frontends.tools.webrtc_file import WebRTCFileReceiver
+        aio.install_glib_asyncio_iteration()
+        root_logger = logging.getLogger()
+        # we don't want any formatting for messages from webrtc
+        for handler in root_logger.handlers:
+            handler.setFormatter(None)
+        if self.verbosity == 0:
+            root_logger.setLevel(logging.ERROR)
+        if self.verbosity >= 1:
+            root_logger.setLevel(logging.WARNING)
+        if self.verbosity >= 2:
+            root_logger.setLevel(logging.DEBUG)
+
+        dest_path = Path(self.path)
+
+        if dest_path.is_dir():
+            filename = file_data.get("name", "unammed_file")
+            dest_path /= filename
+            if dest_path.exists() and not self.args.force:
+                self.host.disp(
+                    "Destination file already exists",
+                    error=True
+                )
+                aio.run_from_thread(
+                    self.host.a_quit, C.EXIT_ERROR, loop=self.host.loop.loop
+                )
+                return
+
+        file_receiver = WebRTCFileReceiver(
+            self.host.bridge,
+            self.profile,
+            on_close_cb=self._on_webrtc_close
+        )
+
+        await file_receiver.receive_file_webrtc(
+            from_jid,
+            session_id,
+            dest_path,
+            file_data
+        )
+
+
     def get_xmlui_id(self, action_data):
         # FIXME: we temporarily use ElementTree, but a real XMLUI managing module
         #        should be available in the futur
@@ -376,12 +450,16 @@
         if action_data.get("subtype") != C.META_TYPE_FILE:
             self.disp(_("Ignoring confirm dialog unrelated to file."), 1)
             return
+        try:
+            from_jid = jid.JID(action_data["from_jid"])
+        except KeyError:
+            self.disp(_("Ignoring action without from_jid data"), 1)
+            return
 
-        # we always accept preflight confirmation dialog, as for now a second dialog is
-        # always sent
-        # FIXME: real confirmation should be done here, and second dialog should not be
-        #   sent from backend
-        xmlui_data = {"answer": C.BOOL_TRUE}
+        # We accept if no JID is specified (meaning "accept all") or if the sender is
+        # explicitly specified.
+        answer = not self.bare_jids or from_jid.bare in self.bare_jids
+        xmlui_data = {"answer": C.bool_const(answer)}
         await self.host.bridge.action_launch(
             xmlui_id, data_format.serialise(xmlui_data), profile_key=profile
         )
@@ -401,7 +479,10 @@
             self.disp(_("ignoring action without progress id"), 1)
             return
 
-        if not self.bare_jids or from_jid.bare in self.bare_jids:
+        webrtc = action_data.get("webrtc", False)
+        file_accepted = action_data.get("file_accepted", False)
+
+        if file_accepted or not self.bare_jids or from_jid.bare in self.bare_jids:
             if self._overwrite_refused:
                 self.disp(_("File refused because overwrite is needed"), error=True)
                 await self.host.bridge.action_launch(
@@ -410,7 +491,22 @@
                 )
                 return self.host.quit_from_signal(2)
             await self.set_progress_id(progress_id)
-            xmlui_data = {"path": self.path}
+            if webrtc:
+                xmlui_data = {"answer": C.BOOL_TRUE}
+                file_data = action_data.get("file_data") or {}
+                try:
+                    session_id = action_data["session_id"]
+                except KeyError:
+                    self.disp(_("ignoring action without session id"), 1)
+                    return
+                await self.on_webrtc_file(
+                    from_jid,
+                    session_id,
+                    file_data
+                )
+
+            else:
+                xmlui_data = {"path": self.path}
             await self.host.bridge.action_launch(
                 xmlui_id, data_format.serialise(xmlui_data), profile_key=profile
             )
@@ -438,7 +534,9 @@
                 xmlui_id, data_format.serialise(xmlui_data), profile_key=profile
             )
 
-    async def on_not_in_roster_action(self, action_data, action_id, security_limit, profile):
+    async def on_not_in_roster_action(
+        self, action_data, action_id, security_limit, profile
+    ):
         xmlui_id = self.get_xmlui_id(action_data)
         if xmlui_id is None:
             return self.host.quit_from_signal(1)
@@ -618,12 +716,8 @@
     async def on_progress_error(self, error_msg):
         self.disp(_("Error while uploading file: {}").format(error_msg), error=True)
 
-    async def got_id(self, data, file_):
-        """Called when a progress id has been received
-
-        @param pid(unicode): progress id
-        @param file_(str): file path
-        """
+    async def got_id(self, data):
+        """Called when a progress id has been received"""
         try:
             await self.set_progress_id(data["progress"])
         except KeyError:
@@ -669,7 +763,7 @@
             self.disp(f"error while trying to upload a file: {e}", error=True)
             self.host.quit(C.EXIT_BRIDGE_ERRBACK)
         else:
-            await self.got_id(upload_data, file_)
+            await self.got_id(upload_data)
 
 
 class ShareAffiliationsSet(base.CommandBase):