Mercurial > libervia-backend
comparison libervia/cli/cmd_file.py @ 4075:47401850dec6
refactoring: rename `libervia.frontends.jp` to `libervia.cli`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 14:54:26 +0200 |
parents | libervia/frontends/jp/cmd_file.py@26b7ed2817da |
children | cd889f4771cb |
comparison
equal
deleted
inserted
replaced
4074:26b7ed2817da | 4075:47401850dec6 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 | |
4 # Libervia CLI | |
5 # Copyright (C) 2009-2021 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 from . import base | |
22 from . import xmlui_manager | |
23 import sys | |
24 import os | |
25 import os.path | |
26 import tarfile | |
27 from libervia.backend.core.i18n import _ | |
28 from libervia.backend.tools.common import data_format | |
29 from libervia.cli.constants import Const as C | |
30 from libervia.cli import common | |
31 from libervia.frontends.tools import jid | |
32 from libervia.backend.tools.common.ansi import ANSI as A | |
33 from libervia.backend.tools.common import utils | |
34 from urllib.parse import urlparse | |
35 from pathlib import Path | |
36 import tempfile | |
37 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI | |
38 import json | |
39 | |
40 __commands__ = ["File"] | |
41 DEFAULT_DEST = "downloaded_file" | |
42 | |
43 | |
44 class Send(base.CommandBase): | |
45 def __init__(self, host): | |
46 super(Send, self).__init__( | |
47 host, | |
48 "send", | |
49 use_progress=True, | |
50 use_verbose=True, | |
51 help=_("send a file to a contact"), | |
52 ) | |
53 | |
54 def add_parser_options(self): | |
55 self.parser.add_argument( | |
56 "files", type=str, nargs="+", metavar="file", help=_("a list of file") | |
57 ) | |
58 self.parser.add_argument("jid", help=_("the destination jid")) | |
59 self.parser.add_argument( | |
60 "-b", "--bz2", action="store_true", help=_("make a bzip2 tarball") | |
61 ) | |
62 self.parser.add_argument( | |
63 "-d", | |
64 "--path", | |
65 help=("path to the directory where the file must be stored"), | |
66 ) | |
67 self.parser.add_argument( | |
68 "-N", | |
69 "--namespace", | |
70 help=("namespace of the file"), | |
71 ) | |
72 self.parser.add_argument( | |
73 "-n", | |
74 "--name", | |
75 default="", | |
76 help=("name to use (DEFAULT: use source file name)"), | |
77 ) | |
78 self.parser.add_argument( | |
79 "-e", | |
80 "--encrypt", | |
81 action="store_true", | |
82 help=_("end-to-end encrypt the file transfer") | |
83 ) | |
84 | |
85 async def on_progress_started(self, metadata): | |
86 self.disp(_("File copy started"), 2) | |
87 | |
88 async def on_progress_finished(self, metadata): | |
89 self.disp(_("File sent successfully"), 2) | |
90 | |
91 async def on_progress_error(self, error_msg): | |
92 if error_msg == C.PROGRESS_ERROR_DECLINED: | |
93 self.disp(_("The file has been refused by your contact")) | |
94 else: | |
95 self.disp(_("Error while sending file: {}").format(error_msg), error=True) | |
96 | |
97 async def got_id(self, data, file_): | |
98 """Called when a progress id has been received | |
99 | |
100 @param pid(unicode): progress id | |
101 @param file_(str): file path | |
102 """ | |
103 # FIXME: this show progress only for last progress_id | |
104 self.disp(_("File request sent to {jid}".format(jid=self.args.jid)), 1) | |
105 try: | |
106 await self.set_progress_id(data["progress"]) | |
107 except KeyError: | |
108 # TODO: if 'xmlui' key is present, manage xmlui message display | |
109 self.disp(_("Can't send file to {jid}".format(jid=self.args.jid)), error=True) | |
110 self.host.quit(2) | |
111 | |
112 async def start(self): | |
113 for file_ in self.args.files: | |
114 if not os.path.exists(file_): | |
115 self.disp( | |
116 _("file {file_} doesn't exist!").format(file_=repr(file_)), error=True | |
117 ) | |
118 self.host.quit(C.EXIT_BAD_ARG) | |
119 if not self.args.bz2 and os.path.isdir(file_): | |
120 self.disp( | |
121 _( | |
122 "{file_} is a dir! Please send files inside or use compression" | |
123 ).format(file_=repr(file_)) | |
124 ) | |
125 self.host.quit(C.EXIT_BAD_ARG) | |
126 | |
127 extra = {} | |
128 if self.args.path: | |
129 extra["path"] = self.args.path | |
130 if self.args.namespace: | |
131 extra["namespace"] = self.args.namespace | |
132 if self.args.encrypt: | |
133 extra["encrypted"] = True | |
134 | |
135 if self.args.bz2: | |
136 with tempfile.NamedTemporaryFile("wb", delete=False) as buf: | |
137 self.host.add_on_quit_callback(os.unlink, buf.name) | |
138 self.disp(_("bz2 is an experimental option, use with caution")) | |
139 # FIXME: check free space | |
140 self.disp(_("Starting compression, please wait...")) | |
141 sys.stdout.flush() | |
142 bz2 = tarfile.open(mode="w:bz2", fileobj=buf) | |
143 archive_name = "{}.tar.bz2".format( | |
144 os.path.basename(self.args.files[0]) or "compressed_files" | |
145 ) | |
146 for file_ in self.args.files: | |
147 self.disp(_("Adding {}").format(file_), 1) | |
148 bz2.add(file_) | |
149 bz2.close() | |
150 self.disp(_("Done !"), 1) | |
151 | |
152 try: | |
153 send_data = await self.host.bridge.file_send( | |
154 self.args.jid, | |
155 buf.name, | |
156 self.args.name or archive_name, | |
157 "", | |
158 data_format.serialise(extra), | |
159 self.profile, | |
160 ) | |
161 except Exception as e: | |
162 self.disp(f"can't send file: {e}", error=True) | |
163 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
164 else: | |
165 await self.got_id(send_data, file_) | |
166 else: | |
167 for file_ in self.args.files: | |
168 path = os.path.abspath(file_) | |
169 try: | |
170 send_data = await self.host.bridge.file_send( | |
171 self.args.jid, | |
172 path, | |
173 self.args.name, | |
174 "", | |
175 data_format.serialise(extra), | |
176 self.profile, | |
177 ) | |
178 except Exception as e: | |
179 self.disp(f"can't send file {file_!r}: {e}", error=True) | |
180 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
181 else: | |
182 await self.got_id(send_data, file_) | |
183 | |
184 | |
185 class Request(base.CommandBase): | |
186 def __init__(self, host): | |
187 super(Request, self).__init__( | |
188 host, | |
189 "request", | |
190 use_progress=True, | |
191 use_verbose=True, | |
192 help=_("request a file from a contact"), | |
193 ) | |
194 | |
195 @property | |
196 def filename(self): | |
197 return self.args.name or self.args.hash or "output" | |
198 | |
199 def add_parser_options(self): | |
200 self.parser.add_argument("jid", help=_("the destination jid")) | |
201 self.parser.add_argument( | |
202 "-D", | |
203 "--dest", | |
204 help=_( | |
205 "destination path where the file will be saved (default: " | |
206 "[current_dir]/[name|hash])" | |
207 ), | |
208 ) | |
209 self.parser.add_argument( | |
210 "-n", | |
211 "--name", | |
212 default="", | |
213 help=_("name of the file"), | |
214 ) | |
215 self.parser.add_argument( | |
216 "-H", | |
217 "--hash", | |
218 default="", | |
219 help=_("hash of the file"), | |
220 ) | |
221 self.parser.add_argument( | |
222 "-a", | |
223 "--hash-algo", | |
224 default="sha-256", | |
225 help=_("hash algorithm use for --hash (default: sha-256)"), | |
226 ) | |
227 self.parser.add_argument( | |
228 "-d", | |
229 "--path", | |
230 help=("path to the directory containing the file"), | |
231 ) | |
232 self.parser.add_argument( | |
233 "-N", | |
234 "--namespace", | |
235 help=("namespace of the file"), | |
236 ) | |
237 self.parser.add_argument( | |
238 "-f", | |
239 "--force", | |
240 action="store_true", | |
241 help=_("overwrite existing file without confirmation"), | |
242 ) | |
243 | |
244 async def on_progress_started(self, metadata): | |
245 self.disp(_("File copy started"), 2) | |
246 | |
247 async def on_progress_finished(self, metadata): | |
248 self.disp(_("File received successfully"), 2) | |
249 | |
250 async def on_progress_error(self, error_msg): | |
251 if error_msg == C.PROGRESS_ERROR_DECLINED: | |
252 self.disp(_("The file request has been refused")) | |
253 else: | |
254 self.disp(_("Error while requesting file: {}").format(error_msg), error=True) | |
255 | |
256 async def start(self): | |
257 if not self.args.name and not self.args.hash: | |
258 self.parser.error(_("at least one of --name or --hash must be provided")) | |
259 if self.args.dest: | |
260 path = os.path.abspath(os.path.expanduser(self.args.dest)) | |
261 if os.path.isdir(path): | |
262 path = os.path.join(path, self.filename) | |
263 else: | |
264 path = os.path.abspath(self.filename) | |
265 | |
266 if os.path.exists(path) and not self.args.force: | |
267 message = _("File {path} already exists! Do you want to overwrite?").format( | |
268 path=path | |
269 ) | |
270 await self.host.confirm_or_quit(message, _("file request cancelled")) | |
271 | |
272 self.full_dest_jid = await self.host.get_full_jid(self.args.jid) | |
273 extra = {} | |
274 if self.args.path: | |
275 extra["path"] = self.args.path | |
276 if self.args.namespace: | |
277 extra["namespace"] = self.args.namespace | |
278 try: | |
279 progress_id = await self.host.bridge.file_jingle_request( | |
280 self.full_dest_jid, | |
281 path, | |
282 self.args.name, | |
283 self.args.hash, | |
284 self.args.hash_algo if self.args.hash else "", | |
285 extra, | |
286 self.profile, | |
287 ) | |
288 except Exception as e: | |
289 self.disp(msg=_("can't request file: {e}").format(e=e), error=True) | |
290 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
291 else: | |
292 await self.set_progress_id(progress_id) | |
293 | |
294 | |
295 class Receive(base.CommandAnswering): | |
296 def __init__(self, host): | |
297 super(Receive, self).__init__( | |
298 host, | |
299 "receive", | |
300 use_progress=True, | |
301 use_verbose=True, | |
302 help=_("wait for a file to be sent by a contact"), | |
303 ) | |
304 self._overwrite_refused = False # True when one overwrite as already been refused | |
305 self.action_callbacks = { | |
306 C.META_TYPE_FILE: self.on_file_action, | |
307 C.META_TYPE_OVERWRITE: self.on_overwrite_action, | |
308 C.META_TYPE_NOT_IN_ROSTER_LEAK: self.on_not_in_roster_action, | |
309 } | |
310 | |
311 def add_parser_options(self): | |
312 self.parser.add_argument( | |
313 "jids", | |
314 nargs="*", | |
315 help=_("jids accepted (accept everything if none is specified)"), | |
316 ) | |
317 self.parser.add_argument( | |
318 "-m", | |
319 "--multiple", | |
320 action="store_true", | |
321 help=_("accept multiple files (you'll have to stop manually)"), | |
322 ) | |
323 self.parser.add_argument( | |
324 "-f", | |
325 "--force", | |
326 action="store_true", | |
327 help=_( | |
328 "force overwritting of existing files (/!\\ name is choosed by sender)" | |
329 ), | |
330 ) | |
331 self.parser.add_argument( | |
332 "--path", | |
333 default=".", | |
334 metavar="DIR", | |
335 help=_("destination path (default: working directory)"), | |
336 ) | |
337 | |
338 async def on_progress_started(self, metadata): | |
339 self.disp(_("File copy started"), 2) | |
340 | |
341 async def on_progress_finished(self, metadata): | |
342 self.disp(_("File received successfully"), 2) | |
343 if metadata.get("hash_verified", False): | |
344 try: | |
345 self.disp( | |
346 _("hash checked: {metadata['hash_algo']}:{metadata['hash']}"), 1 | |
347 ) | |
348 except KeyError: | |
349 self.disp(_("hash is checked but hash value is missing", 1), error=True) | |
350 else: | |
351 self.disp(_("hash can't be verified"), 1) | |
352 | |
353 async def on_progress_error(self, e): | |
354 self.disp(_("Error while receiving file: {e}").format(e=e), error=True) | |
355 | |
356 def get_xmlui_id(self, action_data): | |
357 # FIXME: we temporarily use ElementTree, but a real XMLUI managing module | |
358 # should be available in the futur | |
359 # TODO: XMLUI module | |
360 try: | |
361 xml_ui = action_data["xmlui"] | |
362 except KeyError: | |
363 self.disp(_("Action has no XMLUI"), 1) | |
364 else: | |
365 ui = ET.fromstring(xml_ui.encode("utf-8")) | |
366 xmlui_id = ui.get("submit") | |
367 if not xmlui_id: | |
368 self.disp(_("Invalid XMLUI received"), error=True) | |
369 return xmlui_id | |
370 | |
371 async def on_file_action(self, action_data, action_id, security_limit, profile): | |
372 xmlui_id = self.get_xmlui_id(action_data) | |
373 if xmlui_id is None: | |
374 return self.host.quit_from_signal(1) | |
375 try: | |
376 from_jid = jid.JID(action_data["from_jid"]) | |
377 except KeyError: | |
378 self.disp(_("Ignoring action without from_jid data"), 1) | |
379 return | |
380 try: | |
381 progress_id = action_data["progress_id"] | |
382 except KeyError: | |
383 self.disp(_("ignoring action without progress id"), 1) | |
384 return | |
385 | |
386 if not self.bare_jids or from_jid.bare in self.bare_jids: | |
387 if self._overwrite_refused: | |
388 self.disp(_("File refused because overwrite is needed"), error=True) | |
389 await self.host.bridge.action_launch( | |
390 xmlui_id, data_format.serialise({"cancelled": C.BOOL_TRUE}), | |
391 profile_key=profile | |
392 ) | |
393 return self.host.quit_from_signal(2) | |
394 await self.set_progress_id(progress_id) | |
395 xmlui_data = {"path": self.path} | |
396 await self.host.bridge.action_launch( | |
397 xmlui_id, data_format.serialise(xmlui_data), profile_key=profile | |
398 ) | |
399 | |
400 async def on_overwrite_action(self, action_data, action_id, security_limit, profile): | |
401 xmlui_id = self.get_xmlui_id(action_data) | |
402 if xmlui_id is None: | |
403 return self.host.quit_from_signal(1) | |
404 try: | |
405 progress_id = action_data["progress_id"] | |
406 except KeyError: | |
407 self.disp(_("ignoring action without progress id"), 1) | |
408 return | |
409 self.disp(_("Overwriting needed"), 1) | |
410 | |
411 if progress_id == self.progress_id: | |
412 if self.args.force: | |
413 self.disp(_("Overwrite accepted"), 2) | |
414 else: | |
415 self.disp(_("Refused to overwrite"), 2) | |
416 self._overwrite_refused = True | |
417 | |
418 xmlui_data = {"answer": C.bool_const(self.args.force)} | |
419 await self.host.bridge.action_launch( | |
420 xmlui_id, data_format.serialise(xmlui_data), profile_key=profile | |
421 ) | |
422 | |
423 async def on_not_in_roster_action(self, action_data, action_id, security_limit, profile): | |
424 xmlui_id = self.get_xmlui_id(action_data) | |
425 if xmlui_id is None: | |
426 return self.host.quit_from_signal(1) | |
427 try: | |
428 from_jid = jid.JID(action_data["from_jid"]) | |
429 except ValueError: | |
430 self.disp( | |
431 _('invalid "from_jid" value received, ignoring: {value}').format( | |
432 value=from_jid | |
433 ), | |
434 error=True, | |
435 ) | |
436 return | |
437 except KeyError: | |
438 self.disp(_('ignoring action without "from_jid" value'), error=True) | |
439 return | |
440 self.disp(_("Confirmation needed for request from an entity not in roster"), 1) | |
441 | |
442 if from_jid.bare in self.bare_jids: | |
443 # if the sender is expected, we can confirm the session | |
444 confirmed = True | |
445 self.disp(_("Sender confirmed because she or he is explicitly expected"), 1) | |
446 else: | |
447 xmlui = xmlui_manager.create(self.host, action_data["xmlui"]) | |
448 confirmed = await self.host.confirm(xmlui.dlg.message) | |
449 | |
450 xmlui_data = {"answer": C.bool_const(confirmed)} | |
451 await self.host.bridge.action_launch( | |
452 xmlui_id, data_format.serialise(xmlui_data), profile_key=profile | |
453 ) | |
454 if not confirmed and not self.args.multiple: | |
455 self.disp(_("Session refused for {from_jid}").format(from_jid=from_jid)) | |
456 self.host.quit_from_signal(0) | |
457 | |
458 async def start(self): | |
459 self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids] | |
460 self.path = os.path.abspath(self.args.path) | |
461 if not os.path.isdir(self.path): | |
462 self.disp(_("Given path is not a directory !", error=True)) | |
463 self.host.quit(C.EXIT_BAD_ARG) | |
464 if self.args.multiple: | |
465 self.host.quit_on_progress_end = False | |
466 self.disp(_("waiting for incoming file request"), 2) | |
467 await self.start_answering() | |
468 | |
469 | |
470 class Get(base.CommandBase): | |
471 def __init__(self, host): | |
472 super(Get, self).__init__( | |
473 host, | |
474 "get", | |
475 use_progress=True, | |
476 use_verbose=True, | |
477 help=_("download a file from URI"), | |
478 ) | |
479 | |
480 def add_parser_options(self): | |
481 self.parser.add_argument( | |
482 "-o", | |
483 "--dest-file", | |
484 type=str, | |
485 default="", | |
486 help=_("destination file (DEFAULT: filename from URL)"), | |
487 ) | |
488 self.parser.add_argument( | |
489 "-f", | |
490 "--force", | |
491 action="store_true", | |
492 help=_("overwrite existing file without confirmation"), | |
493 ) | |
494 self.parser.add_argument( | |
495 "attachment", type=str, | |
496 help=_("URI of the file to retrieve or JSON of the whole attachment") | |
497 ) | |
498 | |
499 async def on_progress_started(self, metadata): | |
500 self.disp(_("File download started"), 2) | |
501 | |
502 async def on_progress_finished(self, metadata): | |
503 self.disp(_("File downloaded successfully"), 2) | |
504 | |
505 async def on_progress_error(self, error_msg): | |
506 self.disp(_("Error while downloading file: {}").format(error_msg), error=True) | |
507 | |
508 async def got_id(self, data): | |
509 """Called when a progress id has been received""" | |
510 try: | |
511 await self.set_progress_id(data["progress"]) | |
512 except KeyError: | |
513 if "xmlui" in data: | |
514 ui = xmlui_manager.create(self.host, data["xmlui"]) | |
515 await ui.show() | |
516 else: | |
517 self.disp(_("Can't download file"), error=True) | |
518 self.host.quit(C.EXIT_ERROR) | |
519 | |
520 async def start(self): | |
521 try: | |
522 attachment = json.loads(self.args.attachment) | |
523 except json.JSONDecodeError: | |
524 attachment = {"uri": self.args.attachment} | |
525 dest_file = self.args.dest_file | |
526 if not dest_file: | |
527 try: | |
528 dest_file = attachment["name"].replace("/", "-").strip() | |
529 except KeyError: | |
530 try: | |
531 dest_file = Path(urlparse(attachment["uri"]).path).name.strip() | |
532 except KeyError: | |
533 pass | |
534 if not dest_file: | |
535 dest_file = "downloaded_file" | |
536 | |
537 dest_file = Path(dest_file).expanduser().resolve() | |
538 if dest_file.exists() and not self.args.force: | |
539 message = _("File {path} already exists! Do you want to overwrite?").format( | |
540 path=dest_file | |
541 ) | |
542 await self.host.confirm_or_quit(message, _("file download cancelled")) | |
543 | |
544 options = {} | |
545 | |
546 try: | |
547 download_data_s = await self.host.bridge.file_download( | |
548 data_format.serialise(attachment), | |
549 str(dest_file), | |
550 data_format.serialise(options), | |
551 self.profile, | |
552 ) | |
553 download_data = data_format.deserialise(download_data_s) | |
554 except Exception as e: | |
555 self.disp(f"error while trying to download a file: {e}", error=True) | |
556 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
557 else: | |
558 await self.got_id(download_data) | |
559 | |
560 | |
561 class Upload(base.CommandBase): | |
562 def __init__(self, host): | |
563 super(Upload, self).__init__( | |
564 host, "upload", use_progress=True, use_verbose=True, help=_("upload a file") | |
565 ) | |
566 | |
567 def add_parser_options(self): | |
568 self.parser.add_argument( | |
569 "-e", | |
570 "--encrypt", | |
571 action="store_true", | |
572 help=_("encrypt file using AES-GCM"), | |
573 ) | |
574 self.parser.add_argument("file", type=str, help=_("file to upload")) | |
575 self.parser.add_argument( | |
576 "jid", | |
577 nargs="?", | |
578 help=_("jid of upload component (nothing to autodetect)"), | |
579 ) | |
580 self.parser.add_argument( | |
581 "--ignore-tls-errors", | |
582 action="store_true", | |
583 help=_(r"ignore invalide TLS certificate (/!\ Dangerous /!\)"), | |
584 ) | |
585 | |
586 async def on_progress_started(self, metadata): | |
587 self.disp(_("File upload started"), 2) | |
588 | |
589 async def on_progress_finished(self, metadata): | |
590 self.disp(_("File uploaded successfully"), 2) | |
591 try: | |
592 url = metadata["url"] | |
593 except KeyError: | |
594 self.disp("download URL not found in metadata") | |
595 else: | |
596 self.disp(_("URL to retrieve the file:"), 1) | |
597 # XXX: url is displayed alone on a line to make parsing easier | |
598 self.disp(url) | |
599 | |
600 async def on_progress_error(self, error_msg): | |
601 self.disp(_("Error while uploading file: {}").format(error_msg), error=True) | |
602 | |
603 async def got_id(self, data, file_): | |
604 """Called when a progress id has been received | |
605 | |
606 @param pid(unicode): progress id | |
607 @param file_(str): file path | |
608 """ | |
609 try: | |
610 await self.set_progress_id(data["progress"]) | |
611 except KeyError: | |
612 if "xmlui" in data: | |
613 ui = xmlui_manager.create(self.host, data["xmlui"]) | |
614 await ui.show() | |
615 else: | |
616 self.disp(_("Can't upload file"), error=True) | |
617 self.host.quit(C.EXIT_ERROR) | |
618 | |
619 async def start(self): | |
620 file_ = self.args.file | |
621 if not os.path.exists(file_): | |
622 self.disp( | |
623 _("file {file_} doesn't exist !").format(file_=repr(file_)), error=True | |
624 ) | |
625 self.host.quit(C.EXIT_BAD_ARG) | |
626 if os.path.isdir(file_): | |
627 self.disp(_("{file_} is a dir! Can't upload a dir").format(file_=repr(file_))) | |
628 self.host.quit(C.EXIT_BAD_ARG) | |
629 | |
630 if self.args.jid is None: | |
631 self.full_dest_jid = "" | |
632 else: | |
633 self.full_dest_jid = await self.host.get_full_jid(self.args.jid) | |
634 | |
635 options = {} | |
636 if self.args.ignore_tls_errors: | |
637 options["ignore_tls_errors"] = True | |
638 if self.args.encrypt: | |
639 options["encryption"] = C.ENC_AES_GCM | |
640 | |
641 path = os.path.abspath(file_) | |
642 try: | |
643 upload_data = await self.host.bridge.file_upload( | |
644 path, | |
645 "", | |
646 self.full_dest_jid, | |
647 data_format.serialise(options), | |
648 self.profile, | |
649 ) | |
650 except Exception as e: | |
651 self.disp(f"error while trying to upload a file: {e}", error=True) | |
652 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
653 else: | |
654 await self.got_id(upload_data, file_) | |
655 | |
656 | |
657 class ShareAffiliationsSet(base.CommandBase): | |
658 def __init__(self, host): | |
659 super(ShareAffiliationsSet, self).__init__( | |
660 host, | |
661 "set", | |
662 use_output=C.OUTPUT_DICT, | |
663 help=_("set affiliations for a shared file/directory"), | |
664 ) | |
665 | |
666 def add_parser_options(self): | |
667 self.parser.add_argument( | |
668 "-N", | |
669 "--namespace", | |
670 default="", | |
671 help=_("namespace of the repository"), | |
672 ) | |
673 self.parser.add_argument( | |
674 "-P", | |
675 "--path", | |
676 default="", | |
677 help=_("path to the repository"), | |
678 ) | |
679 self.parser.add_argument( | |
680 "-a", | |
681 "--affiliation", | |
682 dest="affiliations", | |
683 metavar=("JID", "AFFILIATION"), | |
684 required=True, | |
685 action="append", | |
686 nargs=2, | |
687 help=_("entity/affiliation couple(s)"), | |
688 ) | |
689 self.parser.add_argument( | |
690 "jid", | |
691 help=_("jid of file sharing entity"), | |
692 ) | |
693 | |
694 async def start(self): | |
695 affiliations = dict(self.args.affiliations) | |
696 try: | |
697 affiliations = await self.host.bridge.fis_affiliations_set( | |
698 self.args.jid, | |
699 self.args.namespace, | |
700 self.args.path, | |
701 affiliations, | |
702 self.profile, | |
703 ) | |
704 except Exception as e: | |
705 self.disp(f"can't set affiliations: {e}", error=True) | |
706 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
707 else: | |
708 self.host.quit() | |
709 | |
710 | |
711 class ShareAffiliationsGet(base.CommandBase): | |
712 def __init__(self, host): | |
713 super(ShareAffiliationsGet, self).__init__( | |
714 host, | |
715 "get", | |
716 use_output=C.OUTPUT_DICT, | |
717 help=_("retrieve affiliations of a shared file/directory"), | |
718 ) | |
719 | |
720 def add_parser_options(self): | |
721 self.parser.add_argument( | |
722 "-N", | |
723 "--namespace", | |
724 default="", | |
725 help=_("namespace of the repository"), | |
726 ) | |
727 self.parser.add_argument( | |
728 "-P", | |
729 "--path", | |
730 default="", | |
731 help=_("path to the repository"), | |
732 ) | |
733 self.parser.add_argument( | |
734 "jid", | |
735 help=_("jid of sharing entity"), | |
736 ) | |
737 | |
738 async def start(self): | |
739 try: | |
740 affiliations = await self.host.bridge.fis_affiliations_get( | |
741 self.args.jid, | |
742 self.args.namespace, | |
743 self.args.path, | |
744 self.profile, | |
745 ) | |
746 except Exception as e: | |
747 self.disp(f"can't get affiliations: {e}", error=True) | |
748 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
749 else: | |
750 await self.output(affiliations) | |
751 self.host.quit() | |
752 | |
753 | |
754 class ShareAffiliations(base.CommandBase): | |
755 subcommands = (ShareAffiliationsGet, ShareAffiliationsSet) | |
756 | |
757 def __init__(self, host): | |
758 super(ShareAffiliations, self).__init__( | |
759 host, "affiliations", use_profile=False, help=_("affiliations management") | |
760 ) | |
761 | |
762 | |
763 class ShareConfigurationSet(base.CommandBase): | |
764 def __init__(self, host): | |
765 super(ShareConfigurationSet, self).__init__( | |
766 host, | |
767 "set", | |
768 use_output=C.OUTPUT_DICT, | |
769 help=_("set configuration for a shared file/directory"), | |
770 ) | |
771 | |
772 def add_parser_options(self): | |
773 self.parser.add_argument( | |
774 "-N", | |
775 "--namespace", | |
776 default="", | |
777 help=_("namespace of the repository"), | |
778 ) | |
779 self.parser.add_argument( | |
780 "-P", | |
781 "--path", | |
782 default="", | |
783 help=_("path to the repository"), | |
784 ) | |
785 self.parser.add_argument( | |
786 "-f", | |
787 "--field", | |
788 action="append", | |
789 nargs=2, | |
790 dest="fields", | |
791 required=True, | |
792 metavar=("KEY", "VALUE"), | |
793 help=_("configuration field to set (required)"), | |
794 ) | |
795 self.parser.add_argument( | |
796 "jid", | |
797 help=_("jid of file sharing entity"), | |
798 ) | |
799 | |
800 async def start(self): | |
801 configuration = dict(self.args.fields) | |
802 try: | |
803 configuration = await self.host.bridge.fis_configuration_set( | |
804 self.args.jid, | |
805 self.args.namespace, | |
806 self.args.path, | |
807 configuration, | |
808 self.profile, | |
809 ) | |
810 except Exception as e: | |
811 self.disp(f"can't set configuration: {e}", error=True) | |
812 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
813 else: | |
814 self.host.quit() | |
815 | |
816 | |
817 class ShareConfigurationGet(base.CommandBase): | |
818 def __init__(self, host): | |
819 super(ShareConfigurationGet, self).__init__( | |
820 host, | |
821 "get", | |
822 use_output=C.OUTPUT_DICT, | |
823 help=_("retrieve configuration of a shared file/directory"), | |
824 ) | |
825 | |
826 def add_parser_options(self): | |
827 self.parser.add_argument( | |
828 "-N", | |
829 "--namespace", | |
830 default="", | |
831 help=_("namespace of the repository"), | |
832 ) | |
833 self.parser.add_argument( | |
834 "-P", | |
835 "--path", | |
836 default="", | |
837 help=_("path to the repository"), | |
838 ) | |
839 self.parser.add_argument( | |
840 "jid", | |
841 help=_("jid of sharing entity"), | |
842 ) | |
843 | |
844 async def start(self): | |
845 try: | |
846 configuration = await self.host.bridge.fis_configuration_get( | |
847 self.args.jid, | |
848 self.args.namespace, | |
849 self.args.path, | |
850 self.profile, | |
851 ) | |
852 except Exception as e: | |
853 self.disp(f"can't get configuration: {e}", error=True) | |
854 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
855 else: | |
856 await self.output(configuration) | |
857 self.host.quit() | |
858 | |
859 | |
860 class ShareConfiguration(base.CommandBase): | |
861 subcommands = (ShareConfigurationGet, ShareConfigurationSet) | |
862 | |
863 def __init__(self, host): | |
864 super(ShareConfiguration, self).__init__( | |
865 host, | |
866 "configuration", | |
867 use_profile=False, | |
868 help=_("file sharing node configuration"), | |
869 ) | |
870 | |
871 | |
872 class ShareList(base.CommandBase): | |
873 def __init__(self, host): | |
874 extra_outputs = {"default": self.default_output} | |
875 super(ShareList, self).__init__( | |
876 host, | |
877 "list", | |
878 use_output=C.OUTPUT_LIST_DICT, | |
879 extra_outputs=extra_outputs, | |
880 help=_("retrieve files shared by an entity"), | |
881 use_verbose=True, | |
882 ) | |
883 | |
884 def add_parser_options(self): | |
885 self.parser.add_argument( | |
886 "-d", | |
887 "--path", | |
888 default="", | |
889 help=_("path to the directory containing the files"), | |
890 ) | |
891 self.parser.add_argument( | |
892 "jid", | |
893 nargs="?", | |
894 default="", | |
895 help=_("jid of sharing entity (nothing to check our own jid)"), | |
896 ) | |
897 | |
898 def _name_filter(self, name, row): | |
899 if row.type == C.FILE_TYPE_DIRECTORY: | |
900 return A.color(C.A_DIRECTORY, name) | |
901 elif row.type == C.FILE_TYPE_FILE: | |
902 return A.color(C.A_FILE, name) | |
903 else: | |
904 self.disp(_("unknown file type: {type}").format(type=row.type), error=True) | |
905 return name | |
906 | |
907 def _size_filter(self, size, row): | |
908 if not size: | |
909 return "" | |
910 return A.color(A.BOLD, utils.get_human_size(size)) | |
911 | |
912 def default_output(self, files_data): | |
913 """display files a way similar to ls""" | |
914 files_data.sort(key=lambda d: d["name"].lower()) | |
915 show_header = False | |
916 if self.verbosity == 0: | |
917 keys = headers = ("name", "type") | |
918 elif self.verbosity == 1: | |
919 keys = headers = ("name", "type", "size") | |
920 elif self.verbosity > 1: | |
921 show_header = True | |
922 keys = ("name", "type", "size", "file_hash") | |
923 headers = ("name", "type", "size", "hash") | |
924 table = common.Table.from_list_dict( | |
925 self.host, | |
926 files_data, | |
927 keys=keys, | |
928 headers=headers, | |
929 filters={"name": self._name_filter, "size": self._size_filter}, | |
930 defaults={"size": "", "file_hash": ""}, | |
931 ) | |
932 table.display_blank(show_header=show_header, hide_cols=["type"]) | |
933 | |
934 async def start(self): | |
935 try: | |
936 files_data = await self.host.bridge.fis_list( | |
937 self.args.jid, | |
938 self.args.path, | |
939 {}, | |
940 self.profile, | |
941 ) | |
942 except Exception as e: | |
943 self.disp(f"can't retrieve shared files: {e}", error=True) | |
944 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
945 | |
946 await self.output(files_data) | |
947 self.host.quit() | |
948 | |
949 | |
950 class SharePath(base.CommandBase): | |
951 def __init__(self, host): | |
952 super(SharePath, self).__init__( | |
953 host, "path", help=_("share a file or directory"), use_verbose=True | |
954 ) | |
955 | |
956 def add_parser_options(self): | |
957 self.parser.add_argument( | |
958 "-n", | |
959 "--name", | |
960 default="", | |
961 help=_("virtual name to use (default: use directory/file name)"), | |
962 ) | |
963 perm_group = self.parser.add_mutually_exclusive_group() | |
964 perm_group.add_argument( | |
965 "-j", | |
966 "--jid", | |
967 metavar="JID", | |
968 action="append", | |
969 dest="jids", | |
970 default=[], | |
971 help=_("jid of contacts allowed to retrieve the files"), | |
972 ) | |
973 perm_group.add_argument( | |
974 "--public", | |
975 action="store_true", | |
976 help=_( | |
977 r"share publicly the file(s) (/!\ *everybody* will be able to access " | |
978 r"them)" | |
979 ), | |
980 ) | |
981 self.parser.add_argument( | |
982 "path", | |
983 help=_("path to a file or directory to share"), | |
984 ) | |
985 | |
986 async def start(self): | |
987 self.path = os.path.abspath(self.args.path) | |
988 if self.args.public: | |
989 access = {"read": {"type": "public"}} | |
990 else: | |
991 jids = self.args.jids | |
992 if jids: | |
993 access = {"read": {"type": "whitelist", "jids": jids}} | |
994 else: | |
995 access = {} | |
996 try: | |
997 name = await self.host.bridge.fis_share_path( | |
998 self.args.name, | |
999 self.path, | |
1000 json.dumps(access, ensure_ascii=False), | |
1001 self.profile, | |
1002 ) | |
1003 except Exception as e: | |
1004 self.disp(f"can't share path: {e}", error=True) | |
1005 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
1006 else: | |
1007 self.disp( | |
1008 _('{path} shared under the name "{name}"').format( | |
1009 path=self.path, name=name | |
1010 ) | |
1011 ) | |
1012 self.host.quit() | |
1013 | |
1014 | |
1015 class ShareInvite(base.CommandBase): | |
1016 def __init__(self, host): | |
1017 super(ShareInvite, self).__init__( | |
1018 host, "invite", help=_("send invitation for a shared repository") | |
1019 ) | |
1020 | |
1021 def add_parser_options(self): | |
1022 self.parser.add_argument( | |
1023 "-n", | |
1024 "--name", | |
1025 default="", | |
1026 help=_("name of the repository"), | |
1027 ) | |
1028 self.parser.add_argument( | |
1029 "-N", | |
1030 "--namespace", | |
1031 default="", | |
1032 help=_("namespace of the repository"), | |
1033 ) | |
1034 self.parser.add_argument( | |
1035 "-P", | |
1036 "--path", | |
1037 help=_("path to the repository"), | |
1038 ) | |
1039 self.parser.add_argument( | |
1040 "-t", | |
1041 "--type", | |
1042 choices=["files", "photos"], | |
1043 default="files", | |
1044 help=_("type of the repository"), | |
1045 ) | |
1046 self.parser.add_argument( | |
1047 "-T", | |
1048 "--thumbnail", | |
1049 help=_("https URL of a image to use as thumbnail"), | |
1050 ) | |
1051 self.parser.add_argument( | |
1052 "service", | |
1053 help=_("jid of the file sharing service hosting the repository"), | |
1054 ) | |
1055 self.parser.add_argument( | |
1056 "jid", | |
1057 help=_("jid of the person to invite"), | |
1058 ) | |
1059 | |
1060 async def start(self): | |
1061 self.path = os.path.normpath(self.args.path) if self.args.path else "" | |
1062 extra = {} | |
1063 if self.args.thumbnail is not None: | |
1064 if not self.args.thumbnail.startswith("http"): | |
1065 self.parser.error(_("only http(s) links are allowed with --thumbnail")) | |
1066 else: | |
1067 extra["thumb_url"] = self.args.thumbnail | |
1068 try: | |
1069 await self.host.bridge.fis_invite( | |
1070 self.args.jid, | |
1071 self.args.service, | |
1072 self.args.type, | |
1073 self.args.namespace, | |
1074 self.path, | |
1075 self.args.name, | |
1076 data_format.serialise(extra), | |
1077 self.profile, | |
1078 ) | |
1079 except Exception as e: | |
1080 self.disp(f"can't send invitation: {e}", error=True) | |
1081 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
1082 else: | |
1083 self.disp(_("invitation sent to {jid}").format(jid=self.args.jid)) | |
1084 self.host.quit() | |
1085 | |
1086 | |
1087 class Share(base.CommandBase): | |
1088 subcommands = ( | |
1089 ShareList, | |
1090 SharePath, | |
1091 ShareInvite, | |
1092 ShareAffiliations, | |
1093 ShareConfiguration, | |
1094 ) | |
1095 | |
1096 def __init__(self, host): | |
1097 super(Share, self).__init__( | |
1098 host, "share", use_profile=False, help=_("files sharing management") | |
1099 ) | |
1100 | |
1101 | |
1102 class File(base.CommandBase): | |
1103 subcommands = (Send, Request, Receive, Get, Upload, Share) | |
1104 | |
1105 def __init__(self, host): | |
1106 super(File, self).__init__( | |
1107 host, "file", use_profile=False, help=_("files sending/receiving/management") | |
1108 ) |