From c445228af390737d972ac81328fd1f1a6e88f05e Mon Sep 17 00:00:00 2001 From: bugclerk <40872210+bugclerk@users.noreply.github.com> Date: Thu, 26 Sep 2024 05:39:09 -0700 Subject: [PATCH] Ensure that we preserve metadata on root directory (#14581) This commit ensures that our copytree preserves xattrs, acls, timestamp, etc from source directory on target directory. An explicit test for this is added as well. (cherry picked from commit 9062d2334b38fd4082a3a9657dc56eb336a1f615) Co-authored-by: Andrew Walker --- .../pytest/unit/utils/test_copytree.py | 21 +++++++++++----- .../middlewared/utils/filesystem/copy.py | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/middlewared/middlewared/pytest/unit/utils/test_copytree.py b/src/middlewared/middlewared/pytest/unit/utils/test_copytree.py index 433038a3a941..d59953355bb8 100644 --- a/src/middlewared/middlewared/pytest/unit/utils/test_copytree.py +++ b/src/middlewared/middlewared/pytest/unit/utils/test_copytree.py @@ -4,7 +4,6 @@ import pytest import random import stat -import subprocess from middlewared.utils.filesystem import copy from operator import eq, ne @@ -76,11 +75,20 @@ def create_test_data(target: str, symlink_target_path) -> None: Basic tree of files and directories including some symlinks """ - os.mkdir(os.path.join(target, 'SOURCE')) - create_test_files(os.path.join(target, 'SOURCE'), symlink_target_path) + source = os.path.join(target, 'SOURCE') + os.mkdir(source) + + for xat_name, xat_data in TEST_DIR_XATTRS: + os.setxattr(source, xat_name, xat_data) + + os.chown(source, JENNY + 10, JENNY + 11) + os.utime(source, ns=(JENNY + 5, JENNY + 6)) + os.chmod(source, 0o777) + + create_test_files(source, symlink_target_path) for dirname in TEST_DIRS: - path = os.path.join(target, 'SOURCE', dirname) + path = os.path.join(source, dirname) os.mkdir(path) os.chmod(path, 0o777) os.chown(path, JENNY, JENNY) @@ -249,7 +257,6 @@ def validate_the_things( def validate_copy_tree( src: str, dst: str, - flags: copy.CopyFlags ): with os.scandir(src) as it: @@ -264,6 +271,8 @@ def validate_copy_tree( if f.is_dir() and not f.is_symlink(): validate_copy_tree(new_src, new_dst, flags) + validate_the_things(src, dst, flags) + def test__copytree_default(directory_for_test, fd_count): """ test basic behavior of copytree """ @@ -293,7 +302,7 @@ def test__copytree_exclude_ctldir(directory_for_test, fd_count, is_ctldir): snapdir = os.path.join(src, '.zfs', 'snapshot', 'now') os.makedirs(snapdir) - with open(os.path.join(snapdir, 'canary'), 'w') as f: + with open(os.path.join(snapdir, 'canary'), 'w'): pass if is_ctldir: diff --git a/src/middlewared/middlewared/utils/filesystem/copy.py b/src/middlewared/middlewared/utils/filesystem/copy.py index d0b201dca93a..64595f79ca16 100644 --- a/src/middlewared/middlewared/utils/filesystem/copy.py +++ b/src/middlewared/middlewared/utils/filesystem/copy.py @@ -15,6 +15,7 @@ fchown, fstat, getxattr, + listxattr, lseek, makedev, mkdir, @@ -604,6 +605,29 @@ def copytree( try: with DirectoryIterator(src, request_mask=int(dir_request_mask), as_dict=False) as d_iter: _copytree_impl(d_iter, dst, dst_fd, CLONETREE_ROOT_DEPTH, config, fstat(dst_fd), stats) + + # Ensure that root level directory also gets metadata copied + try: + xattrs = listxattr(d_iter.dir_fd) + if config.flags.value & CopyFlags.PERMISSIONS.value: + copy_permissions(d_iter.dir_fd, dst_fd, xattrs, d_iter.stat.stx_mode) + + if config.flags.value & CopyFlags.XATTRS.value: + copy_xattrs(d_iter.dir_fd, dst_fd, xattrs) + + if config.flags.value & CopyFlags.OWNER.value: + fchown(dst_fd, d_iter.stat.stx_uid, d_iter.stat.stx_gid) + + if config.flags.value & CopyFlags.TIMESTAMPS.value: + ns_ts = ( + timespec_convert_int(d_iter.stat.stx_atime), + timespec_convert_int(d_iter.stat.stx_mtime) + ) + utime(dst_fd, ns=ns_ts) + except Exception: + if config.raise_error: + raise + finally: close(dst_fd)