1 """This module provides access to the shared MIME database.
3 types is a dictionary of all known MIME types, indexed by the type name, e.g.
4 types['application/x-python']
6 Applications can install information about MIME types by storing an
7 XML file as <MIME>/packages/<application>.xml and running the
8 update-mime-database command, which is provided by the freedesktop.org
9 shared mime database package.
11 See http://www.freedesktop.org/standards/shared-mime-info-spec/ for
12 information about the format of these files."""
20 from rox
import i18n
, _
, basedir
22 from xml
.dom
import Node
, minidom
, XML_NAMESPACE
24 FREE_NS
= 'http://www.freedesktop.org/standards/shared-mime-info'
26 types
= {} # Maps MIME names to type objects
28 # Icon sizes when requesting MIME type icon
32 ICON_SIZE_UNSCALED
=None
34 exts
= None # Maps extensions to types
35 globs
= None # List of (glob, type) pairs
36 literals
= None # Maps liternal names to types
39 def _get_node_data(node
):
40 """Get text of XML node"""
41 return ''.join([n
.nodeValue
for n
in node
.childNodes
]).strip()
43 def lookup(media
, subtype
= None):
44 "Get the MIMEtype object for this type, creating a new one if needed."
45 if subtype
is None and '/' in media
:
46 media
, subtype
= media
.split('/', 1)
47 if (media
, subtype
) not in types
:
48 types
[(media
, subtype
)] = MIMEtype(media
, subtype
)
49 return types
[(media
, subtype
)]
52 """Type holding data about a MIME type"""
53 def __init__(self
, media
, subtype
):
54 "Don't use this constructor directly; use mime.lookup() instead."
55 assert media
and '/' not in media
56 assert subtype
and '/' not in subtype
57 assert (media
, subtype
) not in types
60 self
.subtype
= subtype
64 "Loads comment for current language. Use get_comment() instead."
65 resource
= os
.path
.join('mime', self
.media
, self
.subtype
+ '.xml')
66 for path
in basedir
.load_data_paths(resource
):
67 doc
= minidom
.parse(path
)
70 for comment
in doc
.documentElement
.getElementsByTagNameNS(FREE_NS
, 'comment'):
71 lang
= comment
.getAttributeNS(XML_NAMESPACE
, 'lang') or 'en'
72 goodness
= 1 + (lang
in i18n
.langs
)
73 if goodness
> self
._comment
[0]:
74 self
._comment
= (goodness
, _get_node_data(comment
))
75 if goodness
== 2: return
77 def get_comment(self
):
78 """Returns comment for current language, loading it if needed."""
79 # Should we ever reload?
80 if self
._comment
is None:
81 self
._comment
= (0, str(self
))
83 return self
._comment
[1]
86 return self
.media
+ '/' + self
.subtype
89 return '[%s: %s]' % (self
, self
._comment
or '(comment not loaded)')
91 def get_icon(self
, size
=None):
92 """Return a GdkPixbuf with the icon for this type. If size
93 is None then the image is returned at its natural size,
94 otherwise the image is scaled to that width with the height
95 at the correct aspect ratio. The constants
96 ICON_SIZE_{HUGE,LARGE,SMALL} match the sizes used by the
98 # I suppose it would make more sense to move the code
99 # from saving to here...
101 base
=saving
.image_for_type(self
.media
+ '/' + self
.subtype
)
102 if not base
or not size
:
105 h
=int(base
.get_width()*float(size
)/base
.get_height())
106 return base
.scale_simple(size
, h
, rox
.g
.gdk
.INTERP_BILINEAR
)
109 def __init__(self
, f
):
131 self
.start
=int(start
)
135 self
.lenvalue
=ord(lb
)+(ord(hb
)<<8)
137 self
.value
=f
.read(self
.lenvalue
)
141 self
.mask
=f
.read(self
.lenvalue
)
148 while c
!='+' and c
!='\n':
150 if c
=='+' or c
=='\n':
171 raise 'Malformed MIME magic line'
174 return self
.start
+self
.lenvalue
+self
.range
176 def appendRule(self
, rule
):
177 if self
.nest
<rule
.nest
:
182 self
.prev
.appendRule(rule
)
184 def match(self
, buffer):
185 if self
.match0(buffer):
187 return self
.next
.match(buffer)
190 def match0(self
, buffer):
192 for o
in range(self
.range):
199 for i
in range(self
.lenvalue
):
200 c
=ord(buffer[s
+i
]) & ord(self
.mask
[i
])
209 return '<MagicRule %d>%d=[%d]%s&%s~%d+%d>' % (self
.nest
,
218 def __init__(self
, mtype
):
223 def getLine(self
, f
):
226 if nrule
.nest
and self
.last_rule
:
227 self
.last_rule
.appendRule(nrule
)
229 self
.top_rules
.append(nrule
)
235 def match(self
, buffer):
236 for rule
in self
.top_rules
:
237 if rule
.match(buffer):
241 return '<MagicType %s>' % self
.mtype
245 self
.types
={} # Indexed by priority, each entry is a list of type rules
248 def mergeFile(self
, fname
):
251 if line
!='MIME-Magic\0\n':
252 raise 'Not a MIME magic file'
259 if shead
[0]!='[' or shead
[-2:]!=']\n':
260 raise 'Malformed section heading'
261 pri
, tname
=shead
[1:-2].split(':')
272 magictype
=MagicType(mtype
)
279 rule
=magictype
.getLine(f
)
281 if rule
and rule
.getLength()>self
.maxlen
:
282 self
.maxlen
=rule
.getLength()
287 ents
.append(magictype
)
288 #self.types[pri]=ents
292 def match(self
, path
, max_pri
=100, min_pri
=0):
294 buf
=file(path
, 'r').read(self
.maxlen
)
295 pris
=self
.types
.keys()
296 pris
.sort(lambda a
, b
: -cmp(a
, b
))
298 #print pri, max_pri, min_pri
303 for type in self
.types
[pri
]:
313 return '<MagicDB %s>' % self
.types
316 # Some well-known types
317 text
= lookup('text', 'plain')
318 inode_block
= lookup('inode', 'blockdevice')
319 inode_char
= lookup('inode', 'chardevice')
320 inode_dir
= lookup('inode', 'directory')
321 inode_fifo
= lookup('inode', 'fifo')
322 inode_socket
= lookup('inode', 'socket')
323 inode_symlink
= lookup('inode', 'symlink')
324 inode_door
= lookup('inode', 'door')
325 app_exe
= lookup('application', 'executable')
327 _cache_uptodate
= False
329 def _cache_database():
330 global exts
, globs
, literals
, magic
, _cache_uptodate
332 _cache_uptodate
= True
334 exts
= {} # Maps extensions to types
335 globs
= [] # List of (glob, type) pairs
336 literals
= {} # Maps liternal names to types
339 def _import_glob_file(path
):
340 """Loads name matching information from a MIME directory."""
341 for line
in file(path
):
342 if line
.startswith('#'): continue
345 type_name
, pattern
= line
.split(':', 1)
346 mtype
= lookup(type_name
)
348 if pattern
.startswith('*.'):
350 if not ('*' in rest
or '[' in rest
or '?' in rest
):
353 if '*' in pattern
or '[' in pattern
or '?' in pattern
:
354 globs
.append((pattern
, mtype
))
356 literals
[pattern
] = mtype
358 for path
in basedir
.load_data_paths(os
.path
.join('mime', 'globs')):
359 _import_glob_file(path
)
360 for path
in basedir
.load_data_paths(os
.path
.join('mime', 'magic')):
361 magic
.mergeFile(path
)
363 # Sort globs by length
364 globs
.sort(lambda a
, b
: cmp(len(b
[0]), len(a
[0])))
366 def get_type_by_name(path
):
367 """Returns type of file by its name, or None if not known"""
368 if not _cache_uptodate
:
371 leaf
= os
.path
.basename(path
)
373 return literals
[leaf
]
376 if lleaf
in literals
:
377 return literals
[lleaf
]
393 for (glob
, mime_type
) in globs
:
394 if fnmatch
.fnmatch(leaf
, glob
):
396 if fnmatch
.fnmatch(lleaf
, glob
):
400 def get_type_by_contents(path
, max_pri
=100, min_pri
=0):
401 """Returns type of file by its contents, or None if not known"""
402 if not _cache_uptodate
:
405 return magic
.match(path
, max_pri
, min_pri
)
407 def get_type(path
, follow
=1, name_pri
=100):
408 """Returns type of file indicated by path.
409 path - pathname to check (need not exist)
410 follow - when reading file, follow symbolic links
411 name_pri - Priority to do name matches. 100=override magic"""
412 if not _cache_uptodate
:
421 t
= get_type_by_name(path
)
424 if stat
.S_ISREG(st
.st_mode
):
425 t
= get_type_by_contents(path
, min_pri
=name_pri
)
426 if not t
: t
= get_type_by_name(path
)
427 if not t
: t
= get_type_by_contents(path
, max_pri
=name_pri
)
429 if stat
.S_IMODE(st
.st_mode
) & 0111:
434 elif stat
.S_ISDIR(st
.st_mode
): return inode_dir
435 elif stat
.S_ISCHR(st
.st_mode
): return inode_char
436 elif stat
.S_ISBLK(st
.st_mode
): return inode_block
437 elif stat
.S_ISFIFO(st
.st_mode
): return inode_fifo
438 elif stat
.S_ISLNK(st
.st_mode
): return inode_symlink
439 elif stat
.S_ISSOCK(st
.st_mode
): return inode_socket
442 def install_mime_info(application
, package_file
= None):
443 """Copy 'package_file' as ~/.local/share/mime/packages/<application>.xml.
444 If package_file is None, install <app_dir>/<application>.xml.
445 If already installed, does nothing. May overwrite an existing
446 file with the same name (if the contents are different)"""
447 application
+= '.xml'
449 package_file
= os
.path
.join(rox
.app_dir
, application
)
451 new_data
= file(package_file
).read()
453 # See if the file is already installed
455 package_dir
= os
.path
.join('mime', 'packages')
456 resource
= os
.path
.join(package_dir
, application
)
457 for x
in basedir
.load_data_paths(resource
):
459 old_data
= file(x
).read()
462 if old_data
== new_data
:
463 return # Already installed
465 global _cache_uptodate
466 _cache_uptodate
= False
468 # Not already installed; add a new copy
470 # Create the directory structure...
471 new_file
= os
.path
.join(basedir
.save_data_path(package_dir
), application
)
474 file(new_file
, 'w').write(new_data
)
476 # Update the database...
477 if os
.path
.isdir('/uri/0install/zero-install.sourceforge.net'):
478 command
= '/uri/0install/zero-install.sourceforge.net/bin/update-mime-database'
480 command
= 'update-mime-database'
481 if os
.spawnlp(os
.P_WAIT
, command
, command
, basedir
.save_data_path('mime')):
483 raise Exception(_("The '%s' command returned an error code!\n" \
484 "Make sure you have the freedesktop.org shared MIME package:\n" \
485 "http://www.freedesktop.org/standards/shared-mime-info.html") % command
)
487 rox
.report_exception()
489 def get_type_handler(mime_type
, handler_type
= 'MIME-types'):
490 """Lookup the ROX-defined run action for a given mime type.
491 mime_type is an object returned by lookup().
492 handler_type is a config directory leaf (e.g.'MIME-types')."""
493 handler
= basedir
.load_first_config('rox.sourceforge.net', handler_type
,
494 mime_type
.media
+ '_' + mime_type
.subtype
)
496 # Fall back to the base handler if no subtype handler exists
497 handler
= basedir
.load_first_config('rox.sourceforge.net', handler_type
,
502 """Print results for name. Test routine"""
503 t
=get_type(name
, name_pri
=80)
504 print name
, t
, t
.get_comment()
506 if __name__
=='__main__':
511 for f
in sys
.argv
[1:]: