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

@@ -247,6 +247,34 @@ _EN = {
# Network interface
"network_interface": "Network Interface",
"auto_default": "Auto (default)",
# File browser
"file_browser": "File Browser",
"local_files": "Local",
"remote_files": "Remote",
"connect_to_browse": "Select a server to browse files",
"connecting_sftp": "Connecting...",
"connected_sftp": "Connected to {alias}",
"disconnected_sftp": "Disconnected",
"sftp_error": "SFTP error: {e}",
"uploading": "Uploading: {name}",
"downloading": "Downloading: {name}",
"transfer_done": "Transfer complete: {name}",
"transfer_failed": "Transfer failed: {e}",
"new_folder": "New Folder",
"new_folder_name": "Folder name:",
"delete_files": "Delete",
"delete_files_confirm": "Delete {count} item(s)?",
"rename_file": "Rename",
"rename_prompt": "New name:",
"permission_denied": "Permission denied: {path}",
"name_col": "Name",
"size_col": "Size",
"date_col": "Date",
"perm_col": "Perm",
"parent_dir": "Parent directory",
"refresh_files": "Refresh",
"items_count": "{count} items",
}
_RU = {
@@ -471,6 +499,34 @@ _RU = {
# Network interface
"network_interface": "Сетевой интерфейс",
"auto_default": "Авто (по умолчанию)",
# File browser
"file_browser": "Файл-менеджер",
"local_files": "Локальные",
"remote_files": "Удалённые",
"connect_to_browse": "Выберите сервер для просмотра файлов",
"connecting_sftp": "Подключение...",
"connected_sftp": "Подключено к {alias}",
"disconnected_sftp": "Отключено",
"sftp_error": "Ошибка SFTP: {e}",
"uploading": "Загрузка: {name}",
"downloading": "Скачивание: {name}",
"transfer_done": "Передача завершена: {name}",
"transfer_failed": "Ошибка передачи: {e}",
"new_folder": "Новая папка",
"new_folder_name": "Имя папки:",
"delete_files": "Удалить",
"delete_files_confirm": "Удалить {count} элемент(ов)?",
"rename_file": "Переименовать",
"rename_prompt": "Новое имя:",
"permission_denied": "Нет доступа: {path}",
"name_col": "Имя",
"size_col": "Размер",
"date_col": "Дата",
"perm_col": "Права",
"parent_dir": "Родительская папка",
"refresh_files": "Обновить",
"items_count": "{count} элементов",
}
_ZH = {
@@ -695,6 +751,34 @@ _ZH = {
# Network interface
"network_interface": "网络接口",
"auto_default": "自动(默认)",
# File browser
"file_browser": "文件管理器",
"local_files": "本地",
"remote_files": "远程",
"connect_to_browse": "选择服务器以浏览文件",
"connecting_sftp": "连接中...",
"connected_sftp": "已连接到 {alias}",
"disconnected_sftp": "已断开",
"sftp_error": "SFTP错误{e}",
"uploading": "上传中:{name}",
"downloading": "下载中:{name}",
"transfer_done": "传输完成:{name}",
"transfer_failed": "传输失败:{e}",
"new_folder": "新建文件夹",
"new_folder_name": "文件夹名称:",
"delete_files": "删除",
"delete_files_confirm": "删除 {count} 个项目?",
"rename_file": "重命名",
"rename_prompt": "新名称:",
"permission_denied": "权限被拒绝:{path}",
"name_col": "名称",
"size_col": "大小",
"date_col": "日期",
"perm_col": "权限",
"parent_dir": "上级目录",
"refresh_files": "刷新",
"items_count": "{count} 个项目",
}
_TRANSLATIONS = {

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("'", "'\\''") + "'"