summaryrefslogtreecommitdiffstats
path: root/Lib/pathlib/_os.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/pathlib/_os.py')
-rw-r--r--Lib/pathlib/_os.py99
1 files changed, 98 insertions, 1 deletions
diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py
index 61923b5..164ee8e 100644
--- a/Lib/pathlib/_os.py
+++ b/Lib/pathlib/_os.py
@@ -2,7 +2,7 @@
Low-level OS functionality wrappers used by pathlib.
"""
-from errno import EBADF, EOPNOTSUPP, ETXTBSY, EXDEV
+from errno import *
import os
import stat
import sys
@@ -178,3 +178,100 @@ def copyfileobj(source_f, target_f):
write_target = target_f.write
while buf := read_source(1024 * 1024):
write_target(buf)
+
+
+# Kinds of metadata supported by the operating system.
+file_metadata_keys = {'mode', 'times_ns'}
+if hasattr(os.stat_result, 'st_flags'):
+ file_metadata_keys.add('flags')
+if hasattr(os, 'listxattr'):
+ file_metadata_keys.add('xattrs')
+file_metadata_keys = frozenset(file_metadata_keys)
+
+
+def read_file_metadata(path, keys=None, *, follow_symlinks=True):
+ """
+ Returns local path metadata as a dict with string keys.
+ """
+ if keys is None:
+ keys = file_metadata_keys
+ assert keys.issubset(file_metadata_keys)
+ result = {}
+ for key in keys:
+ if key == 'xattrs':
+ try:
+ result['xattrs'] = [
+ (attr, os.getxattr(path, attr, follow_symlinks=follow_symlinks))
+ for attr in os.listxattr(path, follow_symlinks=follow_symlinks)]
+ except OSError as err:
+ if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
+ raise
+ continue
+ st = os.stat(path, follow_symlinks=follow_symlinks)
+ if key == 'mode':
+ result['mode'] = stat.S_IMODE(st.st_mode)
+ elif key == 'times_ns':
+ result['times_ns'] = st.st_atime_ns, st.st_mtime_ns
+ elif key == 'flags':
+ result['flags'] = st.st_flags
+ return result
+
+
+def write_file_metadata(path, metadata, *, follow_symlinks=True):
+ """
+ Sets local path metadata from the given dict with string keys.
+ """
+ assert frozenset(metadata.keys()).issubset(file_metadata_keys)
+
+ def _nop(*args, ns=None, follow_symlinks=None):
+ pass
+
+ if follow_symlinks:
+ # use the real function if it exists
+ def lookup(name):
+ return getattr(os, name, _nop)
+ else:
+ # use the real function only if it exists
+ # *and* it supports follow_symlinks
+ def lookup(name):
+ fn = getattr(os, name, _nop)
+ if fn in os.supports_follow_symlinks:
+ return fn
+ return _nop
+
+ times_ns = metadata.get('times_ns')
+ if times_ns is not None:
+ lookup("utime")(path, ns=times_ns, follow_symlinks=follow_symlinks)
+ # We must copy extended attributes before the file is (potentially)
+ # chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
+ xattrs = metadata.get('xattrs')
+ if xattrs is not None:
+ for attr, value in xattrs:
+ try:
+ os.setxattr(path, attr, value, follow_symlinks=follow_symlinks)
+ except OSError as e:
+ if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
+ raise
+ mode = metadata.get('mode')
+ if mode is not None:
+ try:
+ lookup("chmod")(path, mode, follow_symlinks=follow_symlinks)
+ except NotImplementedError:
+ # if we got a NotImplementedError, it's because
+ # * follow_symlinks=False,
+ # * lchown() is unavailable, and
+ # * either
+ # * fchownat() is unavailable or
+ # * fchownat() doesn't implement AT_SYMLINK_NOFOLLOW.
+ # (it returned ENOSUP.)
+ # therefore we're out of options--we simply cannot chown the
+ # symlink. give up, suppress the error.
+ # (which is what shutil always did in this circumstance.)
+ pass
+ flags = metadata.get('flags')
+ if flags is not None:
+ try:
+ lookup("chflags")(path, flags, follow_symlinks=follow_symlinks)
+ except OSError as why:
+ if why.errno not in (EOPNOTSUPP, ENOTSUP):
+ raise