From f3c0d02ffeda8803a7cda15a028db039a2df34f8 Mon Sep 17 00:00:00 2001 From: Alex Drozd Date: Wed, 19 Jul 2023 22:52:50 +0200 Subject: [PATCH] adding support for resolved symlinks for local to remote operations in shutil.py::copytree --- src/smbclient/shutil.py | 29 ++++++++++++++++++++++++----- tests/test_smbclient_shutil.py | 10 +++++++++- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/smbclient/shutil.py b/src/smbclient/shutil.py index c9d7aefe..6b9ad326 100644 --- a/src/smbclient/shutil.py +++ b/src/smbclient/shutil.py @@ -293,6 +293,16 @@ def copytree( :param kwargs: Common arguments used to build the SMB Session for any UNC paths. :return: The dst path. """ + + class LocalDirEntry: + """Mimics the structure of os.DirEntry which is not exposed https://github.com/python/cpython/issues/71225""" + + def __init__(self, path) -> None: + self.path = path + + def is_dir(self): + return os.path.isdir(self.path) + if is_remote_path(src): dir_entries = list(scandir(src, **kwargs)) else: @@ -315,20 +325,29 @@ def copytree( src_path = _join_local_or_remote_path(src, dir_entry.name) dst_path = _join_local_or_remote_path(dst, dir_entry.name) + is_smb_dir_entry = isinstance(dir_entry, SMBDirEntry) + try: if dir_entry.is_symlink(): - if not isinstance(dir_entry, SMBDirEntry): - raise AssertionError("copytree doesn't yet support symlinks for local to remote operations") - - link_target = readlink(src_path, **kwargs) + if is_smb_dir_entry: + link_target = readlink(src_path, **kwargs) + else: + link_target = os.readlink(src_path) if symlinks: + if not is_smb_dir_entry: + raise AssertionError( + "copytree doesn't yet support unresolved symlinks for local to remote operations" + ) symlink(link_target, dst_path, **kwargs) copystat(src_path, dst_path, follow_symlinks=False) continue else: # Manually override the dir_entry with a new one that is the link target and copy that below. try: - dir_entry = SMBDirEntry.from_path(link_target, **kwargs) + if not is_remote_path(link_target): + dir_entry = LocalDirEntry(link_target) + else: + dir_entry = SMBDirEntry.from_path(link_target, **kwargs) except OSError as err: if err.errno == errno.ENOENT and ignore_dangling_symlinks: continue diff --git a/tests/test_smbclient_shutil.py b/tests/test_smbclient_shutil.py index 5b8fd56a..d03bc6f5 100644 --- a/tests/test_smbclient_shutil.py +++ b/tests/test_smbclient_shutil.py @@ -1188,12 +1188,18 @@ def test_copytree_with_local_src(smb_share, tmp_path): with open(os.path.join(src_dirname, "dir1", "subdir1", "file3.txt"), mode="w") as fd: fd.write("file3.txt") + path_to_symlink = os.path.join(src_dirname, "dir1", "subdir1", "symlink.txt") + path_to_symlink_target = os.path.join(tmp_path, "file4.txt") + with open(path_to_symlink_target, mode="w") as fd: + fd.write("file4.txt") + os.symlink(path_to_symlink, path_to_symlink_target) + actual = copytree(src_dirname, dst_dirname) assert actual == dst_dirname assert sorted(list(listdir(dst_dirname))) == ["dir1", "file1.txt"] assert sorted(list(listdir("%s\\dir1" % dst_dirname))) == ["file2.txt", "subdir1"] - assert sorted(list(listdir("%s\\dir1\\subdir1" % dst_dirname))) == ["file3.txt"] + assert sorted(list(listdir("%s\\dir1\\subdir1" % dst_dirname))) == ["file3.txt", "file4.txt"] with open_file("%s\\file1.txt" % dst_dirname) as fd: assert fd.read() == "file1.txt" @@ -1201,6 +1207,8 @@ def test_copytree_with_local_src(smb_share, tmp_path): assert fd.read() == "file2.txt" with open_file("%s\\dir1\\subdir1\\file3.txt" % dst_dirname) as fd: assert fd.read() == "file3.txt" + with open_file("%s\\dir1\\subdir1\\file4.txt" % dst_dirname) as fd: + assert fd.read() == "file4.txt" @pytest.mark.skipif(