Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_xep_0329.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/plugins/plugin_xep_0329.py@524856bd7b19 |
children | 0d7bb4df2343 |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # SAT plugin for File Information Sharing (XEP-0329) | |
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 import mimetypes | |
20 import json | |
21 import os | |
22 import traceback | |
23 from pathlib import Path | |
24 from typing import Optional, Dict | |
25 from zope.interface import implementer | |
26 from twisted.words.protocols.jabber import xmlstream | |
27 from twisted.words.protocols.jabber import jid | |
28 from twisted.words.protocols.jabber import error as jabber_error | |
29 from twisted.internet import defer | |
30 from wokkel import disco, iwokkel, data_form | |
31 from libervia.backend.core.i18n import _ | |
32 from libervia.backend.core.xmpp import SatXMPPEntity | |
33 from libervia.backend.core import exceptions | |
34 from libervia.backend.core.constants import Const as C | |
35 from libervia.backend.core.log import getLogger | |
36 from libervia.backend.tools import stream | |
37 from libervia.backend.tools import utils | |
38 from libervia.backend.tools.common import regex | |
39 | |
40 | |
41 log = getLogger(__name__) | |
42 | |
43 PLUGIN_INFO = { | |
44 C.PI_NAME: "File Information Sharing", | |
45 C.PI_IMPORT_NAME: "XEP-0329", | |
46 C.PI_TYPE: "XEP", | |
47 C.PI_MODES: C.PLUG_MODE_BOTH, | |
48 C.PI_PROTOCOLS: ["XEP-0329"], | |
49 C.PI_DEPENDENCIES: ["XEP-0231", "XEP-0234", "XEP-0300", "XEP-0106"], | |
50 C.PI_MAIN: "XEP_0329", | |
51 C.PI_HANDLER: "yes", | |
52 C.PI_DESCRIPTION: _("""Implementation of File Information Sharing"""), | |
53 } | |
54 | |
55 NS_FIS = "urn:xmpp:fis:0" | |
56 NS_FIS_AFFILIATION = "org.salut-a-toi.fis-affiliation" | |
57 NS_FIS_CONFIGURATION = "org.salut-a-toi.fis-configuration" | |
58 NS_FIS_CREATE = "org.salut-a-toi.fis-create" | |
59 | |
60 IQ_FIS_REQUEST = f'{C.IQ_GET}/query[@xmlns="{NS_FIS}"]' | |
61 # not in the standard, but needed, and it's handy to keep it here | |
62 IQ_FIS_AFFILIATION_GET = f'{C.IQ_GET}/affiliations[@xmlns="{NS_FIS_AFFILIATION}"]' | |
63 IQ_FIS_AFFILIATION_SET = f'{C.IQ_SET}/affiliations[@xmlns="{NS_FIS_AFFILIATION}"]' | |
64 IQ_FIS_CONFIGURATION_GET = f'{C.IQ_GET}/configuration[@xmlns="{NS_FIS_CONFIGURATION}"]' | |
65 IQ_FIS_CONFIGURATION_SET = f'{C.IQ_SET}/configuration[@xmlns="{NS_FIS_CONFIGURATION}"]' | |
66 IQ_FIS_CREATE_DIR = f'{C.IQ_SET}/dir[@xmlns="{NS_FIS_CREATE}"]' | |
67 SINGLE_FILES_DIR = "files" | |
68 TYPE_VIRTUAL = "virtual" | |
69 TYPE_PATH = "path" | |
70 SHARE_TYPES = (TYPE_PATH, TYPE_VIRTUAL) | |
71 KEY_TYPE = "type" | |
72 | |
73 | |
74 class RootPathException(Exception): | |
75 """Root path is requested""" | |
76 | |
77 | |
78 class ShareNode(object): | |
79 """Node containing directory or files to share, virtual or real""" | |
80 | |
81 host = None | |
82 | |
83 def __init__(self, name, parent, type_, access, path=None): | |
84 assert type_ in SHARE_TYPES | |
85 if name is not None: | |
86 if name == ".." or "/" in name or "\\" in name: | |
87 log.warning( | |
88 _("path change chars found in name [{name}], hack attempt?").format( | |
89 name=name | |
90 ) | |
91 ) | |
92 if name == "..": | |
93 name = "--" | |
94 else: | |
95 name = regex.path_escape(name) | |
96 self.name = name | |
97 self.children = {} | |
98 self.type = type_ | |
99 self.access = {} if access is None else access | |
100 assert isinstance(self.access, dict) | |
101 self.parent = None | |
102 if parent is not None: | |
103 assert name | |
104 parent.addChild(self) | |
105 else: | |
106 assert name is None | |
107 if path is not None: | |
108 if type_ != TYPE_PATH: | |
109 raise exceptions.InternalError(_("path can only be set on path nodes")) | |
110 self._path = path | |
111 | |
112 @property | |
113 def path(self): | |
114 return self._path | |
115 | |
116 def __getitem__(self, key): | |
117 return self.children[key] | |
118 | |
119 def __contains__(self, item): | |
120 return self.children.__contains__(item) | |
121 | |
122 def __iter__(self): | |
123 return self.children.__iter__() | |
124 | |
125 def items(self): | |
126 return self.children.items() | |
127 | |
128 def values(self): | |
129 return self.children.values() | |
130 | |
131 def get_or_create(self, name, type_=TYPE_VIRTUAL, access=None): | |
132 """Get a node or create a virtual node and return it""" | |
133 if access is None: | |
134 access = {C.ACCESS_PERM_READ: {KEY_TYPE: C.ACCESS_TYPE_PUBLIC}} | |
135 try: | |
136 return self.children[name] | |
137 except KeyError: | |
138 node = ShareNode(name, self, type_=type_, access=access) | |
139 return node | |
140 | |
141 def addChild(self, node): | |
142 if node.parent is not None: | |
143 raise exceptions.ConflictError(_("a node can't have several parents")) | |
144 node.parent = self | |
145 self.children[node.name] = node | |
146 | |
147 def remove_from_parent(self): | |
148 try: | |
149 del self.parent.children[self.name] | |
150 except TypeError: | |
151 raise exceptions.InternalError( | |
152 "trying to remove a node from inexisting parent" | |
153 ) | |
154 except KeyError: | |
155 raise exceptions.InternalError("node not found in parent's children") | |
156 self.parent = None | |
157 | |
158 def _check_node_permission(self, client, node, perms, peer_jid): | |
159 """Check access to this node for peer_jid | |
160 | |
161 @param node(SharedNode): node to check access | |
162 @param perms(unicode): permissions to check, iterable of C.ACCESS_PERM_* | |
163 @param peer_jid(jid.JID): entity which try to access the node | |
164 @return (bool): True if entity can access | |
165 """ | |
166 file_data = {"access": self.access, "owner": client.jid.userhostJID()} | |
167 try: | |
168 self.host.memory.check_file_permission(file_data, peer_jid, perms) | |
169 except exceptions.PermissionError: | |
170 return False | |
171 else: | |
172 return True | |
173 | |
174 def check_permissions( | |
175 self, client, peer_jid, perms=(C.ACCESS_PERM_READ,), check_parents=True | |
176 ): | |
177 """Check that peer_jid can access this node and all its parents | |
178 | |
179 @param peer_jid(jid.JID): entrity trying to access the node | |
180 @param perms(unicode): permissions to check, iterable of C.ACCESS_PERM_* | |
181 @param check_parents(bool): if True, access of all parents of this node will be | |
182 checked too | |
183 @return (bool): True if entity can access this node | |
184 """ | |
185 peer_jid = peer_jid.userhostJID() | |
186 if peer_jid == client.jid.userhostJID(): | |
187 return True | |
188 | |
189 parent = self | |
190 while parent != None: | |
191 if not self._check_node_permission(client, parent, perms, peer_jid): | |
192 return False | |
193 parent = parent.parent | |
194 | |
195 return True | |
196 | |
197 @staticmethod | |
198 def find(client, path, peer_jid, perms=(C.ACCESS_PERM_READ,)): | |
199 """find node corresponding to a path | |
200 | |
201 @param path(unicode): path to the requested file or directory | |
202 @param peer_jid(jid.JID): entity trying to find the node | |
203 used to check permission | |
204 @return (dict, unicode): shared data, remaining path | |
205 @raise exceptions.PermissionError: user can't access this file | |
206 @raise exceptions.DataError: path is invalid | |
207 @raise NotFound: path lead to a non existing file/directory | |
208 """ | |
209 path_elts = [_f for _f in path.split("/") if _f] | |
210 | |
211 if ".." in path_elts: | |
212 log.warning(_( | |
213 'parent dir ("..") found in path, hack attempt? path is {path} ' | |
214 '[{profile}]').format(path=path, profile=client.profile)) | |
215 raise exceptions.PermissionError("illegal path elements") | |
216 | |
217 node = client._XEP_0329_root_node | |
218 | |
219 while path_elts: | |
220 if node.type == TYPE_VIRTUAL: | |
221 try: | |
222 node = node[path_elts.pop(0)] | |
223 except KeyError: | |
224 raise exceptions.NotFound | |
225 elif node.type == TYPE_PATH: | |
226 break | |
227 | |
228 if not node.check_permissions(client, peer_jid, perms=perms): | |
229 raise exceptions.PermissionError("permission denied") | |
230 | |
231 return node, "/".join(path_elts) | |
232 | |
233 def find_by_local_path(self, path): | |
234 """retrieve nodes linking to local path | |
235 | |
236 @return (list[ShareNode]): found nodes associated to path | |
237 @raise exceptions.NotFound: no node has been found with this path | |
238 """ | |
239 shared_paths = self.get_shared_paths() | |
240 try: | |
241 return shared_paths[path] | |
242 except KeyError: | |
243 raise exceptions.NotFound | |
244 | |
245 def _get_shared_paths(self, node, paths): | |
246 if node.type == TYPE_VIRTUAL: | |
247 for node in node.values(): | |
248 self._get_shared_paths(node, paths) | |
249 elif node.type == TYPE_PATH: | |
250 paths.setdefault(node.path, []).append(node) | |
251 else: | |
252 raise exceptions.InternalError( | |
253 "unknown node type: {type}".format(type=node.type) | |
254 ) | |
255 | |
256 def get_shared_paths(self): | |
257 """retrieve nodes by shared path | |
258 | |
259 this method will retrieve recursively shared path in children of this node | |
260 @return (dict): map from shared path to list of nodes | |
261 """ | |
262 if self.type == TYPE_PATH: | |
263 raise exceptions.InternalError( | |
264 "get_shared_paths must be used on a virtual node" | |
265 ) | |
266 paths = {} | |
267 self._get_shared_paths(self, paths) | |
268 return paths | |
269 | |
270 | |
271 class XEP_0329(object): | |
272 def __init__(self, host): | |
273 log.info(_("File Information Sharing initialization")) | |
274 self.host = host | |
275 ShareNode.host = host | |
276 self._b = host.plugins["XEP-0231"] | |
277 self._h = host.plugins["XEP-0300"] | |
278 self._jf = host.plugins["XEP-0234"] | |
279 host.bridge.add_method( | |
280 "fis_list", | |
281 ".plugin", | |
282 in_sign="ssa{ss}s", | |
283 out_sign="aa{ss}", | |
284 method=self._list_files, | |
285 async_=True, | |
286 ) | |
287 host.bridge.add_method( | |
288 "fis_local_shares_get", | |
289 ".plugin", | |
290 in_sign="s", | |
291 out_sign="as", | |
292 method=self._local_shares_get, | |
293 ) | |
294 host.bridge.add_method( | |
295 "fis_share_path", | |
296 ".plugin", | |
297 in_sign="ssss", | |
298 out_sign="s", | |
299 method=self._share_path, | |
300 ) | |
301 host.bridge.add_method( | |
302 "fis_unshare_path", | |
303 ".plugin", | |
304 in_sign="ss", | |
305 out_sign="", | |
306 method=self._unshare_path, | |
307 ) | |
308 host.bridge.add_method( | |
309 "fis_affiliations_get", | |
310 ".plugin", | |
311 in_sign="ssss", | |
312 out_sign="a{ss}", | |
313 method=self._affiliations_get, | |
314 async_=True, | |
315 ) | |
316 host.bridge.add_method( | |
317 "fis_affiliations_set", | |
318 ".plugin", | |
319 in_sign="sssa{ss}s", | |
320 out_sign="", | |
321 method=self._affiliations_set, | |
322 async_=True, | |
323 ) | |
324 host.bridge.add_method( | |
325 "fis_configuration_get", | |
326 ".plugin", | |
327 in_sign="ssss", | |
328 out_sign="a{ss}", | |
329 method=self._configuration_get, | |
330 async_=True, | |
331 ) | |
332 host.bridge.add_method( | |
333 "fis_configuration_set", | |
334 ".plugin", | |
335 in_sign="sssa{ss}s", | |
336 out_sign="", | |
337 method=self._configuration_set, | |
338 async_=True, | |
339 ) | |
340 host.bridge.add_method( | |
341 "fis_create_dir", | |
342 ".plugin", | |
343 in_sign="sssa{ss}s", | |
344 out_sign="", | |
345 method=self._create_dir, | |
346 async_=True, | |
347 ) | |
348 host.bridge.add_signal("fis_shared_path_new", ".plugin", signature="sss") | |
349 host.bridge.add_signal("fis_shared_path_removed", ".plugin", signature="ss") | |
350 host.trigger.add("XEP-0234_fileSendingRequest", self._file_sending_request_trigger) | |
351 host.register_namespace("fis", NS_FIS) | |
352 | |
353 def get_handler(self, client): | |
354 return XEP_0329_handler(self) | |
355 | |
356 def profile_connected(self, client): | |
357 if client.is_component: | |
358 client._file_sharing_allowed_hosts = self.host.memory.config_get( | |
359 'component file_sharing', | |
360 'http_upload_allowed_hosts_list') or [client.host] | |
361 else: | |
362 client._XEP_0329_root_node = ShareNode( | |
363 None, | |
364 None, | |
365 TYPE_VIRTUAL, | |
366 {C.ACCESS_PERM_READ: {KEY_TYPE: C.ACCESS_TYPE_PUBLIC}}, | |
367 ) | |
368 client._XEP_0329_names_data = {} # name to share map | |
369 | |
370 def _file_sending_request_trigger( | |
371 self, client, session, content_data, content_name, file_data, file_elt | |
372 ): | |
373 """This trigger check that a requested file is available, and fill suitable data | |
374 | |
375 Path and name are used to retrieve the file. If path is missing, we try our luck | |
376 with known names | |
377 """ | |
378 if client.is_component: | |
379 return True, None | |
380 | |
381 try: | |
382 name = file_data["name"] | |
383 except KeyError: | |
384 return True, None | |
385 assert "/" not in name | |
386 | |
387 path = file_data.get("path") | |
388 if path is not None: | |
389 # we have a path, we can follow it to find node | |
390 try: | |
391 node, rem_path = ShareNode.find(client, path, session["peer_jid"]) | |
392 except (exceptions.PermissionError, exceptions.NotFound): | |
393 # no file, or file not allowed, we continue normal workflow | |
394 return True, None | |
395 except exceptions.DataError: | |
396 log.warning(_("invalid path: {path}").format(path=path)) | |
397 return True, None | |
398 | |
399 if node.type == TYPE_VIRTUAL: | |
400 # we have a virtual node, so name must link to a path node | |
401 try: | |
402 path = node[name].path | |
403 except KeyError: | |
404 return True, None | |
405 elif node.type == TYPE_PATH: | |
406 # we have a path node, so we can retrieve the full path now | |
407 path = os.path.join(node.path, rem_path, name) | |
408 else: | |
409 raise exceptions.InternalError( | |
410 "unknown type: {type}".format(type=node.type) | |
411 ) | |
412 if not os.path.exists(path): | |
413 return True, None | |
414 size = os.path.getsize(path) | |
415 else: | |
416 # we don't have the path, we try to find the file by its name | |
417 try: | |
418 name_data = client._XEP_0329_names_data[name] | |
419 except KeyError: | |
420 return True, None | |
421 | |
422 for path, shared_file in name_data.items(): | |
423 if True: # FIXME: filters are here | |
424 break | |
425 else: | |
426 return True, None | |
427 parent_node = shared_file["parent"] | |
428 if not parent_node.check_permissions(client, session["peer_jid"]): | |
429 log.warning( | |
430 _( | |
431 "{peer_jid} requested a file (s)he can't access [{profile}]" | |
432 ).format(peer_jid=session["peer_jid"], profile=client.profile) | |
433 ) | |
434 return True, None | |
435 size = shared_file["size"] | |
436 | |
437 file_data["size"] = size | |
438 file_elt.addElement("size", content=str(size)) | |
439 hash_algo = file_data["hash_algo"] = self._h.get_default_algo() | |
440 hasher = file_data["hash_hasher"] = self._h.get_hasher(hash_algo) | |
441 file_elt.addChild(self._h.build_hash_used_elt(hash_algo)) | |
442 content_data["stream_object"] = stream.FileStreamObject( | |
443 self.host, | |
444 client, | |
445 path, | |
446 uid=self._jf.get_progress_id(session, content_name), | |
447 size=size, | |
448 data_cb=lambda data: hasher.update(data), | |
449 ) | |
450 return False, defer.succeed(True) | |
451 | |
452 # common methods | |
453 | |
454 def _request_handler(self, client, iq_elt, root_nodes_cb, files_from_node_cb): | |
455 iq_elt.handled = True | |
456 node = iq_elt.query.getAttribute("node") | |
457 if not node: | |
458 d = utils.as_deferred(root_nodes_cb, client, iq_elt) | |
459 else: | |
460 d = utils.as_deferred(files_from_node_cb, client, iq_elt, node) | |
461 d.addErrback( | |
462 lambda failure_: log.error( | |
463 _("error while retrieving files: {msg}").format(msg=failure_) | |
464 ) | |
465 ) | |
466 | |
467 def _iq_error(self, client, iq_elt, condition="item-not-found"): | |
468 error_elt = jabber_error.StanzaError(condition).toResponse(iq_elt) | |
469 client.send(error_elt) | |
470 | |
471 # client | |
472 | |
473 def _add_path_data(self, client, query_elt, path, parent_node): | |
474 """Fill query_elt with files/directories found in path""" | |
475 name = os.path.basename(path) | |
476 if os.path.isfile(path): | |
477 size = os.path.getsize(path) | |
478 mime_type = mimetypes.guess_type(path, strict=False)[0] | |
479 file_elt = self._jf.build_file_element( | |
480 client=client, name=name, size=size, mime_type=mime_type, | |
481 modified=os.path.getmtime(path) | |
482 ) | |
483 | |
484 query_elt.addChild(file_elt) | |
485 # we don't specify hash as it would be too resource intensive to calculate | |
486 # it for all files. | |
487 # we add file to name_data, so users can request it later | |
488 name_data = client._XEP_0329_names_data.setdefault(name, {}) | |
489 if path not in name_data: | |
490 name_data[path] = { | |
491 "size": size, | |
492 "mime_type": mime_type, | |
493 "parent": parent_node, | |
494 } | |
495 else: | |
496 # we have a directory | |
497 directory_elt = query_elt.addElement("directory") | |
498 directory_elt["name"] = name | |
499 | |
500 def _path_node_handler(self, client, iq_elt, query_elt, node, path): | |
501 """Fill query_elt for path nodes, i.e. physical directories""" | |
502 path = os.path.join(node.path, path) | |
503 | |
504 if not os.path.exists(path): | |
505 # path may have been moved since it has been shared | |
506 return self._iq_error(client, iq_elt) | |
507 elif os.path.isfile(path): | |
508 self._add_path_data(client, query_elt, path, node) | |
509 else: | |
510 for name in sorted(os.listdir(path.encode("utf-8")), key=lambda n: n.lower()): | |
511 try: | |
512 name = name.decode("utf-8", "strict") | |
513 except UnicodeDecodeError as e: | |
514 log.warning( | |
515 _("ignoring invalid unicode name ({name}): {msg}").format( | |
516 name=name.decode("utf-8", "replace"), msg=e | |
517 ) | |
518 ) | |
519 continue | |
520 full_path = os.path.join(path, name) | |
521 self._add_path_data(client, query_elt, full_path, node) | |
522 | |
523 def _virtual_node_handler(self, client, peer_jid, iq_elt, query_elt, node): | |
524 """Fill query_elt for virtual nodes""" | |
525 for name, child_node in node.items(): | |
526 if not child_node.check_permissions(client, peer_jid, check_parents=False): | |
527 continue | |
528 node_type = child_node.type | |
529 if node_type == TYPE_VIRTUAL: | |
530 directory_elt = query_elt.addElement("directory") | |
531 directory_elt["name"] = name | |
532 elif node_type == TYPE_PATH: | |
533 self._add_path_data(client, query_elt, child_node.path, child_node) | |
534 else: | |
535 raise exceptions.InternalError( | |
536 _("unexpected type: {type}").format(type=node_type) | |
537 ) | |
538 | |
539 def _get_root_nodes_cb(self, client, iq_elt): | |
540 peer_jid = jid.JID(iq_elt["from"]) | |
541 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
542 query_elt = iq_result_elt.addElement((NS_FIS, "query")) | |
543 for name, node in client._XEP_0329_root_node.items(): | |
544 if not node.check_permissions(client, peer_jid, check_parents=False): | |
545 continue | |
546 directory_elt = query_elt.addElement("directory") | |
547 directory_elt["name"] = name | |
548 client.send(iq_result_elt) | |
549 | |
550 def _get_files_from_node_cb(self, client, iq_elt, node_path): | |
551 """Main method to retrieve files/directories from a node_path""" | |
552 peer_jid = jid.JID(iq_elt["from"]) | |
553 try: | |
554 node, path = ShareNode.find(client, node_path, peer_jid) | |
555 except (exceptions.PermissionError, exceptions.NotFound): | |
556 return self._iq_error(client, iq_elt) | |
557 except exceptions.DataError: | |
558 return self._iq_error(client, iq_elt, condition="not-acceptable") | |
559 | |
560 node_type = node.type | |
561 peer_jid = jid.JID(iq_elt["from"]) | |
562 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
563 query_elt = iq_result_elt.addElement((NS_FIS, "query")) | |
564 query_elt["node"] = node_path | |
565 | |
566 # we now fill query_elt according to node_type | |
567 if node_type == TYPE_PATH: | |
568 # it's a physical path | |
569 self._path_node_handler(client, iq_elt, query_elt, node, path) | |
570 elif node_type == TYPE_VIRTUAL: | |
571 assert not path | |
572 self._virtual_node_handler(client, peer_jid, iq_elt, query_elt, node) | |
573 else: | |
574 raise exceptions.InternalError( | |
575 _("unknown node type: {type}").format(type=node_type) | |
576 ) | |
577 | |
578 client.send(iq_result_elt) | |
579 | |
580 def on_request(self, iq_elt, client): | |
581 return self._request_handler( | |
582 client, iq_elt, self._get_root_nodes_cb, self._get_files_from_node_cb | |
583 ) | |
584 | |
585 # Component | |
586 | |
587 def _comp_parse_jids(self, client, iq_elt): | |
588 """Retrieve peer_jid and owner to use from IQ stanza | |
589 | |
590 @param iq_elt(domish.Element): IQ stanza of the FIS request | |
591 @return (tuple[jid.JID, jid.JID]): peer_jid and owner | |
592 """ | |
593 | |
594 async def _comp_get_root_nodes_cb(self, client, iq_elt): | |
595 peer_jid, owner = client.get_owner_and_peer(iq_elt) | |
596 files_data = await self.host.memory.get_files( | |
597 client, | |
598 peer_jid=peer_jid, | |
599 parent="", | |
600 type_=C.FILE_TYPE_DIRECTORY, | |
601 owner=owner, | |
602 ) | |
603 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
604 query_elt = iq_result_elt.addElement((NS_FIS, "query")) | |
605 for file_data in files_data: | |
606 name = file_data["name"] | |
607 directory_elt = query_elt.addElement("directory") | |
608 directory_elt["name"] = name | |
609 client.send(iq_result_elt) | |
610 | |
611 async def _comp_get_files_from_node_cb(self, client, iq_elt, node_path): | |
612 """Retrieve files from local files repository according to permissions | |
613 | |
614 result stanza is then built and sent to requestor | |
615 @trigger XEP-0329_compGetFilesFromNode(client, iq_elt, owner, node_path, | |
616 files_data): | |
617 can be used to add data/elements | |
618 """ | |
619 peer_jid, owner = client.get_owner_and_peer(iq_elt) | |
620 try: | |
621 files_data = await self.host.memory.get_files( | |
622 client, peer_jid=peer_jid, path=node_path, owner=owner | |
623 ) | |
624 except exceptions.NotFound: | |
625 self._iq_error(client, iq_elt) | |
626 return | |
627 except exceptions.PermissionError: | |
628 self._iq_error(client, iq_elt, condition='not-allowed') | |
629 return | |
630 except Exception as e: | |
631 tb = traceback.format_tb(e.__traceback__) | |
632 log.error(f"internal server error: {e}\n{''.join(tb)}") | |
633 self._iq_error(client, iq_elt, condition='internal-server-error') | |
634 return | |
635 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
636 query_elt = iq_result_elt.addElement((NS_FIS, "query")) | |
637 query_elt["node"] = node_path | |
638 if not self.host.trigger.point( | |
639 "XEP-0329_compGetFilesFromNode", | |
640 client, | |
641 iq_elt, | |
642 iq_result_elt, | |
643 owner, | |
644 node_path, | |
645 files_data | |
646 ): | |
647 return | |
648 for file_data in files_data: | |
649 if file_data['type'] == C.FILE_TYPE_DIRECTORY: | |
650 directory_elt = query_elt.addElement("directory") | |
651 directory_elt['name'] = file_data['name'] | |
652 self.host.trigger.point( | |
653 "XEP-0329_compGetFilesFromNode_build_directory", | |
654 client, | |
655 file_data, | |
656 directory_elt, | |
657 owner, | |
658 node_path, | |
659 ) | |
660 else: | |
661 file_elt = self._jf.build_file_element_from_dict( | |
662 client, | |
663 file_data, | |
664 modified=file_data.get("modified", file_data["created"]) | |
665 ) | |
666 query_elt.addChild(file_elt) | |
667 client.send(iq_result_elt) | |
668 | |
669 def on_component_request(self, iq_elt, client): | |
670 return self._request_handler( | |
671 client, iq_elt, self._comp_get_root_nodes_cb, self._comp_get_files_from_node_cb | |
672 ) | |
673 | |
674 async def _parse_result(self, client, peer_jid, iq_elt): | |
675 query_elt = next(iq_elt.elements(NS_FIS, "query")) | |
676 files = [] | |
677 | |
678 for elt in query_elt.elements(): | |
679 if elt.name == "file": | |
680 # we have a file | |
681 try: | |
682 file_data = await self._jf.parse_file_element(client, elt) | |
683 except exceptions.DataError: | |
684 continue | |
685 file_data["type"] = C.FILE_TYPE_FILE | |
686 try: | |
687 thumbs = file_data['extra'][C.KEY_THUMBNAILS] | |
688 except KeyError: | |
689 log.debug(f"No thumbnail found for {file_data}") | |
690 else: | |
691 for thumb in thumbs: | |
692 if 'url' not in thumb and "id" in thumb: | |
693 try: | |
694 file_path = await self._b.get_file(client, peer_jid, thumb['id']) | |
695 except Exception as e: | |
696 log.warning(f"Can't get thumbnail {thumb['id']!r} for {file_data}: {e}") | |
697 else: | |
698 thumb['filename'] = file_path.name | |
699 | |
700 elif elt.name == "directory" and elt.uri == NS_FIS: | |
701 # we have a directory | |
702 | |
703 file_data = {"name": elt["name"], "type": C.FILE_TYPE_DIRECTORY} | |
704 self.host.trigger.point( | |
705 "XEP-0329_parseResult_directory", | |
706 client, | |
707 elt, | |
708 file_data, | |
709 ) | |
710 else: | |
711 log.warning( | |
712 _("unexpected element, ignoring: {elt}") | |
713 .format(elt=elt.toXml()) | |
714 ) | |
715 continue | |
716 files.append(file_data) | |
717 return files | |
718 | |
719 # affiliations # | |
720 | |
721 async def _parse_element(self, client, iq_elt, element, namespace): | |
722 peer_jid, owner = client.get_owner_and_peer(iq_elt) | |
723 elt = next(iq_elt.elements(namespace, element)) | |
724 path = Path("/", elt['path']) | |
725 if len(path.parts) < 2: | |
726 raise RootPathException | |
727 namespace = elt.getAttribute('namespace') | |
728 files_data = await self.host.memory.get_files( | |
729 client, | |
730 peer_jid=peer_jid, | |
731 path=str(path.parent), | |
732 name=path.name, | |
733 namespace=namespace, | |
734 owner=owner, | |
735 ) | |
736 if len(files_data) != 1: | |
737 client.sendError(iq_elt, 'item-not-found') | |
738 raise exceptions.CancelError | |
739 file_data = files_data[0] | |
740 return peer_jid, elt, path, namespace, file_data | |
741 | |
742 def _affiliations_get(self, service_jid_s, namespace, path, profile): | |
743 client = self.host.get_client(profile) | |
744 service = jid.JID(service_jid_s) | |
745 d = defer.ensureDeferred(self.affiliationsGet( | |
746 client, service, namespace or None, path)) | |
747 d.addCallback( | |
748 lambda affiliations: { | |
749 str(entity): affiliation for entity, affiliation in affiliations.items() | |
750 } | |
751 ) | |
752 return d | |
753 | |
754 async def affiliationsGet( | |
755 self, | |
756 client: SatXMPPEntity, | |
757 service: jid.JID, | |
758 namespace: Optional[str], | |
759 path: str | |
760 ) -> Dict[jid.JID, str]: | |
761 if not path: | |
762 raise ValueError(f"invalid path: {path!r}") | |
763 iq_elt = client.IQ("get") | |
764 iq_elt['to'] = service.full() | |
765 affiliations_elt = iq_elt.addElement((NS_FIS_AFFILIATION, "affiliations")) | |
766 if namespace: | |
767 affiliations_elt["namespace"] = namespace | |
768 affiliations_elt["path"] = path | |
769 iq_result_elt = await iq_elt.send() | |
770 try: | |
771 affiliations_elt = next(iq_result_elt.elements(NS_FIS_AFFILIATION, "affiliations")) | |
772 except StopIteration: | |
773 raise exceptions.DataError(f"Invalid result to affiliations request: {iq_result_elt.toXml()}") | |
774 | |
775 affiliations = {} | |
776 for affiliation_elt in affiliations_elt.elements(NS_FIS_AFFILIATION, 'affiliation'): | |
777 try: | |
778 affiliations[jid.JID(affiliation_elt['jid'])] = affiliation_elt['affiliation'] | |
779 except (KeyError, RuntimeError): | |
780 raise exceptions.DataError( | |
781 f"invalid affiliation element: {affiliation_elt.toXml()}") | |
782 | |
783 return affiliations | |
784 | |
785 def _affiliations_set(self, service_jid_s, namespace, path, affiliations, profile): | |
786 client = self.host.get_client(profile) | |
787 service = jid.JID(service_jid_s) | |
788 affiliations = {jid.JID(e): a for e, a in affiliations.items()} | |
789 return defer.ensureDeferred(self.affiliationsSet( | |
790 client, service, namespace or None, path, affiliations)) | |
791 | |
792 async def affiliationsSet( | |
793 self, | |
794 client: SatXMPPEntity, | |
795 service: jid.JID, | |
796 namespace: Optional[str], | |
797 path: str, | |
798 affiliations: Dict[jid.JID, str], | |
799 ): | |
800 if not path: | |
801 raise ValueError(f"invalid path: {path!r}") | |
802 iq_elt = client.IQ("set") | |
803 iq_elt['to'] = service.full() | |
804 affiliations_elt = iq_elt.addElement((NS_FIS_AFFILIATION, "affiliations")) | |
805 if namespace: | |
806 affiliations_elt["namespace"] = namespace | |
807 affiliations_elt["path"] = path | |
808 for entity_jid, affiliation in affiliations.items(): | |
809 affiliation_elt = affiliations_elt.addElement('affiliation') | |
810 affiliation_elt['jid'] = entity_jid.full() | |
811 affiliation_elt['affiliation'] = affiliation | |
812 await iq_elt.send() | |
813 | |
814 def _on_component_affiliations_get(self, iq_elt, client): | |
815 iq_elt.handled = True | |
816 defer.ensureDeferred(self.on_component_affiliations_get(client, iq_elt)) | |
817 | |
818 async def on_component_affiliations_get(self, client, iq_elt): | |
819 try: | |
820 ( | |
821 from_jid, affiliations_elt, path, namespace, file_data | |
822 ) = await self._parse_element(client, iq_elt, "affiliations", NS_FIS_AFFILIATION) | |
823 except exceptions.CancelError: | |
824 return | |
825 except RootPathException: | |
826 # if root path is requested, we only get owner affiliation | |
827 peer_jid, owner = client.get_owner_and_peer(iq_elt) | |
828 is_owner = peer_jid.userhostJID() == owner | |
829 affiliations = {owner: 'owner'} | |
830 except exceptions.NotFound: | |
831 client.sendError(iq_elt, "item-not-found") | |
832 return | |
833 except Exception as e: | |
834 client.sendError(iq_elt, "internal-server-error", str(e)) | |
835 return | |
836 else: | |
837 from_jid_bare = from_jid.userhostJID() | |
838 is_owner = from_jid_bare == file_data.get('owner') | |
839 affiliations = self.host.memory.get_file_affiliations(file_data) | |
840 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
841 affiliations_elt = iq_result_elt.addElement((NS_FIS_AFFILIATION, 'affiliations')) | |
842 for entity_jid, affiliation in affiliations.items(): | |
843 if not is_owner and entity_jid.userhostJID() != from_jid_bare: | |
844 # only onwer can get all affiliations | |
845 continue | |
846 affiliation_elt = affiliations_elt.addElement('affiliation') | |
847 affiliation_elt['jid'] = entity_jid.userhost() | |
848 affiliation_elt['affiliation'] = affiliation | |
849 client.send(iq_result_elt) | |
850 | |
851 def _on_component_affiliations_set(self, iq_elt, client): | |
852 iq_elt.handled = True | |
853 defer.ensureDeferred(self.on_component_affiliations_set(client, iq_elt)) | |
854 | |
855 async def on_component_affiliations_set(self, client, iq_elt): | |
856 try: | |
857 ( | |
858 from_jid, affiliations_elt, path, namespace, file_data | |
859 ) = await self._parse_element(client, iq_elt, "affiliations", NS_FIS_AFFILIATION) | |
860 except exceptions.CancelError: | |
861 return | |
862 except RootPathException: | |
863 client.sendError(iq_elt, 'bad-request', "Root path can't be used") | |
864 return | |
865 | |
866 if from_jid.userhostJID() != file_data['owner']: | |
867 log.warning( | |
868 f"{from_jid} tried to modify {path} affiliations while the owner is " | |
869 f"{file_data['owner']}" | |
870 ) | |
871 client.sendError(iq_elt, 'forbidden') | |
872 return | |
873 | |
874 try: | |
875 affiliations = { | |
876 jid.JID(e['jid']): e['affiliation'] | |
877 for e in affiliations_elt.elements(NS_FIS_AFFILIATION, 'affiliation') | |
878 } | |
879 except (KeyError, RuntimeError): | |
880 log.warning( | |
881 f"invalid affiliation element: {affiliations_elt.toXml()}" | |
882 ) | |
883 client.sendError(iq_elt, 'bad-request', "invalid affiliation element") | |
884 return | |
885 except Exception as e: | |
886 log.error( | |
887 f"unexepected exception while setting affiliation element: {e}\n" | |
888 f"{affiliations_elt.toXml()}" | |
889 ) | |
890 client.sendError(iq_elt, 'internal-server-error', f"{e}") | |
891 return | |
892 | |
893 await self.host.memory.set_file_affiliations(client, file_data, affiliations) | |
894 | |
895 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
896 client.send(iq_result_elt) | |
897 | |
898 # configuration | |
899 | |
900 def _configuration_get(self, service_jid_s, namespace, path, profile): | |
901 client = self.host.get_client(profile) | |
902 service = jid.JID(service_jid_s) | |
903 d = defer.ensureDeferred(self.configuration_get( | |
904 client, service, namespace or None, path)) | |
905 d.addCallback( | |
906 lambda configuration: { | |
907 str(entity): affiliation for entity, affiliation in configuration.items() | |
908 } | |
909 ) | |
910 return d | |
911 | |
912 async def configuration_get( | |
913 self, | |
914 client: SatXMPPEntity, | |
915 service: jid.JID, | |
916 namespace: Optional[str], | |
917 path: str | |
918 ) -> Dict[str, str]: | |
919 if not path: | |
920 raise ValueError(f"invalid path: {path!r}") | |
921 iq_elt = client.IQ("get") | |
922 iq_elt['to'] = service.full() | |
923 configuration_elt = iq_elt.addElement((NS_FIS_CONFIGURATION, "configuration")) | |
924 if namespace: | |
925 configuration_elt["namespace"] = namespace | |
926 configuration_elt["path"] = path | |
927 iq_result_elt = await iq_elt.send() | |
928 try: | |
929 configuration_elt = next(iq_result_elt.elements(NS_FIS_CONFIGURATION, "configuration")) | |
930 except StopIteration: | |
931 raise exceptions.DataError(f"Invalid result to configuration request: {iq_result_elt.toXml()}") | |
932 | |
933 form = data_form.findForm(configuration_elt, NS_FIS_CONFIGURATION) | |
934 configuration = {f.var: f.value for f in form.fields.values()} | |
935 | |
936 return configuration | |
937 | |
938 def _configuration_set(self, service_jid_s, namespace, path, configuration, profile): | |
939 client = self.host.get_client(profile) | |
940 service = jid.JID(service_jid_s) | |
941 return defer.ensureDeferred(self.configuration_set( | |
942 client, service, namespace or None, path, configuration)) | |
943 | |
944 async def configuration_set( | |
945 self, | |
946 client: SatXMPPEntity, | |
947 service: jid.JID, | |
948 namespace: Optional[str], | |
949 path: str, | |
950 configuration: Dict[str, str], | |
951 ): | |
952 if not path: | |
953 raise ValueError(f"invalid path: {path!r}") | |
954 iq_elt = client.IQ("set") | |
955 iq_elt['to'] = service.full() | |
956 configuration_elt = iq_elt.addElement((NS_FIS_CONFIGURATION, "configuration")) | |
957 if namespace: | |
958 configuration_elt["namespace"] = namespace | |
959 configuration_elt["path"] = path | |
960 form = data_form.Form(formType="submit", formNamespace=NS_FIS_CONFIGURATION) | |
961 form.makeFields(configuration) | |
962 configuration_elt.addChild(form.toElement()) | |
963 await iq_elt.send() | |
964 | |
965 def _on_component_configuration_get(self, iq_elt, client): | |
966 iq_elt.handled = True | |
967 defer.ensureDeferred(self.on_component_configuration_get(client, iq_elt)) | |
968 | |
969 async def on_component_configuration_get(self, client, iq_elt): | |
970 try: | |
971 ( | |
972 from_jid, configuration_elt, path, namespace, file_data | |
973 ) = await self._parse_element(client, iq_elt, "configuration", NS_FIS_CONFIGURATION) | |
974 except exceptions.CancelError: | |
975 return | |
976 except RootPathException: | |
977 client.sendError(iq_elt, 'bad-request', "Root path can't be used") | |
978 return | |
979 try: | |
980 access_type = file_data['access'][C.ACCESS_PERM_READ]['type'] | |
981 except KeyError: | |
982 access_model = 'whitelist' | |
983 else: | |
984 access_model = 'open' if access_type == C.ACCESS_TYPE_PUBLIC else 'whitelist' | |
985 | |
986 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
987 configuration_elt = iq_result_elt.addElement((NS_FIS_CONFIGURATION, 'configuration')) | |
988 form = data_form.Form(formType="form", formNamespace=NS_FIS_CONFIGURATION) | |
989 form.makeFields({'access_model': access_model}) | |
990 configuration_elt.addChild(form.toElement()) | |
991 client.send(iq_result_elt) | |
992 | |
993 async def _set_configuration(self, client, configuration_elt, file_data): | |
994 form = data_form.findForm(configuration_elt, NS_FIS_CONFIGURATION) | |
995 for name, value in form.items(): | |
996 if name == 'access_model': | |
997 await self.host.memory.set_file_access_model(client, file_data, value) | |
998 else: | |
999 # TODO: send a IQ error? | |
1000 log.warning( | |
1001 f"Trying to set a not implemented configuration option: {name}") | |
1002 | |
1003 def _on_component_configuration_set(self, iq_elt, client): | |
1004 iq_elt.handled = True | |
1005 defer.ensureDeferred(self.on_component_configuration_set(client, iq_elt)) | |
1006 | |
1007 async def on_component_configuration_set(self, client, iq_elt): | |
1008 try: | |
1009 ( | |
1010 from_jid, configuration_elt, path, namespace, file_data | |
1011 ) = await self._parse_element(client, iq_elt, "configuration", NS_FIS_CONFIGURATION) | |
1012 except exceptions.CancelError: | |
1013 return | |
1014 except RootPathException: | |
1015 client.sendError(iq_elt, 'bad-request', "Root path can't be used") | |
1016 return | |
1017 | |
1018 from_jid_bare = from_jid.userhostJID() | |
1019 is_owner = from_jid_bare == file_data.get('owner') | |
1020 if not is_owner: | |
1021 log.warning( | |
1022 f"{from_jid} tried to modify {path} configuration while the owner is " | |
1023 f"{file_data['owner']}" | |
1024 ) | |
1025 client.sendError(iq_elt, 'forbidden') | |
1026 return | |
1027 | |
1028 await self._set_configuration(client, configuration_elt, file_data) | |
1029 | |
1030 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
1031 client.send(iq_result_elt) | |
1032 | |
1033 # directory creation | |
1034 | |
1035 def _create_dir(self, service_jid_s, namespace, path, configuration, profile): | |
1036 client = self.host.get_client(profile) | |
1037 service = jid.JID(service_jid_s) | |
1038 return defer.ensureDeferred(self.create_dir( | |
1039 client, service, namespace or None, path, configuration or None)) | |
1040 | |
1041 async def create_dir( | |
1042 self, | |
1043 client: SatXMPPEntity, | |
1044 service: jid.JID, | |
1045 namespace: Optional[str], | |
1046 path: str, | |
1047 configuration: Optional[Dict[str, str]], | |
1048 ): | |
1049 if not path: | |
1050 raise ValueError(f"invalid path: {path!r}") | |
1051 iq_elt = client.IQ("set") | |
1052 iq_elt['to'] = service.full() | |
1053 create_dir_elt = iq_elt.addElement((NS_FIS_CREATE, "dir")) | |
1054 if namespace: | |
1055 create_dir_elt["namespace"] = namespace | |
1056 create_dir_elt["path"] = path | |
1057 if configuration: | |
1058 configuration_elt = create_dir_elt.addElement((NS_FIS_CONFIGURATION, "configuration")) | |
1059 form = data_form.Form(formType="submit", formNamespace=NS_FIS_CONFIGURATION) | |
1060 form.makeFields(configuration) | |
1061 configuration_elt.addChild(form.toElement()) | |
1062 await iq_elt.send() | |
1063 | |
1064 def _on_component_create_dir(self, iq_elt, client): | |
1065 iq_elt.handled = True | |
1066 defer.ensureDeferred(self.on_component_create_dir(client, iq_elt)) | |
1067 | |
1068 async def on_component_create_dir(self, client, iq_elt): | |
1069 peer_jid, owner = client.get_owner_and_peer(iq_elt) | |
1070 if peer_jid.host not in client._file_sharing_allowed_hosts: | |
1071 client.sendError(iq_elt, 'forbidden') | |
1072 return | |
1073 create_dir_elt = next(iq_elt.elements(NS_FIS_CREATE, "dir")) | |
1074 namespace = create_dir_elt.getAttribute('namespace') | |
1075 path = Path("/", create_dir_elt['path']) | |
1076 if len(path.parts) < 2: | |
1077 client.sendError(iq_elt, 'bad-request', "Root path can't be used") | |
1078 return | |
1079 # for root directories, we check permission here | |
1080 if len(path.parts) == 2 and owner != peer_jid.userhostJID(): | |
1081 log.warning( | |
1082 f"{peer_jid} is trying to create a dir at {owner}'s repository:\n" | |
1083 f"path: {path}\nnamespace: {namespace!r}" | |
1084 ) | |
1085 client.sendError(iq_elt, 'forbidden', "You can't create a directory there") | |
1086 return | |
1087 # when going further into the path, the permissions will be checked by get_files | |
1088 files_data = await self.host.memory.get_files( | |
1089 client, | |
1090 peer_jid=peer_jid, | |
1091 path=path.parent, | |
1092 namespace=namespace, | |
1093 owner=owner, | |
1094 ) | |
1095 if path.name in [d['name'] for d in files_data]: | |
1096 log.warning( | |
1097 f"Conflict when trying to create a directory (from: {peer_jid} " | |
1098 f"namespace: {namespace!r} path: {path!r})" | |
1099 ) | |
1100 client.sendError( | |
1101 iq_elt, 'conflict', "there is already a file or dir at this path") | |
1102 return | |
1103 | |
1104 try: | |
1105 configuration_elt = next( | |
1106 create_dir_elt.elements(NS_FIS_CONFIGURATION, 'configuration')) | |
1107 except StopIteration: | |
1108 configuration_elt = None | |
1109 | |
1110 await self.host.memory.set_file( | |
1111 client, | |
1112 path.name, | |
1113 path=path.parent, | |
1114 type_=C.FILE_TYPE_DIRECTORY, | |
1115 namespace=namespace, | |
1116 owner=owner, | |
1117 peer_jid=peer_jid | |
1118 ) | |
1119 | |
1120 if configuration_elt is not None: | |
1121 file_data = (await self.host.memory.get_files( | |
1122 client, | |
1123 peer_jid=peer_jid, | |
1124 path=path.parent, | |
1125 name=path.name, | |
1126 namespace=namespace, | |
1127 owner=owner, | |
1128 ))[0] | |
1129 | |
1130 await self._set_configuration(client, configuration_elt, file_data) | |
1131 | |
1132 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
1133 client.send(iq_result_elt) | |
1134 | |
1135 # file methods # | |
1136 | |
1137 def _serialize_data(self, files_data): | |
1138 for file_data in files_data: | |
1139 for key, value in file_data.items(): | |
1140 file_data[key] = ( | |
1141 json.dumps(value) if key in ("extra",) else str(value) | |
1142 ) | |
1143 return files_data | |
1144 | |
1145 def _list_files(self, target_jid, path, extra, profile): | |
1146 client = self.host.get_client(profile) | |
1147 target_jid = client.jid if not target_jid else jid.JID(target_jid) | |
1148 d = defer.ensureDeferred(self.list_files(client, target_jid, path or None)) | |
1149 d.addCallback(self._serialize_data) | |
1150 return d | |
1151 | |
1152 async def list_files(self, client, peer_jid, path=None, extra=None): | |
1153 """List file shared by an entity | |
1154 | |
1155 @param peer_jid(jid.JID): jid of the sharing entity | |
1156 @param path(unicode, None): path to the directory containing shared files | |
1157 None to get root directories | |
1158 @param extra(dict, None): extra data | |
1159 @return list(dict): shared files | |
1160 """ | |
1161 iq_elt = client.IQ("get") | |
1162 iq_elt["to"] = peer_jid.full() | |
1163 query_elt = iq_elt.addElement((NS_FIS, "query")) | |
1164 if path: | |
1165 query_elt["node"] = path | |
1166 iq_result_elt = await iq_elt.send() | |
1167 return await self._parse_result(client, peer_jid, iq_result_elt) | |
1168 | |
1169 def _local_shares_get(self, profile): | |
1170 client = self.host.get_client(profile) | |
1171 return self.local_shares_get(client) | |
1172 | |
1173 def local_shares_get(self, client): | |
1174 return list(client._XEP_0329_root_node.get_shared_paths().keys()) | |
1175 | |
1176 def _share_path(self, name, path, access, profile): | |
1177 client = self.host.get_client(profile) | |
1178 access = json.loads(access) | |
1179 return self.share_path(client, name or None, path, access) | |
1180 | |
1181 def share_path(self, client, name, path, access): | |
1182 if client.is_component: | |
1183 raise exceptions.ClientTypeError | |
1184 if not os.path.exists(path): | |
1185 raise ValueError(_("This path doesn't exist!")) | |
1186 if not path or not path.strip(" /"): | |
1187 raise ValueError(_("A path need to be specified")) | |
1188 if not isinstance(access, dict): | |
1189 raise ValueError(_("access must be a dict")) | |
1190 | |
1191 node = client._XEP_0329_root_node | |
1192 node_type = TYPE_PATH | |
1193 if os.path.isfile(path): | |
1194 # we have a single file, the workflow is diferrent as we store all single | |
1195 # files in the same dir | |
1196 node = node.get_or_create(SINGLE_FILES_DIR) | |
1197 | |
1198 if not name: | |
1199 name = os.path.basename(path.rstrip(" /")) | |
1200 if not name: | |
1201 raise exceptions.InternalError(_("Can't find a proper name")) | |
1202 | |
1203 if name in node or name == SINGLE_FILES_DIR: | |
1204 idx = 1 | |
1205 new_name = name + "_" + str(idx) | |
1206 while new_name in node: | |
1207 idx += 1 | |
1208 new_name = name + "_" + str(idx) | |
1209 name = new_name | |
1210 log.info(_( | |
1211 "A directory with this name is already shared, renamed to {new_name} " | |
1212 "[{profile}]".format( new_name=new_name, profile=client.profile))) | |
1213 | |
1214 ShareNode(name=name, parent=node, type_=node_type, access=access, path=path) | |
1215 self.host.bridge.fis_shared_path_new(path, name, client.profile) | |
1216 return name | |
1217 | |
1218 def _unshare_path(self, path, profile): | |
1219 client = self.host.get_client(profile) | |
1220 return self.unshare_path(client, path) | |
1221 | |
1222 def unshare_path(self, client, path): | |
1223 nodes = client._XEP_0329_root_node.find_by_local_path(path) | |
1224 for node in nodes: | |
1225 node.remove_from_parent() | |
1226 self.host.bridge.fis_shared_path_removed(path, client.profile) | |
1227 | |
1228 | |
1229 @implementer(iwokkel.IDisco) | |
1230 class XEP_0329_handler(xmlstream.XMPPHandler): | |
1231 | |
1232 def __init__(self, plugin_parent): | |
1233 self.plugin_parent = plugin_parent | |
1234 self.host = plugin_parent.host | |
1235 | |
1236 def connectionInitialized(self): | |
1237 if self.parent.is_component: | |
1238 self.xmlstream.addObserver( | |
1239 IQ_FIS_REQUEST, self.plugin_parent.on_component_request, client=self.parent | |
1240 ) | |
1241 self.xmlstream.addObserver( | |
1242 IQ_FIS_AFFILIATION_GET, | |
1243 self.plugin_parent._on_component_affiliations_get, | |
1244 client=self.parent | |
1245 ) | |
1246 self.xmlstream.addObserver( | |
1247 IQ_FIS_AFFILIATION_SET, | |
1248 self.plugin_parent._on_component_affiliations_set, | |
1249 client=self.parent | |
1250 ) | |
1251 self.xmlstream.addObserver( | |
1252 IQ_FIS_CONFIGURATION_GET, | |
1253 self.plugin_parent._on_component_configuration_get, | |
1254 client=self.parent | |
1255 ) | |
1256 self.xmlstream.addObserver( | |
1257 IQ_FIS_CONFIGURATION_SET, | |
1258 self.plugin_parent._on_component_configuration_set, | |
1259 client=self.parent | |
1260 ) | |
1261 self.xmlstream.addObserver( | |
1262 IQ_FIS_CREATE_DIR, | |
1263 self.plugin_parent._on_component_create_dir, | |
1264 client=self.parent | |
1265 ) | |
1266 else: | |
1267 self.xmlstream.addObserver( | |
1268 IQ_FIS_REQUEST, self.plugin_parent.on_request, client=self.parent | |
1269 ) | |
1270 | |
1271 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | |
1272 return [disco.DiscoFeature(NS_FIS)] | |
1273 | |
1274 def getDiscoItems(self, requestor, target, nodeIdentifier=""): | |
1275 return [] |