From 0914b22cd3ebb955b2ea17dcae593fa5242ac614 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 22 Oct 2024 12:47:30 +0200 Subject: [PATCH] Workaround for isabs() on Windows + Python 3.13 (#652) Fixes https://github.com/giampaolo/pyftpdlib/issues/650. Starting from Python 3.13, `os.path.isabs("/foo")` on Windows return `False`. This causes many file operations on Windows to fail with "Permission denied". This PR makes `os.path.isabs()` on Windows behave like in Python <= 3.12. --- .github/workflows/tests.yml | 2 +- HISTORY.rst | 4 ++++ pyftpdlib/filesystems.py | 16 +++++++++++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4316ed60..7d9815d3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11"] + python-version: ["3.13"] os: [ubuntu-latest, windows-latest, macos-latest] steps: diff --git a/HISTORY.rst b/HISTORY.rst index 020ff0ba..296a6520 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,10 @@ Version: 2.1.0 - (IN DEVELOPMENT) for local development. They can also be installed via ``pip install .[test]`` and ``pip install .[dev]``. +**Bug fixes** + +* #650: file operations on Windows with Python 3.13 give "Permission denied". + Version: 2.0.0 - 2024-09-04 =========================== diff --git a/pyftpdlib/filesystems.py b/pyftpdlib/filesystems.py index 9b9326bf..320ffe40 100644 --- a/pyftpdlib/filesystems.py +++ b/pyftpdlib/filesystems.py @@ -121,6 +121,16 @@ def cwd(self, path): # --- Pathname / conversion utilities + @staticmethod + def _isabs(path, _windows=os.name == "nt"): + # Windows + Python 3.13: isabs() changed so that a path + # starting with "/" is no longer considered absolute. + # https://github.com/python/cpython/issues/44626 + # https://github.com/python/cpython/pull/113829/ + if _windows and path.startswith("/"): + return True + return os.path.isabs(path) + def ftpnorm(self, ftppath): """Normalize a "virtual" ftp pathname (typically the raw string coming from client) depending on the current working directory. @@ -132,7 +142,7 @@ def ftpnorm(self, ftppath): Note: directory separators are system independent ("/"). Pathname returned is always absolutized. """ - if os.path.isabs(ftppath): + if self._isabs(ftppath): p = os.path.normpath(ftppath) else: p = os.path.normpath(os.path.join(self.cwd, ftppath)) @@ -148,7 +158,7 @@ def ftpnorm(self, ftppath): # Anti path traversal: don't trust user input, in the event # that self.cwd is not absolute, return "/" as a safety measure. # This is for extra protection, maybe not really necessary. - if not os.path.isabs(p): + if not self._isabs(p): p = "/" return p @@ -185,7 +195,7 @@ def fs2ftp(self, fspath): On invalid pathnames escaping from user's root directory (e.g. "/home" when root is "/home/user") always return "/". """ - if os.path.isabs(fspath): + if self._isabs(fspath): p = os.path.normpath(fspath) else: p = os.path.normpath(os.path.join(self.root, fspath))