# HG changeset patch # User Goffi # Date 1519838919 -3600 # Node ID 8e770ac05b0c621838a83c604ba388acc3ccd448 # Parent 67cc54b01a124b481d37f9271b40a6858986a535 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 diff -r 67cc54b01a12 -r 8e770ac05b0c frontends/src/jp/cmd_file.py --- 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')) diff -r 67cc54b01a12 -r 8e770ac05b0c frontends/src/jp/constants.py --- 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