Remove legacy gum TUI and clean up install script
- Delete all lib/ui_*.sh files (gum-based TUI) - Remove gum download from install script - Remove gum fallback and show_logo from bin/gniza - Update README to reference Textual TUI and web GUI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# gniza - Linux Backup Manager
|
||||
|
||||
A generic Linux backup tool with a Gum TUI and CLI interface. Define named backup targets (sets of directories), configure remote destinations (SSH, local, S3, Google Drive), and run incremental backups with rsync `--link-dest` deduplication.
|
||||
A generic Linux backup tool with a Python Textual TUI, web GUI, and CLI interface. Define named backup targets (sets of directories), configure remote destinations (SSH, local, S3, Google Drive), and run incremental backups with rsync `--link-dest` deduplication.
|
||||
|
||||
```
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
@@ -27,7 +27,8 @@ A generic Linux backup tool with a Gum TUI and CLI interface. Define named backu
|
||||
- **Target-based backups** - Define named profiles with sets of directories to back up
|
||||
- **Multiple remote types** - SSH, local (USB/NFS), S3, Google Drive
|
||||
- **Incremental snapshots** - rsync `--link-dest` for space-efficient deduplication
|
||||
- **Gum TUI** - Beautiful terminal UI powered by [gum](https://github.com/charmbracelet/gum)
|
||||
- **Textual TUI** - Beautiful terminal UI powered by [Textual](https://textual.textualize.io/)
|
||||
- **Web GUI** - Access the TUI from any browser via `gniza web`
|
||||
- **CLI interface** - Scriptable commands for automation and cron
|
||||
- **Atomic snapshots** - `.partial` directory during backup, renamed on success
|
||||
- **Retention policies** - Automatic pruning of old snapshots
|
||||
@@ -66,8 +67,7 @@ Root mode installs to `/usr/local/gniza`. User mode installs to `~/.local/share/
|
||||
|
||||
- **Required**: bash 4+, rsync
|
||||
- **Optional**: ssh, curl (SMTP notifications), rclone (S3/GDrive)
|
||||
|
||||
> **Note**: [gum](https://github.com/charmbracelet/gum) (TUI engine) is automatically downloaded during installation.
|
||||
- **TUI/Web**: python3, textual, textual-serve (installed automatically)
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
51
bin/gniza
51
bin/gniza
@@ -5,9 +5,6 @@ set -euo pipefail
|
||||
GNIZA_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
|
||||
export GNIZA_DIR
|
||||
|
||||
# Add bundled binaries (gum) to PATH if present
|
||||
[[ -d "$GNIZA_DIR/bin" ]] && export PATH="$GNIZA_DIR/bin:$PATH"
|
||||
|
||||
# ── Source libraries in dependency order ─────────────────────
|
||||
source "$GNIZA_DIR/lib/constants.sh"
|
||||
source "$GNIZA_DIR/lib/utils.sh"
|
||||
@@ -26,45 +23,6 @@ source "$GNIZA_DIR/lib/ssh.sh"
|
||||
source "$GNIZA_DIR/lib/rclone.sh"
|
||||
source "$GNIZA_DIR/lib/snapshot.sh"
|
||||
source "$GNIZA_DIR/lib/transfer.sh"
|
||||
source "$GNIZA_DIR/lib/ui_common.sh"
|
||||
source "$GNIZA_DIR/lib/ui_main.sh"
|
||||
source "$GNIZA_DIR/lib/ui_targets.sh"
|
||||
source "$GNIZA_DIR/lib/ui_remotes.sh"
|
||||
source "$GNIZA_DIR/lib/ui_backup.sh"
|
||||
source "$GNIZA_DIR/lib/ui_restore.sh"
|
||||
source "$GNIZA_DIR/lib/ui_snapshots.sh"
|
||||
source "$GNIZA_DIR/lib/ui_retention.sh"
|
||||
source "$GNIZA_DIR/lib/ui_logs.sh"
|
||||
source "$GNIZA_DIR/lib/ui_schedule.sh"
|
||||
source "$GNIZA_DIR/lib/ui_settings.sh"
|
||||
source "$GNIZA_DIR/lib/ui_wizard.sh"
|
||||
|
||||
# ── ASCII Logo ───────────────────────────────────────────────
|
||||
show_logo() {
|
||||
echo "${C_GREEN}"
|
||||
cat <<'LOGO'
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓▓▓
|
||||
▓▓
|
||||
LOGO
|
||||
echo "${C_RESET}"
|
||||
echo " gniza v${GNIZA4LINUX_VERSION} - Linux Backup Manager"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Help ─────────────────────────────────────────────────────
|
||||
show_help() {
|
||||
@@ -91,7 +49,7 @@ Commands:
|
||||
logs [--last] [--tail=N]
|
||||
version
|
||||
|
||||
If no command is given and gum is available, the TUI is launched.
|
||||
If no command is given, the TUI is launched.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -418,13 +376,6 @@ elif [[ "$FORCE_CLI" == "true" ]]; then
|
||||
elif python3 -c "import textual" 2>/dev/null && [[ -t 1 ]]; then
|
||||
# Python Textual TUI mode
|
||||
PYTHONPATH="$GNIZA_DIR:${PYTHONPATH:-}" exec python3 -m tui "$@"
|
||||
elif command -v gum &>/dev/null && [[ -t 1 ]]; then
|
||||
# Legacy gum TUI mode
|
||||
show_logo
|
||||
if ! has_remotes || ! has_targets; then
|
||||
ui_first_run_wizard
|
||||
fi
|
||||
ui_main_menu
|
||||
else
|
||||
# Fallback to CLI help
|
||||
run_cli
|
||||
|
||||
116
lib/ui_backup.sh
116
lib/ui_backup.sh
@@ -1,116 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gniza4linux/lib/ui_backup.sh — Backup TUI
|
||||
|
||||
[[ -n "${_GNIZA4LINUX_UI_BACKUP_LOADED:-}" ]] && return 0
|
||||
_GNIZA4LINUX_UI_BACKUP_LOADED=1
|
||||
|
||||
ui_backup_menu() {
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(ui_menu "Backup" \
|
||||
"SINGLE" "Backup single target" \
|
||||
"ALL" "Backup all targets" \
|
||||
"BACK" "Return to main menu") || return 0
|
||||
|
||||
case "$choice" in
|
||||
SINGLE) ui_backup_wizard ;;
|
||||
ALL) _ui_backup_all ;;
|
||||
BACK) return 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
ui_backup_wizard() {
|
||||
if ! has_targets; then
|
||||
ui_msgbox "No targets configured. Please add a target first."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local -a items=()
|
||||
local targets
|
||||
targets=$(list_targets)
|
||||
while IFS= read -r t; do
|
||||
items+=("$t" "Target: $t")
|
||||
done <<< "$targets"
|
||||
|
||||
local target
|
||||
target=$(ui_menu "Select Target" "${items[@]}") || return 0
|
||||
|
||||
local remote=""
|
||||
if has_remotes; then
|
||||
local -a ritems=("DEFAULT" "Use default/all remotes")
|
||||
local remotes
|
||||
remotes=$(list_remotes)
|
||||
while IFS= read -r r; do
|
||||
ritems+=("$r" "Remote: $r")
|
||||
done <<< "$remotes"
|
||||
|
||||
remote=$(ui_menu "Select Remote" "${ritems[@]}") || return 0
|
||||
[[ "$remote" == "DEFAULT" ]] && remote=""
|
||||
fi
|
||||
|
||||
local confirm_msg="Run backup?\n\nTarget: $target"
|
||||
[[ -n "$remote" ]] && confirm_msg+="\nRemote: $remote"
|
||||
confirm_msg+="\n"
|
||||
|
||||
ui_yesno "$confirm_msg" || return 0
|
||||
|
||||
_ui_run_backup "$target" "$remote"
|
||||
}
|
||||
|
||||
_ui_backup_all() {
|
||||
if ! has_targets; then
|
||||
ui_msgbox "No targets configured."
|
||||
return 0
|
||||
fi
|
||||
|
||||
ui_yesno "Backup ALL targets now?" || return 0
|
||||
|
||||
local targets
|
||||
targets=$(list_targets)
|
||||
local count=0 total=0
|
||||
total=$(echo "$targets" | wc -l)
|
||||
|
||||
local output=""
|
||||
while IFS= read -r t; do
|
||||
((count++))
|
||||
local pct=$(( count * 100 / total ))
|
||||
echo "$pct"
|
||||
local result
|
||||
if result=$(gniza --cli backup --target="$t" 2>&1); then
|
||||
output+="$t: OK\n"
|
||||
else
|
||||
output+="$t: FAILED\n$result\n"
|
||||
fi
|
||||
done <<< "$targets" | ui_gauge "Backing up all targets..."
|
||||
|
||||
ui_msgbox "Backup Results:\n\n$output"
|
||||
}
|
||||
|
||||
_ui_run_backup() {
|
||||
local target="$1"
|
||||
local remote="$2"
|
||||
|
||||
local -a cmd_args=(gniza --cli backup "--target=$target")
|
||||
[[ -n "$remote" ]] && cmd_args+=("--remote=$remote")
|
||||
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp /tmp/gniza-backup-XXXXXX.log)
|
||||
|
||||
(
|
||||
echo "10"
|
||||
if "${cmd_args[@]}" > "$tmpfile" 2>&1; then
|
||||
echo "100"
|
||||
else
|
||||
echo "100"
|
||||
fi
|
||||
) | ui_gauge "Backing up target: $target"
|
||||
|
||||
if [[ -s "$tmpfile" ]]; then
|
||||
ui_textbox "$tmpfile"
|
||||
else
|
||||
ui_msgbox "Backup of '$target' completed."
|
||||
fi
|
||||
|
||||
rm -f "$tmpfile"
|
||||
}
|
||||
187
lib/ui_common.sh
187
lib/ui_common.sh
@@ -1,187 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gniza4linux/lib/ui_common.sh — Gum TUI wrappers
|
||||
|
||||
[[ -n "${_GNIZA4LINUX_UI_COMMON_LOADED:-}" ]] && return 0
|
||||
_GNIZA4LINUX_UI_COMMON_LOADED=1
|
||||
|
||||
# Theme
|
||||
readonly _GUM_ACCENT="212"
|
||||
readonly _GUM_CURSOR="▸ "
|
||||
|
||||
# ── Internal: tag-description mapping ─────────────────────────
|
||||
# gum returns the selected display text, not a separate tag.
|
||||
# To handle duplicate descriptions, we append a zero-width space
|
||||
# marker per item (U+200B repeated i times) to guarantee uniqueness,
|
||||
# then strip the markers from the result to find the index.
|
||||
#
|
||||
# Alternatively we use a simpler approach: display "tag description"
|
||||
# and split on the double-space to extract the tag.
|
||||
# Tags in gniza are always short identifiers without spaces.
|
||||
|
||||
_gum_fmt_items() {
|
||||
local -n _tags=$1 _descs=$2
|
||||
local i
|
||||
for i in "${!_tags[@]}"; do
|
||||
printf '%s\n' "${_tags[$i]} ${_descs[$i]}"
|
||||
done
|
||||
}
|
||||
|
||||
_gum_extract_tag() {
|
||||
local line="$1"
|
||||
echo "${line%% *}"
|
||||
}
|
||||
|
||||
# ── Menu (tag/description pairs) ─────────────────────────────
|
||||
# Usage: ui_menu "Title" "TAG1" "Desc 1" "TAG2" "Desc 2" ...
|
||||
# Returns the selected TAG.
|
||||
ui_menu() {
|
||||
local title="$1"; shift
|
||||
local -a tags=() descs=()
|
||||
while [[ $# -ge 2 ]]; do
|
||||
tags+=("$1")
|
||||
descs+=("$2")
|
||||
shift 2
|
||||
done
|
||||
|
||||
local result
|
||||
result=$(_gum_fmt_items tags descs | gum choose \
|
||||
--header "$title" \
|
||||
--header.foreground "$_GUM_ACCENT" \
|
||||
--cursor.foreground "$_GUM_ACCENT" \
|
||||
--cursor "$_GUM_CURSOR") || return 1
|
||||
|
||||
_gum_extract_tag "$result"
|
||||
}
|
||||
|
||||
# ── Checklist (tag/description/status triplets, multi-select) ─
|
||||
# Usage: ui_checklist "Title" "TAG1" "Desc 1" "ON" "TAG2" "Desc 2" "OFF" ...
|
||||
# Returns space-separated quoted tags: "TAG1" "TAG2"
|
||||
ui_checklist() {
|
||||
local title="$1"; shift
|
||||
local -a tags=() descs=() selected=()
|
||||
while [[ $# -ge 3 ]]; do
|
||||
tags+=("$1")
|
||||
descs+=("$2")
|
||||
[[ "$3" == "ON" ]] && selected+=("${1} ${2}")
|
||||
shift 3
|
||||
done
|
||||
|
||||
local -a sel_args=()
|
||||
local s
|
||||
for s in "${selected[@]}"; do
|
||||
sel_args+=(--selected "$s")
|
||||
done
|
||||
|
||||
local result
|
||||
result=$(_gum_fmt_items tags descs | gum choose --no-limit \
|
||||
--header "$title" \
|
||||
--header.foreground "$_GUM_ACCENT" \
|
||||
--cursor.foreground "$_GUM_ACCENT" \
|
||||
--cursor "$_GUM_CURSOR" \
|
||||
"${sel_args[@]}") || return 1
|
||||
|
||||
local output=""
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
local tag
|
||||
tag=$(_gum_extract_tag "$line")
|
||||
[[ -n "$output" ]] && output+=" "
|
||||
output+="\"$tag\""
|
||||
done <<< "$result"
|
||||
echo "$output"
|
||||
}
|
||||
|
||||
# ── Radiolist (tag/description/status triplets, single select) ─
|
||||
# Usage: ui_radiolist "Title" "TAG1" "Desc 1" "ON" "TAG2" "Desc 2" "OFF" ...
|
||||
# Returns the selected TAG.
|
||||
ui_radiolist() {
|
||||
local title="$1"; shift
|
||||
local -a tags=() descs=()
|
||||
local preselected=""
|
||||
while [[ $# -ge 3 ]]; do
|
||||
tags+=("$1")
|
||||
descs+=("$2")
|
||||
[[ "$3" == "ON" ]] && preselected="${1} ${2}"
|
||||
shift 3
|
||||
done
|
||||
|
||||
local -a sel_args=()
|
||||
[[ -n "$preselected" ]] && sel_args=(--selected "$preselected")
|
||||
|
||||
local result
|
||||
result=$(_gum_fmt_items tags descs | gum choose \
|
||||
--header "$title" \
|
||||
--header.foreground "$_GUM_ACCENT" \
|
||||
--cursor.foreground "$_GUM_ACCENT" \
|
||||
--cursor "$_GUM_CURSOR" \
|
||||
"${sel_args[@]}") || return 1
|
||||
|
||||
_gum_extract_tag "$result"
|
||||
}
|
||||
|
||||
# ── Input box ─────────────────────────────────────────────────
|
||||
ui_inputbox() {
|
||||
local title="$1"
|
||||
local prompt="$2"
|
||||
local default="${3:-}"
|
||||
|
||||
gum input \
|
||||
--header "$prompt" \
|
||||
--header.foreground "$_GUM_ACCENT" \
|
||||
--value "$default" \
|
||||
--width 60 || return 1
|
||||
}
|
||||
|
||||
# ── Yes/No confirmation ──────────────────────────────────────
|
||||
ui_yesno() {
|
||||
local prompt="$1"
|
||||
gum confirm "$(printf '%b' "$prompt")" \
|
||||
--affirmative "Yes" \
|
||||
--negative "No"
|
||||
}
|
||||
|
||||
# ── Message box ───────────────────────────────────────────────
|
||||
ui_msgbox() {
|
||||
local msg="$1"
|
||||
echo ""
|
||||
printf '%b' "$msg" | gum style \
|
||||
--border rounded \
|
||||
--border-foreground "$_GUM_ACCENT" \
|
||||
--padding "1 2" \
|
||||
--margin "0 2"
|
||||
echo ""
|
||||
read -rsp " Press any key to continue..." -n1 < /dev/tty
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Progress gauge (reads percentage lines from stdin) ────────
|
||||
ui_gauge() {
|
||||
local prompt="$1"
|
||||
while IFS= read -r pct; do
|
||||
[[ "$pct" =~ ^[0-9]+$ ]] || continue
|
||||
local filled=$(( pct / 5 ))
|
||||
local empty=$(( 20 - filled ))
|
||||
local bar=""
|
||||
local i
|
||||
for ((i=0; i<filled; i++)); do bar+="█"; done
|
||||
for ((i=0; i<empty; i++)); do bar+="░"; done
|
||||
printf "\r %s [%s] %s%%" "$prompt" "$bar" "$pct"
|
||||
done
|
||||
printf "\033[2K\r %s [████████████████████] done\n" "$prompt"
|
||||
}
|
||||
|
||||
# ── Text file viewer ─────────────────────────────────────────
|
||||
ui_textbox() {
|
||||
local filepath="$1"
|
||||
gum pager < "$filepath"
|
||||
}
|
||||
|
||||
# ── Password input ────────────────────────────────────────────
|
||||
ui_password() {
|
||||
local prompt="$1"
|
||||
gum input \
|
||||
--password \
|
||||
--header "$prompt" \
|
||||
--header.foreground "$_GUM_ACCENT" \
|
||||
--width 60 || return 1
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gniza4linux/lib/ui_logs.sh — Log viewer TUI
|
||||
|
||||
[[ -n "${_GNIZA4LINUX_UI_LOGS_LOADED:-}" ]] && return 0
|
||||
_GNIZA4LINUX_UI_LOGS_LOADED=1
|
||||
|
||||
ui_logs_menu() {
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(ui_menu "Logs" \
|
||||
"VIEW" "View log files" \
|
||||
"STATUS" "Show backup status" \
|
||||
"BACK" "Return to main menu") || return 0
|
||||
|
||||
case "$choice" in
|
||||
VIEW) _ui_logs_view ;;
|
||||
STATUS) ui_logs_status ;;
|
||||
BACK) return 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
_ui_logs_view() {
|
||||
local log_dir="${LOG_DIR:-/var/log/gniza}"
|
||||
|
||||
if [[ ! -d "$log_dir" ]]; then
|
||||
ui_msgbox "Log directory does not exist: $log_dir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local -a items=()
|
||||
local logs
|
||||
logs=$(ls -1t "$log_dir"/gniza-*.log 2>/dev/null | head -20)
|
||||
|
||||
if [[ -z "$logs" ]]; then
|
||||
ui_msgbox "No log files found."
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS= read -r f; do
|
||||
local fname
|
||||
fname=$(basename "$f")
|
||||
local fsize
|
||||
fsize=$(stat -c%s "$f" 2>/dev/null || echo "0")
|
||||
items+=("$fname" "$(human_size "$fsize")")
|
||||
done <<< "$logs"
|
||||
items+=("BACK" "Return")
|
||||
|
||||
local selected
|
||||
selected=$(ui_menu "Log Files (recent first)" "${items[@]}") || return 0
|
||||
|
||||
[[ "$selected" == "BACK" ]] && return 0
|
||||
|
||||
local filepath="$log_dir/$selected"
|
||||
if [[ -f "$filepath" ]]; then
|
||||
ui_textbox "$filepath"
|
||||
else
|
||||
ui_msgbox "Log file not found: $filepath"
|
||||
fi
|
||||
}
|
||||
|
||||
ui_logs_status() {
|
||||
local log_dir="${LOG_DIR:-/var/log/gniza}"
|
||||
local status_msg="Backup Status Overview\n"
|
||||
status_msg+="=====================\n\n"
|
||||
|
||||
# Last backup time
|
||||
local latest_log
|
||||
latest_log=$(ls -1t "$log_dir"/gniza-*.log 2>/dev/null | head -1)
|
||||
if [[ -n "$latest_log" ]]; then
|
||||
local log_date
|
||||
log_date=$(stat -c%y "$latest_log" 2>/dev/null | cut -d. -f1)
|
||||
status_msg+="Last log: $log_date\n"
|
||||
|
||||
# Last result
|
||||
local last_line
|
||||
last_line=$(tail -1 "$latest_log" 2>/dev/null)
|
||||
status_msg+="Last entry: $last_line\n"
|
||||
else
|
||||
status_msg+="No backup logs found.\n"
|
||||
fi
|
||||
|
||||
# Disk usage
|
||||
if [[ -d "$log_dir" ]]; then
|
||||
local du_output
|
||||
du_output=$(du -sh "$log_dir" 2>/dev/null | cut -f1)
|
||||
status_msg+="\nLog disk usage: ${du_output:-unknown}\n"
|
||||
fi
|
||||
|
||||
# Log count
|
||||
local log_count
|
||||
log_count=$(ls -1 "$log_dir"/gniza-*.log 2>/dev/null | wc -l)
|
||||
status_msg+="Log files: $log_count\n"
|
||||
|
||||
ui_msgbox "$status_msg"
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gniza4linux/lib/ui_main.sh — Main menu loop
|
||||
|
||||
[[ -n "${_GNIZA4LINUX_UI_MAIN_LOADED:-}" ]] && return 0
|
||||
_GNIZA4LINUX_UI_MAIN_LOADED=1
|
||||
|
||||
ui_main_menu() {
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(ui_menu "Main Menu" \
|
||||
"1" "Backup" \
|
||||
"2" "Restore" \
|
||||
"3" "Targets" \
|
||||
"4" "Remotes" \
|
||||
"5" "Snapshots" \
|
||||
"6" "Retention" \
|
||||
"7" "Schedules" \
|
||||
"8" "Logs" \
|
||||
"9" "Settings" \
|
||||
"Q" "Quit") || break
|
||||
|
||||
case "$choice" in
|
||||
1) ui_backup_menu ;;
|
||||
2) ui_restore_menu ;;
|
||||
3) ui_targets_menu ;;
|
||||
4) ui_remotes_menu ;;
|
||||
5) ui_snapshots_menu ;;
|
||||
6) ui_retention_menu ;;
|
||||
7) ui_schedule_menu ;;
|
||||
8) ui_logs_menu ;;
|
||||
9) ui_settings_menu ;;
|
||||
Q) break ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -1,458 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gniza4linux/lib/ui_remotes.sh — Remote management TUI
|
||||
|
||||
[[ -n "${_GNIZA4LINUX_UI_REMOTES_LOADED:-}" ]] && return 0
|
||||
_GNIZA4LINUX_UI_REMOTES_LOADED=1
|
||||
|
||||
ui_remotes_menu() {
|
||||
while true; do
|
||||
local -a items=()
|
||||
local remotes
|
||||
remotes=$(list_remotes)
|
||||
if [[ -n "$remotes" ]]; then
|
||||
while IFS= read -r r; do
|
||||
items+=("$r" "Remote: $r")
|
||||
done <<< "$remotes"
|
||||
fi
|
||||
items+=("ADD" "Add new remote")
|
||||
items+=("BACK" "Return to main menu")
|
||||
|
||||
local choice
|
||||
choice=$(ui_menu "Remotes" "${items[@]}") || return 0
|
||||
|
||||
case "$choice" in
|
||||
ADD) ui_remote_add ;;
|
||||
BACK) return 0 ;;
|
||||
*)
|
||||
local action
|
||||
action=$(ui_menu "Remote: $choice" \
|
||||
"EDIT" "Edit remote" \
|
||||
"DELETE" "Delete remote" \
|
||||
"TEST" "Test connection" \
|
||||
"BACK" "Back") || continue
|
||||
case "$action" in
|
||||
EDIT) ui_remote_edit "$choice" ;;
|
||||
DELETE) ui_remote_delete "$choice" ;;
|
||||
TEST) ui_remote_test "$choice" ;;
|
||||
BACK) continue ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
ui_remote_add() {
|
||||
local name
|
||||
name=$(ui_inputbox "Add Remote" "Enter remote name (letters, digits, _ -):" "") || return 0
|
||||
[[ -z "$name" ]] && return 0
|
||||
|
||||
if ! validate_target_name "$name" 2>/dev/null; then
|
||||
ui_msgbox "Invalid remote name. Must start with a letter and contain only letters, digits, underscore, or hyphen (max 32 chars)."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -f "$CONFIG_DIR/remotes.d/${name}.conf" ]]; then
|
||||
ui_msgbox "Remote '$name' already exists."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local rtype
|
||||
rtype=$(ui_radiolist "Remote Type" \
|
||||
"ssh" "SSH remote" "ON" \
|
||||
"local" "Local directory" "OFF" \
|
||||
"s3" "Amazon S3 / compatible" "OFF" \
|
||||
"gdrive" "Google Drive" "OFF") || return 0
|
||||
|
||||
local conf="$CONFIG_DIR/remotes.d/${name}.conf"
|
||||
|
||||
case "$rtype" in
|
||||
ssh) _ui_remote_add_ssh "$name" "$conf" ;;
|
||||
local) _ui_remote_add_local "$name" "$conf" ;;
|
||||
s3) _ui_remote_add_s3 "$name" "$conf" ;;
|
||||
gdrive) _ui_remote_add_gdrive "$name" "$conf" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
_ui_remote_add_ssh() {
|
||||
local name="$1" conf="$2"
|
||||
|
||||
local host; host=$(ui_inputbox "SSH Remote" "Hostname or IP:" "") || return 0
|
||||
[[ -z "$host" ]] && { ui_msgbox "Host is required."; return 0; }
|
||||
|
||||
local port; port=$(ui_inputbox "SSH Remote" "Port:" "$DEFAULT_REMOTE_PORT") || port="$DEFAULT_REMOTE_PORT"
|
||||
local user; user=$(ui_inputbox "SSH Remote" "Username:" "$DEFAULT_REMOTE_USER") || user="$DEFAULT_REMOTE_USER"
|
||||
local base; base=$(ui_inputbox "SSH Remote" "Base path on remote:" "$DEFAULT_REMOTE_BASE") || base="$DEFAULT_REMOTE_BASE"
|
||||
|
||||
local auth_method
|
||||
auth_method=$(ui_radiolist "Authentication" \
|
||||
"key" "SSH key" "ON" \
|
||||
"password" "Password" "OFF") || auth_method="key"
|
||||
|
||||
local key="" password=""
|
||||
if [[ "$auth_method" == "key" ]]; then
|
||||
key=$(ui_inputbox "SSH Remote" "Path to SSH key:" "$HOME/.ssh/id_rsa") || key=""
|
||||
else
|
||||
password=$(ui_password "Enter SSH password:") || password=""
|
||||
fi
|
||||
|
||||
local bwlimit; bwlimit=$(ui_inputbox "SSH Remote" "Bandwidth limit (KB/s, 0=unlimited):" "$DEFAULT_BWLIMIT") || bwlimit="$DEFAULT_BWLIMIT"
|
||||
local retention; retention=$(ui_inputbox "SSH Remote" "Retention count:" "$DEFAULT_RETENTION_COUNT") || retention="$DEFAULT_RETENTION_COUNT"
|
||||
|
||||
# Test connection before saving
|
||||
if ui_yesno "Test connection to ${user}@${host}:${port} before saving?"; then
|
||||
local test_result
|
||||
local -a ssh_cmd=(ssh -o BatchMode=yes -o ConnectTimeout=10 -p "$port")
|
||||
if [[ "$auth_method" == "key" && -n "$key" ]]; then
|
||||
ssh_cmd+=(-i "$key")
|
||||
fi
|
||||
ssh_cmd+=("${user}@${host}" "echo OK")
|
||||
if test_result=$("${ssh_cmd[@]}" 2>&1); then
|
||||
ui_msgbox "Connection successful!"
|
||||
else
|
||||
ui_msgbox "Connection failed:\n\n$test_result"
|
||||
if ! ui_yesno "Save remote anyway?"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
cat > "$conf" <<EOF
|
||||
REMOTE_TYPE="ssh"
|
||||
REMOTE_HOST="$host"
|
||||
REMOTE_PORT="$port"
|
||||
REMOTE_USER="$user"
|
||||
REMOTE_AUTH_METHOD="$auth_method"
|
||||
REMOTE_KEY="$key"
|
||||
REMOTE_PASSWORD="$password"
|
||||
REMOTE_BASE="$base"
|
||||
BWLIMIT="$bwlimit"
|
||||
RETENTION_COUNT="$retention"
|
||||
EOF
|
||||
chmod 600 "$conf"
|
||||
ui_msgbox "Remote '$name' created successfully."
|
||||
}
|
||||
|
||||
_ui_remote_add_local() {
|
||||
local name="$1" conf="$2"
|
||||
|
||||
local base; base=$(ui_inputbox "Local Remote" "Local backup directory:" "/backups") || return 0
|
||||
[[ -z "$base" ]] && { ui_msgbox "Directory is required."; return 0; }
|
||||
|
||||
local retention; retention=$(ui_inputbox "Local Remote" "Retention count:" "$DEFAULT_RETENTION_COUNT") || retention="$DEFAULT_RETENTION_COUNT"
|
||||
|
||||
# Test path exists
|
||||
if [[ -d "$base" ]]; then
|
||||
ui_msgbox "Directory '$base' exists and is accessible."
|
||||
else
|
||||
ui_msgbox "Directory '$base' does NOT exist yet.\nIt will be created during the first backup."
|
||||
fi
|
||||
|
||||
cat > "$conf" <<EOF
|
||||
REMOTE_TYPE="local"
|
||||
REMOTE_BASE="$base"
|
||||
RETENTION_COUNT="$retention"
|
||||
EOF
|
||||
chmod 600 "$conf"
|
||||
ui_msgbox "Remote '$name' created successfully."
|
||||
}
|
||||
|
||||
_ui_remote_add_s3() {
|
||||
local name="$1" conf="$2"
|
||||
|
||||
local bucket; bucket=$(ui_inputbox "S3 Remote" "S3 Bucket name:" "") || return 0
|
||||
[[ -z "$bucket" ]] && { ui_msgbox "Bucket is required."; return 0; }
|
||||
|
||||
local region; region=$(ui_inputbox "S3 Remote" "Region:" "$DEFAULT_S3_REGION") || region="$DEFAULT_S3_REGION"
|
||||
local endpoint; endpoint=$(ui_inputbox "S3 Remote" "Endpoint (leave empty for AWS):" "") || endpoint=""
|
||||
local access_key; access_key=$(ui_inputbox "S3 Remote" "Access Key ID:" "") || access_key=""
|
||||
local secret_key; secret_key=$(ui_password "Secret Access Key:") || secret_key=""
|
||||
local base; base=$(ui_inputbox "S3 Remote" "Base path in bucket:" "/backups") || base="/backups"
|
||||
local retention; retention=$(ui_inputbox "S3 Remote" "Retention count:" "$DEFAULT_RETENTION_COUNT") || retention="$DEFAULT_RETENTION_COUNT"
|
||||
|
||||
# Test S3 connection before saving
|
||||
if command -v rclone &>/dev/null && ui_yesno "Test S3 connection before saving?"; then
|
||||
# Set globals temporarily for test_rclone_connection
|
||||
REMOTE_TYPE="s3" S3_BUCKET="$bucket" S3_REGION="$region" \
|
||||
S3_ENDPOINT="$endpoint" S3_ACCESS_KEY_ID="$access_key" \
|
||||
S3_SECRET_ACCESS_KEY="$secret_key" REMOTE_BASE="$base"
|
||||
local test_result
|
||||
if test_result=$(test_rclone_connection 2>&1); then
|
||||
ui_msgbox "S3 connection successful!"
|
||||
else
|
||||
ui_msgbox "S3 connection failed:\n\n$test_result"
|
||||
if ! ui_yesno "Save remote anyway?"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
cat > "$conf" <<EOF
|
||||
REMOTE_TYPE="s3"
|
||||
S3_BUCKET="$bucket"
|
||||
S3_REGION="$region"
|
||||
S3_ENDPOINT="$endpoint"
|
||||
S3_ACCESS_KEY_ID="$access_key"
|
||||
S3_SECRET_ACCESS_KEY="$secret_key"
|
||||
REMOTE_BASE="$base"
|
||||
RETENTION_COUNT="$retention"
|
||||
EOF
|
||||
chmod 600 "$conf"
|
||||
ui_msgbox "Remote '$name' created successfully."
|
||||
}
|
||||
|
||||
_ui_remote_add_gdrive() {
|
||||
local name="$1" conf="$2"
|
||||
|
||||
local sa_file; sa_file=$(ui_inputbox "Google Drive Remote" "Service account JSON file path:" "") || return 0
|
||||
[[ -z "$sa_file" ]] && { ui_msgbox "Service account file is required."; return 0; }
|
||||
|
||||
local folder_id; folder_id=$(ui_inputbox "Google Drive Remote" "Root folder ID:" "") || folder_id=""
|
||||
local base; base=$(ui_inputbox "Google Drive Remote" "Base path:" "/backups") || base="/backups"
|
||||
local retention; retention=$(ui_inputbox "Google Drive Remote" "Retention count:" "$DEFAULT_RETENTION_COUNT") || retention="$DEFAULT_RETENTION_COUNT"
|
||||
|
||||
# Test GDrive connection before saving
|
||||
if command -v rclone &>/dev/null && ui_yesno "Test Google Drive connection before saving?"; then
|
||||
REMOTE_TYPE="gdrive" GDRIVE_SERVICE_ACCOUNT_FILE="$sa_file" \
|
||||
GDRIVE_ROOT_FOLDER_ID="$folder_id" REMOTE_BASE="$base"
|
||||
local test_result
|
||||
if test_result=$(test_rclone_connection 2>&1); then
|
||||
ui_msgbox "Google Drive connection successful!"
|
||||
else
|
||||
ui_msgbox "Google Drive connection failed:\n\n$test_result"
|
||||
if ! ui_yesno "Save remote anyway?"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
cat > "$conf" <<EOF
|
||||
REMOTE_TYPE="gdrive"
|
||||
GDRIVE_SERVICE_ACCOUNT_FILE="$sa_file"
|
||||
GDRIVE_ROOT_FOLDER_ID="$folder_id"
|
||||
REMOTE_BASE="$base"
|
||||
RETENTION_COUNT="$retention"
|
||||
EOF
|
||||
chmod 600 "$conf"
|
||||
ui_msgbox "Remote '$name' created successfully."
|
||||
}
|
||||
|
||||
ui_remote_edit() {
|
||||
local name="$1"
|
||||
local conf="$CONFIG_DIR/remotes.d/${name}.conf"
|
||||
|
||||
if [[ ! -f "$conf" ]]; then
|
||||
ui_msgbox "Remote '$name' not found."
|
||||
return 0
|
||||
fi
|
||||
|
||||
load_remote "$name" || { ui_msgbox "Failed to load remote '$name'."; return 0; }
|
||||
|
||||
case "${REMOTE_TYPE:-ssh}" in
|
||||
ssh)
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(ui_menu "Edit Remote: $name (SSH)" \
|
||||
"HOST" "Host: ${REMOTE_HOST}" \
|
||||
"PORT" "Port: ${REMOTE_PORT}" \
|
||||
"USER" "User: ${REMOTE_USER}" \
|
||||
"AUTH" "Auth: ${REMOTE_AUTH_METHOD}" \
|
||||
"BASE" "Base: ${REMOTE_BASE}" \
|
||||
"BWLIMIT" "BW Limit: ${BWLIMIT} KB/s" \
|
||||
"RETENTION" "Retention: ${RETENTION_COUNT}" \
|
||||
"SAVE" "Save and return" \
|
||||
"BACK" "Cancel") || return 0
|
||||
|
||||
case "$choice" in
|
||||
HOST) local v; v=$(ui_inputbox "Edit" "Hostname:" "$REMOTE_HOST") && REMOTE_HOST="$v" ;;
|
||||
PORT) local v; v=$(ui_inputbox "Edit" "Port:" "$REMOTE_PORT") && REMOTE_PORT="$v" ;;
|
||||
USER) local v; v=$(ui_inputbox "Edit" "User:" "$REMOTE_USER") && REMOTE_USER="$v" ;;
|
||||
AUTH)
|
||||
local v; v=$(ui_radiolist "Auth Method" \
|
||||
"key" "SSH key" "$([ "$REMOTE_AUTH_METHOD" = "key" ] && echo ON || echo OFF)" \
|
||||
"password" "Password" "$([ "$REMOTE_AUTH_METHOD" = "password" ] && echo ON || echo OFF)") && REMOTE_AUTH_METHOD="$v"
|
||||
if [[ "$REMOTE_AUTH_METHOD" == "key" ]]; then
|
||||
local k; k=$(ui_inputbox "Edit" "SSH key path:" "$REMOTE_KEY") && REMOTE_KEY="$k"
|
||||
else
|
||||
local p; p=$(ui_password "Enter SSH password:") && REMOTE_PASSWORD="$p"
|
||||
fi
|
||||
;;
|
||||
BASE) local v; v=$(ui_inputbox "Edit" "Base path:" "$REMOTE_BASE") && REMOTE_BASE="$v" ;;
|
||||
BWLIMIT) local v; v=$(ui_inputbox "Edit" "BW Limit (KB/s):" "$BWLIMIT") && BWLIMIT="$v" ;;
|
||||
RETENTION) local v; v=$(ui_inputbox "Edit" "Retention count:" "$RETENTION_COUNT") && RETENTION_COUNT="$v" ;;
|
||||
SAVE)
|
||||
cat > "$conf" <<EOF
|
||||
REMOTE_TYPE="ssh"
|
||||
REMOTE_HOST="$REMOTE_HOST"
|
||||
REMOTE_PORT="$REMOTE_PORT"
|
||||
REMOTE_USER="$REMOTE_USER"
|
||||
REMOTE_AUTH_METHOD="$REMOTE_AUTH_METHOD"
|
||||
REMOTE_KEY="$REMOTE_KEY"
|
||||
REMOTE_PASSWORD="$REMOTE_PASSWORD"
|
||||
REMOTE_BASE="$REMOTE_BASE"
|
||||
BWLIMIT="$BWLIMIT"
|
||||
RETENTION_COUNT="$RETENTION_COUNT"
|
||||
EOF
|
||||
chmod 600 "$conf"
|
||||
ui_msgbox "Remote '$name' saved."
|
||||
return 0
|
||||
;;
|
||||
BACK) return 0 ;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
local)
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(ui_menu "Edit Remote: $name (Local)" \
|
||||
"BASE" "Directory: ${REMOTE_BASE}" \
|
||||
"RETENTION" "Retention: ${RETENTION_COUNT}" \
|
||||
"SAVE" "Save and return" \
|
||||
"BACK" "Cancel") || return 0
|
||||
|
||||
case "$choice" in
|
||||
BASE) local v; v=$(ui_inputbox "Edit" "Directory:" "$REMOTE_BASE") && REMOTE_BASE="$v" ;;
|
||||
RETENTION) local v; v=$(ui_inputbox "Edit" "Retention count:" "$RETENTION_COUNT") && RETENTION_COUNT="$v" ;;
|
||||
SAVE)
|
||||
cat > "$conf" <<EOF
|
||||
REMOTE_TYPE="local"
|
||||
REMOTE_BASE="$REMOTE_BASE"
|
||||
RETENTION_COUNT="$RETENTION_COUNT"
|
||||
EOF
|
||||
chmod 600 "$conf"
|
||||
ui_msgbox "Remote '$name' saved."
|
||||
return 0
|
||||
;;
|
||||
BACK) return 0 ;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
s3)
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(ui_menu "Edit Remote: $name (S3)" \
|
||||
"BUCKET" "Bucket: ${S3_BUCKET}" \
|
||||
"REGION" "Region: ${S3_REGION}" \
|
||||
"ENDPOINT" "Endpoint: ${S3_ENDPOINT:-default}" \
|
||||
"KEY" "Access Key: ${S3_ACCESS_KEY_ID:+****}" \
|
||||
"SECRET" "Secret Key: ****" \
|
||||
"BASE" "Base: ${REMOTE_BASE}" \
|
||||
"RETENTION" "Retention: ${RETENTION_COUNT}" \
|
||||
"SAVE" "Save and return" \
|
||||
"BACK" "Cancel") || return 0
|
||||
|
||||
case "$choice" in
|
||||
BUCKET) local v; v=$(ui_inputbox "Edit" "Bucket:" "$S3_BUCKET") && S3_BUCKET="$v" ;;
|
||||
REGION) local v; v=$(ui_inputbox "Edit" "Region:" "$S3_REGION") && S3_REGION="$v" ;;
|
||||
ENDPOINT) local v; v=$(ui_inputbox "Edit" "Endpoint:" "$S3_ENDPOINT") && S3_ENDPOINT="$v" ;;
|
||||
KEY) local v; v=$(ui_inputbox "Edit" "Access Key ID:" "$S3_ACCESS_KEY_ID") && S3_ACCESS_KEY_ID="$v" ;;
|
||||
SECRET) local v; v=$(ui_password "Secret Access Key:") && S3_SECRET_ACCESS_KEY="$v" ;;
|
||||
BASE) local v; v=$(ui_inputbox "Edit" "Base path:" "$REMOTE_BASE") && REMOTE_BASE="$v" ;;
|
||||
RETENTION) local v; v=$(ui_inputbox "Edit" "Retention count:" "$RETENTION_COUNT") && RETENTION_COUNT="$v" ;;
|
||||
SAVE)
|
||||
cat > "$conf" <<EOF
|
||||
REMOTE_TYPE="s3"
|
||||
S3_BUCKET="$S3_BUCKET"
|
||||
S3_REGION="$S3_REGION"
|
||||
S3_ENDPOINT="$S3_ENDPOINT"
|
||||
S3_ACCESS_KEY_ID="$S3_ACCESS_KEY_ID"
|
||||
S3_SECRET_ACCESS_KEY="$S3_SECRET_ACCESS_KEY"
|
||||
REMOTE_BASE="$REMOTE_BASE"
|
||||
RETENTION_COUNT="$RETENTION_COUNT"
|
||||
EOF
|
||||
chmod 600 "$conf"
|
||||
ui_msgbox "Remote '$name' saved."
|
||||
return 0
|
||||
;;
|
||||
BACK) return 0 ;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
gdrive)
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(ui_menu "Edit Remote: $name (GDrive)" \
|
||||
"SA" "Service Account: ${GDRIVE_SERVICE_ACCOUNT_FILE}" \
|
||||
"FOLDER" "Folder ID: ${GDRIVE_ROOT_FOLDER_ID:-none}" \
|
||||
"BASE" "Base: ${REMOTE_BASE}" \
|
||||
"RETENTION" "Retention: ${RETENTION_COUNT}" \
|
||||
"SAVE" "Save and return" \
|
||||
"BACK" "Cancel") || return 0
|
||||
|
||||
case "$choice" in
|
||||
SA) local v; v=$(ui_inputbox "Edit" "Service account file:" "$GDRIVE_SERVICE_ACCOUNT_FILE") && GDRIVE_SERVICE_ACCOUNT_FILE="$v" ;;
|
||||
FOLDER) local v; v=$(ui_inputbox "Edit" "Root folder ID:" "$GDRIVE_ROOT_FOLDER_ID") && GDRIVE_ROOT_FOLDER_ID="$v" ;;
|
||||
BASE) local v; v=$(ui_inputbox "Edit" "Base path:" "$REMOTE_BASE") && REMOTE_BASE="$v" ;;
|
||||
RETENTION) local v; v=$(ui_inputbox "Edit" "Retention count:" "$RETENTION_COUNT") && RETENTION_COUNT="$v" ;;
|
||||
SAVE)
|
||||
cat > "$conf" <<EOF
|
||||
REMOTE_TYPE="gdrive"
|
||||
GDRIVE_SERVICE_ACCOUNT_FILE="$GDRIVE_SERVICE_ACCOUNT_FILE"
|
||||
GDRIVE_ROOT_FOLDER_ID="$GDRIVE_ROOT_FOLDER_ID"
|
||||
REMOTE_BASE="$REMOTE_BASE"
|
||||
RETENTION_COUNT="$RETENTION_COUNT"
|
||||
EOF
|
||||
chmod 600 "$conf"
|
||||
ui_msgbox "Remote '$name' saved."
|
||||
return 0
|
||||
;;
|
||||
BACK) return 0 ;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
ui_remote_delete() {
|
||||
local name="$1"
|
||||
local conf="$CONFIG_DIR/remotes.d/${name}.conf"
|
||||
|
||||
if [[ ! -f "$conf" ]]; then
|
||||
ui_msgbox "Remote '$name' not found."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ui_yesno "Delete remote '$name'? This cannot be undone."; then
|
||||
rm -f "$conf"
|
||||
log_info "Deleted remote config: $conf"
|
||||
ui_msgbox "Remote '$name' deleted."
|
||||
fi
|
||||
}
|
||||
|
||||
ui_remote_test() {
|
||||
local name="$1"
|
||||
load_remote "$name" || { ui_msgbox "Failed to load remote '$name'."; return 0; }
|
||||
|
||||
local result
|
||||
case "${REMOTE_TYPE:-ssh}" in
|
||||
ssh)
|
||||
result=$(ssh -o BatchMode=yes -o ConnectTimeout=10 \
|
||||
-p "$REMOTE_PORT" -i "$REMOTE_KEY" \
|
||||
"${REMOTE_USER}@${REMOTE_HOST}" "echo OK" 2>&1) \
|
||||
&& ui_msgbox "Connection to '$name' successful.\n\nResponse: $result" \
|
||||
|| ui_msgbox "Connection to '$name' failed.\n\nError: $result"
|
||||
;;
|
||||
local)
|
||||
if [[ -d "$REMOTE_BASE" ]]; then
|
||||
ui_msgbox "Local directory '$REMOTE_BASE' exists and is accessible."
|
||||
else
|
||||
ui_msgbox "Local directory '$REMOTE_BASE' does NOT exist."
|
||||
fi
|
||||
;;
|
||||
s3|gdrive)
|
||||
if command -v rclone &>/dev/null; then
|
||||
# Use the proper rclone transport layer (temp config file, not CLI args)
|
||||
load_remote "$name" || { ui_msgbox "Failed to load remote."; break; }
|
||||
if result=$(test_rclone_connection 2>&1); then
|
||||
ui_msgbox "${REMOTE_TYPE} connection to '$name' successful."
|
||||
else
|
||||
ui_msgbox "${REMOTE_TYPE} connection to '$name' failed.\n\nError: $result"
|
||||
fi
|
||||
else
|
||||
ui_msgbox "rclone is not installed. Cannot test ${REMOTE_TYPE} connection."
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
ui_msgbox "Unknown remote type: ${REMOTE_TYPE}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gniza4linux/lib/ui_restore.sh — Restore TUI
|
||||
|
||||
[[ -n "${_GNIZA4LINUX_UI_RESTORE_LOADED:-}" ]] && return 0
|
||||
_GNIZA4LINUX_UI_RESTORE_LOADED=1
|
||||
|
||||
ui_restore_menu() {
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(ui_menu "Restore" \
|
||||
"TARGET" "Restore full target" \
|
||||
"FOLDER" "Restore single folder" \
|
||||
"BACK" "Return to main menu") || return 0
|
||||
|
||||
case "$choice" in
|
||||
TARGET) ui_restore_wizard "full" ;;
|
||||
FOLDER) ui_restore_wizard "folder" ;;
|
||||
BACK) return 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
ui_restore_wizard() {
|
||||
local mode="$1"
|
||||
|
||||
if ! has_targets; then
|
||||
ui_msgbox "No targets configured."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Select target
|
||||
local -a titems=()
|
||||
local targets
|
||||
targets=$(list_targets)
|
||||
while IFS= read -r t; do
|
||||
titems+=("$t" "Target: $t")
|
||||
done <<< "$targets"
|
||||
|
||||
local target
|
||||
target=$(ui_menu "Select Target to Restore" "${titems[@]}") || return 0
|
||||
|
||||
# Select remote
|
||||
local remote=""
|
||||
if has_remotes; then
|
||||
local -a ritems=()
|
||||
local remotes
|
||||
remotes=$(list_remotes)
|
||||
while IFS= read -r r; do
|
||||
ritems+=("$r" "Remote: $r")
|
||||
done <<< "$remotes"
|
||||
|
||||
remote=$(ui_menu "Select Remote" "${ritems[@]}") || return 0
|
||||
else
|
||||
ui_msgbox "No remotes configured."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Load remote for snapshot listing
|
||||
load_remote "$remote" || { ui_msgbox "Failed to load remote '$remote'."; return 0; }
|
||||
|
||||
# Select snapshot
|
||||
local snapshots
|
||||
snapshots=$(list_remote_snapshots "$target" 2>/dev/null)
|
||||
if [[ -z "$snapshots" ]]; then
|
||||
ui_msgbox "No snapshots found for target '$target' on remote '$remote'."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local -a sitems=()
|
||||
while IFS= read -r s; do
|
||||
sitems+=("$s" "Snapshot: $s")
|
||||
done <<< "$snapshots"
|
||||
|
||||
local snapshot
|
||||
snapshot=$(ui_menu "Select Snapshot" "${sitems[@]}") || return 0
|
||||
|
||||
# Restore location
|
||||
local restore_dest=""
|
||||
local restore_type
|
||||
restore_type=$(ui_radiolist "Restore Location" \
|
||||
"inplace" "Restore in-place (original location)" "ON" \
|
||||
"directory" "Restore to a different directory" "OFF") || return 0
|
||||
|
||||
if [[ "$restore_type" == "directory" ]]; then
|
||||
restore_dest=$(ui_inputbox "Restore" "Enter destination directory:" "/tmp/restore") || return 0
|
||||
[[ -z "$restore_dest" ]] && { ui_msgbox "Destination is required."; return 0; }
|
||||
fi
|
||||
|
||||
# Folder selection for single-folder mode
|
||||
local folder_arg=""
|
||||
if [[ "$mode" == "folder" ]]; then
|
||||
load_target "$target" || { ui_msgbox "Failed to load target."; return 0; }
|
||||
local -a fitems=()
|
||||
local folders
|
||||
folders=$(get_target_folders)
|
||||
while IFS= read -r f; do
|
||||
[[ -z "$f" ]] && continue
|
||||
fitems+=("$f" "$f")
|
||||
done <<< "$folders"
|
||||
|
||||
if [[ ${#fitems[@]} -eq 0 ]]; then
|
||||
ui_msgbox "No folders defined in target '$target'."
|
||||
return 0
|
||||
fi
|
||||
|
||||
folder_arg=$(ui_menu "Select Folder to Restore" "${fitems[@]}") || return 0
|
||||
fi
|
||||
|
||||
# Confirm
|
||||
local confirm_msg="Restore snapshot?\n\nTarget: $target\nRemote: $remote\nSnapshot: $snapshot"
|
||||
[[ -n "$folder_arg" ]] && confirm_msg+="\nFolder: $folder_arg"
|
||||
if [[ "$restore_type" == "inplace" ]]; then
|
||||
confirm_msg+="\nLocation: In-place (original)"
|
||||
else
|
||||
confirm_msg+="\nLocation: $restore_dest"
|
||||
fi
|
||||
confirm_msg+="\n"
|
||||
|
||||
ui_yesno "$confirm_msg" || return 0
|
||||
|
||||
# Run restore
|
||||
local -a cmd_args=(gniza --cli restore "--target=$target" "--remote=$remote" "--snapshot=$snapshot")
|
||||
[[ -n "$restore_dest" ]] && cmd_args+=("--dest=$restore_dest")
|
||||
[[ -n "$folder_arg" ]] && cmd_args+=("--folder=$folder_arg")
|
||||
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp /tmp/gniza-restore-XXXXXX.log)
|
||||
|
||||
(
|
||||
echo "10"
|
||||
if "${cmd_args[@]}" > "$tmpfile" 2>&1; then
|
||||
echo "100"
|
||||
else
|
||||
echo "100"
|
||||
fi
|
||||
) | ui_gauge "Restoring target: $target"
|
||||
|
||||
if [[ -s "$tmpfile" ]]; then
|
||||
ui_textbox "$tmpfile"
|
||||
else
|
||||
ui_msgbox "Restore of '$target' completed."
|
||||
fi
|
||||
|
||||
rm -f "$tmpfile"
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gniza4linux/lib/ui_retention.sh — Retention cleanup TUI
|
||||
|
||||
[[ -n "${_GNIZA4LINUX_UI_RETENTION_LOADED:-}" ]] && return 0
|
||||
_GNIZA4LINUX_UI_RETENTION_LOADED=1
|
||||
|
||||
ui_retention_menu() {
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(ui_menu "Retention" \
|
||||
"SINGLE" "Run cleanup for single target" \
|
||||
"ALL" "Run cleanup for all targets" \
|
||||
"CONFIG" "Configure retention count" \
|
||||
"BACK" "Return to main menu") || return 0
|
||||
|
||||
case "$choice" in
|
||||
SINGLE) _ui_retention_single ;;
|
||||
ALL) _ui_retention_all ;;
|
||||
CONFIG) _ui_retention_config ;;
|
||||
BACK) return 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
_ui_retention_single() {
|
||||
if ! has_targets; then
|
||||
ui_msgbox "No targets configured."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local -a items=()
|
||||
local targets
|
||||
targets=$(list_targets)
|
||||
while IFS= read -r t; do
|
||||
items+=("$t" "Target: $t")
|
||||
done <<< "$targets"
|
||||
|
||||
local target
|
||||
target=$(ui_menu "Select Target for Cleanup" "${items[@]}") || return 0
|
||||
|
||||
ui_yesno "Run retention cleanup for target '$target'?" || return 0
|
||||
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp /tmp/gniza-retention-XXXXXX.log)
|
||||
|
||||
(
|
||||
echo "10"
|
||||
if gniza --cli retention --target="$target" > "$tmpfile" 2>&1; then
|
||||
echo "100"
|
||||
else
|
||||
echo "100"
|
||||
fi
|
||||
) | ui_gauge "Running retention cleanup: $target"
|
||||
|
||||
if [[ -s "$tmpfile" ]]; then
|
||||
ui_textbox "$tmpfile"
|
||||
else
|
||||
ui_msgbox "Retention cleanup for '$target' completed."
|
||||
fi
|
||||
|
||||
rm -f "$tmpfile"
|
||||
}
|
||||
|
||||
_ui_retention_all() {
|
||||
if ! has_targets; then
|
||||
ui_msgbox "No targets configured."
|
||||
return 0
|
||||
fi
|
||||
|
||||
ui_yesno "Run retention cleanup for ALL targets?" || return 0
|
||||
|
||||
local targets
|
||||
targets=$(list_targets)
|
||||
local count=0 total=0
|
||||
total=$(echo "$targets" | wc -l)
|
||||
|
||||
local output=""
|
||||
while IFS= read -r t; do
|
||||
((count++))
|
||||
local pct=$(( count * 100 / total ))
|
||||
echo "$pct"
|
||||
local result
|
||||
if result=$(gniza --cli retention --target="$t" 2>&1); then
|
||||
output+="$t: OK\n"
|
||||
else
|
||||
output+="$t: FAILED\n$result\n"
|
||||
fi
|
||||
done <<< "$targets" | ui_gauge "Running retention cleanup..."
|
||||
|
||||
ui_msgbox "Retention Results:\n\n$output"
|
||||
}
|
||||
|
||||
_ui_retention_config() {
|
||||
local current="${RETENTION_COUNT:-$DEFAULT_RETENTION_COUNT}"
|
||||
local new_count
|
||||
new_count=$(ui_inputbox "Retention Config" "Number of snapshots to keep (current: $current):" "$current") || return 0
|
||||
|
||||
if [[ ! "$new_count" =~ ^[0-9]+$ ]] || (( new_count < 1 )); then
|
||||
ui_msgbox "Retention count must be a positive integer."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local config_file="$CONFIG_DIR/gniza.conf"
|
||||
if [[ -f "$config_file" ]] && grep -q "^RETENTION_COUNT=" "$config_file"; then
|
||||
sed -i "s/^RETENTION_COUNT=.*/RETENTION_COUNT=\"$new_count\"/" "$config_file"
|
||||
else
|
||||
echo "RETENTION_COUNT=\"$new_count\"" >> "$config_file"
|
||||
fi
|
||||
|
||||
RETENTION_COUNT="$new_count"
|
||||
ui_msgbox "Retention count set to $new_count."
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gniza4linux/lib/ui_schedule.sh — Schedule management TUI
|
||||
|
||||
[[ -n "${_GNIZA4LINUX_UI_SCHEDULE_LOADED:-}" ]] && return 0
|
||||
_GNIZA4LINUX_UI_SCHEDULE_LOADED=1
|
||||
|
||||
ui_schedule_menu() {
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(ui_menu "Schedules" \
|
||||
"LIST" "Show current schedules" \
|
||||
"ADD" "Add schedule" \
|
||||
"DELETE" "Delete schedule" \
|
||||
"INSTALL" "Install schedules to crontab" \
|
||||
"REMOVE" "Remove schedules from crontab" \
|
||||
"SHOW" "Show crontab entries" \
|
||||
"BACK" "Return to main menu") || return 0
|
||||
|
||||
case "$choice" in
|
||||
LIST) _ui_schedule_list ;;
|
||||
ADD) _ui_schedule_add ;;
|
||||
DELETE) _ui_schedule_delete ;;
|
||||
INSTALL) _ui_schedule_install ;;
|
||||
REMOVE) _ui_schedule_remove ;;
|
||||
SHOW) _ui_schedule_show_cron ;;
|
||||
BACK) return 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
_ui_schedule_list() {
|
||||
if ! has_schedules; then
|
||||
ui_msgbox "No schedules configured."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local schedules
|
||||
schedules=$(list_schedules)
|
||||
local info="Configured Schedules:\n\n"
|
||||
|
||||
while IFS= read -r sname; do
|
||||
[[ -z "$sname" ]] && continue
|
||||
load_schedule "$sname" 2>/dev/null || continue
|
||||
info+="[$sname]\n"
|
||||
info+=" Type: ${SCHEDULE:-not set}\n"
|
||||
info+=" Time: ${SCHEDULE_TIME:-02:00}\n"
|
||||
[[ -n "${SCHEDULE_DAY:-}" ]] && info+=" Day: $SCHEDULE_DAY\n"
|
||||
[[ -n "${SCHEDULE_REMOTES:-}" ]] && info+=" Remotes: $SCHEDULE_REMOTES\n"
|
||||
[[ -n "${SCHEDULE_TARGETS:-}" ]] && info+=" Targets: $SCHEDULE_TARGETS\n"
|
||||
info+="\n"
|
||||
done <<< "$schedules"
|
||||
|
||||
ui_msgbox "$info"
|
||||
}
|
||||
|
||||
_ui_schedule_add() {
|
||||
local name
|
||||
name=$(ui_inputbox "Add Schedule" "Schedule name:" "") || return 0
|
||||
[[ -z "$name" ]] && return 0
|
||||
|
||||
if ! validate_target_name "$name" 2>/dev/null; then
|
||||
ui_msgbox "Invalid name. Must start with a letter, max 32 chars, [a-zA-Z0-9_-]."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local conf="$CONFIG_DIR/schedules.d/${name}.conf"
|
||||
if [[ -f "$conf" ]]; then
|
||||
ui_msgbox "Schedule '$name' already exists."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local stype
|
||||
stype=$(ui_radiolist "Schedule Type" \
|
||||
"hourly" "Every hour" "OFF" \
|
||||
"daily" "Once a day" "ON" \
|
||||
"weekly" "Once a week" "OFF" \
|
||||
"monthly" "Once a month" "OFF" \
|
||||
"custom" "Custom cron expression" "OFF") || return 0
|
||||
|
||||
local stime="02:00"
|
||||
if [[ "$stype" != "hourly" && "$stype" != "custom" ]]; then
|
||||
stime=$(ui_inputbox "Schedule Time" "Time (HH:MM, 24h format):" "02:00") || return 0
|
||||
fi
|
||||
|
||||
local sday=""
|
||||
if [[ "$stype" == "weekly" ]]; then
|
||||
sday=$(ui_inputbox "Day of Week" "Day (0=Sun, 1=Mon, ..., 6=Sat):" "0") || return 0
|
||||
elif [[ "$stype" == "monthly" ]]; then
|
||||
sday=$(ui_inputbox "Day of Month" "Day (1-28):" "1") || return 0
|
||||
fi
|
||||
|
||||
local scron=""
|
||||
if [[ "$stype" == "custom" ]]; then
|
||||
scron=$(ui_inputbox "Custom Cron" "Enter 5-field cron expression:" "0 2 * * *") || return 0
|
||||
fi
|
||||
|
||||
local stargets=""
|
||||
stargets=$(ui_inputbox "Targets" "Target names (comma-separated, empty=all):" "") || return 0
|
||||
|
||||
local sremotes=""
|
||||
sremotes=$(ui_inputbox "Remotes" "Remote names (comma-separated, empty=all):" "") || return 0
|
||||
|
||||
cat > "$conf" <<EOF
|
||||
# gniza schedule: $name
|
||||
SCHEDULE="$stype"
|
||||
SCHEDULE_TIME="$stime"
|
||||
SCHEDULE_DAY="$sday"
|
||||
SCHEDULE_CRON="$scron"
|
||||
TARGETS="$stargets"
|
||||
REMOTES="$sremotes"
|
||||
EOF
|
||||
chmod 600 "$conf"
|
||||
|
||||
ui_msgbox "Schedule '$name' created.\n\nRun 'Install schedules to crontab' to activate."
|
||||
}
|
||||
|
||||
_ui_schedule_delete() {
|
||||
if ! has_schedules; then
|
||||
ui_msgbox "No schedules configured."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local -a items=()
|
||||
local schedules
|
||||
schedules=$(list_schedules)
|
||||
while IFS= read -r s; do
|
||||
items+=("$s" "Schedule: $s")
|
||||
done <<< "$schedules"
|
||||
|
||||
local selected
|
||||
selected=$(ui_menu "Delete Schedule" "${items[@]}") || return 0
|
||||
|
||||
if ui_yesno "Delete schedule '$selected'?"; then
|
||||
rm -f "$CONFIG_DIR/schedules.d/${selected}.conf"
|
||||
ui_msgbox "Schedule '$selected' deleted."
|
||||
fi
|
||||
}
|
||||
|
||||
_ui_schedule_install() {
|
||||
if ! has_schedules; then
|
||||
ui_msgbox "No schedules configured. Add a schedule first."
|
||||
return 0
|
||||
fi
|
||||
|
||||
ui_yesno "Install all schedules to crontab?" || return 0
|
||||
|
||||
local result
|
||||
if result=$(install_schedules 2>&1); then
|
||||
ui_msgbox "Schedules installed.\n\n$result"
|
||||
else
|
||||
ui_msgbox "Failed to install schedules.\n\n$result"
|
||||
fi
|
||||
}
|
||||
|
||||
_ui_schedule_remove() {
|
||||
ui_yesno "Remove all gniza schedule entries from crontab?" || return 0
|
||||
|
||||
local result
|
||||
if result=$(remove_schedules 2>&1); then
|
||||
ui_msgbox "$result"
|
||||
else
|
||||
ui_msgbox "Failed to remove schedules.\n\n$result"
|
||||
fi
|
||||
}
|
||||
|
||||
_ui_schedule_show_cron() {
|
||||
local result
|
||||
result=$(show_schedules 2>&1)
|
||||
ui_msgbox "$result"
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gniza4linux/lib/ui_settings.sh — Settings editor TUI
|
||||
|
||||
[[ -n "${_GNIZA4LINUX_UI_SETTINGS_LOADED:-}" ]] && return 0
|
||||
_GNIZA4LINUX_UI_SETTINGS_LOADED=1
|
||||
|
||||
ui_settings_menu() {
|
||||
local config_file="$CONFIG_DIR/gniza.conf"
|
||||
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(ui_menu "Settings" \
|
||||
"LOGLEVEL" "Log level: ${LOG_LEVEL:-$DEFAULT_LOG_LEVEL}" \
|
||||
"EMAIL" "Notification email: ${NOTIFY_EMAIL:-none}" \
|
||||
"SMTP_HOST" "SMTP host: ${SMTP_HOST:-none}" \
|
||||
"SMTP_PORT" "SMTP port: ${SMTP_PORT:-$DEFAULT_SMTP_PORT}" \
|
||||
"SMTP_USER" "SMTP user: ${SMTP_USER:-none}" \
|
||||
"SMTP_PASS" "SMTP password: ****" \
|
||||
"SMTP_FROM" "SMTP from: ${SMTP_FROM:-none}" \
|
||||
"SMTP_SEC" "SMTP security: ${SMTP_SECURITY:-$DEFAULT_SMTP_SECURITY}" \
|
||||
"RETENTION" "Default retention: ${RETENTION_COUNT:-$DEFAULT_RETENTION_COUNT}" \
|
||||
"BWLIMIT" "Default BW limit: ${BWLIMIT:-$DEFAULT_BWLIMIT} KB/s" \
|
||||
"BACK" "Return to main menu") || return 0
|
||||
|
||||
case "$choice" in
|
||||
LOGLEVEL)
|
||||
local val
|
||||
val=$(ui_radiolist "Log Level" \
|
||||
"debug" "Debug" "$([ "${LOG_LEVEL:-info}" = "debug" ] && echo ON || echo OFF)" \
|
||||
"info" "Info" "$([ "${LOG_LEVEL:-info}" = "info" ] && echo ON || echo OFF)" \
|
||||
"warn" "Warning" "$([ "${LOG_LEVEL:-info}" = "warn" ] && echo ON || echo OFF)" \
|
||||
"error" "Error" "$([ "${LOG_LEVEL:-info}" = "error" ] && echo ON || echo OFF)") || continue
|
||||
LOG_LEVEL="$val"
|
||||
_ui_settings_save "LOG_LEVEL" "$val" "$config_file"
|
||||
;;
|
||||
EMAIL)
|
||||
local val
|
||||
val=$(ui_inputbox "Settings" "Notification email:" "${NOTIFY_EMAIL:-}") || continue
|
||||
NOTIFY_EMAIL="$val"
|
||||
_ui_settings_save "NOTIFY_EMAIL" "$val" "$config_file"
|
||||
;;
|
||||
SMTP_HOST)
|
||||
local val
|
||||
val=$(ui_inputbox "Settings" "SMTP host:" "${SMTP_HOST:-}") || continue
|
||||
SMTP_HOST="$val"
|
||||
_ui_settings_save "SMTP_HOST" "$val" "$config_file"
|
||||
;;
|
||||
SMTP_PORT)
|
||||
local val
|
||||
val=$(ui_inputbox "Settings" "SMTP port:" "${SMTP_PORT:-$DEFAULT_SMTP_PORT}") || continue
|
||||
SMTP_PORT="$val"
|
||||
_ui_settings_save "SMTP_PORT" "$val" "$config_file"
|
||||
;;
|
||||
SMTP_USER)
|
||||
local val
|
||||
val=$(ui_inputbox "Settings" "SMTP user:" "${SMTP_USER:-}") || continue
|
||||
SMTP_USER="$val"
|
||||
_ui_settings_save "SMTP_USER" "$val" "$config_file"
|
||||
;;
|
||||
SMTP_PASS)
|
||||
local val
|
||||
val=$(ui_password "SMTP password:") || continue
|
||||
SMTP_PASSWORD="$val"
|
||||
_ui_settings_save "SMTP_PASSWORD" "$val" "$config_file"
|
||||
;;
|
||||
SMTP_FROM)
|
||||
local val
|
||||
val=$(ui_inputbox "Settings" "SMTP from address:" "${SMTP_FROM:-}") || continue
|
||||
SMTP_FROM="$val"
|
||||
_ui_settings_save "SMTP_FROM" "$val" "$config_file"
|
||||
;;
|
||||
SMTP_SEC)
|
||||
local val
|
||||
val=$(ui_radiolist "SMTP Security" \
|
||||
"tls" "TLS" "$([ "${SMTP_SECURITY:-tls}" = "tls" ] && echo ON || echo OFF)" \
|
||||
"ssl" "SSL" "$([ "${SMTP_SECURITY:-tls}" = "ssl" ] && echo ON || echo OFF)" \
|
||||
"none" "None" "$([ "${SMTP_SECURITY:-tls}" = "none" ] && echo ON || echo OFF)") || continue
|
||||
SMTP_SECURITY="$val"
|
||||
_ui_settings_save "SMTP_SECURITY" "$val" "$config_file"
|
||||
;;
|
||||
RETENTION)
|
||||
local val
|
||||
val=$(ui_inputbox "Settings" "Default retention count:" "${RETENTION_COUNT:-$DEFAULT_RETENTION_COUNT}") || continue
|
||||
if [[ ! "$val" =~ ^[0-9]+$ ]] || (( val < 1 )); then
|
||||
ui_msgbox "Retention count must be a positive integer."
|
||||
continue
|
||||
fi
|
||||
RETENTION_COUNT="$val"
|
||||
_ui_settings_save "RETENTION_COUNT" "$val" "$config_file"
|
||||
;;
|
||||
BWLIMIT)
|
||||
local val
|
||||
val=$(ui_inputbox "Settings" "Default bandwidth limit (KB/s, 0=unlimited):" "${BWLIMIT:-$DEFAULT_BWLIMIT}") || continue
|
||||
if [[ ! "$val" =~ ^[0-9]+$ ]]; then
|
||||
ui_msgbox "Bandwidth limit must be a non-negative integer."
|
||||
continue
|
||||
fi
|
||||
BWLIMIT="$val"
|
||||
_ui_settings_save "BWLIMIT" "$val" "$config_file"
|
||||
;;
|
||||
BACK) return 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
_ui_settings_save() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local config_file="$3"
|
||||
|
||||
# Ensure config file exists
|
||||
[[ -f "$config_file" ]] || touch "$config_file"
|
||||
|
||||
if grep -q "^${key}=" "$config_file"; then
|
||||
# Use awk to avoid sed delimiter injection issues
|
||||
local tmpconf
|
||||
tmpconf=$(mktemp)
|
||||
awk -v k="$key" -v v="$value" 'BEGIN{FS=OFS="="} $1==k{print k "=\"" v "\""; next} {print}' "$config_file" > "$tmpconf"
|
||||
mv "$tmpconf" "$config_file"
|
||||
else
|
||||
printf '%s="%s"\n' "$key" "$value" >> "$config_file"
|
||||
fi
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gniza4linux/lib/ui_snapshots.sh — Snapshot browsing TUI
|
||||
|
||||
[[ -n "${_GNIZA4LINUX_UI_SNAPSHOTS_LOADED:-}" ]] && return 0
|
||||
_GNIZA4LINUX_UI_SNAPSHOTS_LOADED=1
|
||||
|
||||
ui_snapshots_menu() {
|
||||
if ! has_targets; then
|
||||
ui_msgbox "No targets configured."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Select target
|
||||
local -a titems=()
|
||||
local targets
|
||||
targets=$(list_targets)
|
||||
while IFS= read -r t; do
|
||||
titems+=("$t" "Target: $t")
|
||||
done <<< "$targets"
|
||||
|
||||
local target
|
||||
target=$(ui_menu "Select Target" "${titems[@]}") || return 0
|
||||
|
||||
# Select remote
|
||||
if ! has_remotes; then
|
||||
ui_msgbox "No remotes configured."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local -a ritems=()
|
||||
local remotes
|
||||
remotes=$(list_remotes)
|
||||
while IFS= read -r r; do
|
||||
ritems+=("$r" "Remote: $r")
|
||||
done <<< "$remotes"
|
||||
|
||||
local remote
|
||||
remote=$(ui_menu "Select Remote" "${ritems[@]}") || return 0
|
||||
|
||||
load_remote "$remote" || { ui_msgbox "Failed to load remote '$remote'."; return 0; }
|
||||
|
||||
# List snapshots
|
||||
while true; do
|
||||
local snapshots
|
||||
snapshots=$(list_remote_snapshots "$target" 2>/dev/null)
|
||||
if [[ -z "$snapshots" ]]; then
|
||||
ui_msgbox "No snapshots found for target '$target' on remote '$remote'."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local -a sitems=()
|
||||
while IFS= read -r s; do
|
||||
sitems+=("$s" "Snapshot: $s")
|
||||
done <<< "$snapshots"
|
||||
sitems+=("BACK" "Return")
|
||||
|
||||
local snapshot
|
||||
snapshot=$(ui_menu "Snapshots: $target @ $remote" "${sitems[@]}") || return 0
|
||||
|
||||
[[ "$snapshot" == "BACK" ]] && return 0
|
||||
|
||||
# Snapshot detail menu
|
||||
while true; do
|
||||
local snap_dir
|
||||
snap_dir=$(get_snapshot_dir "$target")
|
||||
local meta_info="Snapshot: $snapshot\nTarget: $target\nRemote: $remote"
|
||||
|
||||
# Try to read meta.json
|
||||
local meta_content=""
|
||||
if [[ "${REMOTE_TYPE:-ssh}" == "local" ]]; then
|
||||
if [[ -f "$snap_dir/$snapshot/meta.json" ]]; then
|
||||
meta_content=$(cat "$snap_dir/$snapshot/meta.json" 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
[[ -n "$meta_content" ]] && meta_info+="\n\n$meta_content"
|
||||
|
||||
local action
|
||||
action=$(ui_menu "Snapshot: $snapshot" \
|
||||
"DETAILS" "View details" \
|
||||
"DELETE" "Delete snapshot" \
|
||||
"BACK" "Back to list") || break
|
||||
|
||||
case "$action" in
|
||||
DETAILS)
|
||||
ui_msgbox "$meta_info"
|
||||
;;
|
||||
DELETE)
|
||||
if ui_yesno "Delete snapshot '$snapshot'?\nThis cannot be undone."; then
|
||||
local snap_path_del
|
||||
snap_path_del=$(get_snapshot_dir "$target")
|
||||
local del_ok=false
|
||||
if _is_rclone_mode; then
|
||||
rclone_purge "targets/${target}/snapshots/${snapshot}" 2>/dev/null && del_ok=true
|
||||
elif [[ "${REMOTE_TYPE:-ssh}" == "local" ]]; then
|
||||
rm -rf "$snap_path_del/$snapshot" 2>/dev/null && del_ok=true
|
||||
else
|
||||
remote_exec "rm -rf '$snap_path_del/$snapshot'" 2>/dev/null && del_ok=true
|
||||
fi
|
||||
if [[ "$del_ok" == "true" ]]; then
|
||||
ui_msgbox "Snapshot '$snapshot' deleted."
|
||||
else
|
||||
ui_msgbox "Failed to delete snapshot '$snapshot'."
|
||||
fi
|
||||
break
|
||||
fi
|
||||
;;
|
||||
BACK) break ;;
|
||||
esac
|
||||
done
|
||||
done
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gniza4linux/lib/ui_targets.sh — Target management TUI
|
||||
|
||||
[[ -n "${_GNIZA4LINUX_UI_TARGETS_LOADED:-}" ]] && return 0
|
||||
_GNIZA4LINUX_UI_TARGETS_LOADED=1
|
||||
|
||||
ui_targets_menu() {
|
||||
while true; do
|
||||
local -a items=()
|
||||
local targets
|
||||
targets=$(list_targets)
|
||||
if [[ -n "$targets" ]]; then
|
||||
while IFS= read -r t; do
|
||||
items+=("$t" "Target: $t")
|
||||
done <<< "$targets"
|
||||
fi
|
||||
items+=("ADD" "Add new target")
|
||||
items+=("BACK" "Return to main menu")
|
||||
|
||||
local choice
|
||||
choice=$(ui_menu "Targets" "${items[@]}") || return 0
|
||||
|
||||
case "$choice" in
|
||||
ADD) ui_target_add ;;
|
||||
BACK) return 0 ;;
|
||||
*)
|
||||
local action
|
||||
action=$(ui_menu "Target: $choice" \
|
||||
"EDIT" "Edit target" \
|
||||
"DELETE" "Delete target" \
|
||||
"BACK" "Back") || continue
|
||||
case "$action" in
|
||||
EDIT) ui_target_edit "$choice" ;;
|
||||
DELETE) ui_target_delete "$choice" ;;
|
||||
BACK) continue ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
ui_target_add() {
|
||||
local name
|
||||
name=$(ui_inputbox "Add Target" "Enter target name (letters, digits, _ -). A folder browser will open next:" "") || return 0
|
||||
[[ -z "$name" ]] && return 0
|
||||
|
||||
if ! validate_target_name "$name" 2>/dev/null; then
|
||||
ui_msgbox "Invalid target name. Must start with a letter and contain only letters, digits, underscore, or hyphen (max 32 chars)."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -f "$CONFIG_DIR/targets.d/${name}.conf" ]]; then
|
||||
ui_msgbox "Target '$name' already exists."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local folders
|
||||
folders=$(ui_target_folder_picker) || return 0
|
||||
[[ -z "$folders" ]] && { ui_msgbox "No folders selected. Target not created."; return 0; }
|
||||
|
||||
local exclude
|
||||
exclude=$(ui_inputbox "Add Target" "Exclude patterns (comma-separated, e.g. *.log,*.tmp):" "") || exclude=""
|
||||
|
||||
local remote
|
||||
remote=$(ui_inputbox "Add Target" "Remote override (leave empty for default):" "") || remote=""
|
||||
|
||||
create_target "$name" "$folders" "$exclude" "$remote"
|
||||
ui_msgbox "Target '$name' created successfully."
|
||||
}
|
||||
|
||||
ui_target_edit() {
|
||||
local name="$1"
|
||||
load_target "$name" || { ui_msgbox "Failed to load target '$name'."; return 0; }
|
||||
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(ui_menu "Edit Target: $name" \
|
||||
"FOLDERS" "Folders: ${TARGET_FOLDERS}" \
|
||||
"EXCLUDE" "Exclude: ${TARGET_EXCLUDE:-none}" \
|
||||
"REMOTE" "Remote: ${TARGET_REMOTE:-default}" \
|
||||
"ENABLED" "Enabled: ${TARGET_ENABLED}" \
|
||||
"SAVE" "Save and return" \
|
||||
"BACK" "Cancel") || return 0
|
||||
|
||||
case "$choice" in
|
||||
FOLDERS)
|
||||
local folders
|
||||
folders=$(ui_target_folder_picker "$TARGET_FOLDERS") || continue
|
||||
[[ -n "$folders" ]] && TARGET_FOLDERS="$folders"
|
||||
;;
|
||||
EXCLUDE)
|
||||
local exclude
|
||||
exclude=$(ui_inputbox "Edit Exclude" "Exclude patterns (comma-separated):" "$TARGET_EXCLUDE") || continue
|
||||
TARGET_EXCLUDE="$exclude"
|
||||
;;
|
||||
REMOTE)
|
||||
local remote
|
||||
remote=$(ui_inputbox "Edit Remote" "Remote override (leave empty for default):" "$TARGET_REMOTE") || continue
|
||||
TARGET_REMOTE="$remote"
|
||||
;;
|
||||
ENABLED)
|
||||
if ui_yesno "Enable this target?"; then
|
||||
TARGET_ENABLED="yes"
|
||||
else
|
||||
TARGET_ENABLED="no"
|
||||
fi
|
||||
;;
|
||||
SAVE)
|
||||
create_target "$name" "$TARGET_FOLDERS" "$TARGET_EXCLUDE" "$TARGET_REMOTE" \
|
||||
"$TARGET_RETENTION" "$TARGET_PRE_HOOK" "$TARGET_POST_HOOK" "$TARGET_ENABLED"
|
||||
ui_msgbox "Target '$name' saved."
|
||||
return 0
|
||||
;;
|
||||
BACK) return 0 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
ui_target_delete() {
|
||||
local name="$1"
|
||||
if ui_yesno "Delete target '$name'? This cannot be undone."; then
|
||||
delete_target "$name"
|
||||
ui_msgbox "Target '$name' deleted."
|
||||
fi
|
||||
}
|
||||
|
||||
ui_target_folder_picker() {
|
||||
local existing="${1:-}"
|
||||
local -a folders=()
|
||||
|
||||
if [[ -n "$existing" ]]; then
|
||||
IFS=',' read -ra folders <<< "$existing"
|
||||
fi
|
||||
|
||||
# If no existing folders, open file browser immediately
|
||||
if [[ ${#folders[@]} -eq 0 ]]; then
|
||||
local path
|
||||
path=$(gum file --directory --header "Select folder to back up (Esc when done)" \
|
||||
--cursor.foreground "$_GUM_ACCENT" --height 15 /) || return 1
|
||||
[[ -z "$path" ]] && return 1
|
||||
[[ "$path" != /* ]] && path="/$path"
|
||||
folders+=("$path")
|
||||
fi
|
||||
|
||||
while true; do
|
||||
# Show current selection and options
|
||||
local selected_list=""
|
||||
local i
|
||||
for i in "${!folders[@]}"; do
|
||||
selected_list+=" $(( i + 1 )). ${folders[$i]}\n"
|
||||
done
|
||||
|
||||
local action
|
||||
action=$(ui_menu "Selected folders:\n${selected_list}" \
|
||||
"ADD" "Add another folder" \
|
||||
"REMOVE" "Remove a folder" \
|
||||
"DONE" "Done — use these ${#folders[@]} folder(s)") || return 1
|
||||
|
||||
case "$action" in
|
||||
ADD)
|
||||
local path
|
||||
path=$(gum file --directory --header "Select folder to back up" \
|
||||
--cursor.foreground "$_GUM_ACCENT" --height 15 /) || continue
|
||||
[[ -z "$path" ]] && continue
|
||||
[[ "$path" != /* ]] && path="/$path"
|
||||
# Avoid duplicates
|
||||
local dup=false
|
||||
for f in "${folders[@]}"; do
|
||||
[[ "$f" == "$path" ]] && dup=true
|
||||
done
|
||||
if $dup; then
|
||||
ui_msgbox "Folder '$path' is already selected."
|
||||
else
|
||||
folders+=("$path")
|
||||
fi
|
||||
;;
|
||||
REMOVE)
|
||||
local -a rm_items=()
|
||||
local j=0
|
||||
for f in "${folders[@]}"; do
|
||||
rm_items+=("$j" "$f")
|
||||
((j++))
|
||||
done
|
||||
local idx
|
||||
idx=$(ui_menu "Remove which folder?" "${rm_items[@]}") || continue
|
||||
unset 'folders[idx]'
|
||||
folders=("${folders[@]}")
|
||||
if [[ ${#folders[@]} -eq 0 ]]; then
|
||||
ui_msgbox "All folders removed. Please select at least one."
|
||||
local path
|
||||
path=$(gum file --directory --header "Select folder to back up" \
|
||||
--cursor.foreground "$_GUM_ACCENT" --height 15 /) || return 1
|
||||
[[ -z "$path" ]] && return 1
|
||||
[[ "$path" != /* ]] && path="/$path"
|
||||
folders+=("$path")
|
||||
fi
|
||||
;;
|
||||
DONE)
|
||||
local result
|
||||
result=$(IFS=','; echo "${folders[*]}")
|
||||
echo "$result"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gniza4linux/lib/ui_wizard.sh — First-time setup wizard
|
||||
|
||||
[[ -n "${_GNIZA4LINUX_UI_WIZARD_LOADED:-}" ]] && return 0
|
||||
_GNIZA4LINUX_UI_WIZARD_LOADED=1
|
||||
|
||||
ui_first_run_wizard() {
|
||||
# Step 1: Welcome
|
||||
ui_msgbox "Welcome to gniza Backup Manager!\n\nThis wizard will help you set up your first backup:\n\n 1. Configure a backup destination (remote)\n 2. Define what to back up (target)\n 3. Optionally run your first backup\n\nPress OK to start, or Cancel to skip." \
|
||||
|| return 0
|
||||
|
||||
# Step 2: Create first remote (skip if one already exists)
|
||||
if ! has_remotes; then
|
||||
ui_msgbox "Step 1: Configure Backup Destination\n\nChoose where your backups will be stored:\n - SSH server\n - Local directory (USB/NFS)\n - Amazon S3\n - Google Drive"
|
||||
|
||||
local remote_created=false
|
||||
while ! $remote_created; do
|
||||
ui_remote_add
|
||||
if has_remotes; then
|
||||
remote_created=true
|
||||
else
|
||||
if ! ui_yesno "No remote was created.\n\nWould you like to try again?"; then
|
||||
ui_msgbox "You can configure remotes later from the main menu.\n\nSetup wizard exiting."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Step 3: Create first target (skip if one already exists)
|
||||
if ! has_targets; then
|
||||
ui_msgbox "Step 2: Define Backup Target\n\nChoose a name for your backup profile and select the folders you want to back up."
|
||||
|
||||
local target_created=false
|
||||
while ! $target_created; do
|
||||
ui_target_add
|
||||
if has_targets; then
|
||||
target_created=true
|
||||
else
|
||||
if ! ui_yesno "No target was created.\n\nWould you like to try again?"; then
|
||||
ui_msgbox "You can configure targets later from the main menu.\n\nSetup wizard exiting."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Step 4: Optionally run first backup (only if both exist)
|
||||
if has_remotes && has_targets; then
|
||||
local target
|
||||
target=$(list_targets | head -1)
|
||||
local remote
|
||||
remote=$(list_remotes | head -1)
|
||||
|
||||
if ui_yesno "Run First Backup?\n\nTarget: $target\nRemote: $remote\n\nRun your first backup now?"; then
|
||||
_ui_run_backup "$target" "$remote"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Done
|
||||
ui_msgbox "Setup complete!\n\nYou can manage your backups from the main menu:\n - Add more targets and remotes\n - Schedule automatic backups\n - Browse and restore snapshots"
|
||||
}
|
||||
@@ -74,55 +74,6 @@ for cmd in ssh curl; do
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Install bundled gum binary ───────────────────────────────
|
||||
GUM_VERSION="0.17.0"
|
||||
|
||||
_detect_arch() {
|
||||
local arch; arch=$(uname -m)
|
||||
case "$arch" in
|
||||
x86_64) echo "x86_64" ;;
|
||||
aarch64) echo "arm64" ;;
|
||||
armv7*) echo "armv7" ;;
|
||||
i386|i686) echo "i386" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if command -v gum &>/dev/null; then
|
||||
info "gum already installed: $(command -v gum)"
|
||||
else
|
||||
GUM_ARCH=$(_detect_arch)
|
||||
if [[ -n "$GUM_ARCH" ]] && command -v curl &>/dev/null; then
|
||||
GUM_URL="https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/gum_${GUM_VERSION}_Linux_${GUM_ARCH}.tar.gz"
|
||||
info "Downloading gum v${GUM_VERSION} (${GUM_ARCH})..."
|
||||
GUM_TMP=$(mktemp -d)
|
||||
if curl -fsSL "$GUM_URL" | tar xz -C "$GUM_TMP" 2>/dev/null; then
|
||||
mkdir -p "$INSTALL_DIR/bin"
|
||||
# The tarball extracts to a subdirectory
|
||||
if [[ -f "$GUM_TMP/gum_${GUM_VERSION}_Linux_${GUM_ARCH}/gum" ]]; then
|
||||
cp "$GUM_TMP/gum_${GUM_VERSION}_Linux_${GUM_ARCH}/gum" "$INSTALL_DIR/bin/gum"
|
||||
elif [[ -f "$GUM_TMP/gum" ]]; then
|
||||
cp "$GUM_TMP/gum" "$INSTALL_DIR/bin/gum"
|
||||
fi
|
||||
if [[ -f "$INSTALL_DIR/bin/gum" ]]; then
|
||||
chmod +x "$INSTALL_DIR/bin/gum"
|
||||
info "Installed gum to $INSTALL_DIR/bin/gum"
|
||||
else
|
||||
warn "Failed to locate gum binary in archive"
|
||||
fi
|
||||
else
|
||||
warn "Failed to download gum (TUI will not be available)"
|
||||
fi
|
||||
rm -rf "$GUM_TMP"
|
||||
else
|
||||
if [[ -z "${GUM_ARCH:-}" ]]; then
|
||||
warn "Unsupported architecture for gum: $(uname -m)"
|
||||
else
|
||||
warn "curl not found, cannot download gum (TUI will not be available)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Install files ────────────────────────────────────────────
|
||||
info "Installing to $INSTALL_DIR..."
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
Reference in New Issue
Block a user