Mercurial > libervia-backend
comparison src/plugins/plugin_xep_0234.py @ 1528:1c71d7335d02
plugin XEP-0234: jingle file transfer first draft
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 25 Sep 2015 19:24:00 +0200 |
parents | |
children | a151f3a5a2d0 |
comparison
equal
deleted
inserted
replaced
1527:bfef1934a8f3 | 1528:1c71d7335d02 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT plugin for Jingle File Transfer (XEP-0234) | |
5 # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 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 from sat.core.i18n import _, D_ | |
21 from sat.core.constants import Const as C | |
22 from sat.core.log import getLogger | |
23 log = getLogger(__name__) | |
24 from sat.core import exceptions | |
25 from sat.tools import xml_tools | |
26 from wokkel import disco, iwokkel | |
27 from zope.interface import implements | |
28 from sat.tools import utils | |
29 import os.path | |
30 from twisted.words.xish import domish | |
31 from twisted.words.protocols.jabber import jid | |
32 from twisted.python import failure | |
33 | |
34 try: | |
35 from twisted.words.protocols.xmlstream import XMPPHandler | |
36 except ImportError: | |
37 from wokkel.subprotocols import XMPPHandler | |
38 | |
39 | |
40 NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:4' | |
41 CONFIRM = D_(u'{entity} wants to send the file "{name}" to you:\n{desc}\n\nThe file has a size of {size_human}\n\nDo you accept ?') | |
42 CONFIRM_TITLE = D_(u'Confirm file transfer') | |
43 CONFIRM_OVERWRITE = D_(u'File {} already exists, are you sure you want to overwrite ?') | |
44 CONFIRM_OVERWRITE_TITLE = D_(u'File exists') | |
45 | |
46 PLUGIN_INFO = { | |
47 "name": "Jingle File Transfer", | |
48 "import_name": "XEP-0234", | |
49 "type": "XEP", | |
50 "protocols": ["XEP-0234"], | |
51 "dependencies": ["XEP-0166", "XEP-0300", "FILE"], | |
52 "main": "XEP_0234", | |
53 "handler": "yes", | |
54 "description": _("""Implementation of Jingle File Transfer""") | |
55 } | |
56 | |
57 | |
58 class XEP_0234(object): | |
59 | |
60 def __init__(self, host): | |
61 log.info(_("plugin Jingle File Transfer initialization")) | |
62 self.host = host | |
63 self._j = host.plugins["XEP-0166"] # shortcut to access jingle | |
64 self._j.registerApplication(NS_JINGLE_FT, self) | |
65 self._f = host.plugins["FILE"] | |
66 host.bridge.addMethod("__test", ".plugin", in_sign='', out_sign='', method=self.__test) | |
67 host.bridge.addMethod("fileJingleSend", ".plugin", in_sign='sssss', out_sign='', method=self._fileJingleSend) | |
68 | |
69 def getHandler(self, profile): | |
70 return XEP_0234_handler() | |
71 | |
72 def _fileJingleSend(self, to_jid, filepath, name="", file_desc="", profile=C.PROF_KEY_NONE): | |
73 return self.fileJingleSend(jid.JID(to_jid), filepath, name or None, file_desc or None, profile) | |
74 | |
75 def fileJingleSend(self, to_jid, filepath, name=None, file_desc=None, profile=C.PROF_KEY_NONE): | |
76 self._j.initiate(to_jid, | |
77 [{'app_ns': NS_JINGLE_FT, | |
78 'app_kwargs': {'filepath': filepath, | |
79 'name': name, | |
80 'file_desc': file_desc}, | |
81 }], | |
82 profile=profile) | |
83 | |
84 | |
85 # Dialogs with user | |
86 # the overwrite check is done here | |
87 | |
88 def _getDestDir(self, session, content_data, profile): | |
89 """Request confirmation and destination dir to user | |
90 | |
91 if transfer is confirmed, session is filled | |
92 @param session(dict): jingle session data | |
93 @param content_data(dict): content informations | |
94 @param profile: %(doc_profile)s | |
95 return (defer.Deferred): True if transfer is accepted | |
96 """ | |
97 file_data = content_data['file_data'] | |
98 d = xml_tools.deferDialog(self.host, | |
99 _(CONFIRM).format(entity=session['to_jid'].full(), **file_data), | |
100 _(CONFIRM_TITLE), | |
101 type_=C.XMLUI_DIALOG_FILE, | |
102 options={C.XMLUI_DATA_FILETYPE: C.XMLUI_DATA_FILETYPE_DIR}, | |
103 profile=profile) | |
104 d.addCallback(self._gotConfirmation, session, content_data, profile) | |
105 return d | |
106 | |
107 def _gotConfirmation(self, data, session, content_data, profile): | |
108 """Called when the permission and dest path have been received | |
109 | |
110 @param data(dict): xmlui data received from file dialog | |
111 return (bool): True if copy is wanted and OK | |
112 False if user wants to cancel | |
113 if fill exists ask confirmation and call again self._getDestDir if needed | |
114 """ | |
115 if data.get('cancelled', False): | |
116 return False | |
117 file_data = content_data['file_data'] | |
118 path = data['path'] | |
119 file_data['file_path'] = file_path = os.path.join(path, file_data['name']) | |
120 log.debug(u'destination file path set to {}'.format(file_path)) | |
121 | |
122 # we manage case where file already exists | |
123 if os.path.exists(file_path): | |
124 def check_overwrite(overwrite): | |
125 if overwrite: | |
126 assert 'file_obj' not in content_data | |
127 content_data['file_obj'] = open(file_path, 'w') | |
128 return True | |
129 else: | |
130 return self._getDestDir(session, content_data, profile) | |
131 | |
132 exists_d = xml_tools.deferConfirm( | |
133 self.host, | |
134 _(CONFIRM_OVERWRITE).format(file_path), | |
135 _(CONFIRM_OVERWRITE_TITLE), | |
136 profile=profile) | |
137 exists_d.addCallback(check_overwrite) | |
138 return exists_d | |
139 | |
140 assert 'file_obj' not in content_data | |
141 content_data['file_obj'] = open(file_path, 'w') | |
142 return True | |
143 | |
144 # jingle callbacks | |
145 | |
146 def jingleSessionInit(self, session, content_name, filepath, name=None, file_desc=None, profile=C.PROF_KEY_NONE): | |
147 content_data = session['contents'][content_name] | |
148 assert 'file_path' not in content_data | |
149 content_data['file_path'] = filepath | |
150 file_data = content_data['file_data'] = {} | |
151 file_data['date'] = utils.xmpp_date() | |
152 file_data['desc'] = file_desc or '' | |
153 file_data['media-type'] = "application/octet-stream" # TODO | |
154 file_data['name'] = os.path.basename(filepath) if name is None else name | |
155 file_data['size'] = os.path.getsize(filepath) | |
156 desc_elt = domish.Element((NS_JINGLE_FT, 'description')) | |
157 file_elt = desc_elt.addElement("file") | |
158 for name in ('date', 'desc', 'media-type', 'name', 'size'): | |
159 file_elt.addElement(name, content=unicode(file_data[name])) | |
160 file_elt.addElement("range") # TODO | |
161 file_elt.addChild(self.host.plugins["XEP-0300"].buidHash()) | |
162 return desc_elt | |
163 | |
164 def jingleRequestConfirmation(self, action, session, content_name, desc_elt, profile): | |
165 """This method request confirmation for a jingle session""" | |
166 content_data = session['contents'][content_name] | |
167 # first we grab file informations | |
168 try: | |
169 file_elt = desc_elt.elements(NS_JINGLE_FT, 'file').next() | |
170 except StopIteration: | |
171 raise failure.Failure(exceptions.DataError) | |
172 file_data = {} | |
173 for name in ('date', 'desc', 'media-type', 'name', 'range', 'size'): | |
174 try: | |
175 file_data[name] = unicode(file_elt.elements(NS_JINGLE_FT, name).next()) | |
176 except StopIteration: | |
177 file_data[name] = '' | |
178 | |
179 try: | |
180 size = file_data['size'] = int(file_data['size']) | |
181 except ValueError: | |
182 raise failure.Failure(exceptions.DataError) | |
183 else: | |
184 # human readable size | |
185 file_data['size_human'] = u'{:.6n} Mio'.format(float(size)/(1024**2)) | |
186 | |
187 name = file_data['name'] | |
188 if '/' in name or '\\' in name: | |
189 log.warning(u"File name contain path characters, we replace them: {}".format(name)) | |
190 file_data['name'] = name.replace('/', '_').replace('\\', '_') | |
191 | |
192 # TODO: parse hash using plugin XEP-0300 | |
193 | |
194 content_data['file_data'] = file_data | |
195 | |
196 # now we actualy request permission to user | |
197 return self._getDestDir(session, content_data, profile) | |
198 | |
199 | |
200 def jingleHandler(self, action, session, content_name, desc_elt, profile): | |
201 content_data = session['contents'][content_name] | |
202 if action in (self._j.A_SESSION_INITIATE, self._j.A_ACCEPTED_ACK): | |
203 pass | |
204 elif action == self._j.A_SESSION_ACCEPT: | |
205 assert not 'file_obj' in content_data | |
206 file_path = content_data['file_path'] | |
207 size = content_data['file_data']['size'] | |
208 file_obj = content_data['file_obj'] = self._f.File(self.host, | |
209 file_path, | |
210 size=size, | |
211 profile=profile | |
212 ) | |
213 file_obj.eof.addCallback(lambda dummy: file_obj.close()) | |
214 else: | |
215 log.warning(u"FIXME: unmanaged action {}".format(action)) | |
216 return desc_elt | |
217 | |
218 | |
219 class XEP_0234_handler(XMPPHandler): | |
220 implements(iwokkel.IDisco) | |
221 | |
222 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
223 return [disco.DiscoFeature(NS_JINGLE_FT)] | |
224 | |
225 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
226 return [] |