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