Changed 'Dismiss' to 'Close' (Chris Shaffer).
[rox-lib.git] / python / rox / mime.py
blob715367c4a0ec9bc252f2a1ba62854325a4764133
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 from rox import i18n, _
21 from xml.dom import Node, minidom, XML_NAMESPACE
23 FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info'
25 exts = {} # Maps extensions to types
26 globs = [] # List of (glob, type) pairs
27 literals = {} # Maps liternal names to types
29 types = {} # Maps MIME names to type objects
31 _home = os.environ.get('HOME', '/')
32 _xdg_data_home = os.environ.get('XDG_DATA_HOME',
33 os.path.join(_home, '.local', 'share'))
35 _xdg_data_dirs = os.environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share')
37 mimedirs = []
39 _user_install = os.path.join(_xdg_data_home, 'mime')
40 if os.access(_user_install, os.R_OK):
41 mimedirs.append(_user_install)
42 else:
43 # See if we have the old directory
44 _old_user_install = os.path.join(_home, '.mime')
45 if os.access(_old_user_install, os.R_OK):
46 mimedirs.append(_old_user_install)
47 rox.info(_("WARNING: %s not found for shared MIME database version %s, "
48 "using %s for version %s") %
49 (_user_install, '0.11', _old_user_install, '0.10'))
50 else:
51 # Neither old nor new. Assume new for installing files
52 mimedirs.append(_user_install)
54 for _dir in _xdg_data_dirs.split(':'):
55 mimedirs.append(os.path.join(_dir, 'mime'))
57 def _get_node_data(node):
58 """Get text of XML node"""
59 return ''.join([n.nodeValue for n in node.childNodes]).strip()
61 def lookup(media, subtype = None):
62 "Get the MIMEtype object for this type, creating a new one if needed."
63 if subtype is None and '/' in media:
64 media, subtype = media.split('/', 1)
65 if (media, subtype) not in types:
66 types[(media, subtype)] = MIMEtype(media, subtype)
67 return types[(media, subtype)]
69 class MIMEtype:
70 """Type holding data about a MIME type"""
71 def __init__(self, media, subtype):
72 "Don't use this constructor directly; use mime.lookup() instead."
73 assert media and '/' not in media
74 assert subtype and '/' not in subtype
75 assert (media, subtype) not in types
77 self.media = media
78 self.subtype = subtype
79 self.comment = None
81 def _load(self):
82 "Loads comment for current language. Use get_comment() instead."
83 for dir in mimedirs:
84 path = os.path.join(dir, self.media, self.subtype + '.xml')
85 if not os.path.exists(path):
86 continue
88 doc = minidom.parse(path)
89 if doc is None:
90 continue
91 for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'):
92 lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en'
93 goodness = 1 + (lang in i18n.langs)
94 if goodness > self.comment[0]:
95 self.comment = (goodness, _get_node_data(comment))
96 if goodness == 2: return
98 def get_comment(self):
99 """Returns comment for current language, loading it if needed."""
100 # Should we ever reload?
101 if self.comment is None:
102 self.comment = (0, str(self))
103 self._load()
104 return self.comment[1]
106 def __str__(self):
107 return self.media + '/' + self.subtype
109 def __repr__(self):
110 return '[%s: %s]' % (self, self.comment or '(comment not loaded)')
112 # Some well-known types
113 text = lookup('text', 'plain')
114 inode_block = lookup('inode', 'blockdevice')
115 inode_char = lookup('inode', 'chardevice')
116 inode_dir = lookup('inode', 'directory')
117 inode_fifo = lookup('inode', 'fifo')
118 inode_socket = lookup('inode', 'socket')
119 inode_symlink = lookup('inode', 'symlink')
120 inode_door = lookup('inode', 'door')
121 app_exe = lookup('application', 'executable')
123 def _import_glob_file(dir):
124 """Loads name matching information from a MIME directory."""
125 path = os.path.join(dir, 'globs')
126 if not os.path.exists(path):
127 return
129 for line in file(path):
130 if line.startswith('#'): continue
131 line = line[:-1]
133 type, pattern = line.split(':', 1)
134 mtype = lookup(type)
136 if pattern.startswith('*.'):
137 rest = pattern[2:]
138 if not ('*' in rest or '[' in rest or '?' in rest):
139 exts[rest] = mtype
140 continue
141 if '*' in pattern or '[' in pattern or '?' in pattern:
142 globs.append((pattern, mtype))
143 else:
144 literals[pattern] = mtype
146 for dir in mimedirs:
147 _import_glob_file(dir)
149 # Sort globs by length
150 globs.sort(lambda a, b: cmp(len(b[0]), len(a[0])))
152 def get_type_by_name(path):
153 """Returns type of file by its name, or None if not known"""
154 leaf = os.path.basename(path)
155 if leaf in literals:
156 return literals[leaf]
158 lleaf = leaf.lower()
159 if lleaf in literals:
160 return literals[lleaf]
162 ext = leaf
163 while 1:
164 p = ext.find('.')
165 if p < 0: break
166 ext = ext[p + 1:]
167 if ext in exts:
168 return exts[ext]
169 ext = lleaf
170 while 1:
171 p = ext.find('.')
172 if p < 0: break
173 ext = ext[p+1:]
174 if ext in exts:
175 return exts[ext]
176 for (glob, type) in globs:
177 if fnmatch.fnmatch(leaf, glob):
178 return type
179 if fnmatch.fnmatch(lleaf, glob):
180 return type
181 return None
183 def get_type(path, follow=1, name_pri=100):
184 """Returns type of file indicated by path.
185 path - pathname to check (need not exist)
186 follow - when reading file, follow symbolic links
187 name_pri - Priority to do name matches. 100=override magic"""
188 # name_pri is not implemented
189 try:
190 if follow:
191 st = os.stat(path)
192 else:
193 st = os.lstat(path)
194 except:
195 t = get_type_by_name(path)
196 return t or text
198 if stat.S_ISREG(st.st_mode):
199 t = get_type_by_name(path)
200 if t is None:
201 if stat.S_IMODE(st.st_mode) & 0111:
202 return app_exe
203 else:
204 return text
205 return t
206 elif stat.S_ISDIR(st.st_mode): return inode_dir
207 elif stat.S_ISCHR(st.st_mode): return inode_char
208 elif stat.S_ISBLK(st.st_mode): return inode_block
209 elif stat.S_ISFIFO(st.st_mode): return inode_fifo
210 elif stat.S_ISLNK(st.st_mode): return inode_symlink
211 elif stat.S_ISSOCK(st.st_mode): return inode_socket
212 return inode_door
214 def install_mime_info(application, package_file = None):
215 """Copy 'package_file' as ~/.local/share/mime/packages/<application>.xml.
216 If package_file is None, install <app_dir>/<application>.xml.
217 If already installed, does nothing. May overwrite an existing
218 file with the same name (if the contents are different)"""
219 application += '.xml'
220 if not package_file:
221 package_file = os.path.join(rox.app_dir, application)
223 new_data = file(package_file).read()
225 # See if the file is already installed
227 for x in mimedirs:
228 test = os.path.join(x, 'packages', application)
229 try:
230 old_data = file(test).read()
231 except:
232 continue
233 if old_data == new_data:
234 return # Already installed
236 # Not already installed; add a new copy
237 try:
238 # Create the directory structure...
240 packages = os.path.join(mimedirs[0], 'packages')
241 if not os.path.exists(packages): os.makedirs(packages)
243 # Write the file...
244 new_file = os.path.join(packages, application)
245 file(new_file, 'w').write(new_data)
247 # Update the database...
248 if os.spawnlp(os.P_WAIT, 'update-mime-database', 'update-mime-database', mimedirs[0]):
249 os.unlink(new_file)
250 raise Exception(_("The 'update-mime-database' command returned an error code!\n" \
251 "Make sure you have the freedesktop.org shared MIME package:\n" \
252 "http://www.freedesktop.org/standards/shared-mime-info.html"))
253 except:
254 rox.report_exception()
256 def test(name):
257 """Print results for name. Test routine"""
258 t=get_type(name)
259 print name, t, t.get_comment()
261 if __name__=='__main__':
262 import sys
263 if len(sys.argv)<2:
264 test('file.txt')
265 else:
266 for f in sys.argv[1:]:
267 test(f)
268 print globs