Mercurial > libervia-backend
comparison 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 |
comparison
equal
deleted
inserted
replaced
4232:0fbe5c605eb6 | 4233:d01b8d002619 |
---|---|
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 17 # You should have received a copy of the GNU Affero General Public License |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 | 20 |
21 import asyncio | |
22 from functools import partial | |
23 import importlib | |
24 import logging | |
25 from typing import IO | |
21 from . import base | 26 from . import base |
22 from . import xmlui_manager | 27 from . import xmlui_manager |
23 import sys | 28 import sys |
24 import os | 29 import os |
25 import os.path | 30 import os.path |
26 import tarfile | 31 import tarfile |
27 from libervia.backend.core.i18n import _ | 32 from libervia.backend.core.i18n import _ |
28 from libervia.backend.tools.common import data_format | 33 from libervia.backend.tools.common import data_format |
29 from libervia.cli.constants import Const as C | 34 from libervia.cli.constants import Const as C |
30 from libervia.cli import common | 35 from libervia.cli import common |
31 from libervia.frontends.tools import jid | 36 from libervia.frontends.tools import aio, jid |
32 from libervia.backend.tools.common.ansi import ANSI as A | 37 from libervia.backend.tools.common.ansi import ANSI as A |
33 from libervia.backend.tools.common import utils | 38 from libervia.backend.tools.common import utils |
34 from urllib.parse import urlparse | 39 from urllib.parse import urlparse |
35 from pathlib import Path | 40 from pathlib import Path |
36 import tempfile | 41 import tempfile |
79 "-e", | 84 "-e", |
80 "--encrypt", | 85 "--encrypt", |
81 action="store_true", | 86 action="store_true", |
82 help=_("end-to-end encrypt the file transfer") | 87 help=_("end-to-end encrypt the file transfer") |
83 ) | 88 ) |
89 self.parser.add_argument( | |
90 "--webrtc", | |
91 action="store_true", | |
92 help=_("Use WebRTC Data Channel transport.") | |
93 ) | |
84 | 94 |
85 async def on_progress_started(self, metadata): | 95 async def on_progress_started(self, metadata): |
86 self.disp(_("File copy started"), 2) | 96 self.disp(_("File copy started"), 2) |
87 | 97 |
88 async def on_progress_finished(self, metadata): | 98 async def on_progress_finished(self, metadata): |
92 if error_msg == C.PROGRESS_ERROR_DECLINED: | 102 if error_msg == C.PROGRESS_ERROR_DECLINED: |
93 self.disp(_("The file has been refused by your contact")) | 103 self.disp(_("The file has been refused by your contact")) |
94 else: | 104 else: |
95 self.disp(_("Error while sending file: {}").format(error_msg), error=True) | 105 self.disp(_("Error while sending file: {}").format(error_msg), error=True) |
96 | 106 |
97 async def got_id(self, data, file_): | 107 async def got_id(self, data: dict): |
98 """Called when a progress id has been received | 108 """Called when a progress id has been received""" |
99 | |
100 @param pid(unicode): progress id | |
101 @param file_(str): file path | |
102 """ | |
103 # FIXME: this show progress only for last progress_id | 109 # FIXME: this show progress only for last progress_id |
104 self.disp(_("File request sent to {jid}".format(jid=self.args.jid)), 1) | 110 self.disp(_("File request sent to {jid}".format(jid=self.args.jid)), 1) |
105 try: | 111 try: |
106 await self.set_progress_id(data["progress"]) | 112 await self.set_progress_id(data["progress"]) |
107 except KeyError: | 113 except KeyError: |
108 # TODO: if 'xmlui' key is present, manage xmlui message display | 114 # TODO: if 'xmlui' key is present, manage xmlui message display |
109 self.disp(_("Can't send file to {jid}".format(jid=self.args.jid)), error=True) | 115 self.disp(_("Can't send file to {jid}".format(jid=self.args.jid)), error=True) |
110 self.host.quit(2) | 116 self.host.quit(2) |
111 | 117 |
118 | |
112 async def start(self): | 119 async def start(self): |
120 file_ = None | |
113 for file_ in self.args.files: | 121 for file_ in self.args.files: |
114 if not os.path.exists(file_): | 122 if not os.path.exists(file_): |
115 self.disp( | 123 self.disp( |
116 _("file {file_} doesn't exist!").format(file_=repr(file_)), error=True | 124 _("file {file_} doesn't exist!").format(file_=repr(file_)), error=True |
117 ) | 125 ) |
146 for file_ in self.args.files: | 154 for file_ in self.args.files: |
147 self.disp(_("Adding {}").format(file_), 1) | 155 self.disp(_("Adding {}").format(file_), 1) |
148 bz2.add(file_) | 156 bz2.add(file_) |
149 bz2.close() | 157 bz2.close() |
150 self.disp(_("Done !"), 1) | 158 self.disp(_("Done !"), 1) |
151 | 159 self.args.files = [buf.name] |
160 if not self.args.name: | |
161 self.args.name = archive_name | |
162 | |
163 for file_ in self.args.files: | |
164 file_path = Path(file_) | |
165 if self.args.webrtc: | |
166 root_logger = logging.getLogger() | |
167 # we don't want any formatting for messages from webrtc | |
168 for handler in root_logger.handlers: | |
169 handler.setFormatter(None) | |
170 if self.verbosity == 0: | |
171 root_logger.setLevel(logging.ERROR) | |
172 if self.verbosity >= 1: | |
173 root_logger.setLevel(logging.WARNING) | |
174 if self.verbosity >= 2: | |
175 root_logger.setLevel(logging.DEBUG) | |
176 from libervia.frontends.tools.webrtc_file import WebRTCFileSender | |
177 aio.install_glib_asyncio_iteration() | |
178 file_sender = WebRTCFileSender( | |
179 self.host.bridge, | |
180 self.profile, | |
181 on_call_start_cb=self.got_id, | |
182 end_call_cb=self.host.a_quit | |
183 ) | |
184 await file_sender.send_file_webrtc( | |
185 file_path, | |
186 self.args.jid, | |
187 self.args.name | |
188 ) | |
189 else: | |
152 try: | 190 try: |
153 send_data = await self.host.bridge.file_send( | 191 send_data_raw = await self.host.bridge.file_send( |
154 self.args.jid, | 192 self.args.jid, |
155 buf.name, | 193 str(file_path.absolute()), |
156 self.args.name or archive_name, | |
157 "", | |
158 data_format.serialise(extra), | |
159 self.profile, | |
160 ) | |
161 except Exception as e: | |
162 self.disp(f"can't send file: {e}", error=True) | |
163 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
164 else: | |
165 await self.got_id(send_data, file_) | |
166 else: | |
167 for file_ in self.args.files: | |
168 path = os.path.abspath(file_) | |
169 try: | |
170 send_data = await self.host.bridge.file_send( | |
171 self.args.jid, | |
172 path, | |
173 self.args.name, | 194 self.args.name, |
174 "", | 195 "", |
175 data_format.serialise(extra), | 196 data_format.serialise(extra), |
176 self.profile, | 197 self.profile, |
177 ) | 198 ) |
178 except Exception as e: | 199 except Exception as e: |
179 self.disp(f"can't send file {file_!r}: {e}", error=True) | 200 self.disp(f"can't send file {file_!r}: {e}", error=True) |
180 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | 201 self.host.quit(C.EXIT_BRIDGE_ERRBACK) |
181 else: | 202 else: |
182 await self.got_id(send_data, file_) | 203 send_data = data_format.deserialise(send_data_raw) |
204 await self.got_id(send_data) | |
183 | 205 |
184 | 206 |
185 class Request(base.CommandBase): | 207 class Request(base.CommandBase): |
186 def __init__(self, host): | 208 def __init__(self, host): |
187 super(Request, self).__init__( | 209 super(Request, self).__init__( |
299 "receive", | 321 "receive", |
300 use_progress=True, | 322 use_progress=True, |
301 use_verbose=True, | 323 use_verbose=True, |
302 help=_("wait for a file to be sent by a contact"), | 324 help=_("wait for a file to be sent by a contact"), |
303 ) | 325 ) |
304 self._overwrite_refused = False # True when one overwrite as already been refused | 326 self._overwrite_refused = False # True when one overwrite has already been refused |
305 self.action_callbacks = { | 327 self.action_callbacks = { |
306 C.META_TYPE_CONFIRM: self.on_confirm_action, | 328 C.META_TYPE_CONFIRM: self.on_confirm_action, |
307 C.META_TYPE_FILE: self.on_file_action, | 329 C.META_TYPE_FILE: self.on_file_action, |
308 C.META_TYPE_OVERWRITE: self.on_overwrite_action, | 330 C.META_TYPE_OVERWRITE: self.on_overwrite_action, |
309 C.META_TYPE_NOT_IN_ROSTER_LEAK: self.on_not_in_roster_action, | 331 C.META_TYPE_NOT_IN_ROSTER_LEAK: self.on_not_in_roster_action, |
324 self.parser.add_argument( | 346 self.parser.add_argument( |
325 "-f", | 347 "-f", |
326 "--force", | 348 "--force", |
327 action="store_true", | 349 action="store_true", |
328 help=_( | 350 help=_( |
329 "force overwritting of existing files (/!\\ name is choosed by sender)" | 351 "force overwriting of existing files (/!\\ name is choosed by sender)" |
330 ), | 352 ), |
331 ) | 353 ) |
332 self.parser.add_argument( | 354 self.parser.add_argument( |
333 "--path", | 355 "--path", |
334 default=".", | 356 default=".", |
352 self.disp(_("hash can't be verified"), 1) | 374 self.disp(_("hash can't be verified"), 1) |
353 | 375 |
354 async def on_progress_error(self, e): | 376 async def on_progress_error(self, e): |
355 self.disp(_("Error while receiving file: {e}").format(e=e), error=True) | 377 self.disp(_("Error while receiving file: {e}").format(e=e), error=True) |
356 | 378 |
379 async def _on_webrtc_close(self) -> None: | |
380 if not self.args.multiple: | |
381 await self.host.a_quit() | |
382 | |
383 async def on_webrtc_file( | |
384 self, | |
385 from_jid: jid.JID, | |
386 session_id: str, | |
387 file_data: dict | |
388 ) -> None: | |
389 from libervia.frontends.tools.webrtc_file import WebRTCFileReceiver | |
390 aio.install_glib_asyncio_iteration() | |
391 root_logger = logging.getLogger() | |
392 # we don't want any formatting for messages from webrtc | |
393 for handler in root_logger.handlers: | |
394 handler.setFormatter(None) | |
395 if self.verbosity == 0: | |
396 root_logger.setLevel(logging.ERROR) | |
397 if self.verbosity >= 1: | |
398 root_logger.setLevel(logging.WARNING) | |
399 if self.verbosity >= 2: | |
400 root_logger.setLevel(logging.DEBUG) | |
401 | |
402 dest_path = Path(self.path) | |
403 | |
404 if dest_path.is_dir(): | |
405 filename = file_data.get("name", "unammed_file") | |
406 dest_path /= filename | |
407 if dest_path.exists() and not self.args.force: | |
408 self.host.disp( | |
409 "Destination file already exists", | |
410 error=True | |
411 ) | |
412 aio.run_from_thread( | |
413 self.host.a_quit, C.EXIT_ERROR, loop=self.host.loop.loop | |
414 ) | |
415 return | |
416 | |
417 file_receiver = WebRTCFileReceiver( | |
418 self.host.bridge, | |
419 self.profile, | |
420 on_close_cb=self._on_webrtc_close | |
421 ) | |
422 | |
423 await file_receiver.receive_file_webrtc( | |
424 from_jid, | |
425 session_id, | |
426 dest_path, | |
427 file_data | |
428 ) | |
429 | |
430 | |
357 def get_xmlui_id(self, action_data): | 431 def get_xmlui_id(self, action_data): |
358 # FIXME: we temporarily use ElementTree, but a real XMLUI managing module | 432 # FIXME: we temporarily use ElementTree, but a real XMLUI managing module |
359 # should be available in the futur | 433 # should be available in the futur |
360 # TODO: XMLUI module | 434 # TODO: XMLUI module |
361 try: | 435 try: |
374 if xmlui_id is None: | 448 if xmlui_id is None: |
375 return self.host.quit_from_signal(1) | 449 return self.host.quit_from_signal(1) |
376 if action_data.get("subtype") != C.META_TYPE_FILE: | 450 if action_data.get("subtype") != C.META_TYPE_FILE: |
377 self.disp(_("Ignoring confirm dialog unrelated to file."), 1) | 451 self.disp(_("Ignoring confirm dialog unrelated to file."), 1) |
378 return | 452 return |
379 | 453 try: |
380 # we always accept preflight confirmation dialog, as for now a second dialog is | 454 from_jid = jid.JID(action_data["from_jid"]) |
381 # always sent | 455 except KeyError: |
382 # FIXME: real confirmation should be done here, and second dialog should not be | 456 self.disp(_("Ignoring action without from_jid data"), 1) |
383 # sent from backend | 457 return |
384 xmlui_data = {"answer": C.BOOL_TRUE} | 458 |
459 # We accept if no JID is specified (meaning "accept all") or if the sender is | |
460 # explicitly specified. | |
461 answer = not self.bare_jids or from_jid.bare in self.bare_jids | |
462 xmlui_data = {"answer": C.bool_const(answer)} | |
385 await self.host.bridge.action_launch( | 463 await self.host.bridge.action_launch( |
386 xmlui_id, data_format.serialise(xmlui_data), profile_key=profile | 464 xmlui_id, data_format.serialise(xmlui_data), profile_key=profile |
387 ) | 465 ) |
388 | 466 |
389 async def on_file_action(self, action_data, action_id, security_limit, profile): | 467 async def on_file_action(self, action_data, action_id, security_limit, profile): |
399 progress_id = action_data["progress_id"] | 477 progress_id = action_data["progress_id"] |
400 except KeyError: | 478 except KeyError: |
401 self.disp(_("ignoring action without progress id"), 1) | 479 self.disp(_("ignoring action without progress id"), 1) |
402 return | 480 return |
403 | 481 |
404 if not self.bare_jids or from_jid.bare in self.bare_jids: | 482 webrtc = action_data.get("webrtc", False) |
483 file_accepted = action_data.get("file_accepted", False) | |
484 | |
485 if file_accepted or not self.bare_jids or from_jid.bare in self.bare_jids: | |
405 if self._overwrite_refused: | 486 if self._overwrite_refused: |
406 self.disp(_("File refused because overwrite is needed"), error=True) | 487 self.disp(_("File refused because overwrite is needed"), error=True) |
407 await self.host.bridge.action_launch( | 488 await self.host.bridge.action_launch( |
408 xmlui_id, data_format.serialise({"cancelled": C.BOOL_TRUE}), | 489 xmlui_id, data_format.serialise({"cancelled": C.BOOL_TRUE}), |
409 profile_key=profile | 490 profile_key=profile |
410 ) | 491 ) |
411 return self.host.quit_from_signal(2) | 492 return self.host.quit_from_signal(2) |
412 await self.set_progress_id(progress_id) | 493 await self.set_progress_id(progress_id) |
413 xmlui_data = {"path": self.path} | 494 if webrtc: |
495 xmlui_data = {"answer": C.BOOL_TRUE} | |
496 file_data = action_data.get("file_data") or {} | |
497 try: | |
498 session_id = action_data["session_id"] | |
499 except KeyError: | |
500 self.disp(_("ignoring action without session id"), 1) | |
501 return | |
502 await self.on_webrtc_file( | |
503 from_jid, | |
504 session_id, | |
505 file_data | |
506 ) | |
507 | |
508 else: | |
509 xmlui_data = {"path": self.path} | |
414 await self.host.bridge.action_launch( | 510 await self.host.bridge.action_launch( |
415 xmlui_id, data_format.serialise(xmlui_data), profile_key=profile | 511 xmlui_id, data_format.serialise(xmlui_data), profile_key=profile |
416 ) | 512 ) |
417 | 513 |
418 async def on_overwrite_action(self, action_data, action_id, security_limit, profile): | 514 async def on_overwrite_action(self, action_data, action_id, security_limit, profile): |
436 xmlui_data = {"answer": C.bool_const(self.args.force)} | 532 xmlui_data = {"answer": C.bool_const(self.args.force)} |
437 await self.host.bridge.action_launch( | 533 await self.host.bridge.action_launch( |
438 xmlui_id, data_format.serialise(xmlui_data), profile_key=profile | 534 xmlui_id, data_format.serialise(xmlui_data), profile_key=profile |
439 ) | 535 ) |
440 | 536 |
441 async def on_not_in_roster_action(self, action_data, action_id, security_limit, profile): | 537 async def on_not_in_roster_action( |
538 self, action_data, action_id, security_limit, profile | |
539 ): | |
442 xmlui_id = self.get_xmlui_id(action_data) | 540 xmlui_id = self.get_xmlui_id(action_data) |
443 if xmlui_id is None: | 541 if xmlui_id is None: |
444 return self.host.quit_from_signal(1) | 542 return self.host.quit_from_signal(1) |
445 try: | 543 try: |
446 from_jid = jid.JID(action_data["from_jid"]) | 544 from_jid = jid.JID(action_data["from_jid"]) |
616 self.disp(url) | 714 self.disp(url) |
617 | 715 |
618 async def on_progress_error(self, error_msg): | 716 async def on_progress_error(self, error_msg): |
619 self.disp(_("Error while uploading file: {}").format(error_msg), error=True) | 717 self.disp(_("Error while uploading file: {}").format(error_msg), error=True) |
620 | 718 |
621 async def got_id(self, data, file_): | 719 async def got_id(self, data): |
622 """Called when a progress id has been received | 720 """Called when a progress id has been received""" |
623 | |
624 @param pid(unicode): progress id | |
625 @param file_(str): file path | |
626 """ | |
627 try: | 721 try: |
628 await self.set_progress_id(data["progress"]) | 722 await self.set_progress_id(data["progress"]) |
629 except KeyError: | 723 except KeyError: |
630 if "xmlui" in data: | 724 if "xmlui" in data: |
631 ui = xmlui_manager.create(self.host, data["xmlui"]) | 725 ui = xmlui_manager.create(self.host, data["xmlui"]) |
667 ) | 761 ) |
668 except Exception as e: | 762 except Exception as e: |
669 self.disp(f"error while trying to upload a file: {e}", error=True) | 763 self.disp(f"error while trying to upload a file: {e}", error=True) |
670 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | 764 self.host.quit(C.EXIT_BRIDGE_ERRBACK) |
671 else: | 765 else: |
672 await self.got_id(upload_data, file_) | 766 await self.got_id(upload_data) |
673 | 767 |
674 | 768 |
675 class ShareAffiliationsSet(base.CommandBase): | 769 class ShareAffiliationsSet(base.CommandBase): |
676 def __init__(self, host): | 770 def __init__(self, host): |
677 super(ShareAffiliationsSet, self).__init__( | 771 super(ShareAffiliationsSet, self).__init__( |