mirror of
https://github.com/clinton-hall/nzbToMedia.git
synced 2025-08-19 12:59:36 -07:00
Update pyxdg to 0.26
This commit is contained in:
parent
41ccbfdede
commit
79011dbbc1
12 changed files with 1568 additions and 1144 deletions
|
@ -25,7 +25,7 @@ Typical usage:
|
||||||
Note: see the rox.Options module for a higher-level API for managing options.
|
Note: see the rox.Options module for a higher-level API for managing options.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os, stat
|
||||||
|
|
||||||
_home = os.path.expanduser('~')
|
_home = os.path.expanduser('~')
|
||||||
xdg_data_home = os.environ.get('XDG_DATA_HOME') or \
|
xdg_data_home = os.environ.get('XDG_DATA_HOME') or \
|
||||||
|
@ -131,15 +131,30 @@ def get_runtime_dir(strict=True):
|
||||||
|
|
||||||
import getpass
|
import getpass
|
||||||
fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser()
|
fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser()
|
||||||
|
create = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.mkdir(fallback, 0o700)
|
# This must be a real directory, not a symlink, so attackers can't
|
||||||
|
# point it elsewhere. So we use lstat to check it.
|
||||||
|
st = os.lstat(fallback)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
import errno
|
import errno
|
||||||
if e.errno == errno.EEXIST:
|
if e.errno == errno.ENOENT:
|
||||||
# Already exists - set 700 permissions again.
|
create = True
|
||||||
import stat
|
else:
|
||||||
os.chmod(fallback, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)
|
|
||||||
else: # pragma: no cover
|
|
||||||
raise
|
raise
|
||||||
|
else:
|
||||||
|
# The fallback must be a directory
|
||||||
|
if not stat.S_ISDIR(st.st_mode):
|
||||||
|
os.unlink(fallback)
|
||||||
|
create = True
|
||||||
|
# Must be owned by the user and not accessible by anyone else
|
||||||
|
elif (st.st_uid != os.getuid()) \
|
||||||
|
or (st.st_mode & (stat.S_IRWXG | stat.S_IRWXO)):
|
||||||
|
os.rmdir(fallback)
|
||||||
|
create = True
|
||||||
|
|
||||||
|
if create:
|
||||||
|
os.mkdir(fallback, 0o700)
|
||||||
|
|
||||||
return fallback
|
return fallback
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Complete implementation of the XDG Desktop Entry Specification Version 0.9.4
|
Complete implementation of the XDG Desktop Entry Specification
|
||||||
http://standards.freedesktop.org/desktop-entry-spec/
|
http://standards.freedesktop.org/desktop-entry-spec/
|
||||||
|
|
||||||
Not supported:
|
Not supported:
|
||||||
|
@ -13,6 +13,7 @@ Not supported:
|
||||||
from xdg.IniFile import IniFile, is_ascii
|
from xdg.IniFile import IniFile, is_ascii
|
||||||
import xdg.Locale
|
import xdg.Locale
|
||||||
from xdg.Exceptions import ParsingError
|
from xdg.Exceptions import ParsingError
|
||||||
|
from xdg.util import which
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
|
@ -23,7 +24,7 @@ class DesktopEntry(IniFile):
|
||||||
defaultGroup = 'Desktop Entry'
|
defaultGroup = 'Desktop Entry'
|
||||||
|
|
||||||
def __init__(self, filename=None):
|
def __init__(self, filename=None):
|
||||||
"""Create a new DesktopEntry
|
"""Create a new DesktopEntry.
|
||||||
|
|
||||||
If filename exists, it will be parsed as a desktop entry file. If not,
|
If filename exists, it will be parsed as a desktop entry file. If not,
|
||||||
or if filename is None, a blank DesktopEntry is created.
|
or if filename is None, a blank DesktopEntry is created.
|
||||||
|
@ -38,9 +39,23 @@ class DesktopEntry(IniFile):
|
||||||
return self.getName()
|
return self.getName()
|
||||||
|
|
||||||
def parse(self, file):
|
def parse(self, file):
|
||||||
"""Parse a desktop entry file."""
|
"""Parse a desktop entry file.
|
||||||
|
|
||||||
|
This can raise :class:`~xdg.Exceptions.ParsingError`,
|
||||||
|
:class:`~xdg.Exceptions.DuplicateGroupError` or
|
||||||
|
:class:`~xdg.Exceptions.DuplicateKeyError`.
|
||||||
|
"""
|
||||||
IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"])
|
IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"])
|
||||||
|
|
||||||
|
def findTryExec(self):
|
||||||
|
"""Looks in the PATH for the executable given in the TryExec field.
|
||||||
|
|
||||||
|
Returns the full path to the executable if it is found, None if not.
|
||||||
|
Raises :class:`~xdg.Exceptions.NoKeyError` if TryExec is not present.
|
||||||
|
"""
|
||||||
|
tryexec = self.get('TryExec', strict=True)
|
||||||
|
return which(tryexec)
|
||||||
|
|
||||||
# start standard keys
|
# start standard keys
|
||||||
def getType(self):
|
def getType(self):
|
||||||
return self.get('Type')
|
return self.get('Type')
|
||||||
|
@ -140,10 +155,11 @@ class DesktopEntry(IniFile):
|
||||||
|
|
||||||
# desktop entry edit stuff
|
# desktop entry edit stuff
|
||||||
def new(self, filename):
|
def new(self, filename):
|
||||||
"""Make this instance into a new desktop entry.
|
"""Make this instance into a new, blank desktop entry.
|
||||||
|
|
||||||
If filename has a .desktop extension, Type is set to Application. If it
|
If filename has a .desktop extension, Type is set to Application. If it
|
||||||
has a .directory extension, Type is Directory.
|
has a .directory extension, Type is Directory. Other extensions will
|
||||||
|
cause :class:`~xdg.Exceptions.ParsingError` to be raised.
|
||||||
"""
|
"""
|
||||||
if os.path.splitext(filename)[1] == ".desktop":
|
if os.path.splitext(filename)[1] == ".desktop":
|
||||||
type = "Application"
|
type = "Application"
|
||||||
|
@ -185,7 +201,7 @@ class DesktopEntry(IniFile):
|
||||||
def checkGroup(self, group):
|
def checkGroup(self, group):
|
||||||
# check if group header is valid
|
# check if group header is valid
|
||||||
if not (group == self.defaultGroup \
|
if not (group == self.defaultGroup \
|
||||||
or re.match("^Desktop Action [a-zA-Z0-9\-]+$", group) \
|
or re.match("^Desktop Action [a-zA-Z0-9-]+$", group) \
|
||||||
or (re.match("^X-", group) and is_ascii(group))):
|
or (re.match("^X-", group) and is_ascii(group))):
|
||||||
self.errors.append("Invalid Group name: %s" % group)
|
self.errors.append("Invalid Group name: %s" % group)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -5,6 +5,7 @@ Exception Classes for the xdg package
|
||||||
debug = False
|
debug = False
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
|
"""Base class for exceptions defined here."""
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
Exception.__init__(self, msg)
|
Exception.__init__(self, msg)
|
||||||
|
@ -12,40 +13,72 @@ class Error(Exception):
|
||||||
return self.msg
|
return self.msg
|
||||||
|
|
||||||
class ValidationError(Error):
|
class ValidationError(Error):
|
||||||
|
"""Raised when a file fails to validate.
|
||||||
|
|
||||||
|
The filename is the .file attribute.
|
||||||
|
"""
|
||||||
def __init__(self, msg, file):
|
def __init__(self, msg, file):
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.file = file
|
self.file = file
|
||||||
Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg))
|
Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg))
|
||||||
|
|
||||||
class ParsingError(Error):
|
class ParsingError(Error):
|
||||||
|
"""Raised when a file cannot be parsed.
|
||||||
|
|
||||||
|
The filename is the .file attribute.
|
||||||
|
"""
|
||||||
def __init__(self, msg, file):
|
def __init__(self, msg, file):
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.file = file
|
self.file = file
|
||||||
Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg))
|
Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg))
|
||||||
|
|
||||||
class NoKeyError(Error):
|
class NoKeyError(Error):
|
||||||
|
"""Raised when trying to access a nonexistant key in an INI-style file.
|
||||||
|
|
||||||
|
Attributes are .key, .group and .file.
|
||||||
|
"""
|
||||||
def __init__(self, key, group, file):
|
def __init__(self, key, group, file):
|
||||||
Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file))
|
Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file))
|
||||||
self.key = key
|
self.key = key
|
||||||
self.group = group
|
self.group = group
|
||||||
|
self.file = file
|
||||||
|
|
||||||
class DuplicateKeyError(Error):
|
class DuplicateKeyError(Error):
|
||||||
|
"""Raised when the same key occurs twice in an INI-style file.
|
||||||
|
|
||||||
|
Attributes are .key, .group and .file.
|
||||||
|
"""
|
||||||
def __init__(self, key, group, file):
|
def __init__(self, key, group, file):
|
||||||
Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file))
|
Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file))
|
||||||
self.key = key
|
self.key = key
|
||||||
self.group = group
|
self.group = group
|
||||||
|
self.file = file
|
||||||
|
|
||||||
class NoGroupError(Error):
|
class NoGroupError(Error):
|
||||||
|
"""Raised when trying to access a nonexistant group in an INI-style file.
|
||||||
|
|
||||||
|
Attributes are .group and .file.
|
||||||
|
"""
|
||||||
def __init__(self, group, file):
|
def __init__(self, group, file):
|
||||||
Error.__init__(self, "No group: %s in file %s" % (group, file))
|
Error.__init__(self, "No group: %s in file %s" % (group, file))
|
||||||
self.group = group
|
self.group = group
|
||||||
|
self.file = file
|
||||||
|
|
||||||
class DuplicateGroupError(Error):
|
class DuplicateGroupError(Error):
|
||||||
|
"""Raised when the same key occurs twice in an INI-style file.
|
||||||
|
|
||||||
|
Attributes are .group and .file.
|
||||||
|
"""
|
||||||
def __init__(self, group, file):
|
def __init__(self, group, file):
|
||||||
Error.__init__(self, "Duplicate group: %s in file %s" % (group, file))
|
Error.__init__(self, "Duplicate group: %s in file %s" % (group, file))
|
||||||
self.group = group
|
self.group = group
|
||||||
|
self.file = file
|
||||||
|
|
||||||
class NoThemeError(Error):
|
class NoThemeError(Error):
|
||||||
|
"""Raised when trying to access a nonexistant icon theme.
|
||||||
|
|
||||||
|
The name of the theme is the .theme attribute.
|
||||||
|
"""
|
||||||
def __init__(self, theme):
|
def __init__(self, theme):
|
||||||
Error.__init__(self, "No such icon-theme: %s" % theme)
|
Error.__init__(self, "No such icon-theme: %s" % theme)
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Complete implementation of the XDG Icon Spec Version 0.8
|
Complete implementation of the XDG Icon Spec
|
||||||
http://standards.freedesktop.org/icon-theme-spec/
|
http://standards.freedesktop.org/icon-theme-spec/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -37,6 +37,8 @@ class IconTheme(IniFile):
|
||||||
return self.get('Inherits', list=True)
|
return self.get('Inherits', list=True)
|
||||||
def getDirectories(self):
|
def getDirectories(self):
|
||||||
return self.get('Directories', list=True)
|
return self.get('Directories', list=True)
|
||||||
|
def getScaledDirectories(self):
|
||||||
|
return self.get('ScaledDirectories', list=True)
|
||||||
def getHidden(self):
|
def getHidden(self):
|
||||||
return self.get('Hidden', type="boolean")
|
return self.get('Hidden', type="boolean")
|
||||||
def getExample(self):
|
def getExample(self):
|
||||||
|
@ -72,6 +74,10 @@ class IconTheme(IniFile):
|
||||||
else:
|
else:
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
def getScale(self, directory):
|
||||||
|
value = self.get('Scale', type="integer", group=directory)
|
||||||
|
return value or 1
|
||||||
|
|
||||||
# validation stuff
|
# validation stuff
|
||||||
def checkExtras(self):
|
def checkExtras(self):
|
||||||
# header
|
# header
|
||||||
|
@ -125,7 +131,7 @@ class IconTheme(IniFile):
|
||||||
self.name = self.content[group]["Size"]
|
self.name = self.content[group]["Size"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.errors.append("Key 'Size' in Group '%s' is missing" % group)
|
self.errors.append("Key 'Size' in Group '%s' is missing" % group)
|
||||||
elif not (re.match("^\[X-", group) and is_ascii(group)):
|
elif not (re.match(r"^\[X-", group) and is_ascii(group)):
|
||||||
self.errors.append("Invalid Group name: %s" % group)
|
self.errors.append("Invalid Group name: %s" % group)
|
||||||
|
|
||||||
def checkKey(self, key, value, group):
|
def checkKey(self, key, value, group):
|
||||||
|
@ -139,6 +145,8 @@ class IconTheme(IniFile):
|
||||||
self.checkValue(key, value, list=True)
|
self.checkValue(key, value, list=True)
|
||||||
elif key == "Directories":
|
elif key == "Directories":
|
||||||
self.checkValue(key, value, list=True)
|
self.checkValue(key, value, list=True)
|
||||||
|
elif key == "ScaledDirectories":
|
||||||
|
self.checkValue(key, value, list=True)
|
||||||
elif key == "Hidden":
|
elif key == "Hidden":
|
||||||
self.checkValue(key, value, type="boolean")
|
self.checkValue(key, value, type="boolean")
|
||||||
elif key == "Example":
|
elif key == "Example":
|
||||||
|
@ -168,6 +176,8 @@ class IconTheme(IniFile):
|
||||||
self.checkValue(key, value, type="integer")
|
self.checkValue(key, value, type="integer")
|
||||||
if self.type != "Threshold":
|
if self.type != "Threshold":
|
||||||
self.errors.append("Key 'Threshold' give, but Type is %s" % self.type)
|
self.errors.append("Key 'Threshold' give, but Type is %s" % self.type)
|
||||||
|
elif key == "Scale":
|
||||||
|
self.checkValue(key, value, type="integer")
|
||||||
elif re.match("^X-[a-zA-Z0-9-]+", key):
|
elif re.match("^X-[a-zA-Z0-9-]+", key):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -211,7 +221,7 @@ class IconData(IniFile):
|
||||||
def checkGroup(self, group):
|
def checkGroup(self, group):
|
||||||
# check if group header is valid
|
# check if group header is valid
|
||||||
if not (group == self.defaultGroup \
|
if not (group == self.defaultGroup \
|
||||||
or (re.match("^\[X-", group) and is_ascii(group))):
|
or (re.match(r"^\[X-", group) and is_ascii(group))):
|
||||||
self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace"))
|
self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace"))
|
||||||
|
|
||||||
def checkKey(self, key, value, group):
|
def checkKey(self, key, value, group):
|
||||||
|
|
|
@ -102,7 +102,7 @@ class IniFile:
|
||||||
raise ParsingError("[%s]-Header missing" % headers[0], filename)
|
raise ParsingError("[%s]-Header missing" % headers[0], filename)
|
||||||
|
|
||||||
# start stuff to access the keys
|
# start stuff to access the keys
|
||||||
def get(self, key, group=None, locale=False, type="string", list=False):
|
def get(self, key, group=None, locale=False, type="string", list=False, strict=False):
|
||||||
# set default group
|
# set default group
|
||||||
if not group:
|
if not group:
|
||||||
group = self.defaultGroup
|
group = self.defaultGroup
|
||||||
|
@ -114,7 +114,7 @@ class IniFile:
|
||||||
else:
|
else:
|
||||||
value = self.content[group][key]
|
value = self.content[group][key]
|
||||||
else:
|
else:
|
||||||
if debug:
|
if strict or debug:
|
||||||
if group not in self.content:
|
if group not in self.content:
|
||||||
raise NoGroupError(group, self.filename)
|
raise NoGroupError(group, self.filename)
|
||||||
elif key not in self.content[group]:
|
elif key not in self.content[group]:
|
||||||
|
@ -192,8 +192,8 @@ class IniFile:
|
||||||
|
|
||||||
# start validation stuff
|
# start validation stuff
|
||||||
def validate(self, report="All"):
|
def validate(self, report="All"):
|
||||||
"""Validate the contents, raising ``ValidationError`` if there
|
"""Validate the contents, raising :class:`~xdg.Exceptions.ValidationError`
|
||||||
is anything amiss.
|
if there is anything amiss.
|
||||||
|
|
||||||
report can be 'All' / 'Warnings' / 'Errors'
|
report can be 'All' / 'Warnings' / 'Errors'
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -9,7 +9,7 @@ http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&vi
|
||||||
import os
|
import os
|
||||||
from locale import normalize
|
from locale import normalize
|
||||||
|
|
||||||
regex = "(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z\-0-9]+)?(@[a-zA-Z]+)?\])?"
|
regex = r"(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z0-9-]+)?(@[a-zA-Z]+)?\])?"
|
||||||
|
|
||||||
def _expand_lang(locale):
|
def _expand_lang(locale):
|
||||||
locale = normalize(locale)
|
locale = normalize(locale)
|
||||||
|
|
1530
libs/xdg/Menu.py
1530
libs/xdg/Menu.py
File diff suppressed because it is too large
Load diff
|
@ -1,14 +1,14 @@
|
||||||
""" CLass to edit XDG Menus """
|
""" CLass to edit XDG Menus """
|
||||||
|
|
||||||
from xdg.Menu import *
|
|
||||||
from xdg.BaseDirectory import *
|
|
||||||
from xdg.Exceptions import *
|
|
||||||
from xdg.DesktopEntry import *
|
|
||||||
from xdg.Config import *
|
|
||||||
|
|
||||||
import xml.dom.minidom
|
|
||||||
import os
|
import os
|
||||||
import re
|
try:
|
||||||
|
import xml.etree.cElementTree as etree
|
||||||
|
except ImportError:
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
from xdg.Menu import Menu, MenuEntry, Layout, Separator, XMLMenuBuilder
|
||||||
|
from xdg.BaseDirectory import xdg_config_dirs, xdg_data_dirs
|
||||||
|
from xdg.Exceptions import ParsingError
|
||||||
|
from xdg.Config import setRootMode
|
||||||
|
|
||||||
# XML-Cleanups: Move / Exclude
|
# XML-Cleanups: Move / Exclude
|
||||||
# FIXME: proper reverte/delete
|
# FIXME: proper reverte/delete
|
||||||
|
@ -20,28 +20,31 @@ import re
|
||||||
# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile
|
# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile
|
||||||
# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs
|
# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs
|
||||||
|
|
||||||
class MenuEditor:
|
|
||||||
|
class MenuEditor(object):
|
||||||
|
|
||||||
def __init__(self, menu=None, filename=None, root=False):
|
def __init__(self, menu=None, filename=None, root=False):
|
||||||
self.menu = None
|
self.menu = None
|
||||||
self.filename = None
|
self.filename = None
|
||||||
self.doc = None
|
self.tree = None
|
||||||
|
self.parser = XMLMenuBuilder()
|
||||||
self.parse(menu, filename, root)
|
self.parse(menu, filename, root)
|
||||||
|
|
||||||
# fix for creating two menus with the same name on the fly
|
# fix for creating two menus with the same name on the fly
|
||||||
self.filenames = []
|
self.filenames = []
|
||||||
|
|
||||||
def parse(self, menu=None, filename=None, root=False):
|
def parse(self, menu=None, filename=None, root=False):
|
||||||
if root == True:
|
if root:
|
||||||
setRootMode(True)
|
setRootMode(True)
|
||||||
|
|
||||||
if isinstance(menu, Menu):
|
if isinstance(menu, Menu):
|
||||||
self.menu = menu
|
self.menu = menu
|
||||||
elif menu:
|
elif menu:
|
||||||
self.menu = parse(menu)
|
self.menu = self.parser.parse(menu)
|
||||||
else:
|
else:
|
||||||
self.menu = parse()
|
self.menu = self.parser.parse()
|
||||||
|
|
||||||
if root == True:
|
if root:
|
||||||
self.filename = self.menu.Filename
|
self.filename = self.menu.Filename
|
||||||
elif filename:
|
elif filename:
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
|
@ -49,13 +52,21 @@ class MenuEditor:
|
||||||
self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1])
|
self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.doc = xml.dom.minidom.parse(self.filename)
|
self.tree = etree.parse(self.filename)
|
||||||
except IOError:
|
except IOError:
|
||||||
self.doc = xml.dom.minidom.parseString('<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://standards.freedesktop.org/menu-spec/menu-1.0.dtd"><Menu><Name>Applications</Name><MergeFile type="parent">'+self.menu.Filename+'</MergeFile></Menu>')
|
root = etree.fromtring("""
|
||||||
except xml.parsers.expat.ExpatError:
|
<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://standards.freedesktop.org/menu-spec/menu-1.0.dtd">
|
||||||
|
<Menu>
|
||||||
|
<Name>Applications</Name>
|
||||||
|
<MergeFile type="parent">%s</MergeFile>
|
||||||
|
</Menu>
|
||||||
|
""" % self.menu.Filename)
|
||||||
|
self.tree = etree.ElementTree(root)
|
||||||
|
except ParsingError:
|
||||||
raise ParsingError('Not a valid .menu file', self.filename)
|
raise ParsingError('Not a valid .menu file', self.filename)
|
||||||
|
|
||||||
self.__remove_whilespace_nodes(self.doc)
|
#FIXME: is this needed with etree ?
|
||||||
|
self.__remove_whitespace_nodes(self.tree)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
self.__saveEntries(self.menu)
|
self.__saveEntries(self.menu)
|
||||||
|
@ -67,7 +78,7 @@ class MenuEditor:
|
||||||
|
|
||||||
self.__addEntry(parent, menuentry, after, before)
|
self.__addEntry(parent, menuentry, after, before)
|
||||||
|
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
|
|
||||||
return menuentry
|
return menuentry
|
||||||
|
|
||||||
|
@ -83,7 +94,7 @@ class MenuEditor:
|
||||||
|
|
||||||
self.__addEntry(parent, menu, after, before)
|
self.__addEntry(parent, menu, after, before)
|
||||||
|
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
|
|
||||||
return menu
|
return menu
|
||||||
|
|
||||||
|
@ -92,7 +103,7 @@ class MenuEditor:
|
||||||
|
|
||||||
self.__addEntry(parent, separator, after, before)
|
self.__addEntry(parent, separator, after, before)
|
||||||
|
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
|
|
||||||
return separator
|
return separator
|
||||||
|
|
||||||
|
@ -100,7 +111,7 @@ class MenuEditor:
|
||||||
self.__deleteEntry(oldparent, menuentry, after, before)
|
self.__deleteEntry(oldparent, menuentry, after, before)
|
||||||
self.__addEntry(newparent, menuentry, after, before)
|
self.__addEntry(newparent, menuentry, after, before)
|
||||||
|
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
|
|
||||||
return menuentry
|
return menuentry
|
||||||
|
|
||||||
|
@ -112,7 +123,7 @@ class MenuEditor:
|
||||||
if oldparent.getPath(True) != newparent.getPath(True):
|
if oldparent.getPath(True) != newparent.getPath(True):
|
||||||
self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name))
|
self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name))
|
||||||
|
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
|
|
||||||
return menu
|
return menu
|
||||||
|
|
||||||
|
@ -120,14 +131,14 @@ class MenuEditor:
|
||||||
self.__deleteEntry(parent, separator, after, before)
|
self.__deleteEntry(parent, separator, after, before)
|
||||||
self.__addEntry(parent, separator, after, before)
|
self.__addEntry(parent, separator, after, before)
|
||||||
|
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
|
|
||||||
return separator
|
return separator
|
||||||
|
|
||||||
def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
|
def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
|
||||||
self.__addEntry(newparent, menuentry, after, before)
|
self.__addEntry(newparent, menuentry, after, before)
|
||||||
|
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
|
|
||||||
return menuentry
|
return menuentry
|
||||||
|
|
||||||
|
@ -137,39 +148,39 @@ class MenuEditor:
|
||||||
if name:
|
if name:
|
||||||
if not deskentry.hasKey("Name"):
|
if not deskentry.hasKey("Name"):
|
||||||
deskentry.set("Name", name)
|
deskentry.set("Name", name)
|
||||||
deskentry.set("Name", name, locale = True)
|
deskentry.set("Name", name, locale=True)
|
||||||
if comment:
|
if comment:
|
||||||
if not deskentry.hasKey("Comment"):
|
if not deskentry.hasKey("Comment"):
|
||||||
deskentry.set("Comment", comment)
|
deskentry.set("Comment", comment)
|
||||||
deskentry.set("Comment", comment, locale = True)
|
deskentry.set("Comment", comment, locale=True)
|
||||||
if genericname:
|
if genericname:
|
||||||
if not deskentry.hasKey("GnericNe"):
|
if not deskentry.hasKey("GenericName"):
|
||||||
deskentry.set("GenericName", genericname)
|
deskentry.set("GenericName", genericname)
|
||||||
deskentry.set("GenericName", genericname, locale = True)
|
deskentry.set("GenericName", genericname, locale=True)
|
||||||
if command:
|
if command:
|
||||||
deskentry.set("Exec", command)
|
deskentry.set("Exec", command)
|
||||||
if icon:
|
if icon:
|
||||||
deskentry.set("Icon", icon)
|
deskentry.set("Icon", icon)
|
||||||
|
|
||||||
if terminal == True:
|
if terminal:
|
||||||
deskentry.set("Terminal", "true")
|
deskentry.set("Terminal", "true")
|
||||||
elif terminal == False:
|
elif not terminal:
|
||||||
deskentry.set("Terminal", "false")
|
deskentry.set("Terminal", "false")
|
||||||
|
|
||||||
if nodisplay == True:
|
if nodisplay is True:
|
||||||
deskentry.set("NoDisplay", "true")
|
deskentry.set("NoDisplay", "true")
|
||||||
elif nodisplay == False:
|
elif nodisplay is False:
|
||||||
deskentry.set("NoDisplay", "false")
|
deskentry.set("NoDisplay", "false")
|
||||||
|
|
||||||
if hidden == True:
|
if hidden is True:
|
||||||
deskentry.set("Hidden", "true")
|
deskentry.set("Hidden", "true")
|
||||||
elif hidden == False:
|
elif hidden is False:
|
||||||
deskentry.set("Hidden", "false")
|
deskentry.set("Hidden", "false")
|
||||||
|
|
||||||
menuentry.updateAttributes()
|
menuentry.updateAttributes()
|
||||||
|
|
||||||
if len(menuentry.Parents) > 0:
|
if len(menuentry.Parents) > 0:
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
|
|
||||||
return menuentry
|
return menuentry
|
||||||
|
|
||||||
|
@ -195,56 +206,58 @@ class MenuEditor:
|
||||||
if name:
|
if name:
|
||||||
if not deskentry.hasKey("Name"):
|
if not deskentry.hasKey("Name"):
|
||||||
deskentry.set("Name", name)
|
deskentry.set("Name", name)
|
||||||
deskentry.set("Name", name, locale = True)
|
deskentry.set("Name", name, locale=True)
|
||||||
if genericname:
|
if genericname:
|
||||||
if not deskentry.hasKey("GenericName"):
|
if not deskentry.hasKey("GenericName"):
|
||||||
deskentry.set("GenericName", genericname)
|
deskentry.set("GenericName", genericname)
|
||||||
deskentry.set("GenericName", genericname, locale = True)
|
deskentry.set("GenericName", genericname, locale=True)
|
||||||
if comment:
|
if comment:
|
||||||
if not deskentry.hasKey("Comment"):
|
if not deskentry.hasKey("Comment"):
|
||||||
deskentry.set("Comment", comment)
|
deskentry.set("Comment", comment)
|
||||||
deskentry.set("Comment", comment, locale = True)
|
deskentry.set("Comment", comment, locale=True)
|
||||||
if icon:
|
if icon:
|
||||||
deskentry.set("Icon", icon)
|
deskentry.set("Icon", icon)
|
||||||
|
|
||||||
if nodisplay == True:
|
if nodisplay is True:
|
||||||
deskentry.set("NoDisplay", "true")
|
deskentry.set("NoDisplay", "true")
|
||||||
elif nodisplay == False:
|
elif nodisplay is False:
|
||||||
deskentry.set("NoDisplay", "false")
|
deskentry.set("NoDisplay", "false")
|
||||||
|
|
||||||
if hidden == True:
|
if hidden is True:
|
||||||
deskentry.set("Hidden", "true")
|
deskentry.set("Hidden", "true")
|
||||||
elif hidden == False:
|
elif hidden is False:
|
||||||
deskentry.set("Hidden", "false")
|
deskentry.set("Hidden", "false")
|
||||||
|
|
||||||
menu.Directory.updateAttributes()
|
menu.Directory.updateAttributes()
|
||||||
|
|
||||||
if isinstance(menu.Parent, Menu):
|
if isinstance(menu.Parent, Menu):
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
|
|
||||||
return menu
|
return menu
|
||||||
|
|
||||||
def hideMenuEntry(self, menuentry):
|
def hideMenuEntry(self, menuentry):
|
||||||
self.editMenuEntry(menuentry, nodisplay = True)
|
self.editMenuEntry(menuentry, nodisplay=True)
|
||||||
|
|
||||||
def unhideMenuEntry(self, menuentry):
|
def unhideMenuEntry(self, menuentry):
|
||||||
self.editMenuEntry(menuentry, nodisplay = False, hidden = False)
|
self.editMenuEntry(menuentry, nodisplay=False, hidden=False)
|
||||||
|
|
||||||
def hideMenu(self, menu):
|
def hideMenu(self, menu):
|
||||||
self.editMenu(menu, nodisplay = True)
|
self.editMenu(menu, nodisplay=True)
|
||||||
|
|
||||||
def unhideMenu(self, menu):
|
def unhideMenu(self, menu):
|
||||||
self.editMenu(menu, nodisplay = False, hidden = False)
|
self.editMenu(menu, nodisplay=False, hidden=False)
|
||||||
xml_menu = self.__getXmlMenu(menu.getPath(True,True), False)
|
xml_menu = self.__getXmlMenu(menu.getPath(True, True), False)
|
||||||
for node in self.__getXmlNodesByName(["Deleted", "NotDeleted"], xml_menu):
|
deleted = xml_menu.findall('Deleted')
|
||||||
node.parentNode.removeChild(node)
|
not_deleted = xml_menu.findall('NotDeleted')
|
||||||
|
for node in deleted + not_deleted:
|
||||||
|
xml_menu.remove(node)
|
||||||
|
|
||||||
def deleteMenuEntry(self, menuentry):
|
def deleteMenuEntry(self, menuentry):
|
||||||
if self.getAction(menuentry) == "delete":
|
if self.getAction(menuentry) == "delete":
|
||||||
self.__deleteFile(menuentry.DesktopEntry.filename)
|
self.__deleteFile(menuentry.DesktopEntry.filename)
|
||||||
for parent in menuentry.Parents:
|
for parent in menuentry.Parents:
|
||||||
self.__deleteEntry(parent, menuentry)
|
self.__deleteEntry(parent, menuentry)
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
return menuentry
|
return menuentry
|
||||||
|
|
||||||
def revertMenuEntry(self, menuentry):
|
def revertMenuEntry(self, menuentry):
|
||||||
|
@ -257,7 +270,7 @@ class MenuEditor:
|
||||||
index = parent.MenuEntries.index(menuentry)
|
index = parent.MenuEntries.index(menuentry)
|
||||||
parent.MenuEntries[index] = menuentry.Original
|
parent.MenuEntries[index] = menuentry.Original
|
||||||
menuentry.Original.Parents.append(parent)
|
menuentry.Original.Parents.append(parent)
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
return menuentry
|
return menuentry
|
||||||
|
|
||||||
def deleteMenu(self, menu):
|
def deleteMenu(self, menu):
|
||||||
|
@ -265,21 +278,22 @@ class MenuEditor:
|
||||||
self.__deleteFile(menu.Directory.DesktopEntry.filename)
|
self.__deleteFile(menu.Directory.DesktopEntry.filename)
|
||||||
self.__deleteEntry(menu.Parent, menu)
|
self.__deleteEntry(menu.Parent, menu)
|
||||||
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
|
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
|
||||||
xml_menu.parentNode.removeChild(xml_menu)
|
parent = self.__get_parent_node(xml_menu)
|
||||||
sort(self.menu)
|
parent.remove(xml_menu)
|
||||||
|
self.menu.sort()
|
||||||
return menu
|
return menu
|
||||||
|
|
||||||
def revertMenu(self, menu):
|
def revertMenu(self, menu):
|
||||||
if self.getAction(menu) == "revert":
|
if self.getAction(menu) == "revert":
|
||||||
self.__deleteFile(menu.Directory.DesktopEntry.filename)
|
self.__deleteFile(menu.Directory.DesktopEntry.filename)
|
||||||
menu.Directory = menu.Directory.Original
|
menu.Directory = menu.Directory.Original
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
return menu
|
return menu
|
||||||
|
|
||||||
def deleteSeparator(self, separator):
|
def deleteSeparator(self, separator):
|
||||||
self.__deleteEntry(separator.Parent, separator, after=True)
|
self.__deleteEntry(separator.Parent, separator, after=True)
|
||||||
|
|
||||||
sort(self.menu)
|
self.menu.sort()
|
||||||
|
|
||||||
return separator
|
return separator
|
||||||
|
|
||||||
|
@ -290,8 +304,9 @@ class MenuEditor:
|
||||||
return "none"
|
return "none"
|
||||||
elif entry.Directory.getType() == "Both":
|
elif entry.Directory.getType() == "Both":
|
||||||
return "revert"
|
return "revert"
|
||||||
elif entry.Directory.getType() == "User" \
|
elif entry.Directory.getType() == "User" and (
|
||||||
and (len(entry.Submenus) + len(entry.MenuEntries)) == 0:
|
len(entry.Submenus) + len(entry.MenuEntries)
|
||||||
|
) == 0:
|
||||||
return "delete"
|
return "delete"
|
||||||
|
|
||||||
elif isinstance(entry, MenuEntry):
|
elif isinstance(entry, MenuEntry):
|
||||||
|
@ -318,9 +333,7 @@ class MenuEditor:
|
||||||
def __saveMenu(self):
|
def __saveMenu(self):
|
||||||
if not os.path.isdir(os.path.dirname(self.filename)):
|
if not os.path.isdir(os.path.dirname(self.filename)):
|
||||||
os.makedirs(os.path.dirname(self.filename))
|
os.makedirs(os.path.dirname(self.filename))
|
||||||
fd = open(self.filename, 'w')
|
self.tree.write(self.filename, encoding='utf-8')
|
||||||
fd.write(re.sub("\n[\s]*([^\n<]*)\n[\s]*</", "\\1</", self.doc.toprettyxml().replace('<?xml version="1.0" ?>\n', '')))
|
|
||||||
fd.close()
|
|
||||||
|
|
||||||
def __getFileName(self, name, extension):
|
def __getFileName(self, name, extension):
|
||||||
postfix = 0
|
postfix = 0
|
||||||
|
@ -333,8 +346,9 @@ class MenuEditor:
|
||||||
dir = "applications"
|
dir = "applications"
|
||||||
elif extension == ".directory":
|
elif extension == ".directory":
|
||||||
dir = "desktop-directories"
|
dir = "desktop-directories"
|
||||||
if not filename in self.filenames and not \
|
if not filename in self.filenames and not os.path.isfile(
|
||||||
os.path.isfile(os.path.join(xdg_data_dirs[0], dir, filename)):
|
os.path.join(xdg_data_dirs[0], dir, filename)
|
||||||
|
):
|
||||||
self.filenames.append(filename)
|
self.filenames.append(filename)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
@ -343,8 +357,11 @@ class MenuEditor:
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
def __getXmlMenu(self, path, create=True, element=None):
|
def __getXmlMenu(self, path, create=True, element=None):
|
||||||
|
# FIXME: we should also return the menu's parent,
|
||||||
|
# to avoid looking for it later on
|
||||||
|
# @see Element.getiterator()
|
||||||
if not element:
|
if not element:
|
||||||
element = self.doc
|
element = self.tree
|
||||||
|
|
||||||
if "/" in path:
|
if "/" in path:
|
||||||
(name, path) = path.split("/", 1)
|
(name, path) = path.split("/", 1)
|
||||||
|
@ -353,17 +370,16 @@ class MenuEditor:
|
||||||
path = ""
|
path = ""
|
||||||
|
|
||||||
found = None
|
found = None
|
||||||
for node in self.__getXmlNodesByName("Menu", element):
|
for node in element.findall("Menu"):
|
||||||
for child in self.__getXmlNodesByName("Name", node):
|
name_node = node.find('Name')
|
||||||
if child.childNodes[0].nodeValue == name:
|
if name_node.text == name:
|
||||||
if path:
|
if path:
|
||||||
found = self.__getXmlMenu(path, create, node)
|
found = self.__getXmlMenu(path, create, node)
|
||||||
else:
|
else:
|
||||||
found = node
|
found = node
|
||||||
break
|
|
||||||
if found:
|
if found:
|
||||||
break
|
break
|
||||||
if not found and create == True:
|
if not found and create:
|
||||||
node = self.__addXmlMenuElement(element, name)
|
node = self.__addXmlMenuElement(element, name)
|
||||||
if path:
|
if path:
|
||||||
found = self.__getXmlMenu(path, create, node)
|
found = self.__getXmlMenu(path, create, node)
|
||||||
|
@ -373,58 +389,62 @@ class MenuEditor:
|
||||||
return found
|
return found
|
||||||
|
|
||||||
def __addXmlMenuElement(self, element, name):
|
def __addXmlMenuElement(self, element, name):
|
||||||
node = self.doc.createElement('Menu')
|
menu_node = etree.SubElement('Menu', element)
|
||||||
self.__addXmlTextElement(node, 'Name', name)
|
name_node = etree.SubElement('Name', menu_node)
|
||||||
return element.appendChild(node)
|
name_node.text = name
|
||||||
|
return menu_node
|
||||||
|
|
||||||
def __addXmlTextElement(self, element, name, text):
|
def __addXmlTextElement(self, element, name, text):
|
||||||
node = self.doc.createElement(name)
|
node = etree.SubElement(name, element)
|
||||||
text = self.doc.createTextNode(text)
|
node.text = text
|
||||||
node.appendChild(text)
|
return node
|
||||||
return element.appendChild(node)
|
|
||||||
|
|
||||||
def __addXmlFilename(self, element, filename, type = "Include"):
|
def __addXmlFilename(self, element, filename, type_="Include"):
|
||||||
# remove old filenames
|
# remove old filenames
|
||||||
for node in self.__getXmlNodesByName(["Include", "Exclude"], element):
|
includes = element.findall('Include')
|
||||||
if node.childNodes[0].nodeName == "Filename" and node.childNodes[0].childNodes[0].nodeValue == filename:
|
excludes = element.findall('Exclude')
|
||||||
element.removeChild(node)
|
rules = includes + excludes
|
||||||
|
for rule in rules:
|
||||||
|
#FIXME: this finds only Rules whose FIRST child is a Filename element
|
||||||
|
if rule[0].tag == "Filename" and rule[0].text == filename:
|
||||||
|
element.remove(rule)
|
||||||
|
# shouldn't it remove all occurences, like the following:
|
||||||
|
#filename_nodes = rule.findall('.//Filename'):
|
||||||
|
#for fn in filename_nodes:
|
||||||
|
#if fn.text == filename:
|
||||||
|
##element.remove(rule)
|
||||||
|
#parent = self.__get_parent_node(fn)
|
||||||
|
#parent.remove(fn)
|
||||||
|
|
||||||
# add new filename
|
# add new filename
|
||||||
node = self.doc.createElement(type)
|
node = etree.SubElement(type_, element)
|
||||||
node.appendChild(self.__addXmlTextElement(node, 'Filename', filename))
|
self.__addXmlTextElement(node, 'Filename', filename)
|
||||||
return element.appendChild(node)
|
return node
|
||||||
|
|
||||||
def __addXmlMove(self, element, old, new):
|
def __addXmlMove(self, element, old, new):
|
||||||
node = self.doc.createElement("Move")
|
node = etree.SubElement("Move", element)
|
||||||
node.appendChild(self.__addXmlTextElement(node, 'Old', old))
|
self.__addXmlTextElement(node, 'Old', old)
|
||||||
node.appendChild(self.__addXmlTextElement(node, 'New', new))
|
self.__addXmlTextElement(node, 'New', new)
|
||||||
return element.appendChild(node)
|
return node
|
||||||
|
|
||||||
def __addXmlLayout(self, element, layout):
|
def __addXmlLayout(self, element, layout):
|
||||||
# remove old layout
|
# remove old layout
|
||||||
for node in self.__getXmlNodesByName("Layout", element):
|
for node in element.findall("Layout"):
|
||||||
element.removeChild(node)
|
element.remove(node)
|
||||||
|
|
||||||
# add new layout
|
# add new layout
|
||||||
node = self.doc.createElement("Layout")
|
node = etree.SubElement("Layout", element)
|
||||||
for order in layout.order:
|
for order in layout.order:
|
||||||
if order[0] == "Separator":
|
if order[0] == "Separator":
|
||||||
child = self.doc.createElement("Separator")
|
child = etree.SubElement("Separator", node)
|
||||||
node.appendChild(child)
|
|
||||||
elif order[0] == "Filename":
|
elif order[0] == "Filename":
|
||||||
child = self.__addXmlTextElement(node, "Filename", order[1])
|
child = self.__addXmlTextElement(node, "Filename", order[1])
|
||||||
elif order[0] == "Menuname":
|
elif order[0] == "Menuname":
|
||||||
child = self.__addXmlTextElement(node, "Menuname", order[1])
|
child = self.__addXmlTextElement(node, "Menuname", order[1])
|
||||||
elif order[0] == "Merge":
|
elif order[0] == "Merge":
|
||||||
child = self.doc.createElement("Merge")
|
child = etree.SubElement("Merge", node)
|
||||||
child.setAttribute("type", order[1])
|
child.attrib["type"] = order[1]
|
||||||
node.appendChild(child)
|
return node
|
||||||
return element.appendChild(node)
|
|
||||||
|
|
||||||
def __getXmlNodesByName(self, name, element):
|
|
||||||
for child in element.childNodes:
|
|
||||||
if child.nodeType == xml.dom.Node.ELEMENT_NODE and child.nodeName in name:
|
|
||||||
yield child
|
|
||||||
|
|
||||||
def __addLayout(self, parent):
|
def __addLayout(self, parent):
|
||||||
layout = Layout()
|
layout = Layout()
|
||||||
|
@ -498,14 +518,24 @@ class MenuEditor:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __remove_whilespace_nodes(self, node):
|
def __remove_whitespace_nodes(self, node):
|
||||||
remove_list = []
|
for child in node:
|
||||||
for child in node.childNodes:
|
text = child.text.strip()
|
||||||
if child.nodeType == xml.dom.minidom.Node.TEXT_NODE:
|
if not text:
|
||||||
child.data = child.data.strip()
|
child.text = ''
|
||||||
if not child.data.strip():
|
tail = child.tail.strip()
|
||||||
remove_list.append(child)
|
if not tail:
|
||||||
elif child.hasChildNodes():
|
child.tail = ''
|
||||||
|
if len(child):
|
||||||
self.__remove_whilespace_nodes(child)
|
self.__remove_whilespace_nodes(child)
|
||||||
for node in remove_list:
|
|
||||||
node.parentNode.removeChild(node)
|
def __get_parent_node(self, node):
|
||||||
|
# elements in ElementTree doesn't hold a reference to their parent
|
||||||
|
for parent, child in self.__iter_parent():
|
||||||
|
if child is node:
|
||||||
|
return child
|
||||||
|
|
||||||
|
def __iter_parent(self):
|
||||||
|
for parent in self.tree.getiterator():
|
||||||
|
for child in parent:
|
||||||
|
yield parent, child
|
||||||
|
|
700
libs/xdg/Mime.py
700
libs/xdg/Mime.py
|
@ -20,6 +20,7 @@ information about the format of these files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import stat
|
import stat
|
||||||
import sys
|
import sys
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
@ -46,25 +47,42 @@ def _get_node_data(node):
|
||||||
return ''.join([n.nodeValue for n in node.childNodes]).strip()
|
return ''.join([n.nodeValue for n in node.childNodes]).strip()
|
||||||
|
|
||||||
def lookup(media, subtype = None):
|
def lookup(media, subtype = None):
|
||||||
"""Get the MIMEtype object for this type, creating a new one if needed.
|
"""Get the MIMEtype object for the given type.
|
||||||
|
|
||||||
|
This remains for backwards compatibility; calling MIMEtype now does
|
||||||
|
the same thing.
|
||||||
|
|
||||||
The name can either be passed as one part ('text/plain'), or as two
|
The name can either be passed as one part ('text/plain'), or as two
|
||||||
('text', 'plain').
|
('text', 'plain').
|
||||||
"""
|
"""
|
||||||
if subtype is None and '/' in media:
|
return MIMEtype(media, subtype)
|
||||||
media, subtype = media.split('/', 1)
|
|
||||||
if (media, subtype) not in types:
|
|
||||||
types[(media, subtype)] = MIMEtype(media, subtype)
|
|
||||||
return types[(media, subtype)]
|
|
||||||
|
|
||||||
class MIMEtype:
|
class MIMEtype(object):
|
||||||
"""Type holding data about a MIME type"""
|
"""Class holding data about a MIME type.
|
||||||
def __init__(self, media, subtype):
|
|
||||||
"Don't use this constructor directly; use mime.lookup() instead."
|
|
||||||
assert media and '/' not in media
|
|
||||||
assert subtype and '/' not in subtype
|
|
||||||
assert (media, subtype) not in types
|
|
||||||
|
|
||||||
|
Calling the class will return a cached instance, so there is only one
|
||||||
|
instance for each MIME type. The name can either be passed as one part
|
||||||
|
('text/plain'), or as two ('text', 'plain').
|
||||||
|
"""
|
||||||
|
def __new__(cls, media, subtype=None):
|
||||||
|
if subtype is None and '/' in media:
|
||||||
|
media, subtype = media.split('/', 1)
|
||||||
|
assert '/' not in subtype
|
||||||
|
media = media.lower()
|
||||||
|
subtype = subtype.lower()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return types[(media, subtype)]
|
||||||
|
except KeyError:
|
||||||
|
mtype = super(MIMEtype, cls).__new__(cls)
|
||||||
|
mtype._init(media, subtype)
|
||||||
|
types[(media, subtype)] = mtype
|
||||||
|
return mtype
|
||||||
|
|
||||||
|
# If this is done in __init__, it is automatically called again each time
|
||||||
|
# the MIMEtype is returned by __new__, which we don't want. So we call it
|
||||||
|
# explicitly only when we construct a new instance.
|
||||||
|
def _init(self, media, subtype):
|
||||||
self.media = media
|
self.media = media
|
||||||
self.subtype = subtype
|
self.subtype = subtype
|
||||||
self._comment = None
|
self._comment = None
|
||||||
|
@ -109,100 +127,106 @@ class MIMEtype:
|
||||||
return self.media + '/' + self.subtype
|
return self.media + '/' + self.subtype
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<%s: %s>' % (self, self._comment or '(comment not loaded)')
|
return 'MIMEtype(%r, %r)' % (self.media, self.subtype)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.media) ^ hash(self.subtype)
|
||||||
|
|
||||||
|
class UnknownMagicRuleFormat(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DiscardMagicRules(Exception):
|
||||||
|
"Raised when __NOMAGIC__ is found, and caught to discard previous rules."
|
||||||
|
pass
|
||||||
|
|
||||||
class MagicRule:
|
class MagicRule:
|
||||||
def __init__(self, f):
|
also = None
|
||||||
self.next=None
|
|
||||||
self.prev=None
|
|
||||||
|
|
||||||
|
def __init__(self, start, value, mask, word, range):
|
||||||
|
self.start = start
|
||||||
|
self.value = value
|
||||||
|
self.mask = mask
|
||||||
|
self.word = word
|
||||||
|
self.range = range
|
||||||
|
|
||||||
|
rule_ending_re = re.compile(br'(?:~(\d+))?(?:\+(\d+))?\n$')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_file(cls, f):
|
||||||
|
"""Read a rule from the binary magics file. Returns a 2-tuple of
|
||||||
|
the nesting depth and the MagicRule."""
|
||||||
|
line = f.readline()
|
||||||
#print line
|
#print line
|
||||||
ind=b''
|
|
||||||
while True:
|
# [indent] '>'
|
||||||
c=f.read(1)
|
nest_depth, line = line.split(b'>', 1)
|
||||||
if c == b'>':
|
nest_depth = int(nest_depth) if nest_depth else 0
|
||||||
break
|
|
||||||
ind+=c
|
# start-offset '='
|
||||||
if not ind:
|
start, line = line.split(b'=', 1)
|
||||||
self.nest=0
|
start = int(start)
|
||||||
|
|
||||||
|
if line == b'__NOMAGIC__\n':
|
||||||
|
raise DiscardMagicRules
|
||||||
|
|
||||||
|
# value length (2 bytes, big endian)
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
lenvalue = int.from_bytes(line[:2], byteorder='big')
|
||||||
else:
|
else:
|
||||||
self.nest=int(ind.decode('ascii'))
|
lenvalue = (ord(line[0])<<8)+ord(line[1])
|
||||||
|
line = line[2:]
|
||||||
|
|
||||||
start = b''
|
# value
|
||||||
while True:
|
# This can contain newlines, so we may need to read more lines
|
||||||
c = f.read(1)
|
while len(line) <= lenvalue:
|
||||||
if c == b'=':
|
line += f.readline()
|
||||||
break
|
value, line = line[:lenvalue], line[lenvalue:]
|
||||||
start += c
|
|
||||||
self.start = int(start.decode('ascii'))
|
|
||||||
|
|
||||||
hb=f.read(1)
|
# ['&' mask]
|
||||||
lb=f.read(1)
|
if line.startswith(b'&'):
|
||||||
self.lenvalue = ord(lb)+(ord(hb)<<8)
|
# This can contain newlines, so we may need to read more lines
|
||||||
|
while len(line) <= lenvalue:
|
||||||
self.value = f.read(self.lenvalue)
|
line += f.readline()
|
||||||
|
mask, line = line[1:lenvalue+1], line[lenvalue+1:]
|
||||||
c = f.read(1)
|
|
||||||
if c == b'&':
|
|
||||||
self.mask = f.read(self.lenvalue)
|
|
||||||
c = f.read(1)
|
|
||||||
else:
|
else:
|
||||||
self.mask=None
|
mask = None
|
||||||
|
|
||||||
if c == b'~':
|
# ['~' word-size] ['+' range-length]
|
||||||
w = b''
|
ending = cls.rule_ending_re.match(line)
|
||||||
while c!=b'+' and c!=b'\n':
|
if not ending:
|
||||||
c=f.read(1)
|
# Per the spec, this will be caught and ignored, to allow
|
||||||
if c==b'+' or c==b'\n':
|
# for future extensions.
|
||||||
break
|
raise UnknownMagicRuleFormat(repr(line))
|
||||||
w+=c
|
|
||||||
|
|
||||||
self.word=int(w.decode('ascii'))
|
word, range = ending.groups()
|
||||||
else:
|
word = int(word) if (word is not None) else 1
|
||||||
self.word=1
|
range = int(range) if (range is not None) else 1
|
||||||
|
|
||||||
if c==b'+':
|
return nest_depth, cls(start, value, mask, word, range)
|
||||||
r=b''
|
|
||||||
while c!=b'\n':
|
|
||||||
c=f.read(1)
|
|
||||||
if c==b'\n':
|
|
||||||
break
|
|
||||||
r+=c
|
|
||||||
#print r
|
|
||||||
self.range = int(r.decode('ascii'))
|
|
||||||
else:
|
|
||||||
self.range = 1
|
|
||||||
|
|
||||||
if c != b'\n':
|
def maxlen(self):
|
||||||
raise ValueError('Malformed MIME magic line')
|
l = self.start + len(self.value) + self.range
|
||||||
|
if self.also:
|
||||||
def getLength(self):
|
return max(l, self.also.maxlen())
|
||||||
return self.start+self.lenvalue+self.range
|
return l
|
||||||
|
|
||||||
def appendRule(self, rule):
|
|
||||||
if self.nest<rule.nest:
|
|
||||||
self.next=rule
|
|
||||||
rule.prev=self
|
|
||||||
|
|
||||||
elif self.prev:
|
|
||||||
self.prev.appendRule(rule)
|
|
||||||
|
|
||||||
def match(self, buffer):
|
def match(self, buffer):
|
||||||
if self.match0(buffer):
|
if self.match0(buffer):
|
||||||
if self.next:
|
if self.also:
|
||||||
return self.next.match(buffer)
|
return self.also.match(buffer)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def match0(self, buffer):
|
def match0(self, buffer):
|
||||||
l=len(buffer)
|
l=len(buffer)
|
||||||
|
lenvalue = len(self.value)
|
||||||
for o in range(self.range):
|
for o in range(self.range):
|
||||||
s=self.start+o
|
s=self.start+o
|
||||||
e=s+self.lenvalue
|
e=s+lenvalue
|
||||||
if l<e:
|
if l<e:
|
||||||
return False
|
return False
|
||||||
if self.mask:
|
if self.mask:
|
||||||
test=''
|
test=''
|
||||||
for i in range(self.lenvalue):
|
for i in range(lenvalue):
|
||||||
if PY3:
|
if PY3:
|
||||||
c = buffer[s+i] & self.mask[i]
|
c = buffer[s+i] & self.mask[i]
|
||||||
else:
|
else:
|
||||||
|
@ -215,46 +239,81 @@ class MagicRule:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<MagicRule %d>%d=[%d]%r&%r~%d+%d>' % (self.nest,
|
return 'MagicRule(start=%r, value=%r, mask=%r, word=%r, range=%r)' %(
|
||||||
self.start,
|
self.start,
|
||||||
self.lenvalue,
|
|
||||||
self.value,
|
self.value,
|
||||||
self.mask,
|
self.mask,
|
||||||
self.word,
|
self.word,
|
||||||
self.range)
|
self.range)
|
||||||
|
|
||||||
class MagicType:
|
|
||||||
def __init__(self, mtype):
|
|
||||||
self.mtype=mtype
|
|
||||||
self.top_rules=[]
|
|
||||||
self.last_rule=None
|
|
||||||
|
|
||||||
def getLine(self, f):
|
class MagicMatchAny(object):
|
||||||
nrule=MagicRule(f)
|
"""Match any of a set of magic rules.
|
||||||
|
|
||||||
if nrule.nest and self.last_rule:
|
This has a similar interface to MagicRule objects (i.e. its match() and
|
||||||
self.last_rule.appendRule(nrule)
|
maxlen() methods), to allow for duck typing.
|
||||||
else:
|
"""
|
||||||
self.top_rules.append(nrule)
|
def __init__(self, rules):
|
||||||
|
self.rules = rules
|
||||||
self.last_rule=nrule
|
|
||||||
|
|
||||||
return nrule
|
|
||||||
|
|
||||||
def match(self, buffer):
|
def match(self, buffer):
|
||||||
for rule in self.top_rules:
|
return any(r.match(buffer) for r in self.rules)
|
||||||
if rule.match(buffer):
|
|
||||||
return self.mtype
|
|
||||||
|
|
||||||
def __repr__(self):
|
def maxlen(self):
|
||||||
return '<MagicType %s>' % self.mtype
|
return max(r.maxlen() for r in self.rules)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_file(cls, f):
|
||||||
|
"""Read a set of rules from the binary magic file."""
|
||||||
|
c=f.read(1)
|
||||||
|
f.seek(-1, 1)
|
||||||
|
depths_rules = []
|
||||||
|
while c and c != b'[':
|
||||||
|
try:
|
||||||
|
depths_rules.append(MagicRule.from_file(f))
|
||||||
|
except UnknownMagicRuleFormat:
|
||||||
|
# Ignored to allow for extensions to the rule format.
|
||||||
|
pass
|
||||||
|
c=f.read(1)
|
||||||
|
if c:
|
||||||
|
f.seek(-1, 1)
|
||||||
|
|
||||||
|
# Build the rule tree
|
||||||
|
tree = [] # (rule, [(subrule,[subsubrule,...]), ...])
|
||||||
|
insert_points = {0:tree}
|
||||||
|
for depth, rule in depths_rules:
|
||||||
|
subrules = []
|
||||||
|
insert_points[depth].append((rule, subrules))
|
||||||
|
insert_points[depth+1] = subrules
|
||||||
|
|
||||||
|
return cls.from_rule_tree(tree)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_rule_tree(cls, tree):
|
||||||
|
"""From a nested list of (rule, subrules) pairs, build a MagicMatchAny
|
||||||
|
instance, recursing down the tree.
|
||||||
|
|
||||||
|
Where there's only one top-level rule, this is returned directly,
|
||||||
|
to simplify the nested structure. Returns None if no rules were read.
|
||||||
|
"""
|
||||||
|
rules = []
|
||||||
|
for rule, subrules in tree:
|
||||||
|
if subrules:
|
||||||
|
rule.also = cls.from_rule_tree(subrules)
|
||||||
|
rules.append(rule)
|
||||||
|
|
||||||
|
if len(rules)==0:
|
||||||
|
return None
|
||||||
|
if len(rules)==1:
|
||||||
|
return rules[0]
|
||||||
|
return cls(rules)
|
||||||
|
|
||||||
class MagicDB:
|
class MagicDB:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.types={} # Indexed by priority, each entry is a list of type rules
|
self.bytype = defaultdict(list) # mimetype -> [(priority, rule), ...]
|
||||||
self.maxlen=0
|
|
||||||
|
|
||||||
def mergeFile(self, fname):
|
def merge_file(self, fname):
|
||||||
|
"""Read a magic binary file, and add its rules to this MagicDB."""
|
||||||
with open(fname, 'rb') as f:
|
with open(fname, 'rb') as f:
|
||||||
line = f.readline()
|
line = f.readline()
|
||||||
if line != b'MIME-Magic\0\n':
|
if line != b'MIME-Magic\0\n':
|
||||||
|
@ -262,68 +321,210 @@ class MagicDB:
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
shead = f.readline().decode('ascii')
|
shead = f.readline().decode('ascii')
|
||||||
#print shead
|
#print(shead)
|
||||||
if not shead:
|
if not shead:
|
||||||
break
|
break
|
||||||
if shead[0] != '[' or shead[-2:] != ']\n':
|
if shead[0] != '[' or shead[-2:] != ']\n':
|
||||||
raise ValueError('Malformed section heading')
|
raise ValueError('Malformed section heading', shead)
|
||||||
pri, tname = shead[1:-2].split(':')
|
pri, tname = shead[1:-2].split(':')
|
||||||
#print shead[1:-2]
|
#print shead[1:-2]
|
||||||
pri = int(pri)
|
pri = int(pri)
|
||||||
mtype = lookup(tname)
|
mtype = lookup(tname)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ents = self.types[pri]
|
rule = MagicMatchAny.from_file(f)
|
||||||
except:
|
except DiscardMagicRules:
|
||||||
ents = []
|
self.bytype.pop(mtype, None)
|
||||||
self.types[pri] = ents
|
rule = MagicMatchAny.from_file(f)
|
||||||
|
if rule is None:
|
||||||
|
continue
|
||||||
|
#print rule
|
||||||
|
|
||||||
magictype = MagicType(mtype)
|
self.bytype[mtype].append((pri, rule))
|
||||||
#print tname
|
|
||||||
|
|
||||||
#rline=f.readline()
|
def finalise(self):
|
||||||
c=f.read(1)
|
"""Prepare the MagicDB for matching.
|
||||||
f.seek(-1, 1)
|
|
||||||
while c and c != b'[':
|
|
||||||
rule=magictype.getLine(f)
|
|
||||||
#print rule
|
|
||||||
if rule and rule.getLength() > self.maxlen:
|
|
||||||
self.maxlen = rule.getLength()
|
|
||||||
|
|
||||||
c = f.read(1)
|
This should be called after all rules have been merged into it.
|
||||||
f.seek(-1, 1)
|
"""
|
||||||
|
maxlen = 0
|
||||||
|
self.alltypes = [] # (priority, mimetype, rule)
|
||||||
|
|
||||||
ents.append(magictype)
|
for mtype, rules in self.bytype.items():
|
||||||
#self.types[pri]=ents
|
for pri, rule in rules:
|
||||||
if not c:
|
self.alltypes.append((pri, mtype, rule))
|
||||||
break
|
maxlen = max(maxlen, rule.maxlen())
|
||||||
|
|
||||||
def match_data(self, data, max_pri=100, min_pri=0):
|
self.maxlen = maxlen # Number of bytes to read from files
|
||||||
for priority in sorted(self.types.keys(), reverse=True):
|
self.alltypes.sort(key=lambda x: x[0], reverse=True)
|
||||||
|
|
||||||
|
def match_data(self, data, max_pri=100, min_pri=0, possible=None):
|
||||||
|
"""Do magic sniffing on some bytes.
|
||||||
|
|
||||||
|
max_pri & min_pri can be used to specify the maximum & minimum priority
|
||||||
|
rules to look for. possible can be a list of mimetypes to check, or None
|
||||||
|
(the default) to check all mimetypes until one matches.
|
||||||
|
|
||||||
|
Returns the MIMEtype found, or None if no entries match.
|
||||||
|
"""
|
||||||
|
if possible is not None:
|
||||||
|
types = []
|
||||||
|
for mt in possible:
|
||||||
|
for pri, rule in self.bytype[mt]:
|
||||||
|
types.append((pri, mt, rule))
|
||||||
|
types.sort(key=lambda x: x[0])
|
||||||
|
else:
|
||||||
|
types = self.alltypes
|
||||||
|
|
||||||
|
for priority, mimetype, rule in types:
|
||||||
#print priority, max_pri, min_pri
|
#print priority, max_pri, min_pri
|
||||||
if priority > max_pri:
|
if priority > max_pri:
|
||||||
continue
|
continue
|
||||||
if priority < min_pri:
|
if priority < min_pri:
|
||||||
break
|
break
|
||||||
for type in self.types[priority]:
|
|
||||||
m=type.match(data)
|
|
||||||
if m:
|
|
||||||
return m
|
|
||||||
|
|
||||||
def match(self, path, max_pri=100, min_pri=0):
|
if rule.match(data):
|
||||||
try:
|
return mimetype
|
||||||
with open(path, 'rb') as f:
|
|
||||||
buf = f.read(self.maxlen)
|
def match(self, path, max_pri=100, min_pri=0, possible=None):
|
||||||
return self.match_data(buf, max_pri, min_pri)
|
"""Read data from the file and do magic sniffing on it.
|
||||||
except:
|
|
||||||
pass
|
max_pri & min_pri can be used to specify the maximum & minimum priority
|
||||||
|
rules to look for. possible can be a list of mimetypes to check, or None
|
||||||
|
(the default) to check all mimetypes until one matches.
|
||||||
|
|
||||||
|
Returns the MIMEtype found, or None if no entries match. Raises IOError
|
||||||
|
if the file can't be opened.
|
||||||
|
"""
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
buf = f.read(self.maxlen)
|
||||||
|
return self.match_data(buf, max_pri, min_pri, possible)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<MagicDB %s>' % self.types
|
return '<MagicDB (%d types)>' % len(self.alltypes)
|
||||||
|
|
||||||
|
class GlobDB(object):
|
||||||
|
def __init__(self):
|
||||||
|
"""Prepare the GlobDB. It can't actually be used until .finalise() is
|
||||||
|
called, but merge_file() can be used to add data before that.
|
||||||
|
"""
|
||||||
|
# Maps mimetype to {(weight, glob, flags), ...}
|
||||||
|
self.allglobs = defaultdict(set)
|
||||||
|
|
||||||
|
def merge_file(self, path):
|
||||||
|
"""Loads name matching information from a globs2 file."""#
|
||||||
|
allglobs = self.allglobs
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith('#'): continue # Comment
|
||||||
|
|
||||||
|
fields = line[:-1].split(':')
|
||||||
|
weight, type_name, pattern = fields[:3]
|
||||||
|
weight = int(weight)
|
||||||
|
mtype = lookup(type_name)
|
||||||
|
if len(fields) > 3:
|
||||||
|
flags = fields[3].split(',')
|
||||||
|
else:
|
||||||
|
flags = ()
|
||||||
|
|
||||||
|
if pattern == '__NOGLOBS__':
|
||||||
|
# This signals to discard any previous globs
|
||||||
|
allglobs.pop(mtype, None)
|
||||||
|
continue
|
||||||
|
|
||||||
|
allglobs[mtype].add((weight, pattern, tuple(flags)))
|
||||||
|
|
||||||
|
def finalise(self):
|
||||||
|
"""Prepare the GlobDB for matching.
|
||||||
|
|
||||||
|
This should be called after all files have been merged into it.
|
||||||
|
"""
|
||||||
|
self.exts = defaultdict(list) # Maps extensions to [(type, weight),...]
|
||||||
|
self.cased_exts = defaultdict(list)
|
||||||
|
self.globs = [] # List of (regex, type, weight) triplets
|
||||||
|
self.literals = {} # Maps literal names to (type, weight)
|
||||||
|
self.cased_literals = {}
|
||||||
|
|
||||||
|
for mtype, globs in self.allglobs.items():
|
||||||
|
mtype = mtype.canonical()
|
||||||
|
for weight, pattern, flags in globs:
|
||||||
|
|
||||||
|
cased = 'cs' in flags
|
||||||
|
|
||||||
|
if pattern.startswith('*.'):
|
||||||
|
# *.foo -- extension pattern
|
||||||
|
rest = pattern[2:]
|
||||||
|
if not ('*' in rest or '[' in rest or '?' in rest):
|
||||||
|
if cased:
|
||||||
|
self.cased_exts[rest].append((mtype, weight))
|
||||||
|
else:
|
||||||
|
self.exts[rest.lower()].append((mtype, weight))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ('*' in pattern or '[' in pattern or '?' in pattern):
|
||||||
|
# Translate the glob pattern to a regex & compile it
|
||||||
|
re_flags = 0 if cased else re.I
|
||||||
|
pattern = re.compile(fnmatch.translate(pattern), flags=re_flags)
|
||||||
|
self.globs.append((pattern, mtype, weight))
|
||||||
|
else:
|
||||||
|
# No wildcards - literal pattern
|
||||||
|
if cased:
|
||||||
|
self.cased_literals[pattern] = (mtype, weight)
|
||||||
|
else:
|
||||||
|
self.literals[pattern.lower()] = (mtype, weight)
|
||||||
|
|
||||||
|
# Sort globs by weight & length
|
||||||
|
self.globs.sort(reverse=True, key=lambda x: (x[2], len(x[0].pattern)) )
|
||||||
|
|
||||||
|
def first_match(self, path):
|
||||||
|
"""Return the first match found for a given path, or None if no match
|
||||||
|
is found."""
|
||||||
|
try:
|
||||||
|
return next(self._match_path(path))[0]
|
||||||
|
except StopIteration:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def all_matches(self, path):
|
||||||
|
"""Return a list of (MIMEtype, glob weight) pairs for the path."""
|
||||||
|
return list(self._match_path(path))
|
||||||
|
|
||||||
|
def _match_path(self, path):
|
||||||
|
"""Yields pairs of (mimetype, glob weight)."""
|
||||||
|
leaf = os.path.basename(path)
|
||||||
|
|
||||||
|
# Literals (no wildcards)
|
||||||
|
if leaf in self.cased_literals:
|
||||||
|
yield self.cased_literals[leaf]
|
||||||
|
|
||||||
|
lleaf = leaf.lower()
|
||||||
|
if lleaf in self.literals:
|
||||||
|
yield self.literals[lleaf]
|
||||||
|
|
||||||
|
# Extensions
|
||||||
|
ext = leaf
|
||||||
|
while 1:
|
||||||
|
p = ext.find('.')
|
||||||
|
if p < 0: break
|
||||||
|
ext = ext[p + 1:]
|
||||||
|
if ext in self.cased_exts:
|
||||||
|
for res in self.cased_exts[ext]:
|
||||||
|
yield res
|
||||||
|
ext = lleaf
|
||||||
|
while 1:
|
||||||
|
p = ext.find('.')
|
||||||
|
if p < 0: break
|
||||||
|
ext = ext[p+1:]
|
||||||
|
if ext in self.exts:
|
||||||
|
for res in self.exts[ext]:
|
||||||
|
yield res
|
||||||
|
|
||||||
|
# Other globs
|
||||||
|
for (regex, mime_type, weight) in self.globs:
|
||||||
|
if regex.match(leaf):
|
||||||
|
yield (mime_type, weight)
|
||||||
|
|
||||||
# Some well-known types
|
# Some well-known types
|
||||||
text = lookup('text', 'plain')
|
text = lookup('text', 'plain')
|
||||||
|
octet_stream = lookup('application', 'octet-stream')
|
||||||
inode_block = lookup('inode', 'blockdevice')
|
inode_block = lookup('inode', 'blockdevice')
|
||||||
inode_char = lookup('inode', 'chardevice')
|
inode_char = lookup('inode', 'chardevice')
|
||||||
inode_dir = lookup('inode', 'directory')
|
inode_dir = lookup('inode', 'directory')
|
||||||
|
@ -336,44 +537,12 @@ app_exe = lookup('application', 'executable')
|
||||||
_cache_uptodate = False
|
_cache_uptodate = False
|
||||||
|
|
||||||
def _cache_database():
|
def _cache_database():
|
||||||
global exts, globs, literals, magic, aliases, inheritance, _cache_uptodate
|
global globs, magic, aliases, inheritance, _cache_uptodate
|
||||||
|
|
||||||
_cache_uptodate = True
|
_cache_uptodate = True
|
||||||
|
|
||||||
exts = {} # Maps extensions to types
|
|
||||||
globs = [] # List of (glob, type) pairs
|
|
||||||
literals = {} # Maps literal names to types
|
|
||||||
aliases = {} # Maps alias Mime types to canonical names
|
aliases = {} # Maps alias Mime types to canonical names
|
||||||
inheritance = defaultdict(set) # Maps to sets of parent mime types.
|
inheritance = defaultdict(set) # Maps to sets of parent mime types.
|
||||||
magic = MagicDB()
|
|
||||||
|
|
||||||
def _import_glob_file(path):
|
|
||||||
"""Loads name matching information from a MIME directory."""
|
|
||||||
with open(path) as f:
|
|
||||||
for line in f:
|
|
||||||
if line.startswith('#'): continue
|
|
||||||
line = line[:-1]
|
|
||||||
|
|
||||||
type_name, pattern = line.split(':', 1)
|
|
||||||
mtype = lookup(type_name)
|
|
||||||
|
|
||||||
if pattern.startswith('*.'):
|
|
||||||
rest = pattern[2:]
|
|
||||||
if not ('*' in rest or '[' in rest or '?' in rest):
|
|
||||||
exts[rest] = mtype
|
|
||||||
continue
|
|
||||||
if '*' in pattern or '[' in pattern or '?' in pattern:
|
|
||||||
globs.append((pattern, mtype))
|
|
||||||
else:
|
|
||||||
literals[pattern] = mtype
|
|
||||||
|
|
||||||
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'globs')):
|
|
||||||
_import_glob_file(path)
|
|
||||||
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'magic')):
|
|
||||||
magic.mergeFile(path)
|
|
||||||
|
|
||||||
# Sort globs by length
|
|
||||||
globs.sort(key=lambda x: len(x[0]) )
|
|
||||||
|
|
||||||
# Load aliases
|
# Load aliases
|
||||||
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'aliases')):
|
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'aliases')):
|
||||||
|
@ -382,6 +551,18 @@ def _cache_database():
|
||||||
alias, canonical = line.strip().split(None, 1)
|
alias, canonical = line.strip().split(None, 1)
|
||||||
aliases[alias] = canonical
|
aliases[alias] = canonical
|
||||||
|
|
||||||
|
# Load filename patterns (globs)
|
||||||
|
globs = GlobDB()
|
||||||
|
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'globs2')):
|
||||||
|
globs.merge_file(path)
|
||||||
|
globs.finalise()
|
||||||
|
|
||||||
|
# Load magic sniffing data
|
||||||
|
magic = MagicDB()
|
||||||
|
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'magic')):
|
||||||
|
magic.merge_file(path)
|
||||||
|
magic.finalise()
|
||||||
|
|
||||||
# Load subclasses
|
# Load subclasses
|
||||||
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')):
|
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')):
|
||||||
with open(path, 'r') as f:
|
with open(path, 'r') as f:
|
||||||
|
@ -396,35 +577,7 @@ def update_cache():
|
||||||
def get_type_by_name(path):
|
def get_type_by_name(path):
|
||||||
"""Returns type of file by its name, or None if not known"""
|
"""Returns type of file by its name, or None if not known"""
|
||||||
update_cache()
|
update_cache()
|
||||||
|
return globs.first_match(path)
|
||||||
leaf = os.path.basename(path)
|
|
||||||
if leaf in literals:
|
|
||||||
return literals[leaf]
|
|
||||||
|
|
||||||
lleaf = leaf.lower()
|
|
||||||
if lleaf in literals:
|
|
||||||
return literals[lleaf]
|
|
||||||
|
|
||||||
ext = leaf
|
|
||||||
while 1:
|
|
||||||
p = ext.find('.')
|
|
||||||
if p < 0: break
|
|
||||||
ext = ext[p + 1:]
|
|
||||||
if ext in exts:
|
|
||||||
return exts[ext]
|
|
||||||
ext = lleaf
|
|
||||||
while 1:
|
|
||||||
p = ext.find('.')
|
|
||||||
if p < 0: break
|
|
||||||
ext = ext[p+1:]
|
|
||||||
if ext in exts:
|
|
||||||
return exts[ext]
|
|
||||||
for (glob, mime_type) in globs:
|
|
||||||
if fnmatch.fnmatch(leaf, glob):
|
|
||||||
return mime_type
|
|
||||||
if fnmatch.fnmatch(lleaf, glob):
|
|
||||||
return mime_type
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_type_by_contents(path, max_pri=100, min_pri=0):
|
def get_type_by_contents(path, max_pri=100, min_pri=0):
|
||||||
"""Returns type of file by its contents, or None if not known"""
|
"""Returns type of file by its contents, or None if not known"""
|
||||||
|
@ -438,15 +591,24 @@ def get_type_by_data(data, max_pri=100, min_pri=0):
|
||||||
|
|
||||||
return magic.match_data(data, max_pri, min_pri)
|
return magic.match_data(data, max_pri, min_pri)
|
||||||
|
|
||||||
|
def _get_type_by_stat(st_mode):
|
||||||
|
"""Match special filesystem objects to Mimetypes."""
|
||||||
|
if stat.S_ISDIR(st_mode): return inode_dir
|
||||||
|
elif stat.S_ISCHR(st_mode): return inode_char
|
||||||
|
elif stat.S_ISBLK(st_mode): return inode_block
|
||||||
|
elif stat.S_ISFIFO(st_mode): return inode_fifo
|
||||||
|
elif stat.S_ISLNK(st_mode): return inode_symlink
|
||||||
|
elif stat.S_ISSOCK(st_mode): return inode_socket
|
||||||
|
return inode_door
|
||||||
|
|
||||||
def get_type(path, follow=True, name_pri=100):
|
def get_type(path, follow=True, name_pri=100):
|
||||||
"""Returns type of file indicated by path.
|
"""Returns type of file indicated by path.
|
||||||
|
|
||||||
path :
|
This function is *deprecated* - :func:`get_type2` is more accurate.
|
||||||
pathname to check (need not exist)
|
|
||||||
follow :
|
:param path: pathname to check (need not exist)
|
||||||
when reading file, follow symbolic links
|
:param follow: when reading file, follow symbolic links
|
||||||
name_pri :
|
:param name_pri: Priority to do name matches. 100=override magic
|
||||||
Priority to do name matches. 100=override magic
|
|
||||||
|
|
||||||
This tries to use the contents of the file, and falls back to the name. It
|
This tries to use the contents of the file, and falls back to the name. It
|
||||||
can also handle special filesystem objects like directories and sockets.
|
can also handle special filesystem objects like directories and sockets.
|
||||||
|
@ -463,6 +625,7 @@ def get_type(path, follow=True, name_pri=100):
|
||||||
return t or text
|
return t or text
|
||||||
|
|
||||||
if stat.S_ISREG(st.st_mode):
|
if stat.S_ISREG(st.st_mode):
|
||||||
|
# Regular file
|
||||||
t = get_type_by_contents(path, min_pri=name_pri)
|
t = get_type_by_contents(path, min_pri=name_pri)
|
||||||
if not t: t = get_type_by_name(path)
|
if not t: t = get_type_by_name(path)
|
||||||
if not t: t = get_type_by_contents(path, max_pri=name_pri)
|
if not t: t = get_type_by_contents(path, max_pri=name_pri)
|
||||||
|
@ -472,13 +635,112 @@ def get_type(path, follow=True, name_pri=100):
|
||||||
else:
|
else:
|
||||||
return text
|
return text
|
||||||
return t
|
return t
|
||||||
elif stat.S_ISDIR(st.st_mode): return inode_dir
|
else:
|
||||||
elif stat.S_ISCHR(st.st_mode): return inode_char
|
return _get_type_by_stat(st.st_mode)
|
||||||
elif stat.S_ISBLK(st.st_mode): return inode_block
|
|
||||||
elif stat.S_ISFIFO(st.st_mode): return inode_fifo
|
def get_type2(path, follow=True):
|
||||||
elif stat.S_ISLNK(st.st_mode): return inode_symlink
|
"""Find the MIMEtype of a file using the XDG recommended checking order.
|
||||||
elif stat.S_ISSOCK(st.st_mode): return inode_socket
|
|
||||||
return inode_door
|
This first checks the filename, then uses file contents if the name doesn't
|
||||||
|
give an unambiguous MIMEtype. It can also handle special filesystem objects
|
||||||
|
like directories and sockets.
|
||||||
|
|
||||||
|
:param path: file path to examine (need not exist)
|
||||||
|
:param follow: whether to follow symlinks
|
||||||
|
|
||||||
|
:rtype: :class:`MIMEtype`
|
||||||
|
|
||||||
|
.. versionadded:: 1.0
|
||||||
|
"""
|
||||||
|
update_cache()
|
||||||
|
|
||||||
|
try:
|
||||||
|
st = os.stat(path) if follow else os.lstat(path)
|
||||||
|
except OSError:
|
||||||
|
return get_type_by_name(path) or octet_stream
|
||||||
|
|
||||||
|
if not stat.S_ISREG(st.st_mode):
|
||||||
|
# Special filesystem objects
|
||||||
|
return _get_type_by_stat(st.st_mode)
|
||||||
|
|
||||||
|
mtypes = sorted(globs.all_matches(path), key=(lambda x: x[1]), reverse=True)
|
||||||
|
if mtypes:
|
||||||
|
max_weight = mtypes[0][1]
|
||||||
|
i = 1
|
||||||
|
for mt, w in mtypes[1:]:
|
||||||
|
if w < max_weight:
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
mtypes = mtypes[:i]
|
||||||
|
if len(mtypes) == 1:
|
||||||
|
return mtypes[0][0]
|
||||||
|
|
||||||
|
possible = [mt for mt,w in mtypes]
|
||||||
|
else:
|
||||||
|
possible = None # Try all magic matches
|
||||||
|
|
||||||
|
try:
|
||||||
|
t = magic.match(path, possible=possible)
|
||||||
|
except IOError:
|
||||||
|
t = None
|
||||||
|
|
||||||
|
if t:
|
||||||
|
return t
|
||||||
|
elif mtypes:
|
||||||
|
return mtypes[0][0]
|
||||||
|
elif stat.S_IMODE(st.st_mode) & 0o111:
|
||||||
|
return app_exe
|
||||||
|
else:
|
||||||
|
return text if is_text_file(path) else octet_stream
|
||||||
|
|
||||||
|
def is_text_file(path):
|
||||||
|
"""Guess whether a file contains text or binary data.
|
||||||
|
|
||||||
|
Heuristic: binary if the first 32 bytes include ASCII control characters.
|
||||||
|
This rule may change in future versions.
|
||||||
|
|
||||||
|
.. versionadded:: 1.0
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
f = open(path, 'rb')
|
||||||
|
except IOError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
with f:
|
||||||
|
return _is_text(f.read(32))
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
def _is_text(data):
|
||||||
|
return not any(b <= 0x8 or 0xe <= b < 0x20 or b == 0x7f for b in data)
|
||||||
|
else:
|
||||||
|
def _is_text(data):
|
||||||
|
return not any(b <= '\x08' or '\x0e' <= b < '\x20' or b == '\x7f' \
|
||||||
|
for b in data)
|
||||||
|
|
||||||
|
_mime2ext_cache = None
|
||||||
|
_mime2ext_cache_uptodate = False
|
||||||
|
|
||||||
|
def get_extensions(mimetype):
|
||||||
|
"""Retrieve the set of filename extensions matching a given MIMEtype.
|
||||||
|
|
||||||
|
Extensions are returned without a leading dot, e.g. 'py'. If no extensions
|
||||||
|
are registered for the MIMEtype, returns an empty set.
|
||||||
|
|
||||||
|
The extensions are stored in a cache the first time this is called.
|
||||||
|
|
||||||
|
.. versionadded:: 1.0
|
||||||
|
"""
|
||||||
|
global _mime2ext_cache, _mime2ext_cache_uptodate
|
||||||
|
update_cache()
|
||||||
|
if not _mime2ext_cache_uptodate:
|
||||||
|
_mime2ext_cache = defaultdict(set)
|
||||||
|
for ext, mtypes in globs.exts.items():
|
||||||
|
for mtype, prio in mtypes:
|
||||||
|
_mime2ext_cache[mtype].add(ext)
|
||||||
|
_mime2ext_cache_uptodate = True
|
||||||
|
|
||||||
|
return _mime2ext_cache[mimetype]
|
||||||
|
|
||||||
|
|
||||||
def install_mime_info(application, package_file):
|
def install_mime_info(application, package_file):
|
||||||
"""Copy 'package_file' as ``~/.local/share/mime/packages/<application>.xml.``
|
"""Copy 'package_file' as ``~/.local/share/mime/packages/<application>.xml.``
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Implementation of the XDG Recent File Storage Specification Version 0.2
|
Implementation of the XDG Recent File Storage Specification
|
||||||
http://standards.freedesktop.org/recent-file-spec
|
http://standards.freedesktop.org/recent-file-spec
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ]
|
__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ]
|
||||||
|
|
||||||
__version__ = "0.25"
|
__version__ = "0.26"
|
||||||
|
|
|
@ -9,3 +9,67 @@ else:
|
||||||
# Unicode-like literals
|
# Unicode-like literals
|
||||||
def u(s):
|
def u(s):
|
||||||
return s.decode('utf-8')
|
return s.decode('utf-8')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# which() is available from Python 3.3
|
||||||
|
from shutil import which
|
||||||
|
except ImportError:
|
||||||
|
import os
|
||||||
|
# This is a copy of which() from Python 3.3
|
||||||
|
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
||||||
|
"""Given a command, mode, and a PATH string, return the path which
|
||||||
|
conforms to the given mode on the PATH, or None if there is no such
|
||||||
|
file.
|
||||||
|
|
||||||
|
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
|
||||||
|
of os.environ.get("PATH"), or can be overridden with a custom search
|
||||||
|
path.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Check that a given file can be accessed with the correct mode.
|
||||||
|
# Additionally check that `file` is not a directory, as on Windows
|
||||||
|
# directories pass the os.access check.
|
||||||
|
def _access_check(fn, mode):
|
||||||
|
return (os.path.exists(fn) and os.access(fn, mode)
|
||||||
|
and not os.path.isdir(fn))
|
||||||
|
|
||||||
|
# If we're given a path with a directory part, look it up directly rather
|
||||||
|
# than referring to PATH directories. This includes checking relative to the
|
||||||
|
# current directory, e.g. ./script
|
||||||
|
if os.path.dirname(cmd):
|
||||||
|
if _access_check(cmd, mode):
|
||||||
|
return cmd
|
||||||
|
return None
|
||||||
|
|
||||||
|
path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep)
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# The current directory takes precedence on Windows.
|
||||||
|
if not os.curdir in path:
|
||||||
|
path.insert(0, os.curdir)
|
||||||
|
|
||||||
|
# PATHEXT is necessary to check on Windows.
|
||||||
|
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
|
||||||
|
# See if the given file matches any of the expected path extensions.
|
||||||
|
# This will allow us to short circuit when given "python.exe".
|
||||||
|
# If it does match, only test that one, otherwise we have to try
|
||||||
|
# others.
|
||||||
|
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
|
||||||
|
files = [cmd]
|
||||||
|
else:
|
||||||
|
files = [cmd + ext for ext in pathext]
|
||||||
|
else:
|
||||||
|
# On other platforms you don't have things like PATHEXT to tell you
|
||||||
|
# what file suffixes are executable, so just pass on cmd as-is.
|
||||||
|
files = [cmd]
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
for dir in path:
|
||||||
|
normdir = os.path.normcase(dir)
|
||||||
|
if not normdir in seen:
|
||||||
|
seen.add(normdir)
|
||||||
|
for thefile in files:
|
||||||
|
name = os.path.join(dir, thefile)
|
||||||
|
if _access_check(name, mode):
|
||||||
|
return name
|
||||||
|
return None
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue