Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_misc_file.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_misc_file.py@877145b4ba01 |
children | e11b13418ba6 |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 | |
4 # SAT plugin for file tansfer | |
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 import os | |
21 import os.path | |
22 from functools import partial | |
23 from twisted.internet import defer | |
24 from twisted.words.protocols.jabber import jid | |
25 from libervia.backend.core.i18n import _, D_ | |
26 from libervia.backend.core.constants import Const as C | |
27 from libervia.backend.core.log import getLogger | |
28 from libervia.backend.core import exceptions | |
29 from libervia.backend.tools import xml_tools | |
30 from libervia.backend.tools import stream | |
31 from libervia.backend.tools import utils | |
32 from libervia.backend.tools.common import data_format, utils as common_utils | |
33 | |
34 | |
35 log = getLogger(__name__) | |
36 | |
37 | |
38 PLUGIN_INFO = { | |
39 C.PI_NAME: "File Tansfer", | |
40 C.PI_IMPORT_NAME: "FILE", | |
41 C.PI_TYPE: C.PLUG_TYPE_MISC, | |
42 C.PI_MODES: C.PLUG_MODE_BOTH, | |
43 C.PI_MAIN: "FilePlugin", | |
44 C.PI_HANDLER: "no", | |
45 C.PI_DESCRIPTION: _( | |
46 """File Tansfer Management: | |
47 This plugin manage the various ways of sending a file, and choose the best one.""" | |
48 ), | |
49 } | |
50 | |
51 | |
52 SENDING = D_("Please select a file to send to {peer}") | |
53 SENDING_TITLE = D_("File sending") | |
54 CONFIRM = D_( | |
55 '{peer} wants to send the file "{name}" to you:\n{desc}\n\nThe file has a size of ' | |
56 '{size_human}\n\nDo you accept ?' | |
57 ) | |
58 CONFIRM_TITLE = D_("Confirm file transfer") | |
59 CONFIRM_OVERWRITE = D_("File {} already exists, are you sure you want to overwrite ?") | |
60 CONFIRM_OVERWRITE_TITLE = D_("File exists") | |
61 SECURITY_LIMIT = 30 | |
62 | |
63 PROGRESS_ID_KEY = "progress_id" | |
64 | |
65 | |
66 class FilePlugin: | |
67 File = stream.SatFile | |
68 | |
69 def __init__(self, host): | |
70 log.info(_("plugin File initialization")) | |
71 self.host = host | |
72 host.bridge.add_method( | |
73 "file_send", | |
74 ".plugin", | |
75 in_sign="ssssss", | |
76 out_sign="a{ss}", | |
77 method=self._file_send, | |
78 async_=True, | |
79 ) | |
80 self._file_managers = [] | |
81 host.import_menu( | |
82 (D_("Action"), D_("send file")), | |
83 self._file_send_menu, | |
84 security_limit=10, | |
85 help_string=D_("Send a file"), | |
86 type_=C.MENU_SINGLE, | |
87 ) | |
88 | |
89 def _file_send( | |
90 self, | |
91 peer_jid_s: str, | |
92 filepath: str, | |
93 name: str, | |
94 file_desc: str, | |
95 extra_s: str, | |
96 profile: str = C.PROF_KEY_NONE | |
97 ) -> defer.Deferred: | |
98 client = self.host.get_client(profile) | |
99 return defer.ensureDeferred(self.file_send( | |
100 client, jid.JID(peer_jid_s), filepath, name or None, file_desc or None, | |
101 data_format.deserialise(extra_s) | |
102 )) | |
103 | |
104 async def file_send( | |
105 self, client, peer_jid, filepath, filename=None, file_desc=None, extra=None | |
106 ): | |
107 """Send a file using best available method | |
108 | |
109 @param peer_jid(jid.JID): jid of the destinee | |
110 @param filepath(str): absolute path to the file | |
111 @param filename(unicode, None): name to use, or None to find it from filepath | |
112 @param file_desc(unicode, None): description of the file | |
113 @param profile: %(doc_profile)s | |
114 @return (dict): action dictionary, with progress id in case of success, else | |
115 xmlui message | |
116 """ | |
117 if not os.path.isfile(filepath): | |
118 raise exceptions.DataError("The given path doesn't link to a file") | |
119 if not filename: | |
120 filename = os.path.basename(filepath) or "_" | |
121 for manager, priority in self._file_managers: | |
122 if await utils.as_deferred(manager.can_handle_file_send, | |
123 client, peer_jid, filepath): | |
124 try: | |
125 method_name = manager.name | |
126 except AttributeError: | |
127 method_name = manager.__class__.__name__ | |
128 log.info( | |
129 _("{name} method will be used to send the file").format( | |
130 name=method_name | |
131 ) | |
132 ) | |
133 try: | |
134 progress_id = await utils.as_deferred( | |
135 manager.file_send, client, peer_jid, filepath, filename, file_desc, | |
136 extra | |
137 ) | |
138 except Exception as e: | |
139 log.warning( | |
140 _("Can't send {filepath} to {peer_jid} with {method_name}: " | |
141 "{reason}").format( | |
142 filepath=filepath, | |
143 peer_jid=peer_jid, | |
144 method_name=method_name, | |
145 reason=e | |
146 ) | |
147 ) | |
148 continue | |
149 return {"progress": progress_id} | |
150 msg = "Can't find any method to send file to {jid}".format(jid=peer_jid.full()) | |
151 log.warning(msg) | |
152 return { | |
153 "xmlui": xml_tools.note( | |
154 "Can't transfer file", msg, C.XMLUI_DATA_LVL_WARNING | |
155 ).toXml() | |
156 } | |
157 | |
158 def _on_file_choosed(self, peer_jid, data, profile): | |
159 client = self.host.get_client(profile) | |
160 cancelled = C.bool(data.get("cancelled", C.BOOL_FALSE)) | |
161 if cancelled: | |
162 return | |
163 path = data["path"] | |
164 return self.file_send(client, peer_jid, path) | |
165 | |
166 def _file_send_menu(self, data, profile): | |
167 """ XMLUI activated by menu: return file sending UI | |
168 | |
169 @param profile: %(doc_profile)s | |
170 """ | |
171 try: | |
172 jid_ = jid.JID(data["jid"]) | |
173 except RuntimeError: | |
174 raise exceptions.DataError(_("Invalid JID")) | |
175 | |
176 file_choosed_id = self.host.register_callback( | |
177 partial(self._on_file_choosed, jid_), | |
178 with_data=True, | |
179 one_shot=True, | |
180 ) | |
181 xml_ui = xml_tools.XMLUI( | |
182 C.XMLUI_DIALOG, | |
183 dialog_opt={ | |
184 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_FILE, | |
185 C.XMLUI_DATA_MESS: _(SENDING).format(peer=jid_.full()), | |
186 }, | |
187 title=_(SENDING_TITLE), | |
188 submit_id=file_choosed_id, | |
189 ) | |
190 | |
191 return {"xmlui": xml_ui.toXml()} | |
192 | |
193 def register(self, manager, priority: int = 0) -> None: | |
194 """Register a fileSending manager | |
195 | |
196 @param manager: object implementing can_handle_file_send, and file_send methods | |
197 @param priority: pririoty of this manager, the higher available will be used | |
198 """ | |
199 m_data = (manager, priority) | |
200 if m_data in self._file_managers: | |
201 raise exceptions.ConflictError( | |
202 f"Manager {manager} is already registered" | |
203 ) | |
204 if not hasattr(manager, "can_handle_file_send") or not hasattr(manager, "file_send"): | |
205 raise ValueError( | |
206 f'{manager} must have both "can_handle_file_send" and "file_send" methods to ' | |
207 'be registered') | |
208 self._file_managers.append(m_data) | |
209 self._file_managers.sort(key=lambda m: m[1], reverse=True) | |
210 | |
211 def unregister(self, manager): | |
212 for idx, data in enumerate(self._file_managers): | |
213 if data[0] == manager: | |
214 break | |
215 else: | |
216 raise exceptions.NotFound("The file manager {manager} is not registered") | |
217 del self._file_managers[idx] | |
218 | |
219 # Dialogs with user | |
220 # the overwrite check is done here | |
221 | |
222 def open_file_write(self, client, file_path, transfer_data, file_data, stream_object): | |
223 """create SatFile or FileStremaObject for the requested file and fill suitable data | |
224 """ | |
225 if stream_object: | |
226 assert "stream_object" not in transfer_data | |
227 transfer_data["stream_object"] = stream.FileStreamObject( | |
228 self.host, | |
229 client, | |
230 file_path, | |
231 mode="wb", | |
232 uid=file_data[PROGRESS_ID_KEY], | |
233 size=file_data["size"], | |
234 data_cb=file_data.get("data_cb"), | |
235 ) | |
236 else: | |
237 assert "file_obj" not in transfer_data | |
238 transfer_data["file_obj"] = stream.SatFile( | |
239 self.host, | |
240 client, | |
241 file_path, | |
242 mode="wb", | |
243 uid=file_data[PROGRESS_ID_KEY], | |
244 size=file_data["size"], | |
245 data_cb=file_data.get("data_cb"), | |
246 ) | |
247 | |
248 async def _got_confirmation( | |
249 self, client, data, peer_jid, transfer_data, file_data, stream_object | |
250 ): | |
251 """Called when the permission and dest path have been received | |
252 | |
253 @param peer_jid(jid.JID): jid of the file sender | |
254 @param transfer_data(dict): same as for [self.get_dest_dir] | |
255 @param file_data(dict): same as for [self.get_dest_dir] | |
256 @param stream_object(bool): same as for [self.get_dest_dir] | |
257 return (bool): True if copy is wanted and OK | |
258 False if user wants to cancel | |
259 if file exists ask confirmation and call again self._getDestDir if needed | |
260 """ | |
261 if data.get("cancelled", False): | |
262 return False | |
263 path = data["path"] | |
264 file_data["file_path"] = file_path = os.path.join(path, file_data["name"]) | |
265 log.debug("destination file path set to {}".format(file_path)) | |
266 | |
267 # we manage case where file already exists | |
268 if os.path.exists(file_path): | |
269 overwrite = await xml_tools.defer_confirm( | |
270 self.host, | |
271 _(CONFIRM_OVERWRITE).format(file_path), | |
272 _(CONFIRM_OVERWRITE_TITLE), | |
273 action_extra={ | |
274 "from_jid": peer_jid.full(), | |
275 "type": C.META_TYPE_OVERWRITE, | |
276 "progress_id": file_data[PROGRESS_ID_KEY], | |
277 }, | |
278 security_limit=SECURITY_LIMIT, | |
279 profile=client.profile, | |
280 ) | |
281 | |
282 if not overwrite: | |
283 return await self.get_dest_dir(client, peer_jid, transfer_data, file_data) | |
284 | |
285 self.open_file_write(client, file_path, transfer_data, file_data, stream_object) | |
286 return True | |
287 | |
288 async def get_dest_dir( | |
289 self, client, peer_jid, transfer_data, file_data, stream_object=False | |
290 ): | |
291 """Request confirmation and destination dir to user | |
292 | |
293 Overwrite confirmation is managed. | |
294 if transfer is confirmed, 'file_obj' is added to transfer_data | |
295 @param peer_jid(jid.JID): jid of the file sender | |
296 @param filename(unicode): name of the file | |
297 @param transfer_data(dict): data of the transfer session, | |
298 it will be only used to store the file_obj. | |
299 "file_obj" (or "stream_object") key *MUST NOT* exist before using get_dest_dir | |
300 @param file_data(dict): information about the file to be transfered | |
301 It MUST contain the following keys: | |
302 - peer_jid (jid.JID): other peer jid | |
303 - name (unicode): name of the file to trasnsfer | |
304 the name must not be empty or contain a "/" character | |
305 - size (int): size of the file | |
306 - desc (unicode): description of the file | |
307 - progress_id (unicode): id to use for progression | |
308 It *MUST NOT* contain the "peer" key | |
309 It may contain: | |
310 - data_cb (callable): method called on each data read/write | |
311 "file_path" will be added to this dict once destination selected | |
312 "size_human" will also be added with human readable file size | |
313 @param stream_object(bool): if True, a stream_object will be used instead of file_obj | |
314 a stream.FileStreamObject will be used | |
315 return: True if transfer is accepted | |
316 """ | |
317 cont, ret_value = await self.host.trigger.async_return_point( | |
318 "FILE_getDestDir", client, peer_jid, transfer_data, file_data, stream_object | |
319 ) | |
320 if not cont: | |
321 return ret_value | |
322 filename = file_data["name"] | |
323 assert filename and not "/" in filename | |
324 assert PROGRESS_ID_KEY in file_data | |
325 # human readable size | |
326 file_data["size_human"] = common_utils.get_human_size(file_data["size"]) | |
327 resp_data = await xml_tools.defer_dialog( | |
328 self.host, | |
329 _(CONFIRM).format(peer=peer_jid.full(), **file_data), | |
330 _(CONFIRM_TITLE), | |
331 type_=C.XMLUI_DIALOG_FILE, | |
332 options={C.XMLUI_DATA_FILETYPE: C.XMLUI_DATA_FILETYPE_DIR}, | |
333 action_extra={ | |
334 "from_jid": peer_jid.full(), | |
335 "type": C.META_TYPE_FILE, | |
336 "progress_id": file_data[PROGRESS_ID_KEY], | |
337 }, | |
338 security_limit=SECURITY_LIMIT, | |
339 profile=client.profile, | |
340 ) | |
341 | |
342 accepted = await self._got_confirmation( | |
343 client, | |
344 resp_data, | |
345 peer_jid, | |
346 transfer_data, | |
347 file_data, | |
348 stream_object, | |
349 ) | |
350 return accepted |