- 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
1220 lines
40 KiB
Python
1220 lines
40 KiB
Python
#!/usr/bin/env python3
|
|
|
|
|
|
# Libervia CLI
|
|
# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# 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
|
|
import os
|
|
import os.path
|
|
import tarfile
|
|
from libervia.backend.core.i18n import _
|
|
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 aio, jid
|
|
from libervia.backend.tools.common.ansi import ANSI as A
|
|
from libervia.backend.tools.common import utils
|
|
from urllib.parse import urlparse
|
|
from pathlib import Path
|
|
import tempfile
|
|
import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI
|
|
import json
|
|
|
|
__commands__ = ["File"]
|
|
DEFAULT_DEST = "downloaded_file"
|
|
|
|
|
|
class Send(base.CommandBase):
|
|
def __init__(self, host):
|
|
super(Send, self).__init__(
|
|
host,
|
|
"send",
|
|
use_progress=True,
|
|
use_verbose=True,
|
|
help=_("send a file to a contact"),
|
|
)
|
|
|
|
def add_parser_options(self):
|
|
self.parser.add_argument(
|
|
"files", type=str, nargs="+", metavar="file", help=_("a list of file")
|
|
)
|
|
self.parser.add_argument("jid", help=_("the destination jid"))
|
|
self.parser.add_argument(
|
|
"-b", "--bz2", action="store_true", help=_("make a bzip2 tarball")
|
|
)
|
|
self.parser.add_argument(
|
|
"-d",
|
|
"--path",
|
|
help=("path to the directory where the file must be stored"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-N",
|
|
"--namespace",
|
|
help=("namespace of the file"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-n",
|
|
"--name",
|
|
default="",
|
|
help=("name to use (DEFAULT: use source file name)"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-e",
|
|
"--encrypt",
|
|
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)
|
|
|
|
async def on_progress_finished(self, metadata):
|
|
self.disp(_("File sent successfully"), 2)
|
|
|
|
async def on_progress_error(self, error_msg):
|
|
if error_msg == C.PROGRESS_ERROR_DECLINED:
|
|
self.disp(_("The file has been refused by your contact"))
|
|
else:
|
|
self.disp(_("Error while sending file: {}").format(error_msg), error=True)
|
|
|
|
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:
|
|
await self.set_progress_id(data["progress"])
|
|
except KeyError:
|
|
# TODO: if 'xmlui' key is present, manage xmlui message display
|
|
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(
|
|
_("file {file_} doesn't exist!").format(file_=repr(file_)), error=True
|
|
)
|
|
self.host.quit(C.EXIT_BAD_ARG)
|
|
if not self.args.bz2 and os.path.isdir(file_):
|
|
self.disp(
|
|
_(
|
|
"{file_} is a dir! Please send files inside or use compression"
|
|
).format(file_=repr(file_))
|
|
)
|
|
self.host.quit(C.EXIT_BAD_ARG)
|
|
|
|
extra = {}
|
|
if self.args.path:
|
|
extra["path"] = self.args.path
|
|
if self.args.namespace:
|
|
extra["namespace"] = self.args.namespace
|
|
if self.args.encrypt:
|
|
extra["encrypted"] = True
|
|
|
|
if self.args.bz2:
|
|
with tempfile.NamedTemporaryFile("wb", delete=False) as buf:
|
|
self.host.add_on_quit_callback(os.unlink, buf.name)
|
|
self.disp(_("bz2 is an experimental option, use with caution"))
|
|
# FIXME: check free space
|
|
self.disp(_("Starting compression, please wait..."))
|
|
sys.stdout.flush()
|
|
bz2 = tarfile.open(mode="w:bz2", fileobj=buf)
|
|
archive_name = "{}.tar.bz2".format(
|
|
os.path.basename(self.args.files[0]) or "compressed_files"
|
|
)
|
|
for file_ in self.args.files:
|
|
self.disp(_("Adding {}").format(file_), 1)
|
|
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_raw = await self.host.bridge.file_send(
|
|
self.args.jid,
|
|
str(file_path.absolute()),
|
|
self.args.name,
|
|
"",
|
|
data_format.serialise(extra),
|
|
self.profile,
|
|
)
|
|
except Exception as e:
|
|
self.disp(f"can't send file {file_!r}: {e}", error=True)
|
|
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
|
else:
|
|
send_data = data_format.deserialise(send_data_raw)
|
|
await self.got_id(send_data)
|
|
|
|
|
|
class Request(base.CommandBase):
|
|
def __init__(self, host):
|
|
super(Request, self).__init__(
|
|
host,
|
|
"request",
|
|
use_progress=True,
|
|
use_verbose=True,
|
|
help=_("request a file from a contact"),
|
|
)
|
|
|
|
@property
|
|
def filename(self):
|
|
return self.args.name or self.args.hash or "output"
|
|
|
|
def add_parser_options(self):
|
|
self.parser.add_argument("jid", help=_("the destination jid"))
|
|
self.parser.add_argument(
|
|
"-D",
|
|
"--dest",
|
|
help=_(
|
|
"destination path where the file will be saved (default: "
|
|
"[current_dir]/[name|hash])"
|
|
),
|
|
)
|
|
self.parser.add_argument(
|
|
"-n",
|
|
"--name",
|
|
default="",
|
|
help=_("name of the file"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-H",
|
|
"--hash",
|
|
default="",
|
|
help=_("hash of the file"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-a",
|
|
"--hash-algo",
|
|
default="sha-256",
|
|
help=_("hash algorithm use for --hash (default: sha-256)"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-d",
|
|
"--path",
|
|
help=("path to the directory containing the file"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-N",
|
|
"--namespace",
|
|
help=("namespace of the file"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-f",
|
|
"--force",
|
|
action="store_true",
|
|
help=_("overwrite existing file without confirmation"),
|
|
)
|
|
|
|
async def on_progress_started(self, metadata):
|
|
self.disp(_("File copy started"), 2)
|
|
|
|
async def on_progress_finished(self, metadata):
|
|
self.disp(_("File received successfully"), 2)
|
|
|
|
async def on_progress_error(self, error_msg):
|
|
if error_msg == C.PROGRESS_ERROR_DECLINED:
|
|
self.disp(_("The file request has been refused"))
|
|
else:
|
|
self.disp(_("Error while requesting file: {}").format(error_msg), error=True)
|
|
|
|
async def start(self):
|
|
if not self.args.name and not self.args.hash:
|
|
self.parser.error(_("at least one of --name or --hash must be provided"))
|
|
if self.args.dest:
|
|
path = os.path.abspath(os.path.expanduser(self.args.dest))
|
|
if os.path.isdir(path):
|
|
path = os.path.join(path, self.filename)
|
|
else:
|
|
path = os.path.abspath(self.filename)
|
|
|
|
if os.path.exists(path) and not self.args.force:
|
|
message = _("File {path} already exists! Do you want to overwrite?").format(
|
|
path=path
|
|
)
|
|
await self.host.confirm_or_quit(message, _("file request cancelled"))
|
|
|
|
self.full_dest_jid = await self.host.get_full_jid(self.args.jid)
|
|
extra = {}
|
|
if self.args.path:
|
|
extra["path"] = self.args.path
|
|
if self.args.namespace:
|
|
extra["namespace"] = self.args.namespace
|
|
try:
|
|
progress_id = await self.host.bridge.file_jingle_request(
|
|
self.full_dest_jid,
|
|
path,
|
|
self.args.name,
|
|
self.args.hash,
|
|
self.args.hash_algo if self.args.hash else "",
|
|
extra,
|
|
self.profile,
|
|
)
|
|
except Exception as e:
|
|
self.disp(msg=_("can't request file: {e}").format(e=e), error=True)
|
|
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
|
else:
|
|
await self.set_progress_id(progress_id)
|
|
|
|
|
|
class Receive(base.CommandAnswering):
|
|
def __init__(self, host):
|
|
super(Receive, self).__init__(
|
|
host,
|
|
"receive",
|
|
use_progress=True,
|
|
use_verbose=True,
|
|
help=_("wait for a file to be sent by a contact"),
|
|
)
|
|
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,
|
|
C.META_TYPE_OVERWRITE: self.on_overwrite_action,
|
|
C.META_TYPE_NOT_IN_ROSTER_LEAK: self.on_not_in_roster_action,
|
|
}
|
|
|
|
def add_parser_options(self):
|
|
self.parser.add_argument(
|
|
"jids",
|
|
nargs="*",
|
|
help=_("jids accepted (accept everything if none is specified)"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-m",
|
|
"--multiple",
|
|
action="store_true",
|
|
help=_("accept multiple files (you'll have to stop manually)"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-f",
|
|
"--force",
|
|
action="store_true",
|
|
help=_(
|
|
"force overwriting of existing files (/!\\ name is choosed by sender)"
|
|
),
|
|
)
|
|
self.parser.add_argument(
|
|
"--path",
|
|
default=".",
|
|
metavar="DIR",
|
|
help=_("destination path (default: working directory)"),
|
|
)
|
|
|
|
async def on_progress_started(self, metadata):
|
|
self.disp(_("File copy started"), 2)
|
|
|
|
async def on_progress_finished(self, metadata):
|
|
self.disp(_("File received successfully"), 2)
|
|
if metadata.get("hash_verified", False):
|
|
try:
|
|
self.disp(
|
|
_("hash checked: {metadata['hash_algo']}:{metadata['hash']}"), 1
|
|
)
|
|
except KeyError:
|
|
self.disp(_("hash is checked but hash value is missing", 1), error=True)
|
|
else:
|
|
self.disp(_("hash can't be verified"), 1)
|
|
|
|
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
|
|
# TODO: XMLUI module
|
|
try:
|
|
xml_ui = action_data["xmlui"]
|
|
except KeyError:
|
|
self.disp(_("Action has no XMLUI"), 1)
|
|
else:
|
|
ui = ET.fromstring(xml_ui.encode("utf-8"))
|
|
xmlui_id = ui.get("submit")
|
|
if not xmlui_id:
|
|
self.disp(_("Invalid XMLUI received"), error=True)
|
|
return xmlui_id
|
|
|
|
async def on_confirm_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)
|
|
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 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
|
|
)
|
|
|
|
async def on_file_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)
|
|
try:
|
|
from_jid = jid.JID(action_data["from_jid"])
|
|
except KeyError:
|
|
self.disp(_("Ignoring action without from_jid data"), 1)
|
|
return
|
|
try:
|
|
progress_id = action_data["progress_id"]
|
|
except KeyError:
|
|
self.disp(_("ignoring action without progress id"), 1)
|
|
return
|
|
|
|
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(
|
|
xmlui_id, data_format.serialise({"cancelled": C.BOOL_TRUE}),
|
|
profile_key=profile
|
|
)
|
|
return self.host.quit_from_signal(2)
|
|
await self.set_progress_id(progress_id)
|
|
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
|
|
)
|
|
|
|
async def on_overwrite_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)
|
|
try:
|
|
progress_id = action_data["progress_id"]
|
|
except KeyError:
|
|
self.disp(_("ignoring action without progress id"), 1)
|
|
return
|
|
self.disp(_("Overwriting needed"), 1)
|
|
|
|
if progress_id == self.progress_id:
|
|
if self.args.force:
|
|
self.disp(_("Overwrite accepted"), 2)
|
|
else:
|
|
self.disp(_("Refused to overwrite"), 2)
|
|
self._overwrite_refused = True
|
|
|
|
xmlui_data = {"answer": C.bool_const(self.args.force)}
|
|
await self.host.bridge.action_launch(
|
|
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
|
|
):
|
|
xmlui_id = self.get_xmlui_id(action_data)
|
|
if xmlui_id is None:
|
|
return self.host.quit_from_signal(1)
|
|
try:
|
|
from_jid = jid.JID(action_data["from_jid"])
|
|
except ValueError:
|
|
self.disp(
|
|
_('invalid "from_jid" value received, ignoring: {value}').format(
|
|
value=action_data["from_jid"]
|
|
),
|
|
error=True,
|
|
)
|
|
return
|
|
except KeyError:
|
|
self.disp(_('ignoring action without "from_jid" value'), error=True)
|
|
return
|
|
self.disp(_("Confirmation needed for request from an entity not in roster"), 1)
|
|
|
|
if from_jid.bare in self.bare_jids:
|
|
# if the sender is expected, we can confirm the session
|
|
confirmed = True
|
|
self.disp(_("Sender confirmed because she or he is explicitly expected"), 1)
|
|
else:
|
|
xmlui = xmlui_manager.create(self.host, action_data["xmlui"])
|
|
confirmed = await self.host.confirm(xmlui.dlg.message)
|
|
|
|
xmlui_data = {"answer": C.bool_const(confirmed)}
|
|
await self.host.bridge.action_launch(
|
|
xmlui_id, data_format.serialise(xmlui_data), profile_key=profile
|
|
)
|
|
if not confirmed and not self.args.multiple:
|
|
self.disp(_("Session refused for {from_jid}").format(from_jid=from_jid))
|
|
self.host.quit_from_signal(0)
|
|
|
|
async def start(self):
|
|
self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids]
|
|
self.path = os.path.abspath(self.args.path)
|
|
if not os.path.isdir(self.path):
|
|
self.disp(_("Given path is not a directory !"), error=True)
|
|
self.host.quit(C.EXIT_BAD_ARG)
|
|
if self.args.multiple:
|
|
self.host.quit_on_progress_end = False
|
|
self.disp(_("waiting for incoming file request"), 2)
|
|
await self.start_answering()
|
|
|
|
|
|
class Get(base.CommandBase):
|
|
def __init__(self, host):
|
|
super(Get, self).__init__(
|
|
host,
|
|
"get",
|
|
use_progress=True,
|
|
use_verbose=True,
|
|
help=_("download a file from URI"),
|
|
)
|
|
|
|
def add_parser_options(self):
|
|
self.parser.add_argument(
|
|
"-o",
|
|
"--dest-file",
|
|
type=str,
|
|
default="",
|
|
help=_("destination file (DEFAULT: filename from URL)"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-f",
|
|
"--force",
|
|
action="store_true",
|
|
help=_("overwrite existing file without confirmation"),
|
|
)
|
|
self.parser.add_argument(
|
|
"attachment", type=str,
|
|
help=_("URI of the file to retrieve or JSON of the whole attachment")
|
|
)
|
|
|
|
async def on_progress_started(self, metadata):
|
|
self.disp(_("File download started"), 2)
|
|
|
|
async def on_progress_finished(self, metadata):
|
|
self.disp(_("File downloaded successfully"), 2)
|
|
|
|
async def on_progress_error(self, error_msg):
|
|
self.disp(_("Error while downloading file: {}").format(error_msg), error=True)
|
|
|
|
async def got_id(self, data):
|
|
"""Called when a progress id has been received"""
|
|
try:
|
|
await self.set_progress_id(data["progress"])
|
|
except KeyError:
|
|
if "xmlui" in data:
|
|
ui = xmlui_manager.create(self.host, data["xmlui"])
|
|
await ui.show()
|
|
else:
|
|
self.disp(_("Can't download file"), error=True)
|
|
self.host.quit(C.EXIT_ERROR)
|
|
|
|
async def start(self):
|
|
try:
|
|
attachment = json.loads(self.args.attachment)
|
|
except json.JSONDecodeError:
|
|
attachment = {"uri": self.args.attachment}
|
|
dest_file = self.args.dest_file
|
|
if not dest_file:
|
|
try:
|
|
dest_file = attachment["name"].replace("/", "-").strip()
|
|
except KeyError:
|
|
try:
|
|
dest_file = Path(urlparse(attachment["uri"]).path).name.strip()
|
|
except KeyError:
|
|
pass
|
|
if not dest_file:
|
|
dest_file = "downloaded_file"
|
|
|
|
dest_file = Path(dest_file).expanduser().resolve()
|
|
if dest_file.exists() and not self.args.force:
|
|
message = _("File {path} already exists! Do you want to overwrite?").format(
|
|
path=dest_file
|
|
)
|
|
await self.host.confirm_or_quit(message, _("file download cancelled"))
|
|
|
|
options = {}
|
|
|
|
try:
|
|
download_data_s = await self.host.bridge.file_download(
|
|
data_format.serialise(attachment),
|
|
str(dest_file),
|
|
data_format.serialise(options),
|
|
self.profile,
|
|
)
|
|
download_data = data_format.deserialise(download_data_s)
|
|
except Exception as e:
|
|
self.disp(f"error while trying to download a file: {e}", error=True)
|
|
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
|
else:
|
|
await self.got_id(download_data)
|
|
|
|
|
|
class Upload(base.CommandBase):
|
|
def __init__(self, host):
|
|
super(Upload, self).__init__(
|
|
host, "upload", use_progress=True, use_verbose=True, help=_("upload a file")
|
|
)
|
|
|
|
def add_parser_options(self):
|
|
self.parser.add_argument(
|
|
"-e",
|
|
"--encrypt",
|
|
action="store_true",
|
|
help=_("encrypt file using AES-GCM"),
|
|
)
|
|
self.parser.add_argument("file", type=str, help=_("file to upload"))
|
|
self.parser.add_argument(
|
|
"jid",
|
|
nargs="?",
|
|
help=_("jid of upload component (nothing to autodetect)"),
|
|
)
|
|
self.parser.add_argument(
|
|
"--ignore-tls-errors",
|
|
action="store_true",
|
|
help=_(r"ignore invalide TLS certificate (/!\ Dangerous /!\)"),
|
|
)
|
|
|
|
async def on_progress_started(self, metadata):
|
|
self.disp(_("File upload started"), 2)
|
|
|
|
async def on_progress_finished(self, metadata):
|
|
self.disp(_("File uploaded successfully"), 2)
|
|
try:
|
|
url = metadata["url"]
|
|
except KeyError:
|
|
self.disp("download URL not found in metadata")
|
|
else:
|
|
self.disp(_("URL to retrieve the file:"), 1)
|
|
# XXX: url is displayed alone on a line to make parsing easier
|
|
self.disp(url)
|
|
|
|
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):
|
|
"""Called when a progress id has been received"""
|
|
try:
|
|
await self.set_progress_id(data["progress"])
|
|
except KeyError:
|
|
if "xmlui" in data:
|
|
ui = xmlui_manager.create(self.host, data["xmlui"])
|
|
await ui.show()
|
|
else:
|
|
self.disp(_("Can't upload file"), error=True)
|
|
self.host.quit(C.EXIT_ERROR)
|
|
|
|
async def start(self):
|
|
file_ = self.args.file
|
|
if not os.path.exists(file_):
|
|
self.disp(
|
|
_("file {file_} doesn't exist !").format(file_=repr(file_)), error=True
|
|
)
|
|
self.host.quit(C.EXIT_BAD_ARG)
|
|
if os.path.isdir(file_):
|
|
self.disp(_("{file_} is a dir! Can't upload a dir").format(file_=repr(file_)))
|
|
self.host.quit(C.EXIT_BAD_ARG)
|
|
|
|
if self.args.jid is None:
|
|
self.full_dest_jid = ""
|
|
else:
|
|
self.full_dest_jid = await self.host.get_full_jid(self.args.jid)
|
|
|
|
options = {}
|
|
if self.args.ignore_tls_errors:
|
|
options["ignore_tls_errors"] = True
|
|
if self.args.encrypt:
|
|
options["encryption"] = C.ENC_AES_GCM
|
|
|
|
path = os.path.abspath(file_)
|
|
try:
|
|
upload_data = await self.host.bridge.file_upload(
|
|
path,
|
|
"",
|
|
self.full_dest_jid,
|
|
data_format.serialise(options),
|
|
self.profile,
|
|
)
|
|
except Exception as e:
|
|
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)
|
|
|
|
|
|
class ShareAffiliationsSet(base.CommandBase):
|
|
def __init__(self, host):
|
|
super(ShareAffiliationsSet, self).__init__(
|
|
host,
|
|
"set",
|
|
use_output=C.OUTPUT_DICT,
|
|
help=_("set affiliations for a shared file/directory"),
|
|
)
|
|
|
|
def add_parser_options(self):
|
|
self.parser.add_argument(
|
|
"-N",
|
|
"--namespace",
|
|
default="",
|
|
help=_("namespace of the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-P",
|
|
"--path",
|
|
default="",
|
|
help=_("path to the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-a",
|
|
"--affiliation",
|
|
dest="affiliations",
|
|
metavar=("JID", "AFFILIATION"),
|
|
required=True,
|
|
action="append",
|
|
nargs=2,
|
|
help=_("entity/affiliation couple(s)"),
|
|
)
|
|
self.parser.add_argument(
|
|
"jid",
|
|
help=_("jid of file sharing entity"),
|
|
)
|
|
|
|
async def start(self):
|
|
affiliations = dict(self.args.affiliations)
|
|
try:
|
|
affiliations = await self.host.bridge.fis_affiliations_set(
|
|
self.args.jid,
|
|
self.args.namespace,
|
|
self.args.path,
|
|
affiliations,
|
|
self.profile,
|
|
)
|
|
except Exception as e:
|
|
self.disp(f"can't set affiliations: {e}", error=True)
|
|
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
|
else:
|
|
self.host.quit()
|
|
|
|
|
|
class ShareAffiliationsGet(base.CommandBase):
|
|
def __init__(self, host):
|
|
super(ShareAffiliationsGet, self).__init__(
|
|
host,
|
|
"get",
|
|
use_output=C.OUTPUT_DICT,
|
|
help=_("retrieve affiliations of a shared file/directory"),
|
|
)
|
|
|
|
def add_parser_options(self):
|
|
self.parser.add_argument(
|
|
"-N",
|
|
"--namespace",
|
|
default="",
|
|
help=_("namespace of the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-P",
|
|
"--path",
|
|
default="",
|
|
help=_("path to the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"jid",
|
|
help=_("jid of sharing entity"),
|
|
)
|
|
|
|
async def start(self):
|
|
try:
|
|
affiliations = await self.host.bridge.fis_affiliations_get(
|
|
self.args.jid,
|
|
self.args.namespace,
|
|
self.args.path,
|
|
self.profile,
|
|
)
|
|
except Exception as e:
|
|
self.disp(f"can't get affiliations: {e}", error=True)
|
|
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
|
else:
|
|
await self.output(affiliations)
|
|
self.host.quit()
|
|
|
|
|
|
class ShareAffiliations(base.CommandBase):
|
|
subcommands = (ShareAffiliationsGet, ShareAffiliationsSet)
|
|
|
|
def __init__(self, host):
|
|
super(ShareAffiliations, self).__init__(
|
|
host, "affiliations", use_profile=False, help=_("affiliations management")
|
|
)
|
|
|
|
|
|
class ShareConfigurationSet(base.CommandBase):
|
|
def __init__(self, host):
|
|
super(ShareConfigurationSet, self).__init__(
|
|
host,
|
|
"set",
|
|
use_output=C.OUTPUT_DICT,
|
|
help=_("set configuration for a shared file/directory"),
|
|
)
|
|
|
|
def add_parser_options(self):
|
|
self.parser.add_argument(
|
|
"-N",
|
|
"--namespace",
|
|
default="",
|
|
help=_("namespace of the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-P",
|
|
"--path",
|
|
default="",
|
|
help=_("path to the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-f",
|
|
"--field",
|
|
action="append",
|
|
nargs=2,
|
|
dest="fields",
|
|
required=True,
|
|
metavar=("KEY", "VALUE"),
|
|
help=_("configuration field to set (required)"),
|
|
)
|
|
self.parser.add_argument(
|
|
"jid",
|
|
help=_("jid of file sharing entity"),
|
|
)
|
|
|
|
async def start(self):
|
|
configuration = dict(self.args.fields)
|
|
try:
|
|
configuration = await self.host.bridge.fis_configuration_set(
|
|
self.args.jid,
|
|
self.args.namespace,
|
|
self.args.path,
|
|
configuration,
|
|
self.profile,
|
|
)
|
|
except Exception as e:
|
|
self.disp(f"can't set configuration: {e}", error=True)
|
|
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
|
else:
|
|
self.host.quit()
|
|
|
|
|
|
class ShareConfigurationGet(base.CommandBase):
|
|
def __init__(self, host):
|
|
super(ShareConfigurationGet, self).__init__(
|
|
host,
|
|
"get",
|
|
use_output=C.OUTPUT_DICT,
|
|
help=_("retrieve configuration of a shared file/directory"),
|
|
)
|
|
|
|
def add_parser_options(self):
|
|
self.parser.add_argument(
|
|
"-N",
|
|
"--namespace",
|
|
default="",
|
|
help=_("namespace of the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-P",
|
|
"--path",
|
|
default="",
|
|
help=_("path to the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"jid",
|
|
help=_("jid of sharing entity"),
|
|
)
|
|
|
|
async def start(self):
|
|
try:
|
|
configuration = await self.host.bridge.fis_configuration_get(
|
|
self.args.jid,
|
|
self.args.namespace,
|
|
self.args.path,
|
|
self.profile,
|
|
)
|
|
except Exception as e:
|
|
self.disp(f"can't get configuration: {e}", error=True)
|
|
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
|
else:
|
|
await self.output(configuration)
|
|
self.host.quit()
|
|
|
|
|
|
class ShareConfiguration(base.CommandBase):
|
|
subcommands = (ShareConfigurationGet, ShareConfigurationSet)
|
|
|
|
def __init__(self, host):
|
|
super(ShareConfiguration, self).__init__(
|
|
host,
|
|
"configuration",
|
|
use_profile=False,
|
|
help=_("file sharing node configuration"),
|
|
)
|
|
|
|
|
|
class ShareList(base.CommandBase):
|
|
def __init__(self, host):
|
|
extra_outputs = {"default": self.default_output}
|
|
super(ShareList, self).__init__(
|
|
host,
|
|
"list",
|
|
use_output=C.OUTPUT_LIST_DICT,
|
|
extra_outputs=extra_outputs,
|
|
help=_("retrieve files shared by an entity"),
|
|
use_verbose=True,
|
|
)
|
|
|
|
def add_parser_options(self):
|
|
self.parser.add_argument(
|
|
"-d",
|
|
"--path",
|
|
default="",
|
|
help=_("path to the directory containing the files"),
|
|
)
|
|
self.parser.add_argument(
|
|
"jid",
|
|
nargs="?",
|
|
default="",
|
|
help=_("jid of sharing entity (nothing to check our own jid)"),
|
|
)
|
|
|
|
def _name_filter(self, name, row):
|
|
if row.type == C.FILE_TYPE_DIRECTORY:
|
|
return A.color(C.A_DIRECTORY, name)
|
|
elif row.type == C.FILE_TYPE_FILE:
|
|
return A.color(C.A_FILE, name)
|
|
else:
|
|
self.disp(_("unknown file type: {type}").format(type=row.type), error=True)
|
|
return name
|
|
|
|
def _size_filter(self, size, row):
|
|
if not size:
|
|
return ""
|
|
return A.color(A.BOLD, utils.get_human_size(size))
|
|
|
|
def default_output(self, files_data):
|
|
"""display files a way similar to ls"""
|
|
files_data.sort(key=lambda d: d["name"].lower())
|
|
show_header = False
|
|
if self.verbosity == 0:
|
|
keys = headers = ("name", "type")
|
|
elif self.verbosity == 1:
|
|
keys = headers = ("name", "type", "size")
|
|
elif self.verbosity > 1:
|
|
show_header = True
|
|
keys = ("name", "type", "size", "file_hash")
|
|
headers = ("name", "type", "size", "hash")
|
|
table = common.Table.from_list_dict(
|
|
self.host,
|
|
files_data,
|
|
keys=keys,
|
|
headers=headers,
|
|
filters={"name": self._name_filter, "size": self._size_filter},
|
|
defaults={"size": "", "file_hash": ""},
|
|
)
|
|
table.display_blank(show_header=show_header, hide_cols=["type"])
|
|
|
|
async def start(self):
|
|
try:
|
|
files_data = await self.host.bridge.fis_list(
|
|
self.args.jid,
|
|
self.args.path,
|
|
{},
|
|
self.profile,
|
|
)
|
|
except Exception as e:
|
|
self.disp(f"can't retrieve shared files: {e}", error=True)
|
|
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
|
|
|
await self.output(files_data)
|
|
self.host.quit()
|
|
|
|
|
|
class SharePath(base.CommandBase):
|
|
def __init__(self, host):
|
|
super(SharePath, self).__init__(
|
|
host, "path", help=_("share a file or directory"), use_verbose=True
|
|
)
|
|
|
|
def add_parser_options(self):
|
|
self.parser.add_argument(
|
|
"-n",
|
|
"--name",
|
|
default="",
|
|
help=_("virtual name to use (default: use directory/file name)"),
|
|
)
|
|
perm_group = self.parser.add_mutually_exclusive_group()
|
|
perm_group.add_argument(
|
|
"-j",
|
|
"--jid",
|
|
metavar="JID",
|
|
action="append",
|
|
dest="jids",
|
|
default=[],
|
|
help=_("jid of contacts allowed to retrieve the files"),
|
|
)
|
|
perm_group.add_argument(
|
|
"--public",
|
|
action="store_true",
|
|
help=_(
|
|
r"share publicly the file(s) (/!\ *everybody* will be able to access "
|
|
r"them)"
|
|
),
|
|
)
|
|
self.parser.add_argument(
|
|
"path",
|
|
help=_("path to a file or directory to share"),
|
|
)
|
|
|
|
async def start(self):
|
|
self.path = os.path.abspath(self.args.path)
|
|
if self.args.public:
|
|
access = {"read": {"type": "public"}}
|
|
else:
|
|
jids = self.args.jids
|
|
if jids:
|
|
access = {"read": {"type": "whitelist", "jids": jids}}
|
|
else:
|
|
access = {}
|
|
try:
|
|
name = await self.host.bridge.fis_share_path(
|
|
self.args.name,
|
|
self.path,
|
|
json.dumps(access, ensure_ascii=False),
|
|
self.profile,
|
|
)
|
|
except Exception as e:
|
|
self.disp(f"can't share path: {e}", error=True)
|
|
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
|
else:
|
|
self.disp(
|
|
_('{path} shared under the name "{name}"').format(
|
|
path=self.path, name=name
|
|
)
|
|
)
|
|
self.host.quit()
|
|
|
|
|
|
class ShareInvite(base.CommandBase):
|
|
def __init__(self, host):
|
|
super(ShareInvite, self).__init__(
|
|
host, "invite", help=_("send invitation for a shared repository")
|
|
)
|
|
|
|
def add_parser_options(self):
|
|
self.parser.add_argument(
|
|
"-n",
|
|
"--name",
|
|
default="",
|
|
help=_("name of the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-N",
|
|
"--namespace",
|
|
default="",
|
|
help=_("namespace of the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-P",
|
|
"--path",
|
|
help=_("path to the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-t",
|
|
"--type",
|
|
choices=["files", "photos"],
|
|
default="files",
|
|
help=_("type of the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"-T",
|
|
"--thumbnail",
|
|
help=_("https URL of a image to use as thumbnail"),
|
|
)
|
|
self.parser.add_argument(
|
|
"service",
|
|
help=_("jid of the file sharing service hosting the repository"),
|
|
)
|
|
self.parser.add_argument(
|
|
"jid",
|
|
help=_("jid of the person to invite"),
|
|
)
|
|
|
|
async def start(self):
|
|
self.path = os.path.normpath(self.args.path) if self.args.path else ""
|
|
extra = {}
|
|
if self.args.thumbnail is not None:
|
|
if not self.args.thumbnail.startswith("http"):
|
|
self.parser.error(_("only http(s) links are allowed with --thumbnail"))
|
|
else:
|
|
extra["thumb_url"] = self.args.thumbnail
|
|
try:
|
|
await self.host.bridge.fis_invite(
|
|
self.args.jid,
|
|
self.args.service,
|
|
self.args.type,
|
|
self.args.namespace,
|
|
self.path,
|
|
self.args.name,
|
|
data_format.serialise(extra),
|
|
self.profile,
|
|
)
|
|
except Exception as e:
|
|
self.disp(f"can't send invitation: {e}", error=True)
|
|
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
|
|
else:
|
|
self.disp(_("invitation sent to {jid}").format(jid=self.args.jid))
|
|
self.host.quit()
|
|
|
|
|
|
class Share(base.CommandBase):
|
|
subcommands = (
|
|
ShareList,
|
|
SharePath,
|
|
ShareInvite,
|
|
ShareAffiliations,
|
|
ShareConfiguration,
|
|
)
|
|
|
|
def __init__(self, host):
|
|
super(Share, self).__init__(
|
|
host, "share", use_profile=False, help=_("files sharing management")
|
|
)
|
|
|
|
|
|
class File(base.CommandBase):
|
|
subcommands = (Send, Request, Receive, Get, Upload, Share)
|
|
|
|
def __init__(self, host):
|
|
super(File, self).__init__(
|
|
host, "file", use_profile=False, help=_("files sending/receiving/management")
|
|
)
|