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
This commit is contained in:
Goffi 2018-02-28 18:28:39 +01:00
commit ac1740d235
2 changed files with 234 additions and 14 deletions

View file

@ -25,23 +25,28 @@ import os.path
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 @@ class Send(base.CommandBase):
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 @@ class Send(base.CommandBase):
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 @@ class Send(base.CommandBase):
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 @@ class Send(base.CommandBase):
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 @@ class Receive(base.CommandAnswering):
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 Receive(base.CommandAnswering):
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 @@ class Upload(base.CommandBase):
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'))

View file

@ -46,6 +46,9 @@ class Const(constants.Const):
# 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