v1.7.0: dual-pane SFTP file manager

Replace primitive upload/download form with a full dual-pane file manager
(local + remote) featuring directory browsing, navigation history,
file operations (upload, download, mkdir, delete, rename), progress
tracking, and persistent SFTP sessions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-02-23 16:09:55 -05:00
parent c95ce8119b
commit a77ca6fee7
6 changed files with 912 additions and 139 deletions

View File

@@ -308,5 +308,78 @@ class SSHClientWrapper:
return f"Key generated: {self.key_path}"
class SFTPSession:
"""Persistent SFTP session for file browser."""
def __init__(self, server: dict, key_path: str):
self.server = server
self.key_path = key_path
self._client: paramiko.SSHClient | None = None
self._sftp: paramiko.SFTPClient | None = None
@property
def connected(self) -> bool:
try:
return (
self._client is not None
and self._sftp is not None
and self._client.get_transport() is not None
and self._client.get_transport().is_active()
)
except Exception:
return False
def connect(self):
self._client = _connect_client(self.server, self.key_path)
self._sftp = self._client.open_sftp()
def disconnect(self):
if self._sftp:
try:
self._sftp.close()
except Exception:
pass
self._sftp = None
if self._client:
try:
self._client.close()
except Exception:
pass
self._client = None
def listdir_attr(self, path: str) -> list:
return self._sftp.listdir_attr(path)
def stat(self, path: str):
return self._sftp.stat(path)
def mkdir(self, path: str):
self._sftp.mkdir(path)
def rmdir(self, path: str):
self._sftp.rmdir(path)
def remove(self, path: str):
self._sftp.remove(path)
def rename(self, old: str, new: str):
self._sftp.rename(old, new)
def upload(self, local_path: str, remote_path: str, progress_cb=None):
if progress_cb:
self._sftp.put(local_path, remote_path, callback=progress_cb)
else:
self._sftp.put(local_path, remote_path)
def download(self, remote_path: str, local_path: str, progress_cb=None):
if progress_cb:
self._sftp.get(remote_path, local_path, callback=progress_cb)
else:
self._sftp.get(remote_path, local_path)
def normalize(self, path: str) -> str:
return self._sftp.normalize(path)
def _shell_quote(s: str) -> str:
return "'" + s.replace("'", "'\\''") + "'"