~diegosarmentero/+junk/xdg

« back to all changes in this revision

Viewing changes to xdg/Mime.py

  • Committer: Manuel de la Pena
  • Date: 2011-03-16 10:26:03 UTC
  • Revision ID: mandel@themacaque.com-20110316102603-5zhgptihr1x39bd0
Added intial impl of xdg on windows. Is not yet complete but is enough atm

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""
 
2
This module is based on a rox module (LGPL):
 
3
 
 
4
http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log
 
5
 
 
6
This module provides access to the shared MIME database.
 
7
 
 
8
types is a dictionary of all known MIME types, indexed by the type name, e.g.
 
9
types['application/x-python']
 
10
 
 
11
Applications can install information about MIME types by storing an
 
12
XML file as <MIME>/packages/<application>.xml and running the
 
13
update-mime-database command, which is provided by the freedesktop.org
 
14
shared mime database package.
 
15
 
 
16
See http://www.freedesktop.org/standards/shared-mime-info-spec/ for
 
17
information about the format of these files.
 
18
 
 
19
(based on version 0.13)
 
20
"""
 
21
 
 
22
import os
 
23
import stat
 
24
import fnmatch
 
25
 
 
26
import xdg.BaseDirectory
 
27
import xdg.Locale
 
28
 
 
29
from xml.dom import Node, minidom, XML_NAMESPACE
 
30
 
 
31
FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info'
 
32
 
 
33
types = {}      # Maps MIME names to type objects
 
34
 
 
35
exts = None     # Maps extensions to types
 
36
globs = None    # List of (glob, type) pairs
 
37
literals = None # Maps liternal names to types
 
38
magic = None
 
39
 
 
40
def _get_node_data(node):
 
41
    """Get text of XML node"""
 
42
    return ''.join([n.nodeValue for n in node.childNodes]).strip()
 
43
 
 
44
def lookup(media, subtype = None):
 
45
    "Get the MIMEtype object for this type, creating a new one if needed."
 
46
    if subtype is None and '/' in media:
 
47
        media, subtype = media.split('/', 1)
 
48
    if (media, subtype) not in types:
 
49
        types[(media, subtype)] = MIMEtype(media, subtype)
 
50
    return types[(media, subtype)]
 
51
 
 
52
class MIMEtype:
 
53
    """Type holding data about a MIME type"""
 
54
    def __init__(self, media, subtype):
 
55
        "Don't use this constructor directly; use mime.lookup() instead."
 
56
        assert media and '/' not in media
 
57
        assert subtype and '/' not in subtype
 
58
        assert (media, subtype) not in types
 
59
 
 
60
        self.media = media
 
61
        self.subtype = subtype
 
62
        self._comment = None
 
63
 
 
64
    def _load(self):
 
65
        "Loads comment for current language. Use get_comment() instead."
 
66
        resource = os.path.join('mime', self.media, self.subtype + '.xml')
 
67
        for path in xdg.BaseDirectory.load_data_paths(resource):
 
68
            doc = minidom.parse(path)
 
69
            if doc is None:
 
70
                continue
 
71
            for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'):
 
72
                lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en'
 
73
                goodness = 1 + (lang in xdg.Locale.langs)
 
74
                if goodness > self._comment[0]:
 
75
                    self._comment = (goodness, _get_node_data(comment))
 
76
                if goodness == 2: return
 
77
 
 
78
    # FIXME: add get_icon method
 
79
    def get_comment(self):
 
80
        """Returns comment for current language, loading it if needed."""
 
81
        # Should we ever reload?
 
82
        if self._comment is None:
 
83
            self._comment = (0, str(self))
 
84
            self._load()
 
85
        return self._comment[1]
 
86
 
 
87
    def __str__(self):
 
88
        return self.media + '/' + self.subtype
 
89
 
 
90
    def __repr__(self):
 
91
        return '[%s: %s]' % (self, self._comment or '(comment not loaded)')
 
92
 
 
93
class MagicRule:
 
94
    def __init__(self, f):
 
95
        self.next=None
 
96
        self.prev=None
 
97
 
 
98
        #print line
 
99
        ind=''
 
100
        while True:
 
101
            c=f.read(1)
 
102
            if c=='>':
 
103
                break
 
104
            ind+=c
 
105
        if not ind:
 
106
            self.nest=0
 
107
        else:
 
108
            self.nest=int(ind)
 
109
 
 
110
        start=''
 
111
        while True:
 
112
            c=f.read(1)
 
113
            if c=='=':
 
114
                break
 
115
            start+=c
 
116
        self.start=int(start)
 
117
        
 
118
        hb=f.read(1)
 
119
        lb=f.read(1)
 
120
        self.lenvalue=ord(lb)+(ord(hb)<<8)
 
121
 
 
122
        self.value=f.read(self.lenvalue)
 
123
 
 
124
        c=f.read(1)
 
125
        if c=='&':
 
126
            self.mask=f.read(self.lenvalue)
 
127
            c=f.read(1)
 
128
        else:
 
129
            self.mask=None
 
130
 
 
131
        if c=='~':
 
132
            w=''
 
133
            while c!='+' and c!='\n':
 
134
                c=f.read(1)
 
135
                if c=='+' or c=='\n':
 
136
                    break
 
137
                w+=c
 
138
            
 
139
            self.word=int(w)
 
140
        else:
 
141
            self.word=1
 
142
 
 
143
        if c=='+':
 
144
            r=''
 
145
            while c!='\n':
 
146
                c=f.read(1)
 
147
                if c=='\n':
 
148
                    break
 
149
                r+=c
 
150
            #print r
 
151
            self.range=int(r)
 
152
        else:
 
153
            self.range=1
 
154
 
 
155
        if c!='\n':
 
156
            raise 'Malformed MIME magic line'
 
157
 
 
158
    def getLength(self):
 
159
        return self.start+self.lenvalue+self.range
 
160
 
 
161
    def appendRule(self, rule):
 
162
        if self.nest<rule.nest:
 
163
            self.next=rule
 
164
            rule.prev=self
 
165
 
 
166
        elif self.prev:
 
167
            self.prev.appendRule(rule)
 
168
        
 
169
    def match(self, buffer):
 
170
        if self.match0(buffer):
 
171
            if self.next:
 
172
                return self.next.match(buffer)
 
173
            return True
 
174
 
 
175
    def match0(self, buffer):
 
176
        l=len(buffer)
 
177
        for o in range(self.range):
 
178
            s=self.start+o
 
179
            e=s+self.lenvalue
 
180
            if l<e:
 
181
                return False
 
182
            if self.mask:
 
183
                test=''
 
184
                for i in range(self.lenvalue):
 
185
                    c=ord(buffer[s+i]) & ord(self.mask[i])
 
186
                    test+=chr(c)
 
187
            else:
 
188
                test=buffer[s:e]
 
189
 
 
190
            if test==self.value:
 
191
                return True
 
192
 
 
193
    def __repr__(self):
 
194
        return '<MagicRule %d>%d=[%d]%s&%s~%d+%d>' % (self.nest,
 
195
                                  self.start,
 
196
                                  self.lenvalue,
 
197
                                  `self.value`,
 
198
                                  `self.mask`,
 
199
                                  self.word,
 
200
                                  self.range)
 
201
 
 
202
class MagicType:
 
203
    def __init__(self, mtype):
 
204
        self.mtype=mtype
 
205
        self.top_rules=[]
 
206
        self.last_rule=None
 
207
 
 
208
    def getLine(self, f):
 
209
        nrule=MagicRule(f)
 
210
 
 
211
        if nrule.nest and self.last_rule:
 
212
            self.last_rule.appendRule(nrule)
 
213
        else:
 
214
            self.top_rules.append(nrule)
 
215
 
 
216
        self.last_rule=nrule
 
217
 
 
218
        return nrule
 
219
 
 
220
    def match(self, buffer):
 
221
        for rule in self.top_rules:
 
222
            if rule.match(buffer):
 
223
                return self.mtype
 
224
 
 
225
    def __repr__(self):
 
226
        return '<MagicType %s>' % self.mtype
 
227
    
 
228
class MagicDB:
 
229
    def __init__(self):
 
230
        self.types={}   # Indexed by priority, each entry is a list of type rules
 
231
        self.maxlen=0
 
232
 
 
233
    def mergeFile(self, fname):
 
234
        f=file(fname, 'r')
 
235
        line=f.readline()
 
236
        if line!='MIME-Magic\0\n':
 
237
            raise 'Not a MIME magic file'
 
238
 
 
239
        while True:
 
240
            shead=f.readline()
 
241
            #print shead
 
242
            if not shead:
 
243
                break
 
244
            if shead[0]!='[' or shead[-2:]!=']\n':
 
245
                raise 'Malformed section heading'
 
246
            pri, tname=shead[1:-2].split(':')
 
247
            #print shead[1:-2]
 
248
            pri=int(pri)
 
249
            mtype=lookup(tname)
 
250
 
 
251
            try:
 
252
                ents=self.types[pri]
 
253
            except:
 
254
                ents=[]
 
255
                self.types[pri]=ents
 
256
 
 
257
            magictype=MagicType(mtype)
 
258
            #print tname
 
259
 
 
260
            #rline=f.readline()
 
261
            c=f.read(1)
 
262
            f.seek(-1, 1)
 
263
            while c and c!='[':
 
264
                rule=magictype.getLine(f)
 
265
                #print rule
 
266
                if rule and rule.getLength()>self.maxlen:
 
267
                    self.maxlen=rule.getLength()
 
268
 
 
269
                c=f.read(1)
 
270
                f.seek(-1, 1)
 
271
 
 
272
            ents.append(magictype)
 
273
            #self.types[pri]=ents
 
274
            if not c:
 
275
                break
 
276
 
 
277
    def match_data(self, data, max_pri=100, min_pri=0):
 
278
        pris=self.types.keys()
 
279
        pris.sort(lambda a, b: -cmp(a, b))
 
280
        for pri in pris:
 
281
            #print pri, max_pri, min_pri
 
282
            if pri>max_pri:
 
283
                continue
 
284
            if pri<min_pri:
 
285
                break
 
286
            for type in self.types[pri]:
 
287
                m=type.match(data)
 
288
                if m:
 
289
                    return m
 
290
        
 
291
 
 
292
    def match(self, path, max_pri=100, min_pri=0):
 
293
        try:
 
294
            buf=file(path, 'r').read(self.maxlen)
 
295
            return self.match_data(buf, max_pri, min_pri)
 
296
        except:
 
297
            pass
 
298
 
 
299
        return None
 
300
    
 
301
    def __repr__(self):
 
302
        return '<MagicDB %s>' % self.types
 
303
            
 
304
 
 
305
# Some well-known types
 
306
text = lookup('text', 'plain')
 
307
inode_block = lookup('inode', 'blockdevice')
 
308
inode_char = lookup('inode', 'chardevice')
 
309
inode_dir = lookup('inode', 'directory')
 
310
inode_fifo = lookup('inode', 'fifo')
 
311
inode_socket = lookup('inode', 'socket')
 
312
inode_symlink = lookup('inode', 'symlink')
 
313
inode_door = lookup('inode', 'door')
 
314
app_exe = lookup('application', 'executable')
 
315
 
 
316
_cache_uptodate = False
 
317
 
 
318
def _cache_database():
 
319
    global exts, globs, literals, magic, _cache_uptodate
 
320
 
 
321
    _cache_uptodate = True
 
322
 
 
323
    exts = {}       # Maps extensions to types
 
324
    globs = []      # List of (glob, type) pairs
 
325
    literals = {}   # Maps liternal names to types
 
326
    magic = MagicDB()
 
327
 
 
328
    def _import_glob_file(path):
 
329
        """Loads name matching information from a MIME directory."""
 
330
        for line in file(path):
 
331
            if line.startswith('#'): continue
 
332
            line = line[:-1]
 
333
 
 
334
            type_name, pattern = line.split(':', 1)
 
335
            mtype = lookup(type_name)
 
336
 
 
337
            if pattern.startswith('*.'):
 
338
                rest = pattern[2:]
 
339
                if not ('*' in rest or '[' in rest or '?' in rest):
 
340
                    exts[rest] = mtype
 
341
                    continue
 
342
            if '*' in pattern or '[' in pattern or '?' in pattern:
 
343
                globs.append((pattern, mtype))
 
344
            else:
 
345
                literals[pattern] = mtype
 
346
 
 
347
    for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'globs')):
 
348
        _import_glob_file(path)
 
349
    for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'magic')):
 
350
        magic.mergeFile(path)
 
351
 
 
352
    # Sort globs by length
 
353
    globs.sort(lambda a, b: cmp(len(b[0]), len(a[0])))
 
354
 
 
355
def get_type_by_name(path):
 
356
    """Returns type of file by its name, or None if not known"""
 
357
    if not _cache_uptodate:
 
358
        _cache_database()
 
359
 
 
360
    leaf = os.path.basename(path)
 
361
    if leaf in literals:
 
362
        return literals[leaf]
 
363
 
 
364
    lleaf = leaf.lower()
 
365
    if lleaf in literals:
 
366
        return literals[lleaf]
 
367
 
 
368
    ext = leaf
 
369
    while 1:
 
370
        p = ext.find('.')
 
371
        if p < 0: break
 
372
        ext = ext[p + 1:]
 
373
        if ext in exts:
 
374
            return exts[ext]
 
375
    ext = lleaf
 
376
    while 1:
 
377
        p = ext.find('.')
 
378
        if p < 0: break
 
379
        ext = ext[p+1:]
 
380
        if ext in exts:
 
381
            return exts[ext]
 
382
    for (glob, mime_type) in globs:
 
383
        if fnmatch.fnmatch(leaf, glob):
 
384
            return mime_type
 
385
        if fnmatch.fnmatch(lleaf, glob):
 
386
            return mime_type
 
387
    return None
 
388
 
 
389
def get_type_by_contents(path, max_pri=100, min_pri=0):
 
390
    """Returns type of file by its contents, or None if not known"""
 
391
    if not _cache_uptodate:
 
392
        _cache_database()
 
393
 
 
394
    return magic.match(path, max_pri, min_pri)
 
395
 
 
396
def get_type_by_data(data, max_pri=100, min_pri=0):
 
397
    """Returns type of the data"""
 
398
    if not _cache_uptodate:
 
399
        _cache_database()
 
400
 
 
401
    return magic.match_data(data, max_pri, min_pri)
 
402
 
 
403
def get_type(path, follow=1, name_pri=100):
 
404
    """Returns type of file indicated by path.
 
405
    path     - pathname to check (need not exist)
 
406
    follow   - when reading file, follow symbolic links
 
407
    name_pri - Priority to do name matches.  100=override magic"""
 
408
    if not _cache_uptodate:
 
409
        _cache_database()
 
410
    
 
411
    try:
 
412
        if follow:
 
413
            st = os.stat(path)
 
414
        else:
 
415
            st = os.lstat(path)
 
416
    except:
 
417
        t = get_type_by_name(path)
 
418
        return t or text
 
419
 
 
420
    if stat.S_ISREG(st.st_mode):
 
421
        t = get_type_by_contents(path, min_pri=name_pri)
 
422
        if not t: t = get_type_by_name(path)
 
423
        if not t: t = get_type_by_contents(path, max_pri=name_pri)
 
424
        if t is None:
 
425
            if stat.S_IMODE(st.st_mode) & 0111:
 
426
                return app_exe
 
427
            else:
 
428
                return text
 
429
        return t
 
430
    elif stat.S_ISDIR(st.st_mode): return inode_dir
 
431
    elif stat.S_ISCHR(st.st_mode): return inode_char
 
432
    elif stat.S_ISBLK(st.st_mode): return inode_block
 
433
    elif stat.S_ISFIFO(st.st_mode): return inode_fifo
 
434
    elif stat.S_ISLNK(st.st_mode): return inode_symlink
 
435
    elif stat.S_ISSOCK(st.st_mode): return inode_socket
 
436
    return inode_door
 
437
 
 
438
def install_mime_info(application, package_file):
 
439
    """Copy 'package_file' as ~/.local/share/mime/packages/<application>.xml.
 
440
    If package_file is None, install <app_dir>/<application>.xml.
 
441
    If already installed, does nothing. May overwrite an existing
 
442
    file with the same name (if the contents are different)"""
 
443
    application += '.xml'
 
444
 
 
445
    new_data = file(package_file).read()
 
446
 
 
447
    # See if the file is already installed
 
448
    package_dir = os.path.join('mime', 'packages')
 
449
    resource = os.path.join(package_dir, application)
 
450
    for x in xdg.BaseDirectory.load_data_paths(resource):
 
451
        try:
 
452
            old_data = file(x).read()
 
453
        except:
 
454
            continue
 
455
        if old_data == new_data:
 
456
            return  # Already installed
 
457
 
 
458
    global _cache_uptodate
 
459
    _cache_uptodate = False
 
460
 
 
461
    # Not already installed; add a new copy
 
462
    # Create the directory structure...
 
463
    new_file = os.path.join(xdg.BaseDirectory.save_data_path(package_dir), application)
 
464
 
 
465
    # Write the file...
 
466
    file(new_file, 'w').write(new_data)
 
467
 
 
468
    # Update the database...
 
469
    command = 'update-mime-database'
 
470
    if os.spawnlp(os.P_WAIT, command, command, xdg.BaseDirectory.save_data_path('mime')):
 
471
        os.unlink(new_file)
 
472
        raise Exception("The '%s' command returned an error code!\n" \
 
473
                  "Make sure you have the freedesktop.org shared MIME package:\n" \
 
474
                  "http://standards.freedesktop.org/shared-mime-info/") % command