Initial commit: Debian Manager — управление Debian-серверами
- SSH/SFTP управление без агентов - Пакеты (apt), Docker, файлы, systemd, ufw, cron - Плейбуки, групповые операции, планировщик - Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy - Терминал, Telegram-уведомления, тёмная/светлая тема
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user