Files
debian-manager/app/ui/tabs/files_tab.py
T
dinlo f624fee5e0 Initial commit: Debian Manager — управление Debian-серверами
- SSH/SFTP управление без агентов
- Пакеты (apt), Docker, файлы, systemd, ufw, cron
- Плейбуки, групповые операции, планировщик
- Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy
- Терминал, Telegram-уведомления, тёмная/светлая тема
2026-09-05 19:46:45 +08:00

230 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Вкладка Файлы: SFTP-браузер с upload/download/редактором."""
import os
from pathlib import PurePosixPath
import flet as ft
from app.core import files as fcore
from app.core.ssh import SSHClient
from app.db.models import Server
from app.ui.components import confirm_dialog, snack
class FilesTab(ft.Column):
def __init__(self, page: ft.Page, srv: Server):
super().__init__(expand=True, spacing=8)
self._page = page
self.srv = srv
self._path = f"/home/{srv.username}" if srv.username != "root" else "/root"
self._client = None
self.path_text = ft.Text(self._path, size=13, weight=ft.FontWeight.BOLD)
self.list_col = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True, spacing=0)
self.progress = ft.ProgressBar(visible=False, height=4)
self.status = ft.Text("", size=12, color=ft.Colors.GREY_500)
self.controls = [
ft.Container(height=8),
ft.Row([
ft.IconButton(ft.Icons.ARROW_UPWARD, tooltip="Вверх",
on_click=self._up),
ft.IconButton(ft.Icons.HOME, tooltip="Домой", on_click=self._home),
ft.IconButton(ft.Icons.REFRESH, on_click=lambda e: self._reload()),
self.path_text,
ft.Container(expand=True),
ft.IconButton(ft.Icons.CREATE_NEW_FOLDER, tooltip="Новая папка",
on_click=self._mkdir_dialog),
ft.IconButton(ft.Icons.UPLOAD_FILE, tooltip="Загрузить файл",
on_click=self._upload_dialog),
]),
self.progress,
ft.Container(expand=True, border=ft.Border.all(1, ft.Colors.GREY_800),
border_radius=8, padding=4, content=self.list_col),
self.status,
]
# file picker для upload/download (async API flet ≥0.80)
self.picker = ft.FilePicker()
page.services.append(self.picker)
self._page.run_task(self._connect)
async def _connect(self):
try:
self._client = SSHClient(self.srv)
await self._client.connect()
await self._load()
except Exception as e:
snack(self._page, f"SFTP: {e}", error=True)
async def _load(self):
if not self._client:
return
try:
entries = await fcore.list_dir(self._client, self._path)
except PermissionError as e:
self.status.value = f"нет доступа: {e}"
self._page.update()
return
self.path_text.value = self._path
self.list_col.controls = []
for en in entries:
icon = ft.Icons.FOLDER if en.is_dir else ft.Icons.INSERT_DRIVE_FILE
size = "" if en.is_dir else self._fmt_size(en.size)
self.list_col.controls.append(
ft.ListTile(
leading=ft.Icon(icon, size=20,
color=ft.Colors.AMBER_300 if en.is_dir else None),
title=ft.Text(en.name, size=13),
subtitle=ft.Text(en.permissions, size=10,
color=ft.Colors.GREY_600),
trailing=ft.Row([
ft.Text(size, size=11, color=ft.Colors.GREY_500),
ft.IconButton(ft.Icons.DOWNLOAD, icon_size=16, tooltip="Скачать",
visible=not en.is_dir,
on_click=lambda e, p=en.path: self._download(p)),
ft.IconButton(ft.Icons.EDIT, icon_size=16, tooltip="Редактировать",
visible=not en.is_dir,
on_click=lambda e, p=en.path: self._edit(p)),
ft.IconButton(ft.Icons.DRIVE_FILE_RENAME_OUTLINE, icon_size=16,
tooltip="Переименовать",
on_click=lambda e, en=en: self._rename_dialog(en)),
ft.IconButton(ft.Icons.DELETE_OUTLINE, icon_size=16, tooltip="Удалить",
on_click=lambda e, en=en: self._delete(en)),
], spacing=0, tight=True),
dense=True,
on_click=lambda e, en=en: self._enter(en),
)
)
self.status.value = f"{len(entries)} объектов"
self._page.update()
def _fmt_size(self, s):
for unit in ("Б", "КБ", "МБ", "ГБ"):
if s < 1024 or unit == "ГБ":
return f"{s:.0f} {unit}" if unit == "Б" else f"{s:.1f} {unit}"
s /= 1024
def _reload(self):
self._page.run_task(self._load)
def _enter(self, en):
if en.is_dir:
self._path = en.path
self._reload()
else:
self._edit(en.path)
def _up(self, e):
parent = str(PurePosixPath(self._path).parent)
self._path = parent or "/"
self._reload()
def _home(self, e):
self._path = f"/home/{self.srv.username}" if self.srv.username != "root" else "/root"
self._reload()
# ---------- действия ----------
def _delete(self, en):
def do():
async def _d():
await fcore.delete(self._client, en.path, en.is_dir)
await self._load()
self._page.run_task(_d)
confirm_dialog(self._page, f"Удалить {en.name}?",
f"{'Папка и всё содержимое' if en.is_dir else 'Файл'} будет удалён с сервера.", do)
def _rename_dialog(self, en):
tf = ft.TextField(label="Новое имя", value=en.name, autofocus=True)
def do(e):
async def _r():
new = str(PurePosixPath(self._path) / tf.value)
await fcore.rename(self._client, en.path, new)
self._page.pop_dialog()
await self._load()
self._page.run_task(_r)
dlg = ft.AlertDialog(modal=True, title=ft.Text("Переименовать"), content=tf,
actions=[ft.TextButton("Отмена", on_click=lambda e: self._close(dlg)),
ft.FilledButton("Ок", on_click=do)])
self._page.show_dialog(dlg)
def _mkdir_dialog(self, e):
tf = ft.TextField(label="Имя папки", autofocus=True)
def do(e):
async def _m():
await fcore.mkdir(self._client, str(PurePosixPath(self._path) / tf.value))
self._page.pop_dialog()
await self._load()
self._page.run_task(_m)
dlg = ft.AlertDialog(modal=True, title=ft.Text("Новая папка"), content=tf,
actions=[ft.TextButton("Отмена", on_click=lambda e: self._close(dlg)),
ft.FilledButton("Создать", on_click=do)])
self._page.show_dialog(dlg)
def _close(self, dlg):
self._page.pop_dialog()
def _download(self, remote_path):
async def _do():
local = await self.picker.save_file(
dialog_title="Сохранить как",
file_name=PurePosixPath(remote_path).name,
)
if not local:
return
self.progress.visible = True
self._page.update()
def prog(done, total):
self.progress.value = done / total if total else None
self.status.value = f"скачивание… {done//1024}/{total//1024} КБ"
self._page.update()
await fcore.download(self._client, remote_path, local, prog)
self.progress.visible = False
self.status.value = f"скачано: {local}"
self._page.update()
self._page.run_task(_do)
def _upload_dialog(self, e):
async def _do():
files = await self.picker.pick_files(dialog_title="Выберите файл для загрузки")
if not files:
return
f = files[0]
remote = str(PurePosixPath(self._path) / f.name)
self.progress.visible = True
self._page.update()
def prog(done, total):
self.progress.value = done / total if total else None
self.status.value = f"загрузка… {done//1024}/{total//1024} КБ"
self._page.update()
await fcore.upload(self._client, f.path, remote, prog)
self.progress.visible = False
await self._load()
self._page.run_task(_do)
def _edit(self, path):
tf = ft.TextField(multiline=True, min_lines=20, max_lines=20,
text_size=12, text_style=ft.TextStyle(font_family="Consolas"),
expand=True)
sudo_cb = ft.Checkbox(label="sudo (root-файл)", value=False)
async def _load_text():
try:
tf.value = await fcore.read_text(self._client, path)
dlg.content.update()
except Exception as ex:
tf.value = f"// ошибка чтения: {ex}"
dlg.content.update()
def save(e):
async def _s():
await fcore.write_text(self._client, path, tf.value, sudo_cb.value)
self._page.pop_dialog()
snack(self._page, "Сохранено")
self._page.run_task(_s)
dlg = ft.AlertDialog(
modal=True, title=ft.Text(path, size=13),
content=ft.Container(width=700, content=ft.Column([sudo_cb, tf], tight=True)),
actions=[ft.TextButton("Отмена", on_click=lambda e: self._close(dlg)),
ft.FilledButton("Сохранить", on_click=save)],
)
self._page.show_dialog(dlg)
self._page.run_task(_load_text)