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