plugin XEP-0234: some cleaning + added triggers to allow plugins to change parsing/generation of <file> element

This commit is contained in:
Goffi 2018-03-02 17:53:19 +01:00
commit 29d9713462
2 changed files with 63 additions and 49 deletions

View file

@ -36,6 +36,7 @@ from twisted.internet import reactor
from twisted.internet import error as internet_error
from collections import namedtuple
import regex
import mimetypes
NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:5'
@ -59,6 +60,7 @@ Range = namedtuple('Range', ('offset', 'length'))
class XEP_0234(object):
# TODO: assure everything is closed when file is sent or session terminate is received
# TODO: call self._f.unregister when unloading order will be managing (i.e. when dependencies will be unloaded at the end)
Range = Range # we copy the class here, so it can be used by other plugins
def __init__(self, host):
log.info(_("plugin Jingle File Transfer initialization"))
@ -86,41 +88,46 @@ class XEP_0234(object):
# generic methods
def buildFileElement(self, name, file_hash=None, hash_algo=None, size=None, media_type=None, desc=None,
date=None, range_offset=None, range_length=None, path=None, namespace=None, file_elt=None):
def buildFileElement(self, name, file_hash=None, hash_algo=None, size=None, mime_type=None, desc=None,
modified=None, transfer_range=None, path=None, namespace=None, file_elt=None, **kwargs):
"""Generate a <file> element with available metadata
@param file_hash(unicode, None): hash of the file
empty string to set <hash-used/> element
@param hash_algo(unicode, None): hash algorithm used
if file_hash is None and hash_algo is set, a <hash-used/> element will be generated
@param range_offset(int, None): offset where transfer must start
use -1 to add an empty <range/> element
@param date(int, unicode, None): date of last modification
@param transfer_range(Range, None): where transfer must start/stop
@param modified(int, unicode, None): date of last modification
0 to use current date
int to use an unix timestamp
else must be an unicode string which will be used as it (it must be an XMPP time)
@param file_elt(domish.Element, None): element to use
None to create a new one
@param **kwargs: data for plugin extension (ignored by default)
@return (domish.Element): generated element
@trigger XEP-0234_buildFileElement(file_elt, extra_args): can be used to extend elements to add
"""
if file_elt is None:
file_elt = domish.Element((NS_JINGLE_FT, u'file'))
for name, value in ((u'name', name), (u'size', size), ('media-type', media_type),
for name, value in ((u'name', name), (u'size', size), ('media-type', mime_type),
(u'desc', desc), (u'path', path), (u'namespace', namespace)):
if value is not None:
file_elt.addElement(name, content=unicode(value))
if date is not None:
if isinstance(date, int):
file_elt.addElement(u'date', utils.xmpp_date(date or None))
if modified is not None:
if isinstance(modified, int):
file_elt.addElement(u'date', utils.xmpp_date(modified or None))
else:
file_elt.addElement(u'date', date)
if range_offset or range_length:
range_elt = file_elt.addElement(u'range')
if range_offset is not None and range_offset != -1:
range_elt[u'offset'] = range_offset
if range_length is not None:
range_elt[u'length'] = range_length
file_elt.addElement(u'date', modified)
elif 'created' in kwargs:
file_elt.addElement(u'date', utils.xmpp_date(kwargs.pop('created')))
range_elt = file_elt.addElement(u'range')
if transfer_range is not None:
if transfer_range.offset is not None:
range_elt[u'offset'] = transfer_range.offset
if transfer_range.length is not None:
range_elt[u'length'] = transfer_range.length
if file_hash is not None:
if not file_hash:
file_elt.addChild(self._hash.buildHashUsedElt())
@ -128,9 +135,13 @@ class XEP_0234(object):
file_elt.addChild(self._hash.buildHashElt(file_hash, hash_algo))
elif hash_algo is not None:
file_elt.addChild(self._hash.buildHashUsedElt(hash_algo))
self.host.trigger.point(u'XEP-0234_buildFileElement', file_elt, extra_args=kwargs)
if kwargs:
for kw in kwargs:
log.debug('ignored keyword: {}'.format(kw))
return file_elt
def buildFileElementFromDict(self, file_data, file_elt = None, **kwargs):
def buildFileElementFromDict(self, file_data, **kwargs):
"""like buildFileElement but get values from a file_data dict
@param file_data(dict): metadata to use
@ -139,37 +150,23 @@ class XEP_0234(object):
if kwargs:
file_data = file_data.copy()
file_data.update(kwargs)
(name, file_hash, hash_algo, size, media_type,
desc, date, path, namespace) = (file_data.get(u'name'),
file_data.get(u'file_hash'),
file_data.get(u'hash_algo'),
file_data.get(u'size'),
file_data.get(u'media-type'),
file_data.get(u'desc'),
file_data.get(u'date'),
file_data.get(u'path'),
file_data.get(u'namespace'))
try:
range_offset, range_length = file_data[u'range']
except KeyError:
range_offset = range_length = None
return self. buildFileElement(name = name, file_hash = file_hash, hash_algo = hash_algo, size = size,
media_type = media_type, desc = desc, date = date,
range_offset = range_offset, range_length = range_length, path = path,
namespace = namespace, file_elt = file_elt)
return self. buildFileElement(**file_data)
def parseFileElement(self, file_elt, file_data=None, given=False, parent_elt=None):
def parseFileElement(self, file_elt, file_data=None, given=False, parent_elt=None, keep_empty_range=False):
"""Parse a <file> element and file dictionary accordingly
@param file_data(dict, None): dict where the data will be set
following keys will be set (and overwritten if they already exist):
name, file_hash, hash_algo, size, media-type, desc, path, namespace, range
name, file_hash, hash_algo, size, mime_type, desc, path, namespace, range
if None, a new dict is created
@param given(bool): if True, prefix hash key with "given_"
@param parent_elt(domish.Element, None): parent of the file element
if set, file_elt must not be set
@param keep_empty_range(bool): if True, keep empty range (i.e. range when offset and length are None)
empty range are useful to know if a peer_jid can handle range
@return (dict): file_data
@trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new elements
@raise exceptions.NotFound: there is not <file> element in parent_elt
@raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT
"""
@ -187,12 +184,13 @@ class XEP_0234(object):
if file_data is None:
file_data = {}
for name in (u'name', u'media-type', u'desc', u'path', u'namespace'):
for name in (u'name', u'desc', u'path', u'namespace'):
try:
file_data[name] = unicode(file_elt.elements(NS_JINGLE_FT, name).next())
file_data[name] = unicode(next(file_elt.elements(NS_JINGLE_FT, name)))
except StopIteration:
pass
name = file_data.get(u'name')
if name == u'..':
# we don't want to go to parent dir when joining to a path
@ -202,7 +200,17 @@ class XEP_0234(object):
file_data[u'name'] = regex.pathEscape(name)
try:
file_data[u'size'] = int(unicode(file_elt.elements(NS_JINGLE_FT, u'size').next()))
file_data[u'mime_type'] = unicode(next(file_elt.elements(NS_JINGLE_FT, u'media-type')))
except StopIteration:
pass
try:
file_data[u'size'] = int(unicode(next(file_elt.elements(NS_JINGLE_FT, u'size'))))
except StopIteration:
pass
try:
file_data[u'modified'] = utils.date_parse(next(file_elt.elements(NS_JINGLE_FT, u'date')))
except StopIteration:
pass
@ -213,7 +221,8 @@ class XEP_0234(object):
else:
offset = range_elt.getAttribute('offset')
length = range_elt.getAttribute('length')
file_data[u'range'] = Range(offset=offset, length=length)
if offset or length or keep_empty_range:
file_data[u'transfer_range'] = Range(offset=offset, length=length)
prefix = u'given_' if given else u''
hash_algo_key, hash_key = u'hash_algo', prefix + u'file_hash'
@ -222,6 +231,8 @@ class XEP_0234(object):
except exceptions.NotFound:
pass
self.host.trigger.point(u'XEP-0234_parseFileElement', file_elt, file_data)
return file_data
# bridge methods
@ -314,17 +325,20 @@ class XEP_0234(object):
if content_data[u'senders'] == self._j.ROLE_INITIATOR:
# we send a file
if name is None:
name = os.path.basename(filepath)
file_data[u'date'] = utils.xmpp_date()
file_data[u'desc'] = extra.pop(u'file_desc', u'')
file_data[u'media-type'] = "application/octet-stream" # TODO
file_data[u'name'] = os.path.basename(filepath) if name is None else name
file_data[u'name'] = name
mime_type = mimetypes.guess_type(name, strict=False)[0]
if mime_type is not None:
file_data[u'mime_type'] = mime_type
file_data[u'size'] = os.path.getsize(filepath)
if u'namespace' in extra:
file_data[u'namespace'] = extra[u'namespace']
if u'path' in extra:
file_data[u'path'] = extra[u'path']
self.buildFileElementFromDict(file_data, file_elt=file_elt, file_hash=u'')
file_elt.addElement("range") # TODO
else:
# we request a file
file_hash = extra.pop(u'file_hash', u'')

View file

@ -311,11 +311,11 @@ class XEP_0329(object):
name = os.path.basename(path)
if os.path.isfile(path):
size = os.path.getsize(path)
media_type = mimetypes.guess_type(path, strict=False)[0]
mime_type = mimetypes.guess_type(path, strict=False)[0]
file_elt = self._jf.buildFileElement(name = name,
size = size,
media_type = media_type,
date = os.path.getmtime(path))
mime_type = mime_type,
modified = os.path.getmtime(path))
query_elt.addChild(file_elt)
# we don't specify hash as it would be too resource intensive to calculate it for all files
@ -323,7 +323,7 @@ class XEP_0329(object):
name_data = client._XEP_0329_names_data.setdefault(name, {})
if path not in name_data:
name_data[path] = {'size': size,
'media_type': media_type,
'mime_type': mime_type,
'parent': parent_node}
else:
# we have a directory
@ -435,7 +435,7 @@ class XEP_0329(object):
query_elt[u'node'] = node_path
for file_data in files_data:
file_elt = self._jf.buildFileElementFromDict(file_data,
date=file_data.get(u'modified', file_data[u'created']))
modified=file_data.get(u'modified', file_data[u'created']))
query_elt.addChild(file_elt)
client.send(iq_result_elt)