Mercurial > libervia-desktop-kivy
comparison libervia/desktop_kivy/plugins/plugin_wid_file_sharing.py @ 493:b3cedbee561d
refactoring: rename `cagou` to `libervia.desktop_kivy` + update imports and names following backend changes
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 18:26:16 +0200 |
parents | cagou/plugins/plugin_wid_file_sharing.py@203755bbe0fe |
children | 196483685a63 |
comparison
equal
deleted
inserted
replaced
492:5114bbb5daa3 | 493:b3cedbee561d |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 #Libervia Desktop-Kivy | |
4 # Copyright (C) 2016-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 | |
20 from functools import partial | |
21 import os.path | |
22 import json | |
23 from libervia.backend.core import log as logging | |
24 from libervia.backend.core import exceptions | |
25 from libervia.backend.core.i18n import _ | |
26 from libervia.backend.tools.common import files_utils | |
27 from libervia.frontends.quick_frontend import quick_widgets | |
28 from libervia.frontends.tools import jid | |
29 from ..core.constants import Const as C | |
30 from ..core import cagou_widget | |
31 from ..core.menu import EntitiesSelectorMenu | |
32 from ..core.behaviors import TouchMenuBehavior, FilterBehavior | |
33 from ..core.common_widgets import (Identities, ItemWidget, DeviceWidget, | |
34 CategorySeparator) | |
35 from libervia.desktop_kivy import G | |
36 from kivy import properties | |
37 from kivy.uix.label import Label | |
38 from kivy.uix.button import Button | |
39 from kivy import utils as kivy_utils | |
40 | |
41 log = logging.getLogger(__name__) | |
42 | |
43 | |
44 PLUGIN_INFO = { | |
45 "name": _("file sharing"), | |
46 "main": "FileSharing", | |
47 "description": _("share/transfer files between devices"), | |
48 "icon_symbol": "exchange", | |
49 } | |
50 MODE_VIEW = "view" | |
51 MODE_LOCAL = "local" | |
52 SELECT_INSTRUCTIONS = _("Please select entities to share with") | |
53 | |
54 if kivy_utils.platform == "android": | |
55 from jnius import autoclass | |
56 Environment = autoclass("android.os.Environment") | |
57 base_dir = Environment.getExternalStorageDirectory().getAbsolutePath() | |
58 def expanduser(path): | |
59 if path == '~' or path.startswith('~/'): | |
60 return path.replace('~', base_dir, 1) | |
61 return path | |
62 else: | |
63 expanduser = os.path.expanduser | |
64 | |
65 | |
66 class ModeBtn(Button): | |
67 | |
68 def __init__(self, parent, **kwargs): | |
69 super(ModeBtn, self).__init__(**kwargs) | |
70 parent.bind(mode=self.on_mode) | |
71 self.on_mode(parent, parent.mode) | |
72 | |
73 def on_mode(self, parent, new_mode): | |
74 if new_mode == MODE_VIEW: | |
75 self.text = _("view shared files") | |
76 elif new_mode == MODE_LOCAL: | |
77 self.text = _("share local files") | |
78 else: | |
79 exceptions.InternalError("Unknown mode: {mode}".format(mode=new_mode)) | |
80 | |
81 | |
82 class PathWidget(ItemWidget): | |
83 | |
84 def __init__(self, filepath, main_wid, **kw): | |
85 name = os.path.basename(filepath) | |
86 self.filepath = os.path.normpath(filepath) | |
87 if self.filepath == '.': | |
88 self.filepath = '' | |
89 super(PathWidget, self).__init__(name=name, main_wid=main_wid, **kw) | |
90 | |
91 @property | |
92 def is_dir(self): | |
93 raise NotImplementedError | |
94 | |
95 def do_item_action(self, touch): | |
96 if self.is_dir: | |
97 self.main_wid.current_dir = self.filepath | |
98 | |
99 def open_menu(self, touch, dt): | |
100 log.debug(_("opening menu for {path}").format(path=self.filepath)) | |
101 super(PathWidget, self).open_menu(touch, dt) | |
102 | |
103 | |
104 class LocalPathWidget(PathWidget): | |
105 | |
106 @property | |
107 def is_dir(self): | |
108 return os.path.isdir(self.filepath) | |
109 | |
110 def get_menu_choices(self): | |
111 choices = [] | |
112 if self.shared: | |
113 choices.append(dict(text=_('unshare'), | |
114 index=len(choices)+1, | |
115 callback=self.main_wid.unshare)) | |
116 else: | |
117 choices.append(dict(text=_('share'), | |
118 index=len(choices)+1, | |
119 callback=self.main_wid.share)) | |
120 return choices | |
121 | |
122 | |
123 class RemotePathWidget(PathWidget): | |
124 | |
125 def __init__(self, main_wid, filepath, type_, **kw): | |
126 self.type_ = type_ | |
127 super(RemotePathWidget, self).__init__(filepath, main_wid=main_wid, **kw) | |
128 | |
129 @property | |
130 def is_dir(self): | |
131 return self.type_ == C.FILE_TYPE_DIRECTORY | |
132 | |
133 def do_item_action(self, touch): | |
134 if self.is_dir: | |
135 if self.filepath == '..': | |
136 self.main_wid.remote_entity = '' | |
137 else: | |
138 super(RemotePathWidget, self).do_item_action(touch) | |
139 else: | |
140 self.main_wid.request_item(self) | |
141 return True | |
142 | |
143 class SharingDeviceWidget(DeviceWidget): | |
144 | |
145 def do_item_action(self, touch): | |
146 self.main_wid.remote_entity = self.entity_jid | |
147 self.main_wid.remote_dir = '' | |
148 | |
149 | |
150 class FileSharing(quick_widgets.QuickWidget, cagou_widget.LiberviaDesktopKivyWidget, FilterBehavior, | |
151 TouchMenuBehavior): | |
152 SINGLE=False | |
153 layout = properties.ObjectProperty() | |
154 mode = properties.OptionProperty(MODE_VIEW, options=[MODE_VIEW, MODE_LOCAL]) | |
155 local_dir = properties.StringProperty(expanduser('~')) | |
156 remote_dir = properties.StringProperty('') | |
157 remote_entity = properties.StringProperty('') | |
158 shared_paths = properties.ListProperty() | |
159 use_header_input = True | |
160 signals_registered = False | |
161 | |
162 def __init__(self, host, target, profiles): | |
163 quick_widgets.QuickWidget.__init__(self, host, target, profiles) | |
164 cagou_widget.LiberviaDesktopKivyWidget.__init__(self) | |
165 FilterBehavior.__init__(self) | |
166 TouchMenuBehavior.__init__(self) | |
167 self.mode_btn = ModeBtn(self) | |
168 self.mode_btn.bind(on_release=self.change_mode) | |
169 self.header_input_add_extra(self.mode_btn) | |
170 self.bind(local_dir=self.update_view, | |
171 remote_dir=self.update_view, | |
172 remote_entity=self.update_view) | |
173 self.update_view() | |
174 if not FileSharing.signals_registered: | |
175 # FIXME: we use this hack (registering the signal for the whole class) now | |
176 # as there is currently no unregisterSignal available in bridges | |
177 G.host.register_signal("fis_shared_path_new", | |
178 handler=FileSharing.shared_path_new, | |
179 iface="plugin") | |
180 G.host.register_signal("fis_shared_path_removed", | |
181 handler=FileSharing.shared_path_removed, | |
182 iface="plugin") | |
183 FileSharing.signals_registered = True | |
184 G.host.bridge.fis_local_shares_get(self.profile, | |
185 callback=self.fill_paths, | |
186 errback=G.host.errback) | |
187 | |
188 @property | |
189 def current_dir(self): | |
190 return self.local_dir if self.mode == MODE_LOCAL else self.remote_dir | |
191 | |
192 @current_dir.setter | |
193 def current_dir(self, new_dir): | |
194 if self.mode == MODE_LOCAL: | |
195 self.local_dir = new_dir | |
196 else: | |
197 self.remote_dir = new_dir | |
198 | |
199 def fill_paths(self, shared_paths): | |
200 self.shared_paths.extend(shared_paths) | |
201 | |
202 def change_mode(self, mode_btn): | |
203 self.clear_menu() | |
204 opt = self.__class__.mode.options | |
205 new_idx = (opt.index(self.mode)+1) % len(opt) | |
206 self.mode = opt[new_idx] | |
207 | |
208 def on_mode(self, instance, new_mode): | |
209 self.update_view(None, self.local_dir) | |
210 | |
211 def on_header_wid_input(self): | |
212 if '/' in self.header_input.text or self.header_input.text == '~': | |
213 self.current_dir = expanduser(self.header_input.text) | |
214 | |
215 def on_header_wid_input_complete(self, wid, text, **kwargs): | |
216 """we filter items when text is entered in input box""" | |
217 if '/' in text: | |
218 return | |
219 self.do_filter(self.layout, | |
220 text, | |
221 lambda c: c.name, | |
222 width_cb=lambda c: c.base_width, | |
223 height_cb=lambda c: c.minimum_height, | |
224 continue_tests=[lambda c: not isinstance(c, ItemWidget), | |
225 lambda c: c.name == '..']) | |
226 | |
227 | |
228 ## remote sharing callback ## | |
229 | |
230 def _disco_find_by_features_cb(self, data): | |
231 entities_services, entities_own, entities_roster = data | |
232 for entities_map, title in ((entities_services, | |
233 _('services')), | |
234 (entities_own, | |
235 _('your devices')), | |
236 (entities_roster, | |
237 _('your contacts devices'))): | |
238 if entities_map: | |
239 self.layout.add_widget(CategorySeparator(text=title)) | |
240 for entity_str, entity_ids in entities_map.items(): | |
241 entity_jid = jid.JID(entity_str) | |
242 item = SharingDeviceWidget( | |
243 self, entity_jid, Identities(entity_ids)) | |
244 self.layout.add_widget(item) | |
245 if not entities_services and not entities_own and not entities_roster: | |
246 self.layout.add_widget(Label( | |
247 size_hint=(1, 1), | |
248 halign='center', | |
249 text_size=self.size, | |
250 text=_("No sharing device found"))) | |
251 | |
252 def discover_devices(self): | |
253 """Looks for devices handling file "File Information Sharing" and display them""" | |
254 try: | |
255 namespace = self.host.ns_map['fis'] | |
256 except KeyError: | |
257 msg = _("can't find file information sharing namespace, " | |
258 "is the plugin running?") | |
259 log.warning(msg) | |
260 G.host.add_note(_("missing plugin"), msg, C.XMLUI_DATA_LVL_ERROR) | |
261 return | |
262 self.host.bridge.disco_find_by_features( | |
263 [namespace], [], False, True, True, True, False, self.profile, | |
264 callback=self._disco_find_by_features_cb, | |
265 errback=partial(G.host.errback, | |
266 title=_("shared folder error"), | |
267 message=_("can't check sharing devices: {msg}"))) | |
268 | |
269 def fis_list_cb(self, files_data): | |
270 for file_data in files_data: | |
271 filepath = os.path.join(self.current_dir, file_data['name']) | |
272 item = RemotePathWidget( | |
273 filepath=filepath, | |
274 main_wid=self, | |
275 type_=file_data['type']) | |
276 self.layout.add_widget(item) | |
277 | |
278 def fis_list_eb(self, failure_): | |
279 self.remote_dir = '' | |
280 G.host.add_note( | |
281 _("shared folder error"), | |
282 _("can't list files for {remote_entity}: {msg}").format( | |
283 remote_entity=self.remote_entity, | |
284 msg=failure_), | |
285 level=C.XMLUI_DATA_LVL_WARNING) | |
286 | |
287 ## view generation ## | |
288 | |
289 def update_view(self, *args): | |
290 """update items according to current mode, entity and dir""" | |
291 log.debug('updating {}, {}'.format(self.current_dir, args)) | |
292 self.layout.clear_widgets() | |
293 self.header_input.text = '' | |
294 self.header_input.hint_text = self.current_dir | |
295 | |
296 if self.mode == MODE_LOCAL: | |
297 filepath = os.path.join(self.local_dir, '..') | |
298 self.layout.add_widget(LocalPathWidget(filepath=filepath, main_wid=self)) | |
299 try: | |
300 files = sorted(os.listdir(self.local_dir)) | |
301 except OSError as e: | |
302 msg = _("can't list files in \"{local_dir}\": {msg}").format( | |
303 local_dir=self.local_dir, | |
304 msg=e) | |
305 G.host.add_note( | |
306 _("shared folder error"), | |
307 msg, | |
308 level=C.XMLUI_DATA_LVL_WARNING) | |
309 self.local_dir = expanduser('~') | |
310 return | |
311 for f in files: | |
312 filepath = os.path.join(self.local_dir, f) | |
313 self.layout.add_widget(LocalPathWidget(filepath=filepath, | |
314 main_wid=self)) | |
315 elif self.mode == MODE_VIEW: | |
316 if not self.remote_entity: | |
317 self.discover_devices() | |
318 else: | |
319 # we always a way to go back | |
320 # so user can return to previous list even in case of error | |
321 parent_path = os.path.join(self.remote_dir, '..') | |
322 item = RemotePathWidget( | |
323 filepath = parent_path, | |
324 main_wid=self, | |
325 type_ = C.FILE_TYPE_DIRECTORY) | |
326 self.layout.add_widget(item) | |
327 self.host.bridge.fis_list( | |
328 str(self.remote_entity), | |
329 self.remote_dir, | |
330 {}, | |
331 self.profile, | |
332 callback=self.fis_list_cb, | |
333 errback=self.fis_list_eb) | |
334 | |
335 ## Share methods ## | |
336 | |
337 def do_share(self, entities_jids, item): | |
338 if entities_jids: | |
339 access = {'read': {'type': 'whitelist', | |
340 'jids': entities_jids}} | |
341 else: | |
342 access = {} | |
343 | |
344 G.host.bridge.fis_share_path( | |
345 item.name, | |
346 item.filepath, | |
347 json.dumps(access, ensure_ascii=False), | |
348 self.profile, | |
349 callback=lambda name: G.host.add_note( | |
350 _("sharing folder"), | |
351 _("{name} is now shared").format(name=name)), | |
352 errback=partial(G.host.errback, | |
353 title=_("sharing folder"), | |
354 message=_("can't share folder: {msg}"))) | |
355 | |
356 def share(self, menu): | |
357 item = self.menu_item | |
358 self.clear_menu() | |
359 EntitiesSelectorMenu(instructions=SELECT_INSTRUCTIONS, | |
360 callback=partial(self.do_share, item=item)).show() | |
361 | |
362 def unshare(self, menu): | |
363 item = self.menu_item | |
364 self.clear_menu() | |
365 G.host.bridge.fis_unshare_path( | |
366 item.filepath, | |
367 self.profile, | |
368 callback=lambda: G.host.add_note( | |
369 _("sharing folder"), | |
370 _("{name} is not shared anymore").format(name=item.name)), | |
371 errback=partial(G.host.errback, | |
372 title=_("sharing folder"), | |
373 message=_("can't unshare folder: {msg}"))) | |
374 | |
375 def file_jingle_request_cb(self, progress_id, item, dest_path): | |
376 G.host.add_note( | |
377 _("file request"), | |
378 _("{name} download started at {dest_path}").format( | |
379 name = item.name, | |
380 dest_path = dest_path)) | |
381 | |
382 def request_item(self, item): | |
383 """Retrieve an item from remote entity | |
384 | |
385 @param item(RemotePathWidget): item to retrieve | |
386 """ | |
387 path, name = os.path.split(item.filepath) | |
388 assert name | |
389 assert self.remote_entity | |
390 extra = {'path': path} | |
391 dest_path = files_utils.get_unique_name(os.path.join(G.host.downloads_dir, name)) | |
392 G.host.bridge.file_jingle_request(str(self.remote_entity), | |
393 str(dest_path), | |
394 name, | |
395 '', | |
396 '', | |
397 extra, | |
398 self.profile, | |
399 callback=partial(self.file_jingle_request_cb, | |
400 item=item, | |
401 dest_path=dest_path), | |
402 errback=partial(G.host.errback, | |
403 title = _("file request error"), | |
404 message = _("can't request file: {msg}"))) | |
405 | |
406 @classmethod | |
407 def shared_path_new(cls, shared_path, name, profile): | |
408 for wid in G.host.get_visible_list(cls): | |
409 if shared_path not in wid.shared_paths: | |
410 wid.shared_paths.append(shared_path) | |
411 | |
412 @classmethod | |
413 def shared_path_removed(cls, shared_path, profile): | |
414 for wid in G.host.get_visible_list(cls): | |
415 if shared_path in wid.shared_paths: | |
416 wid.shared_paths.remove(shared_path) | |
417 else: | |
418 log.warning(_("shared path {path} not found in {widget}".format( | |
419 path = shared_path, widget = wid))) |