Mercurial > libervia-backend
comparison src/plugins/plugin_xep_0234.py @ 2512:025afb04c10b
plugin XEP-0234: some cleaning + added triggers to allow plugins to change parsing/generation of <file> element
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Mar 2018 17:53:19 +0100 |
parents | 7ad5f2c4e34a |
children | a19b2c43e719 |
comparison
equal
deleted
inserted
replaced
2511:20a5e7db0609 | 2512:025afb04c10b |
---|---|
34 from twisted.internet import defer | 34 from twisted.internet import defer |
35 from twisted.internet import reactor | 35 from twisted.internet import reactor |
36 from twisted.internet import error as internet_error | 36 from twisted.internet import error as internet_error |
37 from collections import namedtuple | 37 from collections import namedtuple |
38 import regex | 38 import regex |
39 import mimetypes | |
39 | 40 |
40 | 41 |
41 NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:5' | 42 NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:5' |
42 | 43 |
43 PLUGIN_INFO = { | 44 PLUGIN_INFO = { |
57 | 58 |
58 | 59 |
59 class XEP_0234(object): | 60 class XEP_0234(object): |
60 # TODO: assure everything is closed when file is sent or session terminate is received | 61 # TODO: assure everything is closed when file is sent or session terminate is received |
61 # TODO: call self._f.unregister when unloading order will be managing (i.e. when dependencies will be unloaded at the end) | 62 # TODO: call self._f.unregister when unloading order will be managing (i.e. when dependencies will be unloaded at the end) |
63 Range = Range # we copy the class here, so it can be used by other plugins | |
62 | 64 |
63 def __init__(self, host): | 65 def __init__(self, host): |
64 log.info(_("plugin Jingle File Transfer initialization")) | 66 log.info(_("plugin Jingle File Transfer initialization")) |
65 self.host = host | 67 self.host = host |
66 host.registerNamespace('jingle-ft', NS_JINGLE_FT) | 68 host.registerNamespace('jingle-ft', NS_JINGLE_FT) |
84 """ | 86 """ |
85 return u'{}_{}'.format(session['id'], content_name) | 87 return u'{}_{}'.format(session['id'], content_name) |
86 | 88 |
87 # generic methods | 89 # generic methods |
88 | 90 |
89 def buildFileElement(self, name, file_hash=None, hash_algo=None, size=None, media_type=None, desc=None, | 91 def buildFileElement(self, name, file_hash=None, hash_algo=None, size=None, mime_type=None, desc=None, |
90 date=None, range_offset=None, range_length=None, path=None, namespace=None, file_elt=None): | 92 modified=None, transfer_range=None, path=None, namespace=None, file_elt=None, **kwargs): |
91 """Generate a <file> element with available metadata | 93 """Generate a <file> element with available metadata |
92 | 94 |
93 @param file_hash(unicode, None): hash of the file | 95 @param file_hash(unicode, None): hash of the file |
94 empty string to set <hash-used/> element | 96 empty string to set <hash-used/> element |
95 @param hash_algo(unicode, None): hash algorithm used | 97 @param hash_algo(unicode, None): hash algorithm used |
96 if file_hash is None and hash_algo is set, a <hash-used/> element will be generated | 98 if file_hash is None and hash_algo is set, a <hash-used/> element will be generated |
97 @param range_offset(int, None): offset where transfer must start | 99 @param transfer_range(Range, None): where transfer must start/stop |
98 use -1 to add an empty <range/> element | 100 @param modified(int, unicode, None): date of last modification |
99 @param date(int, unicode, None): date of last modification | |
100 0 to use current date | 101 0 to use current date |
101 int to use an unix timestamp | 102 int to use an unix timestamp |
102 else must be an unicode string which will be used as it (it must be an XMPP time) | 103 else must be an unicode string which will be used as it (it must be an XMPP time) |
103 @param file_elt(domish.Element, None): element to use | 104 @param file_elt(domish.Element, None): element to use |
104 None to create a new one | 105 None to create a new one |
106 @param **kwargs: data for plugin extension (ignored by default) | |
105 @return (domish.Element): generated element | 107 @return (domish.Element): generated element |
108 @trigger XEP-0234_buildFileElement(file_elt, extra_args): can be used to extend elements to add | |
106 """ | 109 """ |
107 if file_elt is None: | 110 if file_elt is None: |
108 file_elt = domish.Element((NS_JINGLE_FT, u'file')) | 111 file_elt = domish.Element((NS_JINGLE_FT, u'file')) |
109 for name, value in ((u'name', name), (u'size', size), ('media-type', media_type), | 112 for name, value in ((u'name', name), (u'size', size), ('media-type', mime_type), |
110 (u'desc', desc), (u'path', path), (u'namespace', namespace)): | 113 (u'desc', desc), (u'path', path), (u'namespace', namespace)): |
111 if value is not None: | 114 if value is not None: |
112 file_elt.addElement(name, content=unicode(value)) | 115 file_elt.addElement(name, content=unicode(value)) |
113 if date is not None: | 116 |
114 if isinstance(date, int): | 117 if modified is not None: |
115 file_elt.addElement(u'date', utils.xmpp_date(date or None)) | 118 if isinstance(modified, int): |
119 file_elt.addElement(u'date', utils.xmpp_date(modified or None)) | |
116 else: | 120 else: |
117 file_elt.addElement(u'date', date) | 121 file_elt.addElement(u'date', modified) |
118 if range_offset or range_length: | 122 elif 'created' in kwargs: |
119 range_elt = file_elt.addElement(u'range') | 123 file_elt.addElement(u'date', utils.xmpp_date(kwargs.pop('created'))) |
120 if range_offset is not None and range_offset != -1: | 124 |
121 range_elt[u'offset'] = range_offset | 125 range_elt = file_elt.addElement(u'range') |
122 if range_length is not None: | 126 if transfer_range is not None: |
123 range_elt[u'length'] = range_length | 127 if transfer_range.offset is not None: |
128 range_elt[u'offset'] = transfer_range.offset | |
129 if transfer_range.length is not None: | |
130 range_elt[u'length'] = transfer_range.length | |
124 if file_hash is not None: | 131 if file_hash is not None: |
125 if not file_hash: | 132 if not file_hash: |
126 file_elt.addChild(self._hash.buildHashUsedElt()) | 133 file_elt.addChild(self._hash.buildHashUsedElt()) |
127 else: | 134 else: |
128 file_elt.addChild(self._hash.buildHashElt(file_hash, hash_algo)) | 135 file_elt.addChild(self._hash.buildHashElt(file_hash, hash_algo)) |
129 elif hash_algo is not None: | 136 elif hash_algo is not None: |
130 file_elt.addChild(self._hash.buildHashUsedElt(hash_algo)) | 137 file_elt.addChild(self._hash.buildHashUsedElt(hash_algo)) |
138 self.host.trigger.point(u'XEP-0234_buildFileElement', file_elt, extra_args=kwargs) | |
139 if kwargs: | |
140 for kw in kwargs: | |
141 log.debug('ignored keyword: {}'.format(kw)) | |
131 return file_elt | 142 return file_elt |
132 | 143 |
133 def buildFileElementFromDict(self, file_data, file_elt = None, **kwargs): | 144 def buildFileElementFromDict(self, file_data, **kwargs): |
134 """like buildFileElement but get values from a file_data dict | 145 """like buildFileElement but get values from a file_data dict |
135 | 146 |
136 @param file_data(dict): metadata to use | 147 @param file_data(dict): metadata to use |
137 @param **kwargs: data to override | 148 @param **kwargs: data to override |
138 """ | 149 """ |
139 if kwargs: | 150 if kwargs: |
140 file_data = file_data.copy() | 151 file_data = file_data.copy() |
141 file_data.update(kwargs) | 152 file_data.update(kwargs) |
142 (name, file_hash, hash_algo, size, media_type, | 153 return self. buildFileElement(**file_data) |
143 desc, date, path, namespace) = (file_data.get(u'name'), | 154 |
144 file_data.get(u'file_hash'), | 155 |
145 file_data.get(u'hash_algo'), | 156 def parseFileElement(self, file_elt, file_data=None, given=False, parent_elt=None, keep_empty_range=False): |
146 file_data.get(u'size'), | |
147 file_data.get(u'media-type'), | |
148 file_data.get(u'desc'), | |
149 file_data.get(u'date'), | |
150 file_data.get(u'path'), | |
151 file_data.get(u'namespace')) | |
152 try: | |
153 range_offset, range_length = file_data[u'range'] | |
154 except KeyError: | |
155 range_offset = range_length = None | |
156 return self. buildFileElement(name = name, file_hash = file_hash, hash_algo = hash_algo, size = size, | |
157 media_type = media_type, desc = desc, date = date, | |
158 range_offset = range_offset, range_length = range_length, path = path, | |
159 namespace = namespace, file_elt = file_elt) | |
160 | |
161 | |
162 def parseFileElement(self, file_elt, file_data=None, given=False, parent_elt=None): | |
163 """Parse a <file> element and file dictionary accordingly | 157 """Parse a <file> element and file dictionary accordingly |
164 | 158 |
165 @param file_data(dict, None): dict where the data will be set | 159 @param file_data(dict, None): dict where the data will be set |
166 following keys will be set (and overwritten if they already exist): | 160 following keys will be set (and overwritten if they already exist): |
167 name, file_hash, hash_algo, size, media-type, desc, path, namespace, range | 161 name, file_hash, hash_algo, size, mime_type, desc, path, namespace, range |
168 if None, a new dict is created | 162 if None, a new dict is created |
169 @param given(bool): if True, prefix hash key with "given_" | 163 @param given(bool): if True, prefix hash key with "given_" |
170 @param parent_elt(domish.Element, None): parent of the file element | 164 @param parent_elt(domish.Element, None): parent of the file element |
171 if set, file_elt must not be set | 165 if set, file_elt must not be set |
166 @param keep_empty_range(bool): if True, keep empty range (i.e. range when offset and length are None) | |
167 empty range are useful to know if a peer_jid can handle range | |
172 @return (dict): file_data | 168 @return (dict): file_data |
169 @trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new elements | |
173 @raise exceptions.NotFound: there is not <file> element in parent_elt | 170 @raise exceptions.NotFound: there is not <file> element in parent_elt |
174 @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT | 171 @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT |
175 """ | 172 """ |
176 if parent_elt is not None: | 173 if parent_elt is not None: |
177 if file_elt is not None: | 174 if file_elt is not None: |
185 raise exceptions.DataError(u'invalid <file> element: {stanza}'.format(stanza = file_elt.toXml())) | 182 raise exceptions.DataError(u'invalid <file> element: {stanza}'.format(stanza = file_elt.toXml())) |
186 | 183 |
187 if file_data is None: | 184 if file_data is None: |
188 file_data = {} | 185 file_data = {} |
189 | 186 |
190 for name in (u'name', u'media-type', u'desc', u'path', u'namespace'): | 187 for name in (u'name', u'desc', u'path', u'namespace'): |
191 try: | 188 try: |
192 file_data[name] = unicode(file_elt.elements(NS_JINGLE_FT, name).next()) | 189 file_data[name] = unicode(next(file_elt.elements(NS_JINGLE_FT, name))) |
193 except StopIteration: | 190 except StopIteration: |
194 pass | 191 pass |
192 | |
195 | 193 |
196 name = file_data.get(u'name') | 194 name = file_data.get(u'name') |
197 if name == u'..': | 195 if name == u'..': |
198 # we don't want to go to parent dir when joining to a path | 196 # we don't want to go to parent dir when joining to a path |
199 name = u'--' | 197 name = u'--' |
200 file_data[u'name'] = name | 198 file_data[u'name'] = name |
201 elif name is not None and u'/' in name or u'\\' in name: | 199 elif name is not None and u'/' in name or u'\\' in name: |
202 file_data[u'name'] = regex.pathEscape(name) | 200 file_data[u'name'] = regex.pathEscape(name) |
203 | 201 |
204 try: | 202 try: |
205 file_data[u'size'] = int(unicode(file_elt.elements(NS_JINGLE_FT, u'size').next())) | 203 file_data[u'mime_type'] = unicode(next(file_elt.elements(NS_JINGLE_FT, u'media-type'))) |
204 except StopIteration: | |
205 pass | |
206 | |
207 try: | |
208 file_data[u'size'] = int(unicode(next(file_elt.elements(NS_JINGLE_FT, u'size')))) | |
209 except StopIteration: | |
210 pass | |
211 | |
212 try: | |
213 file_data[u'modified'] = utils.date_parse(next(file_elt.elements(NS_JINGLE_FT, u'date'))) | |
206 except StopIteration: | 214 except StopIteration: |
207 pass | 215 pass |
208 | 216 |
209 try: | 217 try: |
210 range_elt = file_elt.elements(NS_JINGLE_FT, u'range').next() | 218 range_elt = file_elt.elements(NS_JINGLE_FT, u'range').next() |
211 except StopIteration: | 219 except StopIteration: |
212 pass | 220 pass |
213 else: | 221 else: |
214 offset = range_elt.getAttribute('offset') | 222 offset = range_elt.getAttribute('offset') |
215 length = range_elt.getAttribute('length') | 223 length = range_elt.getAttribute('length') |
216 file_data[u'range'] = Range(offset=offset, length=length) | 224 if offset or length or keep_empty_range: |
225 file_data[u'transfer_range'] = Range(offset=offset, length=length) | |
217 | 226 |
218 prefix = u'given_' if given else u'' | 227 prefix = u'given_' if given else u'' |
219 hash_algo_key, hash_key = u'hash_algo', prefix + u'file_hash' | 228 hash_algo_key, hash_key = u'hash_algo', prefix + u'file_hash' |
220 try: | 229 try: |
221 file_data[hash_algo_key], file_data[hash_key] = self._hash.parseHashElt(file_elt) | 230 file_data[hash_algo_key], file_data[hash_key] = self._hash.parseHashElt(file_elt) |
222 except exceptions.NotFound: | 231 except exceptions.NotFound: |
223 pass | 232 pass |
233 | |
234 self.host.trigger.point(u'XEP-0234_parseFileElement', file_elt, file_data) | |
224 | 235 |
225 return file_data | 236 return file_data |
226 | 237 |
227 # bridge methods | 238 # bridge methods |
228 | 239 |
312 desc_elt = domish.Element((NS_JINGLE_FT, 'description')) | 323 desc_elt = domish.Element((NS_JINGLE_FT, 'description')) |
313 file_elt = desc_elt.addElement("file") | 324 file_elt = desc_elt.addElement("file") |
314 | 325 |
315 if content_data[u'senders'] == self._j.ROLE_INITIATOR: | 326 if content_data[u'senders'] == self._j.ROLE_INITIATOR: |
316 # we send a file | 327 # we send a file |
328 if name is None: | |
329 name = os.path.basename(filepath) | |
317 file_data[u'date'] = utils.xmpp_date() | 330 file_data[u'date'] = utils.xmpp_date() |
318 file_data[u'desc'] = extra.pop(u'file_desc', u'') | 331 file_data[u'desc'] = extra.pop(u'file_desc', u'') |
319 file_data[u'media-type'] = "application/octet-stream" # TODO | 332 file_data[u'name'] = name |
320 file_data[u'name'] = os.path.basename(filepath) if name is None else name | 333 mime_type = mimetypes.guess_type(name, strict=False)[0] |
334 if mime_type is not None: | |
335 file_data[u'mime_type'] = mime_type | |
321 file_data[u'size'] = os.path.getsize(filepath) | 336 file_data[u'size'] = os.path.getsize(filepath) |
322 if u'namespace' in extra: | 337 if u'namespace' in extra: |
323 file_data[u'namespace'] = extra[u'namespace'] | 338 file_data[u'namespace'] = extra[u'namespace'] |
324 if u'path' in extra: | 339 if u'path' in extra: |
325 file_data[u'path'] = extra[u'path'] | 340 file_data[u'path'] = extra[u'path'] |
326 self.buildFileElementFromDict(file_data, file_elt=file_elt, file_hash=u'') | 341 self.buildFileElementFromDict(file_data, file_elt=file_elt, file_hash=u'') |
327 file_elt.addElement("range") # TODO | |
328 else: | 342 else: |
329 # we request a file | 343 # we request a file |
330 file_hash = extra.pop(u'file_hash', u'') | 344 file_hash = extra.pop(u'file_hash', u'') |
331 if not name and not file_hash: | 345 if not name and not file_hash: |
332 raise ValueError(_(u'you need to provide at least name or file hash')) | 346 raise ValueError(_(u'you need to provide at least name or file hash')) |