view libervia/cli/cmd_file.py @ 4309:b56b1eae7994

component email gateway: add multicasting: XEP-0033 multicasting is now supported both for incoming and outgoing messages. XEP-0033 metadata are converted to suitable Email headers and vice versa. Email address and JID are both supported, and delivery is done by the gateway when suitable on incoming messages. rel 450
author Goffi <goffi@goffi.org>
date Thu, 26 Sep 2024 16:12:01 +0200
parents 0d7bb4df2343
children
line wrap: on
line source

#!/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 logging
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)
        pre_accepted = action_data.get("pre_accepted", False)

        if pre_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")
        )