# 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)