|  | 
| 2 | 2 | Low-level OS functionality wrappers used by pathlib. | 
| 3 | 3 | """ | 
| 4 | 4 | 
 | 
| 5 |  | -from errno import EBADF, EOPNOTSUPP, ETXTBSY, EXDEV | 
|  | 5 | +from errno import * | 
| 6 | 6 | import os | 
| 7 | 7 | import stat | 
| 8 | 8 | import sys | 
| @@ -178,3 +178,100 @@ def copyfileobj(source_f, target_f): | 
| 178 | 178 |     write_target = target_f.write | 
| 179 | 179 |     while buf := read_source(1024 * 1024): | 
| 180 | 180 |         write_target(buf) | 
|  | 181 | + | 
|  | 182 | + | 
|  | 183 | +# Kinds of metadata supported by the operating system. | 
|  | 184 | +file_metadata_keys = {'mode', 'times_ns'} | 
|  | 185 | +if hasattr(os.stat_result, 'st_flags'): | 
|  | 186 | +    file_metadata_keys.add('flags') | 
|  | 187 | +if hasattr(os, 'listxattr'): | 
|  | 188 | +    file_metadata_keys.add('xattrs') | 
|  | 189 | +file_metadata_keys = frozenset(file_metadata_keys) | 
|  | 190 | + | 
|  | 191 | + | 
|  | 192 | +def read_file_metadata(path, keys=None, *, follow_symlinks=True): | 
|  | 193 | +    """ | 
|  | 194 | +    Returns local path metadata as a dict with string keys. | 
|  | 195 | +    """ | 
|  | 196 | +    if keys is None: | 
|  | 197 | +        keys = file_metadata_keys | 
|  | 198 | +    assert keys.issubset(file_metadata_keys) | 
|  | 199 | +    result = {} | 
|  | 200 | +    for key in keys: | 
|  | 201 | +        if key == 'xattrs': | 
|  | 202 | +            try: | 
|  | 203 | +                result['xattrs'] = [ | 
|  | 204 | +                    (attr, os.getxattr(path, attr, follow_symlinks=follow_symlinks)) | 
|  | 205 | +                    for attr in os.listxattr(path, follow_symlinks=follow_symlinks)] | 
|  | 206 | +            except OSError as err: | 
|  | 207 | +                if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): | 
|  | 208 | +                    raise | 
|  | 209 | +            continue | 
|  | 210 | +        st = os.stat(path, follow_symlinks=follow_symlinks) | 
|  | 211 | +        if key == 'mode': | 
|  | 212 | +            result['mode'] = stat.S_IMODE(st.st_mode) | 
|  | 213 | +        elif key == 'times_ns': | 
|  | 214 | +            result['times_ns'] = st.st_atime_ns, st.st_mtime_ns | 
|  | 215 | +        elif key == 'flags': | 
|  | 216 | +            result['flags'] = st.st_flags | 
|  | 217 | +    return result | 
|  | 218 | + | 
|  | 219 | + | 
|  | 220 | +def write_file_metadata(path, metadata, *, follow_symlinks=True): | 
|  | 221 | +    """ | 
|  | 222 | +    Sets local path metadata from the given dict with string keys. | 
|  | 223 | +    """ | 
|  | 224 | +    assert frozenset(metadata.keys()).issubset(file_metadata_keys) | 
|  | 225 | + | 
|  | 226 | +    def _nop(*args, ns=None, follow_symlinks=None): | 
|  | 227 | +        pass | 
|  | 228 | + | 
|  | 229 | +    if follow_symlinks: | 
|  | 230 | +        # use the real function if it exists | 
|  | 231 | +        def lookup(name): | 
|  | 232 | +            return getattr(os, name, _nop) | 
|  | 233 | +    else: | 
|  | 234 | +        # use the real function only if it exists | 
|  | 235 | +        # *and* it supports follow_symlinks | 
|  | 236 | +        def lookup(name): | 
|  | 237 | +            fn = getattr(os, name, _nop) | 
|  | 238 | +            if fn in os.supports_follow_symlinks: | 
|  | 239 | +                return fn | 
|  | 240 | +            return _nop | 
|  | 241 | + | 
|  | 242 | +    times_ns = metadata.get('times_ns') | 
|  | 243 | +    if times_ns is not None: | 
|  | 244 | +        lookup("utime")(path, ns=times_ns, follow_symlinks=follow_symlinks) | 
|  | 245 | +    # We must copy extended attributes before the file is (potentially) | 
|  | 246 | +    # chmod()'ed read-only, otherwise setxattr() will error with -EACCES. | 
|  | 247 | +    xattrs = metadata.get('xattrs') | 
|  | 248 | +    if xattrs is not None: | 
|  | 249 | +        for attr, value in xattrs: | 
|  | 250 | +            try: | 
|  | 251 | +                os.setxattr(path, attr, value, follow_symlinks=follow_symlinks) | 
|  | 252 | +            except OSError as e: | 
|  | 253 | +                if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES): | 
|  | 254 | +                    raise | 
|  | 255 | +    mode = metadata.get('mode') | 
|  | 256 | +    if mode is not None: | 
|  | 257 | +        try: | 
|  | 258 | +            lookup("chmod")(path, mode, follow_symlinks=follow_symlinks) | 
|  | 259 | +        except NotImplementedError: | 
|  | 260 | +            # if we got a NotImplementedError, it's because | 
|  | 261 | +            #   * follow_symlinks=False, | 
|  | 262 | +            #   * lchown() is unavailable, and | 
|  | 263 | +            #   * either | 
|  | 264 | +            #       * fchownat() is unavailable or | 
|  | 265 | +            #       * fchownat() doesn't implement AT_SYMLINK_NOFOLLOW. | 
|  | 266 | +            #         (it returned ENOSUP.) | 
|  | 267 | +            # therefore we're out of options--we simply cannot chown the | 
|  | 268 | +            # symlink.  give up, suppress the error. | 
|  | 269 | +            # (which is what shutil always did in this circumstance.) | 
|  | 270 | +            pass | 
|  | 271 | +    flags = metadata.get('flags') | 
|  | 272 | +    if flags is not None: | 
|  | 273 | +        try: | 
|  | 274 | +            lookup("chflags")(path, flags, follow_symlinks=follow_symlinks) | 
|  | 275 | +        except OSError as why: | 
|  | 276 | +            if why.errno not in (EOPNOTSUPP, ENOTSUP): | 
|  | 277 | +                raise | 
0 commit comments