Mercurial > libervia-backend
comparison sat/plugins/plugin_comp_file_sharing_management.py @ 2929:e0429ff7f6b6
plugin comp file sharing: file sharing management first draft:
the new "component file sharing management" plugin add ad-hoc commands to changes permission of one or several shared files, delete one or more files, or regenerate thumbnails.
This is a temporary plugin to make file sharing through a component usable with other entities, but should be removed (at least permission management and file deletion) in the future if we move on a pubsub based solution.
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 28 Apr 2019 09:00:51 +0200 |
parents | |
children | 0bafdbda7f5f |
comparison
equal
deleted
inserted
replaced
2928:c0f6fd75af5f | 2929:e0429ff7f6b6 |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT plugin to detect language (experimental) | |
5 # Copyright (C) 2009-2019 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.path | |
21 from functools import partial | |
22 from sat.core.i18n import _, D_ | |
23 from sat.core import exceptions | |
24 from sat.core.constants import Const as C | |
25 from sat.core.log import getLogger | |
26 from wokkel import data_form | |
27 from twisted.internet import defer | |
28 from twisted.words.protocols.jabber import jid | |
29 | |
30 log = getLogger(__name__) | |
31 | |
32 | |
33 PLUGIN_INFO = { | |
34 C.PI_NAME: u"File Sharing Management", | |
35 C.PI_IMPORT_NAME: u"FILE_SHARING_MANAGEMENT", | |
36 C.PI_MODES: [C.PLUG_MODE_COMPONENT], | |
37 C.PI_TYPE: u"EXP", | |
38 C.PI_PROTOCOLS: [], | |
39 C.PI_DEPENDENCIES: [u"XEP-0050", u"XEP-0264"], | |
40 C.PI_RECOMMENDATIONS: [], | |
41 C.PI_MAIN: u"FileSharingManagement", | |
42 C.PI_HANDLER: u"no", | |
43 C.PI_DESCRIPTION: _( | |
44 u"Experimental handling of file management for file sharing. This plugins allows " | |
45 u"to change permissions of stored files/directories or remove them." | |
46 ), | |
47 } | |
48 | |
49 NS_FILE_MANAGEMENT = u"https://salut-a-toi.org/protocol/file-management:0" | |
50 NS_FILE_MANAGEMENT_PERM = u"https://salut-a-toi.org/protocol/file-management:0#perm" | |
51 NS_FILE_MANAGEMENT_DELETE = u"https://salut-a-toi.org/protocol/file-management:0#delete" | |
52 NS_FILE_MANAGEMENT_THUMB = u"https://salut-a-toi.org/protocol/file-management:0#thumb" | |
53 | |
54 | |
55 class WorkflowError(Exception): | |
56 """Raised when workflow can't be completed""" | |
57 | |
58 def __init__(self, err_args): | |
59 """ | |
60 @param err_args(tuple): arguments to return to finish the command workflow | |
61 """ | |
62 Exception.__init__(self) | |
63 self.err_args = err_args | |
64 | |
65 | |
66 class FileSharingManagement(object): | |
67 # This is a temporary way (Q&D) to handle stored files, a better way (using pubsub | |
68 # syntax?) should be elaborated and proposed as a standard. | |
69 | |
70 def __init__(self, host): | |
71 log.info(_(u"File Sharing Management plugin initialization")) | |
72 self.host = host | |
73 self._c = host.plugins["XEP-0050"] | |
74 self._t = host.plugins["XEP-0264"] | |
75 self.files_path = host.getLocalPath(None, C.FILES_DIR, profile=False) | |
76 | |
77 def profileConnected(self, client): | |
78 self._c.addAdHocCommand( | |
79 client, self._onChangeFile, u"Change Permissions of File(s)", | |
80 node=NS_FILE_MANAGEMENT_PERM, | |
81 allowed_magics=C.ENTITY_ALL, | |
82 ) | |
83 self._c.addAdHocCommand( | |
84 client, self._onDeleteFile, u"Delete File(s)", | |
85 node=NS_FILE_MANAGEMENT_DELETE, | |
86 allowed_magics=C.ENTITY_ALL, | |
87 ) | |
88 self._c.addAdHocCommand( | |
89 client, self._onGenThumbnails, u"Generate Thumbnails", | |
90 node=NS_FILE_MANAGEMENT_THUMB, | |
91 allowed_magics=C.ENTITY_ALL, | |
92 ) | |
93 | |
94 def _err(self, reason): | |
95 """Helper method to get argument to return for error | |
96 | |
97 workflow will be interrupted with an error note | |
98 @param reason(unicode): reason of the error | |
99 @return (tuple): arguments to use in defer.returnValue | |
100 """ | |
101 status = self._c.STATUS.COMPLETED | |
102 payload = None | |
103 note = (self._c.NOTE.ERROR, reason) | |
104 return payload, status, None, note | |
105 | |
106 def _getRootArgs(self): | |
107 """Create the form to select the file to use | |
108 | |
109 @return (tuple): arguments to use in defer.returnValue | |
110 """ | |
111 status = self._c.STATUS.EXECUTING | |
112 form = data_form.Form("form", title=u"File Management", | |
113 formNamespace=NS_FILE_MANAGEMENT) | |
114 | |
115 field = data_form.Field( | |
116 "text-single", "path", required=True | |
117 ) | |
118 form.addField(field) | |
119 | |
120 field = data_form.Field( | |
121 "text-single", "namespace", required=False | |
122 ) | |
123 form.addField(field) | |
124 | |
125 payload = form.toElement() | |
126 return payload, status, None, None | |
127 | |
128 @defer.inlineCallbacks | |
129 def _getFileData(self, client, session_data, command_form): | |
130 """Retrieve field requested in root form | |
131 | |
132 "found_file" will also be set in session_data | |
133 @param command_form(data_form.Form): response to root form | |
134 @return (D(dict)): found file data | |
135 @raise WorkflowError: something is wrong | |
136 """ | |
137 fields = command_form.fields | |
138 try: | |
139 path = fields[u'path'].value.strip() | |
140 namespace = fields[u'namespace'].value or None | |
141 except KeyError: | |
142 self._c.adHocError(self._c.ERROR.BAD_PAYLOAD) | |
143 | |
144 if not path: | |
145 self._c.adHocError(self._c.ERROR.BAD_PAYLOAD) | |
146 | |
147 requestor = session_data[u'requestor'] | |
148 requestor_bare = requestor.userhostJID() | |
149 path = path.rstrip(u'/') | |
150 parent_path, basename = os.path.split(path) | |
151 | |
152 # TODO: if parent_path and basename are empty, we ask for root directory | |
153 # this must be managed | |
154 | |
155 try: | |
156 found_files = yield self.host.memory.getFiles( | |
157 client, requestor_bare, path=parent_path, name=basename, | |
158 namespace=namespace) | |
159 found_file = found_files[0] | |
160 except (exceptions.NotFound, IndexError): | |
161 raise WorkflowError(self._err(_(u"file not found"))) | |
162 except exceptions.PermissionError: | |
163 raise WorkflowError(self._err(_(u"forbidden"))) | |
164 | |
165 if found_file['owner'] != requestor_bare: | |
166 # only owner can manage files | |
167 log.warning(_(u"Only owner can manage files")) | |
168 raise WorkflowError(self._err(_(u"forbidden"))) | |
169 | |
170 session_data[u'found_file'] = found_file | |
171 session_data[u'namespace'] = namespace | |
172 defer.returnValue(found_file) | |
173 | |
174 def _updateReadPermission(self, access, allowed_jids): | |
175 if not allowed_jids: | |
176 if C.ACCESS_PERM_READ in access: | |
177 del access[C.ACCESS_PERM_READ] | |
178 elif allowed_jids == u'PUBLIC': | |
179 access[C.ACCESS_PERM_READ] = { | |
180 u"type": C.ACCESS_TYPE_PUBLIC | |
181 } | |
182 else: | |
183 access[C.ACCESS_PERM_READ] = { | |
184 u"type": C.ACCESS_TYPE_WHITELIST, | |
185 u"jids": [j.full() for j in allowed_jids] | |
186 } | |
187 | |
188 @defer.inlineCallbacks | |
189 def _updateDir(self, client, requestor, namespace, file_data, allowed_jids): | |
190 """Recursively update permission of a directory and all subdirectories | |
191 | |
192 @param file_data(dict): metadata of the file | |
193 @param allowed_jids(list[jid.JID]): list of entities allowed to read the file | |
194 """ | |
195 assert file_data[u'type'] == C.FILE_TYPE_DIRECTORY | |
196 files_data = yield self.host.memory.getFiles( | |
197 client, requestor, parent=file_data[u'id'], namespace=namespace) | |
198 | |
199 for file_data in files_data: | |
200 if not file_data[u'access'].get(C.ACCESS_PERM_READ, {}): | |
201 log.debug(u"setting {perm} read permission for {name}".format( | |
202 perm=allowed_jids, name=file_data[u'name'])) | |
203 yield self.host.memory.fileUpdate( | |
204 file_data[u'id'], u'access', | |
205 partial(self._updateReadPermission, allowed_jids=allowed_jids)) | |
206 if file_data[u'type'] == C.FILE_TYPE_DIRECTORY: | |
207 yield self._updateDir(client, requestor, namespace, file_data, u'PUBLIC') | |
208 | |
209 @defer.inlineCallbacks | |
210 def _onChangeFile(self, client, command_elt, session_data, action, node): | |
211 try: | |
212 x_elt = command_elt.elements(data_form.NS_X_DATA, "x").next() | |
213 command_form = data_form.Form.fromElement(x_elt) | |
214 except StopIteration: | |
215 command_form = None | |
216 | |
217 found_file = session_data.get('found_file') | |
218 requestor = session_data[u'requestor'] | |
219 requestor_bare = requestor.userhostJID() | |
220 | |
221 if command_form is None or len(command_form.fields) == 0: | |
222 # root request | |
223 defer.returnValue(self._getRootArgs()) | |
224 | |
225 elif found_file is None: | |
226 # file selected, we retrieve it and ask for permissions | |
227 try: | |
228 found_file = yield self._getFileData(client, session_data, command_form) | |
229 except WorkflowError as e: | |
230 defer.returnValue(e.err_args) | |
231 | |
232 # management request | |
233 if found_file[u'type'] == C.FILE_TYPE_DIRECTORY: | |
234 instructions = D_(u"Please select permissions for this directory") | |
235 else: | |
236 instructions = D_(u"Please select permissions for this file") | |
237 | |
238 form = data_form.Form("form", title=u"File Management", | |
239 instructions=[instructions], | |
240 formNamespace=NS_FILE_MANAGEMENT) | |
241 field = data_form.Field( | |
242 "text-multi", "read_allowed", required=False, | |
243 desc=u'list of jids allowed to read this file (beside yourself), or ' | |
244 u'"PUBLIC" to let a public access' | |
245 ) | |
246 read_access = found_file[u"access"].get(C.ACCESS_PERM_READ, {}) | |
247 access_type = read_access.get(u'type', C.ACCESS_TYPE_WHITELIST) | |
248 if access_type == C.ACCESS_TYPE_PUBLIC: | |
249 field.values = [u'PUBLIC'] | |
250 else: | |
251 field.values = read_access.get('jids', []) | |
252 form.addField(field) | |
253 if found_file[u'type'] == C.FILE_TYPE_DIRECTORY: | |
254 field = data_form.Field( | |
255 "boolean", "recursive", value=False, required=False, | |
256 desc=u"Files under it will be made public to follow this dir " | |
257 u"permission (only if they don't have already a permission set)." | |
258 ) | |
259 form.addField(field) | |
260 | |
261 status = self._c.STATUS.EXECUTING | |
262 payload = form.toElement() | |
263 defer.returnValue((payload, status, None, None)) | |
264 | |
265 else: | |
266 # final phase, we'll do permission change here | |
267 try: | |
268 read_allowed = command_form.fields['read_allowed'] | |
269 except KeyError: | |
270 self._c.adHocError(self._c.ERROR.BAD_PAYLOAD) | |
271 | |
272 if read_allowed.value == u'PUBLIC': | |
273 allowed_jids = u'PUBLIC' | |
274 elif read_allowed.value.strip() == u'': | |
275 allowed_jids = None | |
276 else: | |
277 try: | |
278 allowed_jids = [jid.JID(v.strip()) for v in read_allowed.values] | |
279 except RuntimeError as e: | |
280 log.warning(_(u"Can't use read_allowed values: {reason}").format( | |
281 reason=e)) | |
282 self._c.adHocError(self._c.ERROR.BAD_PAYLOAD) | |
283 | |
284 if found_file[u'type'] == C.FILE_TYPE_FILE: | |
285 yield self.host.memory.fileUpdate( | |
286 found_file[u'id'], u'access', | |
287 partial(self._updateReadPermission, allowed_jids=allowed_jids)) | |
288 else: | |
289 try: | |
290 recursive = command_form.fields['recursive'] | |
291 except KeyError: | |
292 self._c.adHocError(self._c.ERROR.BAD_PAYLOAD) | |
293 yield self.host.memory.fileUpdate( | |
294 found_file[u'id'], u'access', | |
295 partial(self._updateReadPermission, allowed_jids=allowed_jids)) | |
296 if recursive: | |
297 # we set all file under the directory as public (if they haven't | |
298 # already a permission set), so allowed entities of root directory | |
299 # can read them. | |
300 namespace = session_data[u'namespace'] | |
301 yield self._updateDir( | |
302 client, requestor_bare, namespace, found_file, u'PUBLIC') | |
303 | |
304 # job done, we can end the session | |
305 status = self._c.STATUS.COMPLETED | |
306 payload = None | |
307 note = (self._c.NOTE.INFO, _(u"management session done")) | |
308 defer.returnValue((payload, status, None, note)) | |
309 | |
310 @defer.inlineCallbacks | |
311 def _onDeleteFile(self, client, command_elt, session_data, action, node): | |
312 try: | |
313 x_elt = command_elt.elements(data_form.NS_X_DATA, "x").next() | |
314 command_form = data_form.Form.fromElement(x_elt) | |
315 except StopIteration: | |
316 command_form = None | |
317 | |
318 found_file = session_data.get('found_file') | |
319 requestor = session_data[u'requestor'] | |
320 requestor_bare = requestor.userhostJID() | |
321 | |
322 if command_form is None or len(command_form.fields) == 0: | |
323 # root request | |
324 defer.returnValue(self._getRootArgs()) | |
325 | |
326 elif found_file is None: | |
327 # file selected, we need confirmation before actually deleting | |
328 try: | |
329 found_file = yield self._getFileData(client, session_data, command_form) | |
330 except WorkflowError as e: | |
331 defer.returnValue(e.err_args) | |
332 if found_file[u'type'] == C.FILE_TYPE_DIRECTORY: | |
333 msg = D_(u"Are you sure to delete directory {name} and all files and " | |
334 u"directories under it?").format(name=found_file[u'name']) | |
335 else: | |
336 msg = D_(u"Are you sure to delete file {name}?" | |
337 .format(name=found_file[u'name'])) | |
338 form = data_form.Form("form", title=u"File Management", | |
339 instructions = [msg], | |
340 formNamespace=NS_FILE_MANAGEMENT) | |
341 field = data_form.Field( | |
342 "boolean", "confirm", value=False, required=True, | |
343 desc=u"check this box to confirm" | |
344 ) | |
345 form.addField(field) | |
346 status = self._c.STATUS.EXECUTING | |
347 payload = form.toElement() | |
348 defer.returnValue((payload, status, None, None)) | |
349 | |
350 else: | |
351 # final phase, we'll do deletion here | |
352 try: | |
353 confirmed = command_form.fields['confirm'] | |
354 except KeyError: | |
355 self._c.adHocError(self._c.ERROR.BAD_PAYLOAD) | |
356 if not confirmed: | |
357 note = None | |
358 else: | |
359 recursive = found_file[u'type'] == C.FILE_TYPE_DIRECTORY | |
360 yield self.host.memory.fileDelete( | |
361 client, requestor_bare, found_file[u'id'], recursive) | |
362 note = (self._c.NOTE.INFO, _(u"file deleted")) | |
363 status = self._c.STATUS.COMPLETED | |
364 payload = None | |
365 defer.returnValue((payload, status, None, note)) | |
366 | |
367 def _updateThumbs(self, extra, thumbnails): | |
368 extra[C.KEY_THUMBNAILS] = thumbnails | |
369 | |
370 @defer.inlineCallbacks | |
371 def _genThumbs(self, client, requestor, namespace, file_data): | |
372 """Recursively generate thumbnails | |
373 | |
374 @param file_data(dict): metadata of the file | |
375 """ | |
376 if file_data[u'type'] == C.FILE_TYPE_DIRECTORY: | |
377 sub_files_data = yield self.host.memory.getFiles( | |
378 client, requestor, parent=file_data[u'id'], namespace=namespace) | |
379 for sub_file_data in sub_files_data: | |
380 yield self._genThumbs(client, requestor, namespace, sub_file_data) | |
381 | |
382 elif file_data[u'type'] == C.FILE_TYPE_FILE: | |
383 mime_type = file_data[u'mime_type'] | |
384 file_path = os.path.join(self.files_path, file_data[u'file_hash']) | |
385 if mime_type is not None and mime_type.startswith(u"image"): | |
386 thumbnails = [] | |
387 | |
388 for max_thumb_size in (self._t.SIZE_SMALL, self._t.SIZE_MEDIUM): | |
389 try: | |
390 thumb_size, thumb_id = yield self._t.generateThumbnail( | |
391 file_path, | |
392 max_thumb_size, | |
393 # we keep thumbnails for 6 months | |
394 60 * 60 * 24 * 31 * 6, | |
395 ) | |
396 except Exception as e: | |
397 log.warning(_(u"Can't create thumbnail: {reason}") | |
398 .format(reason=e)) | |
399 break | |
400 thumbnails.append({u"id": thumb_id, u"size": thumb_size}) | |
401 | |
402 yield self.host.memory.fileUpdate( | |
403 file_data[u'id'], u'extra', | |
404 partial(self._updateThumbs, thumbnails=thumbnails)) | |
405 | |
406 log.info(u"thumbnails for [{file_name}] generated" | |
407 .format(file_name=file_data[u'name'])) | |
408 | |
409 else: | |
410 log.warning(u"unmanaged file type: {type_}".format(type_=file_data[u'type'])) | |
411 | |
412 @defer.inlineCallbacks | |
413 def _onGenThumbnails(self, client, command_elt, session_data, action, node): | |
414 try: | |
415 x_elt = command_elt.elements(data_form.NS_X_DATA, "x").next() | |
416 command_form = data_form.Form.fromElement(x_elt) | |
417 except StopIteration: | |
418 command_form = None | |
419 | |
420 found_file = session_data.get('found_file') | |
421 requestor = session_data[u'requestor'] | |
422 | |
423 if command_form is None or len(command_form.fields) == 0: | |
424 # root request | |
425 defer.returnValue(self._getRootArgs()) | |
426 | |
427 elif found_file is None: | |
428 # file selected, we retrieve it and ask for permissions | |
429 try: | |
430 found_file = yield self._getFileData(client, session_data, command_form) | |
431 except WorkflowError as e: | |
432 defer.returnValue(e.err_args) | |
433 | |
434 log.info(u"Generating thumbnails as requested") | |
435 yield self._genThumbs(client, requestor, found_file[u'namespace'], found_file) | |
436 | |
437 # job done, we can end the session | |
438 status = self._c.STATUS.COMPLETED | |
439 payload = None | |
440 note = (self._c.NOTE.INFO, _(u"thumbnails generated")) | |
441 defer.returnValue((payload, status, None, note)) |