Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 5 additions & 3 deletions src/portkeydrop/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"

Expand All @@ -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):
Expand Down
35 changes: 35 additions & 0 deletions tests/test_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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 = []
Expand Down
Loading