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:
84
core/i18n.py
84
core/i18n.py
@@ -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 = {
|
||||
|
||||
@@ -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("'", "'\\''") + "'"
|
||||
|
||||
Reference in New Issue
Block a user