Files
gniza4linux/tui/screens/target_edit.py
shuki 733bb5de62 Add MySQL database backup support
Dump MySQL/MariaDB databases as part of backup snapshots. Each database
is dumped to a separate gzipped file under _mysql/ in the snapshot.
Supports all-databases mode with exclude list, or explicit database
selection. Includes TUI form fields for full configuration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 02:48:47 +02:00

157 lines
7.4 KiB
Python

import re
from textual.app import ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Static, Button, Input, Select
from textual.containers import Vertical, Horizontal
from tui.config import parse_conf, write_conf, CONFIG_DIR, list_conf_dir
from tui.models import Target
from tui.widgets import FolderPicker
_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]{0,31}$')
class TargetEditScreen(Screen):
BINDINGS = [("escape", "go_back", "Back")]
def __init__(self, name: str = ""):
super().__init__()
self._edit_name = name
self._is_new = not name
def compose(self) -> ComposeResult:
yield Header()
title = "Add Target" if self._is_new else f"Edit Target: {self._edit_name}"
target = Target()
if not self._is_new:
data = parse_conf(CONFIG_DIR / "targets.d" / f"{self._edit_name}.conf")
target = Target.from_conf(self._edit_name, data)
with Vertical(id="target-edit"):
yield Static(title, id="screen-title")
if self._is_new:
yield Static("Name:")
yield Input(value="", placeholder="Target name", id="te-name")
yield Static("Folders (comma-separated):")
yield Input(value=target.folders, placeholder="/path1,/path2", id="te-folders")
yield Button("Browse...", id="btn-browse")
yield Static("Exclude patterns:")
yield Input(value=target.exclude, placeholder="*.tmp,*.log", id="te-exclude")
yield Static("Remote override:")
yield Input(value=target.remote, placeholder="Leave empty for default", id="te-remote")
yield Static("Retention override:")
yield Input(value=target.retention, placeholder="Leave empty for default", id="te-retention")
yield Static("Pre-backup hook:")
yield Input(value=target.pre_hook, placeholder="Command to run before backup", id="te-prehook")
yield Static("Post-backup hook:")
yield Input(value=target.post_hook, placeholder="Command to run after backup", id="te-posthook")
yield Static("Enabled:")
yield Select(
[("Yes", "yes"), ("No", "no")],
value="yes" if target.enabled == "yes" else "no",
id="te-enabled",
)
yield Static("--- MySQL Backup ---", classes="section-label")
yield Static("MySQL Enabled:")
yield Select(
[("No", "no"), ("Yes", "yes")],
value=target.mysql_enabled,
id="te-mysql-enabled",
)
yield Static("MySQL Mode:")
yield Select(
[("All databases", "all"), ("Select databases", "select")],
value=target.mysql_mode,
id="te-mysql-mode",
)
yield Static("Databases (comma-separated, when mode=select):")
yield Input(value=target.mysql_databases, placeholder="db1,db2", id="te-mysql-databases")
yield Static("Exclude databases (comma-separated, when mode=all):")
yield Input(value=target.mysql_exclude, placeholder="test_db,dev_db", id="te-mysql-exclude")
yield Static("MySQL User:")
yield Input(value=target.mysql_user, placeholder="Leave empty for socket/~/.my.cnf auth", id="te-mysql-user")
yield Static("MySQL Password:")
yield Input(value=target.mysql_password, placeholder="Leave empty for socket/~/.my.cnf auth", password=True, id="te-mysql-password")
yield Static("MySQL Host:")
yield Input(value=target.mysql_host, placeholder="localhost", id="te-mysql-host")
yield Static("MySQL Port:")
yield Input(value=target.mysql_port, placeholder="3306", id="te-mysql-port")
yield Static("MySQL Extra Options:")
yield Input(value=target.mysql_extra_opts, placeholder="--single-transaction --routines --triggers", id="te-mysql-extra-opts")
with Horizontal(id="te-buttons"):
yield Button("Save", variant="primary", id="btn-save")
yield Button("Cancel", id="btn-cancel")
yield Footer()
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn-cancel":
self.dismiss(None)
elif event.button.id == "btn-browse":
self.app.push_screen(
FolderPicker("Select folder to back up"),
callback=self._folder_selected,
)
elif event.button.id == "btn-save":
self._save()
def _folder_selected(self, path: str | None) -> None:
if path:
folders_input = self.query_one("#te-folders", Input)
current = folders_input.value.strip()
if current:
existing = [f.strip() for f in current.split(",")]
if path not in existing:
folders_input.value = current + "," + path
else:
folders_input.value = path
def _save(self) -> None:
if self._is_new:
name = self.query_one("#te-name", Input).value.strip()
if not name:
self.notify("Name is required", severity="error")
return
if not _NAME_RE.match(name):
self.notify("Invalid name. Use letters, digits, _ - (max 32 chars, start with letter).", severity="error")
return
conf = CONFIG_DIR / "targets.d" / f"{name}.conf"
if conf.exists():
self.notify(f"Target '{name}' already exists.", severity="error")
return
else:
name = self._edit_name
folders = self.query_one("#te-folders", Input).value.strip()
mysql_enabled = str(self.query_one("#te-mysql-enabled", Select).value)
if not folders and mysql_enabled != "yes":
self.notify("At least one folder or MySQL backup is required", severity="error")
return
target = Target(
name=name,
folders=folders,
exclude=self.query_one("#te-exclude", Input).value.strip(),
remote=self.query_one("#te-remote", Input).value.strip(),
retention=self.query_one("#te-retention", Input).value.strip(),
pre_hook=self.query_one("#te-prehook", Input).value.strip(),
post_hook=self.query_one("#te-posthook", Input).value.strip(),
enabled=str(self.query_one("#te-enabled", Select).value),
mysql_enabled=mysql_enabled,
mysql_mode=str(self.query_one("#te-mysql-mode", Select).value),
mysql_databases=self.query_one("#te-mysql-databases", Input).value.strip(),
mysql_exclude=self.query_one("#te-mysql-exclude", Input).value.strip(),
mysql_user=self.query_one("#te-mysql-user", Input).value.strip(),
mysql_password=self.query_one("#te-mysql-password", Input).value.strip(),
mysql_host=self.query_one("#te-mysql-host", Input).value.strip(),
mysql_port=self.query_one("#te-mysql-port", Input).value.strip(),
mysql_extra_opts=self.query_one("#te-mysql-extra-opts", Input).value.strip(),
)
conf = CONFIG_DIR / "targets.d" / f"{name}.conf"
write_conf(conf, target.to_conf())
self.notify(f"Target '{name}' saved.")
self.dismiss(name)
def action_go_back(self) -> None:
self.dismiss(None)