Short help shows a compact command list. Full help includes detailed usage for every command with descriptions and config paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
704 lines
28 KiB
Bash
Executable File
704 lines
28 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# ── Resolve GNIZA_DIR ────────────────────────────────────────
|
|
GNIZA_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
|
|
export GNIZA_DIR
|
|
|
|
# ── Source libraries in dependency order ─────────────────────
|
|
source "$GNIZA_DIR/lib/constants.sh"
|
|
source "$GNIZA_DIR/lib/utils.sh"
|
|
detect_mode
|
|
source "$GNIZA_DIR/lib/logging.sh"
|
|
source "$GNIZA_DIR/lib/config.sh"
|
|
source "$GNIZA_DIR/lib/locking.sh"
|
|
source "$GNIZA_DIR/lib/targets.sh"
|
|
source "$GNIZA_DIR/lib/remotes.sh"
|
|
source "$GNIZA_DIR/lib/backup.sh"
|
|
source "$GNIZA_DIR/lib/mysql.sh"
|
|
source "$GNIZA_DIR/lib/restore.sh"
|
|
source "$GNIZA_DIR/lib/retention.sh"
|
|
source "$GNIZA_DIR/lib/schedule.sh"
|
|
source "$GNIZA_DIR/lib/notify.sh"
|
|
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/snaplog.sh"
|
|
source "$GNIZA_DIR/lib/source.sh"
|
|
|
|
# ── Help ─────────────────────────────────────────────────────
|
|
show_help() {
|
|
cat <<EOF
|
|
gniza v${GNIZA4LINUX_VERSION} - Linux Backup Manager
|
|
|
|
Usage: gniza [OPTIONS] [COMMAND]
|
|
|
|
Options:
|
|
--cli Force CLI mode (no TUI)
|
|
--debug Enable debug logging
|
|
--config=FILE Override config file path
|
|
--help Show this help (use --full-help for detailed docs)
|
|
--full-help Show full documentation
|
|
--version Show version
|
|
|
|
Commands:
|
|
targets Manage sources (list, add, delete, show)
|
|
remotes Manage destinations (list, add, delete, show, test)
|
|
backup Run backup
|
|
restore Restore from a snapshot
|
|
snapshots List or browse snapshots
|
|
retention Enforce retention policies
|
|
schedule Manage cron schedules (install, show, remove)
|
|
logs View backup logs
|
|
web Web dashboard (start, install-service, status)
|
|
uninstall Uninstall gniza
|
|
|
|
Run 'gniza --full-help' for detailed usage of all commands.
|
|
If no command is given, the TUI is launched.
|
|
EOF
|
|
}
|
|
|
|
show_full_help() {
|
|
cat <<EOF
|
|
gniza v${GNIZA4LINUX_VERSION} - Linux Backup Manager
|
|
|
|
Usage: gniza [OPTIONS] [COMMAND]
|
|
|
|
Options:
|
|
--cli Force CLI mode (no TUI)
|
|
--debug Enable debug logging
|
|
--config=FILE Override config file path
|
|
--help Show short help
|
|
--full-help Show this full documentation
|
|
--version Show version
|
|
|
|
Sources (what to back up):
|
|
targets list List all configured sources
|
|
targets add --name=NAME --folders=PATHS
|
|
Create a new source
|
|
targets delete --name=NAME Delete a source
|
|
targets show --name=NAME Show full source configuration
|
|
|
|
Destinations (where to store backups):
|
|
remotes list List all configured destinations
|
|
remotes add --name=NAME Create a new destination (edit config manually or via TUI)
|
|
remotes delete --name=NAME Delete a destination
|
|
remotes show --name=NAME Show full destination configuration
|
|
remotes test --name=NAME Validate destination connectivity
|
|
remotes disk-info-short --name=NAME Show destination disk usage (used/total/free)
|
|
|
|
Operations:
|
|
backup [--target=NAME] [--remote=NAME] [--all]
|
|
Run backup. If no source is specified, all sources are backed up.
|
|
Use --target=a,b,c to back up multiple sources.
|
|
Use --remote=NAME to target a specific destination.
|
|
|
|
restore --target=NAME --snapshot=TS [--remote=NAME] [--dest=DIR]
|
|
[--folder=PATH] [--skip-mysql]
|
|
Restore from a snapshot. Use --dest for custom restore location.
|
|
Use --folder to restore a single directory from the snapshot.
|
|
Use --skip-mysql to skip MySQL database restore.
|
|
|
|
retention [--target=NAME] [--remote=NAME] [--all]
|
|
Enforce snapshot retention policies. Deletes snapshots beyond
|
|
the configured retention count (oldest first, pinned preserved).
|
|
|
|
Snapshots:
|
|
snapshots list [--target=NAME] [--remote=NAME]
|
|
List available snapshots. Filter by source and/or destination.
|
|
|
|
snapshots browse --target=NAME --snapshot=TS [--remote=NAME]
|
|
List all files in a specific snapshot.
|
|
|
|
Scheduling:
|
|
schedule install Install cron entries for all active schedules
|
|
schedule show Show current gniza cron entries
|
|
schedule remove Remove all gniza cron entries from crontab
|
|
|
|
Logs & Info:
|
|
logs List all log files
|
|
logs --last Show the most recent backup log
|
|
logs --last --tail=N Show last N lines of the most recent log
|
|
version Show version
|
|
|
|
Web Dashboard:
|
|
web start [--port=PORT] [--host=HOST]
|
|
Start the web dashboard (default: port 2323, host 0.0.0.0).
|
|
Serves the full TUI in a browser with HTTP Basic Auth.
|
|
|
|
web install-service Install as a systemd service (auto-starts on boot)
|
|
web remove-service Stop and remove the systemd service
|
|
web status Show service status
|
|
|
|
System:
|
|
uninstall Run the uninstall script
|
|
|
|
Configuration:
|
|
Sources: \$CONFIG_DIR/targets.d/<name>.conf
|
|
Destinations: \$CONFIG_DIR/remotes.d/<name>.conf
|
|
Schedules: \$CONFIG_DIR/schedules.d/<name>.conf
|
|
Settings: \$CONFIG_DIR/gniza.conf
|
|
|
|
Root mode: /etc/gniza/
|
|
User mode: ~/.config/gniza/
|
|
|
|
If no command is given, the TUI is launched.
|
|
EOF
|
|
}
|
|
|
|
# ── CLI argument parsing ─────────────────────────────────────
|
|
FORCE_CLI=false
|
|
CONFIG_FILE=""
|
|
SUBCOMMAND=""
|
|
declare -a SUBCMD_ARGS=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--cli)
|
|
FORCE_CLI=true
|
|
shift
|
|
;;
|
|
--debug)
|
|
export GNIZA4LINUX_DEBUG="true"
|
|
shift
|
|
;;
|
|
--config=*)
|
|
CONFIG_FILE="${1#--config=}"
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
--full-help|--docs)
|
|
show_full_help
|
|
exit 0
|
|
;;
|
|
--version)
|
|
echo "gniza v${GNIZA4LINUX_VERSION}"
|
|
exit 0
|
|
;;
|
|
-*)
|
|
# Unknown flags before subcommand are errors
|
|
die "Unknown option: $1 (see --help)"
|
|
;;
|
|
*)
|
|
SUBCOMMAND="$1"
|
|
shift
|
|
SUBCMD_ARGS=("$@")
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# ── Initialise ───────────────────────────────────────────────
|
|
ensure_dirs
|
|
|
|
# Load config if it exists
|
|
if [[ -n "$CONFIG_FILE" ]]; then
|
|
load_config "$CONFIG_FILE"
|
|
elif [[ -f "$CONFIG_DIR/gniza.conf" ]]; then
|
|
load_config
|
|
fi
|
|
|
|
# Only create log files for operations that produce meaningful output
|
|
case "${SUBCOMMAND:-}" in
|
|
backup|restore|retention|scheduled-run) init_logging ;;
|
|
esac
|
|
|
|
# ── Parse subcommand flags helper ────────────────────────────
|
|
_parse_flag() {
|
|
local flag="$1"
|
|
shift
|
|
local arg
|
|
for arg in "$@"; do
|
|
if [[ "$arg" == "${flag}="* ]]; then
|
|
echo "${arg#"${flag}="}"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
_has_flag() {
|
|
local flag="$1"
|
|
shift
|
|
local arg
|
|
for arg in "$@"; do
|
|
[[ "$arg" == "$flag" ]] && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# ── CLI subcommand dispatch ──────────────────────────────────
|
|
run_cli() {
|
|
case "${SUBCOMMAND:-}" in
|
|
backup)
|
|
local target="" remote="" all=false
|
|
target=$(_parse_flag "--target" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
remote=$(_parse_flag "--remote" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
_has_flag "--all" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}" && all=true
|
|
|
|
trap release_all_target_locks EXIT
|
|
|
|
if [[ "$all" == "true" || -z "$target" ]]; then
|
|
backup_all_targets "$remote"
|
|
elif [[ "$target" == *,* ]]; then
|
|
# Comma-separated targets (e.g. from schedule --target=web,db)
|
|
local IFS=','
|
|
local names
|
|
read -ra names <<< "$target"
|
|
local rc=0
|
|
for t in "${names[@]}"; do
|
|
t="${t#"${t%%[![:space:]]*}"}"
|
|
t="${t%"${t##*[![:space:]]}"}"
|
|
[[ -z "$t" ]] && continue
|
|
backup_target "$t" "$remote" || rc=$?
|
|
done
|
|
exit "$rc"
|
|
else
|
|
backup_target "$target" "$remote"
|
|
fi
|
|
;;
|
|
|
|
restore)
|
|
local target="" snapshot="" remote="" dest="" folder=""
|
|
target=$(_parse_flag "--target" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
snapshot=$(_parse_flag "--snapshot" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
remote=$(_parse_flag "--remote" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
dest=$(_parse_flag "--dest" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
folder=$(_parse_flag "--folder" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
|
|
# Check for --skip-mysql flag
|
|
local skip_mysql=""
|
|
local arg
|
|
for arg in "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}"; do
|
|
[[ "$arg" == "--skip-mysql" ]] && skip_mysql="yes"
|
|
done
|
|
[[ -n "$skip_mysql" ]] && export SKIP_MYSQL_RESTORE="yes"
|
|
|
|
[[ -z "$target" ]] && die "restore requires --target=NAME"
|
|
|
|
if [[ -z "$remote" ]]; then
|
|
remote=$(list_remotes | head -1)
|
|
[[ -z "$remote" ]] && die "No destinations configured"
|
|
fi
|
|
|
|
if [[ -n "$folder" ]]; then
|
|
restore_folder "$target" "$folder" "${snapshot:-latest}" "$remote" "$dest"
|
|
else
|
|
restore_target "$target" "${snapshot:-latest}" "$remote" "$dest"
|
|
fi
|
|
;;
|
|
|
|
targets)
|
|
local action="${SUBCMD_ARGS[0]:-list}"
|
|
local name="" folders=""
|
|
name=$(_parse_flag "--name" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
folders=$(_parse_flag "--folders" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
|
|
case "$action" in
|
|
list)
|
|
local targets; targets=$(list_targets)
|
|
if [[ -z "$targets" ]]; then
|
|
echo "No sources configured."
|
|
else
|
|
echo "$targets"
|
|
fi
|
|
;;
|
|
add)
|
|
[[ -z "$name" ]] && die "sources add requires --name=NAME"
|
|
[[ -z "$folders" ]] && die "sources add requires --folders=PATHS"
|
|
create_target "$name" "$folders"
|
|
;;
|
|
delete)
|
|
[[ -z "$name" ]] && die "sources delete requires --name=NAME"
|
|
delete_target "$name"
|
|
;;
|
|
show)
|
|
[[ -z "$name" ]] && die "sources show requires --name=NAME"
|
|
load_target "$name"
|
|
echo "=== Source: $TARGET_NAME ==="
|
|
echo ""
|
|
echo "Name: $TARGET_NAME"
|
|
echo "Enabled: $TARGET_ENABLED"
|
|
echo "Folders: $TARGET_FOLDERS"
|
|
echo "Exclude: ${TARGET_EXCLUDE:-(none)}"
|
|
echo "Include: ${TARGET_INCLUDE:-(none)}"
|
|
echo "Destination: ${TARGET_REMOTE:-(all)}"
|
|
echo "Retention: ${TARGET_RETENTION:-(default)}"
|
|
echo "Pre-hook: ${TARGET_PRE_HOOK:-(none)}"
|
|
echo "Post-hook: ${TARGET_POST_HOOK:-(none)}"
|
|
echo ""
|
|
echo "--- Source Type ---"
|
|
echo "Type: $TARGET_SOURCE_TYPE"
|
|
case "$TARGET_SOURCE_TYPE" in
|
|
ssh)
|
|
echo "Host: $TARGET_SOURCE_HOST"
|
|
echo "Port: $TARGET_SOURCE_PORT"
|
|
echo "User: $TARGET_SOURCE_USER"
|
|
echo "Auth method: $TARGET_SOURCE_AUTH_METHOD"
|
|
if [[ "$TARGET_SOURCE_AUTH_METHOD" == "key" ]]; then
|
|
echo "Key: ${TARGET_SOURCE_KEY:-(default)}"
|
|
else
|
|
echo "Password: ****"
|
|
fi
|
|
;;
|
|
s3)
|
|
echo "Bucket: $TARGET_SOURCE_S3_BUCKET"
|
|
echo "Region: $TARGET_SOURCE_S3_REGION"
|
|
echo "Endpoint: ${TARGET_SOURCE_S3_ENDPOINT:-(default)}"
|
|
echo "Access Key: ${TARGET_SOURCE_S3_ACCESS_KEY_ID:+****}"
|
|
echo "Secret Key: ${TARGET_SOURCE_S3_SECRET_ACCESS_KEY:+****}"
|
|
;;
|
|
gdrive)
|
|
echo "SA File: $TARGET_SOURCE_GDRIVE_SERVICE_ACCOUNT_FILE"
|
|
echo "Root Folder ID: ${TARGET_SOURCE_GDRIVE_ROOT_FOLDER_ID:-(root)}"
|
|
;;
|
|
esac
|
|
if [[ "$TARGET_MYSQL_ENABLED" == "yes" ]]; then
|
|
echo ""
|
|
echo "--- MySQL ---"
|
|
echo "Enabled: yes"
|
|
echo "Mode: $TARGET_MYSQL_MODE"
|
|
echo "Databases: ${TARGET_MYSQL_DATABASES:-(all)}"
|
|
echo "Exclude: ${TARGET_MYSQL_EXCLUDE:-(none)}"
|
|
echo "User: ${TARGET_MYSQL_USER:-(current)}"
|
|
echo "Password: ${TARGET_MYSQL_PASSWORD:+****}"
|
|
echo "Host: $TARGET_MYSQL_HOST"
|
|
echo "Port: $TARGET_MYSQL_PORT"
|
|
echo "Extra opts: $TARGET_MYSQL_EXTRA_OPTS"
|
|
fi
|
|
;;
|
|
*)
|
|
die "Unknown sources action: $action (expected list|add|delete|show)"
|
|
;;
|
|
esac
|
|
;;
|
|
|
|
remotes)
|
|
local action="${SUBCMD_ARGS[0]:-list}"
|
|
local name=""
|
|
name=$(_parse_flag "--name" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
|
|
case "$action" in
|
|
list)
|
|
local remotes; remotes=$(list_remotes)
|
|
if [[ -z "$remotes" ]]; then
|
|
echo "No destinations configured."
|
|
else
|
|
echo "$remotes"
|
|
fi
|
|
;;
|
|
add)
|
|
[[ -z "$name" ]] && die "destinations add requires --name=NAME"
|
|
echo "Use the TUI or manually create $CONFIG_DIR/remotes.d/${name}.conf to configure the destination."
|
|
;;
|
|
delete)
|
|
[[ -z "$name" ]] && die "destinations delete requires --name=NAME"
|
|
local conf="$CONFIG_DIR/remotes.d/${name}.conf"
|
|
[[ ! -f "$conf" ]] && die "Destination config not found: $conf"
|
|
rm -f "$conf"
|
|
log_info "Deleted destination config: $conf"
|
|
;;
|
|
show)
|
|
[[ -z "$name" ]] && die "destinations show requires --name=NAME"
|
|
load_remote "$name"
|
|
echo "=== Destination: $name ==="
|
|
echo ""
|
|
echo "Type: $REMOTE_TYPE"
|
|
case "$REMOTE_TYPE" in
|
|
ssh)
|
|
echo "Host: $REMOTE_HOST"
|
|
echo "Port: $REMOTE_PORT"
|
|
echo "User: $REMOTE_USER"
|
|
echo "Auth method: $REMOTE_AUTH_METHOD"
|
|
if [[ "$REMOTE_AUTH_METHOD" == "key" ]]; then
|
|
echo "Key: ${REMOTE_KEY:-(default)}"
|
|
else
|
|
echo "Password: ****"
|
|
fi
|
|
echo "Base path: $REMOTE_BASE"
|
|
;;
|
|
local)
|
|
echo "Base path: $REMOTE_BASE"
|
|
;;
|
|
s3)
|
|
echo "Bucket: ${S3_BUCKET:-}"
|
|
echo "Region: ${S3_REGION:-}"
|
|
echo "Endpoint: ${S3_ENDPOINT:-(default)}"
|
|
echo "Access Key: ${S3_ACCESS_KEY_ID:+****}"
|
|
echo "Secret Key: ${S3_SECRET_ACCESS_KEY:+****}"
|
|
echo "Base path: $REMOTE_BASE"
|
|
;;
|
|
gdrive)
|
|
echo "SA File: ${GDRIVE_SERVICE_ACCOUNT_FILE:-}"
|
|
echo "Root Folder ID: ${GDRIVE_ROOT_FOLDER_ID:-(root)}"
|
|
echo "Base path: $REMOTE_BASE"
|
|
;;
|
|
esac
|
|
echo "Bandwidth: ${BWLIMIT:-0} KB/s"
|
|
echo "Retention: $RETENTION_COUNT snapshots"
|
|
;;
|
|
test)
|
|
[[ -z "$name" ]] && die "destinations test requires --name=NAME"
|
|
validate_remote "$name"
|
|
echo "Destination '$name' is valid."
|
|
;;
|
|
disk-info-short)
|
|
[[ -z "$name" ]] && die "destinations disk-info-short requires --name=NAME"
|
|
load_remote "$name"
|
|
remote_disk_info_short
|
|
;;
|
|
*)
|
|
die "Unknown destinations action: $action (expected list|add|delete|show|test|disk-info-short)"
|
|
;;
|
|
esac
|
|
;;
|
|
|
|
snapshots)
|
|
local action="${SUBCMD_ARGS[0]:-list}"
|
|
local target="" remote=""
|
|
target=$(_parse_flag "--target" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
remote=$(_parse_flag "--remote" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
|
|
case "$action" in
|
|
list)
|
|
if [[ -z "$remote" ]]; then
|
|
remote=$(list_remotes | head -1)
|
|
[[ -z "$remote" ]] && die "No destinations configured"
|
|
fi
|
|
_save_remote_globals
|
|
load_remote "$remote" || die "Failed to load destination: $remote"
|
|
if [[ -n "$target" ]]; then
|
|
list_remote_snapshots "$target"
|
|
else
|
|
local targets; targets=$(list_targets)
|
|
while IFS= read -r t; do
|
|
[[ -z "$t" ]] && continue
|
|
echo "=== $t ==="
|
|
list_remote_snapshots "$t" || true
|
|
done <<< "$targets"
|
|
fi
|
|
_restore_remote_globals
|
|
;;
|
|
browse)
|
|
[[ -z "$target" ]] && die "browse requires --target=NAME"
|
|
local snapshot=""
|
|
snapshot=$(_parse_flag "--snapshot" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
[[ -z "$snapshot" ]] && die "browse requires --snapshot=TS"
|
|
if [[ -z "$remote" ]]; then
|
|
remote=$(list_remotes | head -1)
|
|
[[ -z "$remote" ]] && die "No destinations configured"
|
|
fi
|
|
list_snapshot_contents "$target" "$snapshot" "$remote"
|
|
;;
|
|
*)
|
|
die "Unknown snapshots action: $action (expected list|browse)"
|
|
;;
|
|
esac
|
|
;;
|
|
|
|
retention)
|
|
local target="" remote="" all=false
|
|
target=$(_parse_flag "--target" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
remote=$(_parse_flag "--remote" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
_has_flag "--all" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}" && all=true
|
|
|
|
if [[ -z "$remote" ]]; then
|
|
remote=$(list_remotes | head -1)
|
|
[[ -z "$remote" ]] && die "No destinations configured"
|
|
fi
|
|
_save_remote_globals
|
|
load_remote "$remote" || die "Failed to load destination: $remote"
|
|
|
|
if [[ "$all" == "true" || -z "$target" ]]; then
|
|
local targets; targets=$(list_targets)
|
|
while IFS= read -r t; do
|
|
[[ -z "$t" ]] && continue
|
|
enforce_retention "$t"
|
|
done <<< "$targets"
|
|
else
|
|
enforce_retention "$target"
|
|
fi
|
|
_restore_remote_globals
|
|
;;
|
|
|
|
schedule)
|
|
local action="${SUBCMD_ARGS[0]:-show}"
|
|
case "$action" in
|
|
install) install_schedules ;;
|
|
show) show_schedules ;;
|
|
remove) remove_schedules ;;
|
|
*) die "Unknown schedule action: $action (expected install|show|remove)" ;;
|
|
esac
|
|
;;
|
|
|
|
logs)
|
|
local last=false tail_n=""
|
|
_has_flag "--last" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}" && last=true
|
|
tail_n=$(_parse_flag "--tail" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
|
|
local log_dir="${LOG_DIR}"
|
|
if [[ "$last" == "true" ]]; then
|
|
local latest; latest=$(ls -t "$log_dir"/gniza-*.log 2>/dev/null | head -1)
|
|
if [[ -z "$latest" ]]; then
|
|
echo "No log files found."
|
|
else
|
|
if [[ -n "$tail_n" ]]; then
|
|
tail -n "$tail_n" "$latest"
|
|
else
|
|
cat "$latest"
|
|
fi
|
|
fi
|
|
else
|
|
ls -lt "$log_dir"/gniza-*.log 2>/dev/null || echo "No log files found."
|
|
fi
|
|
;;
|
|
|
|
scheduled-run)
|
|
local sched_name="" target="" remote=""
|
|
sched_name=$(_parse_flag "--schedule" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
target=$(_parse_flag "--target" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
remote=$(_parse_flag "--remote" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
[[ -z "$sched_name" ]] && die "scheduled-run requires --schedule=NAME"
|
|
|
|
trap release_all_target_locks EXIT
|
|
|
|
local rc=0
|
|
if [[ -n "$target" && "$target" == *,* ]]; then
|
|
local IFS=','
|
|
local names
|
|
read -ra names <<< "$target"
|
|
for t in "${names[@]}"; do
|
|
t="${t#"${t%%[![:space:]]*}"}"
|
|
t="${t%"${t##*[![:space:]]}"}"
|
|
[[ -z "$t" ]] && continue
|
|
backup_target "$t" "$remote" || rc=$?
|
|
done
|
|
elif [[ -n "$target" ]]; then
|
|
backup_target "$target" "$remote" || rc=$?
|
|
else
|
|
backup_all_targets "$remote" || rc=$?
|
|
fi
|
|
|
|
# Stamp LAST_RUN on success
|
|
if (( rc == 0 )); then
|
|
local conf="$CONFIG_DIR/schedules.d/${sched_name}.conf"
|
|
if [[ -f "$conf" ]]; then
|
|
sed -i '/^LAST_RUN=/d' "$conf"
|
|
echo "LAST_RUN=\"$(date '+%Y-%m-%d %H:%M')\"" >> "$conf"
|
|
fi
|
|
fi
|
|
exit "$rc"
|
|
;;
|
|
|
|
version)
|
|
echo "gniza v${GNIZA4LINUX_VERSION}"
|
|
;;
|
|
|
|
"")
|
|
show_help
|
|
;;
|
|
|
|
*)
|
|
die "Unknown command: $SUBCOMMAND (see --help)"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ── Mode selection ───────────────────────────────────────────
|
|
if [[ "$SUBCOMMAND" == "web" ]]; then
|
|
_web_action="${SUBCMD_ARGS[0]:-start}"
|
|
case "$_web_action" in
|
|
start)
|
|
_web_port="" _web_host=""
|
|
_web_port=$(_parse_flag "--port" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
_web_host=$(_parse_flag "--host" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true
|
|
_web_args=(--web)
|
|
[[ -n "$_web_port" ]] && _web_args+=(--port="$_web_port")
|
|
[[ -n "$_web_host" ]] && _web_args+=(--host="$_web_host")
|
|
PYTHONPATH="$GNIZA_DIR:${PYTHONPATH:-}" exec python3 -m tui "${_web_args[@]}"
|
|
;;
|
|
install-service)
|
|
if [[ $EUID -eq 0 ]]; then
|
|
_service_src="$GNIZA_DIR/etc/gniza-web.service"
|
|
_service_dst="/etc/systemd/system/gniza-web.service"
|
|
if [[ ! -f "$_service_src" ]]; then
|
|
die "Service file not found: $_service_src"
|
|
fi
|
|
cp "$_service_src" "$_service_dst"
|
|
systemctl daemon-reload
|
|
systemctl enable gniza-web
|
|
systemctl restart gniza-web
|
|
else
|
|
_user_service_dir="$HOME/.config/systemd/user"
|
|
mkdir -p "$_user_service_dir"
|
|
cat > "$_user_service_dir/gniza-web.service" <<SVCEOF
|
|
[Unit]
|
|
Description=GNIZA Web Dashboard
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=$(command -v python3) -m tui --web --host 0.0.0.0 --port 2323
|
|
WorkingDirectory=$GNIZA_DIR
|
|
Environment=GNIZA_DIR=$GNIZA_DIR
|
|
Environment=PYTHONPATH=$GNIZA_DIR
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
SVCEOF
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable gniza-web
|
|
systemctl --user restart gniza-web
|
|
fi
|
|
echo "GNIZA web service installed and started."
|
|
echo "Access the dashboard at http://$(hostname -I | awk '{print $1}'):2323"
|
|
;;
|
|
remove-service)
|
|
if [[ $EUID -eq 0 ]]; then
|
|
systemctl stop gniza-web 2>/dev/null || true
|
|
systemctl disable gniza-web 2>/dev/null || true
|
|
rm -f /etc/systemd/system/gniza-web.service
|
|
systemctl daemon-reload
|
|
else
|
|
systemctl --user stop gniza-web 2>/dev/null || true
|
|
systemctl --user disable gniza-web 2>/dev/null || true
|
|
rm -f "$HOME/.config/systemd/user/gniza-web.service"
|
|
systemctl --user daemon-reload 2>/dev/null || true
|
|
fi
|
|
echo "GNIZA web service removed."
|
|
;;
|
|
status)
|
|
if [[ $EUID -eq 0 ]]; then
|
|
systemctl status gniza-web 2>/dev/null || echo "GNIZA web service is not installed."
|
|
else
|
|
systemctl --user status gniza-web 2>/dev/null || echo "GNIZA web service is not installed."
|
|
fi
|
|
;;
|
|
*) die "Unknown web action: $_web_action (expected start|install-service|remove-service|status)" ;;
|
|
esac
|
|
elif [[ "$SUBCOMMAND" == "uninstall" ]]; then
|
|
_uninstall_script="$GNIZA_DIR/scripts/uninstall.sh"
|
|
if [[ -f "$_uninstall_script" ]]; then
|
|
exec bash "$_uninstall_script"
|
|
else
|
|
die "Uninstall script not found: $_uninstall_script"
|
|
fi
|
|
elif [[ -n "$SUBCOMMAND" ]]; then
|
|
# Explicit subcommand: always CLI
|
|
run_cli
|
|
elif [[ "$FORCE_CLI" == "true" ]]; then
|
|
run_cli
|
|
elif python3 -c "import textual" 2>/dev/null && [[ -t 1 ]]; then
|
|
# Python Textual TUI mode
|
|
PYTHONPATH="$GNIZA_DIR:${PYTHONPATH:-}" exec python3 -m tui "$@"
|
|
else
|
|
# Fallback to CLI help
|
|
run_cli
|
|
fi
|