glob patterns are now sorted
[rox-lib/lack.git] / python / rox / mime.py
blob6be8875d8d3be548d058cb3ce473e6daedad2c48
1 # Access to shared MIME database
2 # $Id$
4 """This module provides access to the shared MIME database.
6 types is a dictionary of all known MIME types, indexed by the type name, e.g.
7 types['application/x-python']
9 Applications can install information about MIME types by storing an
10 XML file as <MIME>/packages/<application>.xml and running the
11 update-mime-database command, which is provided by the freedesktop.org
12 shared mime database package.
14 See http://www.freedesktop.org/standards/shared-mime-info-spec/ for
15 information about the format of these files."""
17 import os
18 import stat
19 import fnmatch
21 import rox
22 from rox import i18n
24 from xml.dom import Node, minidom, XML_NAMESPACE
26 FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info'
28 exts = {} # Maps extensions to types
29 globs = [] # List of (glob, type) pairs
30 literals = {} # Maps liternal names to types
32 types = {} # Maps MIME names to type objects
34 _home = os.environ.get('HOME', '/')
35 _xdg_data_home = os.environ.get('XDG_DATA_HOME',
36 os.path.join(_home, '.local', 'share'))
38 _xdg_data_dirs = os.environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share')
40 mimedirs = []
42 _user_install = os.path.join(_xdg_data_home, 'mime')
43 if os.access(_user_install, os.R_OK):
44 mimedirs.append(_user_install)
45 else:
46 # See if we have the old directory
47 _old_user_install = os.path.join(_home, '.mime')
48 if os.access(_old_user_install, os.R_OK):
49 mimedirs.append(_old_user_install)
50 rox.info(_("WARNING: %s not found for shared MIME database version %s, "
51 "using %s for version %s") %
52 (_user_install, '0.11', _old_user_install, '0.10'))
53 else:
54 # Neither old nor new. Assume new for installing files
55 mimedirs.append(_user_install)
57 for _dir in _xdg_data_dirs.split(':'):
58 mimedirs.append(os.path.join(_dir, 'mime'))
60 def _get_node_data(node):
61 """Get text of XML node"""
62 return ''.join([n.nodeValue for n in node.childNodes]).strip()
64 def lookup(media, subtype = None):
65 "Get the MIMEtype object for this type, creating a new one if needed."
66 if subtype is None and '/' in media:
67 media, subtype = media.split('/', 1)
68 if (media, subtype) not in types:
69 types[(media, subtype)] = MIMEtype(media, subtype)
70 return types[(media, subtype)]
72 class MIMEtype:
73 """Type holding data about a MIME type"""
74 def __init__(self, media, subtype):
75 "Don't use this constructor directly; use mime.lookup() instead."
76 assert media and '/' not in media
77 assert subtype and '/' not in subtype
78 assert (media, subtype) not in types
80 self.media = media
81 self.subtype = subtype
82 self.comment = None
84 def _load(self):
85 "Loads comment for current language. Use get_comment() instead."
86 for dir in mimedirs:
87 path = os.path.join(dir, self.media, self.subtype + '.xml')
88 if not os.path.exists(path):
89 continue
91 doc = minidom.parse(path)
92 if doc is None:
93 continue
94 for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'):
95 lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en'
96 goodness = 1 + (lang in i18n.langs)
97 if goodness > self.comment[0]:
98 self.comment = (goodness, _get_node_data(comment))
99 if goodness == 2: return
101 def get_comment(self):
102 """Returns comment for current language, loading it if needed."""
103 # Should we ever reload?
104 if self.comment is None:
105 self.comment = (0, str(self))
106 self._load()
107 return self.comment[1]
109 def __str__(self):
110 return self.media + '/' + self.subtype
112 def __repr__(self):
113 return '[%s: %s]' % (self, self.comment or '(comment not loaded)')
115 # Some well-known types
116 text = lookup('text', 'plain')
117 inode_block = lookup('inode', 'blockdevice')
118 inode_char = lookup('inode', 'chardevice')
119 inode_dir = lookup('inode', 'directory')
120 inode_fifo = lookup('inode', 'fifo')
121 inode_socket = lookup('inode', 'socket')
122 inode_symlink = lookup('inode', 'symlink')
123 inode_door = lookup('inode', 'door')
124 app_exe = lookup('application', 'executable')
126 def _import_glob_file(dir):
127 """Loads name matching information from a MIME directory."""
128 path = os.path.join(dir, 'globs')
129 if not os.path.exists(path):
130 return
132 for line in file(path):
133 if line.startswith('#'): continue
134 line = line[:-1]
136 type, pattern = line.split(':', 1)
137 mtype = lookup(type)
139 if pattern.startswith('*.'):
140 rest = pattern[2:]
141 if not ('*' in rest or '[' in rest or '?' in rest):
142 exts[rest] = mtype
143 continue
144 if '*' in pattern or '[' in pattern or '?' in pattern:
145 globs.append((pattern, mtype))
146 else:
147 literals[pattern] = mtype
149 for dir in mimedirs:
150 _import_glob_file(dir)
152 def longest_first(a, b):
153 stra=a[0]
154 strb=b[0]
155 if len(stra)>len(strb):
156 return -1
157 elif len(stra)<len(strb):
158 return 1
159 return 0
161 # Sort globs by length
162 globs.sort(longest_first)
164 def get_type_by_name(path):
165 """Returns type of file by its name, or None if not known"""
166 leaf = os.path.basename(path)
167 if leaf in literals:
168 return literals[leaf]
170 lleaf = leaf.lower()
171 if lleaf in literals:
172 return literals[lleaf]
174 ext = leaf
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 ext = lleaf
182 while 1:
183 p = ext.find('.')
184 if p < 0: break
185 ext = ext[p+1:]
186 if ext in exts:
187 return exts[ext]
188 for (glob, type) in globs:
189 if fnmatch.fnmatch(leaf, glob):
190 return type
191 if fnmatch.fnmatch(lleaf, glob):
192 return type
193 return None
195 def get_type(path, follow=1, name_pri=100):
196 """Returns type of file indicated by path.
197 path - pathname to check (need not exist)
198 follow - when reading file, follow symbolic links
199 name_pri - Priority to do name matches. 100=override magic"""
200 # name_pri is not implemented
201 try:
202 if follow:
203 st = os.stat(path)
204 else:
205 st = os.lstat(path)
206 except:
207 t = get_type_by_name(path)
208 return t or text
210 if stat.S_ISREG(st.st_mode):
211 t = get_type_by_name(path)
212 if t is None:
213 if stat.S_IMODE(st.st_mode) & 0111:
214 return app_exe
215 else:
216 return text
217 return t
218 elif stat.S_ISDIR(st.st_mode): return inode_dir
219 elif stat.S_ISCHR(st.st_mode): return inode_char
220 elif stat.S_ISBLK(st.st_mode): return inode_block
221 elif stat.S_ISFIFO(st.st_mode): return inode_fifo
222 elif stat.S_ISLNK(st.st_mode): return inode_symlink
223 elif stat.S_ISSOCK(st.st_mode): return inode_socket
224 return inode_door
226 def install_mime_info(application, package_file = None):
227 """Copy 'package_file' as ~/.local/share/mime/packages/<application>.xml.
228 If package_file is None, install <app_dir>/<application>.xml.
229 If already installed, does nothing. May overwrite an existing
230 file with the same name (if the contents are different)"""
231 application += '.xml'
232 if not package_file:
233 package_file = os.path.join(rox.app_dir, application)
235 new_data = file(package_file).read()
237 # See if the file is already installed
239 for x in mimedirs:
240 test = os.path.join(x, 'packages', application)
241 try:
242 old_data = file(test).read()
243 except:
244 continue
245 if old_data == new_data:
246 return # Already installed
248 # Not already installed; add a new copy
249 try:
250 # Create the directory structure...
252 packages = os.path.join(mimedirs[0], 'packages')
253 if not os.path.exists(packages): os.makedirs(packages)
255 # Write the file...
256 new_file = os.path.join(packages, application)
257 file(new_file, 'w').write(new_data)
259 # Update the database...
260 if os.spawnlp(os.P_WAIT, 'update-mime-database', 'update-mime-database', mimedirs[0]):
261 os.unlink(new_file)
262 raise Exception(_("The 'update-mime-database' 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"))
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