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 )