Skip to content

Commit 9fd7157

Browse files
committed
Check for recursive symlinks by default before copying a folder
Avoids endless loops and creating infinite length paths when `symlinks=True` is NOT also passed to copy_dir
1 parent 4e83468 commit 9fd7157

File tree

2 files changed

+23
-2
lines changed

2 files changed

+23
-2
lines changed

easybuild/tools/filetools.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2356,18 +2356,21 @@ def has_recursive_symlinks(path):
23562356
linkpath = os.path.realpath(fullpath)
23572357
fullpath += os.sep # To catch the case where both are equal
23582358
if fullpath.startswith(linkpath + os.sep):
2359+
_log.info("Recursive symlink detected at %s", fullpath)
23592360
return True
23602361
return False
23612362

23622363

2363-
def copy_dir(path, target_path, force_in_dry_run=False, dirs_exist_ok=False, **kwargs):
2364+
def copy_dir(path, target_path, force_in_dry_run=False, dirs_exist_ok=False, check_for_recursive_symlinks=True,
2365+
**kwargs):
23642366
"""
23652367
Copy a directory from specified location to specified location
23662368
23672369
:param path: the original directory path
23682370
:param target_path: path to copy the directory to
23692371
:param force_in_dry_run: force running the command during dry run
23702372
: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
23712374
23722375
shutil.copytree is used if the target path does not exist yet;
23732376
if the target path already exists, the 'copy' function will be used to copy the contents of
@@ -2379,6 +2382,13 @@ def copy_dir(path, target_path, force_in_dry_run=False, dirs_exist_ok=False, **k
23792382
dry_run_msg("copied directory %s to %s" % (path, target_path))
23802383
else:
23812384
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)
23822392
if not dirs_exist_ok and os.path.exists(target_path):
23832393
raise EasyBuildError("Target location %s to copy %s to already exists", target_path, path)
23842394

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

24082418
copy(paths_to_copy, target_path,
2409-
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)
24102422

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

test/framework/filetools.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,6 +1901,15 @@ def ignore_func(_, names):
19011901
ft.mkdir(subdir)
19021902
ft.copy_dir(srcdir, target_dir, symlinks=True, dirs_exist_ok=True)
19031903

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+
19041913
# also test behaviour of copy_file under --dry-run
19051914
build_options = {
19061915
'extended_dry_run': True,

0 commit comments

Comments
 (0)