comparison sat_frontends/jp/cmd_file.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents frontends/src/jp/cmd_file.py@842bd1594077
children c0401a72cbb4
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # jp: a SAT command line tool
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20
21 import base
22 import sys
23 import os
24 import os.path
25 import tarfile
26 from sat.core.i18n import _
27 from sat_frontends.jp.constants import Const as C
28 from sat_frontends.jp import common
29 from sat_frontends.tools import jid
30 from sat.tools.common.ansi import ANSI as A
31 import tempfile
32 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI
33 from functools import partial
34 import json
35
36 __commands__ = ["File"]
37
38
39 class Send(base.CommandBase):
40 def __init__(self, host):
41 super(Send, self).__init__(host, 'send', use_progress=True, use_verbose=True, help=_('send a file to a contact'))
42 self.need_loop=True
43
44 def add_parser_options(self):
45 self.parser.add_argument("files", type=str, nargs='+', metavar='file', help=_(u"a list of file"))
46 self.parser.add_argument("jid", type=base.unicode_decoder, help=_(u"the destination jid"))
47 self.parser.add_argument("-b", "--bz2", action="store_true", help=_(u"make a bzip2 tarball"))
48 self.parser.add_argument("-d", "--path", type=base.unicode_decoder, help=(u"path to the directory where the file must be stored"))
49 self.parser.add_argument("-N", "--namespace", type=base.unicode_decoder, help=(u"namespace of the file"))
50 self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default=u'', help=(u"name to use (DEFAULT: use source file name)"))
51
52 def start(self):
53 """Send files to jabber contact"""
54 self.send_files()
55
56 def onProgressStarted(self, metadata):
57 self.disp(_(u'File copy started'),2)
58
59 def onProgressFinished(self, metadata):
60 self.disp(_(u'File sent successfully'),2)
61
62 def onProgressError(self, error_msg):
63 if error_msg == C.PROGRESS_ERROR_DECLINED:
64 self.disp(_(u'The file has been refused by your contact'))
65 else:
66 self.disp(_(u'Error while sending file: {}').format(error_msg),error=True)
67
68 def gotId(self, data, file_):
69 """Called when a progress id has been received
70
71 @param pid(unicode): progress id
72 @param file_(str): file path
73 """
74 #FIXME: this show progress only for last progress_id
75 self.disp(_(u"File request sent to {jid}".format(jid=self.full_dest_jid)), 1)
76 try:
77 self.progress_id = data['progress']
78 except KeyError:
79 # TODO: if 'xmlui' key is present, manage xmlui message display
80 self.disp(_(u"Can't send file to {jid}".format(jid=self.full_dest_jid)), error=True)
81 self.host.quit(2)
82
83 def error(self, failure):
84 self.disp(_("Error while trying to send a file: {reason}").format(reason=failure), error=True)
85 self.host.quit(1)
86
87 def send_files(self):
88 for file_ in self.args.files:
89 if not os.path.exists(file_):
90 self.disp(_(u"file [{}] doesn't exist !").format(file_), error=True)
91 self.host.quit(1)
92 if not self.args.bz2 and os.path.isdir(file_):
93 self.disp(_(u"[{}] is a dir ! Please send files inside or use compression").format(file_))
94 self.host.quit(1)
95
96 self.full_dest_jid = self.host.get_full_jid(self.args.jid)
97 extra = {}
98 if self.args.path:
99 extra[u'path'] = self.args.path
100 if self.args.namespace:
101 extra[u'namespace'] = self.args.namespace
102
103 if self.args.bz2:
104 with tempfile.NamedTemporaryFile('wb', delete=False) as buf:
105 self.host.addOnQuitCallback(os.unlink, buf.name)
106 self.disp(_(u"bz2 is an experimental option, use with caution"))
107 #FIXME: check free space
108 self.disp(_(u"Starting compression, please wait..."))
109 sys.stdout.flush()
110 bz2 = tarfile.open(mode="w:bz2", fileobj=buf)
111 archive_name = u'{}.tar.bz2'.format(os.path.basename(self.args.files[0]) or u'compressed_files')
112 for file_ in self.args.files:
113 self.disp(_(u"Adding {}").format(file_), 1)
114 bz2.add(file_)
115 bz2.close()
116 self.disp(_(u"Done !"), 1)
117
118 self.host.bridge.fileSend(self.full_dest_jid, buf.name, self.args.name or archive_name, '', extra, self.profile,
119 callback=lambda pid, file_=buf.name: self.gotId(pid, file_), errback=self.error)
120 else:
121 for file_ in self.args.files:
122 path = os.path.abspath(file_)
123 self.host.bridge.fileSend(self.full_dest_jid, path, self.args.name, '', extra, self.profile,
124 callback=lambda pid, file_=file_: self.gotId(pid, file_), errback=self.error)
125
126
127 class Request(base.CommandBase):
128
129 def __init__(self, host):
130 super(Request, self).__init__(host, 'request', use_progress=True, use_verbose=True, help=_('request a file from a contact'))
131 self.need_loop=True
132
133 @property
134 def filename(self):
135 return self.args.name or self.args.hash or u"output"
136
137 def add_parser_options(self):
138 self.parser.add_argument("jid", type=base.unicode_decoder, help=_(u"the destination jid"))
139 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])"))
140 self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default=u'', help=_(u"name of the file"))
141 self.parser.add_argument("-H", "--hash", type=base.unicode_decoder, default=u'', help=_(u"hash of the file"))
142 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)"))
143 self.parser.add_argument("-d", "--path", type=base.unicode_decoder, help=(u"path to the directory containing the file"))
144 self.parser.add_argument("-N", "--namespace", type=base.unicode_decoder, help=(u"namespace of the file"))
145 self.parser.add_argument("-f", "--force", action='store_true', help=_(u"overwrite existing file without confirmation"))
146
147 def onProgressStarted(self, metadata):
148 self.disp(_(u'File copy started'),2)
149
150 def onProgressFinished(self, metadata):
151 self.disp(_(u'File received successfully'),2)
152
153 def onProgressError(self, error_msg):
154 if error_msg == C.PROGRESS_ERROR_DECLINED:
155 self.disp(_(u'The file request has been refused'))
156 else:
157 self.disp(_(u'Error while requesting file: {}').format(error_msg), error=True)
158
159 def gotId(self, progress_id):
160 """Called when a progress id has been received
161
162 @param progress_id(unicode): progress id
163 """
164 self.progress_id = progress_id
165
166 def error(self, failure):
167 self.disp(_("Error while trying to send a file: {reason}").format(reason=failure), error=True)
168 self.host.quit(1)
169
170 def start(self):
171 if not self.args.name and not self.args.hash:
172 self.parser.error(_(u'at least one of --name or --hash must be provided'))
173 # extra = dict(self.args.extra)
174 if self.args.dest:
175 path = os.path.abspath(os.path.expanduser(self.args.dest))
176 if os.path.isdir(path):
177 path = os.path.join(path, self.filename)
178 else:
179 path = os.path.abspath(self.filename)
180
181 if os.path.exists(path) and not self.args.force:
182 message = _(u'File {path} already exists! Do you want to overwrite?').format(path=path)
183 confirm = raw_input(u"{} (y/N) ".format(message).encode('utf-8'))
184 if confirm not in (u"y", u"Y"):
185 self.disp(_(u"file request cancelled"))
186 self.host.quit(2)
187
188 self.full_dest_jid = self.host.get_full_jid(self.args.jid)
189 extra = {}
190 if self.args.path:
191 extra[u'path'] = self.args.path
192 if self.args.namespace:
193 extra[u'namespace'] = self.args.namespace
194 self.host.bridge.fileJingleRequest(self.full_dest_jid,
195 path,
196 self.args.name,
197 self.args.hash,
198 self.args.hash_algo if self.args.hash else u'',
199 extra,
200 self.profile,
201 callback=self.gotId,
202 errback=partial(self.errback,
203 msg=_(u"can't request file: {}"),
204 exit_code=C.EXIT_BRIDGE_ERRBACK))
205
206
207 class Receive(base.CommandAnswering):
208
209 def __init__(self, host):
210 super(Receive, self).__init__(host, 'receive', use_progress=True, use_verbose=True, help=_('wait for a file to be sent by a contact'))
211 self._overwrite_refused = False # True when one overwrite as already been refused
212 self.action_callbacks = {C.META_TYPE_FILE: self.onFileAction,
213 C.META_TYPE_OVERWRITE: self.onOverwriteAction}
214
215 def onProgressStarted(self, metadata):
216 self.disp(_(u'File copy started'),2)
217
218 def onProgressFinished(self, metadata):
219 self.disp(_(u'File received successfully'),2)
220 if metadata.get('hash_verified', False):
221 try:
222 self.disp(_(u'hash checked: {algo}:{checksum}').format(
223 algo=metadata['hash_algo'],
224 checksum=metadata['hash']),
225 1)
226 except KeyError:
227 self.disp(_(u'hash is checked but hash value is missing', 1), error=True)
228 else:
229 self.disp(_(u"hash can't be verified"), 1)
230
231 def onProgressError(self, error_msg):
232 self.disp(_(u'Error while receiving file: {}').format(error_msg),error=True)
233
234 def getXmluiId(self, action_data):
235 # FIXME: we temporarily use ElementTree, but a real XMLUI managing module
236 # should be available in the futur
237 # TODO: XMLUI module
238 try:
239 xml_ui = action_data['xmlui']
240 except KeyError:
241 self.disp(_(u"Action has no XMLUI"), 1)
242 else:
243 ui = ET.fromstring(xml_ui.encode('utf-8'))
244 xmlui_id = ui.get('submit')
245 if not xmlui_id:
246 self.disp(_(u"Invalid XMLUI received"), error=True)
247 return xmlui_id
248
249 def onFileAction(self, action_data, action_id, security_limit, profile):
250 xmlui_id = self.getXmluiId(action_data)
251 if xmlui_id is None:
252 return self.host.quitFromSignal(1)
253 try:
254 from_jid = jid.JID(action_data['meta_from_jid'])
255 except KeyError:
256 self.disp(_(u"Ignoring action without from_jid data"), 1)
257 return
258 try:
259 progress_id = action_data['meta_progress_id']
260 except KeyError:
261 self.disp(_(u"ignoring action without progress id"), 1)
262 return
263
264 if not self.bare_jids or from_jid.bare in self.bare_jids:
265 if self._overwrite_refused:
266 self.disp(_(u"File refused because overwrite is needed"), error=True)
267 self.host.bridge.launchAction(xmlui_id, {'cancelled': C.BOOL_TRUE}, profile_key=profile)
268 return self.host.quitFromSignal(2)
269 self.progress_id = progress_id
270 xmlui_data = {'path': self.path}
271 self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
272
273 def onOverwriteAction(self, action_data, action_id, security_limit, profile):
274 xmlui_id = self.getXmluiId(action_data)
275 if xmlui_id is None:
276 return self.host.quitFromSignal(1)
277 try:
278 progress_id = action_data['meta_progress_id']
279 except KeyError:
280 self.disp(_(u"ignoring action without progress id"), 1)
281 return
282 self.disp(_(u"Overwriting needed"), 1)
283
284 if progress_id == self.progress_id:
285 if self.args.force:
286 self.disp(_(u"Overwrite accepted"), 2)
287 else:
288 self.disp(_(u"Refused to overwrite"), 2)
289 self._overwrite_refused = True
290
291 xmlui_data = {'answer': C.boolConst(self.args.force)}
292 self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
293
294 def add_parser_options(self):
295 self.parser.add_argument("jids", type=base.unicode_decoder, nargs="*", help=_(u'jids accepted (accept everything if none is specified)'))
296 self.parser.add_argument("-m", "--multiple", action="store_true", help=_(u"accept multiple files (you'll have to stop manually)"))
297 self.parser.add_argument("-f", "--force", action="store_true", help=_(u"force overwritting of existing files (/!\\ name is choosed by sender)"))
298 self.parser.add_argument("--path", default='.', metavar='DIR', help=_(u"destination path (default: working directory)"))
299
300 def start(self):
301 self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids]
302 self.path = os.path.abspath(self.args.path)
303 if not os.path.isdir(self.path):
304 self.disp(_(u"Given path is not a directory !", error=True))
305 self.host.quit(2)
306 if self.args.multiple:
307 self.host.quit_on_progress_end = False
308 self.disp(_(u"waiting for incoming file request"),2)
309
310
311 class Upload(base.CommandBase):
312
313 def __init__(self, host):
314 super(Upload, self).__init__(host, 'upload', use_progress=True, use_verbose=True, help=_('upload a file'))
315 self.need_loop=True
316
317 def add_parser_options(self):
318 self.parser.add_argument("file", type=str, help=_("file to upload"))
319 self.parser.add_argument("jid", type=base.unicode_decoder, nargs='?', help=_("jid of upload component (nothing to autodetect)"))
320 self.parser.add_argument("--ignore-tls-errors", action="store_true", help=_("ignore invalide TLS certificate"))
321
322 def onProgressStarted(self, metadata):
323 self.disp(_(u'File upload started'),2)
324
325 def onProgressFinished(self, metadata):
326 self.disp(_(u'File uploaded successfully'),2)
327 try:
328 url = metadata['url']
329 except KeyError:
330 self.disp(u'download URL not found in metadata')
331 else:
332 self.disp(_(u'URL to retrieve the file:'),1)
333 # XXX: url is display alone on a line to make parsing easier
334 self.disp(url)
335
336 def onProgressError(self, error_msg):
337 self.disp(_(u'Error while uploading file: {}').format(error_msg),error=True)
338
339 def gotId(self, data, file_):
340 """Called when a progress id has been received
341
342 @param pid(unicode): progress id
343 @param file_(str): file path
344 """
345 try:
346 self.progress_id = data['progress']
347 except KeyError:
348 # TODO: if 'xmlui' key is present, manage xmlui message display
349 self.disp(_(u"Can't upload file"), error=True)
350 self.host.quit(2)
351
352 def error(self, failure):
353 self.disp(_("Error while trying to upload a file: {reason}").format(reason=failure), error=True)
354 self.host.quit(1)
355
356 def start(self):
357 file_ = self.args.file
358 if not os.path.exists(file_):
359 self.disp(_(u"file [{}] doesn't exist !").format(file_), error=True)
360 self.host.quit(1)
361 if os.path.isdir(file_):
362 self.disp(_(u"[{}] is a dir! Can't upload a dir").format(file_))
363 self.host.quit(1)
364
365 self.full_dest_jid = self.host.get_full_jid(self.args.jid) if self.args.jid is not None else ''
366 options = {}
367 if self.args.ignore_tls_errors:
368 options['ignore_tls_errors'] = C.BOOL_TRUE
369
370 path = os.path.abspath(file_)
371 self.host.bridge.fileUpload(path, '', self.full_dest_jid, options, self.profile, callback=lambda pid, file_=file_: self.gotId(pid, file_), errback=self.error)
372
373
374 class ShareList(base.CommandBase):
375
376 def __init__(self, host):
377 extra_outputs = {'default': self.default_output}
378 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)
379 self.need_loop=True
380
381 def add_parser_options(self):
382 self.parser.add_argument("-d", "--path", default=u'', help=_(u"path to the directory containing the files"))
383 self.parser.add_argument("jid", type=base.unicode_decoder, nargs='?', help=_("jid of sharing entity (nothing to check our own jid)"))
384
385 def file_gen(self, files_data):
386 for file_data in files_data:
387 yield file_data[u'name']
388 yield file_data.get(u'size', '')
389 yield file_data.get(u'hash','')
390
391 def _name_filter(self, name, row):
392 if row.type == C.FILE_TYPE_DIRECTORY:
393 return A.color(C.A_DIRECTORY, name)
394 elif row.type == C.FILE_TYPE_FILE:
395 return A.color(C.A_FILE, name)
396 else:
397 self.disp(_(u'unknown file type: {type}').format(type=row.type), error=True)
398 return name
399
400 def _size_filter(self, size, row):
401 if not size:
402 return u''
403 size = int(size)
404 # cf. https://stackoverflow.com/a/1094933 (thanks)
405 suffix = u'o'
406 for unit in [u'', u'Ki', u'Mi', u'Gi', u'Ti', u'Pi', u'Ei', u'Zi']:
407 if abs(size) < 1024.0:
408 return A.color(A.BOLD, u"{:.2f}".format(size), unit, suffix)
409 size /= 1024.0
410
411 return A.color(A.BOLD, u"{:.2f}".format(size), u'Yi', suffix)
412
413 def default_output(self, files_data):
414 """display files a way similar to ls"""
415 files_data.sort(key=lambda d: d['name'].lower())
416 show_header = False
417 if self.verbosity == 0:
418 headers = (u'name', u'type')
419 elif self.verbosity == 1:
420 headers = (u'name', u'type', u'size')
421 elif self.verbosity > 1:
422 show_header = True
423 headers = (u'name', u'type', u'size', u'hash')
424 table = common.Table.fromDict(self.host,
425 files_data,
426 headers,
427 filters={u'name': self._name_filter,
428 u'size': self._size_filter},
429 defaults={u'size': u'',
430 u'hash': u''},
431 )
432 table.display_blank(show_header=show_header, hide_cols=['type'])
433
434 def _FISListCb(self, files_data):
435 self.output(files_data)
436 self.host.quit()
437
438 def start(self):
439 self.host.bridge.FISList(
440 self.args.jid,
441 self.args.path,
442 {},
443 self.profile,
444 callback=self._FISListCb,
445 errback=partial(self.errback,
446 msg=_(u"can't retrieve shared files: {}"),
447 exit_code=C.EXIT_BRIDGE_ERRBACK))
448
449
450 class SharePath(base.CommandBase):
451
452 def __init__(self, host):
453 super(SharePath, self).__init__(host, 'path', help=_(u'share a file or directory'), use_verbose=True)
454 self.need_loop=True
455
456 def add_parser_options(self):
457 self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default=u'', help=_(u"virtual name to use (default: use directory/file name)"))
458 perm_group = self.parser.add_mutually_exclusive_group()
459 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"))
460 perm_group.add_argument("--public", action='store_true', help=_(u"share publicly the file(s) (/!\\ *everybody* will be able to access them)"))
461 self.parser.add_argument("path", type=base.unicode_decoder, help=_(u"path to a file or directory to share"))
462
463
464 def _FISSharePathCb(self, name):
465 self.disp(_(u'{path} shared under the name "{name}"').format(
466 path = self.path,
467 name = name))
468 self.host.quit()
469
470 def start(self):
471 self.path = os.path.abspath(self.args.path)
472 if self.args.public:
473 access = {u'read': {u'type': u'public'}}
474 else:
475 jids = self.args.jids
476 if jids:
477 access = {u'read': {u'type': 'whitelist',
478 u'jids': jids}}
479 else:
480 access = {}
481 self.host.bridge.FISSharePath(
482 self.args.name,
483 self.path,
484 json.dumps(access, ensure_ascii=False),
485 self.profile,
486 callback=self._FISSharePathCb,
487 errback=partial(self.errback,
488 msg=_(u"can't share path: {}"),
489 exit_code=C.EXIT_BRIDGE_ERRBACK))
490
491
492 class Share(base.CommandBase):
493 subcommands = (ShareList, SharePath)
494
495 def __init__(self, host):
496 super(Share, self).__init__(host, 'share', use_profile=False, help=_(u'files sharing management'))
497
498
499 class File(base.CommandBase):
500 subcommands = (Send, Request, Receive, Upload, Share)
501
502 def __init__(self, host):
503 super(File, self).__init__(host, 'file', use_profile=False, help=_(u'files sending/receiving/management'))