changeset 2505:8e770ac05b0c

jp (file): file sharing + improvments: - file sending now handle --path and --namespace (used to specify how to share a file) - implemented file request, a file must be identified by it's name or hash, using --path when possible - added share subcommands - share/list allows to list file shared by an entity at a specific path - share/path allows one to share a local path privatly (only accessible by its bare jid), publicly or to a whitelist of jids
author Goffi <goffi@goffi.org>
date Wed, 28 Feb 2018 18:28:39 +0100
parents 67cc54b01a12
children 516bf5309517
files frontends/src/jp/cmd_file.py frontends/src/jp/constants.py
diffstat 2 files changed, 235 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/jp/cmd_file.py	Wed Feb 28 18:28:39 2018 +0100
+++ b/frontends/src/jp/cmd_file.py	Wed Feb 28 18:28:39 2018 +0100
@@ -25,23 +25,28 @@
 import tarfile
 from sat.core.i18n import _
 from sat_frontends.jp.constants import Const as C
+from sat_frontends.jp import common
 from sat_frontends.tools import jid
+from sat.tools.common.ansi import ANSI as A
 import tempfile
 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI
-
+from functools import partial
+import json
 
 __commands__ = ["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'))
+        super(Send, self).__init__(host, 'send', use_progress=True, use_verbose=True, help=_('send a file to a contact'))
         self.need_loop=True
 
     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", type=base.unicode_decoder, help=_("the destination jid"))
-        self.parser.add_argument("-b", "--bz2", action="store_true", help=_("make a bzip2 tarball"))
+        self.parser.add_argument("files", type=str, nargs='+', metavar='file', help=_(u"a list of file"))
+        self.parser.add_argument("jid", type=base.unicode_decoder, help=_(u"the destination jid"))
+        self.parser.add_argument("-b", "--bz2", action="store_true", help=_(u"make a bzip2 tarball"))
+        self.parser.add_argument("-d", "--path", type=base.unicode_decoder, help=(u"path to the directory where the file must be stored"))
+        self.parser.add_argument("-N", "--namespace", type=base.unicode_decoder, help=(u"namespace of the file"))
 
     def start(self):
         """Send files to jabber contact"""
@@ -54,7 +59,10 @@
         self.disp(_(u'File sent successfully'),2)
 
     def onProgressError(self, error_msg):
-        self.disp(_(u'Error while sending file: {}').format(error_msg),error=True)
+        if error_msg == C.PROGRESS_ERROR_DECLINED:
+            self.disp(_(u'The file has been refused by your contact'))
+        else:
+            self.disp(_(u'Error while sending file: {}').format(error_msg),error=True)
 
     def gotId(self, data, file_):
         """Called when a progress id has been received
@@ -76,7 +84,6 @@
         self.host.quit(1)
 
     def send_files(self):
-
         for file_ in self.args.files:
             if not os.path.exists(file_):
                 self.disp(_(u"file [{}] doesn't exist !").format(file_), error=True)
@@ -86,6 +93,11 @@
                 self.host.quit(1)
 
         self.full_dest_jid = self.host.get_full_jid(self.args.jid)
+        extra = {}
+        if self.args.path:
+            extra[u'path'] = self.args.path
+        if self.args.namespace:
+            extra[u'namespace'] = self.args.namespace
 
         if self.args.bz2:
             with tempfile.NamedTemporaryFile('wb', delete=False) as buf:
@@ -102,17 +114,97 @@
                 bz2.close()
                 self.disp(_(u"Done !"), 1)
 
-                self.host.bridge.fileSend(self.full_dest_jid, buf.name, archive_name, '', self.profile, callback=lambda pid, file_=buf.name: self.gotId(pid, file_), errback=self.error)
+                self.host.bridge.fileSend(self.full_dest_jid, buf.name, archive_name, '', extra, self.profile, callback=lambda pid, file_=buf.name: self.gotId(pid, file_), errback=self.error)
         else:
             for file_ in self.args.files:
                 path = os.path.abspath(file_)
-                self.host.bridge.fileSend(self.full_dest_jid, path, '', '', self.profile, callback=lambda pid, file_=file_: self.gotId(pid, file_), errback=self.error)
+                self.host.bridge.fileSend(self.full_dest_jid, path, '', '', extra, self.profile, callback=lambda pid, file_=file_: self.gotId(pid, file_), errback=self.error)
+
+
+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'))
+        self.need_loop=True
+
+    @property
+    def filename(self):
+        return self.args.name or self.args.hash or u"output"
+
+    def add_parser_options(self):
+        self.parser.add_argument("jid", type=base.unicode_decoder, help=_(u"the destination jid"))
+        self.parser.add_argument("-D", "--dest", type=base.unicode_decoder, help=_(u"destination path where the file will be saved (default: [current_dir]/[name|hash])"))
+        self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default=u'', help=_(u"name of the file"))
+        self.parser.add_argument("-H", "--hash", type=base.unicode_decoder, default=u'', help=_(u"hash of the file"))
+        self.parser.add_argument("-a", "--hash-algo", type=base.unicode_decoder, default=u'sha-256', help=_(u"hash algorithm use for --hash (default: sha-256)"))
+        self.parser.add_argument("-d", "--path", type=base.unicode_decoder, help=(u"path to the directory containing the file"))
+        self.parser.add_argument("-N", "--namespace", type=base.unicode_decoder, help=(u"namespace of the file"))
+        self.parser.add_argument("-f", "--force", action='store_true', help=_(u"overwrite existing file without confirmation"))
+
+    def onProgressStarted(self, metadata):
+        self.disp(_(u'File copy started'),2)
+
+    def onProgressFinished(self, metadata):
+        self.disp(_(u'File received successfully'),2)
+
+    def onProgressError(self, error_msg):
+        if error_msg == C.PROGRESS_ERROR_DECLINED:
+            self.disp(_(u'The file request has been refused'))
+        else:
+            self.disp(_(u'Error while requesting file: {}').format(error_msg), error=True)
+
+    def gotId(self, progress_id):
+        """Called when a progress id has been received
+
+        @param progress_id(unicode): progress id
+        """
+        self.progress_id = progress_id
+
+    def error(self, failure):
+        self.disp(_("Error while trying to send a file: {reason}").format(reason=failure), error=True)
+        self.host.quit(1)
+
+    def start(self):
+        if not self.args.name and not self.args.hash:
+            self.parser.error(_(u'at least one of --name or --hash must be provided'))
+        # extra = dict(self.args.extra)
+        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 = _(u'File {path} already exists! Do you want to overwrite?').format(path=path)
+            confirm = raw_input(u"{} (y/N) ".format(message).encode('utf-8'))
+            if confirm not in (u"y", u"Y"):
+                self.disp(_(u"file request cancelled"))
+                self.host.quit(2)
+
+        self.full_dest_jid = self.host.get_full_jid(self.args.jid)
+        extra = {}
+        if self.args.path:
+            extra[u'path'] = self.args.path
+        if self.args.namespace:
+            extra[u'namespace'] = self.args.namespace
+        self.host.bridge.fileJingleRequest(self.full_dest_jid,
+                                           path,
+                                           self.args.name,
+                                           self.args.hash,
+                                           self.args.hash_algo if self.args.hash else u'',
+                                           extra,
+                                           self.profile,
+                                           callback=self.gotId,
+                                           errback=partial(self.errback,
+                                               msg=_(u"can't request file: {}"),
+                                               exit_code=C.EXIT_BRIDGE_ERRBACK))
 
 
 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'))
+        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 as already been refused
         self.action_callbacks = {C.META_TYPE_FILE: self.onFileAction,
                                  C.META_TYPE_OVERWRITE: self.onOverwriteAction}
@@ -197,7 +289,7 @@
             self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
 
     def add_parser_options(self):
-        self.parser.add_argument("jids", type=base.unicode_decoder, nargs="*", help=_(u'JIDs accepted (accept everything if none is specified)'))
+        self.parser.add_argument("jids", type=base.unicode_decoder, nargs="*", help=_(u'jids accepted (accept everything if none is specified)'))
         self.parser.add_argument("-m", "--multiple", action="store_true", help=_(u"accept multiple files (you'll have to stop manually)"))
         self.parser.add_argument("-f", "--force", action="store_true", help=_(u"force overwritting of existing files (/!\\ name is choosed by sender)"))
         self.parser.add_argument("--path", default='.', metavar='DIR', help=_(u"destination path (default: working directory)"))
@@ -216,7 +308,7 @@
 class Upload(base.CommandBase):
 
     def __init__(self, host):
-        super(Upload, self).__init__(host, 'upload', use_progress=True, use_verbose=True, help=_('Upload a file'))
+        super(Upload, self).__init__(host, 'upload', use_progress=True, use_verbose=True, help=_('upload a file'))
         self.need_loop=True
 
     def add_parser_options(self):
@@ -276,8 +368,133 @@
         self.host.bridge.fileUpload(path, '', self.full_dest_jid, options, self.profile, callback=lambda pid, file_=file_: self.gotId(pid, file_), errback=self.error)
 
 
-class File(base.CommandBase):
-    subcommands = (Send, Receive, Upload)
+class ShareList(base.CommandBase):
 
     def __init__(self, host):
-        super(File, self).__init__(host, 'file', use_profile=False, help=_('File sending/receiving'))
+        extra_outputs = {'default': self.default_output}
+        super(ShareList, self).__init__(host, 'list', use_output=C.OUTPUT_LIST_DICT, extra_outputs=extra_outputs, help=_(u'retrieve files shared by an entity'), use_verbose=True)
+        self.need_loop=True
+
+    def add_parser_options(self):
+        self.parser.add_argument("-d", "--path", default=u'', help=_(u"path to the directory containing the files"))
+        self.parser.add_argument("jid", type=base.unicode_decoder, nargs='?',  help=_("jid of sharing entity (nothing to check our own jid)"))
+
+    def file_gen(self, files_data):
+        for file_data in files_data:
+            yield file_data[u'name']
+            yield file_data.get(u'size', '')
+            yield file_data.get(u'hash','')
+
+    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(_(u'unknown file type: {type}').format(type=row.type), error=True)
+            return name
+
+    def _size_filter(self, size, row):
+        if not size:
+            return u''
+        size = int(size)
+        # cf. https://stackoverflow.com/a/1094933 (thanks)
+        suffix = u'o'
+        for unit in [u'', u'Ki', u'Mi', u'Gi', u'Ti', u'Pi', u'Ei', u'Zi']:
+            if abs(size) < 1024.0:
+                return A.color(A.BOLD, u"{:.2f}".format(size), unit, suffix)
+            size /= 1024.0
+
+        return A.color(A.BOLD, u"{:.2f}".format(size), u'Yi', suffix)
+
+    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:
+            headers = (u'name', u'type')
+        elif self.verbosity == 1:
+            headers = (u'name', u'type', u'size')
+        elif self.verbosity > 1:
+            show_header = True
+            headers = (u'name', u'type', u'size', u'hash')
+        table = common.Table.fromDict(self.host,
+                                      files_data,
+                                      headers,
+                                      filters={u'name': self._name_filter,
+                                               u'size': self._size_filter},
+                                      defaults={u'size': u'',
+                                                u'hash': u''},
+                                      )
+        table.display_blank(show_header=show_header, hide_cols=['type'])
+
+    def _FISListCb(self, files_data):
+        self.output(files_data)
+        self.host.quit()
+
+    def start(self):
+        self.host.bridge.FISList(
+            self.args.jid,
+            self.args.path,
+            {},
+            self.profile,
+            callback=self._FISListCb,
+            errback=partial(self.errback,
+                            msg=_(u"can't retrieve shared files: {}"),
+                            exit_code=C.EXIT_BRIDGE_ERRBACK))
+
+
+class SharePath(base.CommandBase):
+
+    def __init__(self, host):
+        super(SharePath, self).__init__(host, 'path', help=_(u'share a file or directory'), use_verbose=True)
+        self.need_loop=True
+
+    def add_parser_options(self):
+        self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default=u'', help=_(u"virtual name to use (default: use directory/file name)"))
+        perm_group = self.parser.add_mutually_exclusive_group()
+        perm_group.add_argument("-j", "--jid", type=base.unicode_decoder, action='append', dest="jids", default=[], help=_(u"jid of contacts allowed to retrieve the files"))
+        perm_group.add_argument("--public", action='store_true', help=_(u"share publicly the file(s) (/!\\ *everybody* will be able to access them)"))
+        self.parser.add_argument("path", type=base.unicode_decoder, help=_(u"path to a file or directory to share"))
+
+
+    def _FISSharePathCb(self, name):
+        self.disp(_(u'{path} shared under the name "{name}"').format(
+            path = self.path,
+            name = name))
+        self.host.quit()
+
+    def start(self):
+        self.path = os.path.abspath(self.args.path)
+        if self.args.public:
+            access = {u'read': {u'type': u'public'}}
+        else:
+            jids = self.args.jids
+            if jids:
+                access = {u'read': {u'type': 'whitelist',
+                                         u'jids': jids}}
+            else:
+                access = {}
+        self.host.bridge.FISSharePath(
+            self.args.name,
+            self.path,
+            json.dumps(access, ensure_ascii=False),
+            self.profile,
+            callback=self._FISSharePathCb,
+            errback=partial(self.errback,
+                            msg=_(u"can't share path: {}"),
+                            exit_code=C.EXIT_BRIDGE_ERRBACK))
+
+
+class Share(base.CommandBase):
+    subcommands = (ShareList, SharePath)
+
+    def __init__(self, host):
+        super(Share, self).__init__(host, 'share', use_profile=False, help=_(u'files sharing management'))
+
+
+class File(base.CommandBase):
+    subcommands = (Send, Request, Receive, Upload, Share)
+
+    def __init__(self, host):
+        super(File, self).__init__(host, 'file', use_profile=False, help=_(u'files sending/receiving/management'))
--- a/frontends/src/jp/constants.py	Wed Feb 28 18:28:39 2018 +0100
+++ b/frontends/src/jp/constants.py	Wed Feb 28 18:28:39 2018 +0100
@@ -46,6 +46,9 @@
     # A_PROMPT_* is for shell
     A_PROMPT_PATH = A.BOLD + A.FG_CYAN
     A_PROMPT_SUF = A.BOLD
+    # Files
+    A_DIRECTORY = A.BOLD + A.FG_CYAN
+    A_FILE = A.FG_WHITE
 
     # exit codes
     EXIT_OK = 0