Style improvements.
[rox-lib.git] / python / rox / mime.py
blob27902f55398833f1e4085d103b1e27a9127cc729
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