Skip to content

Commit af02665

Browse files
authored
Merge pull request #3784 from Flamefire/avoid_recursive_copies
Check for recursive symlinks by default before copying a folder
2 parents 616abca + 9fd7157 commit af02665

File tree

2 files changed

+83
-2
lines changed

2 files changed

+83
-2
lines changed

easybuild/tools/filetools.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import hashlib
4545
import imp
4646
import inspect
47+
import itertools
4748
import os
4849
import re
4950
import shutil
@@ -2340,14 +2341,36 @@ def copy_files(paths, target_path, force_in_dry_run=False, target_single_file=Fa
23402341
raise EasyBuildError("One or more files to copy should be specified!")
23412342

23422343

2343-
def copy_dir(path, target_path, force_in_dry_run=False, dirs_exist_ok=False, **kwargs):
2344+
def has_recursive_symlinks(path):
2345+
"""
2346+
Check the given directory for recursive symlinks.
2347+
2348+
That means symlinks to folders inside the path which would cause infinite loops when traversed regularily.
2349+
2350+
:param path: Path to directory to check
2351+
"""
2352+
for dirpath, dirnames, filenames in os.walk(path, followlinks=True):
2353+
for name in itertools.chain(dirnames, filenames):
2354+
fullpath = os.path.join(dirpath, name)
2355+
if os.path.islink(fullpath):
2356+
linkpath = os.path.realpath(fullpath)
2357+
fullpath += os.sep # To catch the case where both are equal
2358+
if fullpath.startswith(linkpath + os.sep):
2359+
_log.info("Recursive symlink detected at %s", fullpath)
2360+
return True
2361+
return False
2362+
2363+
2364+
def copy_dir(path, target_path, force_in_dry_run=False, dirs_exist_ok=False, check_for_recursive_symlinks=True,
2365+
**kwargs):
23442366
"""
23452367
Copy a directory from specified location to specified location
23462368
23472369
:param path: the original directory path
23482370
:param target_path: path to copy the directory to
23492371
:param force_in_dry_run: force running the command during dry run
23502372
:param dirs_exist_ok: boolean indicating whether it's OK if the target directory already exists
2373+
:param check_for_recursive_symlinks: If symlink arg is not given or False check for recursive symlinks first
23512374
23522375
shutil.copytree is used if the target path does not exist yet;
23532376
if the target path already exists, the 'copy' function will be used to copy the contents of
@@ -2359,6 +2382,13 @@ def copy_dir(path, target_path, force_in_dry_run=False, dirs_exist_ok=False, **k
23592382
dry_run_msg("copied directory %s to %s" % (path, target_path))
23602383
else:
23612384
try:
2385+
if check_for_recursive_symlinks and not kwargs.get('symlinks'):
2386+
if has_recursive_symlinks(path):
2387+
raise EasyBuildError("Recursive symlinks detected in %s. "
2388+
"Will not try copying this unless `symlinks=True` is passed",
2389+
path)
2390+
else:
2391+
_log.debug("No recursive symlinks in %s", path)
23622392
if not dirs_exist_ok and os.path.exists(target_path):
23632393
raise EasyBuildError("Target location %s to copy %s to already exists", target_path, path)
23642394

@@ -2386,7 +2416,9 @@ def copy_dir(path, target_path, force_in_dry_run=False, dirs_exist_ok=False, **k
23862416
paths_to_copy = [os.path.join(path, x) for x in entries]
23872417

23882418
copy(paths_to_copy, target_path,
2389-
force_in_dry_run=force_in_dry_run, dirs_exist_ok=dirs_exist_ok, **kwargs)
2419+
force_in_dry_run=force_in_dry_run, dirs_exist_ok=dirs_exist_ok,
2420+
check_for_recursive_symlinks=False, # Don't check again
2421+
**kwargs)
23902422

23912423
else:
23922424
# if dirs_exist_ok is not enabled or target directory doesn't exist, just use shutil.copytree

test/framework/filetools.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1790,6 +1790,46 @@ def test_copy_files(self):
17901790
regex = re.compile("^copied 2 files to .*/target")
17911791
self.assertTrue(regex.match(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout))
17921792

1793+
def test_has_recursive_symlinks(self):
1794+
"""Test has_recursive_symlinks function"""
1795+
test_folder = tempfile.mkdtemp()
1796+
self.assertFalse(ft.has_recursive_symlinks(test_folder))
1797+
# Clasic Loop: Symlink to .
1798+
os.symlink('.', os.path.join(test_folder, 'self_link_dot'))
1799+
self.assertTrue(ft.has_recursive_symlinks(test_folder))
1800+
# Symlink to self
1801+
test_folder = tempfile.mkdtemp()
1802+
os.symlink('self_link', os.path.join(test_folder, 'self_link'))
1803+
self.assertTrue(ft.has_recursive_symlinks(test_folder))
1804+
# Symlink from 2 folders up
1805+
test_folder = tempfile.mkdtemp()
1806+
sub_folder = os.path.join(test_folder, 'sub1', 'sub2')
1807+
os.makedirs(sub_folder)
1808+
os.symlink(os.path.join('..', '..'), os.path.join(sub_folder, 'uplink'))
1809+
self.assertTrue(ft.has_recursive_symlinks(test_folder))
1810+
# Non-issue: Symlink to sibling folders
1811+
test_folder = tempfile.mkdtemp()
1812+
sub_folder = os.path.join(test_folder, 'sub1', 'sub2')
1813+
os.makedirs(sub_folder)
1814+
sibling_folder = os.path.join(test_folder, 'sub1', 'sibling')
1815+
os.mkdir(sibling_folder)
1816+
os.symlink('sibling', os.path.join(test_folder, 'sub1', 'sibling_link'))
1817+
os.symlink(os.path.join('..', 'sibling'), os.path.join(test_folder, sub_folder, 'sibling_link'))
1818+
self.assertFalse(ft.has_recursive_symlinks(test_folder))
1819+
# Tricky case: Sibling symlink to folder starting with the same name
1820+
os.mkdir(os.path.join(test_folder, 'sub11'))
1821+
os.symlink(os.path.join('..', 'sub11'), os.path.join(test_folder, 'sub1', 'trick_link'))
1822+
self.assertFalse(ft.has_recursive_symlinks(test_folder))
1823+
# Symlink cycle: sub1/cycle_2 -> sub2, sub2/cycle_1 -> sub1, ...
1824+
test_folder = tempfile.mkdtemp()
1825+
sub_folder1 = os.path.join(test_folder, 'sub1')
1826+
sub_folder2 = sub_folder = os.path.join(test_folder, 'sub2')
1827+
os.mkdir(sub_folder1)
1828+
os.mkdir(sub_folder2)
1829+
os.symlink(os.path.join('..', 'sub2'), os.path.join(sub_folder1, 'cycle_1'))
1830+
os.symlink(os.path.join('..', 'sub1'), os.path.join(sub_folder2, 'cycle_2'))
1831+
self.assertTrue(ft.has_recursive_symlinks(test_folder))
1832+
17931833
def test_copy_dir(self):
17941834
"""Test copy_dir function."""
17951835
testdir = os.path.dirname(os.path.abspath(__file__))
@@ -1861,6 +1901,15 @@ def ignore_func(_, names):
18611901
ft.mkdir(subdir)
18621902
ft.copy_dir(srcdir, target_dir, symlinks=True, dirs_exist_ok=True)
18631903

1904+
# Detect recursive symlinks by default instead of infinite loop during copy
1905+
ft.remove_dir(target_dir)
1906+
os.symlink('.', os.path.join(subdir, 'recursive_link'))
1907+
self.assertErrorRegex(EasyBuildError, 'Recursive symlinks detected', ft.copy_dir, srcdir, target_dir)
1908+
self.assertFalse(os.path.exists(target_dir))
1909+
# Ok for symlinks=True
1910+
ft.copy_dir(srcdir, target_dir, symlinks=True)
1911+
self.assertTrue(os.path.exists(target_dir))
1912+
18641913
# also test behaviour of copy_file under --dry-run
18651914
build_options = {
18661915
'extended_dry_run': True,

0 commit comments

Comments
 (0)