# gniza4cp

cPanel Backup, Restore & Disaster Recovery tool.

Uses `pkgacct --nocompress --skiphomedir` for account backups, gzips SQL files individually, and transfers everything (including homedirs) to remote destinations with incremental snapshots. Supports three remote types: **SSH** (rsync with hardlink-based `--link-dest`), **Amazon S3** / S3-compatible (via rclone), and **Google Drive** (via rclone). Supports multiple remote destinations with independent schedules and retention policies.

## Repository

| | URL |
|---|-----|
| **Git (SSH)** | `gitea:shukivaknin/gniza4cp.git` (uses `Host gitea` from `~/.ssh/config`) |
| **Git (HTTPS)** | `https://git.linux-hosting.co.il/shukivaknin/gniza4cp.git` |
| **Web UI** | https://git.linux-hosting.co.il/shukivaknin/gniza4cp/ |

## Installation

From a clone:

```bash
git clone https://git.linux-hosting.co.il/shukivaknin/gniza4cp.git
cd gniza4cp
sudo bash scripts/install.sh
```

To uninstall:

```bash
sudo bash /usr/local/gniza4cp/uninstall.sh   # from installed copy
# or
sudo bash scripts/uninstall.sh            # from repo clone
```

The uninstall script removes the CLI, symlink, cron entries, and WHM plugin. Config (`/etc/gniza4cp/`) and logs (`/var/log/gniza4cp/`) are preserved — remove manually if desired.

## Quick Start

```bash
# Configure via WHM → GNIZA4CP Backup Manager (setup wizard)
# Or copy example configs manually:
sudo cp /etc/gniza4cp/gniza4cp.conf.example /etc/gniza4cp/gniza4cp.conf
sudo cp /etc/gniza4cp/remote.conf.example /etc/gniza4cp/remotes.d/nas.conf

# Test backup (dry run)
sudo gniza4cp backup --dry-run

# Run backup
sudo gniza4cp backup

# Back up to specific remotes
sudo gniza4cp backup --remote=nas,offsite
```

## Commands

```
gniza4cp backup    [--account=NAME] [--remote=NAME[,NAME2]] [--skip-suspended] [--dry-run]
gniza4cp restore   account <name> --remote=NAME [--timestamp=TS] [--force]
gniza4cp restore   files <name> --remote=NAME [--path=subpath] [--timestamp=TS]
gniza4cp restore   database <name> <dbname> --remote=NAME [--timestamp=TS]
gniza4cp restore   mailbox <name> <email@domain> --remote=NAME [--timestamp=TS]
gniza4cp restore   server --remote=NAME [--timestamp=TS]
gniza4cp list      [--account=NAME] [--remote=NAME]
gniza4cp verify    [--account=NAME] [--remote=NAME]
gniza4cp status
gniza4cp remote    list
gniza4cp remote    delete <name>
gniza4cp schedule  add <name>
gniza4cp schedule  delete <name>
gniza4cp schedule  list
gniza4cp schedule  install
gniza4cp schedule  show
gniza4cp schedule  remove
gniza4cp version
gniza4cp help
```

### Global Options

| Option | Description |
|--------|-------------|
| `--config=PATH` | Alternate config file (default: `/etc/gniza4cp/gniza4cp.conf`) |
| `--remote=NAME[,NAME2]` | Target specific remote(s) from `/etc/gniza4cp/remotes.d/` (comma-separated) |
| `--debug` | Enable debug logging |

## Configuration

### Main Config

**File:** `/etc/gniza4cp/gniza4cp.conf`

Controls local settings (accounts, logging, notifications). Remote destinations are configured in `/etc/gniza4cp/remotes.d/`.

```bash
# Local Settings
TEMP_DIR="/usr/local/gniza4cp/workdir"  # Working dir for pkgacct output
INCLUDE_ACCOUNTS=""               # Comma-separated, empty = all
EXCLUDE_ACCOUNTS="nobody"         # Comma-separated exclusions

# Logging
LOG_DIR="/var/log/gniza4cp"
LOG_LEVEL="info"                  # debug, info, warn, error
LOG_RETAIN=90                     # Days to keep log files

# Notifications
NOTIFY_EMAIL=""                   # Email for notifications
NOTIFY_ON="failure"               # always, failure, never

# Advanced
LOCK_FILE="/var/run/gniza4cp.lock"
SSH_TIMEOUT=30
SSH_RETRIES=3
RSYNC_EXTRA_OPTS=""
```

See `etc/gniza4cp.conf.example` for the full template.

### Remote Destinations

Back up to one or more destinations with independent retention policies and bandwidth limits. Supports SSH, Amazon S3 (and S3-compatible services like MinIO, Wasabi, Backblaze B2), and Google Drive. Remote destinations are configured as individual files in `/etc/gniza4cp/remotes.d/`.

#### Setup

```bash
# Configure via WHM → Remotes, or copy the template manually
sudo cp /etc/gniza4cp/remote.conf.example /etc/gniza4cp/remotes.d/nas.conf
sudo vi /etc/gniza4cp/remotes.d/nas.conf

# List configured remotes
sudo gniza4cp remote list

# Delete a remote
sudo gniza4cp remote delete nas
```

#### Remote Config Format

Each file in `/etc/gniza4cp/remotes.d/<name>.conf`:

```bash
# Remote type: "ssh" (default), "s3", or "gdrive"
REMOTE_TYPE="ssh"

# ── SSH Remote ──────────────────────────────────
REMOTE_HOST="192.168.1.100"       # Required (SSH only)
REMOTE_PORT=22
REMOTE_USER="root"
REMOTE_AUTH_METHOD="key"          # "key" or "password"
REMOTE_KEY="/root/.ssh/id_rsa"    # Required for key auth
REMOTE_PASSWORD=""                # Required for password auth (needs sshpass)

# ── S3 Remote ───────────────────────────────────
S3_ACCESS_KEY_ID=""               # Required (S3 only)
S3_SECRET_ACCESS_KEY=""           # Required (S3 only)
S3_REGION="us-east-1"
S3_ENDPOINT=""                    # For S3-compatible services
S3_BUCKET=""                      # Required (S3 only)

# ── Google Drive Remote ─────────────────────────
GDRIVE_SERVICE_ACCOUNT_FILE=""    # Required (GDrive only)
GDRIVE_ROOT_FOLDER_ID=""          # Optional

# ── Common ──────────────────────────────────────
REMOTE_BASE="/backups"
BWLIMIT=0
RETENTION_COUNT=30
RSYNC_EXTRA_OPTS=""               # SSH only
```

S3 and Google Drive remotes require `rclone` installed on the server.

See `etc/remote.conf.example` for the full template.

#### Usage

Without `--remote`, backup/list/verify operate on **all** configured remotes. Restore always requires `--remote` to specify the source.

```bash
# Back up to all remotes
sudo gniza4cp backup

# Back up to specific remote(s)
sudo gniza4cp backup --remote=nas
sudo gniza4cp backup --remote=nas,offsite

# List snapshots on a specific remote
sudo gniza4cp list --remote=offsite

# Restore requires explicit remote
sudo gniza4cp restore account johndoe --remote=nas
```

### Schedules

Schedules are **decoupled from remotes**. Each schedule lives in `/etc/gniza4cp/schedules.d/<name>.conf` and defines when backups run and which remotes to target. This allows multiple schedules targeting different sets of remotes.

#### Schedule Config Format

```bash
SCHEDULE="daily"                  # hourly, daily, weekly, monthly, custom
SCHEDULE_TIME="02:00"             # HH:MM (24-hour)
SCHEDULE_DAY=""                   # hours between backups (1-23) for hourly
                                  # day-of-week (0=Sun..6=Sat) for weekly
                                  # day-of-month (1-28) for monthly
SCHEDULE_CRON=""                  # Full cron expression for SCHEDULE=custom
REMOTES=""                        # Comma-separated remote names (empty = all)
SYSBACKUP=""                      # "yes" to include system backup
SKIP_SUSPENDED=""                 # "yes" to skip cPanel suspended accounts
```

#### Managing Schedules

```bash
# Interactive schedule creation
sudo gniza4cp schedule add nightly

# List configured schedules
sudo gniza4cp schedule list

# Delete a schedule
sudo gniza4cp schedule delete nightly

# Install all schedules to crontab
sudo gniza4cp schedule install

# Show current gniza4cp cron entries
sudo gniza4cp schedule show

# Remove all gniza4cp cron entries
sudo gniza4cp schedule remove
```

#### Schedule Types

| Type | SCHEDULE_DAY | Cron Pattern | Example |
|------|-------------|--------------|---------|
| `hourly` | Hours interval (1-23) | `<min> */<interval> * * *` | Every 2 hours at :15 → `15 */2 * * *` |
| `daily` | — | `<min> <hour> * * *` | Daily at 02:00 → `0 2 * * *` |
| `weekly` | Day-of-week (0-6) | `<min> <hour> * * <dow>` | Sundays at 03:00 → `0 3 * * 0` |
| `monthly` | Day-of-month (1-28) | `<min> <hour> <dom> * *` | 1st at 02:00 → `0 2 1 * *` |
| `custom` | — | `SCHEDULE_CRON` (5-field) | `*/30 * * * *` |

Each schedule gets a tagged cron entry for clean install/remove:

```
# gniza4cp:nightly
0 2 * * * /usr/local/bin/gniza4cp backup --remote=nas,offsite >> /var/log/gniza4cp/cron-nightly.log 2>&1
```

## Remote Directory Structure

### SSH Remotes

```
$REMOTE_BASE/<hostname>/accounts/<user>/
├── snapshots/
│   ├── 2026-03-03T020000/          # Completed snapshot
│   │   ├── mysql/                  # SQL dumps (*.sql.gz)
│   │   ├── mysql.sql               # Database grants
│   │   ├── cp/                     # cPanel metadata
│   │   ├── ...                     # Other pkgacct files
│   │   └── homedir/                # Full home directory
│   ├── 2026-03-02T020000/          # Previous (hardlinked unchanged files)
│   └── 2026-03-01T020000.partial/  # Incomplete (failed/in-progress)
└── latest -> snapshots/2026-03-03T020000
```

### Cloud Remotes (S3/GDrive)

```
$REMOTE_BASE/<hostname>/accounts/<user>/snapshots/
├── 2026-03-03T020000/              # Completed snapshot
│   ├── .complete                   # Completion marker (empty file)
│   ├── mysql/                      # SQL dumps (*.sql.gz)
│   ├── ...                         # Other pkgacct files
│   └── homedir/                    # Full home directory
├── 2026-03-02T020000/              # Previous snapshot
│   └── .complete
├── 2026-03-01T020000/              # Partial (no .complete → purged on next run)
└── latest.txt                      # Contains timestamp of newest snapshot
```

Cloud storage has no atomic rename or symlinks, so `.complete` markers and `latest.txt` replace the `.partial` suffix and `latest` symlink used by SSH remotes.

pkgacct output is stored directly in the snapshot root (no wrapper subdirectory). The `homedir/` sits alongside it.

## Ownership & Permissions

All rsync operations use `--rsync-path="rsync --fake-super"` to preserve file ownership and permissions even when the remote SSH user is not root. The real uid/gid/permissions are stored as extended attributes (`user.rsync.%stat`) on the remote filesystem. On restore, the same flag reads the xattrs back, allowing the local root process to set the correct ownership.

## Backup Flow

1. Load main config (TEMP_DIR, LOG_DIR, accounts, notifications)
2. Resolve target remotes (`--remote=NAME[,NAME2]` or all from `remotes.d/`)
3. Test connectivity to all targets upfront (SSH or rclone by type)
4. For each account:
   - `pkgacct` ONCE
   - Gzip SQL ONCE
   - For each remote:
     - `load_remote(name)` — swaps REMOTE_*/S3_*/GDRIVE_* globals
     - Clean partials, get previous snapshot
     - Transfer pkgacct content (rsync for SSH, rclone for cloud)
     - Transfer homedir (rsync for SSH, rclone for cloud)
     - Finalize snapshot (SSH: rename `.partial` + symlink; cloud: `.complete` marker + `latest.txt`)
     - Enforce retention
   - Cleanup local temp
5. Summary + notification

If one remote fails for an account, other remotes still receive the backup.

## Restore Workflows

| Workflow | Command | Description |
|----------|---------|-------------|
| **Full account** | `restore account <user>` | Downloads pkgacct data, decompresses SQL, runs `/scripts/restorepkg`, rsyncs homedir, fixes ownership |
| **Selective files** | `restore files <user> --path=...` | Rsyncs specific path from remote homedir backup |
| **Single database** | `restore database <user> <dbname>` | Downloads SQL dump + grants, imports via `mysql` |
| **Mailbox** | `restore mailbox <user> <email@domain>` | Restores individual email account from `mail/<domain>/<mailbox>/` |
| **Full server** | `restore server` | Restores all accounts found on the remote |

All restore commands require `--remote=NAME` to specify the source.

## Error Handling

- Single account failures don't abort the run
- Exit codes: `0` success, `1` fatal, `2` locked, `5` partial failure
- `.partial` directories mark incomplete snapshots
- rsync retries with exponential backoff (configurable via `SSH_RETRIES`)
- `flock`-based concurrency control prevents parallel runs
- In multi-remote mode, failure on one remote doesn't block others

## File Layout

```
/usr/local/gniza4cp/              # Install directory
├── bin/gniza4cp                  # CLI entrypoint
├── lib/                       # Shell libraries
│   ├── constants.sh           # Version, exit codes, colors, defaults
│   ├── utils.sh               # die(), require_root(), timestamp, human_*
│   ├── logging.sh             # Per-run log files, log_info/warn/error/debug
│   ├── config.sh              # Config loading and validation
│   ├── locking.sh             # flock-based concurrency control
│   ├── ssh.sh                 # SSH connectivity, remote_exec, rsync SSH cmd
│   ├── rclone.sh              # Rclone transport layer for S3/GDrive
│   ├── accounts.sh            # cPanel account discovery and filtering
│   ├── pkgacct.sh             # pkgacct execution, SQL gzip, temp cleanup
│   ├── snapshot.sh            # Timestamp naming, list/resolve snapshots
│   ├── transfer.sh            # rsync --link-dest transfers, finalize
│   ├── retention.sh           # Prune old snapshots beyond RETENTION_COUNT
│   ├── verify.sh              # Remote backup integrity checks
│   ├── notify.sh              # Email notifications
│   ├── restore.sh             # Full account, files, database, mailbox, server restore
│   ├── remotes.sh             # Remote discovery and context switching
│   └── schedule.sh            # Cron management for decoupled schedules
└── etc/
    ├── gniza4cp.conf.example     # Main config template
    ├── remote.conf.example    # Remote destination template
    └── schedule.conf.example  # Schedule template

/etc/gniza4cp/                    # Runtime configuration
├── gniza4cp.conf                 # Main config
├── remotes.d/                 # Remote destination configs
│   ├── nas.conf
│   └── offsite.conf
└── schedules.d/               # Schedule configs
    ├── nightly.conf
    └── weekly-offsite.conf

/var/log/gniza4cp/                # Log files
├── gniza4cp-20260303-020000.log  # Per-run logs
├── cron-nightly.log           # Per-schedule cron output
└── cron-weekly-offsite.log
```

## WHM Plugin

gniza4cp includes a WHM plugin for managing backups through the cPanel/WHM web interface. All pages use **Tailwind CSS v4** with **DaisyUI v5** for styling.

### Installation

The plugin is installed automatically by `scripts/install.sh`. It registers with WHM at **Plugins > gniza4cp Backup Manager**.

Plugin files are deployed to `/usr/local/cpanel/whostmgr/docroot/cgi/gniza4cp-whm/`.

### Setup Wizard

When gniza4cp is not yet configured (no remotes in `/etc/gniza4cp/remotes.d/`), the dashboard automatically redirects to a **3-step setup wizard**:

1. **SSH Key** — Detects existing keys in `/root/.ssh/` (`id_ed25519`, `id_rsa`, `id_ecdsa`, `id_dsa`). Lets you select one or enter a custom path. Shows `ssh-keygen` and `ssh-copy-id` commands for creating new keys.

2. **Remote Destination** — Configure the first remote: name, type (SSH/S3/GDrive), connection details, base path, bandwidth limit, and retention count. Tests the connection before saving. Creates a config file in `/etc/gniza4cp/remotes.d/`.

3. **Schedule** — Optionally set a backup schedule (hourly/daily/weekly/monthly/custom) for the new remote. Installs the cron entry automatically. Can be skipped.

The wizard is also accessible anytime from the dashboard quick links ("Run Setup Wizard").

### Pages

| Page | URL | Description |
|------|-----|-------------|
| Dashboard | `index.cgi` | Overview, remote listing, cron status, quick links |
| Remotes | `remotes.cgi` | Add/edit/delete remote destinations (SSH/S3/GDrive) with connection testing |
| Schedules | `schedules.cgi` | Add/edit/delete schedules, per-schedule cron toggle |
| Restore | `restore.cgi` | Restore workflow: select account, remote, snapshot, then restore type (full/files/database/mailbox) |
| Settings | `settings.cgi` | Edit main config (`/etc/gniza4cp/gniza4cp.conf`) |
| Setup Wizard | `setup.cgi` | Guided initial configuration (3 steps) |

### Plugin File Layout

```
whm/
├── gniza4cp-whm.conf                    # WHM AppConfig registration
└── gniza4cp-whm/
    ├── index.cgi                     # Dashboard
    ├── setup.cgi                     # Setup wizard (3 steps)
    ├── settings.cgi                  # Main config editor
    ├── remotes.cgi                   # Remote CRUD (SSH/S3/GDrive)
    ├── schedules.cgi                 # Schedule CRUD + cron toggles
    ├── restore.cgi                   # Restore workflow (account → remote → snapshot → type)
    ├── assets/
    │   ├── gniza4cp-whm.css             # Built Tailwind/DaisyUI CSS (committed)
    │   └── src/
    │       ├── input.css             # Tailwind v4 entry point
    │       ├── safelist.html         # Class safelist for Tailwind scanner
    │       └── package.json          # Build toolchain
    └── lib/Gniza4cpWHM/
        ├── Config.pm                 # Config parser/writer (pure Perl)
        ├── Validator.pm              # Input validation
        ├── Cron.pm                   # Cron read + per-schedule install/remove
        ├── Runner.pm                 # Pattern-based safe CLI command runner
        └── UI.pm                     # Navigation, flash, CSRF, HTML helpers, CSS delivery
```

## Running Tests

```bash
bash tests/test_utils.sh
```

## License

MIT
