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
, xattr
22 from xml
.dom
import 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 base
=image_for_type(self
.media
+ '/' + self
.subtype
)
99 if not base
or not size
:
102 h
=int(base
.get_height()*float(size
)/base
.get_width())
103 return base
.scale_simple(size
, h
, rox
.g
.gdk
.INTERP_BILINEAR
)
105 def image_for_type(type, size
=48, flags
=0):
106 '''Search XDG_CONFIG or icon theme for a suitable icon. Returns a
108 from icon_theme
import users_theme
110 media
, subtype
= type.split('/', 1)
112 path
=basedir
.load_first_config('rox.sourceforge.net', 'MIME-icons',
113 media
+ '_' + subtype
+ '.png')
116 icon_name
= '%s-%s' % (media
, subtype
)
119 path
=users_theme
.lookup_icon(icon_name
, size
, flags
)
121 print "Error loading MIME icon"
124 icon_name
= 'mime-%s:%s' % (media
, subtype
)
127 path
=users_theme
.lookup_icon(icon_name
, size
, flags
)
129 icon_name
= 'mime-%s' % media
130 path
= users_theme
.lookup_icon(icon_name
, size
)
133 print "Error loading MIME icon"
136 path
= basedir
.load_first_config('rox.sourceforge.net',
137 'MIME-icons', media
+ '.png')
139 icon_name
= '%s-x-generic' % media
142 path
=users_theme
.lookup_icon(icon_name
, size
, flags
)
144 print "Error loading MIME icon"
147 if hasattr(rox
.g
.gdk
, 'pixbuf_new_from_file_at_size'):
148 return rox
.g
.gdk
.pixbuf_new_from_file_at_size(path
,
152 return rox
.g
.gdk
.pixbuf_new_from_file(path
)
157 def __init__(self
, f
):
179 self
.start
=int(start
)
183 self
.lenvalue
=ord(lb
)+(ord(hb
)<<8)
185 self
.value
=f
.read(self
.lenvalue
)
189 self
.mask
=f
.read(self
.lenvalue
)
196 while c
!='+' and c
!='\n':
198 if c
=='+' or c
=='\n':
219 raise 'Malformed MIME magic line'
222 return self
.start
+self
.lenvalue
+self
.range
224 def appendRule(self
, rule
):
225 if self
.nest
<rule
.nest
:
230 self
.prev
.appendRule(rule
)
232 def match(self
, buffer):
233 if self
.match0(buffer):
235 return self
.next
.match(buffer)
238 def match0(self
, buffer):
240 for o
in range(self
.range):
247 for i
in range(self
.lenvalue
):
248 c
=ord(buffer[s
+i
]) & ord(self
.mask
[i
])
257 return '<MagicRule %d>%d=[%d]%s&%s~%d+%d>' % (self
.nest
,
266 def __init__(self
, mtype
):
271 def getLine(self
, f
):
274 if nrule
.nest
and self
.last_rule
:
275 self
.last_rule
.appendRule(nrule
)
277 self
.top_rules
.append(nrule
)
283 def match(self
, buffer):
284 for rule
in self
.top_rules
:
285 if rule
.match(buffer):
289 return '<MagicType %s>' % self
.mtype
293 self
.types
={} # Indexed by priority, each entry is a list of type rules
296 def mergeFile(self
, fname
):
299 if line
!='MIME-Magic\0\n':
300 raise 'Not a MIME magic file'
307 if shead
[0]!='[' or shead
[-2:]!=']\n':
308 raise 'Malformed section heading'
309 pri
, tname
=shead
[1:-2].split(':')
320 magictype
=MagicType(mtype
)
327 rule
=magictype
.getLine(f
)
329 if rule
and rule
.getLength()>self
.maxlen
:
330 self
.maxlen
=rule
.getLength()
335 ents
.append(magictype
)
336 #self.types[pri]=ents
340 def match(self
, path
, max_pri
=100, min_pri
=0):
342 buf
=file(path
, 'r').read(self
.maxlen
)
343 pris
=self
.types
.keys()
344 pris
.sort(lambda a
, b
: -cmp(a
, b
))
346 #print pri, max_pri, min_pri
351 for type in self
.types
[pri
]:
361 return '<MagicDB %s>' % self
.types
364 # Some well-known types
365 text
= lookup('text', 'plain')
366 inode_block
= lookup('inode', 'blockdevice')
367 inode_char
= lookup('inode', 'chardevice')
368 inode_dir
= lookup('inode', 'directory')
369 inode_fifo
= lookup('inode', 'fifo')
370 inode_socket
= lookup('inode', 'socket')
371 inode_symlink
= lookup('inode', 'symlink')
372 inode_door
= lookup('inode', 'door')
373 app_exe
= lookup('application', 'executable')
375 _cache_uptodate
= False
377 def _cache_database():
378 global exts
, globs
, literals
, magic
, _cache_uptodate
380 _cache_uptodate
= True
382 exts
= {} # Maps extensions to types
383 globs
= [] # List of (glob, type) pairs
384 literals
= {} # Maps liternal names to types
387 def _import_glob_file(path
):
388 """Loads name matching information from a MIME directory."""
389 for line
in file(path
):
390 if line
.startswith('#'): continue
393 type_name
, pattern
= line
.split(':', 1)
394 mtype
= lookup(type_name
)
396 if pattern
.startswith('*.'):
398 if not ('*' in rest
or '[' in rest
or '?' in rest
):
401 if '*' in pattern
or '[' in pattern
or '?' in pattern
:
402 globs
.append((pattern
, mtype
))
404 literals
[pattern
] = mtype
406 for path
in basedir
.load_data_paths(os
.path
.join('mime', 'globs')):
407 _import_glob_file(path
)
408 for path
in basedir
.load_data_paths(os
.path
.join('mime', 'magic')):
409 magic
.mergeFile(path
)
411 # Sort globs by length
412 globs
.sort(lambda a
, b
: cmp(len(b
[0]), len(a
[0])))
414 def get_type_by_name(path
):
415 """Returns type of file by its name, or None if not known"""
416 if not _cache_uptodate
:
419 leaf
= os
.path
.basename(path
)
421 return literals
[leaf
]
424 if lleaf
in literals
:
425 return literals
[lleaf
]
441 for (glob
, mime_type
) in globs
:
442 if fnmatch
.fnmatch(leaf
, glob
):
444 if fnmatch
.fnmatch(lleaf
, glob
):
448 def get_type_by_contents(path
, max_pri
=100, min_pri
=0):
449 """Returns type of file by its contents, or None if not known"""
450 if not _cache_uptodate
:
453 return magic
.match(path
, max_pri
, min_pri
)
455 def get_type(path
, follow
=1, name_pri
=100):
456 """Returns type of file indicated by path.
457 path - pathname to check (need not exist)
458 follow - when reading file, follow symbolic links
459 name_pri - Priority to do name matches. 100=override magic"""
460 if not _cache_uptodate
:
469 t
= get_type_by_name(path
)
473 if xattr
.present(path
):
474 name
= xattr
.get(path
, xattr
.USER_MIME_TYPE
)
475 if name
and '/' in name
:
476 media
, subtype
=name
.split('/')
477 return lookup(media
, subtype
)
481 if stat
.S_ISREG(st
.st_mode
):
482 t
= get_type_by_contents(path
, min_pri
=name_pri
)
483 if not t
: t
= get_type_by_name(path
)
484 if not t
: t
= get_type_by_contents(path
, max_pri
=name_pri
)
486 if stat
.S_IMODE(st
.st_mode
) & 0111:
491 elif stat
.S_ISDIR(st
.st_mode
): return inode_dir
492 elif stat
.S_ISCHR(st
.st_mode
): return inode_char
493 elif stat
.S_ISBLK(st
.st_mode
): return inode_block
494 elif stat
.S_ISFIFO(st
.st_mode
): return inode_fifo
495 elif stat
.S_ISLNK(st
.st_mode
): return inode_symlink
496 elif stat
.S_ISSOCK(st
.st_mode
): return inode_socket
499 def install_mime_info(application
, package_file
= None):
500 """Copy 'package_file' as ~/.local/share/mime/packages/<application>.xml.
501 If package_file is None, install <app_dir>/<application>.xml.
502 If already installed, does nothing. May overwrite an existing
503 file with the same name (if the contents are different)"""
504 application
+= '.xml'
506 package_file
= os
.path
.join(rox
.app_dir
, application
)
508 new_data
= file(package_file
).read()
510 # See if the file is already installed
512 package_dir
= os
.path
.join('mime', 'packages')
513 resource
= os
.path
.join(package_dir
, application
)
514 for x
in basedir
.load_data_paths(resource
):
516 old_data
= file(x
).read()
519 if old_data
== new_data
:
520 return # Already installed
522 global _cache_uptodate
523 _cache_uptodate
= False
525 # Not already installed; add a new copy
527 # Create the directory structure...
528 new_file
= os
.path
.join(basedir
.save_data_path(package_dir
), application
)
531 file(new_file
, 'w').write(new_data
)
533 # Update the database...
534 if os
.path
.isdir('/uri/0install/zero-install.sourceforge.net'):
535 command
= '/uri/0install/zero-install.sourceforge.net/bin/update-mime-database'
537 command
= 'update-mime-database'
538 if os
.spawnlp(os
.P_WAIT
, command
, command
, basedir
.save_data_path('mime')):
540 raise Exception(_("The '%s' command returned an error code!\n" \
541 "Make sure you have the freedesktop.org shared MIME package:\n" \
542 "http://www.freedesktop.org/standards/shared-mime-info.html") % command
)
544 rox
.report_exception()
546 def get_type_handler(mime_type
, handler_type
= 'MIME-types'):
547 """Lookup the ROX-defined run action for a given mime type.
548 mime_type is an object returned by lookup().
549 handler_type is a config directory leaf (e.g.'MIME-types')."""
550 handler
= basedir
.load_first_config('rox.sourceforge.net', handler_type
,
551 mime_type
.media
+ '_' + mime_type
.subtype
)
553 # Fall back to the base handler if no subtype handler exists
554 handler
= basedir
.load_first_config('rox.sourceforge.net', handler_type
,
559 """Print results for name. Test routine"""
560 t
=get_type(name
, name_pri
=80)
561 print name
, t
, t
.get_comment()
563 if __name__
=='__main__':
568 for f
in sys
.argv
[1:]: