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
This commit is contained in:
x3
2026-08-31 17:19:15 +00:00
parent db84a6aaf8
commit 226108f4dc
2 changed files with 26 additions and 9 deletions
+4 -2
View File
@@ -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(
+22 -7
View File
@@ -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