From 226108f4dca5171c2879ab5cf7af93cf2b430874 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 31 Aug 2026 17:19:15 +0000 Subject: [PATCH] Up-/Download gefixt: absoluter Remote-Pfad statt "/./datei" (#3) paramikos getcwd() liefert None, solange kein chdir() erfolgt ist. Der Rueckfall auf "." landete als remote_path in der App, woraus _join()/_abs() Pfade wie "/./datei" bauten. Der Server loeste die relativ zum Dateisystem- Root statt zum Home auf -> "No such file" beim Download, "Permission denied" beim Upload. Das Listing funktionierte, weil listdir(".") serverseitig relativ zum Home aufgeloest wird -- daher fiel es erst beim Transfer auf. - connect()/chdir()/listdir() loesen den Pfad ueber sftp.normalize() auf - _abs() haengt relative Pfade an das CWD an, statt blind "/" voranzustellen - _copy_tree(): die beiden mkdir-Zweige waren vertauscht. Beim Download wurde backend.mkdir() auf einen lokalen Pfad angewendet (Verzeichnis entstand auf dem Server), beim Upload os.makedirs() auf einen entfernten. Ordner-Transfers waren dadurch in beide Richtungen defekt. Verifiziert per End-to-End-Test gegen einen echten sshd/SFTP-Server: Datei- und rekursiver Ordner-Transfer in beide Richtungen. Closes #3 --- ftptui/app.py | 6 ++++-- ftptui/sftp_backend.py | 29 ++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/ftptui/app.py b/ftptui/app.py index a4f952d..8d276e0 100644 --- a/ftptui/app.py +++ b/ftptui/app.py @@ -450,7 +450,8 @@ class BrowserScreen(Screen): def _copy_tree(self, src: str, dst: str, download: bool) -> None: try: if download: - self.backend.mkdir(dst) + # dst ist ein *lokaler* Pfad -> lokal anlegen, nicht auf dem Server + os.makedirs(dst, exist_ok=True) entries = self.backend.listdir(src) for e in entries: if e.kind == "dir": @@ -460,7 +461,8 @@ class BrowserScreen(Screen): else: self.backend.download(_join(src, e.name, False), _join(dst, e.name, True)) else: - os.makedirs(dst, exist_ok=True) + # dst ist ein *entfernter* Pfad -> auf dem Server anlegen + self.backend.mkdir(dst) for item in os.scandir(src): if item.is_dir(): self._copy_tree( diff --git a/ftptui/sftp_backend.py b/ftptui/sftp_backend.py index 979a75a..fc88e8c 100644 --- a/ftptui/sftp_backend.py +++ b/ftptui/sftp_backend.py @@ -29,7 +29,21 @@ class SFTPBackend: timeout=30, ) self._sftp = self._ssh.open_sftp() - self._cwd = self._sftp.getcwd() or "." + self._cwd = self._resolve(".") + + def _resolve(self, path: str) -> str: + """Löst einen Pfad serverseitig zu einem absoluten Pfad auf. + + ``paramiko.SFTPClient.getcwd()`` liefert ``None``, solange kein + explizites ``chdir()`` erfolgt ist – ein Rückfall auf ``"."`` würde + später zu Pfaden wie ``/./datei`` führen, die der Server relativ zum + Dateisystem-Root statt zum Home-Verzeichnis auflöst. + """ + assert self._sftp is not None + try: + return self._sftp.normalize(path) + except OSError: + return path if path.startswith("/") else "/" def listdir(self, path: str) -> list[RemoteEntry]: assert self._sftp is not None @@ -45,22 +59,23 @@ class SFTPBackend: modified=attr.st_mtime, ) ) - self._cwd = path + self._cwd = self._resolve(path) return entries def chdir(self, path: str) -> None: assert self._sftp is not None self._sftp.chdir(path) - self._cwd = self._sftp.getcwd() or path + self._cwd = self._resolve(".") def pwd(self) -> str: return self._cwd def _abs(self, path: str) -> str: - """Baut einen absolut wertigen Pfad relativ zum SFTP-Root.""" - if path.startswith("/"): - return path or "/" - return "/" + path + """Baut einen absoluten Pfad – relative Pfade gelten zum aktuellen CWD.""" + if not path: + return self._cwd or "/" + base = self._cwd if self._cwd.startswith("/") else "/" + return str(PurePosixPath(base) / path) def download(self, remote: str, local: str) -> None: assert self._sftp is not None