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