From f112c10577239fa09de6041fb194db40346787f2 Mon Sep 17 00:00:00 2001 From: Eli Date: Thu, 23 Jul 2026 22:17:51 +0200 Subject: [PATCH] fix(delete): resolve selected paths before removal --- src/portkeydrop/app.py | 22 +++++++++++++++++++--- src/portkeydrop/local_files.py | 6 ++++-- tests/test_app.py | 32 ++++++++++++++++++++++++++++++++ tests/test_local_files.py | 13 +++++++++++++ 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/portkeydrop/app.py b/src/portkeydrop/app.py index 004553f..cf38a46 100644 --- a/src/portkeydrop/app.py +++ b/src/portkeydrop/app.py @@ -1933,6 +1933,21 @@ def _on_delete(self, event) -> None: else: self._delete_remote() + def _resolve_remote_file_path(self, remote_file: RemoteFile) -> str: + path = (remote_file.path or remote_file.name).strip() + if not path.startswith("/"): + base = self._client.cwd if self._client else "/" + path = f"{base.rstrip('/')}/{path}" if base != "/" else f"/{path.lstrip('/')}" + if remote_file.is_dir and remote_file.path.endswith("/") and not path.endswith("/"): + path += "/" + return path or "/" + + def _resolve_local_file_path(self, remote_file: RemoteFile) -> str: + path = Path(remote_file.path or remote_file.name).expanduser() + if not path.is_absolute(): + path = Path(self._local_cwd) / path + return str(path.resolve(strict=False)) + def _delete_remote(self) -> None: f = self._get_selected_remote_file() if not f or not self._client or f.name == "..": @@ -1942,11 +1957,12 @@ def _delete_remote(self) -> None: ) if result == wx.YES: try: + target_path = self._resolve_remote_file_path(f) self._update_status(f"Deleting {f.name}...", self._client.cwd) if f.is_dir: - self._client.rmdir(f.path) + self._client.rmdir(target_path) else: - self._client.delete(f.path) + self._client.delete(target_path) self._announce(f"Deleted {f.name}") self._update_status("Delete complete.", self._client.cwd) self._play_sound_event("delete_complete") @@ -1965,7 +1981,7 @@ def _delete_local(self) -> None: ) if result == wx.YES: try: - delete_local(f.path) + delete_local(self._resolve_local_file_path(f)) self._announce(f"Deleted {f.name}") self._play_sound_event("delete_complete") self._refresh_local_files() diff --git a/src/portkeydrop/local_files.py b/src/portkeydrop/local_files.py index a57bbcd..605026b 100644 --- a/src/portkeydrop/local_files.py +++ b/src/portkeydrop/local_files.py @@ -64,8 +64,10 @@ def parent_local(current: str | Path) -> Path: def delete_local(path: str | Path) -> None: """Delete a local file or directory.""" - p = Path(path) - if p.is_dir(): + p = Path(path).expanduser() + if p.is_symlink() or p.is_file(): + p.unlink() + elif p.is_dir(): shutil.rmtree(p) else: p.unlink() diff --git a/tests/test_app.py b/tests/test_app.py index 28ab0bc..804afbe 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -851,6 +851,38 @@ def test_delete_remote_updates_status_on_success(app_module): frame._play_sound_event.assert_called_with("delete_complete") +def test_delete_remote_resolves_relative_path_against_cwd(app_module): + app, fake_wx = app_module + frame = _hydrate_frame(app_module) + frame._client = MagicMock(connected=True, cwd="/remote") + remote = MagicMock(name="doc.txt") + remote.name = "doc.txt" + remote.path = "doc.txt" + remote.is_dir = False + frame._get_selected_remote_file.return_value = remote + fake_wx.MessageBox.return_value = fake_wx.YES + + frame._delete_remote() + + frame._client.delete.assert_called_once_with("/remote/doc.txt") + + +def test_delete_local_resolves_relative_path_against_cwd(app_module): + app, fake_wx = app_module + frame = _hydrate_frame(app_module) + frame._local_cwd = "/tmp/example" + local = MagicMock(name="doc.txt") + local.name = "doc.txt" + local.path = "doc.txt" + frame._get_selected_local_file.return_value = local + fake_wx.MessageBox.return_value = fake_wx.YES + + with patch.object(app, "delete_local") as delete_local_mock: + frame._delete_local() + + delete_local_mock.assert_called_once_with("/tmp/example/doc.txt") + + def test_delete_remote_reports_failure(app_module): app, fake_wx = app_module frame = _hydrate_frame(app_module) diff --git a/tests/test_local_files.py b/tests/test_local_files.py index a7adc81..3be5213 100644 --- a/tests/test_local_files.py +++ b/tests/test_local_files.py @@ -114,6 +114,19 @@ def test_delete_dir(self, tmp_path): delete_local(d) assert not d.exists() + def test_delete_directory_symlink_unlinks_link_only(self, tmp_path): + target = tmp_path / "target" + target.mkdir() + (target / "inner.txt").write_text("x") + link = tmp_path / "target-link" + link.symlink_to(target, target_is_directory=True) + + delete_local(link) + + assert not link.exists() + assert target.exists() + assert (target / "inner.txt").exists() + def test_delete_nonexistent_raises(self, tmp_path): with pytest.raises(FileNotFoundError): delete_local(tmp_path / "nope.txt")