Handle thumbnail creation failure according to the standard.
[rox-lib.git] / ROX-Lib2 / python / rox / mime.py
blobe0260a174215c83ee4c8bb1f36f9287f2cb54ef4
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."""
14 import os
15 import stat
16 import fnmatch
18 import rox
19 import rox.choices
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
29 ICON_SIZE_HUGE=96
30 ICON_SIZE_LARGE=52
31 ICON_SIZE_SMALL=18
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
37 magic = None
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)]
51 class MIMEtype:
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
59 self.media = media
60 self.subtype = subtype
61 self._comment = None
63 def _load(self):
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)
68 if doc is None:
69 continue
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))
82 self._load()
83 return self._comment[1]
85 def __str__(self):
86 return self.media + '/' + self.subtype
88 def __repr__(self):
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
97 filer."""
98 base=image_for_type(self.media + '/' + self.subtype)
99 if not base or not size:
100 return base
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
107 pixbuf, or None.'''
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')
114 icon=None
115 if not path:
116 icon_name = '%s-%s' % (media, subtype)
118 try:
119 path=users_theme.lookup_icon(icon_name, size, flags)
120 except:
121 print "Error loading MIME icon"
123 if not path:
124 icon_name = 'mime-%s:%s' % (media, subtype)
126 try:
127 path=users_theme.lookup_icon(icon_name, size, flags)
128 if not path:
129 icon_name = 'mime-%s' % media
130 path = users_theme.lookup_icon(icon_name, size)
132 except:
133 print "Error loading MIME icon"
135 if not path:
136 path = basedir.load_first_config('rox.sourceforge.net',
137 'MIME-icons', media + '.png')
138 if not path:
139 icon_name = '%s-x-generic' % media
141 try:
142 path=users_theme.lookup_icon(icon_name, size, flags)
143 except:
144 print "Error loading MIME icon"
146 if path:
147 if hasattr(rox.g.gdk, 'pixbuf_new_from_file_at_size'):
148 return rox.g.gdk.pixbuf_new_from_file_at_size(path,
149 size,
150 size)
151 else:
152 return rox.g.gdk.pixbuf_new_from_file(path)
153 else:
154 return None
156 class MagicRule:
157 def __init__(self, f):
158 self.next=None
159 self.prev=None
161 #print line
162 ind=''
163 while True:
164 c=f.read(1)
165 if c=='>':
166 break
167 ind+=c
168 if not ind:
169 self.nest=0
170 else:
171 self.nest=int(ind)
173 start=''
174 while True:
175 c=f.read(1)
176 if c=='=':
177 break
178 start+=c
179 self.start=int(start)
181 hb=f.read(1)
182 lb=f.read(1)
183 self.lenvalue=ord(lb)+(ord(hb)<<8)
185 self.value=f.read(self.lenvalue)
187 c=f.read(1)
188 if c=='&':
189 self.mask=f.read(self.lenvalue)
190 c=f.read(1)
191 else:
192 self.mask=None
194 if c=='~':
195 w=''
196 while c!='+' and c!='\n':
197 c=f.read(1)
198 if c=='+' or c=='\n':
199 break
200 w+=c
202 self.word=int(w)
203 else:
204 self.word=1
206 if c=='+':
207 r=''
208 while c!='\n':
209 c=f.read(1)
210 if c=='\n':
211 break
212 r+=c
213 #print r
214 self.range=int(r)
215 else:
216 self.range=1
218 if c!='\n':
219 raise 'Malformed MIME magic line'
221 def getLength(self):
222 return self.start+self.lenvalue+self.range
224 def appendRule(self, rule):
225 if self.nest<rule.nest:
226 self.next=rule
227 rule.prev=self
229 elif self.prev:
230 self.prev.appendRule(rule)
232 def match(self, buffer):
233 if self.match0(buffer):
234 if self.next:
235 return self.next.match(buffer)
236 return True
238 def match0(self, buffer):
239 l=len(buffer)
240 for o in range(self.range):
241 s=self.start+o
242 e=s+self.lenvalue
243 if l<e:
244 return False
245 if self.mask:
246 test=''
247 for i in range(self.lenvalue):
248 c=ord(buffer[s+i]) & ord(self.mask[i])
249 test+=chr(c)
250 else:
251 test=buffer[s:e]
253 if test==self.value:
254 return True
256 def __repr__(self):
257 return '<MagicRule %d>%d=[%d]%s&%s~%d+%d>' % (self.nest,
258 self.start,
259 self.lenvalue,
260 `self.value`,
261 `self.mask`,
262 self.word,
263 self.range)
265 class MagicType:
266 def __init__(self, mtype):
267 self.mtype=mtype
268 self.top_rules=[]
269 self.last_rule=None
271 def getLine(self, f):
272 nrule=MagicRule(f)
274 if nrule.nest and self.last_rule:
275 self.last_rule.appendRule(nrule)
276 else:
277 self.top_rules.append(nrule)
279 self.last_rule=nrule
281 return nrule
283 def match(self, buffer):
284 for rule in self.top_rules:
285 if rule.match(buffer):
286 return self.mtype
288 def __repr__(self):
289 return '<MagicType %s>' % self.mtype
291 class MagicDB:
292 def __init__(self):
293 self.types={} # Indexed by priority, each entry is a list of type rules
294 self.maxlen=0
296 def mergeFile(self, fname):
297 f=file(fname, 'r')
298 line=f.readline()
299 if line!='MIME-Magic\0\n':
300 raise 'Not a MIME magic file'
302 while True:
303 shead=f.readline()
304 #print shead
305 if not shead:
306 break
307 if shead[0]!='[' or shead[-2:]!=']\n':
308 raise 'Malformed section heading'
309 pri, tname=shead[1:-2].split(':')
310 #print shead[1:-2]
311 pri=int(pri)
312 mtype=lookup(tname)
314 try:
315 ents=self.types[pri]
316 except:
317 ents=[]
318 self.types[pri]=ents
320 magictype=MagicType(mtype)
321 #print tname
323 #rline=f.readline()
324 c=f.read(1)
325 f.seek(-1, 1)
326 while c and c!='[':
327 rule=magictype.getLine(f)
328 #print rule
329 if rule and rule.getLength()>self.maxlen:
330 self.maxlen=rule.getLength()
332 c=f.read(1)
333 f.seek(-1, 1)
335 ents.append(magictype)
336 #self.types[pri]=ents
337 if not c:
338 break
340 def match(self, path, max_pri=100, min_pri=0):
341 try:
342 buf=file(path, 'r').read(self.maxlen)
343 pris=self.types.keys()
344 pris.sort(lambda a, b: -cmp(a, b))
345 for pri in pris:
346 #print pri, max_pri, min_pri
347 if pri>max_pri:
348 continue
349 if pri<min_pri:
350 break
351 for type in self.types[pri]:
352 m=type.match(buf)
353 if m:
354 return m
355 except:
356 pass
358 return None
360 def __repr__(self):
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
385 magic = MagicDB()
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
391 line = line[:-1]
393 type_name, pattern = line.split(':', 1)
394 mtype = lookup(type_name)
396 if pattern.startswith('*.'):
397 rest = pattern[2:]
398 if not ('*' in rest or '[' in rest or '?' in rest):
399 exts[rest] = mtype
400 continue
401 if '*' in pattern or '[' in pattern or '?' in pattern:
402 globs.append((pattern, mtype))
403 else:
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:
417 _cache_database()
419 leaf = os.path.basename(path)
420 if leaf in literals:
421 return literals[leaf]
423 lleaf = leaf.lower()
424 if lleaf in literals:
425 return literals[lleaf]
427 ext = leaf
428 while 1:
429 p = ext.find('.')
430 if p < 0: break
431 ext = ext[p + 1:]
432 if ext in exts:
433 return exts[ext]
434 ext = lleaf
435 while 1:
436 p = ext.find('.')
437 if p < 0: break
438 ext = ext[p+1:]
439 if ext in exts:
440 return exts[ext]
441 for (glob, mime_type) in globs:
442 if fnmatch.fnmatch(leaf, glob):
443 return mime_type
444 if fnmatch.fnmatch(lleaf, glob):
445 return mime_type
446 return None
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:
451 _cache_database()
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:
461 _cache_database()
463 try:
464 if follow:
465 st = os.stat(path)
466 else:
467 st = os.lstat(path)
468 except:
469 t = get_type_by_name(path)
470 return t or text
472 try:
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)
478 except:
479 pass
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)
485 if t is None:
486 if stat.S_IMODE(st.st_mode) & 0111:
487 return app_exe
488 else:
489 return text
490 return t
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
497 return inode_door
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'
505 if not package_file:
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):
515 try:
516 old_data = file(x).read()
517 except:
518 continue
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
526 try:
527 # Create the directory structure...
528 new_file = os.path.join(basedir.save_data_path(package_dir), application)
530 # Write the file...
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'
536 else:
537 command = 'update-mime-database'
538 if os.spawnlp(os.P_WAIT, command, command, basedir.save_data_path('mime')):
539 os.unlink(new_file)
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)
543 except:
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)
552 if not handler:
553 # Fall back to the base handler if no subtype handler exists
554 handler = basedir.load_first_config('rox.sourceforge.net', handler_type,
555 mime_type.media)
556 return handler
558 def _test(name):
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__':
564 import sys
565 if len(sys.argv)<2:
566 _test('file.txt')
567 else:
568 for f in sys.argv[1:]:
569 _test(f)
570 #print globs