blob: 9a8704d702561ba217640e7cd99b0b45f2e329fb [file] [log] [blame]
# 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)