diff --git a/CHANGELOG.md b/CHANGELOG.md index 1595377..972091c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Changelog CI now enforces curated user-facing entries and uses them for nightly and stable release notes. +- WebDAV shares that use a username with no password now connect and open folders correctly. ### Added - FTP connections can now enable explicit SSL with the AUTH SSL command. diff --git a/src/portkeydrop/protocols.py b/src/portkeydrop/protocols.py index b042516..6c3ef9f 100644 --- a/src/portkeydrop/protocols.py +++ b/src/portkeydrop/protocols.py @@ -447,6 +447,8 @@ def connect(self) -> None: "webdav_timeout": self._info.timeout, } ) + if self._info.username: + self._client.session.auth = (self._info.username, self._info.password) self._cwd = "/" self._connected = True except ImportError as e: @@ -538,14 +540,14 @@ def _parse_modified(value: object) -> datetime | None: return None @staticmethod - def _is_dir_info(info: dict) -> bool: + def _is_dir_info(info: dict, fallback_path: str = "") -> bool: for key in ("isdir", "is_dir", "directory"): value = info.get(key) if isinstance(value, bool): return value if isinstance(value, str) and value.lower() in {"true", "1", "yes", "dir", "directory"}: return True - path = str(info.get("path") or info.get("href") or info.get("name") or "") + path = str(info.get("path") or info.get("href") or info.get("name") or fallback_path) content_type = str(info.get("content_type") or info.get("content-type") or "").lower() return path.endswith("/") or content_type == "httpd/unix-directory" @@ -554,7 +556,7 @@ def _remote_file_from_info(self, info: dict, fallback_path: str = "") -> RemoteF name = str( info.get("name") or PurePosixPath(path.rstrip("/")).name or path.strip("/") or "/" ) - is_dir = self._is_dir_info(info) + is_dir = self._is_dir_info(info, fallback_path=fallback_path) try: size = 0 if is_dir else int(info.get("size") or info.get("content_length") or 0) except (TypeError, ValueError): diff --git a/tests/test_protocols.py b/tests/test_protocols.py index bf5a082..2802090 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -272,6 +272,23 @@ def test_connect_builds_webdavclient_options(self, monkeypatch): assert client.connected assert client.cwd == "/" + def test_connect_sets_session_auth_for_blank_webdav_password(self, monkeypatch): + webdav = MagicMock() + webdav.list.return_value = [] + client_class = self._install_fake_webdav(monkeypatch, webdav) + info = ConnectionInfo( + protocol=Protocol.WEBDAV, + host="dav.example.com", + username="share-token", + password="", + ) + + client = WebDAVClient(info) + client.connect() + + client_class.assert_called_once() + assert webdav.session.auth == ("share-token", "") + def test_connect_preserves_explicit_webdav_url_and_port(self, monkeypatch): webdav = MagicMock() webdav.list.return_value = [] @@ -403,6 +420,24 @@ def test_chdir_requires_existing_directory(self, monkeypatch): assert client.chdir("docs") == "/docs/" assert client.cwd == "/docs/" + def test_chdir_treats_trailing_slash_fallback_path_as_webdav_directory(self, monkeypatch): + webdav = MagicMock() + webdav.list.return_value = [] + webdav.info.return_value = { + "created": None, + "name": None, + "size": None, + "modified": "Thu, 18 Sep 2025 17:39:08 GMT", + "etag": '"68cc43bcc684e"', + "content_type": None, + } + self._install_fake_webdav(monkeypatch, webdav) + client = WebDAVClient(ConnectionInfo(protocol=Protocol.WEBDAV, host="dav.example.com")) + client.connect() + + assert client.chdir("/11 ai/") == "/11 ai/" + assert client.cwd == "/11 ai/" + def test_file_operations_delegate_to_webdavclient(self, monkeypatch, tmp_path): webdav = MagicMock() webdav.list.return_value = []