Several incompatible changes to the experimental proxy API to make it simpler
[rox-lib.git] / python / rox / mime.py
blobd6b332ebf33a330e3b9249c1b06e68f3e27225de
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
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
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
38 def _get_node_data(node):
39 """Get text of XML node"""
40 return ''.join([n.nodeValue for n in node.childNodes]).strip()
42 def lookup(media, subtype = None):
43 "Get the MIMEtype object for this type, creating a new one if needed."
44 if subtype is None and '/' in media:
45 media, subtype = media.split('/', 1)
46 if (media, subtype) not in types:
47 types[(media, subtype)] = MIMEtype(media, subtype)
48 return types[(media, subtype)]
50 class MIMEtype:
51 """Type holding data about a MIME type"""
52 def __init__(self, media, subtype):
53 "Don't use this constructor directly; use mime.lookup() instead."
54 assert media and '/' not in media
55 assert subtype and '/' not in subtype
56 assert (media, subtype) not in types
58 self.media = media
59 self.subtype = subtype
60 self._comment = None
62 def _load(self):
63 "Loads comment for current language. Use get_comment() instead."
64 resource = os.path.join('mime', self.media, self.subtype + '.xml')
65 for path in basedir.load_data_paths(resource):
66 doc = minidom.parse(path)
67 if doc is None:
68 continue
69 for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'):
70 lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en'
71 goodness = 1 + (lang in i18n.langs)
72 if goodness > self._comment[0]:
73 self._comment = (goodness, _get_node_data(comment))
74 if goodness == 2: return
76 def get_comment(self):
77 """Returns comment for current language, loading it if needed."""
78 # Should we ever reload?
79 if self._comment is None:
80 self._comment = (0, str(self))
81 self._load()
82 return self._comment[1]
84 def __str__(self):
85 return self.media + '/' + self.subtype
87 def __repr__(self):
88 return '[%s: %s]' % (self, self._comment or '(comment not loaded)')
90 def get_icon(self, size=None):
91 """Return a GdkPixbuf with the icon for this type. If size
92 is None then the image is returned at its natural size,
93 otherwise the image is scaled to that width with the height
94 at the correct aspect ratio. The constants
95 ICON_SIZE_{HUGE,LARGE,SMALL} match the sizes used by the
96 filer."""
97 # I suppose it would make more sense to move the code
98 # from saving to here...
99 import saving
100 base=saving.image_for_type(self.media + '/' + self.subtype)
101 if not base or not size:
102 return base
104 h=int(base.get_width()*float(size)/base.get_height())
105 return base.scale_simple(size, h, rox.g.gdk.INTERP_BILINEAR)
107 # Some well-known types
108 text = lookup('text', 'plain')
109 inode_block = lookup('inode', 'blockdevice')
110 inode_char = lookup('inode', 'chardevice')
111 inode_dir = lookup('inode', 'directory')
112 inode_fifo = lookup('inode', 'fifo')
113 inode_socket = lookup('inode', 'socket')
114 inode_symlink = lookup('inode', 'symlink')
115 inode_door = lookup('inode', 'door')
116 app_exe = lookup('application', 'executable')
118 _cache_uptodate = False
120 def _cache_database():
121 global exts, globs, literals, _cache_uptodate
123 _cache_uptodate = True
125 exts = {} # Maps extensions to types
126 globs = [] # List of (glob, type) pairs
127 literals = {} # Maps liternal names to types
129 def _import_glob_file(path):
130 """Loads name matching information from a MIME directory."""
131 for line in file(path):
132 if line.startswith('#'): continue
133 line = line[:-1]
135 type_name, pattern = line.split(':', 1)
136 mtype = lookup(type_name)
138 if pattern.startswith('*.'):
139 rest = pattern[2:]
140 if not ('*' in rest or '[' in rest or '?' in rest):
141 exts[rest] = mtype
142 continue
143 if '*' in pattern or '[' in pattern or '?' in pattern:
144 globs.append((pattern, mtype))
145 else:
146 literals[pattern] = mtype
148 for path in basedir.load_data_paths(os.path.join('mime', 'globs')):
149 _import_glob_file(path)
151 # Sort globs by length
152 globs.sort(lambda a, b: cmp(len(b[0]), len(a[0])))
154 def get_type_by_name(path):
155 """Returns type of file by its name, or None if not known"""
156 if not _cache_uptodate:
157 _cache_database()
159 leaf = os.path.basename(path)
160 if leaf in literals:
161 return literals[leaf]
163 lleaf = leaf.lower()
164 if lleaf in literals:
165 return literals[lleaf]
167 ext = leaf
168 while 1:
169 p = ext.find('.')
170 if p < 0: break
171 ext = ext[p + 1:]
172 if ext in exts:
173 return exts[ext]
174 ext = lleaf
175 while 1:
176 p = ext.find('.')
177 if p < 0: break
178 ext = ext[p+1:]
179 if ext in exts:
180 return exts[ext]
181 for (glob, mime_type) in globs:
182 if fnmatch.fnmatch(leaf, glob):
183 return mime_type
184 if fnmatch.fnmatch(lleaf, glob):
185 return mime_type
186 return None
188 def get_type(path, follow=1, name_pri=100):
189 """Returns type of file indicated by path.
190 path - pathname to check (need not exist)
191 follow - when reading file, follow symbolic links
192 name_pri - Priority to do name matches. 100=override magic"""
193 if not _cache_uptodate:
194 _cache_database()
195 # name_pri is not implemented
196 try:
197 if follow:
198 st = os.stat(path)
199 else:
200 st = os.lstat(path)
201 except:
202 t = get_type_by_name(path)
203 return t or text
205 if stat.S_ISREG(st.st_mode):
206 t = get_type_by_name(path)
207 if t is None:
208 if stat.S_IMODE(st.st_mode) & 0111:
209 return app_exe
210 else:
211 return text
212 return t
213 elif stat.S_ISDIR(st.st_mode): return inode_dir
214 elif stat.S_ISCHR(st.st_mode): return inode_char
215 elif stat.S_ISBLK(st.st_mode): return inode_block
216 elif stat.S_ISFIFO(st.st_mode): return inode_fifo
217 elif stat.S_ISLNK(st.st_mode): return inode_symlink
218 elif stat.S_ISSOCK(st.st_mode): return inode_socket
219 return inode_door
221 def install_mime_info(application, package_file = None):
222 """Copy 'package_file' as ~/.local/share/mime/packages/<application>.xml.
223 If package_file is None, install <app_dir>/<application>.xml.
224 If already installed, does nothing. May overwrite an existing
225 file with the same name (if the contents are different)"""
226 application += '.xml'
227 if not package_file:
228 package_file = os.path.join(rox.app_dir, application)
230 new_data = file(package_file).read()
232 # See if the file is already installed
234 package_dir = os.path.join('mime', 'packages')
235 resource = os.path.join(package_dir, application)
236 for x in basedir.load_data_paths(resource):
237 try:
238 old_data = file(x).read()
239 except:
240 continue
241 if old_data == new_data:
242 return # Already installed
244 global _cache_uptodate
245 _cache_uptodate = False
247 # Not already installed; add a new copy
248 try:
249 # Create the directory structure...
250 new_file = os.path.join(basedir.save_data_path(package_dir), application)
252 # Write the file...
253 file(new_file, 'w').write(new_data)
255 # Update the database...
256 if os.path.isdir('/uri/0install/zero-install.sourceforge.net'):
257 command = '/uri/0install/zero-install.sourceforge.net/bin/update-mime-database'
258 else:
259 command = 'update-mime-database'
260 if os.spawnlp(os.P_WAIT, command, command, basedir.save_data_path('mime')):
261 os.unlink(new_file)
262 raise Exception(_("The '%s' command returned an error code!\n" \
263 "Make sure you have the freedesktop.org shared MIME package:\n" \
264 "http://www.freedesktop.org/standards/shared-mime-info.html") % command)
265 except:
266 rox.report_exception()
268 def _test(name):
269 """Print results for name. Test routine"""
270 t=get_type(name)
271 print name, t, t.get_comment()
273 if __name__=='__main__':
274 import sys
275 if len(sys.argv)<2:
276 _test('file.txt')
277 else:
278 for f in sys.argv[1:]:
279 _test(f)
280 print globs