2
This module is based on a rox module (LGPL):
4
http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log
6
This module provides access to the shared MIME database.
8
types is a dictionary of all known MIME types, indexed by the type name, e.g.
9
types['application/x-python']
11
Applications can install information about MIME types by storing an
12
XML file as <MIME>/packages/<application>.xml and running the
13
update-mime-database command, which is provided by the freedesktop.org
14
shared mime database package.
16
See http://www.freedesktop.org/standards/shared-mime-info-spec/ for
17
information about the format of these files.
19
(based on version 0.13)
26
import xdg.BaseDirectory
29
from xml.dom import Node, minidom, XML_NAMESPACE
31
FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info'
33
types = {} # Maps MIME names to type objects
35
exts = None # Maps extensions to types
36
globs = None # List of (glob, type) pairs
37
literals = None # Maps liternal names to types
40
def _get_node_data(node):
41
"""Get text of XML node"""
42
return ''.join([n.nodeValue for n in node.childNodes]).strip()
44
def lookup(media, subtype = None):
45
"Get the MIMEtype object for this type, creating a new one if needed."
46
if subtype is None and '/' in media:
47
media, subtype = media.split('/', 1)
48
if (media, subtype) not in types:
49
types[(media, subtype)] = MIMEtype(media, subtype)
50
return types[(media, subtype)]
53
"""Type holding data about a MIME type"""
54
def __init__(self, media, subtype):
55
"Don't use this constructor directly; use mime.lookup() instead."
56
assert media and '/' not in media
57
assert subtype and '/' not in subtype
58
assert (media, subtype) not in types
61
self.subtype = subtype
65
"Loads comment for current language. Use get_comment() instead."
66
resource = os.path.join('mime', self.media, self.subtype + '.xml')
67
for path in xdg.BaseDirectory.load_data_paths(resource):
68
doc = minidom.parse(path)
71
for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'):
72
lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en'
73
goodness = 1 + (lang in xdg.Locale.langs)
74
if goodness > self._comment[0]:
75
self._comment = (goodness, _get_node_data(comment))
76
if goodness == 2: return
78
# FIXME: add get_icon method
79
def get_comment(self):
80
"""Returns comment for current language, loading it if needed."""
81
# Should we ever reload?
82
if self._comment is None:
83
self._comment = (0, str(self))
85
return self._comment[1]
88
return self.media + '/' + self.subtype
91
return '[%s: %s]' % (self, self._comment or '(comment not loaded)')
94
def __init__(self, f):
116
self.start=int(start)
120
self.lenvalue=ord(lb)+(ord(hb)<<8)
122
self.value=f.read(self.lenvalue)
126
self.mask=f.read(self.lenvalue)
133
while c!='+' and c!='\n':
135
if c=='+' or c=='\n':
156
raise 'Malformed MIME magic line'
159
return self.start+self.lenvalue+self.range
161
def appendRule(self, rule):
162
if self.nest<rule.nest:
167
self.prev.appendRule(rule)
169
def match(self, buffer):
170
if self.match0(buffer):
172
return self.next.match(buffer)
175
def match0(self, buffer):
177
for o in range(self.range):
184
for i in range(self.lenvalue):
185
c=ord(buffer[s+i]) & ord(self.mask[i])
194
return '<MagicRule %d>%d=[%d]%s&%s~%d+%d>' % (self.nest,
203
def __init__(self, mtype):
208
def getLine(self, f):
211
if nrule.nest and self.last_rule:
212
self.last_rule.appendRule(nrule)
214
self.top_rules.append(nrule)
220
def match(self, buffer):
221
for rule in self.top_rules:
222
if rule.match(buffer):
226
return '<MagicType %s>' % self.mtype
230
self.types={} # Indexed by priority, each entry is a list of type rules
233
def mergeFile(self, fname):
236
if line!='MIME-Magic\0\n':
237
raise 'Not a MIME magic file'
244
if shead[0]!='[' or shead[-2:]!=']\n':
245
raise 'Malformed section heading'
246
pri, tname=shead[1:-2].split(':')
257
magictype=MagicType(mtype)
264
rule=magictype.getLine(f)
266
if rule and rule.getLength()>self.maxlen:
267
self.maxlen=rule.getLength()
272
ents.append(magictype)
273
#self.types[pri]=ents
277
def match_data(self, data, max_pri=100, min_pri=0):
278
pris=self.types.keys()
279
pris.sort(lambda a, b: -cmp(a, b))
281
#print pri, max_pri, min_pri
286
for type in self.types[pri]:
292
def match(self, path, max_pri=100, min_pri=0):
294
buf=file(path, 'r').read(self.maxlen)
295
return self.match_data(buf, max_pri, min_pri)
302
return '<MagicDB %s>' % self.types
305
# Some well-known types
306
text = lookup('text', 'plain')
307
inode_block = lookup('inode', 'blockdevice')
308
inode_char = lookup('inode', 'chardevice')
309
inode_dir = lookup('inode', 'directory')
310
inode_fifo = lookup('inode', 'fifo')
311
inode_socket = lookup('inode', 'socket')
312
inode_symlink = lookup('inode', 'symlink')
313
inode_door = lookup('inode', 'door')
314
app_exe = lookup('application', 'executable')
316
_cache_uptodate = False
318
def _cache_database():
319
global exts, globs, literals, magic, _cache_uptodate
321
_cache_uptodate = True
323
exts = {} # Maps extensions to types
324
globs = [] # List of (glob, type) pairs
325
literals = {} # Maps liternal names to types
328
def _import_glob_file(path):
329
"""Loads name matching information from a MIME directory."""
330
for line in file(path):
331
if line.startswith('#'): continue
334
type_name, pattern = line.split(':', 1)
335
mtype = lookup(type_name)
337
if pattern.startswith('*.'):
339
if not ('*' in rest or '[' in rest or '?' in rest):
342
if '*' in pattern or '[' in pattern or '?' in pattern:
343
globs.append((pattern, mtype))
345
literals[pattern] = mtype
347
for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'globs')):
348
_import_glob_file(path)
349
for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'magic')):
350
magic.mergeFile(path)
352
# Sort globs by length
353
globs.sort(lambda a, b: cmp(len(b[0]), len(a[0])))
355
def get_type_by_name(path):
356
"""Returns type of file by its name, or None if not known"""
357
if not _cache_uptodate:
360
leaf = os.path.basename(path)
362
return literals[leaf]
365
if lleaf in literals:
366
return literals[lleaf]
382
for (glob, mime_type) in globs:
383
if fnmatch.fnmatch(leaf, glob):
385
if fnmatch.fnmatch(lleaf, glob):
389
def get_type_by_contents(path, max_pri=100, min_pri=0):
390
"""Returns type of file by its contents, or None if not known"""
391
if not _cache_uptodate:
394
return magic.match(path, max_pri, min_pri)
396
def get_type_by_data(data, max_pri=100, min_pri=0):
397
"""Returns type of the data"""
398
if not _cache_uptodate:
401
return magic.match_data(data, max_pri, min_pri)
403
def get_type(path, follow=1, name_pri=100):
404
"""Returns type of file indicated by path.
405
path - pathname to check (need not exist)
406
follow - when reading file, follow symbolic links
407
name_pri - Priority to do name matches. 100=override magic"""
408
if not _cache_uptodate:
417
t = get_type_by_name(path)
420
if stat.S_ISREG(st.st_mode):
421
t = get_type_by_contents(path, min_pri=name_pri)
422
if not t: t = get_type_by_name(path)
423
if not t: t = get_type_by_contents(path, max_pri=name_pri)
425
if stat.S_IMODE(st.st_mode) & 0111:
430
elif stat.S_ISDIR(st.st_mode): return inode_dir
431
elif stat.S_ISCHR(st.st_mode): return inode_char
432
elif stat.S_ISBLK(st.st_mode): return inode_block
433
elif stat.S_ISFIFO(st.st_mode): return inode_fifo
434
elif stat.S_ISLNK(st.st_mode): return inode_symlink
435
elif stat.S_ISSOCK(st.st_mode): return inode_socket
438
def install_mime_info(application, package_file):
439
"""Copy 'package_file' as ~/.local/share/mime/packages/<application>.xml.
440
If package_file is None, install <app_dir>/<application>.xml.
441
If already installed, does nothing. May overwrite an existing
442
file with the same name (if the contents are different)"""
443
application += '.xml'
445
new_data = file(package_file).read()
447
# See if the file is already installed
448
package_dir = os.path.join('mime', 'packages')
449
resource = os.path.join(package_dir, application)
450
for x in xdg.BaseDirectory.load_data_paths(resource):
452
old_data = file(x).read()
455
if old_data == new_data:
456
return # Already installed
458
global _cache_uptodate
459
_cache_uptodate = False
461
# Not already installed; add a new copy
462
# Create the directory structure...
463
new_file = os.path.join(xdg.BaseDirectory.save_data_path(package_dir), application)
466
file(new_file, 'w').write(new_data)
468
# Update the database...
469
command = 'update-mime-database'
470
if os.spawnlp(os.P_WAIT, command, command, xdg.BaseDirectory.save_data_path('mime')):
472
raise Exception("The '%s' command returned an error code!\n" \
473
"Make sure you have the freedesktop.org shared MIME package:\n" \
474
"http://standards.freedesktop.org/shared-mime-info/") % command