diff --git a/src/maestral/main.py b/src/maestral/main.py index 0202d44b5..70c0df5de 100644 --- a/src/maestral/main.py +++ b/src/maestral/main.py @@ -211,9 +211,6 @@ def __init__( @staticmethod def _check_system_compatibility() -> None: - if os.stat not in os.supports_follow_symlinks: - raise RuntimeError("Maestral requires lstat support") - if not (IS_MACOS or IS_LINUX): raise RuntimeError("Only macOS and Linux are supported") diff --git a/src/maestral/utils/path.py b/src/maestral/utils/path.py index eeae285aa..49efc49c1 100644 --- a/src/maestral/utils/path.py +++ b/src/maestral/utils/path.py @@ -370,7 +370,9 @@ def move( also a folder. :param raise_error: Whether to raise errors or return them. :param preserve_dest_permissions: Whether to apply the permissions of the source - path to the destination path. Permissions will not be set recursively. + path to the destination path. Permissions will not be set recursively and may + will be set for symlinks if this is not supported by the platform, i.e., if + ``os.chmod not in os.supports_follow_symlinks``. :returns: Any caught exception during the move. """ err: Optional[OSError] = None @@ -380,7 +382,7 @@ def move( # save dest permissions try: orig_mode = os.lstat(dest_path).st_mode & 0o777 - except FileNotFoundError: + except (FileNotFoundError, NotADirectoryError): pass try: @@ -392,12 +394,11 @@ def move( err = exc else: if orig_mode: - # reapply dest permissions + # Reapply dest permissions. If the dest is a symlink, only apply permissions + # if this is supported for symlinks by the platform. try: if os.chmod in os.supports_follow_symlinks: os.chmod(dest_path, orig_mode, follow_symlinks=False) - else: - os.chmod(dest_path, orig_mode) except OSError: pass @@ -530,36 +531,32 @@ def opener_no_symlink(path: _AnyPath, flags: int) -> int: return os.open(path, flags=flags) -def _get_stats_no_symlink(path: _AnyPath) -> Optional[os.stat_result]: - try: - return os.lstat(path) - except (FileNotFoundError, NotADirectoryError): - return None - - def exists(path: _AnyPath) -> bool: """Returns whether an item exists at the path. Returns True for symlinks.""" - return _get_stats_no_symlink(path) is not None + try: + os.lstat(path) + except (FileNotFoundError, NotADirectoryError): + return False + else: + return True def isfile(path: _AnyPath) -> bool: """Returns whether a file exists at the path. Returns True for symlinks.""" - stat = _get_stats_no_symlink(path) - - if stat is None: - return False - else: + try: + stat = os.lstat(path) return not S_ISDIR(stat.st_mode) + except (FileNotFoundError, NotADirectoryError): + return False def isdir(path: _AnyPath) -> bool: """Returns whether a folder exists at the path. Returns False for symlinks.""" - stat = _get_stats_no_symlink(path) - - if stat is None: - return False - else: + try: + stat = os.lstat(path) return S_ISDIR(stat.st_mode) + except (FileNotFoundError, NotADirectoryError): + return False def getsize(path: _AnyPath) -> int: diff --git a/tests/linked/integration/test_sync.py b/tests/linked/integration/test_sync.py index 07f3cfb45..2698d3d09 100644 --- a/tests/linked/integration/test_sync.py +++ b/tests/linked/integration/test_sync.py @@ -450,6 +450,10 @@ def test_excluded_folder_cleared_on_deletion(m: Maestral) -> None: assert_no_errors(m) +@pytest.mark.skipif( + os.chmod not in os.supports_follow_symlinks, + reason="chmod does not support follow_symlinks=False", +) def test_unix_permissions(m: Maestral) -> None: """ Tests that a newly downloaded file is created with default permissions for our