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 |
bin/xattr-helper.py | 11 +- |
6 |
pym/portage/tests/util/test_xattr.py | 178 ++++++++++++++++++++++++++++ |
7 |
pym/portage/util/_xattr.py | 221 +++++++++++++++++++++++++++++++++++ |
8 |
pym/portage/util/movefile.py | 100 ++++------------ |
9 |
4 files changed, 423 insertions(+), 87 deletions(-) |
10 |
create mode 100644 pym/portage/tests/util/test_xattr.py |
11 |
create mode 100644 pym/portage/util/_xattr.py |
12 |
|
13 |
diff --git a/bin/xattr-helper.py b/bin/xattr-helper.py |
14 |
index 3e9b81e..19f25f9 100755 |
15 |
--- a/bin/xattr-helper.py |
16 |
+++ b/bin/xattr-helper.py |
17 |
@@ -19,16 +19,7 @@ import re |
18 |
import sys |
19 |
|
20 |
from portage.util._argparse import ArgumentParser |
21 |
- |
22 |
-if hasattr(os, "getxattr"): |
23 |
- |
24 |
- class xattr(object): |
25 |
- get = os.getxattr |
26 |
- set = os.setxattr |
27 |
- list = os.listxattr |
28 |
- |
29 |
-else: |
30 |
- import xattr |
31 |
+from portage.util._xattr import xattr |
32 |
|
33 |
|
34 |
_UNQUOTE_RE = re.compile(br'\\[0-7]{3}') |
35 |
diff --git a/pym/portage/tests/util/test_xattr.py b/pym/portage/tests/util/test_xattr.py |
36 |
new file mode 100644 |
37 |
index 0000000..2e2564a |
38 |
--- /dev/null |
39 |
+++ b/pym/portage/tests/util/test_xattr.py |
40 |
@@ -0,0 +1,178 @@ |
41 |
+# Copyright 2010-2015 Gentoo Foundation |
42 |
+# Distributed under the terms of the GNU General Public License v2 |
43 |
+ |
44 |
+"""Tests for the portage.util._xattr module""" |
45 |
+ |
46 |
+from __future__ import print_function |
47 |
+ |
48 |
+try: |
49 |
+ # Try python-3.3 module first. |
50 |
+ # pylint: disable=no-name-in-module |
51 |
+ from unittest import mock |
52 |
+except ImportError: |
53 |
+ try: |
54 |
+ # Try standalone module. |
55 |
+ import mock |
56 |
+ except ImportError: |
57 |
+ mock = None |
58 |
+ |
59 |
+import subprocess |
60 |
+ |
61 |
+import portage |
62 |
+from portage.tests import TestCase |
63 |
+from portage.util._xattr import (xattr as _xattr, _XattrSystemCommands, |
64 |
+ _XattrStub) |
65 |
+ |
66 |
+ |
67 |
+orig_popen = subprocess.Popen |
68 |
+def MockSubprocessPopen(stdin): |
69 |
+ """Helper to mock (closely) a subprocess.Popen call |
70 |
+ |
71 |
+ The module has minor tweaks in behavior when it comes to encoding and |
72 |
+ python versions, so use a real subprocess.Popen call to fake out the |
73 |
+ runtime behavior. This way we don't have to also implement different |
74 |
+ encodings as that gets ugly real fast. |
75 |
+ """ |
76 |
+ # pylint: disable=protected-access |
77 |
+ proc = orig_popen(['cat'], stdout=subprocess.PIPE, stdin=subprocess.PIPE) |
78 |
+ proc.stdin.write(portage._unicode_encode(stdin, portage._encodings['stdio'])) |
79 |
+ return proc |
80 |
+ |
81 |
+ |
82 |
+class SystemCommandsTest(TestCase): |
83 |
+ """Test _XattrSystemCommands""" |
84 |
+ |
85 |
+ OUTPUT = '\n'.join(( |
86 |
+ '# file: /bin/ping', |
87 |
+ 'security.capability=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=', |
88 |
+ 'user.foo="asdf"', |
89 |
+ '', |
90 |
+ )) |
91 |
+ |
92 |
+ def _setUp(self): |
93 |
+ if mock is None: |
94 |
+ self.skipTest('need mock for testing') |
95 |
+ |
96 |
+ return _XattrSystemCommands |
97 |
+ |
98 |
+ def _testGetBasic(self): |
99 |
+ """Verify the get() behavior""" |
100 |
+ xattr = self._setUp() |
101 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
102 |
+ # Verify basic behavior, and namespace arg works as expected. |
103 |
+ xattr.get('/some/file', 'user.foo') |
104 |
+ xattr.get('/some/file', 'foo', namespace='user') |
105 |
+ self.assertEqual(call_mock.call_args_list[0], call_mock.call_args_list[1]) |
106 |
+ |
107 |
+ # Verify nofollow behavior. |
108 |
+ call_mock.reset() |
109 |
+ xattr.get('/some/file', 'user.foo', nofollow=True) |
110 |
+ self.assertIn('-h', call_mock.call_args[0][0]) |
111 |
+ |
112 |
+ def testGetParsing(self): |
113 |
+ """Verify get() parses output sanely""" |
114 |
+ xattr = self._setUp() |
115 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
116 |
+ # Verify output parsing. |
117 |
+ call_mock.return_value = MockSubprocessPopen('\n'.join([ |
118 |
+ '# file: /some/file', |
119 |
+ 'user.foo="asdf"', |
120 |
+ '', |
121 |
+ ])) |
122 |
+ call_mock.reset() |
123 |
+ self.assertEqual(xattr.get('/some/file', 'user.foo'), b'"asdf"') |
124 |
+ |
125 |
+ def testGetAllBasic(self): |
126 |
+ """Verify the get_all() behavior""" |
127 |
+ xattr = self._setUp() |
128 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
129 |
+ # Verify basic behavior. |
130 |
+ xattr.get_all('/some/file') |
131 |
+ |
132 |
+ # Verify nofollow behavior. |
133 |
+ call_mock.reset() |
134 |
+ xattr.get_all('/some/file', nofollow=True) |
135 |
+ self.assertIn('-h', call_mock.call_args[0][0]) |
136 |
+ |
137 |
+ def testGetAllParsing(self): |
138 |
+ """Verify get_all() parses output sanely""" |
139 |
+ xattr = self._setUp() |
140 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
141 |
+ # Verify output parsing. |
142 |
+ call_mock.return_value = MockSubprocessPopen(self.OUTPUT) |
143 |
+ exp = [ |
144 |
+ (b'security.capability', b'0sAQAAAgAgAAAAAAAAAAAAAAAAAAA='), |
145 |
+ (b'user.foo', b'"asdf"'), |
146 |
+ ] |
147 |
+ self.assertEqual(exp, xattr.get_all('/some/file')) |
148 |
+ |
149 |
+ def testSetBasic(self): |
150 |
+ """Verify the set() behavior""" |
151 |
+ xattr = self._setUp() |
152 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
153 |
+ # Verify basic behavior, and namespace arg works as expected. |
154 |
+ xattr.set('/some/file', 'user.foo', 'bar') |
155 |
+ xattr.set('/some/file', 'foo', 'bar', namespace='user') |
156 |
+ self.assertEqual(call_mock.call_args_list[0], call_mock.call_args_list[1]) |
157 |
+ |
158 |
+ def testListBasic(self): |
159 |
+ """Verify the list() behavior""" |
160 |
+ xattr = self._setUp() |
161 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
162 |
+ # Verify basic behavior. |
163 |
+ xattr.list('/some/file') |
164 |
+ |
165 |
+ # Verify nofollow behavior. |
166 |
+ call_mock.reset() |
167 |
+ xattr.list('/some/file', nofollow=True) |
168 |
+ self.assertIn('-h', call_mock.call_args[0][0]) |
169 |
+ |
170 |
+ def testListParsing(self): |
171 |
+ """Verify list() parses output sanely""" |
172 |
+ xattr = self._setUp() |
173 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
174 |
+ # Verify output parsing. |
175 |
+ call_mock.return_value = MockSubprocessPopen(self.OUTPUT) |
176 |
+ exp = [b'security.capability', b'user.foo'] |
177 |
+ self.assertEqual(exp, xattr.list('/some/file')) |
178 |
+ |
179 |
+ def testRemoveBasic(self): |
180 |
+ """Verify the remove() behavior""" |
181 |
+ xattr = self._setUp() |
182 |
+ with mock.patch.object(subprocess, 'Popen') as call_mock: |
183 |
+ # Verify basic behavior, and namespace arg works as expected. |
184 |
+ xattr.remove('/some/file', 'user.foo') |
185 |
+ xattr.remove('/some/file', 'foo', namespace='user') |
186 |
+ self.assertEqual(call_mock.call_args_list[0], call_mock.call_args_list[1]) |
187 |
+ |
188 |
+ # Verify nofollow behavior. |
189 |
+ call_mock.reset() |
190 |
+ xattr.remove('/some/file', 'user.foo', nofollow=True) |
191 |
+ self.assertIn('-h', call_mock.call_args[0][0]) |
192 |
+ |
193 |
+ |
194 |
+class StubTest(TestCase): |
195 |
+ """Test _XattrStub""" |
196 |
+ |
197 |
+ def testBasic(self): |
198 |
+ """Verify the stub is stubby""" |
199 |
+ # Would be nice to verify raised errno is OperationNotSupported. |
200 |
+ self.assertRaises(OSError, _XattrStub.get, '/', '') |
201 |
+ self.assertRaises(OSError, _XattrStub.set, '/', '', '') |
202 |
+ self.assertRaises(OSError, _XattrStub.get_all, '/') |
203 |
+ self.assertRaises(OSError, _XattrStub.remove, '/', '') |
204 |
+ self.assertRaises(OSError, _XattrStub.list, '/') |
205 |
+ |
206 |
+ |
207 |
+class StandardTest(TestCase): |
208 |
+ """Test basic xattr API""" |
209 |
+ |
210 |
+ MODULES = (_xattr, _XattrSystemCommands, _XattrStub) |
211 |
+ FUNCS = ('get', 'get_all', 'set', 'remove', 'list') |
212 |
+ |
213 |
+ def testApi(self): |
214 |
+ """Make sure the exported API matches""" |
215 |
+ for mod in self.MODULES: |
216 |
+ for f in self.FUNCS: |
217 |
+ self.assertTrue(hasattr(mod, f), |
218 |
+ '%s func missing in %s' % (f, mod)) |
219 |
diff --git a/pym/portage/util/_xattr.py b/pym/portage/util/_xattr.py |
220 |
new file mode 100644 |
221 |
index 0000000..4436c05 |
222 |
--- /dev/null |
223 |
+++ b/pym/portage/util/_xattr.py |
224 |
@@ -0,0 +1,221 @@ |
225 |
+# Copyright 2010-2015 Gentoo Foundation |
226 |
+# Distributed under the terms of the GNU General Public License v2 |
227 |
+ |
228 |
+"""Portability shim for xattr support |
229 |
+ |
230 |
+Exported API is the xattr object with get/get_all/set/remove/list operations. |
231 |
+ |
232 |
+See the standard xattr module for more documentation. |
233 |
+""" |
234 |
+ |
235 |
+from __future__ import print_function |
236 |
+ |
237 |
+import contextlib |
238 |
+import os |
239 |
+import subprocess |
240 |
+ |
241 |
+from portage.exception import OperationNotSupported |
242 |
+ |
243 |
+ |
244 |
+class _XattrGetAll(object): |
245 |
+ """Implement get_all() using list()/get() if there is no easy bulk method""" |
246 |
+ |
247 |
+ @classmethod |
248 |
+ def get_all(cls, item, nofollow=False, namespace=None): |
249 |
+ return [(name, cls.get(item, name, nofollow=nofollow, namespace=namespace)) |
250 |
+ for name in cls.list(item, nofollow=nofollow, namespace=namespace)] |
251 |
+ |
252 |
+ |
253 |
+class _XattrSystemCommands(_XattrGetAll): |
254 |
+ """Implement things with getfattr/setfattr""" |
255 |
+ |
256 |
+ @staticmethod |
257 |
+ def _parse_output(output): |
258 |
+ for line in output.readlines(): |
259 |
+ if line.startswith(b'#'): |
260 |
+ continue |
261 |
+ line = line.rstrip() |
262 |
+ if not line: |
263 |
+ continue |
264 |
+ # The lines will have the format: |
265 |
+ # user.hex=0x12345 |
266 |
+ # user.base64=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA= |
267 |
+ # user.string="value0" |
268 |
+ # But since we don't do interpretation on the value (we just |
269 |
+ # save & restore it), don't bother with decoding here. |
270 |
+ yield line.split(b'=', 1) |
271 |
+ |
272 |
+ @staticmethod |
273 |
+ def _call(*args, **kwargs): |
274 |
+ proc = subprocess.Popen(*args, **kwargs) |
275 |
+ if proc.stdin: |
276 |
+ proc.stdin.close() |
277 |
+ proc.wait() |
278 |
+ return proc |
279 |
+ |
280 |
+ @classmethod |
281 |
+ def get(cls, item, name, nofollow=False, namespace=None): |
282 |
+ if namespace: |
283 |
+ name = '%s.%s' % (namespace, name) |
284 |
+ cmd = ['getfattr', '--absolute-names', '-n', name, item] |
285 |
+ if nofollow: |
286 |
+ cmd += ['-h'] |
287 |
+ proc = cls._call(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
288 |
+ |
289 |
+ value = None |
290 |
+ for _, value in cls._parse_output(proc.stdout): |
291 |
+ break |
292 |
+ |
293 |
+ proc.stdout.close() |
294 |
+ return value |
295 |
+ |
296 |
+ @classmethod |
297 |
+ def set(cls, item, name, value, _flags=0, namespace=None): |
298 |
+ if namespace: |
299 |
+ name = '%s.%s' % (namespace, name) |
300 |
+ cmd = ['setfattr', '-n', name, '-v', value, item] |
301 |
+ cls._call(cmd) |
302 |
+ |
303 |
+ @classmethod |
304 |
+ def remove(cls, item, name, nofollow=False, namespace=None): |
305 |
+ if namespace: |
306 |
+ name = '%s.%s' % (namespace, name) |
307 |
+ cmd = ['setfattr', '-x', name, item] |
308 |
+ if nofollow: |
309 |
+ cmd += ['-h'] |
310 |
+ cls._call(cmd) |
311 |
+ |
312 |
+ @classmethod |
313 |
+ def list(cls, item, nofollow=False, namespace=None, _names_only=True): |
314 |
+ cmd = ['getfattr', '-d', '--absolute-names', item] |
315 |
+ if nofollow: |
316 |
+ cmd += ['-h'] |
317 |
+ cmd += ['-m', ('^%s[.]' % namespace) if namespace else ''] |
318 |
+ proc = cls._call(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
319 |
+ |
320 |
+ ret = [] |
321 |
+ if namespace: |
322 |
+ namespace = '%s.' % namespace |
323 |
+ for name, value in cls._parse_output(proc.stdout): |
324 |
+ if namespace: |
325 |
+ if name.startswith(namespace): |
326 |
+ name = name[len(namespace):] |
327 |
+ else: |
328 |
+ continue |
329 |
+ if _names_only: |
330 |
+ ret.append(name) |
331 |
+ else: |
332 |
+ ret.append((name, value)) |
333 |
+ |
334 |
+ proc.stdout.close() |
335 |
+ return ret |
336 |
+ |
337 |
+ @classmethod |
338 |
+ def get_all(cls, item, nofollow=False, namespace=None): |
339 |
+ return cls.list(item, nofollow=nofollow, namespace=namespace, |
340 |
+ _names_only=False) |
341 |
+ |
342 |
+ |
343 |
+class _XattrStub(_XattrGetAll): |
344 |
+ """Fake object since system doesn't support xattrs""" |
345 |
+ |
346 |
+ # pylint: disable=unused-argument |
347 |
+ |
348 |
+ @staticmethod |
349 |
+ def _raise(): |
350 |
+ e = OSError('stub') |
351 |
+ e.errno = OperationNotSupported.errno |
352 |
+ raise e |
353 |
+ |
354 |
+ @classmethod |
355 |
+ def get(cls, item, name, nofollow=False, namespace=None): |
356 |
+ cls._raise() |
357 |
+ |
358 |
+ @classmethod |
359 |
+ def set(cls, item, name, value, flags=0, namespace=None): |
360 |
+ cls._raise() |
361 |
+ |
362 |
+ @classmethod |
363 |
+ def remove(cls, item, name, nofollow=False, namespace=None): |
364 |
+ cls._raise() |
365 |
+ |
366 |
+ @classmethod |
367 |
+ def list(cls, item, nofollow=False, namespace=None): |
368 |
+ cls._raise() |
369 |
+ |
370 |
+ |
371 |
+if hasattr(os, 'getxattr'): |
372 |
+ # Easy as pie -- active python supports it. |
373 |
+ class xattr(_XattrGetAll): |
374 |
+ """Python >=3.3 and GNU/Linux""" |
375 |
+ |
376 |
+ # pylint: disable=unused-argument |
377 |
+ |
378 |
+ @staticmethod |
379 |
+ def get(item, name, nofollow=False, namespace=None): |
380 |
+ return os.getxattr(item, name, follow_symlinks=not nofollow) |
381 |
+ |
382 |
+ @staticmethod |
383 |
+ def set(item, name, value, flags=0, namespace=None): |
384 |
+ return os.setxattr(item, name, value, flags=flags) |
385 |
+ |
386 |
+ @staticmethod |
387 |
+ def remove(item, name, nofollow=False, namespace=None): |
388 |
+ return os.removexattr(item, name, follow_symlinks=not nofollow) |
389 |
+ |
390 |
+ @staticmethod |
391 |
+ def list(item, nofollow=False, namespace=None): |
392 |
+ return os.listxattr(item, follow_symlinks=not nofollow) |
393 |
+ |
394 |
+else: |
395 |
+ try: |
396 |
+ # Maybe we have the xattr module. |
397 |
+ import xattr |
398 |
+ |
399 |
+ except ImportError: |
400 |
+ try: |
401 |
+ # Maybe we have the attr package. |
402 |
+ with open(os.devnull, 'wb') as f: |
403 |
+ subprocess.call(['getfattr', '--version'], stdout=f) |
404 |
+ subprocess.call(['setfattr', '--version'], stdout=f) |
405 |
+ xattr = _XattrSystemCommands |
406 |
+ |
407 |
+ except OSError: |
408 |
+ # Stub it out completely. |
409 |
+ xattr = _XattrStub |
410 |
+ |
411 |
+ |
412 |
+@××××××××××.contextmanager |
413 |
+def preserve_xattrs(path, nofollow=False, namespace=None): |
414 |
+ """Context manager to save/restore extended attributes on |path| |
415 |
+ |
416 |
+ If you want to rewrite a file (possibly replacing it with a new one), but |
417 |
+ want to preserve the extended attributes, this will do the trick. |
418 |
+ |
419 |
+ # First read all the extended attributes. |
420 |
+ with save_xattrs('/some/file'): |
421 |
+ ... rewrite the file ... |
422 |
+ # Now the extended attributes are restored as needed. |
423 |
+ """ |
424 |
+ kwargs = {'nofollow': nofollow,} |
425 |
+ if namespace: |
426 |
+ # Compiled xattr python module does not like it when namespace=None. |
427 |
+ kwargs['namespace'] = namespace |
428 |
+ |
429 |
+ old_attrs = dict(xattr.get_all(path, **kwargs)) |
430 |
+ try: |
431 |
+ yield |
432 |
+ finally: |
433 |
+ new_attrs = dict(xattr.get_all(path, **kwargs)) |
434 |
+ for name, value in new_attrs.items(): |
435 |
+ if name not in old_attrs: |
436 |
+ # Clear out new ones. |
437 |
+ xattr.remove(path, name, **kwargs) |
438 |
+ elif new_attrs[name] != old_attrs[name]: |
439 |
+ # Update changed ones. |
440 |
+ xattr.set(path, name, value, **kwargs) |
441 |
+ |
442 |
+ for name, value in old_attrs.items(): |
443 |
+ if name not in new_attrs: |
444 |
+ # Re-add missing ones. |
445 |
+ xattr.set(path, name, value, **kwargs) |
446 |
diff --git a/pym/portage/util/movefile.py b/pym/portage/util/movefile.py |
447 |
index d00f624..0cb1977 100644 |
448 |
--- a/pym/portage/util/movefile.py |
449 |
+++ b/pym/portage/util/movefile.py |
450 |
@@ -11,7 +11,6 @@ import os as _os |
451 |
import shutil as _shutil |
452 |
import stat |
453 |
import sys |
454 |
-import subprocess |
455 |
import textwrap |
456 |
|
457 |
import portage |
458 |
@@ -23,6 +22,7 @@ from portage.exception import OperationNotSupported |
459 |
from portage.localization import _ |
460 |
from portage.process import spawn |
461 |
from portage.util import writemsg |
462 |
+from portage.util._xattr import xattr |
463 |
|
464 |
def _apply_stat(src_stat, dest): |
465 |
_os.chown(dest, src_stat.st_uid, src_stat.st_gid) |
466 |
@@ -68,86 +68,32 @@ class _xattr_excluder(object): |
467 |
|
468 |
return False |
469 |
|
470 |
-if hasattr(_os, "getxattr"): |
471 |
- # Python >=3.3 and GNU/Linux |
472 |
- def _copyxattr(src, dest, exclude=None): |
473 |
- |
474 |
- try: |
475 |
- attrs = _os.listxattr(src) |
476 |
- except OSError as e: |
477 |
- if e.errno != OperationNotSupported.errno: |
478 |
- raise |
479 |
- attrs = () |
480 |
- if attrs: |
481 |
- if exclude is not None and isinstance(attrs[0], bytes): |
482 |
- exclude = exclude.encode(_encodings['fs']) |
483 |
- exclude = _get_xattr_excluder(exclude) |
484 |
- |
485 |
- for attr in attrs: |
486 |
- if exclude(attr): |
487 |
- continue |
488 |
- try: |
489 |
- _os.setxattr(dest, attr, _os.getxattr(src, attr)) |
490 |
- raise_exception = False |
491 |
- except OSError: |
492 |
- raise_exception = True |
493 |
- if raise_exception: |
494 |
- raise OperationNotSupported(_("Filesystem containing file '%s' " |
495 |
- "does not support extended attribute '%s'") % |
496 |
- (_unicode_decode(dest), _unicode_decode(attr))) |
497 |
-else: |
498 |
+def _copyxattr(src, dest, exclude=None): |
499 |
+ """Copy the extended attributes from |src| to |dest|""" |
500 |
try: |
501 |
- import xattr |
502 |
- except ImportError: |
503 |
- xattr = None |
504 |
- if xattr is not None: |
505 |
- def _copyxattr(src, dest, exclude=None): |
506 |
- |
507 |
- try: |
508 |
- attrs = xattr.list(src) |
509 |
- except IOError as e: |
510 |
- if e.errno != OperationNotSupported.errno: |
511 |
- raise |
512 |
- attrs = () |
513 |
+ attrs = xattr.list(src) |
514 |
+ except (OSError, IOError) as e: |
515 |
+ if e.errno != OperationNotSupported.errno: |
516 |
+ raise |
517 |
+ attrs = () |
518 |
|
519 |
- if attrs: |
520 |
- if exclude is not None and isinstance(attrs[0], bytes): |
521 |
- exclude = exclude.encode(_encodings['fs']) |
522 |
- exclude = _get_xattr_excluder(exclude) |
523 |
+ if attrs: |
524 |
+ if exclude is not None and isinstance(attrs[0], bytes): |
525 |
+ exclude = exclude.encode(_encodings['fs']) |
526 |
+ exclude = _get_xattr_excluder(exclude) |
527 |
|
528 |
- for attr in attrs: |
529 |
- if exclude(attr): |
530 |
- continue |
531 |
- try: |
532 |
- xattr.set(dest, attr, xattr.get(src, attr)) |
533 |
- raise_exception = False |
534 |
- except IOError: |
535 |
- raise_exception = True |
536 |
- if raise_exception: |
537 |
- raise OperationNotSupported(_("Filesystem containing file '%s' " |
538 |
- "does not support extended attribute '%s'") % |
539 |
- (_unicode_decode(dest), _unicode_decode(attr))) |
540 |
- else: |
541 |
+ for attr in attrs: |
542 |
+ if exclude(attr): |
543 |
+ continue |
544 |
try: |
545 |
- with open(_os.devnull, 'wb') as f: |
546 |
- subprocess.call(["getfattr", "--version"], stdout=f) |
547 |
- subprocess.call(["setfattr", "--version"], stdout=f) |
548 |
- except OSError: |
549 |
- def _copyxattr(src, dest, exclude=None): |
550 |
- # TODO: implement exclude |
551 |
- getfattr_process = subprocess.Popen(["getfattr", "-d", "--absolute-names", src], stdout=subprocess.PIPE) |
552 |
- getfattr_process.wait() |
553 |
- extended_attributes = getfattr_process.stdout.readlines() |
554 |
- getfattr_process.stdout.close() |
555 |
- if extended_attributes: |
556 |
- extended_attributes[0] = b"# file: " + _unicode_encode(dest) + b"\n" |
557 |
- setfattr_process = subprocess.Popen(["setfattr", "--restore=-"], stdin=subprocess.PIPE, stderr=subprocess.PIPE) |
558 |
- setfattr_process.communicate(input=b"".join(extended_attributes)) |
559 |
- if setfattr_process.returncode != 0: |
560 |
- raise OperationNotSupported("Filesystem containing file '%s' does not support extended attributes" % dest) |
561 |
- else: |
562 |
- def _copyxattr(src, dest, exclude=None): |
563 |
- pass |
564 |
+ xattr.set(dest, attr, xattr.get(src, attr)) |
565 |
+ raise_exception = False |
566 |
+ except (OSError, IOError): |
567 |
+ raise_exception = True |
568 |
+ if raise_exception: |
569 |
+ raise OperationNotSupported(_("Filesystem containing file '%s' " |
570 |
+ "does not support extended attribute '%s'") % |
571 |
+ (_unicode_decode(dest), _unicode_decode(attr))) |
572 |
|
573 |
def movefile(src, dest, newmtime=None, sstat=None, mysettings=None, |
574 |
hardlink_candidates=None, encoding=_encodings['fs']): |
575 |
-- |
576 |
2.4.1 |