| # Copyright 2010-2015 Gentoo Foundation |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| """Portability shim for xattr support |
| |
| Exported API is the xattr object with get/get_all/set/remove/list operations. |
| We do not include the functions that Python 3.3+ provides in the os module as |
| the signature there is different compared to xattr. |
| |
| See the standard xattr module for more documentation: |
| https://pypi.python.org/pypi/pyxattr |
| """ |
| |
| from __future__ import print_function |
| |
| import contextlib |
| import os |
| import subprocess |
| |
| from portage.exception import OperationNotSupported |
| |
| |
| class _XattrGetAll(object): |
| """Implement get_all() using list()/get() if there is no easy bulk method""" |
| |
| @classmethod |
| def get_all(cls, item, nofollow=False, namespace=None): |
| return [(name, cls.get(item, name, nofollow=nofollow, namespace=namespace)) |
| for name in cls.list(item, nofollow=nofollow, namespace=namespace)] |
| |
| |
| class _XattrSystemCommands(_XattrGetAll): |
| """Implement things with getfattr/setfattr""" |
| |
| @staticmethod |
| def _parse_output(output): |
| for line in output.readlines(): |
| if line.startswith(b'#'): |
| continue |
| line = line.rstrip() |
| if not line: |
| continue |
| # The lines will have the format: |
| # user.hex=0x12345 |
| # user.base64=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA= |
| # user.string="value0" |
| # But since we don't do interpretation on the value (we just |
| # save & restore it), don't bother with decoding here. |
| yield line.split(b'=', 1) |
| |
| @staticmethod |
| def _call(*args, **kwargs): |
| proc = subprocess.Popen(*args, **kwargs) |
| if proc.stdin: |
| proc.stdin.close() |
| proc.wait() |
| return proc |
| |
| @classmethod |
| def get(cls, item, name, nofollow=False, namespace=None): |
| if namespace: |
| name = '%s.%s' % (namespace, name) |
| cmd = ['getfattr', '--absolute-names', '-n', name, item] |
| if nofollow: |
| cmd += ['-h'] |
| proc = cls._call(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| |
| value = None |
| for _, value in cls._parse_output(proc.stdout): |
| break |
| |
| proc.stdout.close() |
| return value |
| |
| @classmethod |
| def set(cls, item, name, value, _flags=0, namespace=None): |
| if namespace: |
| name = '%s.%s' % (namespace, name) |
| cmd = ['setfattr', '-n', name, '-v', value, item] |
| cls._call(cmd) |
| |
| @classmethod |
| def remove(cls, item, name, nofollow=False, namespace=None): |
| if namespace: |
| name = '%s.%s' % (namespace, name) |
| cmd = ['setfattr', '-x', name, item] |
| if nofollow: |
| cmd += ['-h'] |
| cls._call(cmd) |
| |
| @classmethod |
| def list(cls, item, nofollow=False, namespace=None, _names_only=True): |
| cmd = ['getfattr', '-d', '--absolute-names', item] |
| if nofollow: |
| cmd += ['-h'] |
| cmd += ['-m', ('^%s[.]' % namespace) if namespace else '-'] |
| proc = cls._call(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| |
| ret = [] |
| if namespace: |
| namespace = '%s.' % namespace |
| for name, value in cls._parse_output(proc.stdout): |
| if namespace: |
| if name.startswith(namespace): |
| name = name[len(namespace):] |
| else: |
| continue |
| if _names_only: |
| ret.append(name) |
| else: |
| ret.append((name, value)) |
| |
| proc.stdout.close() |
| return ret |
| |
| @classmethod |
| def get_all(cls, item, nofollow=False, namespace=None): |
| return cls.list(item, nofollow=nofollow, namespace=namespace, |
| _names_only=False) |
| |
| |
| class _XattrStub(_XattrGetAll): |
| """Fake object since system doesn't support xattrs""" |
| |
| # pylint: disable=unused-argument |
| |
| @staticmethod |
| def _raise(): |
| e = OSError('stub') |
| e.errno = OperationNotSupported.errno |
| raise e |
| |
| @classmethod |
| def get(cls, item, name, nofollow=False, namespace=None): |
| cls._raise() |
| |
| @classmethod |
| def set(cls, item, name, value, flags=0, namespace=None): |
| cls._raise() |
| |
| @classmethod |
| def remove(cls, item, name, nofollow=False, namespace=None): |
| cls._raise() |
| |
| @classmethod |
| def list(cls, item, nofollow=False, namespace=None): |
| cls._raise() |
| |
| |
| if hasattr(os, 'getxattr'): |
| # Easy as pie -- active python supports it. |
| class xattr(_XattrGetAll): |
| """Python >=3.3 and GNU/Linux""" |
| |
| # pylint: disable=unused-argument |
| |
| @staticmethod |
| def get(item, name, nofollow=False, namespace=None): |
| return os.getxattr(item, name, follow_symlinks=not nofollow) |
| |
| @staticmethod |
| def set(item, name, value, flags=0, namespace=None): |
| return os.setxattr(item, name, value, flags=flags) |
| |
| @staticmethod |
| def remove(item, name, nofollow=False, namespace=None): |
| return os.removexattr(item, name, follow_symlinks=not nofollow) |
| |
| @staticmethod |
| def list(item, nofollow=False, namespace=None): |
| return os.listxattr(item, follow_symlinks=not nofollow) |
| |
| else: |
| try: |
| # Maybe we have the xattr module. |
| import xattr |
| |
| except ImportError: |
| try: |
| # Maybe we have the attr package. |
| with open(os.devnull, 'wb') as f: |
| subprocess.call(['getfattr', '--version'], stdout=f) |
| subprocess.call(['setfattr', '--version'], stdout=f) |
| xattr = _XattrSystemCommands |
| |
| except OSError: |
| # Stub it out completely. |
| xattr = _XattrStub |
| |
| |
| # Add a knob so code can take evasive action as needed. |
| XATTRS_WORKS = xattr != _XattrStub |
| |
| |
| @contextlib.contextmanager |
| def preserve_xattrs(path, nofollow=False, namespace=None): |
| """Context manager to save/restore extended attributes on |path| |
| |
| If you want to rewrite a file (possibly replacing it with a new one), but |
| want to preserve the extended attributes, this will do the trick. |
| |
| # First read all the extended attributes. |
| with save_xattrs('/some/file'): |
| ... rewrite the file ... |
| # Now the extended attributes are restored as needed. |
| """ |
| kwargs = {'nofollow': nofollow,} |
| if namespace: |
| # Compiled xattr python module does not like it when namespace=None. |
| kwargs['namespace'] = namespace |
| |
| old_attrs = dict(xattr.get_all(path, **kwargs)) |
| try: |
| yield |
| finally: |
| new_attrs = dict(xattr.get_all(path, **kwargs)) |
| for name, value in new_attrs.items(): |
| if name not in old_attrs: |
| # Clear out new ones. |
| xattr.remove(path, name, **kwargs) |
| elif new_attrs[name] != old_attrs[name]: |
| # Update changed ones. |
| xattr.set(path, name, value, **kwargs) |
| |
| for name, value in old_attrs.items(): |
| if name not in new_attrs: |
| # Re-add missing ones. |
| xattr.set(path, name, value, **kwargs) |