Initial commit: Debian Manager — управление Debian-серверами

- SSH/SFTP управление без агентов
- Пакеты (apt), Docker, файлы, systemd, ufw, cron
- Плейбуки, групповые операции, планировщик
- Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy
- Терминал, Telegram-уведомления, тёмная/светлая тема
This commit is contained in:
dinlo
2026-09-05 19:46:45 +08:00
commit f624fee5e0
65 changed files with 6981 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
"""Вкладка Файлы: 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)