Mercurial > libervia-backend
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):