diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 0000000..9147815 --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,1066 @@ +# gniza Documentation + +Complete reference for gniza, a Linux backup manager that works as a stand-alone backup solution or a centralized backup server. + +--- + +## Table of Contents + +- [Overview](#overview) +- [Installation](#installation) +- [Concepts](#concepts) +- [Sources](#sources) +- [Destinations](#destinations) +- [Backup](#backup) +- [Restore](#restore) +- [Snapshots](#snapshots) +- [Retention](#retention) +- [Scheduling](#scheduling) +- [MySQL Backup](#mysql-backup) +- [Notifications](#notifications) +- [Web Dashboard](#web-dashboard) +- [Terminal UI](#terminal-ui) +- [CLI Reference](#cli-reference) +- [Global Settings](#global-settings) +- [Security](#security) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +gniza backs up files and MySQL databases from **sources** to **destinations** using incremental rsync snapshots with hardlink deduplication. + +**Stand-alone mode**: Install gniza on any Linux machine. Define local folders as sources and back them up to an SSH server, USB drive, S3 bucket, or Google Drive. + +**Backup server mode**: Install gniza on a central server. Define remote SSH servers, S3 buckets, or Google Drive accounts as sources. gniza pulls files from them and stores snapshots locally or on another destination — no agent needed on the source machines. + +**Hybrid**: Mix local and remote sources freely. Back up local configs alongside files pulled from multiple remote servers. + +### Interfaces + +gniza provides three interfaces: + +| Interface | Launch | Best for | +|-----------|--------|----------| +| **TUI** | `gniza` | Interactive management | +| **Web** | `gniza web start` | Remote browser access | +| **CLI** | `gniza --cli ` | Scripting and cron | + +--- + +## Installation + +### Quick Install + +```bash +# Root mode (system-wide) +curl -sSL https://git.linux-hosting.co.il/shukivaknin/gniza4linux/raw/branch/main/scripts/install.sh | sudo bash + +# User mode (per-user) +curl -sSL https://git.linux-hosting.co.il/shukivaknin/gniza4linux/raw/branch/main/scripts/install.sh | bash +``` + +### From Source + +```bash +git clone https://git.linux-hosting.co.il/shukivaknin/gniza4linux.git +cd gniza4linux +sudo bash scripts/install.sh # root mode +bash scripts/install.sh # user mode +``` + +### Install Paths + +| Mode | Application | Config | Logs | +|------|-------------|--------|------| +| Root | `/usr/local/gniza` | `/etc/gniza/` | `/var/log/gniza/` | +| User | `~/.local/share/gniza` | `~/.config/gniza/` | `~/.local/state/gniza/log/` | + +### Dependencies + +| Package | Required | Purpose | +|---------|----------|---------| +| bash 4+ | Yes | Core scripting | +| rsync | Yes | File transfer and deduplication | +| ssh | No | SSH sources and destinations | +| sshpass | No | Password-based SSH authentication | +| curl | No | SMTP email notifications | +| rclone | No | S3 and Google Drive support | +| python3 | No | TUI and web dashboard | +| textual | No | Terminal UI framework | +| textual-serve | No | Web dashboard | + +The installer detects available dependencies and warns about missing optional ones. + +### Post-Install + +After installation, run `gniza` to launch the TUI. On first run, the setup wizard guides you through creating your first source and destination. + +--- + +## Concepts + +### Sources + +A **source** defines *what* to back up: a set of folders, optional filters, hooks, and MySQL database settings. Sources can pull data from local directories, remote SSH servers, S3 buckets, or Google Drive. + +Config location: `/targets.d/.conf` + +### Destinations + +A **destination** defines *where* to store backup snapshots. Destinations can be SSH servers, local drives (USB/NFS), S3 buckets, or Google Drive. + +Config location: `/remotes.d/.conf` + +### Snapshots + +Each backup creates a **snapshot** — a timestamped directory containing a full copy of the backed-up files. Unchanged files are hardlinked to the previous snapshot, saving disk space while keeping each snapshot independently browseable and restorable. + +### Retention + +**Retention** controls how many snapshots to keep. Old snapshots are automatically pruned after each backup. Individual snapshots can be **pinned** to prevent deletion. + +--- + +## Sources + +### Creating a Source + +**TUI**: Navigate to Sources > Add. + +**CLI**: +```bash +gniza --cli targets add --name=mysite --folders=/var/www,/etc/nginx +``` + +**Manual**: Create `/targets.d/mysite.conf`. + +### Source Configuration + +#### Basic Fields + +| Field | Default | Description | +|-------|---------|-------------| +| `TARGET_NAME` | (required) | Unique name for this source | +| `TARGET_FOLDERS` | (required) | Comma-separated absolute paths to back up | +| `TARGET_EXCLUDE` | (empty) | Comma-separated rsync exclude patterns | +| `TARGET_INCLUDE` | (empty) | Comma-separated rsync include patterns | +| `TARGET_REMOTE` | (empty) | Pin to a specific destination (empty = all) | +| `TARGET_RETENTION` | (empty) | Override retention count (empty = use destination default) | +| `TARGET_PRE_HOOK` | (empty) | Shell command to run before backup | +| `TARGET_POST_HOOK` | (empty) | Shell command to run after backup | +| `TARGET_ENABLED` | `yes` | Set to `no` to skip this source during backups | + +#### Include and Exclude Filters + +Filters use rsync glob syntax and are comma-separated. + +**Exclude mode**: Skip files matching the patterns. +```ini +TARGET_EXCLUDE="*.log,*.tmp,.cache,node_modules" +``` + +**Include mode**: Only back up files matching the patterns. Everything else is excluded. +```ini +TARGET_INCLUDE="*.conf,*.sh,*.py" +``` + +If both are set, include takes precedence. + +#### Source Types + +By default, sources back up local directories. Set `TARGET_SOURCE_TYPE` to pull from remote locations instead. + +##### Local Source (default) + +```ini +TARGET_SOURCE_TYPE="local" +TARGET_FOLDERS="/var/www,/etc/nginx" +``` + +Folders must exist on the local machine. + +##### SSH Source + +Pull files from a remote server over SSH. No agent needed on the remote machine. + +```ini +TARGET_SOURCE_TYPE="ssh" +TARGET_SOURCE_HOST="web-server.example.com" +TARGET_SOURCE_PORT="22" +TARGET_SOURCE_USER="root" +TARGET_SOURCE_AUTH_METHOD="key" +TARGET_SOURCE_KEY="/root/.ssh/id_rsa" +TARGET_FOLDERS="/var/www,/etc/nginx,/etc/mysql" +``` + +For password authentication: +```ini +TARGET_SOURCE_AUTH_METHOD="password" +TARGET_SOURCE_PASSWORD="your-password" +``` + +Requires `sshpass` for password auth. + +##### S3 Source + +Pull files from an S3 or S3-compatible bucket (MinIO, DigitalOcean Spaces, Backblaze B2). + +```ini +TARGET_SOURCE_TYPE="s3" +TARGET_SOURCE_S3_BUCKET="my-app-data" +TARGET_SOURCE_S3_REGION="us-east-1" +TARGET_SOURCE_S3_ACCESS_KEY_ID="AKIA..." +TARGET_SOURCE_S3_SECRET_ACCESS_KEY="..." +TARGET_SOURCE_S3_ENDPOINT="" +TARGET_FOLDERS="/uploads,/media" +``` + +Set `TARGET_SOURCE_S3_ENDPOINT` for S3-compatible providers: +```ini +TARGET_SOURCE_S3_ENDPOINT="https://nyc3.digitaloceanspaces.com" +``` + +Requires `rclone`. + +##### Google Drive Source + +Pull files from Google Drive using a service account. + +```ini +TARGET_SOURCE_TYPE="gdrive" +TARGET_SOURCE_GDRIVE_SERVICE_ACCOUNT_FILE="/path/to/service-account.json" +TARGET_SOURCE_GDRIVE_ROOT_FOLDER_ID="1abc..." +TARGET_FOLDERS="/shared-docs,/reports" +``` + +Requires `rclone`. + +#### Pre/Post Hooks + +Run shell commands before and after backup: + +```ini +TARGET_PRE_HOOK="systemctl stop myapp" +TARGET_POST_HOOK="systemctl start myapp" +``` + +- **Pre-hook failure** aborts the backup +- **Post-hook failure** is logged as a warning but does not mark the backup as failed + +Common uses: +- Stop/start services for consistent snapshots +- Flush caches or application buffers +- Trigger database snapshots +- Send custom notifications + +### Viewing a Source + +```bash +gniza --cli targets show --name=mysite +``` + +Shows all configured fields including source type details and MySQL settings. + +### Deleting a Source + +```bash +gniza --cli targets delete --name=mysite +``` + +This removes the config file only. Existing snapshots on destinations are not affected. + +--- + +## Destinations + +### Creating a Destination + +**TUI**: Navigate to Destinations > Add. + +**CLI**: +```bash +gniza --cli remotes add --name=backup-server +``` + +This creates a config template. Edit it manually or use the TUI to configure. + +### Destination Types + +#### SSH Destination + +Store snapshots on a remote server via SSH. + +```ini +REMOTE_TYPE="ssh" +REMOTE_HOST="backup.example.com" +REMOTE_PORT="22" +REMOTE_USER="root" +REMOTE_AUTH_METHOD="key" +REMOTE_KEY="/root/.ssh/id_rsa" +REMOTE_BASE="/backups" +BWLIMIT="0" +RETENTION_COUNT="30" +``` + +If `REMOTE_KEY` is not specified, it defaults to `~/.ssh/id_rsa`. + +For password authentication: +```ini +REMOTE_AUTH_METHOD="password" +REMOTE_PASSWORD="your-password" +``` + +#### Local Destination + +Store snapshots on a local drive (USB, NFS mount, second disk). + +```ini +REMOTE_TYPE="local" +REMOTE_BASE="/mnt/backup-drive" +RETENTION_COUNT="30" +``` + +#### S3 Destination + +Store snapshots in an S3 or S3-compatible bucket. + +```ini +REMOTE_TYPE="s3" +S3_BUCKET="my-backups" +S3_ACCESS_KEY_ID="AKIA..." +S3_SECRET_ACCESS_KEY="..." +S3_REGION="us-east-1" +S3_ENDPOINT="" +REMOTE_BASE="/backups" +RETENTION_COUNT="30" +``` + +Requires `rclone`. + +#### Google Drive Destination + +Store snapshots in Google Drive using a service account. + +```ini +REMOTE_TYPE="gdrive" +GDRIVE_SERVICE_ACCOUNT_FILE="/path/to/service-account.json" +GDRIVE_ROOT_FOLDER_ID="" +REMOTE_BASE="/backups" +RETENTION_COUNT="30" +``` + +Requires `rclone`. + +### Destination Fields + +| Field | Default | Description | +|-------|---------|-------------| +| `REMOTE_TYPE` | `ssh` | `ssh`, `local`, `s3`, or `gdrive` | +| `REMOTE_HOST` | (required for SSH) | Hostname or IP | +| `REMOTE_PORT` | `22` | SSH port | +| `REMOTE_USER` | `root` | SSH username | +| `REMOTE_AUTH_METHOD` | `key` | `key` or `password` | +| `REMOTE_KEY` | `~/.ssh/id_rsa` | Path to SSH private key | +| `REMOTE_PASSWORD` | (empty) | SSH password (requires sshpass) | +| `REMOTE_BASE` | `/backups` | Base directory for snapshots | +| `BWLIMIT` | `0` | Bandwidth limit in KB/s (0 = unlimited) | +| `RETENTION_COUNT` | `30` | Number of snapshots to keep per source | + +### Testing a Destination + +```bash +gniza --cli remotes test --name=backup-server +``` + +Validates connectivity and configuration. For SSH destinations, tests the SSH connection. For S3/GDrive, verifies credentials and access. + +### Checking Disk Usage + +```bash +gniza --cli remotes disk-info-short --name=backup-server +``` + +Shows used/total space, free space, and usage percentage. Works with SSH and local destinations. + +--- + +## Backup + +### Running a Backup + +```bash +# Back up all sources to all destinations +gniza --cli backup --all + +# Back up a specific source +gniza --cli backup --target=mysite + +# Back up a specific source to a specific destination +gniza --cli backup --target=mysite --remote=backup-server + +# Back up multiple sources +gniza --cli backup --target=mysite,databases +``` + +### How Backup Works + +1. **Pre-hook** runs (if configured). Failure aborts the backup. +2. **Disk space check** on the destination. If usage exceeds the threshold (default 95%), the backup aborts. +3. **Source pull** (for remote sources): Files are pulled from the remote source to a local staging area. +4. **Transfer**: rsync transfers files to the destination using `--link-dest` to hardlink unchanged files from the previous snapshot. The snapshot is stored in a `.partial` directory during transfer. +5. **MySQL dump** (if enabled): Databases are dumped and included in the snapshot. +6. **Atomic rename**: The `.partial` directory is renamed to the final timestamp on success. +7. **Post-hook** runs (if configured). +8. **Retention** enforcement: Old snapshots beyond the retention count are deleted. +9. **Notification** sent (if configured). + +### Incremental Deduplication + +gniza uses rsync's `--link-dest` to create space-efficient incremental backups. + +- **First backup**: All files are transferred in full. This is the slowest backup. +- **Subsequent backups**: Only changed files are transferred. Unchanged files are hardlinked to the previous snapshot, sharing disk blocks. +- Each snapshot appears as a **complete directory tree** — fully browseable and restorable on its own. +- 10 snapshots of 50 GB with minor daily changes might use 55 GB total instead of 500 GB. +- Deleting a snapshot only frees space for files not referenced by other snapshots. + +### Atomic Snapshots + +During transfer, data is stored in a `.partial` directory. Only when the transfer completes successfully is it renamed to the final timestamp. If a backup is interrupted, no incomplete snapshot is left behind. + +### Disk Space Safety + +Before each backup, gniza checks the destination disk usage. If it equals or exceeds the configured threshold, the backup aborts. + +```ini +# In gniza.conf +DISK_USAGE_THRESHOLD=95 # Abort if destination >= 95% full (0 = disabled) +``` + +### Bandwidth Limiting + +Limit transfer speed globally or per destination: + +```ini +# Global (gniza.conf) +BWLIMIT=10000 # 10 MB/s + +# Per destination (remotes.d/.conf) +BWLIMIT=5000 # 5 MB/s — overrides global +``` + +Value is in KB/s. Set to `0` for unlimited. + +### Retry Logic + +SSH connections are automatically retried on failure with exponential backoff: + +```ini +# In gniza.conf +SSH_TIMEOUT=30 # Connection timeout in seconds +SSH_RETRIES=3 # Number of retry attempts (waits 10s, 20s, 30s) +``` + +Rsync partial transfers (exit codes 23/24) are also handled gracefully. + +### Concurrency + +Each source uses `flock`-based locking to prevent overlapping backups of the same source. Multiple different sources can run in parallel. + +--- + +## Restore + +### Restoring from CLI + +```bash +# Restore latest snapshot in-place +gniza --cli restore --target=mysite --remote=backup-server + +# Restore a specific snapshot +gniza --cli restore --target=mysite --remote=backup-server --snapshot=2026-03-07T020000 + +# Restore to a custom directory +gniza --cli restore --target=mysite --remote=backup-server --dest=/tmp/restore + +# Restore a single folder from a snapshot +gniza --cli restore --target=mysite --remote=backup-server --snapshot=2026-03-07T020000 --folder=/var/www + +# Skip MySQL restore +gniza --cli restore --target=mysite --remote=backup-server --skip-mysql +``` + +### Restore Behavior + +- **In-place restore**: Files are restored to their original locations, overwriting current files. +- **Custom directory restore**: Files are restored under the specified directory, preserving the original path structure. +- **MySQL restore**: If the snapshot contains MySQL dumps (`_mysql/` directory), they are automatically restored unless `--skip-mysql` is passed. +- **Single folder restore**: Only the specified folder is restored from the snapshot. + +### Restoring from TUI + +Navigate to Restore, select a source, destination, and snapshot, choose in-place or custom directory, and optionally toggle MySQL restore on/off. + +--- + +## Snapshots + +### Listing Snapshots + +```bash +# List all snapshots for all sources on the first destination +gniza --cli snapshots list + +# List snapshots for a specific source +gniza --cli snapshots list --target=mysite + +# List snapshots on a specific destination +gniza --cli snapshots list --remote=backup-server + +# Both +gniza --cli snapshots list --target=mysite --remote=backup-server +``` + +### Browsing Snapshot Contents + +```bash +gniza --cli snapshots browse --target=mysite --snapshot=2026-03-07T020000 +``` + +Lists all files in the snapshot. + +### Snapshot Structure + +``` +//targets//snapshots// +├── meta.json # Metadata: source, timestamp, duration, pinned flag +├── manifest.txt # File listing +├── summary # Backup summary +├── var/www/ # Backed-up directories (original structure) +├── etc/nginx/ +└── _mysql/ # MySQL dumps (if enabled) + ├── dbname.sql.gz # Gzip-compressed database dump + └── _grants.sql.gz # User grants and privileges +``` + +### Snapshot Metadata (meta.json) + +```json +{ + "target": "mysite", + "timestamp": "2026-03-07T020000", + "duration_seconds": 45, + "pinned": false +} +``` + +--- + +## Retention + +Retention controls how many snapshots are kept per source per destination. + +### Configuration Priority + +1. **Per-source** `TARGET_RETENTION` (highest priority) +2. **Per-destination** `RETENTION_COUNT` +3. **Global** `RETENTION_COUNT` in `gniza.conf` (default: 30) + +### Automatic Enforcement + +Retention runs automatically after each successful backup. The oldest snapshots beyond the retention count are deleted. + +### Manual Enforcement + +```bash +# Enforce retention for all sources on all destinations +gniza --cli retention --all + +# Enforce for a specific source +gniza --cli retention --target=mysite + +# Enforce on a specific destination +gniza --cli retention --remote=backup-server +``` + +### Snapshot Pinning + +Pinned snapshots are never deleted by retention enforcement. Pin a snapshot by setting `"pinned": true` in its `meta.json` file. + +--- + +## Scheduling + +gniza manages cron entries for automated backups. + +### Creating a Schedule + +**TUI**: Navigate to Schedules > Add. + +**Manual**: Create `/schedules.d/.conf`: + +```ini +SCHEDULE="daily" # hourly | daily | weekly | monthly | custom +SCHEDULE_TIME="02:00" # HH:MM +SCHEDULE_DAY="" # Day of week (0=Sun..6=Sat) or day of month (1-28) +SCHEDULE_CRON="" # Full cron expression (when SCHEDULE=custom) +SCHEDULE_ACTIVE="yes" # yes | no +TARGETS="" # Comma-separated source names (empty = all) +REMOTES="" # Comma-separated destination names (empty = all) +``` + +### Schedule Types + +| Type | SCHEDULE_TIME | SCHEDULE_DAY | Example | +|------|---------------|--------------|---------| +| `hourly` | `:MM` | - | Every hour at :30 | +| `daily` | `HH:MM` | - | Every day at 02:00 | +| `weekly` | `HH:MM` | `0`-`6` | Every Sunday at 03:00 | +| `monthly` | `HH:MM` | `1`-`28` | 1st of each month at 04:00 | +| `custom` | - | - | Full cron: `*/15 * * * *` | + +### Filtering + +Limit which sources and destinations a schedule applies to: + +```ini +TARGETS="mysite,databases" # Only back up these sources +REMOTES="backup-server" # Only to this destination +``` + +Leave empty to include all. + +### Managing Cron Entries + +```bash +# Install all active schedules into crontab +gniza --cli schedule install + +# View current gniza cron entries +gniza --cli schedule show + +# Remove all gniza cron entries +gniza --cli schedule remove +``` + +Cron entries are tagged with `# gniza4linux:` for clean management. Running `schedule install` replaces existing entries cleanly. + +### Cron Logs + +Scheduled backups log output to `/cron.log`. + +--- + +## MySQL Backup + +gniza can dump MySQL/MariaDB databases alongside file backups. Auto-detects `mysqldump` or `mariadb-dump`. + +### Enabling MySQL Backup + +In the source config: + +```ini +TARGET_MYSQL_ENABLED="yes" +TARGET_MYSQL_USER="backup_user" +TARGET_MYSQL_PASSWORD="secret" +TARGET_MYSQL_HOST="localhost" +TARGET_MYSQL_PORT="3306" +``` + +### Dump Modes + +**All databases** (default): +```ini +TARGET_MYSQL_MODE="all" +TARGET_MYSQL_EXCLUDE="test_db,staging_db" +``` + +Dumps every user database except system databases (`information_schema`, `performance_schema`, `sys`, `mysql`) and any in the exclude list. + +**Selected databases**: +```ini +TARGET_MYSQL_MODE="selected" +TARGET_MYSQL_DATABASES="app_db,user_db,analytics" +``` + +### Dump Options + +```ini +TARGET_MYSQL_EXTRA_OPTS="--single-transaction --routines --triggers" +``` + +Default options ensure consistent dumps for InnoDB tables and include stored procedures and triggers. + +### What Gets Dumped + +- Each database is dumped as `.sql.gz` (gzip compressed) +- User grants are dumped as `_grants.sql.gz` +- All dumps are stored in the `_mysql/` directory within the snapshot + +### MySQL Restore + +During restore, MySQL dumps from `_mysql/` are automatically restored. Use `--skip-mysql` to skip: + +```bash +gniza --cli restore --target=mysite --remote=backup-server --skip-mysql +``` + +In the TUI, toggle the "Restore MySQL databases" switch. + +--- + +## Notifications + +gniza sends email notifications on backup completion. + +### Configuration + +In `gniza.conf`: + +```ini +NOTIFY_ON="failure" # never | failure | always +NOTIFY_EMAIL="admin@example.com,ops@example.com" + +# SMTP settings (recommended) +SMTP_HOST="smtp.gmail.com" +SMTP_PORT="587" +SMTP_USER="alerts@example.com" +SMTP_PASSWORD="app-password" +SMTP_FROM="gniza@example.com" +SMTP_SECURITY="tls" # tls | ssl | none +``` + +### Notification Modes + +| Mode | When emails are sent | +|------|---------------------| +| `never` | No notifications | +| `failure` | Only when a backup fails (default) | +| `always` | After every backup run | + +### Email Content + +Notification emails include: +- Status: SUCCESS, PARTIAL FAILURE, or FAILURE +- Source count: total, succeeded, failed +- Duration +- List of failed sources (if any) +- Log file path +- Hostname and timestamp + +### Fallback + +If SMTP is not configured, gniza falls back to the system `mail` or `sendmail` command. + +--- + +## Web Dashboard + +Serve the full TUI in a browser with HTTP Basic Auth. + +### Setup + +```bash +# Install as a systemd service (auto-starts on boot) +gniza web install-service + +# Or start manually +gniza web start +``` + +Access at `http://:2323`. + +### Service Management + +```bash +gniza web install-service # Install and start systemd service +gniza web remove-service # Stop and remove service +gniza web status # Check service status +gniza web start # Start manually (foreground) +gniza web start --port=8080 # Custom port +gniza web start --host=127.0.0.1 # Bind to localhost only +``` + +### Authentication + +Credentials are configured in `gniza.conf`: + +```ini +WEB_USER="admin" +WEB_API_KEY="generated-during-install" +``` + +The API key is generated automatically during installation if you enable the web dashboard. You can change it manually. + +### Root vs User Mode + +- **Root**: Installs as a system service (`/etc/systemd/system/gniza-web.service`) +- **User**: Installs as a user service (`~/.config/systemd/user/gniza-web.service`) + +--- + +## Terminal UI + +Launch with `gniza` (no arguments). Requires Python 3 and Textual. + +### Screens + +| Screen | Description | +|--------|-------------| +| **Sources** | Create, edit, delete, and view backup sources | +| **Destinations** | Configure SSH, local, S3, or Google Drive destinations | +| **Backup** | Select sources and destinations, run backups | +| **Restore** | Browse snapshots, restore to original or custom location | +| **Running Tasks** | Monitor active jobs with live log output and progress | +| **Schedules** | Create and manage cron schedules | +| **Snapshots** | Browse stored snapshots | +| **Logs** | View backup history with pagination | +| **Settings** | Configure global options | + +### Features + +- **Setup Wizard**: Guided first-run configuration +- **Folder Browser**: Navigate local and remote directories when configuring sources +- **Remote Folder Browser**: Browse SSH destination directories +- **Connection Testing**: Test destination connectivity from the edit screen +- **Documentation Panel**: Inline help on wide screens, help modal on narrow ones +- **Responsive Layout**: Adapts to terminal width +- **Job Manager**: Run backups/restores in background with live log streaming + +### Keyboard Shortcuts + +| Key | Action | +|-----|--------| +| `Escape` | Go back / Cancel | +| `F1` | Toggle help panel | +| `Enter` | Select / Confirm | + +--- + +## CLI Reference + +### Global Options + +``` +--cli Force CLI mode (skip TUI) +--debug Enable debug logging +--config=FILE Override config file path +--help Show help +--version Show version +``` + +### Sources + +```bash +gniza --cli targets list # List all sources +gniza --cli targets add --name=NAME --folders=PATHS # Create a source +gniza --cli targets delete --name=NAME # Delete a source +gniza --cli targets show --name=NAME # Show source details +``` + +### Destinations + +```bash +gniza --cli remotes list # List all destinations +gniza --cli remotes add --name=NAME # Create a destination +gniza --cli remotes delete --name=NAME # Delete a destination +gniza --cli remotes show --name=NAME # Show destination details +gniza --cli remotes test --name=NAME # Test connectivity +gniza --cli remotes disk-info-short --name=NAME # Show disk usage +``` + +### Backup + +```bash +gniza --cli backup --all # Back up everything +gniza --cli backup --target=NAME # Back up one source +gniza --cli backup --target=NAME --remote=NAME # Source to specific destination +gniza --cli backup --target=a,b,c # Multiple sources +``` + +### Restore + +```bash +gniza --cli restore --target=NAME --remote=NAME --snapshot=TS +gniza --cli restore --target=NAME --remote=NAME --dest=/tmp/restore +gniza --cli restore --target=NAME --remote=NAME --folder=/var/www +gniza --cli restore --target=NAME --remote=NAME --skip-mysql +``` + +### Snapshots + +```bash +gniza --cli snapshots list # All snapshots +gniza --cli snapshots list --target=NAME # For one source +gniza --cli snapshots list --remote=NAME # On one destination +gniza --cli snapshots browse --target=NAME --snapshot=TS +``` + +### Retention + +```bash +gniza --cli retention --all # Enforce everywhere +gniza --cli retention --target=NAME # One source +gniza --cli retention --remote=NAME # One destination +``` + +### Scheduling + +```bash +gniza --cli schedule install # Install cron entries +gniza --cli schedule show # Show current entries +gniza --cli schedule remove # Remove all entries +``` + +### Logs + +```bash +gniza --cli logs # List log files +gniza --cli logs --last # Show latest log +gniza --cli logs --last --tail=50 # Last 50 lines +``` + +### Web Dashboard + +```bash +gniza web start # Start web server +gniza web start --port=8080 --host=0.0.0.0 # Custom port/host +gniza web install-service # Install systemd service +gniza web remove-service # Remove service +gniza web status # Check status +``` + +### System + +```bash +gniza --version # Show version +gniza uninstall # Uninstall gniza +``` + +--- + +## Global Settings + +All global settings are in `gniza.conf` in the config directory. + +### Backup Settings + +| Setting | Default | Description | +|---------|---------|-------------| +| `BACKUP_MODE` | `incremental` | Backup mode | +| `BWLIMIT` | `0` | Global bandwidth limit in KB/s | +| `RETENTION_COUNT` | `30` | Default snapshots to keep | +| `DISK_USAGE_THRESHOLD` | `95` | Abort if destination >= this % (0 = disabled) | +| `RSYNC_EXTRA_OPTS` | (empty) | Additional rsync flags | +| `WORK_DIR` | `/tmp` | Temp directory for staging and dumps | + +### SSH Settings + +| Setting | Default | Description | +|---------|---------|-------------| +| `SSH_TIMEOUT` | `30` | Connection timeout in seconds | +| `SSH_RETRIES` | `3` | Number of retry attempts | + +### Logging + +| Setting | Default | Description | +|---------|---------|-------------| +| `LOG_LEVEL` | `info` | `info` or `debug` | +| `LOG_RETAIN` | `90` | Days to keep log files | + +### Notifications + +| Setting | Default | Description | +|---------|---------|-------------| +| `NOTIFY_ON` | `failure` | `never`, `failure`, or `always` | +| `NOTIFY_EMAIL` | (empty) | Comma-separated recipient addresses | +| `SMTP_HOST` | (empty) | SMTP server hostname | +| `SMTP_PORT` | `587` | SMTP port | +| `SMTP_USER` | (empty) | SMTP username | +| `SMTP_PASSWORD` | (empty) | SMTP password | +| `SMTP_FROM` | (empty) | Sender address | +| `SMTP_SECURITY` | `tls` | `tls`, `ssl`, or `none` | + +### Web Dashboard + +| Setting | Default | Description | +|---------|---------|-------------| +| `WEB_USER` | `admin` | HTTP Basic Auth username | +| `WEB_API_KEY` | (empty) | HTTP Basic Auth password | +| `WEB_PORT` | `2323` | Dashboard port | +| `WEB_HOST` | `0.0.0.0` | Dashboard bind address | + +--- + +## Security + +### File Permissions + +- Config files are created with mode `600` (owner read/write only) +- Temp files (rclone configs, staging areas) use restrictive umask + +### Credential Handling + +- Passwords are never logged or displayed in output +- `targets show` and `remotes show` mask passwords with `****` +- MySQL passwords are passed via `MYSQL_PWD` environment variable +- SSH passwords are passed via `sshpass`, not command-line arguments + +### Authentication Methods + +| Destination/Source | Methods | +|-------------------|---------| +| SSH | Key-based (RSA/ECDSA/Ed25519), password (via sshpass) | +| S3 | Access Key ID + Secret Access Key | +| Google Drive | Service account JSON file | +| Web dashboard | HTTP Basic Auth | + +--- + +## Troubleshooting + +### Checking Logs + +```bash +# View the latest backup log +gniza --cli logs --last + +# View the last 100 lines +gniza --cli logs --last --tail=100 + +# Enable debug logging for the next run +gniza --cli --debug backup --all +``` + +### Common Issues + +**"No destinations configured"** +Create at least one destination in `/remotes.d/`. + +**SSH connection failures** +- Test with: `gniza --cli remotes test --name=` +- Check that the SSH key exists and has correct permissions (600) +- Verify the remote host is reachable: `ssh -p PORT user@host` +- If using password auth, ensure `sshpass` is installed + +**Backup aborted due to disk space** +The destination disk usage exceeds the threshold. Free space or adjust `DISK_USAGE_THRESHOLD` in `gniza.conf`. + +**Cron not running** +- Check that cron entries are installed: `gniza --cli schedule show` +- Verify the cron daemon is running: `systemctl status cron` +- Check cron logs: `/cron.log` +- Re-install entries: `gniza --cli schedule install` + +**rclone required** +S3 and Google Drive sources/destinations require rclone. Install from https://rclone.org/install/. + +**TUI not launching** +Ensure Python 3 and Textual are installed: `python3 -c "import textual"`. The installer normally handles this. + +**Web dashboard not accessible** +- Check service status: `gniza web status` +- Verify the port is open: `ss -tlnp | grep 2323` +- Check credentials in `gniza.conf` (`WEB_USER` and `WEB_API_KEY`) diff --git a/tui/gniza.tcss b/tui/gniza.tcss index 8668c57..bf4c6f4 100644 --- a/tui/gniza.tcss +++ b/tui/gniza.tcss @@ -395,6 +395,18 @@ Switch { margin: 0 1; } +.sched-switch-row { + height: 3; + align: left middle; + margin: 0; +} + +.sched-switch-label { + width: 1fr; + height: 3; + content-align: left middle; +} + .section-label { text-style: bold; color: #00cc00; diff --git a/tui/screens/schedule_edit.py b/tui/screens/schedule_edit.py index 574190c..cf32faa 100644 --- a/tui/screens/schedule_edit.py +++ b/tui/screens/schedule_edit.py @@ -1,7 +1,7 @@ import re from textual.app import ComposeResult from textual.screen import Screen -from textual.widgets import Header, Footer, Static, Button, Input, Select, SelectionList +from textual.widgets import Header, Footer, Static, Button, Input, Select, SelectionList, Switch from tui.widgets.header import GnizaHeader as Header # noqa: F811 from textual.containers import Vertical, Horizontal @@ -105,27 +105,29 @@ class ScheduleEditScreen(Screen): placeholder="0 2 * * *", classes="sched-cron-field", ) - yield Static("Sources (empty=all):") - yield SelectionList[str]( - *self._build_target_choices(), - id="sched-targets", - ) - yield Static("Destinations (empty=all):") - yield SelectionList[str]( - *self._build_remote_choices(), - id="sched-remotes", - ) + yield Static("Sources (off=all):") + for tname in list_conf_dir("targets.d"): + with Horizontal(classes="sched-switch-row"): + yield Static(tname, classes="sched-switch-label") + yield Switch(value=False, id=f"sched-src-{tname}") + yield Static("Destinations (off=all):") + for rname in list_conf_dir("remotes.d"): + with Horizontal(classes="sched-switch-row"): + yield Static(rname, classes="sched-switch-label") + yield Switch(value=False, id=f"sched-dst-{rname}") with Horizontal(id="sched-edit-buttons"): yield Button("Save", variant="primary", id="btn-save") yield Button("Cancel", id="btn-cancel") yield DocsPanel.for_screen("schedule-edit") yield Footer() - def _build_target_choices(self) -> list[tuple[str, str]]: - return [(name, name) for name in list_conf_dir("targets.d")] - - def _build_remote_choices(self) -> list[tuple[str, str]]: - return [(name, name) for name in list_conf_dir("remotes.d")] + def _get_selected_switches(self, prefix: str) -> list[str]: + """Return names of enabled switches matching the ID prefix.""" + selected = [] + for sw in self.query(Switch): + if sw.id and sw.id.startswith(prefix) and sw.value: + selected.append(sw.id[len(prefix):]) + return selected def on_mount(self) -> None: self._update_type_visibility() @@ -144,22 +146,22 @@ class ScheduleEditScreen(Screen): opt = days_list.get_option_at_index(idx) if opt.value in day_vals: days_list.select(opt.value) - # Targets + # Sources if sched.targets: - target_vals = set(sched.targets.split(",")) - tlist = self.query_one("#sched-targets", SelectionList) - for idx in range(tlist.option_count): - opt = tlist.get_option_at_index(idx) - if opt.value in target_vals: - tlist.select(opt.value) - # Remotes + for tname in sched.targets.split(","): + tname = tname.strip() + try: + self.query_one(f"#sched-src-{tname}", Switch).value = True + except Exception: + pass + # Destinations if sched.remotes: - remote_vals = set(sched.remotes.split(",")) - rlist = self.query_one("#sched-remotes", SelectionList) - for idx in range(rlist.option_count): - opt = rlist.get_option_at_index(idx) - if opt.value in remote_vals: - rlist.select(opt.value) + for rname in sched.remotes.split(","): + rname = rname.strip() + try: + self.query_one(f"#sched-dst-{rname}", Switch).value = True + except Exception: + pass def on_select_changed(self, event: Select.Changed) -> None: if event.select.id == "sched-type": @@ -226,8 +228,8 @@ class ScheduleEditScreen(Screen): time=self.query_one("#sched-time", Input).value.strip() or "02:00", day=day_val, cron=self.query_one("#sched-cron", Input).value.strip(), - targets=",".join(self.query_one("#sched-targets", SelectionList).selected), - remotes=",".join(self.query_one("#sched-remotes", SelectionList).selected), + targets=",".join(self._get_selected_switches("sched-src-")), + remotes=",".join(self._get_selected_switches("sched-dst-")), ) conf = CONFIG_DIR / "schedules.d" / f"{name}.conf" write_conf(conf, sched.to_conf())