1 |
Rather than each module implementing its own shim around the various |
2 |
methods for accessing extended attributes, start a dedicated module |
3 |
that exports a consistent API. |
4 |
--- |
5 |
v4 |
6 |
- merge in recent quickpkg changes |
7 |
- add a XATTRS_WORKS symbol for easy testing |
8 |
- use - with -m when matching all |
9 |
|
10 |
bin/quickpkg | 12 +- |
11 |
bin/xattr-helper.py | 11 +- |
12 |
pym/portage/dbapi/vartree.py | 10 +- |
13 |
pym/portage/tests/util/test_xattr.py | 178 +++++++++++++++++++++++++++ |
14 |
pym/portage/util/_xattr.py | 228 +++++++++++++++++++++++++++++++++++ |
15 |
pym/portage/util/movefile.py | 100 ++++----------- |
16 |
pym/portage/util/xattr.py | 20 --- |
17 |
7 files changed, 441 insertions(+), 118 deletions(-) |
18 |
create mode 100644 pym/portage/tests/util/test_xattr.py |
19 |
create mode 100644 pym/portage/util/_xattr.py |
20 |
delete mode 100644 pym/portage/util/xattr.py |
21 |
|
22 |
diff --git a/bin/quickpkg b/bin/quickpkg |
23 |
index 726abff..262fda4 100755 |
24 |
--- a/bin/quickpkg |
25 |
+++ b/bin/quickpkg |
26 |
@@ -21,8 +21,8 @@ from portage.dbapi.dep_expand import dep_expand |
27 |
from portage.dep import Atom, use_reduce |
28 |
from portage.exception import (AmbiguousPackageName, InvalidAtom, InvalidData, |
29 |
InvalidDependString, PackageSetNotFound, PermissionDenied) |
30 |
-from portage.util import ConfigProtect, ensure_dirs, shlex_split |
31 |
-import portage.util.xattr as _xattr |
32 |
+from portage.util import ConfigProtect, ensure_dirs, shlex_split, _xattr |
33 |
+xattr = _xattr.xattr |
34 |
from portage.dbapi.vartree import dblink, tar_contents |
35 |
from portage.checksum import perform_md5 |
36 |
from portage._sets import load_default_config, SETPREFIX |
37 |
@@ -36,7 +36,7 @@ def quickpkg_atom(options, infos, arg, eout): |
38 |
vartree = trees["vartree"] |
39 |
vardb = vartree.dbapi |
40 |
bintree = trees["bintree"] |
41 |
- xattr = 'xattr' in settings.features |
42 |
+ xattrs = 'xattr' in settings.features |
43 |
|
44 |
include_config = options.include_config == "y" |
45 |
include_unmodified_config = options.include_unmodified_config == "y" |
46 |
@@ -137,8 +137,8 @@ def quickpkg_atom(options, infos, arg, eout): |
47 |
# The tarfile module will write pax headers holding the |
48 |
# xattrs only if PAX_FORMAT is specified here. |
49 |
tar = tarfile.open(binpkg_tmpfile, "w:bz2", |
50 |
- format=tarfile.PAX_FORMAT if xattr else tarfile.DEFAULT_FORMAT) |
51 |
- tar_contents(contents, root, tar, protect=protect, xattr=xattr) |
52 |
+ format=tarfile.PAX_FORMAT if xattrs else tarfile.DEFAULT_FORMAT) |
53 |
+ tar_contents(contents, root, tar, protect=protect, xattrs=xattrs) |
54 |
tar.close() |
55 |
xpak.tbz2(binpkg_tmpfile).recompose_mem(xpdata) |
56 |
finally: |
57 |
@@ -238,7 +238,7 @@ def quickpkg_main(options, args, eout): |
58 |
eout.eerror("No write access to '%s'" % bintree.pkgdir) |
59 |
return errno.EACCES |
60 |
|
61 |
- if 'xattr' in portage.settings.features and not hasattr(_xattr, 'getxattr'): |
62 |
+ if 'xattr' in portage.settings.features and not _xattr.XATTRS_WORKS: |
63 |
eout.eerror("No xattr support library was found, " |
64 |
"so xattrs will not be preserved!") |
65 |
portage.settings.unlock() |
66 |
diff --git a/bin/xattr-helper.py b/bin/xattr-helper.py |
67 |
index 3e9b81e..19f25f9 100755 |
68 |
--- a/bin/xattr-helper.py |
69 |
+++ b/bin/xattr-helper.py |
70 |
@@ -19,16 +19,7 @@ import re |
71 |
import sys |
72 |
|
73 |
from portage.util._argparse import ArgumentParser |
74 |
- |
75 |
-if hasattr(os, "getxattr"): |
76 |
- |
77 |
- class xattr(object): |
78 |
- get = os.getxattr |
79 |
- set = os.setxattr |
80 |
- list = os.listxattr |
81 |
- |
82 |
-else: |
83 |
- import xattr |
84 |
+from portage.util._xattr import xattr |
85 |
|
86 |
|
87 |
_UNQUOTE_RE = re.compile(br'\\[0-7]{3}') |
88 |
diff --git a/pym/portage/dbapi/vartree.py b/pym/portage/dbapi/vartree.py |
89 |
index 62d880e..e755fde 100644 |
90 |
--- a/pym/portage/dbapi/vartree.py |
91 |
+++ b/pym/portage/dbapi/vartree.py |
92 |
@@ -35,7 +35,7 @@ portage.proxy.lazyimport.lazyimport(globals(), |
93 |
'portage.util.movefile:movefile', |
94 |
'portage.util.path:first_existing,iter_parents', |
95 |
'portage.util.writeable_check:get_ro_checker', |
96 |
- 'portage.util:xattr@_xattr', |
97 |
+ 'portage.util._xattr:xattr', |
98 |
'portage.util._dyn_libs.PreservedLibsRegistry:PreservedLibsRegistry', |
99 |
'portage.util._dyn_libs.LinkageMapELF:LinkageMapELF@LinkageMap', |
100 |
'portage.util._async.SchedulerInterface:SchedulerInterface', |
101 |
@@ -5268,7 +5268,7 @@ def write_contents(contents, root, f): |
102 |
f.write(line) |
103 |
|
104 |
def tar_contents(contents, root, tar, protect=None, onProgress=None, |
105 |
- xattr=False): |
106 |
+ xattrs=False): |
107 |
os = _os_merge |
108 |
encoding = _encodings['merge'] |
109 |
|
110 |
@@ -5390,13 +5390,13 @@ def tar_contents(contents, root, tar, protect=None, onProgress=None, |
111 |
encoding=encoding, |
112 |
errors='strict') |
113 |
|
114 |
- if xattr: |
115 |
+ if xattrs: |
116 |
# Compatible with GNU tar, which saves the xattrs |
117 |
# under the SCHILY.xattr namespace. |
118 |
- for k in _xattr.listxattr(path_bytes): |
119 |
+ for k in xattr.list(path_bytes): |
120 |
tarinfo.pax_headers['SCHILY.xattr.' + |
121 |
_unicode_decode(k)] = _unicode_decode( |
122 |
- _xattr.getxattr(path_bytes, _unicode_encode(k))) |
123 |
+ xattr.get(path_bytes, _unicode_encode(k))) |
124 |
|
125 |
with open(path_bytes, 'rb') as f: |
126 |
tar.addfile(tarinfo, f) |
127 |
diff --git a/pym/portage/tests/util/test_xattr.py b/pym/portage/tests/util/test_xattr.py |
128 |
new file mode 100644 |
129 |
index 0000000..2e2564a |
130 |
--- /dev/null |
131 |
+++ b/pym/portage/tests/util/test_xattr.py |
132 |
@@ -0,0 +1,178 @@ |
133 |
+# Copyright 2010-2015 Gentoo Foundation |
134 |
+# Distributed under the terms of the GNU General Public License v2 |
135 |
+ |
136 |
+"""Tests for the portage.util._xattr module""" |
137 |
+ |
138 |
+from __future__ import print_function |
139 |
+ |
140 |
+try: |
141 |
+ # Try python-3.3 module first. |
142 |
+ # pylint: disable=no-name-in-module |
143 |
+ from unittest import mock |
144 |
+except ImportError: |
145 |
+ try: |
146 |
+ # Try standalone module. |
147 |
+ import mock |
148 |
+ except ImportError: |
149 |
+ mock = None |
150 |
+ |
151 |
+import subprocess |
152 |
+ |
153 |
+import portage |
154 |
+from portage.tests import TestCase |
155 |
+from portage.util._xattr import (xattr as _xattr, _XattrSystemCommands, |
156 |
+ _XattrStub) |
157 |
+ |
158 |
+ |
159 |
+orig_popen = subprocess.Popen |
160 |
+def MockSubprocessPopen(stdin): |
161 |
+ """Helper to mock (closely) a subprocess.Popen call |
162 |
+ |
163 |
+ The module has minor tweaks in behavior when it comes to encoding and |
164 |
+ python versions, so use a real subprocess.Popen call to fake out the |
165 |
+ runtime behavior. This way we don't have to also implement different |
166 |
+ encodings as that gets ugly real fast. |
167 |
+ """ |
168 |
+ # pylint: disable=protected-access |
169 |
+ proc = orig_popen(['cat'], stdout=subprocess.PIPE, stdin=subprocess.PIPE) |
170 |
+ proc.stdin.write(portage._unicode_encode(stdin, portage._encodings['stdio'])) |
171 |
+ return proc |
172 |
+ |
173 |
+ |
174 |
+class SystemCommandsTest(TestCase): |
175 |
+ """Test _XattrSystemCommands""" |
176 |
+ |
177 |
+ OUTPUT = '\n'.join(( |
178 |
+ '# file: /bin/ping', |
179 |
+ 'security.capability=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=', |
180 |
+ 'user.foo="asdf"', |
181 |
+ '', |
182 |
+ )) |
183 |
+ |
184 |
+ def _setUp(self): |
185 |
+ if mock is None: |
186 |
+ self.skipTest('need mock for testing') |
187 |
+ |
188 |
+ return _XattrSystemCommands |
189 |
+ |
190 |
+ def _testGetBasic(self): |
191 |
+ """Verify the get() behavior""" |
192 |
+ xattr = self._setUp() |
193 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
194 |
+ # Verify basic behavior, and namespace arg works as expected. |
195 |
+ xattr.get('/some/file', 'user.foo') |
196 |
+ xattr.get('/some/file', 'foo', namespace='user') |
197 |
+ self.assertEqual(call_mock.call_args_list[0], call_mock.call_args_list[1]) |
198 |
+ |
199 |
+ # Verify nofollow behavior. |
200 |
+ call_mock.reset() |
201 |
+ xattr.get('/some/file', 'user.foo', nofollow=True) |
202 |
+ self.assertIn('-h', call_mock.call_args[0][0]) |
203 |
+ |
204 |
+ def testGetParsing(self): |
205 |
+ """Verify get() parses output sanely""" |
206 |
+ xattr = self._setUp() |
207 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
208 |
+ # Verify output parsing. |
209 |
+ call_mock.return_value = MockSubprocessPopen('\n'.join([ |
210 |
+ '# file: /some/file', |
211 |
+ 'user.foo="asdf"', |
212 |
+ '', |
213 |
+ ])) |
214 |
+ call_mock.reset() |
215 |
+ self.assertEqual(xattr.get('/some/file', 'user.foo'), b'"asdf"') |
216 |
+ |
217 |
+ def testGetAllBasic(self): |
218 |
+ """Verify the get_all() behavior""" |
219 |
+ xattr = self._setUp() |
220 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
221 |
+ # Verify basic behavior. |
222 |
+ xattr.get_all('/some/file') |
223 |
+ |
224 |
+ # Verify nofollow behavior. |
225 |
+ call_mock.reset() |
226 |
+ xattr.get_all('/some/file', nofollow=True) |
227 |
+ self.assertIn('-h', call_mock.call_args[0][0]) |
228 |
+ |
229 |
+ def testGetAllParsing(self): |
230 |
+ """Verify get_all() parses output sanely""" |
231 |
+ xattr = self._setUp() |
232 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
233 |
+ # Verify output parsing. |
234 |
+ call_mock.return_value = MockSubprocessPopen(self.OUTPUT) |
235 |
+ exp = [ |
236 |
+ (b'security.capability', b'0sAQAAAgAgAAAAAAAAAAAAAAAAAAA='), |
237 |
+ (b'user.foo', b'"asdf"'), |
238 |
+ ] |
239 |
+ self.assertEqual(exp, xattr.get_all('/some/file')) |
240 |
+ |
241 |
+ def testSetBasic(self): |
242 |
+ """Verify the set() behavior""" |
243 |
+ xattr = self._setUp() |
244 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
245 |
+ # Verify basic behavior, and namespace arg works as expected. |
246 |
+ xattr.set('/some/file', 'user.foo', 'bar') |
247 |
+ xattr.set('/some/file', 'foo', 'bar', namespace='user') |
248 |
+ self.assertEqual(call_mock.call_args_list[0], call_mock.call_args_list[1]) |
249 |
+ |
250 |
+ def testListBasic(self): |
251 |
+ """Verify the list() behavior""" |
252 |
+ xattr = self._setUp() |
253 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
254 |
+ # Verify basic behavior. |
255 |
+ xattr.list('/some/file') |
256 |
+ |
257 |
+ # Verify nofollow behavior. |
258 |
+ call_mock.reset() |
259 |
+ xattr.list('/some/file', nofollow=True) |
260 |
+ self.assertIn('-h', call_mock.call_args[0][0]) |
261 |
+ |
262 |
+ def testListParsing(self): |
263 |
+ """Verify list() parses output sanely""" |
264 |
+ xattr = self._setUp() |
265 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
266 |
+ # Verify output parsing. |
267 |
+ call_mock.return_value = MockSubprocessPopen(self.OUTPUT) |
268 |
+ exp = [b'security.capability', b'user.foo'] |
269 |
+ self.assertEqual(exp, xattr.list('/some/file')) |
270 |
+ |
271 |
+ def testRemoveBasic(self): |
272 |
+ """Verify the remove() behavior""" |
273 |
+ xattr = self._setUp() |
274 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
275 |
+ # Verify basic behavior, and namespace arg works as expected. |
276 |
+ xattr.remove('/some/file', 'user.foo') |
277 |
+ xattr.remove('/some/file', 'foo', namespace='user') |
278 |
+ self.assertEqual(call_mock.call_args_list[0], call_mock.call_args_list[1]) |
279 |
+ |
280 |
+ # Verify nofollow behavior. |
281 |
+ call_mock.reset() |
282 |
+ xattr.remove('/some/file', 'user.foo', nofollow=True) |
283 |
+ self.assertIn('-h', call_mock.call_args[0][0]) |
284 |
+ |
285 |
+ |
286 |
+class StubTest(TestCase): |
287 |
+ """Test _XattrStub""" |
288 |
+ |
289 |
+ def testBasic(self): |
290 |
+ """Verify the stub is stubby""" |
291 |
+ # Would be nice to verify raised errno is OperationNotSupported. |
292 |
+ self.assertRaises(OSError, _XattrStub.get, '/', '') |
293 |
+ self.assertRaises(OSError, _XattrStub.set, '/', '', '') |
294 |
+ self.assertRaises(OSError, _XattrStub.get_all, '/') |
295 |
+ self.assertRaises(OSError, _XattrStub.remove, '/', '') |
296 |
+ self.assertRaises(OSError, _XattrStub.list, '/') |
297 |
+ |
298 |
+ |
299 |
+class StandardTest(TestCase): |
300 |
+ """Test basic xattr API""" |
301 |
+ |
302 |
+ MODULES = (_xattr, _XattrSystemCommands, _XattrStub) |
303 |
+ FUNCS = ('get', 'get_all', 'set', 'remove', 'list') |
304 |
+ |
305 |
+ def testApi(self): |
306 |
+ """Make sure the exported API matches""" |
307 |
+ for mod in self.MODULES: |
308 |
+ for f in self.FUNCS: |
309 |
+ self.assertTrue(hasattr(mod, f), |
310 |
+ '%s func missing in %s' % (f, mod)) |
311 |
diff --git a/pym/portage/util/_xattr.py b/pym/portage/util/_xattr.py |
312 |
new file mode 100644 |
313 |
index 0000000..9a8704d |
314 |
--- /dev/null |
315 |
+++ b/pym/portage/util/_xattr.py |
316 |
@@ -0,0 +1,228 @@ |
317 |
+# Copyright 2010-2015 Gentoo Foundation |
318 |
+# Distributed under the terms of the GNU General Public License v2 |
319 |
+ |
320 |
+"""Portability shim for xattr support |
321 |
+ |
322 |
+Exported API is the xattr object with get/get_all/set/remove/list operations. |
323 |
+We do not include the functions that Python 3.3+ provides in the os module as |
324 |
+the signature there is different compared to xattr. |
325 |
+ |
326 |
+See the standard xattr module for more documentation: |
327 |
+ https://pypi.python.org/pypi/pyxattr |
328 |
+""" |
329 |
+ |
330 |
+from __future__ import print_function |
331 |
+ |
332 |
+import contextlib |
333 |
+import os |
334 |
+import subprocess |
335 |
+ |
336 |
+from portage.exception import OperationNotSupported |
337 |
+ |
338 |
+ |
339 |
+class _XattrGetAll(object): |
340 |
+ """Implement get_all() using list()/get() if there is no easy bulk method""" |
341 |
+ |
342 |
+ @classmethod |
343 |
+ def get_all(cls, item, nofollow=False, namespace=None): |
344 |
+ return [(name, cls.get(item, name, nofollow=nofollow, namespace=namespace)) |
345 |
+ for name in cls.list(item, nofollow=nofollow, namespace=namespace)] |
346 |
+ |
347 |
+ |
348 |
+class _XattrSystemCommands(_XattrGetAll): |
349 |
+ """Implement things with getfattr/setfattr""" |
350 |
+ |
351 |
+ @staticmethod |
352 |
+ def _parse_output(output): |
353 |
+ for line in output.readlines(): |
354 |
+ if line.startswith(b'#'): |
355 |
+ continue |
356 |
+ line = line.rstrip() |
357 |
+ if not line: |
358 |
+ continue |
359 |
+ # The lines will have the format: |
360 |
+ # user.hex=0x12345 |
361 |
+ # user.base64=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA= |
362 |
+ # user.string="value0" |
363 |
+ # But since we don't do interpretation on the value (we just |
364 |
+ # save & restore it), don't bother with decoding here. |
365 |
+ yield line.split(b'=', 1) |
366 |
+ |
367 |
+ @staticmethod |
368 |
+ def _call(*args, **kwargs): |
369 |
+ proc = subprocess.Popen(*args, **kwargs) |
370 |
+ if proc.stdin: |
371 |
+ proc.stdin.close() |
372 |
+ proc.wait() |
373 |
+ return proc |
374 |
+ |
375 |
+ @classmethod |
376 |
+ def get(cls, item, name, nofollow=False, namespace=None): |
377 |
+ if namespace: |
378 |
+ name = '%s.%s' % (namespace, name) |
379 |
+ cmd = ['getfattr', '--absolute-names', '-n', name, item] |
380 |
+ if nofollow: |
381 |
+ cmd += ['-h'] |
382 |
+ proc = cls._call(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
383 |
+ |
384 |
+ value = None |
385 |
+ for _, value in cls._parse_output(proc.stdout): |
386 |
+ break |
387 |
+ |
388 |
+ proc.stdout.close() |
389 |
+ return value |
390 |
+ |
391 |
+ @classmethod |
392 |
+ def set(cls, item, name, value, _flags=0, namespace=None): |
393 |
+ if namespace: |
394 |
+ name = '%s.%s' % (namespace, name) |
395 |
+ cmd = ['setfattr', '-n', name, '-v', value, item] |
396 |
+ cls._call(cmd) |
397 |
+ |
398 |
+ @classmethod |
399 |
+ def remove(cls, item, name, nofollow=False, namespace=None): |
400 |
+ if namespace: |
401 |
+ name = '%s.%s' % (namespace, name) |
402 |
+ cmd = ['setfattr', '-x', name, item] |
403 |
+ if nofollow: |
404 |
+ cmd += ['-h'] |
405 |
+ cls._call(cmd) |
406 |
+ |
407 |
+ @classmethod |
408 |
+ def list(cls, item, nofollow=False, namespace=None, _names_only=True): |
409 |
+ cmd = ['getfattr', '-d', '--absolute-names', item] |
410 |
+ if nofollow: |
411 |
+ cmd += ['-h'] |
412 |
+ cmd += ['-m', ('^%s[.]' % namespace) if namespace else '-'] |
413 |
+ proc = cls._call(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
414 |
+ |
415 |
+ ret = [] |
416 |
+ if namespace: |
417 |
+ namespace = '%s.' % namespace |
418 |
+ for name, value in cls._parse_output(proc.stdout): |
419 |
+ if namespace: |
420 |
+ if name.startswith(namespace): |
421 |
+ name = name[len(namespace):] |
422 |
+ else: |
423 |
+ continue |
424 |
+ if _names_only: |
425 |
+ ret.append(name) |
426 |
+ else: |
427 |
+ ret.append((name, value)) |
428 |
+ |
429 |
+ proc.stdout.close() |
430 |
+ return ret |
431 |
+ |
432 |
+ @classmethod |
433 |
+ def get_all(cls, item, nofollow=False, namespace=None): |
434 |
+ return cls.list(item, nofollow=nofollow, namespace=namespace, |
435 |
+ _names_only=False) |
436 |
+ |
437 |
+ |
438 |
+class _XattrStub(_XattrGetAll): |
439 |
+ """Fake object since system doesn't support xattrs""" |
440 |
+ |
441 |
+ # pylint: disable=unused-argument |
442 |
+ |
443 |
+ @staticmethod |
444 |
+ def _raise(): |
445 |
+ e = OSError('stub') |
446 |
+ e.errno = OperationNotSupported.errno |
447 |
+ raise e |
448 |
+ |
449 |
+ @classmethod |
450 |
+ def get(cls, item, name, nofollow=False, namespace=None): |
451 |
+ cls._raise() |
452 |
+ |
453 |
+ @classmethod |
454 |
+ def set(cls, item, name, value, flags=0, namespace=None): |
455 |
+ cls._raise() |
456 |
+ |
457 |
+ @classmethod |
458 |
+ def remove(cls, item, name, nofollow=False, namespace=None): |
459 |
+ cls._raise() |
460 |
+ |
461 |
+ @classmethod |
462 |
+ def list(cls, item, nofollow=False, namespace=None): |
463 |
+ cls._raise() |
464 |
+ |
465 |
+ |
466 |
+if hasattr(os, 'getxattr'): |
467 |
+ # Easy as pie -- active python supports it. |
468 |
+ class xattr(_XattrGetAll): |
469 |
+ """Python >=3.3 and GNU/Linux""" |
470 |
+ |
471 |
+ # pylint: disable=unused-argument |
472 |
+ |
473 |
+ @staticmethod |
474 |
+ def get(item, name, nofollow=False, namespace=None): |
475 |
+ return os.getxattr(item, name, follow_symlinks=not nofollow) |
476 |
+ |
477 |
+ @staticmethod |
478 |
+ def set(item, name, value, flags=0, namespace=None): |
479 |
+ return os.setxattr(item, name, value, flags=flags) |
480 |
+ |
481 |
+ @staticmethod |
482 |
+ def remove(item, name, nofollow=False, namespace=None): |
483 |
+ return os.removexattr(item, name, follow_symlinks=not nofollow) |
484 |
+ |
485 |
+ @staticmethod |
486 |
+ def list(item, nofollow=False, namespace=None): |
487 |
+ return os.listxattr(item, follow_symlinks=not nofollow) |
488 |
+ |
489 |
+else: |
490 |
+ try: |
491 |
+ # Maybe we have the xattr module. |
492 |
+ import xattr |
493 |
+ |
494 |
+ except ImportError: |
495 |
+ try: |
496 |
+ # Maybe we have the attr package. |
497 |
+ with open(os.devnull, 'wb') as f: |
498 |
+ subprocess.call(['getfattr', '--version'], stdout=f) |
499 |
+ subprocess.call(['setfattr', '--version'], stdout=f) |
500 |
+ xattr = _XattrSystemCommands |
501 |
+ |
502 |
+ except OSError: |
503 |
+ # Stub it out completely. |
504 |
+ xattr = _XattrStub |
505 |
+ |
506 |
+ |
507 |
+# Add a knob so code can take evasive action as needed. |
508 |
+XATTRS_WORKS = xattr != _XattrStub |
509 |
+ |
510 |
+ |
511 |
+@××××××××××.contextmanager |
512 |
+def preserve_xattrs(path, nofollow=False, namespace=None): |
513 |
+ """Context manager to save/restore extended attributes on |path| |
514 |
+ |
515 |
+ If you want to rewrite a file (possibly replacing it with a new one), but |
516 |
+ want to preserve the extended attributes, this will do the trick. |
517 |
+ |
518 |
+ # First read all the extended attributes. |
519 |
+ with save_xattrs('/some/file'): |
520 |
+ ... rewrite the file ... |
521 |
+ # Now the extended attributes are restored as needed. |
522 |
+ """ |
523 |
+ kwargs = {'nofollow': nofollow,} |
524 |
+ if namespace: |
525 |
+ # Compiled xattr python module does not like it when namespace=None. |
526 |
+ kwargs['namespace'] = namespace |
527 |
+ |
528 |
+ old_attrs = dict(xattr.get_all(path, **kwargs)) |
529 |
+ try: |
530 |
+ yield |
531 |
+ finally: |
532 |
+ new_attrs = dict(xattr.get_all(path, **kwargs)) |
533 |
+ for name, value in new_attrs.items(): |
534 |
+ if name not in old_attrs: |
535 |
+ # Clear out new ones. |
536 |
+ xattr.remove(path, name, **kwargs) |
537 |
+ elif new_attrs[name] != old_attrs[name]: |
538 |
+ # Update changed ones. |
539 |
+ xattr.set(path, name, value, **kwargs) |
540 |
+ |
541 |
+ for name, value in old_attrs.items(): |
542 |
+ if name not in new_attrs: |
543 |
+ # Re-add missing ones. |
544 |
+ xattr.set(path, name, value, **kwargs) |
545 |
diff --git a/pym/portage/util/movefile.py b/pym/portage/util/movefile.py |
546 |
index d00f624..0cb1977 100644 |
547 |
--- a/pym/portage/util/movefile.py |
548 |
+++ b/pym/portage/util/movefile.py |
549 |
@@ -11,7 +11,6 @@ import os as _os |
550 |
import shutil as _shutil |
551 |
import stat |
552 |
import sys |
553 |
-import subprocess |
554 |
import textwrap |
555 |
|
556 |
import portage |
557 |
@@ -23,6 +22,7 @@ from portage.exception import OperationNotSupported |
558 |
from portage.localization import _ |
559 |
from portage.process import spawn |
560 |
from portage.util import writemsg |
561 |
+from portage.util._xattr import xattr |
562 |
|
563 |
def _apply_stat(src_stat, dest): |
564 |
_os.chown(dest, src_stat.st_uid, src_stat.st_gid) |
565 |
@@ -68,86 +68,32 @@ class _xattr_excluder(object): |
566 |
|
567 |
return False |
568 |
|
569 |
-if hasattr(_os, "getxattr"): |
570 |
- # Python >=3.3 and GNU/Linux |
571 |
- def _copyxattr(src, dest, exclude=None): |
572 |
- |
573 |
- try: |
574 |
- attrs = _os.listxattr(src) |
575 |
- except OSError as e: |
576 |
- if e.errno != OperationNotSupported.errno: |
577 |
- raise |
578 |
- attrs = () |
579 |
- if attrs: |
580 |
- if exclude is not None and isinstance(attrs[0], bytes): |
581 |
- exclude = exclude.encode(_encodings['fs']) |
582 |
- exclude = _get_xattr_excluder(exclude) |
583 |
- |
584 |
- for attr in attrs: |
585 |
- if exclude(attr): |
586 |
- continue |
587 |
- try: |
588 |
- _os.setxattr(dest, attr, _os.getxattr(src, attr)) |
589 |
- raise_exception = False |
590 |
- except OSError: |
591 |
- raise_exception = True |
592 |
- if raise_exception: |
593 |
- raise OperationNotSupported(_("Filesystem containing file '%s' " |
594 |
- "does not support extended attribute '%s'") % |
595 |
- (_unicode_decode(dest), _unicode_decode(attr))) |
596 |
-else: |
597 |
+def _copyxattr(src, dest, exclude=None): |
598 |
+ """Copy the extended attributes from |src| to |dest|""" |
599 |
try: |
600 |
- import xattr |
601 |
- except ImportError: |
602 |
- xattr = None |
603 |
- if xattr is not None: |
604 |
- def _copyxattr(src, dest, exclude=None): |
605 |
- |
606 |
- try: |
607 |
- attrs = xattr.list(src) |
608 |
- except IOError as e: |
609 |
- if e.errno != OperationNotSupported.errno: |
610 |
- raise |
611 |
- attrs = () |
612 |
+ attrs = xattr.list(src) |
613 |
+ except (OSError, IOError) as e: |
614 |
+ if e.errno != OperationNotSupported.errno: |
615 |
+ raise |
616 |
+ attrs = () |
617 |
|
618 |
- if attrs: |
619 |
- if exclude is not None and isinstance(attrs[0], bytes): |
620 |
- exclude = exclude.encode(_encodings['fs']) |
621 |
- exclude = _get_xattr_excluder(exclude) |
622 |
+ if attrs: |
623 |
+ if exclude is not None and isinstance(attrs[0], bytes): |
624 |
+ exclude = exclude.encode(_encodings['fs']) |
625 |
+ exclude = _get_xattr_excluder(exclude) |
626 |
|
627 |
- for attr in attrs: |
628 |
- if exclude(attr): |
629 |
- continue |
630 |
- try: |
631 |
- xattr.set(dest, attr, xattr.get(src, attr)) |
632 |
- raise_exception = False |
633 |
- except IOError: |
634 |
- raise_exception = True |
635 |
- if raise_exception: |
636 |
- raise OperationNotSupported(_("Filesystem containing file '%s' " |
637 |
- "does not support extended attribute '%s'") % |
638 |
- (_unicode_decode(dest), _unicode_decode(attr))) |
639 |
- else: |
640 |
+ for attr in attrs: |
641 |
+ if exclude(attr): |
642 |
+ continue |
643 |
try: |
644 |
- with open(_os.devnull, 'wb') as f: |
645 |
- subprocess.call(["getfattr", "--version"], stdout=f) |
646 |
- subprocess.call(["setfattr", "--version"], stdout=f) |
647 |
- except OSError: |
648 |
- def _copyxattr(src, dest, exclude=None): |
649 |
- # TODO: implement exclude |
650 |
- getfattr_process = subprocess.Popen(["getfattr", "-d", "--absolute-names", src], stdout=subprocess.PIPE) |
651 |
- getfattr_process.wait() |
652 |
- extended_attributes = getfattr_process.stdout.readlines() |
653 |
- getfattr_process.stdout.close() |
654 |
- if extended_attributes: |
655 |
- extended_attributes[0] = b"# file: " + _unicode_encode(dest) + b"\n" |
656 |
- setfattr_process = subprocess.Popen(["setfattr", "--restore=-"], stdin=subprocess.PIPE, stderr=subprocess.PIPE) |
657 |
- setfattr_process.communicate(input=b"".join(extended_attributes)) |
658 |
- if setfattr_process.returncode != 0: |
659 |
- raise OperationNotSupported("Filesystem containing file '%s' does not support extended attributes" % dest) |
660 |
- else: |
661 |
- def _copyxattr(src, dest, exclude=None): |
662 |
- pass |
663 |
+ xattr.set(dest, attr, xattr.get(src, attr)) |
664 |
+ raise_exception = False |
665 |
+ except (OSError, IOError): |
666 |
+ raise_exception = True |
667 |
+ if raise_exception: |
668 |
+ raise OperationNotSupported(_("Filesystem containing file '%s' " |
669 |
+ "does not support extended attribute '%s'") % |
670 |
+ (_unicode_decode(dest), _unicode_decode(attr))) |
671 |
|
672 |
def movefile(src, dest, newmtime=None, sstat=None, mysettings=None, |
673 |
hardlink_candidates=None, encoding=_encodings['fs']): |
674 |
diff --git a/pym/portage/util/xattr.py b/pym/portage/util/xattr.py |
675 |
deleted file mode 100644 |
676 |
index b8c4620..0000000 |
677 |
--- a/pym/portage/util/xattr.py |
678 |
+++ /dev/null |
679 |
@@ -1,20 +0,0 @@ |
680 |
-# Copyright 2015 Gentoo Foundation |
681 |
-# Distributed under the terms of the GNU General Public License v2 |
682 |
- |
683 |
-from __future__ import absolute_import |
684 |
- |
685 |
-import os as _os |
686 |
- |
687 |
-if hasattr(_os, "getxattr"): |
688 |
- getxattr = _os.getxattr |
689 |
- listxattr = _os.listxattr |
690 |
- setxattr = _os.setxattr |
691 |
-else: |
692 |
- try: |
693 |
- import xattr as _xattr |
694 |
- except ImportError: |
695 |
- pass |
696 |
- else: |
697 |
- getxattr = _xattr.get |
698 |
- listxattr = _xattr.list |
699 |
- setxattr = _xattr.set |
700 |
-- |
701 |
2.4.1 |