Rename product from gniza to gniza4cp across entire codebase

- CLI binary: bin/gniza -> bin/gniza4cp
- Install path: /usr/local/gniza4cp/
- Config path: /etc/gniza4cp/
- Log path: /var/log/gniza4cp/
- WHM plugin: gniza4cp-whm/
- cPanel plugin: cpanel/gniza4cp/
- AdminBin: Gniza4cp::Restore
- Perl modules: Gniza4cpWHM::*, Gniza4cpCPanel::*
- DaisyUI theme: gniza4cp
- All internal references, branding, paths updated
- Git remote updated to gniza4cp repo
This commit is contained in:
shuki
2026-03-05 21:03:30 +02:00
parent f0171a9eb4
commit a162536585
866 changed files with 90907 additions and 1339 deletions

218
CLAUDE.md
View File

@@ -1,19 +1,19 @@
# agents.md — gniza Development Guide
# agents.md — gniza4cp Development Guide
> Reference for AI coding agents working on gniza. Describes architecture, conventions, and key patterns.
> Reference for AI coding agents working on gniza4cp. Describes architecture, conventions, and key patterns.
## Project Overview
gniza is a Bash CLI tool for cPanel server backup and disaster recovery. It runs `pkgacct` to export accounts, gzips SQL files, and transfers everything to one or more remote destinations using hardlink-based incremental snapshots. Supports three remote types: **SSH** (rsync with `--link-dest`), **Amazon S3** / S3-compatible (via rclone), and **Google Drive** (via rclone).
gniza4cp is a Bash CLI tool for cPanel server backup and disaster recovery. It runs `pkgacct` to export accounts, gzips SQL files, and transfers everything to one or more remote destinations using hardlink-based incremental snapshots. Supports three remote types: **SSH** (rsync with `--link-dest`), **Amazon S3** / S3-compatible (via rclone), and **Google Drive** (via rclone).
**Language:** Bash (bash 4+, `set -euo pipefail`)
**Target environment:** CentOS/AlmaLinux cPanel servers, running as root
**Install path:** `/usr/local/gniza/` with symlink at `/usr/local/bin/gniza`
**Install path:** `/usr/local/gniza4cp/` with symlink at `/usr/local/bin/gniza4cp`
## Repository Structure
```
bin/gniza # CLI entrypoint — command routing, argument parsing
bin/gniza4cp # CLI entrypoint — command routing, argument parsing
lib/
├── constants.sh # Version, exit codes, color codes, default values
├── utils.sh # die(), require_root(), timestamp(), human_size/duration(), validate_timestamp/account_name()
@@ -33,17 +33,17 @@ lib/
├── remotes.sh # Multi-remote: list_remotes(), load_remote(), get_target_remotes()
└── schedule.sh # Cron: decoupled schedules from schedules.d/
etc/
├── gniza.conf.example # Main config template
├── gniza4cp.conf.example # Main config template
├── remote.conf.example # Remote destination config template
└── schedule.conf.example # Schedule config template
scripts/
├── install.sh # Install to /usr/local/gniza, create dirs/symlinks, WHM + cPanel plugins
├── install.sh # Install to /usr/local/gniza4cp, create dirs/symlinks, WHM + cPanel plugins
└── uninstall.sh # Remove install dir, symlink, cron entries, WHM + cPanel plugins
tests/
└── test_utils.sh # Unit tests for utils.sh, accounts.sh, config.sh
whm/
├── gniza-whm.conf # WHM AppConfig registration
└── gniza-whm/
├── gniza4cp-whm.conf # WHM AppConfig registration
└── gniza4cp-whm/
├── index.cgi # Dashboard — overview, quick links, auto-redirect if unconfigured
├── setup.cgi # 3-step setup wizard (SSH key → remote → schedule)
├── settings.cgi # Main config editor (local settings only)
@@ -51,29 +51,29 @@ whm/
├── schedules.cgi # Schedule CRUD — add/edit/delete with remote checkboxes
├── restore.cgi # Restore workflow — 4-step form (account → snapshot → confirm → execute)
├── assets/
│ ├── gniza-whm.css # Built Tailwind/DaisyUI CSS (committed, ~58KB)
│ ├── gniza-logo.svg # SVG logo (embedded as data URI in page header)
│ ├── gniza4cp-whm.css # Built Tailwind/DaisyUI CSS (committed, ~58KB)
│ ├── gniza4cp-logo.svg # SVG logo (embedded as data URI in page header)
│ └── src/
│ ├── input.css # Tailwind v4 entry point with DaisyUI plugin
│ ├── safelist.html # Class safelist for Tailwind content scanner
│ └── package.json # Build toolchain (tailwindcss + daisyui)
└── lib/GnizaWHM/
└── lib/Gniza4cpWHM/
├── Config.pm # Pure Perl config parser/writer (KEY="value" files)
├── Validator.pm # Input validation (mirrors lib/config.sh)
├── Cron.pm # Cron read + allowlisted gniza schedule commands
├── Cron.pm # Cron read + allowlisted gniza4cp schedule commands
├── Runner.pm # Pattern-based safe CLI command runner for WHM
└── UI.pm # Nav, flash, CSRF, HTML escaping, CSS delivery
cpanel/
├── gniza/
├── gniza4cp/
│ ├── index.live.cgi # Category grid — 8 restore type cards
│ ├── restore.live.cgi # Multi-step restore workflow (4 steps)
│ ├── install.json # cPanel plugin registration (Files section)
│ ├── assets/
│ │ ├── gniza-whm.css # Built CSS (copy of WHM CSS)
│ │ └── gniza-logo.svg # Logo (copy of WHM logo)
│ └── lib/GnizaCPanel/
│ │ ├── gniza4cp-whm.css # Built CSS (copy of WHM CSS)
│ │ └── gniza4cp-logo.svg # Logo (copy of WHM logo)
│ └── lib/Gniza4cpCPanel/
│ └── UI.pm # Page wrapper, CSRF, flash, CSS delivery
└── admin/Gniza/
└── admin/Gniza4cp/
├── Restore # AdminBin module (runs as root, privilege escalation)
└── Restore.conf # AdminBin config (mode=full)
```
@@ -87,7 +87,7 @@ All library functions (`ssh.sh`, `rclone.sh`, `transfer.sh`, `snapshot.sh`, `ret
Rather than passing remote context through function arguments, `remotes.sh` provides:
- `_save_remote_globals()` — snapshot current globals
- `load_remote(name)` — source `/etc/gniza/remotes.d/<name>.conf`, overriding REMOTE_* globals
- `load_remote(name)` — source `/etc/gniza4cp/remotes.d/<name>.conf`, overriding REMOTE_* globals
- `_restore_remote_globals()` — restore saved snapshot
This keeps the change set minimal — no existing function signatures needed modification.
@@ -131,16 +131,16 @@ cmd_backup()
### Command Routing
`bin/gniza` main() parses the first arg and routes to `cmd_*()` functions. Each command handles its own `--config`, `--remote`, `--account` flags via `get_opt()` and `has_flag()`.
`bin/gniza4cp` main() parses the first arg and routes to `cmd_*()` functions. Each command handles its own `--config`, `--remote`, `--account` flags via `get_opt()` and `has_flag()`.
Commands: `backup`, `restore`, `list`, `verify`, `status`, `remote`, `schedule`, `version`, `help`
### Config Hierarchy
1. `lib/constants.sh``DEFAULT_*` readonly values
2. `/etc/gniza/gniza.conf` — main config: local settings only (accounts, logging, notifications)
3. `/etc/gniza/remotes.d/<name>.conf` — per-remote config (REMOTE_*, retention, transfer)
4. `/etc/gniza/schedules.d/<name>.conf` — per-schedule config (timing, target remotes)
2. `/etc/gniza4cp/gniza4cp.conf` — main config: local settings only (accounts, logging, notifications)
3. `/etc/gniza4cp/remotes.d/<name>.conf` — per-remote config (REMOTE_*, retention, transfer)
4. `/etc/gniza4cp/schedules.d/<name>.conf` — per-schedule config (timing, target remotes)
5. CLI flags (`--debug`, `--config=PATH`)
### Snapshot Layout
@@ -171,13 +171,13 @@ Commands: `backup`, `restore`, `list`, `verify`, `status`, `remote`, `schedule`,
### Decoupled Schedules
Schedules are independent from remotes. Each schedule lives in `/etc/gniza/schedules.d/<name>.conf` and defines when backups run and which remotes to target. This allows multiple schedules targeting different sets of remotes.
Schedules are independent 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.
Cron entries are tagged with `# gniza:<name>` comment lines. `install_schedules()` strips old tagged lines and appends new ones. Format:
Cron entries are tagged with `# gniza4cp:<name>` comment lines. `install_schedules()` strips old tagged lines and appends new ones. Format:
```
# gniza:nightly
0 2 * * * /usr/local/bin/gniza backup --remote=nas,offsite >> /var/log/gniza/cron-nightly.log 2>&1
# gniza4cp:nightly
0 2 * * * /usr/local/bin/gniza4cp backup --remote=nas,offsite >> /var/log/gniza4cp/cron-nightly.log 2>&1
```
### Comma-Separated Remote Targeting
@@ -188,7 +188,7 @@ Cron entries are tagged with `# gniza:<name>` comment lines. `install_schedules(
Allows cPanel account owners to restore their own data (files, databases, email, etc.) without WHM admin access.
**Privilege escalation:** Uses cPanel's AdminBin framework. CGIs run as the logged-in cPanel user; the AdminBin module (`cpanel/admin/Gniza/Restore`) runs as root. The account parameter is always forced to `$ENV{'REMOTE_USER'}` (cPanel-authenticated), never from user input.
**Privilege escalation:** Uses cPanel's AdminBin framework. CGIs run as the logged-in cPanel user; the AdminBin module (`cpanel/admin/Gniza4cp/Restore`) runs as root. The account parameter is always forced to `$ENV{'REMOTE_USER'}` (cPanel-authenticated), never from user input.
**CGI file naming:** cPanel Jupiter theme uses `.live.cgi` extension for CGI files (e.g., `index.live.cgi`, `restore.live.cgi`).
@@ -196,18 +196,18 @@ Allows cPanel account owners to restore their own data (files, databases, email,
- Account isolation: AdminBin forces the authenticated username — users can only restore their own data
- No `--terminate`: AdminBin never passes the terminate flag, preventing destructive full restores
- Remote filtering: `USER_RESTORE_REMOTES` config controls which remotes users can access (`"all"`, comma-separated names, or empty to disable)
- Strict regex validation on all arguments (mirrors `GnizaWHM::Runner` patterns)
- Strict regex validation on all arguments (mirrors `Gniza4cpWHM::Runner` patterns)
- Path traversal prevention: path regex uses negative lookahead to reject `..``qr/^(?!.*\.\.)[a-zA-Z0-9_.\/@ -]+$/`
- Remote name regex: `qr/^[a-zA-Z0-9_-]+$/` (rejects special characters)
- Per-user CSRF tokens at `/tmp/.gniza-cpanel-csrf-$user` (symlink-safe I/O)
- Per-user CSRF tokens at `/tmp/.gniza4cp-cpanel-csrf-$user` (symlink-safe I/O)
- Symlink-safe file operations: `_safe_write` uses `unlink` + `O_CREAT|O_EXCL` with fallback; `_safe_read` rejects symlinks via `-l` check
- Flash message type validated against allowlist (`success`, `error`, `info`, `warning`)
**Install locations:**
- CGIs: `/usr/local/cpanel/base/frontend/jupiter/gniza/`
- AdminBin: `/usr/local/cpanel/bin/admin/Gniza/` (Restore is `0700`, Restore.conf is `0600`)
- CGIs: `/usr/local/cpanel/base/frontend/jupiter/gniza4cp/`
- AdminBin: `/usr/local/cpanel/bin/admin/Gniza4cp/` (Restore is `0700`, Restore.conf is `0600`)
- Plugin registration: via `install_plugin` with tar.gz archive containing `install.json`
- Assets: CSS and logo copied to `gniza/assets/` alongside CGIs
- Assets: CSS and logo copied to `gniza4cp/assets/` alongside CGIs
- `install.json` also copied to CGI directory for `uninstall_plugin` to reference
**Restore categories (8 types):**
@@ -229,9 +229,9 @@ Allows cPanel account owners to restore their own data (files, databases, email,
3. Confirmation summary with CSRF token
4. Execute via AdminBin, display results
**cPanel plugin registration:** `install.json` is an array of plugin definitions passed to `install_plugin`/`uninstall_plugin` inside a **tar.gz archive** (with the icon file included). Required JSON fields per cPanel's `Cpanel::Themes::Assets::Link`: `type` ("link"), `id` (lowercase identifier), `name`, `group_id` (section: "files", "domains", etc.), `uri` (CGI path), `feature` (for Feature Manager), `order` (integer), `icon` (path relative to staging dir). The `feature` key (`gniza_restore`) allows admins to enable/disable per cPanel package.
**cPanel plugin registration:** `install.json` is an array of plugin definitions passed to `install_plugin`/`uninstall_plugin` inside a **tar.gz archive** (with the icon file included). Required JSON fields per cPanel's `Cpanel::Themes::Assets::Link`: `type` ("link"), `id` (lowercase identifier), `name`, `group_id` (section: "files", "domains", etc.), `uri` (CGI path), `feature` (for Feature Manager), `order` (integer), `icon` (path relative to staging dir). The `feature` key (`gniza4cp_restore`) allows admins to enable/disable per cPanel package.
### GnizaCPanel::UI
### Gniza4cpCPanel::UI
| Function | Description |
|----------|-------------|
@@ -239,21 +239,21 @@ Allows cPanel account owners to restore their own data (files, databases, email,
| `get_current_user()` | Returns `$ENV{'REMOTE_USER'}` |
| `_safe_write($file, $content)` | Symlink-safe write: `unlink` + `O_CREAT\|O_EXCL` (0600 perms) |
| `_safe_read($file)` | Symlink-safe read: rejects symlinks (`-l` check) |
| `page_header($title)` | Inline CSS + `data-theme="gniza"` wrapper + logo (base64 data URI) |
| `page_header($title)` | Inline CSS + `data-theme="gniza4cp"` wrapper + logo (base64 data URI) |
| `page_footer()` | Close wrapper div |
| `set_flash($type, $text)` | Store flash message at `/tmp/.gniza-cpanel-flash-$user` |
| `set_flash($type, $text)` | Store flash message at `/tmp/.gniza4cp-cpanel-flash-$user` |
| `get_flash()` | Read and consume flash message |
| `render_flash()` | Render flash as HTML alert (type validated against allowlist) |
| `generate_csrf_token()` | Generate 64-char hex token from `/dev/urandom`, store at `/tmp/.gniza-cpanel-csrf-$user` |
| `generate_csrf_token()` | Generate 64-char hex token from `/dev/urandom`, store at `/tmp/.gniza4cp-cpanel-csrf-$user` |
| `verify_csrf_token($token)` | Validate + delete (single-use), 1-hour expiry, constant-time comparison |
| `csrf_hidden_field()` | Generate CSRF token + hidden input |
| `render_errors(\@errors)` | Render error list as HTML |
| `_unwrap_layers($css)` | Strip `@layer` wrappers from Tailwind CSS |
| `_scope_to_container($css)` | Scope CSS rules to `[data-theme="gniza"]` container |
| `_scope_to_container($css)` | Scope CSS rules to `[data-theme="gniza4cp"]` container |
### AdminBin Module (Gniza::Restore)
### AdminBin Module (Gniza4cp::Restore)
Runs as root via cPanel's AdminBin framework. Each action validates inputs with strict regex patterns before executing gniza CLI via `IPC::Open3` (list execution, no shell).
Runs as root via cPanel's AdminBin framework. Each action validates inputs with strict regex patterns before executing gniza4cp CLI via `IPC::Open3` (list execution, no shell).
**Validation patterns:**
@@ -270,9 +270,9 @@ Runs as root via cPanel's AdminBin framework. Each action validates inputs with
**Actions:** `LIST_ALLOWED_REMOTES`, `LIST_SNAPSHOTS`, `LIST_DATABASES`, `LIST_MAILBOXES`, `LIST_FILES`, `LIST_DBUSERS`, `LIST_CRON`, `LIST_DNS`, `LIST_SSL`, `RESTORE_ACCOUNT`, `RESTORE_FILES`, `RESTORE_DATABASE`, `RESTORE_MAILBOX`, `RESTORE_CRON`, `RESTORE_DBUSERS`, `RESTORE_DOMAINS`, `RESTORE_SSL`
**Remote filtering:** `_get_allowed_remotes()` reads `USER_RESTORE_REMOTES` from `/etc/gniza/gniza.conf`. Returns `"all"` (default), comma-separated names, or empty string (disabled). `_is_remote_allowed()` and `_get_filtered_remotes()` enforce this on every action.
**Remote filtering:** `_get_allowed_remotes()` reads `USER_RESTORE_REMOTES` from `/etc/gniza4cp/gniza4cp.conf`. Returns `"all"` (default), comma-separated names, or empty string (disabled). `_is_remote_allowed()` and `_get_filtered_remotes()` enforce this on every action.
Called from CGI via: `Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'ACTION', @args)`
Called from CGI via: `Cpanel::AdminBin::Call::call('Gniza4cp', 'Restore', 'ACTION', @args)`
## Coding Conventions
@@ -281,7 +281,7 @@ Called from CGI via: `Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'ACTION',
- `set -euo pipefail` at top of entrypoint
- Functions use `local` for all variables
- Error paths: `log_error` + `return 1` (library) or `die "message"` (CLI)
- Guard-include pattern for constants: `[[ -n "${_GNIZA_CONSTANTS_LOADED:-}" ]] && return 0`
- Guard-include pattern for constants: `[[ -n "${_GNIZA4CP_CONSTANTS_LOADED:-}" ]] && return 0`
- `((count++)) || true` to avoid `set -e` traps on zero-to-one arithmetic
### Naming
@@ -289,7 +289,7 @@ Called from CGI via: `Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'ACTION',
- Libraries: `lib/<module>.sh` — each file focuses on one responsibility
- Public functions: `snake_case` (e.g., `transfer_pkgacct`, `list_remote_snapshots`)
- Private/helper functions: `_prefixed` (e.g., `_backup_to_current_remote`, `_save_remote_globals`)
- CLI commands: `cmd_<name>()` in `bin/gniza`
- CLI commands: `cmd_<name>()` in `bin/gniza4cp`
- Constants: `UPPER_SNAKE_CASE`, prefixed with `DEFAULT_` for defaults
- Globals: `UPPER_SNAKE_CASE` (e.g., `REMOTE_HOST`, `LOG_LEVEL`)
@@ -299,7 +299,7 @@ Called from CGI via: `Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'ACTION',
- In multi-remote mode, failure on one remote doesn't block others
- rsync retries with exponential backoff: `sleep $((attempt * 10))`
- Exit codes: `0` OK, `1` fatal, `2` locked, `5` partial failure
- Lock via `flock` on `/var/run/gniza.lock`
- Lock via `flock` on `/var/run/gniza4cp.lock`
### cPanel API Policy
@@ -341,16 +341,16 @@ Called from CGI via: `Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'ACTION',
**CLI (Bash):**
- **Config parsing:** `_safe_source_config()` in `lib/config.sh` reads KEY=VALUE lines via regex without `source`/`eval` — prevents command injection from malicious config files
- **Password handling:** SSH passwords passed via `sshpass -e` (environment variable `SSHPASS`), never `-p` (visible in process list)
- **File permissions:** `umask 077` set at startup in `bin/gniza`; `install.sh` sets config dirs to `chmod 700`
- **File permissions:** `umask 077` set at startup in `bin/gniza4cp`; `install.sh` sets config dirs to `chmod 700`
- **Safe rm:** `${var:?}` pattern prevents `rm -rf ""/\*` expansion on empty variables (SC2115)
- **Input validation:** `validate_timestamp()` and `validate_account_name()` enforce strict regex patterns. Account names: `^[a-z][a-z0-9_-]{0,15}$`. Timestamps: `^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{6}$`
- **RSYNC_EXTRA_OPTS validation:** Both Perl (Validator.pm) and Bash (`validate_config`) reject shell metacharacters (`^[a-zA-Z0-9 ._=/,-]+$`)
**WHM Plugin:**
- **CSRF:** All POST endpoints require CSRF token via `verify_csrf_token()`. Single-use tokens stored at `/var/cpanel/.gniza-whm-csrf/token`. AJAX endpoints (e.g., SMTP test) return a new token in JSON responses; JS updates both the AJAX variable and the main form hidden field to keep them in sync
- **CSRF:** All POST endpoints require CSRF token via `verify_csrf_token()`. Single-use tokens stored at `/var/cpanel/.gniza4cp-whm-csrf/token`. AJAX endpoints (e.g., SMTP test) return a new token in JSON responses; JS updates both the AJAX variable and the main form hidden field to keep them in sync
- **HTML escaping:** All user-controlled output passed through `esc()` (HTML entity encoding)
- **Runner path traversal:** `GnizaWHM::Runner` rejects `--account` and `--path` values containing `..`
- **Config file I/O:** `GnizaWHM::Config::save()` uses `flock(LOCK_EX)` with single file handle (open `+<` then seek+truncate) to prevent TOCTOU races
- **Runner path traversal:** `Gniza4cpWHM::Runner` rejects `--account` and `--path` values containing `..`
- **Config file I/O:** `Gniza4cpWHM::Config::save()` uses `flock(LOCK_EX)` with single file handle (open `+<` then seek+truncate) to prevent TOCTOU races
- **Safe file I/O:** `_safe_write()` uses `unlink` + `O_CREAT|O_EXCL` with plain-write fallback; `_safe_read()` rejects symlinks. Used for CSRF token and flash message files
- **Upgrade path:** `_ensure_dir()` removes stale plain files left by older versions before creating directories (old versions stored CSRF/flash as plain files at the directory path)
@@ -360,10 +360,10 @@ Called from CGI via: `Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'ACTION',
- **Remote filtering:** `USER_RESTORE_REMOTES` config controls which remotes users can access
- **Strict regex validation:** All AdminBin arguments validated against regex patterns (see AdminBin Module section)
- **Path traversal prevention:** Path regex uses negative lookahead: `qr/^(?!.*\.\.)[a-zA-Z0-9_.\/@ -]+$/`
- **CSRF:** Per-user single-use tokens at `/tmp/.gniza-cpanel-csrf-$user`, generated from `/dev/urandom` (64-char hex), 1-hour expiry, constant-time comparison
- **CSRF:** Per-user single-use tokens at `/tmp/.gniza4cp-cpanel-csrf-$user`, generated from `/dev/urandom` (64-char hex), 1-hour expiry, constant-time comparison
- **Symlink-safe I/O:** `_safe_write()` (unlink + `O_CREAT|O_EXCL` with fallback) and `_safe_read()` (rejects symlinks) for all `/tmp/` files
- **Flash type validation:** `render_flash()` validates type against allowlist (`success`, `error`, `info`, `warning`)
- **Command execution:** gniza CLI called via `IPC::Open3` as list (no shell interpolation)
- **Command execution:** gniza4cp CLI called via `IPC::Open3` as list (no shell interpolation)
### SSH/Rsync (REMOTE_TYPE=ssh)
@@ -386,27 +386,27 @@ Called from CGI via: `Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'ACTION',
## Configuration Files
### Main Config (`/etc/gniza/gniza.conf`)
### Main Config (`/etc/gniza4cp/gniza4cp.conf`)
Contains only local settings. Remote destinations are configured in `remotes.d/`.
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `TEMP_DIR` | No | `/usr/local/gniza/workdir` | Local working directory |
| `TEMP_DIR` | No | `/usr/local/gniza4cp/workdir` | Local working directory |
| `INCLUDE_ACCOUNTS` | No | (all) | Comma-separated account list |
| `EXCLUDE_ACCOUNTS` | No | `nobody` | Comma-separated exclusions |
| `LOG_DIR` | No | `/var/log/gniza` | Log directory |
| `LOG_DIR` | No | `/var/log/gniza4cp` | Log directory |
| `LOG_LEVEL` | No | `info` | `debug\|info\|warn\|error` |
| `LOG_RETAIN` | No | `90` | Days to keep log files |
| `NOTIFY_EMAIL` | No | (disabled) | Notification email |
| `NOTIFY_ON` | No | `failure` | `always\|failure\|never` |
| `LOCK_FILE` | No | `/var/run/gniza.lock` | Lock file path |
| `LOCK_FILE` | No | `/var/run/gniza4cp.lock` | Lock file path |
| `SSH_TIMEOUT` | No | `30` | SSH connection timeout (seconds) |
| `SSH_RETRIES` | No | `3` | rsync retry attempts |
| `RSYNC_EXTRA_OPTS` | No | (empty) | Extra rsync options |
| `USER_RESTORE_REMOTES` | No | `all` | Remotes for cPanel user restore (`all`, comma-separated names, or empty to disable) |
### Remote Config (`/etc/gniza/remotes.d/<name>.conf`)
### Remote Config (`/etc/gniza4cp/remotes.d/<name>.conf`)
**Common (all types):**
@@ -446,7 +446,7 @@ Contains only local settings. Remote destinations are configured in `remotes.d/`
| `GDRIVE_SERVICE_ACCOUNT_FILE` | Yes | — | Path to service account JSON key file |
| `GDRIVE_ROOT_FOLDER_ID` | No | (empty) | Root folder ID |
### Schedule Config (`/etc/gniza/schedules.d/<name>.conf`)
### Schedule Config (`/etc/gniza4cp/schedules.d/<name>.conf`)
Schedules are decoupled from remotes. Each schedule targets one or more remotes.
@@ -502,7 +502,7 @@ Schedules are decoupled from remotes. Each schedule targets one or more remotes.
### schedule.sh
Reads schedules from `/etc/gniza/schedules.d/` (decoupled from remotes).
Reads schedules from `/etc/gniza4cp/schedules.d/` (decoupled from remotes).
| Function | Description |
|----------|-------------|
@@ -510,10 +510,10 @@ Reads schedules from `/etc/gniza/schedules.d/` (decoupled from remotes).
| `has_schedules()` | Check if any schedule configs exist |
| `load_schedule(name)` | Source config, set SCHEDULE/SCHEDULE_REMOTES globals |
| `schedule_to_cron(name)` | Convert SCHEDULE vars to 5-field cron expression |
| `build_cron_line(name)` | Full cron line with gniza command, `--remote=` flag, and log redirect |
| `install_schedules()` | Strip old gniza cron entries, add new from all `schedules.d/` |
| `show_schedules()` | Display current gniza cron entries |
| `remove_schedules()` | Remove all gniza cron entries |
| `build_cron_line(name)` | Full cron line with gniza4cp command, `--remote=` flag, and log redirect |
| `install_schedules()` | Strip old gniza4cp cron entries, add new from all `schedules.d/` |
| `show_schedules()` | Display current gniza4cp cron entries |
| `remove_schedules()` | Remove all gniza4cp cron entries |
### restore.sh
@@ -530,7 +530,7 @@ All restore functions dispatch by `_is_rclone_mode` — using `rclone_from_remot
| `_rsync_download(src, dest)` | Download helper — dispatches rclone_from_remote or rsync |
| `_detect_pkgacct_base(user, ts)` | Detect old vs new snapshot format (SSH or cloud) |
### bin/gniza (CLI helpers)
### bin/gniza4cp (CLI helpers)
| Function | Description |
|----------|-------------|
@@ -544,16 +544,16 @@ All restore functions dispatch by `_is_rclone_mode` — using `rclone_from_remot
| `cmd_remote()` | Remote management: list, delete |
| `cmd_schedule()` | Schedule CRUD: add, delete, list, install, show, remove |
### GnizaWHM::UI (WHM plugin)
### Gniza4cpWHM::UI (WHM plugin)
| Function | Description |
|----------|-------------|
| `is_configured()` | True if any remote configs exist in `remotes.d/` |
| `detect_ssh_keys()` | Scan `/root/.ssh/` for key files, return arrayref of hashes |
| `render_ssh_guidance()` | HTML block: detected keys + keygen/ssh-copy-id instructions |
| `has_remotes()` | Check if `/etc/gniza/remotes.d/` has `.conf` files |
| `has_remotes()` | Check if `/etc/gniza4cp/remotes.d/` has `.conf` files |
| `list_remotes()` | Return sorted list of remote names |
| `has_schedules()` | Check if `/etc/gniza/schedules.d/` has `.conf` files |
| `has_schedules()` | Check if `/etc/gniza4cp/schedules.d/` has `.conf` files |
| `list_schedules()` | Return sorted list of schedule names |
| `schedule_conf_path($name)` | Return path to schedule config file |
| `esc($str)` | HTML-escape a string |
@@ -570,19 +570,19 @@ All restore functions dispatch by `_is_rclone_mode` — using `rclone_from_remot
| `test_ssh_connection(%args)` | Test SSH connection via ssh (accepts named args or positional for backward compat) |
| `test_rclone_connection(%args)` | Test S3/GDrive connection via rclone (generates temp config, runs `rclone lsd`) |
### GnizaWHM::Runner (WHM plugin)
### Gniza4cpWHM::Runner (WHM plugin)
Pattern-based command runner for safe CLI execution from the WHM UI. Each allowed command has regex patterns per argument position.
| Function | Description |
|----------|-------------|
| `run($cmd, $subcmd, \@args, \%opts)` | Validate against allowlist and execute gniza CLI |
| `run($cmd, $subcmd, \@args, \%opts)` | Validate against allowlist and execute gniza4cp CLI |
Allowed commands: `restore account/files/database/mailbox/list-databases/list-mailboxes`, `list`.
Named option patterns: `--remote`, `--timestamp`, `--path`, `--account`, `--terminate`, `--exclude`.
Path traversal prevention: `--account` and `--path` values containing `..` are rejected.
### GnizaWHM::Config
### Gniza4cpWHM::Config
Pure Perl config parser/writer. Uses `flock(LOCK_EX)` with single file handle for TOCTOU-safe reads and writes.
@@ -596,7 +596,7 @@ Pure Perl config parser/writer. Uses `flock(LOCK_EX)` with single file handle fo
| `@REMOTE_KEYS` | Remote config keys (REMOTE_TYPE, SSH, S3, GDrive, transfer, retention — no SCHEDULE*) |
| `@SCHEDULE_KEYS` | Schedule config keys (SCHEDULE, SCHEDULE_TIME, SCHEDULE_DAY, SCHEDULE_CRON, REMOTES, SYSBACKUP, SKIP_SUSPENDED) |
### GnizaWHM::Validator
### Gniza4cpWHM::Validator
| Function | Description |
|----------|-------------|
@@ -629,12 +629,12 @@ Tests use a simple `assert_eq`/`assert_ok`/`assert_fail` framework defined in `t
### Adding a new library function
1. Add to the appropriate `lib/<module>.sh`
2. Functions are automatically available — libraries are sourced in `bin/gniza`
2. Functions are automatically available — libraries are sourced in `bin/gniza4cp`
3. Use `local` for all variables, `log_*` for output, `return 1` for errors
### Adding a new command
1. Add `cmd_<name>()` function in `bin/gniza`
1. Add `cmd_<name>()` function in `bin/gniza4cp`
2. Add routing in `main()` case statement
3. Update `cmd_usage()` help text
4. Update `README.md` commands table
@@ -644,7 +644,7 @@ Tests use a simple `assert_eq`/`assert_ok`/`assert_fail` framework defined in `t
1. Add `DEFAULT_<NAME>` to `lib/constants.sh`
2. Add to `load_config()` in `lib/config.sh` with fallback
3. Add validation in `validate_config()` if needed
4. Add to `etc/gniza.conf.example`
4. Add to `etc/gniza4cp.conf.example`
5. Document in `README.md` and this file
### Making a function remote-aware
@@ -662,25 +662,25 @@ _restore_remote_globals
### Adding a new WHM plugin page
1. Create `whm/gniza-whm/<name>.cgi` following the pattern of existing CGIs
2. Use same boilerplate: shebang, `use lib`, `Whostmgr::HTMLInterface`, `Cpanel::Form`, `GnizaWHM::UI`
1. Create `whm/gniza4cp-whm/<name>.cgi` following the pattern of existing CGIs
2. Use same boilerplate: shebang, `use lib`, `Whostmgr::HTMLInterface`, `Cpanel::Form`, `Gniza4cpWHM::UI`
3. Route by `$form->{'action'}` or similar param
4. Use `GnizaWHM::UI::page_header()`, `render_nav()`, `render_flash()`, `csrf_hidden_field()`, `page_footer()`
4. Use `Gniza4cpWHM::UI::page_header()`, `render_nav()`, `render_flash()`, `csrf_hidden_field()`, `page_footer()`
5. Validate POST with `verify_csrf_token()`, redirect with 302 after success
6. No AppConfig change needed — `url=/cgi/gniza-whm/` covers all CGIs in the directory
6. No AppConfig change needed — `url=/cgi/gniza4cp-whm/` covers all CGIs in the directory
7. Add any new DaisyUI/Tailwind classes to `assets/src/safelist.html` and rebuild CSS
8. Add the page to `@NAV_ITEMS` in `UI.pm` if it should appear in the tab bar
### Adding a new cPanel plugin page
1. Create `cpanel/gniza/<name>.live.cgi` (note `.live.cgi` extension for Jupiter theme)
2. Use same boilerplate: shebang, `use lib` pointing to CGI lib dir, `Cpanel::Form`, `GnizaCPanel::UI`
3. For privilege escalation, call AdminBin: `Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'ACTION', @args)`
4. Use `GnizaCPanel::UI::page_header()`, `csrf_hidden_field()`, `page_footer()`
1. Create `cpanel/gniza4cp/<name>.live.cgi` (note `.live.cgi` extension for Jupiter theme)
2. Use same boilerplate: shebang, `use lib` pointing to CGI lib dir, `Cpanel::Form`, `Gniza4cpCPanel::UI`
3. For privilege escalation, call AdminBin: `Cpanel::AdminBin::Call::call('Gniza4cp', 'Restore', 'ACTION', @args)`
4. Use `Gniza4cpCPanel::UI::page_header()`, `csrf_hidden_field()`, `page_footer()`
5. Validate POST with `verify_csrf_token()`, redirect with 302 after success
6. For new AdminBin actions: add the action method to `cpanel/admin/Gniza/Restore` and to `_actions()` list
6. For new AdminBin actions: add the action method to `cpanel/admin/Gniza4cp/Restore` and to `_actions()` list
7. Add the CGI copy command to `scripts/install.sh` in the cPanel section
8. CSS is shared with WHM — same `gniza-whm.css` file, same DaisyUI classes
8. CSS is shared with WHM — same `gniza4cp-whm.css` file, same DaisyUI classes
### WHM CSS Policy
@@ -688,7 +688,7 @@ _restore_remote_globals
### WHM Theme & Color Palette
The WHM plugin uses a custom DaisyUI theme named `gniza` (defined in `assets/src/input.css`). Light-only, no dark mode.
The WHM plugin uses a custom DaisyUI theme named `gniza4cp` (defined in `assets/src/input.css`). Light-only, no dark mode.
| Role | OKLCH Value | Approx Color |
|------|-------------|-------------|
@@ -717,13 +717,13 @@ All WHM pages use Tailwind CSS v4 with DaisyUI v5 for styling. The CSS is built
**Build:**
```bash
cd whm/gniza-whm/assets && npm install && npm run build:css
cd whm/gniza4cp-whm/assets && npm install && npm run build:css
```
**Key files:**
- `assets/src/input.css` — Tailwind entry point with DaisyUI plugin config
- `assets/src/safelist.html` — Class safelist (required because Tailwind v4 scanner doesn't recognize `.cgi`/`.pm` file extensions)
- `assets/gniza-whm.css` — Built output (committed to repo)
- `assets/gniza4cp-whm.css` — Built output (committed to repo)
**WHM CSS delivery quirks:**
- WHM's CGI directory cannot serve static files directly
@@ -734,56 +734,56 @@ cd whm/gniza-whm/assets && npm install && npm run build:css
**Adding new CSS classes:**
1. Add the class to `assets/src/safelist.html` (since Tailwind can't scan `.cgi`/`.pm` files)
2. Rebuild: `cd whm/gniza-whm/assets && npm run build:css`
3. Commit the updated `gniza-whm.css`
2. Rebuild: `cd whm/gniza4cp-whm/assets && npm run build:css`
3. Commit the updated `gniza4cp-whm.css`
### Install / Uninstall Scripts
**install.sh** (`scripts/install.sh`) — must be run as root. Detects whether running from a local clone or downloads via git. Installs to `/usr/local/gniza/`.
**install.sh** (`scripts/install.sh`) — must be run as root. Detects whether running from a local clone or downloads via git. Installs to `/usr/local/gniza4cp/`.
Install steps:
1. Copy `bin/`, `lib/`, `etc/` to `/usr/local/gniza/`
2. Create symlink `/usr/local/bin/gniza``/usr/local/gniza/bin/gniza`
3. Create working directory `/usr/local/gniza/workdir`
4. Create config directories `/etc/gniza/remotes.d/` and `/etc/gniza/schedules.d/` (mode `0700`)
5. Copy example configs to `/etc/gniza/`
6. Create log directory `/var/log/gniza/`
7. If WHM detected: copy `whm/gniza-whm/` to CGI dir, register via `register_appconfig`
1. Copy `bin/`, `lib/`, `etc/` to `/usr/local/gniza4cp/`
2. Create symlink `/usr/local/bin/gniza4cp``/usr/local/gniza4cp/bin/gniza4cp`
3. Create working directory `/usr/local/gniza4cp/workdir`
4. Create config directories `/etc/gniza4cp/remotes.d/` and `/etc/gniza4cp/schedules.d/` (mode `0700`)
5. Copy example configs to `/etc/gniza4cp/`
6. Create log directory `/var/log/gniza4cp/`
7. If WHM detected: copy `whm/gniza4cp-whm/` to CGI dir, register via `register_appconfig`
8. If cPanel detected: copy CGIs + lib + assets to Jupiter theme dir, install AdminBin module, register via `install_plugin`
**uninstall.sh** (`scripts/uninstall.sh`) — must be run as root. Also installed to `/usr/local/gniza/uninstall.sh`.
**uninstall.sh** (`scripts/uninstall.sh`) — must be run as root. Also installed to `/usr/local/gniza4cp/uninstall.sh`.
Uninstall steps:
1. Remove symlink and install directory
2. Remove gniza cron entries (lines matching `# gniza:`)
2. Remove gniza4cp cron entries (lines matching `# gniza4cp:`)
3. If WHM plugin exists: unregister via `unregister_appconfig`, remove directory
4. If cPanel plugin exists: unregister via `uninstall_plugin`, remove CGI directory and AdminBin module
5. Print manual cleanup instructions for `/etc/gniza/`, `/var/log/gniza/`, `/var/run/gniza.lock`
5. Print manual cleanup instructions for `/etc/gniza4cp/`, `/var/log/gniza4cp/`, `/var/run/gniza4cp.lock`
**cPanel plugin registration quirk:** Both `install_plugin` and `uninstall_plugin` expect a **tar.gz archive** containing `install.json` — not a raw JSON file path. Passing a JSON file directly prints usage help and does nothing. The scripts create a temporary tar.gz:
```bash
PLUGIN_TMPDIR="$(mktemp -d)"
cp "$SOURCE_DIR/cpanel/gniza/install.json" "$PLUGIN_TMPDIR/"
tar -czf "$PLUGIN_TMPDIR/gniza-cpanel.tar.gz" -C "$PLUGIN_TMPDIR" install.json
/usr/local/cpanel/scripts/install_plugin "$PLUGIN_TMPDIR/gniza-cpanel.tar.gz"
cp "$SOURCE_DIR/cpanel/gniza4cp/install.json" "$PLUGIN_TMPDIR/"
tar -czf "$PLUGIN_TMPDIR/gniza4cp-cpanel.tar.gz" -C "$PLUGIN_TMPDIR" install.json
/usr/local/cpanel/scripts/install_plugin "$PLUGIN_TMPDIR/gniza4cp-cpanel.tar.gz"
rm -rf "$PLUGIN_TMPDIR"
```
`install.json` is also copied to the CGI directory (`$CPANEL_BASE/gniza/install.json`) so the uninstall script can find it.
`install.json` is also copied to the CGI directory (`$CPANEL_BASE/gniza4cp/install.json`) so the uninstall script can find it.
### Upgrade Considerations
**CSRF/flash storage migration (WHM):** Older versions stored CSRF tokens and flash messages as plain files at `/var/cpanel/.gniza-whm-csrf` and `/var/cpanel/.gniza-whm-flash`. Current versions use these as **directories** containing token files. `_ensure_dir()` in `GnizaWHM::UI` handles this automatically — it removes stale plain files before creating directories. Without this, CSRF token writes fail silently and all form submissions show "Invalid or expired form token."
**CSRF/flash storage migration (WHM):** Older versions stored CSRF tokens and flash messages as plain files at `/var/cpanel/.gniza4cp-whm-csrf` and `/var/cpanel/.gniza4cp-whm-flash`. Current versions use these as **directories** containing token files. `_ensure_dir()` in `Gniza4cpWHM::UI` handles this automatically — it removes stale plain files before creating directories. Without this, CSRF token writes fail silently and all form submissions show "Invalid or expired form token."
**CSRF token write robustness:** `generate_csrf_token()` uses `_safe_write()` (O_CREAT|O_EXCL) with a fallback to plain `open '>'` write. This ensures the token is always persisted even if the O_EXCL approach fails (e.g., race conditions, filesystem quirks).
**SMTP test + form token sync (WHM settings.cgi):** The SMTP test AJAX endpoint consumes the CSRF token and returns a new one. The JS handler updates both the AJAX variable (`gnizaCsrf`) and the main form's hidden `gniza_csrf` field. Without this sync, submitting the main form after an SMTP test would always fail CSRF validation.
**SMTP test + form token sync (WHM settings.cgi):** The SMTP test AJAX endpoint consumes the CSRF token and returns a new one. The JS handler updates both the AJAX variable (`gniza4cpCsrf`) and the main form's hidden `gniza4cp_csrf` field. Without this sync, submitting the main form after an SMTP test would always fail CSRF validation.
### Repository
| | URL |
|---|-----|
| **Git (SSH)** | `gitea:shukivaknin/gniza.git` (uses `Host gitea` from `~/.ssh/config`) |
| **Git (HTTPS)** | `https://git.linux-hosting.co.il/shukivaknin/gniza.git` |
| **Web UI** | https://git.linux-hosting.co.il/shukivaknin/gniza/ |
| **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/ |

160
README.md
View File

@@ -1,4 +1,4 @@
# gniza
# gniza4cp
cPanel Backup, Restore & Disaster Recovery tool.
@@ -8,96 +8,96 @@ Uses `pkgacct --nocompress --skiphomedir` for account backups, gzips SQL files i
| | URL |
|---|-----|
| **Git (SSH)** | `gitea:shukivaknin/gniza.git` (uses `Host gitea` from `~/.ssh/config`) |
| **Git (HTTPS)** | `https://git.linux-hosting.co.il/shukivaknin/gniza.git` |
| **Web UI** | https://git.linux-hosting.co.il/shukivaknin/gniza/ |
| **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/gniza.git
cd gniza
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/gniza/uninstall.sh # from installed copy
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/gniza/`) and logs (`/var/log/gniza/`) are preserved — remove manually if desired.
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 → GNIZA Backup Manager (setup wizard)
# Configure via WHM → GNIZA4CP Backup Manager (setup wizard)
# Or copy example configs manually:
sudo cp /etc/gniza/gniza.conf.example /etc/gniza/gniza.conf
sudo cp /etc/gniza/remote.conf.example /etc/gniza/remotes.d/nas.conf
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 gniza backup --dry-run
sudo gniza4cp backup --dry-run
# Run backup
sudo gniza backup
sudo gniza4cp backup
# Back up to specific remotes
sudo gniza backup --remote=nas,offsite
sudo gniza4cp backup --remote=nas,offsite
```
## Commands
```
gniza backup [--account=NAME] [--remote=NAME[,NAME2]] [--skip-suspended] [--dry-run]
gniza restore account <name> --remote=NAME [--timestamp=TS] [--force]
gniza restore files <name> --remote=NAME [--path=subpath] [--timestamp=TS]
gniza restore database <name> <dbname> --remote=NAME [--timestamp=TS]
gniza restore mailbox <name> <email@domain> --remote=NAME [--timestamp=TS]
gniza restore server --remote=NAME [--timestamp=TS]
gniza list [--account=NAME] [--remote=NAME]
gniza verify [--account=NAME] [--remote=NAME]
gniza status
gniza remote list
gniza remote delete <name>
gniza schedule add <name>
gniza schedule delete <name>
gniza schedule list
gniza schedule install
gniza schedule show
gniza schedule remove
gniza version
gniza help
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/gniza/gniza.conf`) |
| `--remote=NAME[,NAME2]` | Target specific remote(s) from `/etc/gniza/remotes.d/` (comma-separated) |
| `--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/gniza/gniza.conf`
**File:** `/etc/gniza4cp/gniza4cp.conf`
Controls local settings (accounts, logging, notifications). Remote destinations are configured in `/etc/gniza/remotes.d/`.
Controls local settings (accounts, logging, notifications). Remote destinations are configured in `/etc/gniza4cp/remotes.d/`.
```bash
# Local Settings
TEMP_DIR="/usr/local/gniza/workdir" # Working dir for pkgacct output
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/gniza"
LOG_DIR="/var/log/gniza4cp"
LOG_LEVEL="info" # debug, info, warn, error
LOG_RETAIN=90 # Days to keep log files
@@ -106,35 +106,35 @@ NOTIFY_EMAIL="" # Email for notifications
NOTIFY_ON="failure" # always, failure, never
# Advanced
LOCK_FILE="/var/run/gniza.lock"
LOCK_FILE="/var/run/gniza4cp.lock"
SSH_TIMEOUT=30
SSH_RETRIES=3
RSYNC_EXTRA_OPTS=""
```
See `etc/gniza.conf.example` for the full template.
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/gniza/remotes.d/`.
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/gniza/remote.conf.example /etc/gniza/remotes.d/nas.conf
sudo vi /etc/gniza/remotes.d/nas.conf
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 gniza remote list
sudo gniza4cp remote list
# Delete a remote
sudo gniza remote delete nas
sudo gniza4cp remote delete nas
```
#### Remote Config Format
Each file in `/etc/gniza/remotes.d/<name>.conf`:
Each file in `/etc/gniza4cp/remotes.d/<name>.conf`:
```bash
# Remote type: "ssh" (default), "s3", or "gdrive"
@@ -176,22 +176,22 @@ Without `--remote`, backup/list/verify operate on **all** configured remotes. Re
```bash
# Back up to all remotes
sudo gniza backup
sudo gniza4cp backup
# Back up to specific remote(s)
sudo gniza backup --remote=nas
sudo gniza backup --remote=nas,offsite
sudo gniza4cp backup --remote=nas
sudo gniza4cp backup --remote=nas,offsite
# List snapshots on a specific remote
sudo gniza list --remote=offsite
sudo gniza4cp list --remote=offsite
# Restore requires explicit remote
sudo gniza restore account johndoe --remote=nas
sudo gniza4cp restore account johndoe --remote=nas
```
### Schedules
Schedules are **decoupled from remotes**. Each schedule lives in `/etc/gniza/schedules.d/<name>.conf` and defines when backups run and which remotes to target. This allows multiple schedules targeting different sets of remotes.
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
@@ -211,22 +211,22 @@ SKIP_SUSPENDED="" # "yes" to skip cPanel suspended accounts
```bash
# Interactive schedule creation
sudo gniza schedule add nightly
sudo gniza4cp schedule add nightly
# List configured schedules
sudo gniza schedule list
sudo gniza4cp schedule list
# Delete a schedule
sudo gniza schedule delete nightly
sudo gniza4cp schedule delete nightly
# Install all schedules to crontab
sudo gniza schedule install
sudo gniza4cp schedule install
# Show current gniza cron entries
sudo gniza schedule show
# Show current gniza4cp cron entries
sudo gniza4cp schedule show
# Remove all gniza cron entries
sudo gniza schedule remove
# Remove all gniza4cp cron entries
sudo gniza4cp schedule remove
```
#### Schedule Types
@@ -242,8 +242,8 @@ sudo gniza schedule remove
Each schedule gets a tagged cron entry for clean install/remove:
```
# gniza:nightly
0 2 * * * /usr/local/bin/gniza backup --remote=nas,offsite >> /var/log/gniza/cron-nightly.log 2>&1
# gniza4cp:nightly
0 2 * * * /usr/local/bin/gniza4cp backup --remote=nas,offsite >> /var/log/gniza4cp/cron-nightly.log 2>&1
```
## Remote Directory Structure
@@ -331,8 +331,8 @@ All restore commands require `--remote=NAME` to specify the source.
## File Layout
```
/usr/local/gniza/ # Install directory
├── bin/gniza # CLI entrypoint
/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_*
@@ -352,12 +352,12 @@ All restore commands require `--remote=NAME` to specify the source.
│ ├── remotes.sh # Remote discovery and context switching
│ └── schedule.sh # Cron management for decoupled schedules
└── etc/
├── gniza.conf.example # Main config template
├── gniza4cp.conf.example # Main config template
├── remote.conf.example # Remote destination template
└── schedule.conf.example # Schedule template
/etc/gniza/ # Runtime configuration
├── gniza.conf # Main config
/etc/gniza4cp/ # Runtime configuration
├── gniza4cp.conf # Main config
├── remotes.d/ # Remote destination configs
│ ├── nas.conf
│ └── offsite.conf
@@ -365,29 +365,29 @@ All restore commands require `--remote=NAME` to specify the source.
├── nightly.conf
└── weekly-offsite.conf
/var/log/gniza/ # Log files
├── gniza-20260303-020000.log # Per-run logs
/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
gniza includes a WHM plugin for managing backups through the cPanel/WHM web interface. All pages use **Tailwind CSS v4** with **DaisyUI v5** for styling.
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 > gniza Backup Manager**.
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/gniza-whm/`.
Plugin files are deployed to `/usr/local/cpanel/whostmgr/docroot/cgi/gniza4cp-whm/`.
### Setup Wizard
When gniza is not yet configured (no remotes in `/etc/gniza/remotes.d/`), the dashboard automatically redirects to a **3-step 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/gniza/remotes.d/`.
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.
@@ -401,15 +401,15 @@ The wizard is also accessible anytime from the dashboard quick links ("Run Setup
| 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/gniza/gniza.conf`) |
| Settings | `settings.cgi` | Edit main config (`/etc/gniza4cp/gniza4cp.conf`) |
| Setup Wizard | `setup.cgi` | Guided initial configuration (3 steps) |
### Plugin File Layout
```
whm/
├── gniza-whm.conf # WHM AppConfig registration
└── gniza-whm/
├── gniza4cp-whm.conf # WHM AppConfig registration
└── gniza4cp-whm/
├── index.cgi # Dashboard
├── setup.cgi # Setup wizard (3 steps)
├── settings.cgi # Main config editor
@@ -417,12 +417,12 @@ whm/
├── schedules.cgi # Schedule CRUD + cron toggles
├── restore.cgi # Restore workflow (account → remote → snapshot → type)
├── assets/
│ ├── gniza-whm.css # Built Tailwind/DaisyUI CSS (committed)
│ ├── 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/GnizaWHM/
└── lib/Gniza4cpWHM/
├── Config.pm # Config parser/writer (pure Perl)
├── Validator.pm # Input validation
├── Cron.pm # Cron read + per-schedule install/remove

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza — cPanel Backup, Restore & Disaster Recovery
# gniza4cp — cPanel Backup, Restore & Disaster Recovery
# CLI entrypoint and command routing
set -euo pipefail
@@ -256,7 +256,7 @@ cmd_backup() {
[[ -n "$remote_flag" ]] && sysbackup_args+=(--remote="$remote_flag")
[[ "$dry_run" == "true" ]] && sysbackup_args+=(--dry-run)
# Run as subprocess so its exit doesn't kill our process
/usr/local/bin/gniza sysbackup "${sysbackup_args[@]}" || log_error "System backup failed"
/usr/local/bin/gniza4cp sysbackup "${sysbackup_args[@]}" || log_error "System backup failed"
acquire_lock
fi
@@ -301,7 +301,7 @@ cmd_restore() {
account)
local name="${1:-}"
shift 2>/dev/null || true
[[ -z "$name" ]] && die "Usage: gniza restore account <name> [--remote=NAME] [--timestamp=TS] [--terminate]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore account <name> [--remote=NAME] [--timestamp=TS] [--terminate]"
validate_account_name "$name" || die "Invalid account name"
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
@@ -325,7 +325,7 @@ cmd_restore() {
files)
local name="${1:-}"
shift 2>/dev/null || true
[[ -z "$name" ]] && die "Usage: gniza restore files <name> [--remote=NAME] [--path=subpath] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore files <name> [--remote=NAME] [--path=subpath] [--timestamp=TS]"
validate_account_name "$name" || die "Invalid account name"
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
@@ -347,7 +347,7 @@ cmd_restore() {
database)
local name="${1:-}"
local dbname="${2:-}"
[[ -z "$name" ]] && die "Usage: gniza restore database <name> [<dbname>] [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore database <name> [<dbname>] [--remote=NAME] [--timestamp=TS]"
validate_account_name "$name" || die "Invalid account name"
shift 2>/dev/null || true
# If dbname looks like a flag, it's not a dbname
@@ -378,7 +378,7 @@ cmd_restore() {
mailbox)
local name="${1:-}"
local email="${2:-}"
[[ -z "$name" ]] && die "Usage: gniza restore mailbox <name> [<email@domain>] [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore mailbox <name> [<email@domain>] [--remote=NAME] [--timestamp=TS]"
validate_account_name "$name" || die "Invalid account name"
shift 2>/dev/null || true
# If email looks like a flag, it's not an email
@@ -409,7 +409,7 @@ cmd_restore() {
list-databases)
local name="${1:-}"
shift 2>/dev/null || true
[[ -z "$name" ]] && die "Usage: gniza restore list-databases <name> [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore list-databases <name> [--remote=NAME] [--timestamp=TS]"
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
load_config "$config_file"
@@ -427,7 +427,7 @@ cmd_restore() {
list-mailboxes)
local name="${1:-}"
shift 2>/dev/null || true
[[ -z "$name" ]] && die "Usage: gniza restore list-mailboxes <name> [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore list-mailboxes <name> [--remote=NAME] [--timestamp=TS]"
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
load_config "$config_file"
@@ -445,7 +445,7 @@ cmd_restore() {
list-files)
local name="${1:-}"
shift 2>/dev/null || true
[[ -z "$name" ]] && die "Usage: gniza restore list-files <name> [--remote=NAME] [--timestamp=TS] [--path=subdir]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore list-files <name> [--remote=NAME] [--timestamp=TS] [--path=subdir]"
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
load_config "$config_file"
@@ -464,7 +464,7 @@ cmd_restore() {
list-dbusers)
local name="${1:-}"
shift 2>/dev/null || true
[[ -z "$name" ]] && die "Usage: gniza restore list-dbusers <name> [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore list-dbusers <name> [--remote=NAME] [--timestamp=TS]"
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
load_config "$config_file"
@@ -482,7 +482,7 @@ cmd_restore() {
list-cron)
local name="${1:-}"
shift 2>/dev/null || true
[[ -z "$name" ]] && die "Usage: gniza restore list-cron <name> [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore list-cron <name> [--remote=NAME] [--timestamp=TS]"
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
load_config "$config_file"
@@ -500,7 +500,7 @@ cmd_restore() {
list-dns)
local name="${1:-}"
shift 2>/dev/null || true
[[ -z "$name" ]] && die "Usage: gniza restore list-dns <name> [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore list-dns <name> [--remote=NAME] [--timestamp=TS]"
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
load_config "$config_file"
@@ -518,7 +518,7 @@ cmd_restore() {
list-ssl)
local name="${1:-}"
shift 2>/dev/null || true
[[ -z "$name" ]] && die "Usage: gniza restore list-ssl <name> [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore list-ssl <name> [--remote=NAME] [--timestamp=TS]"
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
load_config "$config_file"
@@ -536,7 +536,7 @@ cmd_restore() {
cron)
local name="${1:-}"
shift 2>/dev/null || true
[[ -z "$name" ]] && die "Usage: gniza restore cron <name> [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore cron <name> [--remote=NAME] [--timestamp=TS]"
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
load_config "$config_file"
@@ -554,7 +554,7 @@ cmd_restore() {
dbusers)
local name="${1:-}"
local specific_dbuser="${2:-}"
[[ -z "$name" ]] && die "Usage: gniza restore dbusers <name> [<dbuser>] [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore dbusers <name> [<dbuser>] [--remote=NAME] [--timestamp=TS]"
shift 2>/dev/null || true
if [[ -n "$specific_dbuser" && "$specific_dbuser" != --* ]]; then
shift 2>/dev/null || true
@@ -578,7 +578,7 @@ cmd_restore() {
cpconfig)
local name="${1:-}"
shift 2>/dev/null || true
[[ -z "$name" ]] && die "Usage: gniza restore cpconfig <name> [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore cpconfig <name> [--remote=NAME] [--timestamp=TS]"
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
load_config "$config_file"
@@ -596,7 +596,7 @@ cmd_restore() {
domains)
local name="${1:-}"
local specific_domain="${2:-}"
[[ -z "$name" ]] && die "Usage: gniza restore domains <name> [<domain>] [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore domains <name> [<domain>] [--remote=NAME] [--timestamp=TS]"
shift 2>/dev/null || true
if [[ -n "$specific_domain" && "$specific_domain" != --* ]]; then
shift 2>/dev/null || true
@@ -620,7 +620,7 @@ cmd_restore() {
ssl)
local name="${1:-}"
local specific_cert="${2:-}"
[[ -z "$name" ]] && die "Usage: gniza restore ssl <name> [<cert>] [--remote=NAME] [--timestamp=TS]"
[[ -z "$name" ]] && die "Usage: gniza4cp restore ssl <name> [<cert>] [--remote=NAME] [--timestamp=TS]"
shift 2>/dev/null || true
if [[ -n "$specific_cert" && "$specific_cert" != --* ]]; then
shift 2>/dev/null || true
@@ -656,7 +656,7 @@ cmd_restore() {
restore_server "$timestamp"
;;
*)
die "Unknown restore subcommand: $subcommand"$'\n'"Usage: gniza restore {account|files|database|mailbox|cron|dbusers|cpconfig|domains|ssl|list-databases|list-mailboxes|list-files|list-dbusers|list-cron|list-dns|list-ssl|server}"
die "Unknown restore subcommand: $subcommand"$'\n'"Usage: gniza4cp restore {account|files|database|mailbox|cron|dbusers|cpconfig|domains|ssl|list-databases|list-mailboxes|list-files|list-dbusers|list-cron|list-dns|list-ssl|server}"
;;
esac
}
@@ -707,7 +707,7 @@ cmd_list() {
shift
local remote_flag=""
remote_flag=$(get_opt remote "$@" 2>/dev/null) || true
[[ -z "$remote_flag" ]] && die "Usage: gniza list accounts --remote=NAME"
[[ -z "$remote_flag" ]] && die "Usage: gniza4cp list accounts --remote=NAME"
local remotes; remotes=$(get_target_remotes "$remote_flag") || die "Invalid remote"
local rname; rname=$(head -1 <<< "$remotes")
_save_remote_globals
@@ -847,7 +847,7 @@ cmd_status() {
local hostname; hostname=$(hostname -f)
echo "${C_BOLD}gniza v${GNIZA_VERSION}${C_RESET}"
echo "${C_BOLD}gniza4cp v${GNIZA4CP4CP_VERSION}${C_RESET}"
echo ""
echo "Hostname: $hostname"
echo "Log level: ${LOG_LEVEL}"
@@ -902,7 +902,7 @@ cmd_status() {
# Last log
local log_dir="${LOG_DIR:-$DEFAULT_LOG_DIR}"
echo -n "Last log: "
local last_log; last_log=$(ls -1t "$log_dir"/gniza-*.log 2>/dev/null | head -1)
local last_log; last_log=$(ls -1t "$log_dir"/gniza4cp-*.log 2>/dev/null | head -1)
if [[ -n "$last_log" ]]; then
echo "$(basename "$last_log")"
else
@@ -949,7 +949,7 @@ cmd_remote() {
delete|rm|remove)
require_root
local name="${1:-}"
[[ -z "$name" ]] && die "Usage: gniza remote delete <name>"
[[ -z "$name" ]] && die "Usage: gniza4cp remote delete <name>"
local conf="$REMOTES_DIR/${name}.conf"
if [[ ! -f "$conf" ]]; then
@@ -971,7 +971,7 @@ cmd_remote() {
echo "Remote '$name' deleted."
;;
*)
die "Unknown remote subcommand: $subcommand"$'\n'"Usage: gniza remote {list|delete <name>}"
die "Unknown remote subcommand: $subcommand"$'\n'"Usage: gniza4cp remote {list|delete <name>}"
;;
esac
}
@@ -988,17 +988,17 @@ cmd_schedule() {
case "$subcommand" in
add)
local name="${1:-}"
[[ -z "$name" ]] && die "Usage: gniza schedule add <name>"
[[ -z "$name" ]] && die "Usage: gniza4cp schedule add <name>"
_schedule_add "$name"
;;
delete|rm|remove-schedule)
local name="${1:-}"
[[ -z "$name" ]] && die "Usage: gniza schedule delete <name>"
[[ -z "$name" ]] && die "Usage: gniza4cp schedule delete <name>"
_schedule_delete "$name"
;;
run)
local name="${1:-}"
[[ -z "$name" ]] && die "Usage: gniza schedule run <name>"
[[ -z "$name" ]] && die "Usage: gniza4cp schedule run <name>"
_schedule_run "$name"
;;
list|ls)
@@ -1007,7 +1007,7 @@ cmd_schedule() {
install) install_schedules ;;
show) show_schedules ;;
remove) remove_schedules ;;
*) die "Unknown schedule subcommand: $subcommand"$'\n'"Usage: gniza schedule {add|delete|run|list|install|show|remove}" ;;
*) die "Unknown schedule subcommand: $subcommand"$'\n'"Usage: gniza4cp schedule {add|delete|run|list|install|show|remove}" ;;
esac
}
@@ -1026,7 +1026,7 @@ _schedule_add() {
[[ "$answer" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
fi
echo "${C_BOLD}gniza schedule add${C_RESET} — New schedule: ${C_BOLD}$name${C_RESET}"
echo "${C_BOLD}gniza4cp schedule add${C_RESET} — New schedule: ${C_BOLD}$name${C_RESET}"
echo ""
echo "Schedule options: hourly, daily, weekly, monthly, custom"
@@ -1080,8 +1080,8 @@ _schedule_add() {
# Write config
mkdir -p "$SCHEDULES_DIR"
cat > "$config_file" <<CONF
# gniza schedule config: $name
# Generated by 'gniza schedule add $name'
# gniza4cp schedule config: $name
# Generated by 'gniza4cp schedule add $name'
# $(date -u +"%d/%m/%Y %H:%M:%S UTC")
SCHEDULE="$sched_type"
@@ -1094,7 +1094,7 @@ CONF
echo ""
echo "${C_GREEN}Schedule '$name' created: $config_file${C_RESET}"
echo "Run 'gniza schedule install' to activate cron entries."
echo "Run 'gniza4cp schedule install' to activate cron entries."
}
_schedule_delete() {
@@ -1116,7 +1116,7 @@ _schedule_delete() {
rm -f "$config_file"
echo "Schedule '$name' deleted."
echo "Run 'gniza schedule install' to update cron entries."
echo "Run 'gniza4cp schedule install' to update cron entries."
}
_schedule_run() {
@@ -1147,13 +1147,13 @@ _schedule_run() {
echo ""
# Exec replaces this process with the backup command
exec /usr/local/bin/gniza backup "${args[@]}"
exec /usr/local/bin/gniza4cp backup "${args[@]}"
}
_schedule_list() {
if ! has_schedules; then
echo "No schedules configured."
echo "Run 'gniza schedule add <name>' to create one."
echo "Run 'gniza4cp schedule add <name>' to create one."
return 0
fi
@@ -1445,7 +1445,7 @@ cmd_stats() {
local last_log=""
local latest_log=""
if [[ -d "$log_dir" ]]; then
latest_log=$(ls -1t "$log_dir"/gniza-[0-9]*-[0-9]*.log 2>/dev/null | head -1) || true
latest_log=$(ls -1t "$log_dir"/gniza4cp-[0-9]*-[0-9]*.log 2>/dev/null | head -1) || true
fi
if [[ -n "$latest_log" && -f "$latest_log" ]]; then
last_log=$(basename "$latest_log")
@@ -1468,10 +1468,10 @@ cmd_stats() {
cmd_usage() {
cat <<EOF
${C_BOLD}gniza v${GNIZA_VERSION}${C_RESET} — cPanel Backup, Restore & Disaster Recovery
${C_BOLD}gniza4cp v${GNIZA4CP4CP_VERSION}${C_RESET} — cPanel Backup, Restore & Disaster Recovery
${C_BOLD}Usage:${C_RESET}
gniza <command> [options]
gniza4cp <command> [options]
${C_BOLD}Commands:${C_RESET}
backup [--account=NAME] [--remote=NAME[,NAME2]] [--dry-run] [--sysbackup] [--skip-suspended]
@@ -1504,27 +1504,27 @@ ${C_BOLD}Commands:${C_RESET}
version Show version
${C_BOLD}Global Options:${C_RESET}
--config=PATH Use alternate config file (default: /etc/gniza/gniza.conf)
--config=PATH Use alternate config file (default: /etc/gniza4cp/gniza4cp.conf)
--remote=NAME Target specific remote(s), comma-separated
--debug Enable debug logging
${C_BOLD}Examples:${C_RESET}
gniza backup --dry-run
gniza backup --account=johndoe
gniza backup --remote=nas
gniza backup --remote=nas,offsite
gniza list --remote=offsite
gniza restore files johndoe --remote=nas --path=public_html
gniza restore database johndoe johndoe_wp --remote=nas
gniza restore mailbox johndoe info@example.com --remote=nas
gniza schedule add nightly
gniza schedule list
gniza schedule install
gniza remote list
gniza sysbackup --dry-run
gniza sysbackup --remote=nas
gniza sysrestore --remote=nas
gniza sysrestore --remote=nas --phase=1 --dry-run
gniza4cp backup --dry-run
gniza4cp backup --account=johndoe
gniza4cp backup --remote=nas
gniza4cp backup --remote=nas,offsite
gniza4cp list --remote=offsite
gniza4cp restore files johndoe --remote=nas --path=public_html
gniza4cp restore database johndoe johndoe_wp --remote=nas
gniza4cp restore mailbox johndoe info@example.com --remote=nas
gniza4cp schedule add nightly
gniza4cp schedule list
gniza4cp schedule install
gniza4cp remote list
gniza4cp sysbackup --dry-run
gniza4cp sysbackup --remote=nas
gniza4cp sysrestore --remote=nas
gniza4cp sysrestore --remote=nas --phase=1 --dry-run
EOF
}
@@ -1533,7 +1533,7 @@ EOF
main() {
# Global --debug flag (used by config.sh load_config)
# shellcheck disable=SC2034
has_flag debug "$@" && GNIZA_DEBUG=true || GNIZA_DEBUG=false
has_flag debug "$@" && GNIZA4CP4CP_DEBUG=true || GNIZA4CP4CP_DEBUG=false
local command="${1:-}"
shift 2>/dev/null || true
@@ -1549,10 +1549,10 @@ main() {
remote) cmd_remote "$@" ;;
schedule) cmd_schedule "$@" ;;
stats) cmd_stats "$@" ;;
version) echo "gniza v${GNIZA_VERSION}" ;;
version) echo "gniza4cp v${GNIZA4CP4CP_VERSION}" ;;
help|-h|--help) cmd_usage ;;
"") cmd_usage ;;
*) die "Unknown command: $command"$'\n'"Run 'gniza help' for usage" ;;
*) die "Unknown command: $command"$'\n'"Run 'gniza4cp help' for usage" ;;
esac
}

View File

@@ -1,17 +1,18 @@
#!/usr/local/cpanel/3rdparty/bin/perl
package Cpanel::AdminBin::Script::Call::Gniza::Restore;
package Cpanel::AdminBin::Script::Call::Gniza4cp::Restore;
use strict;
use warnings;
use parent 'Cpanel::AdminBin::Script::Call';
use IPC::Open3;
use Symbol 'gensym';
use POSIX qw(setsid);
my $GNIZA_BIN = '/usr/local/bin/gniza';
my $MAIN_CONFIG = '/etc/gniza/gniza.conf';
my $REMOTES_DIR = '/etc/gniza/remotes.d';
my $GNIZA4CP4CP_BIN = '/usr/local/bin/gniza4cp';
my $MAIN_CONFIG = '/etc/gniza4cp/gniza4cp.conf';
my $REMOTES_DIR = '/etc/gniza4cp/remotes.d';
# Argument validation patterns (mirrors GnizaWHM::Runner)
# Argument validation patterns (mirrors Gniza4cp4cpWHM::Runner)
my %OPT_PATTERNS = (
remote => qr/^[a-zA-Z0-9_,-]+$/,
timestamp => qr/^\d{4}-\d{2}-\d{2}T\d{6}$/,
@@ -84,14 +85,14 @@ sub _get_filtered_remotes {
# ── Command execution ─────────────────────────────────────────
sub _run_gniza {
sub _run_gniza4cp {
my (@args) = @_;
my $err_fh = gensym;
my ($in, $out);
my $pid = eval { open3($in, $out, $err_fh, $GNIZA_BIN, @args) };
my $pid = eval { open3($in, $out, $err_fh, $GNIZA4CP4CP_BIN, @args) };
unless ($pid) {
return (0, '', "Failed to execute gniza: $@");
return (0, '', "Failed to execute gniza4cp: $@");
}
close $in if $in;
@@ -113,7 +114,7 @@ sub _run_gniza {
my $ACTIVITY_ENTRY_RE = qr/^[0-9]+$/;
sub _get_log_dir {
my $log_dir = '/var/log/gniza';
my $log_dir = '/var/log/gniza4cp';
if (open my $fh, '<', $MAIN_CONFIG) {
while (my $line = <$fh>) {
if ($line =~ /^LOG_DIR=(?:"([^"]*)"|'([^']*)'|(\S*))$/) {
@@ -179,6 +180,7 @@ sub _actions {
LIST_SSL
LIST_LOGS
GET_LOG
START_RESTORE
RESTORE_ACCOUNT
RESTORE_FILES
RESTORE_DATABASE
@@ -279,7 +281,7 @@ sub LIST_SNAPSHOTS {
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('list', "--remote=$remote", "--account=$user");
my ($ok, $stdout, $stderr) = _run_gniza4cp('list', "--remote=$remote", "--account=$user");
return $ok ? $stdout : "ERROR: $stderr";
}
@@ -292,7 +294,7 @@ sub LIST_DATABASES {
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-databases', $user,
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-databases', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? $stdout : "ERROR: $stderr";
}
@@ -306,7 +308,7 @@ sub LIST_MAILBOXES {
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-mailboxes', $user,
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-mailboxes', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? $stdout : "ERROR: $stderr";
}
@@ -326,7 +328,7 @@ sub LIST_FILES {
push @opts, "--path=$path";
}
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-files', $user, @opts);
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-files', $user, @opts);
return $ok ? $stdout : "ERROR: $stderr";
}
@@ -339,7 +341,7 @@ sub LIST_DBUSERS {
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-dbusers', $user,
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-dbusers', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? $stdout : "ERROR: $stderr";
}
@@ -353,7 +355,7 @@ sub LIST_CRON {
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-cron', $user,
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-cron', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? $stdout : "ERROR: $stderr";
}
@@ -367,7 +369,7 @@ sub LIST_DNS {
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-dns', $user,
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-dns', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? $stdout : "ERROR: $stderr";
}
@@ -381,11 +383,194 @@ sub LIST_SSL {
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-ssl', $user,
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'list-ssl', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? $stdout : "ERROR: $stderr";
}
# ── Background restore ────────────────────────────────────────
#
# START_RESTORE($remote, $timestamp, $types_str, $path, $exclude_paths)
#
# $types_str encodes selected types and items, semicolon-separated:
# account;files;database:db1,db2;mailbox:a@b.com;cron;dbusers:u1;domains:d1;ssl:d1
#
# Validates all inputs, forks a detached child that runs each gniza4cp
# restore command and logs results via _log_activity(), then returns
# immediately with "OK".
my %TYPE_ITEM_RE = (
account => undef, # no items
files => undef, # uses $path arg instead
cron => undef, # no items
database => $DBNAME_RE,
dbusers => $DBNAME_RE,
mailbox => $EMAIL_RE,
domains => $DOMAIN_RE,
ssl => $DOMAIN_RE,
);
# Map type + item to gniza4cp CLI arguments (excluding --remote/--timestamp which are always added)
sub _build_restore_args {
my ($type, $user, $item, $path, $exclude) = @_;
if ($type eq 'account') {
my @args = ('restore', 'account', $user);
push @args, "--exclude=$exclude" if defined $exclude && $exclude ne '';
return @args;
}
elsif ($type eq 'files') {
my @args = ('restore', 'files', $user);
push @args, "--path=$path" if defined $path && $path ne '';
push @args, "--exclude=$exclude" if defined $exclude && $exclude ne '';
return @args;
}
elsif ($type eq 'cron') {
return ('restore', 'cron', $user);
}
elsif ($type eq 'database') {
my @args = ('restore', 'database', $user);
push @args, $item if defined $item && $item ne '';
return @args;
}
elsif ($type eq 'dbusers') {
my @args = ('restore', 'dbusers', $user);
push @args, $item if defined $item && $item ne '';
return @args;
}
elsif ($type eq 'mailbox') {
my @args = ('restore', 'mailbox', $user);
push @args, $item if defined $item && $item ne '';
return @args;
}
elsif ($type eq 'domains') {
my @args = ('restore', 'domains', $user);
push @args, $item if defined $item && $item ne '';
return @args;
}
elsif ($type eq 'ssl') {
my @args = ('restore', 'ssl', $user);
push @args, $item if defined $item && $item ne '';
return @args;
}
return ();
}
# Map type to RESTORE_* action name for _log_activity
my %TYPE_ACTION_MAP = (
account => 'RESTORE_ACCOUNT',
files => 'RESTORE_FILES',
database => 'RESTORE_DATABASE',
dbusers => 'RESTORE_DBUSERS',
mailbox => 'RESTORE_MAILBOX',
cron => 'RESTORE_CRON',
domains => 'RESTORE_DOMAINS',
ssl => 'RESTORE_SSL',
);
sub START_RESTORE {
my ($self, $remote, $timestamp, $types_str, $path, $exclude_paths) = @_;
my $user = $self->get_caller_username() // '';
# ── Validate common args ──
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
if (defined $path && $path ne '') {
return "ERROR: Invalid path" unless $path =~ $OPT_PATTERNS{path};
}
if (defined $exclude_paths && $exclude_paths ne '') {
return "ERROR: Invalid exclude" unless $exclude_paths =~ $OPT_PATTERNS{exclude};
}
# ── Parse and validate types_str ──
$types_str //= '';
return "ERROR: No restore types specified" if $types_str eq '';
my @jobs; # [ { type => ..., items => [...] }, ... ]
for my $part (split /;/, $types_str) {
next if $part eq '';
my ($type, $items_str) = split /:/, $part, 2;
return "ERROR: Invalid restore type: $type" unless exists $TYPE_ITEM_RE{$type};
my $item_re = $TYPE_ITEM_RE{$type};
my @items;
if (defined $items_str && $items_str ne '') {
return "ERROR: Type '$type' does not accept items" unless defined $item_re;
for my $item (split /,/, $items_str) {
next if $item eq '';
return "ERROR: Invalid item for $type: $item" unless $item =~ $item_re;
push @items, $item;
return "ERROR: Too many items for $type (max 100)" if @items > 100;
}
}
push @jobs, { type => $type, items => \@items };
}
return "ERROR: No valid restore types parsed" unless @jobs;
# ── Pre-build all command arg lists to validate before forking ──
my @cmd_list; # [ { args => [...], action => ..., details => ... }, ... ]
for my $job (@jobs) {
my $type = $job->{type};
my @items = @{$job->{items}};
if (@items) {
# One command per item
for my $item (@items) {
my @args = _build_restore_args($type, $user, $item, $path, $exclude_paths);
return "ERROR: Failed to build command for $type" unless @args;
my $details = "remote=$remote snapshot=$timestamp";
if ($type eq 'database') { $details .= " database=$item"; }
elsif ($type eq 'dbusers') { $details .= " dbuser=$item"; }
elsif ($type eq 'mailbox') { $details .= " email=$item"; }
elsif ($type eq 'domains') { $details .= " domain=$item"; }
elsif ($type eq 'ssl') { $details .= " domain=$item"; }
push @cmd_list, { args => \@args, action => $TYPE_ACTION_MAP{$type}, details => $details };
}
} else {
# Single command for this type
my @args = _build_restore_args($type, $user, '', $path, $exclude_paths);
return "ERROR: Failed to build command for $type" unless @args;
my $details = "remote=$remote snapshot=$timestamp";
$details .= " path=$path" if $type eq 'files' && defined $path && $path ne '';
$details .= " exclude=$exclude_paths" if defined $exclude_paths && $exclude_paths ne '';
push @cmd_list, { args => \@args, action => $TYPE_ACTION_MAP{$type}, details => $details };
}
}
# ── Fork detached child ──
local $SIG{CHLD} = 'IGNORE';
my $pid = fork();
return "ERROR: Fork failed: $!" unless defined $pid;
if ($pid == 0) {
# Child: detach from parent completely
eval {
setsid();
open STDIN, '<', '/dev/null';
open STDOUT, '>', '/dev/null';
open STDERR, '>', '/dev/null';
for my $cmd (@cmd_list) {
my @full_args = (@{$cmd->{args}}, "--remote=$remote", "--timestamp=$timestamp");
my ($ok, $stdout, $stderr) = _run_gniza4cp(@full_args);
_log_activity($user, $cmd->{action}, $cmd->{details},
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
}
};
# Ensure child exits even on error
POSIX::_exit(0);
}
# Parent: return immediately
return "OK";
}
sub RESTORE_ACCOUNT {
my ($self, $remote, $timestamp, $exclude) = @_;
my $user = $self->get_caller_username() // '';
@@ -405,7 +590,7 @@ sub RESTORE_ACCOUNT {
my $details = "remote=$remote snapshot=$timestamp";
$details .= " exclude=$exclude" if defined $exclude && $exclude ne '';
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'account', $user, @opts);
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'account', $user, @opts);
_log_activity($user, 'RESTORE_ACCOUNT', $details,
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
@@ -434,7 +619,7 @@ sub RESTORE_FILES {
$details .= " path=$path" if defined $path && $path ne '';
$details .= " exclude=$exclude" if defined $exclude && $exclude ne '';
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'files', $user, @opts);
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'files', $user, @opts);
_log_activity($user, 'RESTORE_FILES', $details,
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
@@ -458,7 +643,7 @@ sub RESTORE_DATABASE {
my $details = "remote=$remote snapshot=$timestamp";
$details .= " database=$dbname" if defined $dbname && $dbname ne '';
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'database', @args,
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'database', @args,
"--remote=$remote", "--timestamp=$timestamp");
_log_activity($user, 'RESTORE_DATABASE', $details,
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
@@ -483,7 +668,7 @@ sub RESTORE_MAILBOX {
my $details = "remote=$remote snapshot=$timestamp";
$details .= " email=$email" if defined $email && $email ne '';
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'mailbox', @args,
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'mailbox', @args,
"--remote=$remote", "--timestamp=$timestamp");
_log_activity($user, 'RESTORE_MAILBOX', $details,
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
@@ -501,7 +686,7 @@ sub RESTORE_CRON {
my $details = "remote=$remote snapshot=$timestamp";
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'cron', $user,
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'cron', $user,
"--remote=$remote", "--timestamp=$timestamp");
_log_activity($user, 'RESTORE_CRON', $details,
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
@@ -526,7 +711,7 @@ sub RESTORE_DBUSERS {
my $details = "remote=$remote snapshot=$timestamp";
$details .= " dbuser=$dbuser" if defined $dbuser && $dbuser ne '';
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'dbusers', @args,
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'dbusers', @args,
"--remote=$remote", "--timestamp=$timestamp");
_log_activity($user, 'RESTORE_DBUSERS', $details,
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
@@ -551,7 +736,7 @@ sub RESTORE_DOMAINS {
my $details = "remote=$remote snapshot=$timestamp";
$details .= " domain=$domain" if defined $domain && $domain ne '';
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'domains', @args,
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'domains', @args,
"--remote=$remote", "--timestamp=$timestamp");
_log_activity($user, 'RESTORE_DOMAINS', $details,
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);
@@ -576,7 +761,7 @@ sub RESTORE_SSL {
my $details = "remote=$remote snapshot=$timestamp";
$details .= " domain=$domain" if defined $domain && $domain ne '';
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'ssl', @args,
my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'ssl', @args,
"--remote=$remote", "--timestamp=$timestamp");
_log_activity($user, 'RESTORE_SSL', $details,
$ok ? 'OK' : 'Error', $ok ? $stdout : $stderr);

View File

@@ -1,13 +0,0 @@
[
{
"type": "link",
"id": "gniza",
"name": "GNIZA Backups",
"group_id": "files",
"description": "Restore files, databases, email, and more from gniza backups",
"uri": "gniza/index.live.cgi",
"feature": "gniza_restore",
"order": 1,
"icon": "gniza/assets/gniza-cpanel-icon.png"
}
]

View File

Before

Width:  |  Height:  |  Size: 720 B

After

Width:  |  Height:  |  Size: 720 B

View File

Before

Width:  |  Height:  |  Size: 512 B

After

Width:  |  Height:  |  Size: 512 B

View File

Before

Width:  |  Height:  |  Size: 685 B

After

Width:  |  Height:  |  Size: 685 B

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
#!/usr/local/cpanel/3rdparty/bin/perl
# gniza cPanel Plugin — Step 1: Select Remote + Snapshot
# gniza4cp cPanel Plugin — Step 1: Select Remote + Snapshot
use strict;
use warnings;
@@ -16,23 +16,23 @@ BEGIN {
use Cpanel::LiveAPI ();
use Cpanel::AdminBin::Call ();
use GnizaCPanel::UI;
use Gniza4cpCPanel::UI;
my $cpanel = Cpanel::LiveAPI->new();
print "Content-Type: text/html\r\n\r\n";
print $cpanel->header('');
# Get allowed remotes via AdminBin
my $remotes_raw = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'LIST_ALLOWED_REMOTES') } // '';
my $remotes_raw = eval { Cpanel::AdminBin::Call::call('Gniza4cp', 'Restore', 'LIST_ALLOWED_REMOTES') } // '';
my @remotes = grep { $_ ne '' } split /\n/, $remotes_raw;
print GnizaCPanel::UI::page_header('GNIZA Backups');
print GnizaCPanel::UI::render_nav('index.live.cgi');
print GnizaCPanel::UI::render_flash();
print Gniza4cpCPanel::UI::page_header('GNIZA4CP Backups');
print Gniza4cpCPanel::UI::render_nav('index.live.cgi');
print Gniza4cpCPanel::UI::render_flash();
if (!@remotes) {
print qq{<div class="alert alert-info mb-4">No backup remotes are available for restore. Please contact your server administrator.</div>\n};
print GnizaCPanel::UI::page_footer();
print Gniza4cpCPanel::UI::page_footer();
print $cpanel->footer();
$cpanel->end();
exit;
@@ -44,10 +44,10 @@ print qq{<h2 class="card-title text-sm">Select Backup Source</h2>\n};
# Remote dropdown
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
print qq{ <label class="w-36 font-medium text-sm" for="remote">Remote</label>\n};
print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="remote" name="remote" onchange="gnizaLoadSnapshots()">\n};
print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="remote" name="remote" onchange="gniza4cpLoadSnapshots()">\n};
print qq{ <option value="">-- Select remote --</option>\n};
for my $r (@remotes) {
my $esc = GnizaCPanel::UI::esc($r);
my $esc = Gniza4cpCPanel::UI::esc($r);
print qq{ <option value="$esc">$esc</option>\n};
}
print qq{ </select>\n};
@@ -64,13 +64,13 @@ print qq{</div>\n};
print qq{</div>\n</div>\n};
print qq{<div class="flex items-center gap-2">\n};
print qq{ <button type="button" class="btn btn-primary btn-sm" id="next-btn" disabled onclick="gnizaGoNext()">Next</button>\n};
print qq{ <button type="button" class="btn btn-primary btn-sm" id="next-btn" disabled onclick="gniza4cpGoNext()">Next</button>\n};
print qq{</div>\n};
# JavaScript for snapshot loading and navigation
print <<'END_JS';
<script>
function gnizaLoadSnapshots() {
function gniza4cpLoadSnapshots() {
var remote = document.getElementById('remote').value;
var sel = document.getElementById('timestamp');
var btn = document.getElementById('next-btn');
@@ -141,7 +141,7 @@ function _populateSelect(sel, values) {
}
}
function gnizaGoNext() {
function gniza4cpGoNext() {
var remote = document.getElementById('remote').value;
var timestamp = document.getElementById('timestamp').value;
if (!remote || !timestamp) return;
@@ -154,6 +154,6 @@ function gnizaGoNext() {
</script>
END_JS
print GnizaCPanel::UI::page_footer();
print Gniza4cpCPanel::UI::page_footer();
print $cpanel->footer();
$cpanel->end();

View File

@@ -0,0 +1,13 @@
[
{
"type": "link",
"id": "gniza4cp",
"name": "GNIZA4CP Backups",
"group_id": "files",
"description": "Restore files, databases, email, and more from gniza4cp backups",
"uri": "gniza4cp/index.live.cgi",
"feature": "gniza4cp_restore",
"order": 1,
"icon": "gniza4cp/assets/gniza4cp-cpanel-icon.png"
}
]

View File

@@ -1,13 +1,13 @@
package GnizaCPanel::UI;
# Shared UI helpers for the gniza cPanel user restore plugin.
# Adapted from GnizaWHM::UI for cPanel context (runs as user, not root).
package Gniza4cpCPanel::UI;
# Shared UI helpers for the gniza4cp cPanel user restore plugin.
# Adapted from Gniza4cpWHM::UI for cPanel context (runs as user, not root).
use strict;
use warnings;
use Fcntl qw(:DEFAULT);
my $CSS_FILE = '/usr/local/cpanel/base/frontend/jupiter/gniza/assets/gniza-whm.css';
my $LOGO_FILE = '/usr/local/cpanel/base/frontend/jupiter/gniza/assets/gniza-logo.svg';
my $CSS_FILE = '/usr/local/cpanel/base/frontend/jupiter/gniza4cp/assets/gniza4cp-whm.css';
my $LOGO_FILE = '/usr/local/cpanel/base/frontend/jupiter/gniza4cp/assets/gniza4cp-logo.svg';
my $_logo_data_uri = '';
my @NAV_ITEMS = (
@@ -35,7 +35,7 @@ sub render_nav {
my $logo = '';
if ($_logo_data_uri) {
$logo = qq{<img src="$_logo_data_uri" alt="" style="height:3rem;width:auto">}
. qq{<span class="text-3xl font-bold leading-none">GNIZA <span class="text-secondary">Backup</span></span>};
. qq{<span class="text-3xl font-bold leading-none">GNIZA4CP <span class="text-secondary">Backup</span></span>};
}
my $menu_items = '';
for my $item (@NAV_ITEMS) {
@@ -98,7 +98,7 @@ sub _safe_read {
sub _flash_file {
my $user = get_current_user();
return "/tmp/.gniza-cpanel-flash-$user";
return "/tmp/.gniza4cp-cpanel-flash-$user";
}
sub set_flash {
@@ -138,7 +138,7 @@ my $_current_csrf_token;
sub _csrf_file {
my $user = get_current_user();
return "/tmp/.gniza-cpanel-csrf-$user";
return "/tmp/.gniza4cp-cpanel-csrf-$user";
}
sub generate_csrf_token {
@@ -200,14 +200,14 @@ sub verify_csrf_token {
sub csrf_hidden_field {
my $token = generate_csrf_token();
return qq{<input type="hidden" name="gniza_csrf" value="} . esc($token) . qq{">};
return qq{<input type="hidden" name="gniza4cp_csrf" value="} . esc($token) . qq{">};
}
# ── Page Wrappers ────────────────────────────────────────────
sub page_header {
my ($title) = @_;
$title = esc($title // 'gniza Restore');
$title = esc($title // 'gniza4cp Restore');
my $css = '';
if (open my $fh, '<', $CSS_FILE) {
local $/;
@@ -227,7 +227,7 @@ sub page_header {
}
return qq{<style>$css</style>\n}
. qq{<div data-theme="gniza" class="font-sans text-base" style="padding:20px 10px 10px 10px">\n};
. qq{<div data-theme="gniza4cp" class="font-sans text-base" style="padding:20px 10px 10px 10px">\n};
}
sub page_footer {
@@ -277,7 +277,7 @@ sub _scope_to_container {
$css =~ s/:where\(:root\)/\&/g;
$css =~ s/:root,\s*\[data-theme[^\]]*\]/\&/g;
$css =~ s/\[data-theme=light\]/\&/g;
$css =~ s/\[data-theme=gniza\]/\&/g;
$css =~ s/\[data-theme=gniza4cp\]/\&/g;
$css =~ s/:root:not\(span\)/\&/g;
$css =~ s/:root:has\(/\&:has(/g;
$css =~ s/:root\b/\&/g;
@@ -309,7 +309,7 @@ sub _scope_to_container {
$i++;
}
return join('', @top_level) . '[data-theme="gniza"]{' . $scoped . '}';
return join('', @top_level) . '[data-theme="gniza4cp"]{' . $scoped . '}';
}
sub render_errors {

View File

@@ -1,5 +1,5 @@
#!/usr/local/cpanel/3rdparty/bin/perl
# gniza cPanel Plugin — Activity Logs
# gniza4cp cPanel Plugin — Activity Logs
# Shows user-initiated restore actions and their results
use strict;
use warnings;
@@ -17,7 +17,7 @@ BEGIN {
use Cpanel::LiveAPI ();
use Cpanel::AdminBin::Call ();
use Cpanel::Form ();
use GnizaCPanel::UI;
use Gniza4cpCPanel::UI;
my $cpanel = Cpanel::LiveAPI->new();
END { $cpanel->end() if $cpanel }
@@ -37,15 +37,15 @@ exit;
sub show_list {
print "Content-Type: text/html\r\n\r\n";
print $cpanel->header('');
print GnizaCPanel::UI::page_header('GNIZA Activity Log');
print GnizaCPanel::UI::render_nav('logs.live.cgi');
print GnizaCPanel::UI::render_flash();
print Gniza4cpCPanel::UI::page_header('GNIZA4CP Activity Log');
print Gniza4cpCPanel::UI::render_nav('logs.live.cgi');
print Gniza4cpCPanel::UI::render_flash();
my $raw = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'LIST_LOGS') } // '';
my $raw = eval { Cpanel::AdminBin::Call::call('Gniza4cp', 'Restore', 'LIST_LOGS') } // '';
if ($raw =~ /^ERROR: (.*)/) {
print qq{<div class="alert alert-error mb-4">} . GnizaCPanel::UI::esc($1) . qq{</div>\n};
print GnizaCPanel::UI::page_footer();
print qq{<div class="alert alert-error mb-4">} . Gniza4cpCPanel::UI::esc($1) . qq{</div>\n};
print Gniza4cpCPanel::UI::page_footer();
print $cpanel->footer();
return;
}
@@ -65,7 +65,7 @@ sub show_list {
if (!@entries) {
print qq{<div class="alert alert-info mb-4">No restore activity yet. Actions you perform in the Restore section will appear here.</div>\n};
print GnizaCPanel::UI::page_footer();
print Gniza4cpCPanel::UI::page_footer();
print $cpanel->footer();
return;
}
@@ -88,11 +88,11 @@ sub show_list {
print qq{<tbody>\n};
for my $e (@page_entries) {
my $esc_date = GnizaCPanel::UI::esc($e->{date});
my $esc_action = GnizaCPanel::UI::esc($e->{action});
my $esc_details = GnizaCPanel::UI::esc($e->{details});
my $esc_status = GnizaCPanel::UI::esc($e->{status});
my $esc_idx = GnizaCPanel::UI::esc($e->{idx});
my $esc_date = Gniza4cpCPanel::UI::esc($e->{date});
my $esc_action = Gniza4cpCPanel::UI::esc($e->{action});
my $esc_details = Gniza4cpCPanel::UI::esc($e->{details});
my $esc_status = Gniza4cpCPanel::UI::esc($e->{status});
my $esc_idx = Gniza4cpCPanel::UI::esc($e->{idx});
my $status_badge = $e->{status} eq 'Error' ? 'badge-error' : 'badge-success';
my $href = 'logs.live.cgi?entry=' . _uri_escape($e->{idx});
@@ -123,7 +123,7 @@ sub show_list {
print qq{</div>\n};
}
print GnizaCPanel::UI::page_footer();
print Gniza4cpCPanel::UI::page_footer();
print $cpanel->footer();
}
@@ -134,24 +134,24 @@ sub show_entry {
print "Content-Type: text/html\r\n\r\n";
print $cpanel->header('');
print GnizaCPanel::UI::page_header('GNIZA Activity Detail');
print GnizaCPanel::UI::render_nav('logs.live.cgi');
print Gniza4cpCPanel::UI::page_header('GNIZA4CP Activity Detail');
print Gniza4cpCPanel::UI::render_nav('logs.live.cgi');
# Validate entry index (numeric only)
unless ($entry_idx =~ /^[0-9]+$/) {
print qq{<div class="alert alert-error mb-4">Invalid entry.</div>\n};
print qq{<p><a href="logs.live.cgi" class="link">&larr; Back to activity log</a></p>\n};
print GnizaCPanel::UI::page_footer();
print Gniza4cpCPanel::UI::page_footer();
print $cpanel->footer();
return;
}
my $content = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'GET_LOG', $entry_idx) } // '';
my $content = eval { Cpanel::AdminBin::Call::call('Gniza4cp', 'Restore', 'GET_LOG', $entry_idx) } // '';
if ($content =~ /^ERROR: (.*)/) {
print qq{<div class="alert alert-error mb-4">} . GnizaCPanel::UI::esc($1) . qq{</div>\n};
print qq{<div class="alert alert-error mb-4">} . Gniza4cpCPanel::UI::esc($1) . qq{</div>\n};
print qq{<p><a href="logs.live.cgi" class="link">&larr; Back to activity log</a></p>\n};
print GnizaCPanel::UI::page_footer();
print Gniza4cpCPanel::UI::page_footer();
print $cpanel->footer();
return;
}
@@ -180,11 +180,11 @@ sub show_entry {
print qq{<div class="card bg-base-200 shadow-sm border border-base-300 mb-4">\n};
print qq{<div class="card-body py-3 px-4">\n};
print qq{<div class="flex flex-wrap gap-4 items-center text-sm">\n};
print qq{ <span><strong>Date:</strong> } . GnizaCPanel::UI::esc($date) . qq{</span>\n};
print qq{ <span><strong>Action:</strong> <span class="badge badge-info badge-sm">} . GnizaCPanel::UI::esc($action) . qq{</span></span>\n};
print qq{ <span><strong>Status:</strong> <span class="badge $status_badge badge-sm">} . GnizaCPanel::UI::esc($status) . qq{</span></span>\n};
print qq{ <span><strong>Date:</strong> } . Gniza4cpCPanel::UI::esc($date) . qq{</span>\n};
print qq{ <span><strong>Action:</strong> <span class="badge badge-info badge-sm">} . Gniza4cpCPanel::UI::esc($action) . qq{</span></span>\n};
print qq{ <span><strong>Status:</strong> <span class="badge $status_badge badge-sm">} . Gniza4cpCPanel::UI::esc($status) . qq{</span></span>\n};
print qq{</div>\n};
print qq{<div class="text-sm mt-2"><strong>Details:</strong> } . GnizaCPanel::UI::esc($details) . qq{</div>\n};
print qq{<div class="text-sm mt-2"><strong>Details:</strong> } . Gniza4cpCPanel::UI::esc($details) . qq{</div>\n};
print qq{</div>\n</div>\n};
# Output section
@@ -192,7 +192,7 @@ sub show_entry {
print qq{<h3 class="text-sm font-bold mb-2">Command Output</h3>\n};
print qq{<pre class="bg-base-100 border border-base-300 rounded-lg p-4 text-sm font-mono overflow-x-auto leading-relaxed">};
for my $line (@output_lines) {
my $esc = GnizaCPanel::UI::esc($line);
my $esc = Gniza4cpCPanel::UI::esc($line);
if ($line =~ /\[ERROR\]/) {
print qq{<span class="text-error font-bold">$esc</span>\n};
} elsif ($line =~ /\[WARN\]/) {
@@ -208,7 +208,7 @@ sub show_entry {
print qq{<div class="alert alert-info">No output recorded for this action.</div>\n};
}
print GnizaCPanel::UI::page_footer();
print Gniza4cpCPanel::UI::page_footer();
print $cpanel->footer();
}

View File

@@ -1,5 +1,5 @@
#!/usr/local/cpanel/3rdparty/bin/perl
# gniza cPanel Plugin — Restore Workflow
# gniza4cp cPanel Plugin — Restore Workflow
# Multi-step restore with dynamic dropdowns via AdminBin
use strict;
use warnings;
@@ -17,7 +17,7 @@ BEGIN {
use Cpanel::LiveAPI ();
use Cpanel::AdminBin::Call ();
use Cpanel::Form ();
use GnizaCPanel::UI;
use Gniza4cpCPanel::UI;
my $cpanel = Cpanel::LiveAPI->new();
END { $cpanel->end() if $cpanel }
@@ -66,7 +66,7 @@ sub _json_escape {
sub _adminbin_call {
my ($action, @args) = @_;
my $result = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', $action, @args) };
my $result = eval { Cpanel::AdminBin::Call::call('Gniza4cp', 'Restore', $action, @args) };
if ($@) {
return (0, '', "AdminBin call failed: $@");
}
@@ -176,7 +176,7 @@ sub handle_step2 {
my $timestamp = $form->{'timestamp'} // '';
if ($remote eq '' || $timestamp eq '') {
GnizaCPanel::UI::set_flash('error', 'Remote and snapshot are required.');
Gniza4cpCPanel::UI::set_flash('error', 'Remote and snapshot are required.');
print "Status: 302 Found\r\n";
print "Location: index.live.cgi\r\n\r\n";
exit;
@@ -196,12 +196,12 @@ sub handle_step2 {
print "Content-Type: text/html\r\n\r\n";
print $cpanel->header('');
print GnizaCPanel::UI::page_header('Restore Options');
print GnizaCPanel::UI::render_nav('restore.live.cgi');
print GnizaCPanel::UI::render_flash();
print Gniza4cpCPanel::UI::page_header('Restore Options');
print Gniza4cpCPanel::UI::render_nav('restore.live.cgi');
print Gniza4cpCPanel::UI::render_flash();
my $esc_remote = GnizaCPanel::UI::esc($remote);
my $esc_timestamp = GnizaCPanel::UI::esc($timestamp);
my $esc_remote = Gniza4cpCPanel::UI::esc($remote);
my $esc_timestamp = Gniza4cpCPanel::UI::esc($timestamp);
print qq{<form method="GET" action="restore.live.cgi">\n};
print qq{<input type="hidden" name="step" value="3">\n};
@@ -215,9 +215,9 @@ sub handle_step2 {
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
print qq{ <label class="w-36 font-medium text-sm" for="timestamp">Snapshot</label>\n};
if (@snapshots) {
print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="timestamp" name="timestamp" required onchange="gnizaSnapshotChanged()">\n};
print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="timestamp" name="timestamp" required onchange="gniza4cpSnapshotChanged()">\n};
for my $snap (sort { $b cmp $a } @snapshots) {
my $esc = GnizaCPanel::UI::esc($snap);
my $esc = Gniza4cpCPanel::UI::esc($snap);
my $sel = ($snap eq $timestamp) ? ' selected' : '';
print qq{ <option value="$esc"$sel>$esc</option>\n};
}
@@ -231,8 +231,8 @@ sub handle_step2 {
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
print qq{ <label class="w-36 font-medium text-sm whitespace-nowrap">Restore Mode</label>\n};
print qq{ <div class="join inline-flex items-stretch">\n};
print qq{ <input type="radio" name="restore_mode" class="join-item btn btn-sm m-0" aria-label="Full Account" value="full" checked onchange="gnizaModeChanged()">\n};
print qq{ <input type="radio" name="restore_mode" class="join-item btn btn-sm m-0" aria-label="Selective" value="selective" onchange="gnizaModeChanged()">\n};
print qq{ <input type="radio" name="restore_mode" class="join-item btn btn-sm m-0" aria-label="Full Account" value="full" checked onchange="gniza4cpModeChanged()">\n};
print qq{ <input type="radio" name="restore_mode" class="join-item btn btn-sm m-0" aria-label="Selective" value="selective" onchange="gniza4cpModeChanged()">\n};
print qq{ </div>\n};
print qq{</div>\n};
@@ -242,8 +242,8 @@ sub handle_step2 {
print qq{ <h3 class="card-title text-sm">Directories and Files to Exclude</h3>\n};
print qq{ <div class="flex items-center gap-2">\n};
print qq{ <input type="text" class="input input-bordered input-sm flex-1 max-w-xs" id="exclude-input" placeholder="e.g. public_html/cache">\n};
print qq{ <button type="button" class="btn btn-warning btn-sm" onclick="gnizaAddExclude()">Add Path</button>\n};
print qq{ <button type="button" class="btn btn-warning btn-sm" onclick="gnizaOpenExcludeModal()">Insert Multiple</button>\n};
print qq{ <button type="button" class="btn btn-warning btn-sm" onclick="gniza4cpAddExclude()">Add Path</button>\n};
print qq{ <button type="button" class="btn btn-warning btn-sm" onclick="gniza4cpOpenExcludeModal()">Insert Multiple</button>\n};
print qq{ </div>\n};
print qq{ <p class="text-xs text-base-content/60">Exclude files and directories from restoration</p>\n};
print qq{ <div id="exclude-tags" class="flex flex-wrap gap-1 mt-1"></div>\n};
@@ -259,7 +259,7 @@ sub handle_step2 {
print qq{ <p class="text-xs text-base-content/60 mt-1">* Separated by new line</p>\n};
print qq{ <div class="modal-action">\n};
print qq{ <button type="button" class="btn btn-sm" onclick="document.getElementById('exclude-modal').close()">Cancel</button>\n};
print qq{ <button type="button" class="btn btn-warning btn-sm" onclick="gnizaExcludeModalOk()">OK</button>\n};
print qq{ <button type="button" class="btn btn-warning btn-sm" onclick="gniza4cpExcludeModalOk()">OK</button>\n};
print qq{ </div>\n};
print qq{</div>\n};
print qq{<div class="modal-backdrop" onclick="this.closest('dialog').close()"><button type="button">close</button></div>\n};
@@ -285,7 +285,7 @@ sub handle_step2 {
print qq{ <label class="w-36 font-medium text-sm">Restore Types</label>\n};
print qq{ <div class="flex flex-wrap gap-1">\n};
for my $t (@selective_types) {
print qq{ <input type="checkbox" class="btn btn-sm" aria-label="$t->[1]" name="type_$t->[0]" value="1" onchange="gnizaTypesChanged()">\n};
print qq{ <input type="checkbox" class="btn btn-sm" aria-label="$t->[1]" name="type_$t->[0]" value="1" onchange="gniza4cpTypesChanged()">\n};
}
print qq{ </div>\n};
print qq{</div>\n};
@@ -299,7 +299,7 @@ sub handle_step2 {
print qq{ <label class="font-medium text-sm" for="path">Path</label>\n};
print qq{ <div class="flex items-center gap-2 w-full max-w-xs">\n};
print qq{ <input type="text" class="input input-bordered input-sm flex-1" id="path" name="path" placeholder="e.g. public_html/index.html">\n};
print qq{ <button type="button" class="btn btn-secondary btn-sm" onclick="gnizaOpenFileBrowser()">Browse</button>\n};
print qq{ <button type="button" class="btn btn-secondary btn-sm" onclick="gniza4cpOpenFileBrowser()">Browse</button>\n};
print qq{ </div>\n};
print qq{ </div>\n};
print qq{ <p class="text-xs text-base-content/60">Leave empty to restore all files.</p>\n};
@@ -391,7 +391,7 @@ sub handle_step2 {
print qq{ <table class="table table-zebra w-full"><tbody id="fb-tbody"></tbody></table>\n};
print qq{ </div>\n};
print qq{ <div class="modal-action">\n};
print qq{ <button type="button" id="fb-select-btn" class="btn btn-primary btn-sm" disabled onclick="gnizaSelectPath()">Select</button>\n};
print qq{ <button type="button" id="fb-select-btn" class="btn btn-primary btn-sm" disabled onclick="gniza4cpSelectPath()">Select</button>\n};
print qq{ <button type="button" class="btn btn-info btn-sm" onclick="document.getElementById('fb-modal').close()">Cancel</button>\n};
print qq{ </div>\n};
print qq{</div>\n};
@@ -414,7 +414,7 @@ sub handle_step2 {
# JavaScript for dynamic dropdowns and interactive elements
_print_step2_js($esc_remote);
print GnizaCPanel::UI::page_footer();
print Gniza4cpCPanel::UI::page_footer();
print $cpanel->footer();
}
@@ -422,24 +422,24 @@ sub _print_step2_js {
my ($esc_remote) = @_;
print <<"END_JS";
<script>
var gnizaCache = {};
var gnizaRemote = '$esc_remote';
var gniza4cpCache = {};
var gniza4cpRemote = '$esc_remote';
var fbCache = {};
var fbSelected = '';
function gnizaSnapshotChanged() {
gnizaCache = {};
function gniza4cpSnapshotChanged() {
gniza4cpCache = {};
fbCache = {};
gnizaModeChanged();
gniza4cpModeChanged();
}
function gnizaModeChanged() {
function gniza4cpModeChanged() {
var mode = document.querySelector('input[name="restore_mode"]:checked').value;
var selective = mode === 'selective';
document.getElementById('selective-panel').hidden = !selective;
document.getElementById('type_account_hidden').disabled = selective;
if (selective) {
gnizaTypesChanged();
gniza4cpTypesChanged();
} else {
var panels = ['field-path','field-dbname','field-email','field-dbusers','field-cron','field-domains','field-ssl'];
for (var i = 0; i < panels.length; i++) {
@@ -448,7 +448,7 @@ function gnizaModeChanged() {
}
}
function gnizaTypesChanged() {
function gniza4cpTypesChanged() {
var types = {
files: 'field-path',
database: 'field-dbname',
@@ -463,20 +463,20 @@ function gnizaTypesChanged() {
document.getElementById(types[t]).hidden = !(el && el.checked);
}
if (document.querySelector('input[name="type_database"]').checked) { gnizaLoadOptions('database', 'dbname-list', 'dbnames'); }
if (document.querySelector('input[name="type_mailbox"]').checked) { gnizaLoadOptions('mailbox', 'email-list', 'emails'); }
if (document.querySelector('input[name="type_dbusers"]').checked) { gnizaLoadOptions('dbusers', 'dbusers-list', 'dbuser_names'); }
if (document.querySelector('input[name="type_cron"]').checked) { gnizaLoadPreview('cron', 'cron-list'); }
if (document.querySelector('input[name="type_domains"]').checked) { gnizaLoadOptions('domains', 'domains-list', 'domain_names'); }
if (document.querySelector('input[name="type_ssl"]').checked) { gnizaLoadOptions('ssl', 'ssl-list', 'ssl_names'); }
if (document.querySelector('input[name="type_database"]').checked) { gniza4cpLoadOptions('database', 'dbname-list', 'dbnames'); }
if (document.querySelector('input[name="type_mailbox"]').checked) { gniza4cpLoadOptions('mailbox', 'email-list', 'emails'); }
if (document.querySelector('input[name="type_dbusers"]').checked) { gniza4cpLoadOptions('dbusers', 'dbusers-list', 'dbuser_names'); }
if (document.querySelector('input[name="type_cron"]').checked) { gniza4cpLoadPreview('cron', 'cron-list'); }
if (document.querySelector('input[name="type_domains"]').checked) { gniza4cpLoadOptions('domains', 'domains-list', 'domain_names'); }
if (document.querySelector('input[name="type_ssl"]').checked) { gniza4cpLoadOptions('ssl', 'ssl-list', 'ssl_names'); }
}
function gnizaLoadOptions(type, containerId, hiddenId) {
function gniza4cpLoadOptions(type, containerId, hiddenId) {
var ts = document.getElementById('timestamp').value;
var cacheKey = type + ':' + ts;
if (gnizaCache[cacheKey]) {
gnizaPopulateChecklist(containerId, hiddenId, gnizaCache[cacheKey]);
if (gniza4cpCache[cacheKey]) {
gniza4cpPopulateChecklist(containerId, hiddenId, gniza4cpCache[cacheKey]);
return;
}
@@ -492,7 +492,7 @@ function gnizaLoadOptions(type, containerId, hiddenId) {
document.getElementById(hiddenId).value = '';
var url = 'restore.live.cgi?step=fetch_options'
+ '&remote=' + encodeURIComponent(gnizaRemote)
+ '&remote=' + encodeURIComponent(gniza4cpRemote)
+ '&timestamp=' + encodeURIComponent(ts)
+ '&type=' + encodeURIComponent(type);
@@ -506,8 +506,8 @@ function gnizaLoadOptions(type, containerId, hiddenId) {
if (data.error) {
container.textContent = 'Error: ' + data.error;
} else {
gnizaCache[cacheKey] = data.options;
gnizaPopulateChecklist(containerId, hiddenId, data.options);
gniza4cpCache[cacheKey] = data.options;
gniza4cpPopulateChecklist(containerId, hiddenId, data.options);
}
} catch(e) {
container.textContent = 'Failed to parse response';
@@ -519,7 +519,7 @@ function gnizaLoadOptions(type, containerId, hiddenId) {
xhr.send();
}
function gnizaPopulateChecklist(containerId, hiddenId, options) {
function gniza4cpPopulateChecklist(containerId, hiddenId, options) {
var container = document.getElementById(containerId);
var hidden = document.getElementById(hiddenId);
hidden.value = '';
@@ -540,7 +540,7 @@ function gnizaPopulateChecklist(containerId, hiddenId, options) {
allCb.type = 'checkbox';
allCb.className = 'checkbox checkbox-sm';
allCb.setAttribute('data-all', '1');
allCb.onchange = function() { gnizaToggleAll(containerId, hiddenId, this.checked); };
allCb.onchange = function() { gniza4cpToggleAll(containerId, hiddenId, this.checked); };
allRow.appendChild(allCb);
var allSpan = document.createElement('span');
allSpan.className = 'text-sm font-semibold';
@@ -557,7 +557,7 @@ function gnizaPopulateChecklist(containerId, hiddenId, options) {
cb.className = 'checkbox checkbox-sm';
cb.value = options[i];
cb.setAttribute('data-item', '1');
cb.onchange = (function(cId, hId) { return function() { gnizaSyncHidden(cId, hId); }; })(containerId, hiddenId);
cb.onchange = (function(cId, hId) { return function() { gniza4cpSyncHidden(cId, hId); }; })(containerId, hiddenId);
row.appendChild(cb);
var span = document.createElement('span');
span.className = 'text-sm';
@@ -567,7 +567,7 @@ function gnizaPopulateChecklist(containerId, hiddenId, options) {
}
}
function gnizaToggleAll(containerId, hiddenId, checked) {
function gniza4cpToggleAll(containerId, hiddenId, checked) {
var container = document.getElementById(containerId);
var hidden = document.getElementById(hiddenId);
var items = container.querySelectorAll('input[data-item]');
@@ -578,7 +578,7 @@ function gnizaToggleAll(containerId, hiddenId, checked) {
hidden.value = checked ? '__ALL__' : '';
}
function gnizaSyncHidden(containerId, hiddenId) {
function gniza4cpSyncHidden(containerId, hiddenId) {
var container = document.getElementById(containerId);
var hidden = document.getElementById(hiddenId);
var items = container.querySelectorAll('input[data-item]:checked');
@@ -589,12 +589,12 @@ function gnizaSyncHidden(containerId, hiddenId) {
hidden.value = vals.join(',');
}
function gnizaLoadPreview(type, containerId) {
function gniza4cpLoadPreview(type, containerId) {
var ts = document.getElementById('timestamp').value;
var cacheKey = type + ':' + ts;
if (gnizaCache[cacheKey]) {
gnizaPopulatePreview(containerId, gnizaCache[cacheKey], type);
if (gniza4cpCache[cacheKey]) {
gniza4cpPopulatePreview(containerId, gniza4cpCache[cacheKey], type);
return;
}
@@ -609,7 +609,7 @@ function gnizaLoadPreview(type, containerId) {
container.appendChild(loadSpan);
var url = 'restore.live.cgi?step=fetch_options'
+ '&remote=' + encodeURIComponent(gnizaRemote)
+ '&remote=' + encodeURIComponent(gniza4cpRemote)
+ '&timestamp=' + encodeURIComponent(ts)
+ '&type=' + encodeURIComponent(type);
@@ -623,8 +623,8 @@ function gnizaLoadPreview(type, containerId) {
if (data.error) {
container.textContent = 'Error: ' + data.error;
} else {
gnizaCache[cacheKey] = data.options;
gnizaPopulatePreview(containerId, data.options, type);
gniza4cpCache[cacheKey] = data.options;
gniza4cpPopulatePreview(containerId, data.options, type);
}
} catch(e) {
container.textContent = 'Failed to parse response';
@@ -636,7 +636,7 @@ function gnizaLoadPreview(type, containerId) {
xhr.send();
}
function gnizaPopulatePreview(containerId, options, type) {
function gniza4cpPopulatePreview(containerId, options, type) {
var container = document.getElementById(containerId);
container.textContent = '';
if (!options || options.length === 0) {
@@ -660,16 +660,16 @@ function gnizaPopulatePreview(containerId, options, type) {
}
}
function gnizaAddExclude() {
function gniza4cpAddExclude() {
var input = document.getElementById('exclude-input');
var val = input.value.trim();
if (!val) return;
gnizaAddExcludeTag(val);
gniza4cpAddExcludeTag(val);
input.value = '';
gnizaUpdateExcludeField();
gniza4cpUpdateExcludeField();
}
function gnizaAddExcludeTag(text) {
function gniza4cpAddExcludeTag(text) {
var container = document.getElementById('exclude-tags');
var existing = container.querySelectorAll('.badge span');
for (var i = 0; i < existing.length; i++) {
@@ -684,12 +684,12 @@ function gnizaAddExcludeTag(text) {
btn.type = 'button';
btn.className = 'btn btn-xs btn-ghost btn-circle';
btn.textContent = '\\u2715';
btn.onclick = function() { badge.remove(); gnizaUpdateExcludeField(); };
btn.onclick = function() { badge.remove(); gniza4cpUpdateExcludeField(); };
badge.appendChild(btn);
container.appendChild(badge);
}
function gnizaUpdateExcludeField() {
function gniza4cpUpdateExcludeField() {
var tags = document.getElementById('exclude-tags').querySelectorAll('.badge span');
var vals = [];
for (var i = 0; i < tags.length; i++) {
@@ -698,35 +698,35 @@ function gnizaUpdateExcludeField() {
document.getElementById('exclude_paths').value = vals.join(',');
}
function gnizaOpenExcludeModal() {
function gniza4cpOpenExcludeModal() {
document.getElementById('exclude-textarea').value = '';
document.getElementById('exclude-modal').showModal();
}
function gnizaExcludeModalOk() {
function gniza4cpExcludeModalOk() {
var text = document.getElementById('exclude-textarea').value;
var lines = text.split('\\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line) gnizaAddExcludeTag(line);
if (line) gniza4cpAddExcludeTag(line);
}
gnizaUpdateExcludeField();
gniza4cpUpdateExcludeField();
document.getElementById('exclude-modal').close();
}
function gnizaOpenFileBrowser() {
function gniza4cpOpenFileBrowser() {
fbSelected = '';
document.getElementById('fb-select-btn').disabled = true;
document.getElementById('fb-modal').showModal();
gnizaLoadDir('');
gniza4cpLoadDir('');
}
function gnizaLoadDir(path) {
function gniza4cpLoadDir(path) {
var ts = document.getElementById('timestamp').value;
var cacheKey = 'fb:' + ts + ':' + path;
if (fbCache[cacheKey]) {
gnizaRenderFileList(path, fbCache[cacheKey]);
gniza4cpRenderFileList(path, fbCache[cacheKey]);
return;
}
@@ -735,7 +735,7 @@ function gnizaLoadDir(path) {
document.getElementById('fb-tbody').textContent = '';
var url = 'restore.live.cgi?step=fetch_options'
+ '&remote=' + encodeURIComponent(gnizaRemote)
+ '&remote=' + encodeURIComponent(gniza4cpRemote)
+ '&timestamp=' + encodeURIComponent(ts)
+ '&type=files'
+ (path ? '&path=' + encodeURIComponent(path) : '');
@@ -753,7 +753,7 @@ function gnizaLoadDir(path) {
document.getElementById('fb-error').hidden = false;
} else {
fbCache[cacheKey] = data.options;
gnizaRenderFileList(path, data.options);
gniza4cpRenderFileList(path, data.options);
}
} catch(e) {
document.getElementById('fb-error').textContent = 'Failed to parse response';
@@ -767,13 +767,13 @@ function gnizaLoadDir(path) {
xhr.send();
}
function gnizaRenderBreadcrumbs(path) {
function gniza4cpRenderBreadcrumbs(path) {
var ul = document.createElement('ul');
var li = document.createElement('li');
var a = document.createElement('a');
a.textContent = 'homedir';
a.href = '#';
a.onclick = function(e) { e.preventDefault(); gnizaLoadDir(''); };
a.onclick = function(e) { e.preventDefault(); gniza4cpLoadDir(''); };
li.appendChild(a);
ul.appendChild(li);
@@ -787,7 +787,7 @@ function gnizaRenderBreadcrumbs(path) {
a = document.createElement('a');
a.textContent = parts[i];
a.href = '#';
(function(p) { a.onclick = function(e) { e.preventDefault(); gnizaLoadDir(p); }; })(built);
(function(p) { a.onclick = function(e) { e.preventDefault(); gniza4cpLoadDir(p); }; })(built);
li.appendChild(a);
} else {
li.textContent = parts[i];
@@ -801,8 +801,8 @@ function gnizaRenderBreadcrumbs(path) {
bc.appendChild(ul);
}
function gnizaRenderFileList(currentPath, entries) {
gnizaRenderBreadcrumbs(currentPath);
function gniza4cpRenderFileList(currentPath, entries) {
gniza4cpRenderBreadcrumbs(currentPath);
fbSelected = '';
document.getElementById('fb-select-btn').disabled = true;
@@ -835,9 +835,9 @@ function gnizaRenderFileList(currentPath, entries) {
tr.appendChild(td);
(function(row, path, dir) {
row.onclick = function() { gnizaHighlight(row, path); };
row.onclick = function() { gniza4cpHighlight(row, path); };
if (dir) {
row.ondblclick = function() { gnizaLoadDir(path.replace(/\\/\$/, '')); };
row.ondblclick = function() { gniza4cpLoadDir(path.replace(/\\/\$/, '')); };
}
})(tr, fullPath, isDir);
@@ -845,7 +845,7 @@ function gnizaRenderFileList(currentPath, entries) {
}
}
function gnizaHighlight(row, path) {
function gniza4cpHighlight(row, path) {
var rows = document.getElementById('fb-tbody').querySelectorAll('tr');
for (var i = 0; i < rows.length; i++) {
rows[i].classList.remove('bg-primary/10');
@@ -855,14 +855,14 @@ function gnizaHighlight(row, path) {
document.getElementById('fb-select-btn').disabled = false;
}
function gnizaSelectPath() {
function gniza4cpSelectPath() {
if (fbSelected) {
document.getElementById('path').value = fbSelected;
}
document.getElementById('fb-modal').close();
}
gnizaModeChanged();
gniza4cpModeChanged();
</script>
END_JS
}
@@ -888,14 +888,14 @@ sub handle_step3 {
}
if ($remote eq '' || $timestamp eq '') {
GnizaCPanel::UI::set_flash('error', 'Remote and snapshot are required.');
Gniza4cpCPanel::UI::set_flash('error', 'Remote and snapshot are required.');
print "Status: 302 Found\r\n";
print "Location: index.live.cgi\r\n\r\n";
exit;
}
unless (@selected_types) {
GnizaCPanel::UI::set_flash('error', 'Please select at least one restore type.');
Gniza4cpCPanel::UI::set_flash('error', 'Please select at least one restore type.');
print "Status: 302 Found\r\n";
print "Location: restore.live.cgi?step=2&remote=" . _uri_escape($remote) . "&timestamp=" . _uri_escape($timestamp) . "\r\n\r\n";
exit;
@@ -903,15 +903,15 @@ sub handle_step3 {
print "Content-Type: text/html\r\n\r\n";
print $cpanel->header('');
print GnizaCPanel::UI::page_header('Restore: Confirm');
print GnizaCPanel::UI::render_nav('restore.live.cgi');
print GnizaCPanel::UI::render_flash();
print Gniza4cpCPanel::UI::page_header('Restore: Confirm');
print Gniza4cpCPanel::UI::render_nav('restore.live.cgi');
print Gniza4cpCPanel::UI::render_flash();
my $esc_remote = GnizaCPanel::UI::esc($remote);
my $esc_timestamp = GnizaCPanel::UI::esc($timestamp);
my $user = GnizaCPanel::UI::esc(GnizaCPanel::UI::get_current_user());
my $esc_remote = Gniza4cpCPanel::UI::esc($remote);
my $esc_timestamp = Gniza4cpCPanel::UI::esc($timestamp);
my $user = Gniza4cpCPanel::UI::esc(Gniza4cpCPanel::UI::get_current_user());
my $types_display = join(', ', map { GnizaCPanel::UI::esc($TYPE_LABELS{$_} // $_) } @selected_types);
my $types_display = join(', ', map { Gniza4cpCPanel::UI::esc($TYPE_LABELS{$_} // $_) } @selected_types);
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
print qq{<h2 class="card-title text-sm">Step 3: Confirm Restore</h2>\n};
@@ -923,37 +923,37 @@ sub handle_step3 {
# Show sub-field details for applicable types
if (grep { $_ eq 'files' } @selected_types) {
my $path_display = $path ne '' ? GnizaCPanel::UI::esc($path) : 'All files';
my $path_display = $path ne '' ? Gniza4cpCPanel::UI::esc($path) : 'All files';
print qq{<tr><td class="font-medium">Path</td><td>$path_display</td></tr>\n};
}
if (grep { $_ eq 'database' } @selected_types) {
my $db_display = ($dbnames eq '' || $dbnames eq '__ALL__') ? 'All databases' : GnizaCPanel::UI::esc($dbnames);
my $db_display = ($dbnames eq '' || $dbnames eq '__ALL__') ? 'All databases' : Gniza4cpCPanel::UI::esc($dbnames);
$db_display =~ s/,/, /g;
print qq{<tr><td class="font-medium">Database</td><td>$db_display</td></tr>\n};
}
if (grep { $_ eq 'dbusers' } @selected_types) {
my $dbu_display = ($dbuser_names eq '' || $dbuser_names eq '__ALL__') ? 'All database users' : GnizaCPanel::UI::esc($dbuser_names);
my $dbu_display = ($dbuser_names eq '' || $dbuser_names eq '__ALL__') ? 'All database users' : Gniza4cpCPanel::UI::esc($dbuser_names);
$dbu_display =~ s/,/, /g;
print qq{<tr><td class="font-medium">Database Users</td><td>$dbu_display</td></tr>\n};
}
if (grep { $_ eq 'mailbox' } @selected_types) {
my $mb_display = ($emails eq '' || $emails eq '__ALL__') ? 'All mailboxes' : GnizaCPanel::UI::esc($emails);
my $mb_display = ($emails eq '' || $emails eq '__ALL__') ? 'All mailboxes' : Gniza4cpCPanel::UI::esc($emails);
$mb_display =~ s/,/, /g;
print qq{<tr><td class="font-medium">Mailbox</td><td>$mb_display</td></tr>\n};
}
if (grep { $_ eq 'domains' } @selected_types) {
my $dom_display = ($domain_names eq '' || $domain_names eq '__ALL__') ? 'All domains' : GnizaCPanel::UI::esc($domain_names);
my $dom_display = ($domain_names eq '' || $domain_names eq '__ALL__') ? 'All domains' : Gniza4cpCPanel::UI::esc($domain_names);
$dom_display =~ s/,/, /g;
print qq{<tr><td class="font-medium">Domains</td><td>$dom_display</td></tr>\n};
}
if (grep { $_ eq 'ssl' } @selected_types) {
my $ssl_display = ($ssl_names eq '' || $ssl_names eq '__ALL__') ? 'All certificates' : GnizaCPanel::UI::esc($ssl_names);
my $ssl_display = ($ssl_names eq '' || $ssl_names eq '__ALL__') ? 'All certificates' : Gniza4cpCPanel::UI::esc($ssl_names);
$ssl_display =~ s/,/, /g;
print qq{<tr><td class="font-medium">SSL</td><td>$ssl_display</td></tr>\n};
}
if ($exclude_paths ne '') {
my $exclude_display = GnizaCPanel::UI::esc($exclude_paths);
my $exclude_display = Gniza4cpCPanel::UI::esc($exclude_paths);
$exclude_display =~ s/,/, /g;
print qq{<tr><td class="font-medium">Exclude</td><td>$exclude_display</td></tr>\n};
}
@@ -968,14 +968,14 @@ sub handle_step3 {
for my $t (@selected_types) {
print qq{<input type="hidden" name="type_$t" value="1">\n};
}
print qq{<input type="hidden" name="path" value="} . GnizaCPanel::UI::esc($path) . qq{">\n};
print qq{<input type="hidden" name="dbnames" value="} . GnizaCPanel::UI::esc($dbnames) . qq{">\n};
print qq{<input type="hidden" name="dbuser_names" value="} . GnizaCPanel::UI::esc($dbuser_names) . qq{">\n};
print qq{<input type="hidden" name="emails" value="} . GnizaCPanel::UI::esc($emails) . qq{">\n};
print qq{<input type="hidden" name="domain_names" value="} . GnizaCPanel::UI::esc($domain_names) . qq{">\n};
print qq{<input type="hidden" name="ssl_names" value="} . GnizaCPanel::UI::esc($ssl_names) . qq{">\n};
print qq{<input type="hidden" name="exclude_paths" value="} . GnizaCPanel::UI::esc($exclude_paths) . qq{">\n};
print GnizaCPanel::UI::csrf_hidden_field();
print qq{<input type="hidden" name="path" value="} . Gniza4cpCPanel::UI::esc($path) . qq{">\n};
print qq{<input type="hidden" name="dbnames" value="} . Gniza4cpCPanel::UI::esc($dbnames) . qq{">\n};
print qq{<input type="hidden" name="dbuser_names" value="} . Gniza4cpCPanel::UI::esc($dbuser_names) . qq{">\n};
print qq{<input type="hidden" name="emails" value="} . Gniza4cpCPanel::UI::esc($emails) . qq{">\n};
print qq{<input type="hidden" name="domain_names" value="} . Gniza4cpCPanel::UI::esc($domain_names) . qq{">\n};
print qq{<input type="hidden" name="ssl_names" value="} . Gniza4cpCPanel::UI::esc($ssl_names) . qq{">\n};
print qq{<input type="hidden" name="exclude_paths" value="} . Gniza4cpCPanel::UI::esc($exclude_paths) . qq{">\n};
print Gniza4cpCPanel::UI::csrf_hidden_field();
print qq{<div class="flex items-center gap-2">\n};
print qq{ <button type="submit" class="btn btn-error btn-sm" onclick="return confirm('Are you sure? This may overwrite existing data.')">Execute Restore</button>\n};
@@ -983,15 +983,15 @@ sub handle_step3 {
print qq{</div>\n};
print qq{</form>\n};
print GnizaCPanel::UI::page_footer();
print Gniza4cpCPanel::UI::page_footer();
print $cpanel->footer();
}
# ── Step 4: Execute ───────────────────────────────────────────
sub handle_step4 {
unless ($method eq 'POST' && GnizaCPanel::UI::verify_csrf_token($form->{'gniza_csrf'})) {
GnizaCPanel::UI::set_flash('error', 'Invalid or expired form token.');
unless ($method eq 'POST' && Gniza4cpCPanel::UI::verify_csrf_token($form->{'gniza4cp_csrf'})) {
Gniza4cpCPanel::UI::set_flash('error', 'Invalid or expired form token.');
print "Status: 302 Found\r\n";
print "Location: index.live.cgi\r\n\r\n";
exit;
@@ -1015,127 +1015,52 @@ sub handle_step4 {
}
unless (@selected_types) {
GnizaCPanel::UI::set_flash('error', 'No restore types selected.');
Gniza4cpCPanel::UI::set_flash('error', 'No restore types selected.');
print "Status: 302 Found\r\n";
print "Location: index.live.cgi\r\n\r\n";
exit;
}
print "Content-Type: text/html\r\n\r\n";
print $cpanel->header('');
print GnizaCPanel::UI::page_header('Restore Results');
print GnizaCPanel::UI::render_nav('restore.live.cgi');
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
print qq{<h2 class="card-title text-sm">Restore Results</h2>\n};
my @results;
# Build types_str encoding: type1;type2:item1,item2;type3
my @type_parts;
for my $type (@selected_types) {
my $type_label = $TYPE_LABELS{$type} // $type;
if ($type eq 'account') {
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_ACCOUNT', $remote, $timestamp, $exclude_paths);
push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err };
}
elsif ($type eq 'files') {
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_FILES', $remote, $timestamp, $path, $exclude_paths);
push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err };
}
elsif ($type eq 'cron') {
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_CRON', $remote, $timestamp);
push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err };
}
elsif ($type eq 'database') {
if ($dbnames eq '' || $dbnames eq '__ALL__') {
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_DATABASE', $remote, $timestamp, '');
push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err };
} else {
for my $item (split /,/, $dbnames) {
next if $item eq '';
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_DATABASE', $remote, $timestamp, $item);
push @results, { ok => $ok, label => $item, msg => $ok ? $stdout : $err };
}
}
if ($type eq 'database') {
my $items = ($dbnames eq '' || $dbnames eq '__ALL__') ? '' : $dbnames;
push @type_parts, $items ne '' ? "database:$items" : 'database';
}
elsif ($type eq 'dbusers') {
if ($dbuser_names eq '' || $dbuser_names eq '__ALL__') {
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_DBUSERS', $remote, $timestamp, '');
push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err };
} else {
for my $item (split /,/, $dbuser_names) {
next if $item eq '';
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_DBUSERS', $remote, $timestamp, $item);
push @results, { ok => $ok, label => $item, msg => $ok ? $stdout : $err };
}
}
my $items = ($dbuser_names eq '' || $dbuser_names eq '__ALL__') ? '' : $dbuser_names;
push @type_parts, $items ne '' ? "dbusers:$items" : 'dbusers';
}
elsif ($type eq 'mailbox') {
if ($emails eq '' || $emails eq '__ALL__') {
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_MAILBOX', $remote, $timestamp, '');
push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err };
} else {
for my $item (split /,/, $emails) {
next if $item eq '';
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_MAILBOX', $remote, $timestamp, $item);
push @results, { ok => $ok, label => $item, msg => $ok ? $stdout : $err };
}
}
my $items = ($emails eq '' || $emails eq '__ALL__') ? '' : $emails;
push @type_parts, $items ne '' ? "mailbox:$items" : 'mailbox';
}
elsif ($type eq 'domains') {
if ($domain_names eq '' || $domain_names eq '__ALL__') {
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_DOMAINS', $remote, $timestamp, '');
push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err };
} else {
for my $item (split /,/, $domain_names) {
next if $item eq '';
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_DOMAINS', $remote, $timestamp, $item);
push @results, { ok => $ok, label => $item, msg => $ok ? $stdout : $err };
}
}
my $items = ($domain_names eq '' || $domain_names eq '__ALL__') ? '' : $domain_names;
push @type_parts, $items ne '' ? "domains:$items" : 'domains';
}
elsif ($type eq 'ssl') {
if ($ssl_names eq '' || $ssl_names eq '__ALL__') {
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_SSL', $remote, $timestamp, '');
push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err };
} else {
for my $item (split /,/, $ssl_names) {
next if $item eq '';
my ($ok, $stdout, $err) = _adminbin_call('RESTORE_SSL', $remote, $timestamp, $item);
push @results, { ok => $ok, label => $item, msg => $ok ? $stdout : $err };
}
}
my $items = ($ssl_names eq '' || $ssl_names eq '__ALL__') ? '' : $ssl_names;
push @type_parts, $items ne '' ? "ssl:$items" : 'ssl';
}
else {
push @type_parts, $type; # account, files, cron — no items
}
}
my $types_str = join(';', @type_parts);
_render_results(\@results);
my ($ok, $stdout, $err) = _adminbin_call('START_RESTORE',
$remote, $timestamp, $types_str, $path, $exclude_paths);
print qq{</div>\n</div>\n};
print qq{<a href="index.live.cgi" class="btn btn-info btn-sm">Back to Home</a>\n};
print GnizaCPanel::UI::page_footer();
print $cpanel->footer();
}
sub _render_results {
my ($results) = @_;
for my $r (@$results) {
my $icon_class = $r->{ok} ? 'text-success' : 'text-error';
my $icon = $r->{ok} ? '&#10003;' : '&#10007;';
my $label = GnizaCPanel::UI::esc($r->{label});
my $msg = GnizaCPanel::UI::esc($r->{msg} // '');
# Clean up the "OK\n" prefix from successful results
$msg =~ s/^OK\s*//;
print qq{<div class="flex items-start gap-2 mb-3 p-3 rounded-lg bg-base-200">\n};
print qq{ <span class="$icon_class font-bold text-lg">$icon</span>\n};
print qq{ <div>\n};
print qq{ <div class="font-medium text-sm">$label</div>\n};
if ($msg ne '') {
print qq{ <pre class="text-xs mt-1 whitespace-pre-wrap max-h-32 overflow-y-auto">$msg</pre>\n};
}
print qq{ </div>\n};
print qq{</div>\n};
if ($ok) {
Gniza4cpCPanel::UI::set_flash('success', 'Restore started. Watch progress in the activity log.');
print "Status: 302 Found\r\n";
print "Location: logs.live.cgi\r\n\r\n";
} else {
Gniza4cpCPanel::UI::set_flash('error', "Restore failed to start: $err");
print "Status: 302 Found\r\n";
print "Location: index.live.cgi\r\n\r\n";
}
exit;
}

View File

@@ -1,18 +1,18 @@
# gniza configuration
# Copy to /etc/gniza/gniza.conf and edit
# gniza4cp configuration
# Copy to /etc/gniza4cp/gniza4cp.conf and edit
#
# Remote destinations: /etc/gniza/remotes.d/<name>.conf
# Backup schedules: /etc/gniza/schedules.d/<name>.conf
# Remote destinations: /etc/gniza4cp/remotes.d/<name>.conf
# Backup schedules: /etc/gniza4cp/schedules.d/<name>.conf
# ── Local Settings ─────────────────────────────────────────────
TEMP_DIR="/usr/local/gniza/workdir" # Working directory for pkgacct output
TEMP_DIR="/usr/local/gniza4cp/workdir" # Working directory for pkgacct output
# ── Account Filtering ──────────────────────────────────────────
INCLUDE_ACCOUNTS="" # Comma-separated list, empty = all accounts
EXCLUDE_ACCOUNTS="nobody" # Comma-separated list of accounts to exclude
# ── Logging ────────────────────────────────────────────────────
LOG_DIR="/var/log/gniza" # Log directory
LOG_DIR="/var/log/gniza4cp" # Log directory
LOG_LEVEL="info" # debug, info, warn, error
LOG_RETAIN=90 # Days to keep log files
@@ -29,7 +29,7 @@ SMTP_FROM="" # From address (falls back to SMTP_USER)
SMTP_SECURITY="tls" # tls (STARTTLS), ssl (implicit), none
# ── Advanced ───────────────────────────────────────────────────
LOCK_FILE="/var/run/gniza.lock"
LOCK_FILE="/var/run/gniza4cp.lock"
SSH_TIMEOUT=30 # SSH connection timeout in seconds
SSH_RETRIES=3 # Number of rsync retry attempts
RSYNC_EXTRA_OPTS="" # Extra options to pass to rsync

View File

@@ -1,7 +1,7 @@
# gniza remote destination config
# Copy to /etc/gniza/remotes.d/<name>.conf and edit
# gniza4cp remote destination config
# Copy to /etc/gniza4cp/remotes.d/<name>.conf and edit
#
# Each file in /etc/gniza/remotes.d/ defines a remote backup destination.
# Each file in /etc/gniza4cp/remotes.d/ defines a remote backup destination.
# The filename (without .conf) is the remote name used with --remote=NAME.
# ── Remote Type ───────────────────────────────────────────────

View File

@@ -1,7 +1,7 @@
# gniza schedule config
# Copy to /etc/gniza/schedules.d/<name>.conf and edit
# gniza4cp schedule config
# Copy to /etc/gniza4cp/schedules.d/<name>.conf and edit
#
# Each file in /etc/gniza/schedules.d/ defines a backup schedule.
# Each file in /etc/gniza4cp/schedules.d/ defines a backup schedule.
# The filename (without .conf) is the schedule name.
# ── Schedule ──────────────────────────────────────────────────

View File

Before

Width:  |  Height:  |  Size: 685 B

After

Width:  |  Height:  |  Size: 685 B

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/accounts.sh — cPanel account discovery, include/exclude filtering
# gniza4cp/lib/accounts.sh — cPanel account discovery, include/exclude filtering
get_all_accounts() {
if [[ -f /etc/trueuserdomains ]]; then

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/config.sh — Shell-variable config loading & validation
# gniza4cp/lib/config.sh — Shell-variable config loading & validation
# Safe config parser — reads KEY=VALUE lines without executing arbitrary code.
# Only processes lines matching ^[A-Z_][A-Z_0-9]*= and strips surrounding quotes.
@@ -28,7 +28,7 @@ load_config() {
local config_file="${1:-$DEFAULT_CONFIG_FILE}"
if [[ ! -f "$config_file" ]]; then
die "Config file not found: $config_file (create via WHM or copy gniza.conf.example)"
die "Config file not found: $config_file (create via WHM or copy gniza4cp.conf.example)"
fi
# Parse the config (safe key=value reader, no code execution)
@@ -56,7 +56,7 @@ load_config() {
USER_RESTORE_REMOTES="${USER_RESTORE_REMOTES:-$DEFAULT_USER_RESTORE_REMOTES}"
# --debug flag overrides config
[[ "${GNIZA_DEBUG:-false}" == "true" ]] && LOG_LEVEL="debug"
[[ "${GNIZA4CP_DEBUG:-false}" == "true" ]] && LOG_LEVEL="debug"
export TEMP_DIR INCLUDE_ACCOUNTS EXCLUDE_ACCOUNTS BWLIMIT RETENTION_COUNT
export LOG_DIR LOG_LEVEL LOG_RETAIN NOTIFY_EMAIL NOTIFY_ON

View File

@@ -1,12 +1,12 @@
#!/usr/bin/env bash
# gniza/lib/constants.sh — Version, exit codes, colors
# gniza4cp/lib/constants.sh — Version, exit codes, colors
# shellcheck disable=SC2034 # constants are used by sourcing scripts
[[ -n "${_GNIZA_CONSTANTS_LOADED:-}" ]] && return 0
_GNIZA_CONSTANTS_LOADED=1
[[ -n "${_GNIZA4CP_CONSTANTS_LOADED:-}" ]] && return 0
_GNIZA4CP_CONSTANTS_LOADED=1
readonly GNIZA_VERSION="0.1.0"
readonly GNIZA_NAME="gniza"
readonly GNIZA4CP_VERSION="0.1.0"
readonly GNIZA4CP_NAME="gniza4cp"
# Exit codes
readonly EXIT_OK=0
@@ -36,15 +36,15 @@ readonly DEFAULT_REMOTE_AUTH_METHOD="key"
readonly DEFAULT_REMOTE_PORT=22
readonly DEFAULT_REMOTE_USER="root"
readonly DEFAULT_REMOTE_BASE="/backups"
readonly DEFAULT_TEMP_DIR="/usr/local/gniza/workdir"
readonly DEFAULT_TEMP_DIR="/usr/local/gniza4cp/workdir"
readonly DEFAULT_EXCLUDE_ACCOUNTS="nobody"
readonly DEFAULT_BWLIMIT=0
readonly DEFAULT_RETENTION_COUNT=30
readonly DEFAULT_LOG_DIR="/var/log/gniza"
readonly DEFAULT_LOG_DIR="/var/log/gniza4cp"
readonly DEFAULT_LOG_LEVEL="info"
readonly DEFAULT_LOG_RETAIN=90
readonly DEFAULT_NOTIFY_ON="failure"
readonly DEFAULT_LOCK_FILE="/var/run/gniza.lock"
readonly DEFAULT_LOCK_FILE="/var/run/gniza4cp.lock"
readonly DEFAULT_SSH_TIMEOUT=30
readonly DEFAULT_SSH_RETRIES=3
readonly DEFAULT_REMOTE_TYPE="ssh"
@@ -52,4 +52,4 @@ readonly DEFAULT_S3_REGION="us-east-1"
readonly DEFAULT_SMTP_PORT=587
readonly DEFAULT_SMTP_SECURITY="tls"
readonly DEFAULT_USER_RESTORE_REMOTES="all"
readonly DEFAULT_CONFIG_FILE="/etc/gniza/gniza.conf"
readonly DEFAULT_CONFIG_FILE="/etc/gniza4cp/gniza4cp.conf"

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/locking.sh — flock-based concurrency control
# gniza4cp/lib/locking.sh — flock-based concurrency control
declare -g LOCK_FD=""
@@ -11,7 +11,7 @@ acquire_lock() {
exec {LOCK_FD}>"$lock_file"
if ! flock -n "$LOCK_FD"; then
die "Another gniza process is running (lock: $lock_file)" "$EXIT_LOCKED"
die "Another gniza4cp process is running (lock: $lock_file)" "$EXIT_LOCKED"
fi
echo $$ >&"$LOCK_FD"

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/logging.sh — Per-run log files, log_info/warn/error/debug
# gniza4cp/lib/logging.sh — Per-run log files, log_info/warn/error/debug
declare -g LOG_FILE=""
@@ -17,12 +17,12 @@ init_logging() {
local log_dir="${LOG_DIR:-$DEFAULT_LOG_DIR}"
mkdir -p "$log_dir" || die "Cannot create log directory: $log_dir"
LOG_FILE="$log_dir/gniza-$(date -u +%Y%m%d-%H%M%S).log"
LOG_FILE="$log_dir/gniza4cp-$(date -u +%Y%m%d-%H%M%S).log"
touch "$LOG_FILE" || die "Cannot write to log file: $LOG_FILE"
# Clean old logs
local retain="${LOG_RETAIN:-$DEFAULT_LOG_RETAIN}"
find "$log_dir" -name "gniza-*.log" -mtime +"$retain" -delete 2>/dev/null || true
find "$log_dir" -name "gniza4cp-*.log" -mtime +"$retain" -delete 2>/dev/null || true
}
_log() {

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/notify.sh — Email notifications (SMTP via curl or legacy mail/sendmail)
# gniza4cp/lib/notify.sh — Email notifications (SMTP via curl or legacy mail/sendmail)
_send_via_smtp() {
local subject="$1"
@@ -116,7 +116,7 @@ send_notification() {
esac
local hostname; hostname=$(hostname -f)
local full_subject="[gniza] [$hostname] $subject"
local full_subject="[gniza4cp] [$hostname] $subject"
log_debug "Sending notification to $NOTIFY_EMAIL: $full_subject"

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/pkgacct.sh — pkgacct execution, .sql gzipping, temp cleanup
# gniza4cp/lib/pkgacct.sh — pkgacct execution, .sql gzipping, temp cleanup
run_pkgacct() {
local user="$1"

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# gniza/lib/rclone.sh — Rclone transport layer for S3 and Google Drive remotes
# gniza4cp/lib/rclone.sh — Rclone transport layer for S3 and Google Drive remotes
[[ -n "${_GNIZA_RCLONE_LOADED:-}" ]] && return 0
_GNIZA_RCLONE_LOADED=1
[[ -n "${_GNIZA4CP_RCLONE_LOADED:-}" ]] && return 0
_GNIZA4CP_RCLONE_LOADED=1
# ── Mode Detection ────────────────────────────────────────────
@@ -17,7 +17,7 @@ _build_rclone_config() {
local old_umask
old_umask=$(umask)
umask 077
tmpfile=$(mktemp /tmp/gniza-rclone-XXXXXX.conf) || {
tmpfile=$(mktemp /tmp/gniza4cp-rclone-XXXXXX.conf) || {
umask "$old_umask"
log_error "Failed to create temp rclone config"
return 1

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env bash
# gniza/lib/remotes.sh — Remote discovery and context switching
# gniza4cp/lib/remotes.sh — Remote discovery and context switching
#
# Remote destinations are configured in /etc/gniza/remotes.d/<name>.conf.
# Remote destinations are configured in /etc/gniza4cp/remotes.d/<name>.conf.
# Each config overrides REMOTE_* globals so existing functions (ssh,
# transfer, snapshot, retention) work unchanged.
readonly REMOTES_DIR="/etc/gniza/remotes.d"
readonly REMOTES_DIR="/etc/gniza4cp/remotes.d"
# ── Saved state for legacy globals ─────────────────────────────
@@ -132,7 +132,7 @@ load_remote() {
GDRIVE_SERVICE_ACCOUNT_FILE="${GDRIVE_SERVICE_ACCOUNT_FILE:-}"
GDRIVE_ROOT_FOLDER_ID="${GDRIVE_ROOT_FOLDER_ID:-}"
# shellcheck disable=SC2034 # used by bin/gniza
# shellcheck disable=SC2034 # used by bin/gniza4cp
CURRENT_REMOTE_NAME="$name"
if [[ "$REMOTE_TYPE" == "ssh" ]]; then
log_debug "Loaded remote '$name': ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT} -> ${REMOTE_BASE}"

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/restore.sh — Full account, files, database, mailbox, server restores
# gniza4cp/lib/restore.sh — Full account, files, database, mailbox, server restores
# Helper: build rsync download command args for SSH mode
_rsync_download() {

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/retention.sh — Delete old snapshots beyond RETENTION_COUNT on remote
# gniza4cp/lib/retention.sh — Delete old snapshots beyond RETENTION_COUNT on remote
enforce_retention() {
local user="$1"

View File

@@ -1,17 +1,17 @@
#!/usr/bin/env bash
# gniza/lib/schedule.sh — Cron management for decoupled schedules
# gniza4cp/lib/schedule.sh — Cron management for decoupled schedules
#
# Schedules are defined in /etc/gniza/schedules.d/<name>.conf:
# Schedules are defined in /etc/gniza4cp/schedules.d/<name>.conf:
# SCHEDULE="hourly|daily|weekly|monthly|custom"
# SCHEDULE_TIME="HH:MM"
# SCHEDULE_DAY="" # dow (0-6) for weekly, dom (1-28) for monthly
# SCHEDULE_CRON="" # full 5-field cron expr for custom
# REMOTES="" # comma-separated remote names (empty = all)
#
# Cron lines are tagged with "# gniza:<name>" for clean install/remove.
# Cron lines are tagged with "# gniza4cp:<name>" for clean install/remove.
readonly GNIZA_CRON_TAG="# gniza:"
readonly SCHEDULES_DIR="/etc/gniza/schedules.d"
readonly GNIZA4CP_CRON_TAG="# gniza4cp:"
readonly SCHEDULES_DIR="/etc/gniza4cp/schedules.d"
# ── Discovery ─────────────────────────────────────────────────
@@ -148,13 +148,13 @@ build_cron_line() {
extra_flags+=" --skip-suspended"
fi
echo "$cron_expr /usr/local/bin/gniza backup${extra_flags} >/dev/null 2>&1"
echo "$cron_expr /usr/local/bin/gniza4cp backup${extra_flags} >/dev/null 2>&1"
}
# ── Crontab Management ────────────────────────────────────────
# Install cron entries for all schedules in schedules.d/.
# Strips any existing gniza entries first, then appends new ones.
# Strips any existing gniza4cp entries first, then appends new ones.
install_schedules() {
if ! has_schedules; then
log_error "No schedules configured in $SCHEDULES_DIR"
@@ -178,7 +178,7 @@ install_schedules() {
local cron_line
cron_line=$(build_cron_line "$sname") || { log_error "Skipping schedule '$sname': invalid schedule"; continue; }
new_lines+="${GNIZA_CRON_TAG}${sname}"$'\n'
new_lines+="${GNIZA4CP_CRON_TAG}${sname}"$'\n'
new_lines+="${cron_line}"$'\n'
((count++)) || true
done <<< "$schedules"
@@ -188,14 +188,14 @@ install_schedules() {
return 1
fi
# Get current crontab, strip old gniza lines
# Get current crontab, strip old gniza4cp lines
local current_crontab=""
current_crontab=$(crontab -l 2>/dev/null) || true
local filtered=""
local skip_next=false
while IFS= read -r line; do
if [[ "$line" == "${GNIZA_CRON_TAG}"* ]]; then
if [[ "$line" == "${GNIZA4CP_CRON_TAG}"* ]]; then
skip_next=true
continue
fi
@@ -207,8 +207,8 @@ install_schedules() {
done <<< "$current_crontab"
# Append daily stats collection (runs at 05:00 UTC)
new_lines+="${GNIZA_CRON_TAG}_stats"$'\n'
new_lines+="0 5 * * * /usr/local/bin/gniza stats >> /var/log/gniza/cron-stats.log 2>&1"$'\n'
new_lines+="${GNIZA4CP_CRON_TAG}_stats"$'\n'
new_lines+="0 5 * * * /usr/local/bin/gniza4cp stats >> /var/log/gniza4cp/cron-stats.log 2>&1"$'\n'
# Append new lines
local final="${filtered}${new_lines}"
@@ -232,7 +232,7 @@ install_schedules() {
done <<< "$schedules"
}
# Display current gniza cron entries.
# Display current gniza4cp cron entries.
show_schedules() {
local current_crontab=""
current_crontab=$(crontab -l 2>/dev/null) || true
@@ -246,15 +246,15 @@ show_schedules() {
local next_is_command=false
local current_tag=""
while IFS= read -r line; do
if [[ "$line" == "${GNIZA_CRON_TAG}"* ]]; then
current_tag="${line#"$GNIZA_CRON_TAG"}"
if [[ "$line" == "${GNIZA4CP_CRON_TAG}"* ]]; then
current_tag="${line#"$GNIZA4CP_CRON_TAG"}"
next_is_command=true
continue
fi
if [[ "$next_is_command" == "true" ]]; then
next_is_command=false
if [[ "$found" == "false" ]]; then
echo "Current gniza schedules:"
echo "Current gniza4cp schedules:"
echo ""
found=true
fi
@@ -263,11 +263,11 @@ show_schedules() {
done <<< "$current_crontab"
if [[ "$found" == "false" ]]; then
echo "No gniza schedule entries in crontab."
echo "No gniza4cp schedule entries in crontab."
fi
}
# Remove all gniza cron entries.
# Remove all gniza4cp cron entries.
remove_schedules() {
local current_crontab=""
current_crontab=$(crontab -l 2>/dev/null) || true
@@ -281,7 +281,7 @@ remove_schedules() {
local skip_next=false
local removed=0
while IFS= read -r line; do
if [[ "$line" == "${GNIZA_CRON_TAG}"* ]]; then
if [[ "$line" == "${GNIZA4CP_CRON_TAG}"* ]]; then
skip_next=true
((removed++)) || true
continue
@@ -294,7 +294,7 @@ remove_schedules() {
done <<< "$current_crontab"
if (( removed == 0 )); then
echo "No gniza schedule entries found in crontab."
echo "No gniza4cp schedule entries found in crontab."
return 0
fi
@@ -303,5 +303,5 @@ remove_schedules() {
return 1
}
echo "Removed $removed gniza schedule(s) from crontab."
echo "Removed $removed gniza4cp schedule(s) from crontab."
}

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/snapshot.sh — Timestamp naming, list/resolve snapshots, latest symlink
# gniza4cp/lib/snapshot.sh — Timestamp naming, list/resolve snapshots, latest symlink
get_remote_account_base() {
local user="$1"

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/ssh.sh — SSH connectivity, remote exec, ssh_opts builder
# gniza4cp/lib/ssh.sh — SSH connectivity, remote exec, ssh_opts builder
_is_password_mode() {
[[ "${REMOTE_AUTH_METHOD:-key}" == "password" ]]

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# gniza/lib/sysbackup.sh — System-level WHM backup: API exports, file staging, snapshot lifecycle
# gniza4cp/lib/sysbackup.sh — System-level WHM backup: API exports, file staging, snapshot lifecycle
[[ -n "${_GNIZA_SYSBACKUP_LOADED:-}" ]] && return 0
_GNIZA_SYSBACKUP_LOADED=1
[[ -n "${_GNIZA4CP_SYSBACKUP_LOADED:-}" ]] && return 0
_GNIZA4CP_SYSBACKUP_LOADED=1
# ── Path Helpers ─────────────────────────────────────────────
@@ -349,8 +349,8 @@ readonly _SYSBACKUP_PATHS=(
/etc/reservedipreasons
/etc/sysconfig/network
/etc/resolv.conf
# gniza's own config
/etc/gniza
# gniza4cp's own config
/etc/gniza4cp
)
_stage_files() {

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# gniza/lib/sysrestore.sh — System-level WHM restore: phased restore of configs, services, DNS
# gniza4cp/lib/sysrestore.sh — System-level WHM restore: phased restore of configs, services, DNS
[[ -n "${_GNIZA_SYSRESTORE_LOADED:-}" ]] && return 0
_GNIZA_SYSRESTORE_LOADED=1
[[ -n "${_GNIZA4CP_SYSRESTORE_LOADED:-}" ]] && return 0
_GNIZA4CP_SYSRESTORE_LOADED=1
# ── Download ─────────────────────────────────────────────────
@@ -375,7 +375,7 @@ _restore_phase3_security() {
log_info "[DRY RUN] Would restore CSF firewall config + csf -r"
log_info "[DRY RUN] Would restore /root/.ssh/"
log_info "[DRY RUN] Would restore root crontab"
log_info "[DRY RUN] Would restore /etc/gniza/"
log_info "[DRY RUN] Would restore /etc/gniza4cp/"
return 0
fi
@@ -412,9 +412,9 @@ _restore_phase3_security() {
log_info "--- Restoring root crontab ---"
_restore_file "$stage_dir" "var/spool/cron/root" || ((errors++)) || true
# gniza config
log_info "--- Restoring gniza configuration ---"
_restore_dir "$stage_dir" "etc/gniza" || ((errors++)) || true
# gniza4cp config
log_info "--- Restoring gniza4cp configuration ---"
_restore_dir "$stage_dir" "etc/gniza4cp" || ((errors++)) || true
if (( errors > 0 )); then
log_warn "Phase 3 completed with $errors error(s)"

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/transfer.sh — rsync --link-dest to remote, .partial atomicity, retries
# gniza4cp/lib/transfer.sh — rsync --link-dest to remote, .partial atomicity, retries
rsync_to_remote() {
local source_dir="$1"

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/utils.sh — Core utility functions
# gniza4cp/lib/utils.sh — Core utility functions
die() {
local code="${2:-$EXIT_FATAL}"

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza/lib/verify.sh — Remote backup integrity checks
# gniza4cp/lib/verify.sh — Remote backup integrity checks
verify_account_backup() {
local user="$1"

View File

@@ -1,12 +1,12 @@
#!/usr/bin/env bash
# gniza installer — from a local clone:
# gniza4cp installer — from a local clone:
# bash scripts/install.sh
set -eo pipefail
INSTALL_DIR="/usr/local/gniza"
BIN_LINK="/usr/local/bin/gniza"
REPO_URL="https://git.linux-hosting.co.il/shukivaknin/gniza.git"
INSTALL_DIR="/usr/local/gniza4cp"
BIN_LINK="/usr/local/bin/gniza4cp"
REPO_URL="https://git.linux-hosting.co.il/shukivaknin/gniza4cp.git"
TMPDIR_CLONE=""
if [[ $EUID -ne 0 ]]; then
@@ -16,10 +16,10 @@ fi
# Determine source directory — local clone or fresh git clone
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-}")" 2>/dev/null && pwd)" || true
if [[ -n "${SCRIPT_DIR:-}" && -f "$SCRIPT_DIR/../bin/gniza" ]]; then
if [[ -n "${SCRIPT_DIR:-}" && -f "$SCRIPT_DIR/../bin/gniza4cp" ]]; then
SOURCE_DIR="$(dirname "$SCRIPT_DIR")"
else
echo "Cloning gniza..."
echo "Cloning gniza4cp..."
TMPDIR_CLONE="$(mktemp -d)"
git clone --depth 1 "$REPO_URL" "$TMPDIR_CLONE" 2>&1
SOURCE_DIR="$TMPDIR_CLONE"
@@ -28,7 +28,7 @@ fi
cleanup() { [[ -n "${TMPDIR_CLONE:-}" ]] && rm -rf "$TMPDIR_CLONE"; }
trap cleanup EXIT
echo "Installing gniza to $INSTALL_DIR..."
echo "Installing gniza4cp to $INSTALL_DIR..."
# Create install directory
mkdir -p "$INSTALL_DIR"
@@ -41,84 +41,84 @@ cp "$SOURCE_DIR/scripts/uninstall.sh" "$INSTALL_DIR/uninstall.sh"
chmod +x "$INSTALL_DIR/uninstall.sh"
# Make bin executable
chmod +x "$INSTALL_DIR/bin/gniza"
chmod +x "$INSTALL_DIR/bin/gniza4cp"
# Create symlink
ln -sf "$INSTALL_DIR/bin/gniza" "$BIN_LINK"
ln -sf "$INSTALL_DIR/bin/gniza4cp" "$BIN_LINK"
# Create working directory
mkdir -p "$INSTALL_DIR/workdir"
# Create config directory structure with restrictive permissions
mkdir -p -m 700 /etc/gniza/remotes.d /etc/gniza/schedules.d
chmod 700 /etc/gniza
mkdir -p -m 700 /etc/gniza4cp/remotes.d /etc/gniza4cp/schedules.d
chmod 700 /etc/gniza4cp
# Copy example configs if no config exists
if [[ ! -f /etc/gniza/gniza.conf ]]; then
cp "$INSTALL_DIR/etc/gniza.conf.example" /etc/gniza/gniza.conf.example
echo "Example config copied to /etc/gniza/gniza.conf.example"
if [[ ! -f /etc/gniza4cp/gniza4cp.conf ]]; then
cp "$INSTALL_DIR/etc/gniza4cp.conf.example" /etc/gniza4cp/gniza4cp.conf.example
echo "Example config copied to /etc/gniza4cp/gniza4cp.conf.example"
fi
cp "$INSTALL_DIR/etc/remote.conf.example" /etc/gniza/remote.conf.example
cp "$INSTALL_DIR/etc/schedule.conf.example" /etc/gniza/schedule.conf.example
cp "$INSTALL_DIR/etc/remote.conf.example" /etc/gniza4cp/remote.conf.example
cp "$INSTALL_DIR/etc/schedule.conf.example" /etc/gniza4cp/schedule.conf.example
# Create log directory
mkdir -p /var/log/gniza
mkdir -p /var/log/gniza4cp
echo "gniza installed successfully!"
echo "gniza4cp installed successfully!"
# ── WHM Plugin (if cPanel/WHM is present) ─────────────────────
WHM_CGI_DIR="/usr/local/cpanel/whostmgr/docroot/cgi"
if [[ -d "$WHM_CGI_DIR" ]]; then
echo "Installing WHM plugin..."
# Remove old assets cruft (node_modules, src) if upgrading
rm -rf "$WHM_CGI_DIR/gniza-whm/assets/node_modules" \
"$WHM_CGI_DIR/gniza-whm/assets/src" \
"$WHM_CGI_DIR/gniza-whm/assets/package.json" \
"$WHM_CGI_DIR/gniza-whm/assets/package-lock.json" 2>/dev/null || true
cp -r "$SOURCE_DIR/whm/gniza-whm" "$WHM_CGI_DIR/"
cp "$SOURCE_DIR/whm/gniza-whm.conf" "$WHM_CGI_DIR/gniza-whm/"
chmod +x "$WHM_CGI_DIR/gniza-whm/"*.cgi
rm -rf "$WHM_CGI_DIR/gniza4cp-whm/assets/node_modules" \
"$WHM_CGI_DIR/gniza4cp-whm/assets/src" \
"$WHM_CGI_DIR/gniza4cp-whm/assets/package.json" \
"$WHM_CGI_DIR/gniza4cp-whm/assets/package-lock.json" 2>/dev/null || true
cp -r "$SOURCE_DIR/whm/gniza4cp-whm" "$WHM_CGI_DIR/"
cp "$SOURCE_DIR/whm/gniza4cp-whm.conf" "$WHM_CGI_DIR/gniza4cp-whm/"
chmod +x "$WHM_CGI_DIR/gniza4cp-whm/"*.cgi
# Remove build artifacts that shouldn't be on the server
rm -rf "$WHM_CGI_DIR/gniza-whm/assets/node_modules" \
"$WHM_CGI_DIR/gniza-whm/assets/src" \
"$WHM_CGI_DIR/gniza-whm/assets/package.json" \
"$WHM_CGI_DIR/gniza-whm/assets/package-lock.json" 2>/dev/null || true
/usr/local/cpanel/bin/register_appconfig "$WHM_CGI_DIR/gniza-whm/gniza-whm.conf"
echo "WHM plugin installed — access via WHM > Plugins > gniza Backup Manager"
rm -rf "$WHM_CGI_DIR/gniza4cp-whm/assets/node_modules" \
"$WHM_CGI_DIR/gniza4cp-whm/assets/src" \
"$WHM_CGI_DIR/gniza4cp-whm/assets/package.json" \
"$WHM_CGI_DIR/gniza4cp-whm/assets/package-lock.json" 2>/dev/null || true
/usr/local/cpanel/bin/register_appconfig "$WHM_CGI_DIR/gniza4cp-whm/gniza4cp-whm.conf"
echo "WHM plugin installed — access via WHM > Plugins > gniza4cp Backup Manager"
else
echo "WHM not detected, skipping WHM plugin installation."
fi
# ── cPanel User Plugin (if cPanel is present) ────────────────
CPANEL_BASE="/usr/local/cpanel/base/frontend/jupiter"
ADMINBIN_DIR="/usr/local/cpanel/bin/admin/Gniza"
ADMINBIN_DIR="/usr/local/cpanel/bin/admin/Gniza4cp"
if [[ -d "$CPANEL_BASE" ]]; then
echo "Installing cPanel user plugin..."
# Copy CGI files + lib + assets
mkdir -p "$CPANEL_BASE/gniza/lib/GnizaCPanel" "$CPANEL_BASE/gniza/assets"
cp "$SOURCE_DIR/cpanel/gniza/index.live.cgi" "$CPANEL_BASE/gniza/"
cp "$SOURCE_DIR/cpanel/gniza/restore.live.cgi" "$CPANEL_BASE/gniza/"
cp "$SOURCE_DIR/cpanel/gniza/logs.live.cgi" "$CPANEL_BASE/gniza/"
chmod +x "$CPANEL_BASE/gniza/"*.cgi
cp "$SOURCE_DIR/cpanel/gniza/lib/GnizaCPanel/UI.pm" "$CPANEL_BASE/gniza/lib/GnizaCPanel/"
cp "$SOURCE_DIR/cpanel/gniza/assets/gniza-whm.css" "$CPANEL_BASE/gniza/assets/"
cp "$SOURCE_DIR/cpanel/gniza/assets/gniza-logo.svg" "$CPANEL_BASE/gniza/assets/"
cp "$SOURCE_DIR/cpanel/gniza/assets/gniza-cpanel-icon.png" "$CPANEL_BASE/gniza/assets/"
cp "$SOURCE_DIR/cpanel/gniza/install.json" "$CPANEL_BASE/gniza/"
mkdir -p "$CPANEL_BASE/gniza4cp/lib/Gniza4cpCPanel" "$CPANEL_BASE/gniza4cp/assets"
cp "$SOURCE_DIR/cpanel/gniza4cp/index.live.cgi" "$CPANEL_BASE/gniza4cp/"
cp "$SOURCE_DIR/cpanel/gniza4cp/restore.live.cgi" "$CPANEL_BASE/gniza4cp/"
cp "$SOURCE_DIR/cpanel/gniza4cp/logs.live.cgi" "$CPANEL_BASE/gniza4cp/"
chmod +x "$CPANEL_BASE/gniza4cp/"*.cgi
cp "$SOURCE_DIR/cpanel/gniza4cp/lib/Gniza4cpCPanel/UI.pm" "$CPANEL_BASE/gniza4cp/lib/Gniza4cpCPanel/"
cp "$SOURCE_DIR/cpanel/gniza4cp/assets/gniza4cp-whm.css" "$CPANEL_BASE/gniza4cp/assets/"
cp "$SOURCE_DIR/cpanel/gniza4cp/assets/gniza4cp-logo.svg" "$CPANEL_BASE/gniza4cp/assets/"
cp "$SOURCE_DIR/cpanel/gniza4cp/assets/gniza4cp-cpanel-icon.png" "$CPANEL_BASE/gniza4cp/assets/"
cp "$SOURCE_DIR/cpanel/gniza4cp/install.json" "$CPANEL_BASE/gniza4cp/"
# Install AdminBin module (runs as root)
mkdir -p "$ADMINBIN_DIR"
cp "$SOURCE_DIR/cpanel/admin/Gniza/Restore" "$ADMINBIN_DIR/"
cp "$SOURCE_DIR/cpanel/admin/Gniza/Restore.conf" "$ADMINBIN_DIR/"
cp "$SOURCE_DIR/cpanel/admin/Gniza4cp/Restore" "$ADMINBIN_DIR/"
cp "$SOURCE_DIR/cpanel/admin/Gniza4cp/Restore.conf" "$ADMINBIN_DIR/"
chmod 0700 "$ADMINBIN_DIR/Restore"
chmod 0600 "$ADMINBIN_DIR/Restore.conf"
# Register plugin in cPanel interface (install_plugin expects a tar.gz archive
# containing install.json + the icon file referenced in it)
PLUGIN_TMPDIR="$(mktemp -d)"
mkdir -p "$PLUGIN_TMPDIR/gniza/assets"
cp "$SOURCE_DIR/cpanel/gniza/install.json" "$PLUGIN_TMPDIR/"
cp "$SOURCE_DIR/cpanel/gniza/assets/gniza-cpanel-icon.png" "$PLUGIN_TMPDIR/gniza/assets/"
tar -czf "$PLUGIN_TMPDIR/gniza-cpanel.tar.gz" -C "$PLUGIN_TMPDIR" install.json gniza/assets/gniza-cpanel-icon.png
/usr/local/cpanel/scripts/install_plugin "$PLUGIN_TMPDIR/gniza-cpanel.tar.gz" 2>/dev/null || true
mkdir -p "$PLUGIN_TMPDIR/gniza4cp/assets"
cp "$SOURCE_DIR/cpanel/gniza4cp/install.json" "$PLUGIN_TMPDIR/"
cp "$SOURCE_DIR/cpanel/gniza4cp/assets/gniza4cp-cpanel-icon.png" "$PLUGIN_TMPDIR/gniza4cp/assets/"
tar -czf "$PLUGIN_TMPDIR/gniza4cp-cpanel.tar.gz" -C "$PLUGIN_TMPDIR" install.json gniza4cp/assets/gniza4cp-cpanel-icon.png
/usr/local/cpanel/scripts/install_plugin "$PLUGIN_TMPDIR/gniza4cp-cpanel.tar.gz" 2>/dev/null || true
rm -rf "$PLUGIN_TMPDIR"
# Rebuild icon sprites so the new icon appears in cPanel
/usr/local/cpanel/bin/rebuild_sprites 2>/dev/null || true
@@ -129,11 +129,11 @@ if [[ -d "$CPANEL_BASE" ]]; then
[[ -n "$user" ]] && cpapi2 --user="$user" Branding gensprites 2>/dev/null || true
done < /etc/trueuserdomains
fi
echo "cPanel user plugin installed — users will see gniza Restore in Files section"
echo "cPanel user plugin installed — users will see gniza4cp Restore in Files section"
else
echo "cPanel not detected, skipping cPanel user plugin installation."
fi
echo ""
echo "Next steps:"
echo " Open WHM → GNIZA Backup Manager to configure via the setup wizard."
echo " Open WHM → GNIZA4CP Backup Manager to configure via the setup wizard."

View File

@@ -1,17 +1,17 @@
#!/usr/bin/env bash
# gniza uninstall script
# gniza4cp uninstall script
set -euo pipefail
INSTALL_DIR="/usr/local/gniza"
BIN_LINK="/usr/local/bin/gniza"
INSTALL_DIR="/usr/local/gniza4cp"
BIN_LINK="/usr/local/bin/gniza4cp"
if [[ $EUID -ne 0 ]]; then
echo "Error: uninstall.sh must be run as root" >&2
exit 1
fi
echo "Uninstalling gniza..."
echo "Uninstalling gniza4cp..."
# Remove symlink
if [[ -L "$BIN_LINK" ]]; then
@@ -26,38 +26,38 @@ if [[ -d "$INSTALL_DIR" ]]; then
fi
# ── Remove cron entries ───────────────────────────────────────
if crontab -l 2>/dev/null | grep -q '# gniza:'; then
echo "Removing gniza cron entries..."
crontab -l 2>/dev/null | grep -v '# gniza:' | grep -v '/usr/local/bin/gniza' | crontab -
if crontab -l 2>/dev/null | grep -q '# gniza4cp:'; then
echo "Removing gniza4cp cron entries..."
crontab -l 2>/dev/null | grep -v '# gniza4cp:' | grep -v '/usr/local/bin/gniza4cp' | crontab -
echo "Cron entries removed."
fi
# ── WHM Plugin ────────────────────────────────────────────────
WHM_CGI_DIR="/usr/local/cpanel/whostmgr/docroot/cgi"
if [[ -d "$WHM_CGI_DIR/gniza-whm" ]]; then
if [[ -d "$WHM_CGI_DIR/gniza4cp-whm" ]]; then
echo "Removing WHM plugin..."
/usr/local/cpanel/bin/unregister_appconfig gniza-whm 2>/dev/null || true
rm -rf "$WHM_CGI_DIR/gniza-whm"
/usr/local/cpanel/bin/unregister_appconfig gniza4cp-whm 2>/dev/null || true
rm -rf "$WHM_CGI_DIR/gniza4cp-whm"
echo "WHM plugin removed."
fi
# ── cPanel User Plugin ────────────────────────────────────────
CPANEL_BASE="/usr/local/cpanel/base/frontend/jupiter"
ADMINBIN_DIR="/usr/local/cpanel/bin/admin/Gniza"
if [[ -d "$CPANEL_BASE/gniza" ]]; then
ADMINBIN_DIR="/usr/local/cpanel/bin/admin/Gniza4cp"
if [[ -d "$CPANEL_BASE/gniza4cp" ]]; then
echo "Removing cPanel user plugin..."
# uninstall_plugin expects a tar.gz archive with install.json + icon
if [[ -f "$CPANEL_BASE/gniza/install.json" ]]; then
if [[ -f "$CPANEL_BASE/gniza4cp/install.json" ]]; then
PLUGIN_TMPDIR="$(mktemp -d)"
mkdir -p "$PLUGIN_TMPDIR/gniza/assets"
cp "$CPANEL_BASE/gniza/install.json" "$PLUGIN_TMPDIR/"
cp "$CPANEL_BASE/gniza/assets/gniza-cpanel-icon.png" "$PLUGIN_TMPDIR/gniza/assets/" 2>/dev/null || true
tar -czf "$PLUGIN_TMPDIR/gniza-cpanel.tar.gz" -C "$PLUGIN_TMPDIR" install.json gniza/assets/gniza-cpanel-icon.png 2>/dev/null || \
tar -czf "$PLUGIN_TMPDIR/gniza-cpanel.tar.gz" -C "$PLUGIN_TMPDIR" install.json
/usr/local/cpanel/scripts/uninstall_plugin "$PLUGIN_TMPDIR/gniza-cpanel.tar.gz" 2>/dev/null || true
mkdir -p "$PLUGIN_TMPDIR/gniza4cp/assets"
cp "$CPANEL_BASE/gniza4cp/install.json" "$PLUGIN_TMPDIR/"
cp "$CPANEL_BASE/gniza4cp/assets/gniza4cp-cpanel-icon.png" "$PLUGIN_TMPDIR/gniza4cp/assets/" 2>/dev/null || true
tar -czf "$PLUGIN_TMPDIR/gniza4cp-cpanel.tar.gz" -C "$PLUGIN_TMPDIR" install.json gniza4cp/assets/gniza4cp-cpanel-icon.png 2>/dev/null || \
tar -czf "$PLUGIN_TMPDIR/gniza4cp-cpanel.tar.gz" -C "$PLUGIN_TMPDIR" install.json
/usr/local/cpanel/scripts/uninstall_plugin "$PLUGIN_TMPDIR/gniza4cp-cpanel.tar.gz" 2>/dev/null || true
rm -rf "$PLUGIN_TMPDIR"
fi
rm -rf "$CPANEL_BASE/gniza"
rm -rf "$CPANEL_BASE/gniza4cp"
echo "cPanel user plugin removed."
fi
if [[ -d "$ADMINBIN_DIR" ]]; then
@@ -66,12 +66,12 @@ if [[ -d "$ADMINBIN_DIR" ]]; then
fi
echo ""
echo "gniza uninstalled."
echo "gniza4cp uninstalled."
echo ""
echo "The following were NOT removed (manual cleanup if desired):"
echo " /etc/gniza/ (configuration + remotes.d/)"
echo " /var/log/gniza/ (log files)"
echo " /var/run/gniza.lock (lock file)"
echo " /etc/gniza4cp/ (configuration + remotes.d/)"
echo " /var/log/gniza4cp/ (log files)"
echo " /var/run/gniza4cp.lock (lock file)"
echo ""
echo "To remove configs: rm -rf /etc/gniza/"
echo "To remove logs: rm -rf /var/log/gniza/"
echo "To remove configs: rm -rf /etc/gniza4cp/"
echo "To remove logs: rm -rf /var/log/gniza4cp/"

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# gniza tests — utility functions
# gniza4cp tests — utility functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BASE_DIR="$(dirname "$SCRIPT_DIR")"

View File

@@ -1,7 +0,0 @@
name=gniza-whm
service=whostmgr
user=root
url=/cgi/gniza-whm/
acls=all
displayname=gniza Backup Manager
entryurl=gniza-whm/index.cgi

View File

@@ -1,12 +0,0 @@
{
"private": true,
"scripts": {
"build:css": "tailwindcss -i src/input.css -o gniza-whm.css --minify",
"dev:css": "tailwindcss -i src/input.css -o gniza-whm.css --watch"
},
"devDependencies": {
"@tailwindcss/cli": "^4",
"tailwindcss": "^4",
"daisyui": "^5"
}
}

7
whm/gniza4cp-whm.conf Normal file
View File

@@ -0,0 +1,7 @@
name=gniza4cp-whm
service=whostmgr
user=root
url=/cgi/gniza4cp-whm/
acls=all
displayname=gniza4cp Backup Manager
entryurl=gniza4cp-whm/index.cgi

View File

Before

Width:  |  Height:  |  Size: 685 B

After

Width:  |  Height:  |  Size: 685 B

File diff suppressed because one or more lines are too long

1
whm/gniza4cp-whm/assets/node_modules/.bin/jiti generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../jiti/lib/jiti-cli.mjs

1
whm/gniza4cp-whm/assets/node_modules/.bin/tailwindcss generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../@tailwindcss/cli/dist/index.mjs

421
whm/gniza4cp-whm/assets/node_modules/.package-lock.json generated vendored Normal file
View File

@@ -0,0 +1,421 @@
{
"name": "assets",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"detect-libc": "^2.0.3",
"is-glob": "^4.0.3",
"node-addon-api": "^7.0.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.6",
"@parcel/watcher-darwin-arm64": "2.5.6",
"@parcel/watcher-darwin-x64": "2.5.6",
"@parcel/watcher-freebsd-x64": "2.5.6",
"@parcel/watcher-linux-arm-glibc": "2.5.6",
"@parcel/watcher-linux-arm-musl": "2.5.6",
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
"@parcel/watcher-linux-arm64-musl": "2.5.6",
"@parcel/watcher-linux-x64-glibc": "2.5.6",
"@parcel/watcher-linux-x64-musl": "2.5.6",
"@parcel/watcher-win32-arm64": "2.5.6",
"@parcel/watcher-win32-ia32": "2.5.6",
"@parcel/watcher-win32-x64": "2.5.6"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.1.tgz",
"integrity": "sha512-b7MGn51IA80oSG+7fuAgzfQ+7pZBgjzbqwmiv6NO7/+a1sev32cGqnwhscT7h0EcAvMa9r7gjRylqOH8Xhc4DA==",
"dev": true,
"dependencies": {
"@parcel/watcher": "^2.5.1",
"@tailwindcss/node": "4.2.1",
"@tailwindcss/oxide": "4.2.1",
"enhanced-resolve": "^5.19.0",
"mri": "^1.2.0",
"picocolors": "^1.1.1",
"tailwindcss": "4.2.1"
},
"bin": {
"tailwindcss": "dist/index.mjs"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
"integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==",
"dev": true,
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"enhanced-resolve": "^5.19.0",
"jiti": "^2.6.1",
"lightningcss": "1.31.1",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.2.1"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz",
"integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==",
"dev": true,
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.2.1",
"@tailwindcss/oxide-darwin-arm64": "4.2.1",
"@tailwindcss/oxide-darwin-x64": "4.2.1",
"@tailwindcss/oxide-freebsd-x64": "4.2.1",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1",
"@tailwindcss/oxide-linux-arm64-gnu": "4.2.1",
"@tailwindcss/oxide-linux-arm64-musl": "4.2.1",
"@tailwindcss/oxide-linux-x64-gnu": "4.2.1",
"@tailwindcss/oxide-linux-x64-musl": "4.2.1",
"@tailwindcss/oxide-wasm32-wasi": "4.2.1",
"@tailwindcss/oxide-win32-arm64-msvc": "4.2.1",
"@tailwindcss/oxide-win32-x64-msvc": "4.2.1"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz",
"integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz",
"integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/daisyui": {
"version": "5.5.19",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.19.tgz",
"integrity": "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA==",
"dev": true,
"funding": {
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/enhanced-resolve": {
"version": "5.20.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
"integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.3.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/lightningcss": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
"integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==",
"dev": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.31.1",
"lightningcss-darwin-arm64": "1.31.1",
"lightningcss-darwin-x64": "1.31.1",
"lightningcss-freebsd-x64": "1.31.1",
"lightningcss-linux-arm-gnueabihf": "1.31.1",
"lightningcss-linux-arm64-gnu": "1.31.1",
"lightningcss-linux-arm64-musl": "1.31.1",
"lightningcss-linux-x64-gnu": "1.31.1",
"lightningcss-linux-x64-musl": "1.31.1",
"lightningcss-win32-arm64-msvc": "1.31.1",
"lightningcss-win32-x64-msvc": "1.31.1"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
"integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
"integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tailwindcss": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
"integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
"dev": true
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"dev": true,
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
}
}
}

View File

@@ -0,0 +1,19 @@
Copyright 2024 Justin Ridgewell <justin@ridgewell.name>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,227 @@
# @jridgewell/gen-mapping
> Generate source maps
`gen-mapping` allows you to generate a source map during transpilation or minification.
With a source map, you're able to trace the original location in the source file, either in Chrome's
DevTools or using a library like [`@jridgewell/trace-mapping`][trace-mapping].
You may already be familiar with the [`source-map`][source-map] package's `SourceMapGenerator`. This
provides the same `addMapping` and `setSourceContent` API.
## Installation
```sh
npm install @jridgewell/gen-mapping
```
## Usage
```typescript
import { GenMapping, addMapping, setSourceContent, toEncodedMap, toDecodedMap } from '@jridgewell/gen-mapping';
const map = new GenMapping({
file: 'output.js',
sourceRoot: 'https://example.com/',
});
setSourceContent(map, 'input.js', `function foo() {}`);
addMapping(map, {
// Lines start at line 1, columns at column 0.
generated: { line: 1, column: 0 },
source: 'input.js',
original: { line: 1, column: 0 },
});
addMapping(map, {
generated: { line: 1, column: 9 },
source: 'input.js',
original: { line: 1, column: 9 },
name: 'foo',
});
assert.deepEqual(toDecodedMap(map), {
version: 3,
file: 'output.js',
names: ['foo'],
sourceRoot: 'https://example.com/',
sources: ['input.js'],
sourcesContent: ['function foo() {}'],
mappings: [
[ [0, 0, 0, 0], [9, 0, 0, 9, 0] ]
],
});
assert.deepEqual(toEncodedMap(map), {
version: 3,
file: 'output.js',
names: ['foo'],
sourceRoot: 'https://example.com/',
sources: ['input.js'],
sourcesContent: ['function foo() {}'],
mappings: 'AAAA,SAASA',
});
```
### Smaller Sourcemaps
Not everything needs to be added to a sourcemap, and needless markings can cause signficantly
larger file sizes. `gen-mapping` exposes `maybeAddSegment`/`maybeAddMapping` APIs that will
intelligently determine if this marking adds useful information. If not, the marking will be
skipped.
```typescript
import { maybeAddMapping } from '@jridgewell/gen-mapping';
const map = new GenMapping();
// Adding a sourceless marking at the beginning of a line isn't useful.
maybeAddMapping(map, {
generated: { line: 1, column: 0 },
});
// Adding a new source marking is useful.
maybeAddMapping(map, {
generated: { line: 1, column: 0 },
source: 'input.js',
original: { line: 1, column: 0 },
});
// But adding another marking pointing to the exact same original location isn't, even if the
// generated column changed.
maybeAddMapping(map, {
generated: { line: 1, column: 9 },
source: 'input.js',
original: { line: 1, column: 0 },
});
assert.deepEqual(toEncodedMap(map), {
version: 3,
names: [],
sources: ['input.js'],
sourcesContent: [null],
mappings: 'AAAA',
});
```
## Benchmarks
```
node v18.0.0
amp.js.map
Memory Usage:
gen-mapping: addSegment 5852872 bytes
gen-mapping: addMapping 7716042 bytes
source-map-js 6143250 bytes
source-map-0.6.1 6124102 bytes
source-map-0.8.0 6121173 bytes
Smallest memory usage is gen-mapping: addSegment
Adding speed:
gen-mapping: addSegment x 441 ops/sec ±2.07% (90 runs sampled)
gen-mapping: addMapping x 350 ops/sec ±2.40% (86 runs sampled)
source-map-js: addMapping x 169 ops/sec ±2.42% (80 runs sampled)
source-map-0.6.1: addMapping x 167 ops/sec ±2.56% (80 runs sampled)
source-map-0.8.0: addMapping x 168 ops/sec ±2.52% (80 runs sampled)
Fastest is gen-mapping: addSegment
Generate speed:
gen-mapping: decoded output x 150,824,370 ops/sec ±0.07% (102 runs sampled)
gen-mapping: encoded output x 663 ops/sec ±0.22% (98 runs sampled)
source-map-js: encoded output x 197 ops/sec ±0.45% (84 runs sampled)
source-map-0.6.1: encoded output x 198 ops/sec ±0.33% (85 runs sampled)
source-map-0.8.0: encoded output x 197 ops/sec ±0.06% (93 runs sampled)
Fastest is gen-mapping: decoded output
***
babel.min.js.map
Memory Usage:
gen-mapping: addSegment 37578063 bytes
gen-mapping: addMapping 37212897 bytes
source-map-js 47638527 bytes
source-map-0.6.1 47690503 bytes
source-map-0.8.0 47470188 bytes
Smallest memory usage is gen-mapping: addMapping
Adding speed:
gen-mapping: addSegment x 31.05 ops/sec ±8.31% (43 runs sampled)
gen-mapping: addMapping x 29.83 ops/sec ±7.36% (51 runs sampled)
source-map-js: addMapping x 20.73 ops/sec ±6.22% (38 runs sampled)
source-map-0.6.1: addMapping x 20.03 ops/sec ±10.51% (38 runs sampled)
source-map-0.8.0: addMapping x 19.30 ops/sec ±8.27% (37 runs sampled)
Fastest is gen-mapping: addSegment
Generate speed:
gen-mapping: decoded output x 381,379,234 ops/sec ±0.29% (96 runs sampled)
gen-mapping: encoded output x 95.15 ops/sec ±2.98% (72 runs sampled)
source-map-js: encoded output x 15.20 ops/sec ±7.41% (33 runs sampled)
source-map-0.6.1: encoded output x 16.36 ops/sec ±10.46% (31 runs sampled)
source-map-0.8.0: encoded output x 16.06 ops/sec ±6.45% (31 runs sampled)
Fastest is gen-mapping: decoded output
***
preact.js.map
Memory Usage:
gen-mapping: addSegment 416247 bytes
gen-mapping: addMapping 419824 bytes
source-map-js 1024619 bytes
source-map-0.6.1 1146004 bytes
source-map-0.8.0 1113250 bytes
Smallest memory usage is gen-mapping: addSegment
Adding speed:
gen-mapping: addSegment x 13,755 ops/sec ±0.15% (98 runs sampled)
gen-mapping: addMapping x 13,013 ops/sec ±0.11% (101 runs sampled)
source-map-js: addMapping x 4,564 ops/sec ±0.21% (98 runs sampled)
source-map-0.6.1: addMapping x 4,562 ops/sec ±0.11% (99 runs sampled)
source-map-0.8.0: addMapping x 4,593 ops/sec ±0.11% (100 runs sampled)
Fastest is gen-mapping: addSegment
Generate speed:
gen-mapping: decoded output x 379,864,020 ops/sec ±0.23% (93 runs sampled)
gen-mapping: encoded output x 14,368 ops/sec ±4.07% (82 runs sampled)
source-map-js: encoded output x 5,261 ops/sec ±0.21% (99 runs sampled)
source-map-0.6.1: encoded output x 5,124 ops/sec ±0.58% (99 runs sampled)
source-map-0.8.0: encoded output x 5,434 ops/sec ±0.33% (96 runs sampled)
Fastest is gen-mapping: decoded output
***
react.js.map
Memory Usage:
gen-mapping: addSegment 975096 bytes
gen-mapping: addMapping 1102981 bytes
source-map-js 2918836 bytes
source-map-0.6.1 2885435 bytes
source-map-0.8.0 2874336 bytes
Smallest memory usage is gen-mapping: addSegment
Adding speed:
gen-mapping: addSegment x 4,772 ops/sec ±0.15% (100 runs sampled)
gen-mapping: addMapping x 4,456 ops/sec ±0.13% (97 runs sampled)
source-map-js: addMapping x 1,618 ops/sec ±0.24% (97 runs sampled)
source-map-0.6.1: addMapping x 1,622 ops/sec ±0.12% (99 runs sampled)
source-map-0.8.0: addMapping x 1,631 ops/sec ±0.12% (100 runs sampled)
Fastest is gen-mapping: addSegment
Generate speed:
gen-mapping: decoded output x 379,107,695 ops/sec ±0.07% (99 runs sampled)
gen-mapping: encoded output x 5,421 ops/sec ±1.60% (89 runs sampled)
source-map-js: encoded output x 2,113 ops/sec ±1.81% (98 runs sampled)
source-map-0.6.1: encoded output x 2,126 ops/sec ±0.10% (100 runs sampled)
source-map-0.8.0: encoded output x 2,176 ops/sec ±0.39% (98 runs sampled)
Fastest is gen-mapping: decoded output
```
[source-map]: https://www.npmjs.com/package/source-map
[trace-mapping]: https://github.com/jridgewell/sourcemaps/tree/main/packages/trace-mapping

View File

@@ -0,0 +1,292 @@
// src/set-array.ts
var SetArray = class {
constructor() {
this._indexes = { __proto__: null };
this.array = [];
}
};
function cast(set) {
return set;
}
function get(setarr, key) {
return cast(setarr)._indexes[key];
}
function put(setarr, key) {
const index = get(setarr, key);
if (index !== void 0) return index;
const { array, _indexes: indexes } = cast(setarr);
const length = array.push(key);
return indexes[key] = length - 1;
}
function remove(setarr, key) {
const index = get(setarr, key);
if (index === void 0) return;
const { array, _indexes: indexes } = cast(setarr);
for (let i = index + 1; i < array.length; i++) {
const k = array[i];
array[i - 1] = k;
indexes[k]--;
}
indexes[key] = void 0;
array.pop();
}
// src/gen-mapping.ts
import {
encode
} from "@jridgewell/sourcemap-codec";
import { TraceMap, decodedMappings } from "@jridgewell/trace-mapping";
// src/sourcemap-segment.ts
var COLUMN = 0;
var SOURCES_INDEX = 1;
var SOURCE_LINE = 2;
var SOURCE_COLUMN = 3;
var NAMES_INDEX = 4;
// src/gen-mapping.ts
var NO_NAME = -1;
var GenMapping = class {
constructor({ file, sourceRoot } = {}) {
this._names = new SetArray();
this._sources = new SetArray();
this._sourcesContent = [];
this._mappings = [];
this.file = file;
this.sourceRoot = sourceRoot;
this._ignoreList = new SetArray();
}
};
function cast2(map) {
return map;
}
function addSegment(map, genLine, genColumn, source, sourceLine, sourceColumn, name, content) {
return addSegmentInternal(
false,
map,
genLine,
genColumn,
source,
sourceLine,
sourceColumn,
name,
content
);
}
function addMapping(map, mapping) {
return addMappingInternal(false, map, mapping);
}
var maybeAddSegment = (map, genLine, genColumn, source, sourceLine, sourceColumn, name, content) => {
return addSegmentInternal(
true,
map,
genLine,
genColumn,
source,
sourceLine,
sourceColumn,
name,
content
);
};
var maybeAddMapping = (map, mapping) => {
return addMappingInternal(true, map, mapping);
};
function setSourceContent(map, source, content) {
const {
_sources: sources,
_sourcesContent: sourcesContent
// _originalScopes: originalScopes,
} = cast2(map);
const index = put(sources, source);
sourcesContent[index] = content;
}
function setIgnore(map, source, ignore = true) {
const {
_sources: sources,
_sourcesContent: sourcesContent,
_ignoreList: ignoreList
// _originalScopes: originalScopes,
} = cast2(map);
const index = put(sources, source);
if (index === sourcesContent.length) sourcesContent[index] = null;
if (ignore) put(ignoreList, index);
else remove(ignoreList, index);
}
function toDecodedMap(map) {
const {
_mappings: mappings,
_sources: sources,
_sourcesContent: sourcesContent,
_names: names,
_ignoreList: ignoreList
// _originalScopes: originalScopes,
// _generatedRanges: generatedRanges,
} = cast2(map);
removeEmptyFinalLines(mappings);
return {
version: 3,
file: map.file || void 0,
names: names.array,
sourceRoot: map.sourceRoot || void 0,
sources: sources.array,
sourcesContent,
mappings,
// originalScopes,
// generatedRanges,
ignoreList: ignoreList.array
};
}
function toEncodedMap(map) {
const decoded = toDecodedMap(map);
return Object.assign({}, decoded, {
// originalScopes: decoded.originalScopes.map((os) => encodeOriginalScopes(os)),
// generatedRanges: encodeGeneratedRanges(decoded.generatedRanges as GeneratedRange[]),
mappings: encode(decoded.mappings)
});
}
function fromMap(input) {
const map = new TraceMap(input);
const gen = new GenMapping({ file: map.file, sourceRoot: map.sourceRoot });
putAll(cast2(gen)._names, map.names);
putAll(cast2(gen)._sources, map.sources);
cast2(gen)._sourcesContent = map.sourcesContent || map.sources.map(() => null);
cast2(gen)._mappings = decodedMappings(map);
if (map.ignoreList) putAll(cast2(gen)._ignoreList, map.ignoreList);
return gen;
}
function allMappings(map) {
const out = [];
const { _mappings: mappings, _sources: sources, _names: names } = cast2(map);
for (let i = 0; i < mappings.length; i++) {
const line = mappings[i];
for (let j = 0; j < line.length; j++) {
const seg = line[j];
const generated = { line: i + 1, column: seg[COLUMN] };
let source = void 0;
let original = void 0;
let name = void 0;
if (seg.length !== 1) {
source = sources.array[seg[SOURCES_INDEX]];
original = { line: seg[SOURCE_LINE] + 1, column: seg[SOURCE_COLUMN] };
if (seg.length === 5) name = names.array[seg[NAMES_INDEX]];
}
out.push({ generated, source, original, name });
}
}
return out;
}
function addSegmentInternal(skipable, map, genLine, genColumn, source, sourceLine, sourceColumn, name, content) {
const {
_mappings: mappings,
_sources: sources,
_sourcesContent: sourcesContent,
_names: names
// _originalScopes: originalScopes,
} = cast2(map);
const line = getIndex(mappings, genLine);
const index = getColumnIndex(line, genColumn);
if (!source) {
if (skipable && skipSourceless(line, index)) return;
return insert(line, index, [genColumn]);
}
assert(sourceLine);
assert(sourceColumn);
const sourcesIndex = put(sources, source);
const namesIndex = name ? put(names, name) : NO_NAME;
if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = content != null ? content : null;
if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) {
return;
}
return insert(
line,
index,
name ? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex] : [genColumn, sourcesIndex, sourceLine, sourceColumn]
);
}
function assert(_val) {
}
function getIndex(arr, index) {
for (let i = arr.length; i <= index; i++) {
arr[i] = [];
}
return arr[index];
}
function getColumnIndex(line, genColumn) {
let index = line.length;
for (let i = index - 1; i >= 0; index = i--) {
const current = line[i];
if (genColumn >= current[COLUMN]) break;
}
return index;
}
function insert(array, index, value) {
for (let i = array.length; i > index; i--) {
array[i] = array[i - 1];
}
array[index] = value;
}
function removeEmptyFinalLines(mappings) {
const { length } = mappings;
let len = length;
for (let i = len - 1; i >= 0; len = i, i--) {
if (mappings[i].length > 0) break;
}
if (len < length) mappings.length = len;
}
function putAll(setarr, array) {
for (let i = 0; i < array.length; i++) put(setarr, array[i]);
}
function skipSourceless(line, index) {
if (index === 0) return true;
const prev = line[index - 1];
return prev.length === 1;
}
function skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex) {
if (index === 0) return false;
const prev = line[index - 1];
if (prev.length === 1) return false;
return sourcesIndex === prev[SOURCES_INDEX] && sourceLine === prev[SOURCE_LINE] && sourceColumn === prev[SOURCE_COLUMN] && namesIndex === (prev.length === 5 ? prev[NAMES_INDEX] : NO_NAME);
}
function addMappingInternal(skipable, map, mapping) {
const { generated, source, original, name, content } = mapping;
if (!source) {
return addSegmentInternal(
skipable,
map,
generated.line - 1,
generated.column,
null,
null,
null,
null,
null
);
}
assert(original);
return addSegmentInternal(
skipable,
map,
generated.line - 1,
generated.column,
source,
original.line - 1,
original.column,
name,
content
);
}
export {
GenMapping,
addMapping,
addSegment,
allMappings,
fromMap,
maybeAddMapping,
maybeAddSegment,
setIgnore,
setSourceContent,
toDecodedMap,
toEncodedMap
};
//# sourceMappingURL=gen-mapping.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,358 @@
(function (global, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
factory(module, require('@jridgewell/sourcemap-codec'), require('@jridgewell/trace-mapping'));
module.exports = def(module);
} else if (typeof define === 'function' && define.amd) {
define(['module', '@jridgewell/sourcemap-codec', '@jridgewell/trace-mapping'], function(mod) {
factory.apply(this, arguments);
mod.exports = def(mod);
});
} else {
const mod = { exports: {} };
factory(mod, global.sourcemapCodec, global.traceMapping);
global = typeof globalThis !== 'undefined' ? globalThis : global || self;
global.genMapping = def(mod);
}
function def(m) { return 'default' in m.exports ? m.exports.default : m.exports; }
})(this, (function (module, require_sourcemapCodec, require_traceMapping) {
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// umd:@jridgewell/sourcemap-codec
var require_sourcemap_codec = __commonJS({
"umd:@jridgewell/sourcemap-codec"(exports, module2) {
module2.exports = require_sourcemapCodec;
}
});
// umd:@jridgewell/trace-mapping
var require_trace_mapping = __commonJS({
"umd:@jridgewell/trace-mapping"(exports, module2) {
module2.exports = require_traceMapping;
}
});
// src/gen-mapping.ts
var gen_mapping_exports = {};
__export(gen_mapping_exports, {
GenMapping: () => GenMapping,
addMapping: () => addMapping,
addSegment: () => addSegment,
allMappings: () => allMappings,
fromMap: () => fromMap,
maybeAddMapping: () => maybeAddMapping,
maybeAddSegment: () => maybeAddSegment,
setIgnore: () => setIgnore,
setSourceContent: () => setSourceContent,
toDecodedMap: () => toDecodedMap,
toEncodedMap: () => toEncodedMap
});
module.exports = __toCommonJS(gen_mapping_exports);
// src/set-array.ts
var SetArray = class {
constructor() {
this._indexes = { __proto__: null };
this.array = [];
}
};
function cast(set) {
return set;
}
function get(setarr, key) {
return cast(setarr)._indexes[key];
}
function put(setarr, key) {
const index = get(setarr, key);
if (index !== void 0) return index;
const { array, _indexes: indexes } = cast(setarr);
const length = array.push(key);
return indexes[key] = length - 1;
}
function remove(setarr, key) {
const index = get(setarr, key);
if (index === void 0) return;
const { array, _indexes: indexes } = cast(setarr);
for (let i = index + 1; i < array.length; i++) {
const k = array[i];
array[i - 1] = k;
indexes[k]--;
}
indexes[key] = void 0;
array.pop();
}
// src/gen-mapping.ts
var import_sourcemap_codec = __toESM(require_sourcemap_codec());
var import_trace_mapping = __toESM(require_trace_mapping());
// src/sourcemap-segment.ts
var COLUMN = 0;
var SOURCES_INDEX = 1;
var SOURCE_LINE = 2;
var SOURCE_COLUMN = 3;
var NAMES_INDEX = 4;
// src/gen-mapping.ts
var NO_NAME = -1;
var GenMapping = class {
constructor({ file, sourceRoot } = {}) {
this._names = new SetArray();
this._sources = new SetArray();
this._sourcesContent = [];
this._mappings = [];
this.file = file;
this.sourceRoot = sourceRoot;
this._ignoreList = new SetArray();
}
};
function cast2(map) {
return map;
}
function addSegment(map, genLine, genColumn, source, sourceLine, sourceColumn, name, content) {
return addSegmentInternal(
false,
map,
genLine,
genColumn,
source,
sourceLine,
sourceColumn,
name,
content
);
}
function addMapping(map, mapping) {
return addMappingInternal(false, map, mapping);
}
var maybeAddSegment = (map, genLine, genColumn, source, sourceLine, sourceColumn, name, content) => {
return addSegmentInternal(
true,
map,
genLine,
genColumn,
source,
sourceLine,
sourceColumn,
name,
content
);
};
var maybeAddMapping = (map, mapping) => {
return addMappingInternal(true, map, mapping);
};
function setSourceContent(map, source, content) {
const {
_sources: sources,
_sourcesContent: sourcesContent
// _originalScopes: originalScopes,
} = cast2(map);
const index = put(sources, source);
sourcesContent[index] = content;
}
function setIgnore(map, source, ignore = true) {
const {
_sources: sources,
_sourcesContent: sourcesContent,
_ignoreList: ignoreList
// _originalScopes: originalScopes,
} = cast2(map);
const index = put(sources, source);
if (index === sourcesContent.length) sourcesContent[index] = null;
if (ignore) put(ignoreList, index);
else remove(ignoreList, index);
}
function toDecodedMap(map) {
const {
_mappings: mappings,
_sources: sources,
_sourcesContent: sourcesContent,
_names: names,
_ignoreList: ignoreList
// _originalScopes: originalScopes,
// _generatedRanges: generatedRanges,
} = cast2(map);
removeEmptyFinalLines(mappings);
return {
version: 3,
file: map.file || void 0,
names: names.array,
sourceRoot: map.sourceRoot || void 0,
sources: sources.array,
sourcesContent,
mappings,
// originalScopes,
// generatedRanges,
ignoreList: ignoreList.array
};
}
function toEncodedMap(map) {
const decoded = toDecodedMap(map);
return Object.assign({}, decoded, {
// originalScopes: decoded.originalScopes.map((os) => encodeOriginalScopes(os)),
// generatedRanges: encodeGeneratedRanges(decoded.generatedRanges as GeneratedRange[]),
mappings: (0, import_sourcemap_codec.encode)(decoded.mappings)
});
}
function fromMap(input) {
const map = new import_trace_mapping.TraceMap(input);
const gen = new GenMapping({ file: map.file, sourceRoot: map.sourceRoot });
putAll(cast2(gen)._names, map.names);
putAll(cast2(gen)._sources, map.sources);
cast2(gen)._sourcesContent = map.sourcesContent || map.sources.map(() => null);
cast2(gen)._mappings = (0, import_trace_mapping.decodedMappings)(map);
if (map.ignoreList) putAll(cast2(gen)._ignoreList, map.ignoreList);
return gen;
}
function allMappings(map) {
const out = [];
const { _mappings: mappings, _sources: sources, _names: names } = cast2(map);
for (let i = 0; i < mappings.length; i++) {
const line = mappings[i];
for (let j = 0; j < line.length; j++) {
const seg = line[j];
const generated = { line: i + 1, column: seg[COLUMN] };
let source = void 0;
let original = void 0;
let name = void 0;
if (seg.length !== 1) {
source = sources.array[seg[SOURCES_INDEX]];
original = { line: seg[SOURCE_LINE] + 1, column: seg[SOURCE_COLUMN] };
if (seg.length === 5) name = names.array[seg[NAMES_INDEX]];
}
out.push({ generated, source, original, name });
}
}
return out;
}
function addSegmentInternal(skipable, map, genLine, genColumn, source, sourceLine, sourceColumn, name, content) {
const {
_mappings: mappings,
_sources: sources,
_sourcesContent: sourcesContent,
_names: names
// _originalScopes: originalScopes,
} = cast2(map);
const line = getIndex(mappings, genLine);
const index = getColumnIndex(line, genColumn);
if (!source) {
if (skipable && skipSourceless(line, index)) return;
return insert(line, index, [genColumn]);
}
assert(sourceLine);
assert(sourceColumn);
const sourcesIndex = put(sources, source);
const namesIndex = name ? put(names, name) : NO_NAME;
if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = content != null ? content : null;
if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) {
return;
}
return insert(
line,
index,
name ? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex] : [genColumn, sourcesIndex, sourceLine, sourceColumn]
);
}
function assert(_val) {
}
function getIndex(arr, index) {
for (let i = arr.length; i <= index; i++) {
arr[i] = [];
}
return arr[index];
}
function getColumnIndex(line, genColumn) {
let index = line.length;
for (let i = index - 1; i >= 0; index = i--) {
const current = line[i];
if (genColumn >= current[COLUMN]) break;
}
return index;
}
function insert(array, index, value) {
for (let i = array.length; i > index; i--) {
array[i] = array[i - 1];
}
array[index] = value;
}
function removeEmptyFinalLines(mappings) {
const { length } = mappings;
let len = length;
for (let i = len - 1; i >= 0; len = i, i--) {
if (mappings[i].length > 0) break;
}
if (len < length) mappings.length = len;
}
function putAll(setarr, array) {
for (let i = 0; i < array.length; i++) put(setarr, array[i]);
}
function skipSourceless(line, index) {
if (index === 0) return true;
const prev = line[index - 1];
return prev.length === 1;
}
function skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex) {
if (index === 0) return false;
const prev = line[index - 1];
if (prev.length === 1) return false;
return sourcesIndex === prev[SOURCES_INDEX] && sourceLine === prev[SOURCE_LINE] && sourceColumn === prev[SOURCE_COLUMN] && namesIndex === (prev.length === 5 ? prev[NAMES_INDEX] : NO_NAME);
}
function addMappingInternal(skipable, map, mapping) {
const { generated, source, original, name, content } = mapping;
if (!source) {
return addSegmentInternal(
skipable,
map,
generated.line - 1,
generated.column,
null,
null,
null,
null,
null
);
}
assert(original);
return addSegmentInternal(
skipable,
map,
generated.line - 1,
generated.column,
source,
original.line - 1,
original.column,
name,
content
);
}
}));
//# sourceMappingURL=gen-mapping.umd.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,88 @@
import type { SourceMapInput } from '@jridgewell/trace-mapping';
import type { DecodedSourceMap, EncodedSourceMap, Pos, Mapping } from './types';
export type { DecodedSourceMap, EncodedSourceMap, Mapping };
export type Options = {
file?: string | null;
sourceRoot?: string | null;
};
/**
* Provides the state to generate a sourcemap.
*/
export declare class GenMapping {
private _names;
private _sources;
private _sourcesContent;
private _mappings;
private _ignoreList;
file: string | null | undefined;
sourceRoot: string | null | undefined;
constructor({ file, sourceRoot }?: Options);
}
/**
* A low-level API to associate a generated position with an original source position. Line and
* column here are 0-based, unlike `addMapping`.
*/
export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source?: null, sourceLine?: null, sourceColumn?: null, name?: null, content?: null): void;
export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name?: null, content?: string | null): void;
export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name: string, content?: string | null): void;
/**
* A high-level API to associate a generated position with an original source position. Line is
* 1-based, but column is 0-based, due to legacy behavior in `source-map` library.
*/
export declare function addMapping(map: GenMapping, mapping: {
generated: Pos;
source?: null;
original?: null;
name?: null;
content?: null;
}): void;
export declare function addMapping(map: GenMapping, mapping: {
generated: Pos;
source: string;
original: Pos;
name?: null;
content?: string | null;
}): void;
export declare function addMapping(map: GenMapping, mapping: {
generated: Pos;
source: string;
original: Pos;
name: string;
content?: string | null;
}): void;
/**
* Same as `addSegment`, but will only add the segment if it generates useful information in the
* resulting map. This only works correctly if segments are added **in order**, meaning you should
* not add a segment with a lower generated line/column than one that came before.
*/
export declare const maybeAddSegment: typeof addSegment;
/**
* Same as `addMapping`, but will only add the mapping if it generates useful information in the
* resulting map. This only works correctly if mappings are added **in order**, meaning you should
* not add a mapping with a lower generated line/column than one that came before.
*/
export declare const maybeAddMapping: typeof addMapping;
/**
* Adds/removes the content of the source file to the source map.
*/
export declare function setSourceContent(map: GenMapping, source: string, content: string | null): void;
export declare function setIgnore(map: GenMapping, source: string, ignore?: boolean): void;
/**
* Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects
* a sourcemap, or to JSON.stringify.
*/
export declare function toDecodedMap(map: GenMapping): DecodedSourceMap;
/**
* Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects
* a sourcemap, or to JSON.stringify.
*/
export declare function toEncodedMap(map: GenMapping): EncodedSourceMap;
/**
* Constructs a new GenMapping, using the already present mappings of the input.
*/
export declare function fromMap(input: SourceMapInput): GenMapping;
/**
* Returns an array of high-level mapping objects for every recorded segment, which could then be
* passed to the `source-map` library.
*/
export declare function allMappings(map: GenMapping): Mapping[];

View File

@@ -0,0 +1,32 @@
type Key = string | number | symbol;
/**
* SetArray acts like a `Set` (allowing only one occurrence of a string `key`), but provides the
* index of the `key` in the backing array.
*
* This is designed to allow synchronizing a second array with the contents of the backing array,
* like how in a sourcemap `sourcesContent[i]` is the source content associated with `source[i]`,
* and there are never duplicates.
*/
export declare class SetArray<T extends Key = Key> {
private _indexes;
array: readonly T[];
constructor();
}
/**
* Gets the index associated with `key` in the backing array, if it is already present.
*/
export declare function get<T extends Key>(setarr: SetArray<T>, key: T): number | undefined;
/**
* Puts `key` into the backing array, if it is not already present. Returns
* the index of the `key` in the backing array.
*/
export declare function put<T extends Key>(setarr: SetArray<T>, key: T): number;
/**
* Pops the last added item out of the SetArray.
*/
export declare function pop<T extends Key>(setarr: SetArray<T>): void;
/**
* Removes the key, if it exists in the set.
*/
export declare function remove<T extends Key>(setarr: SetArray<T>, key: T): void;
export {};

View File

@@ -0,0 +1,12 @@
type GeneratedColumn = number;
type SourcesIndex = number;
type SourceLine = number;
type SourceColumn = number;
type NamesIndex = number;
export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex];
export declare const COLUMN = 0;
export declare const SOURCES_INDEX = 1;
export declare const SOURCE_LINE = 2;
export declare const SOURCE_COLUMN = 3;
export declare const NAMES_INDEX = 4;
export {};

View File

@@ -0,0 +1,43 @@
import type { SourceMapSegment } from './sourcemap-segment';
export interface SourceMapV3 {
file?: string | null;
names: readonly string[];
sourceRoot?: string;
sources: readonly (string | null)[];
sourcesContent?: readonly (string | null)[];
version: 3;
ignoreList?: readonly number[];
}
export interface EncodedSourceMap extends SourceMapV3 {
mappings: string;
}
export interface DecodedSourceMap extends SourceMapV3 {
mappings: readonly SourceMapSegment[][];
}
export interface Pos {
line: number;
column: number;
}
export interface OriginalPos extends Pos {
source: string;
}
export interface BindingExpressionRange {
start: Pos;
expression: string;
}
export type Mapping = {
generated: Pos;
source: undefined;
original: undefined;
name: undefined;
} | {
generated: Pos;
source: string;
original: Pos;
name: string;
} | {
generated: Pos;
source: string;
original: Pos;
name: undefined;
};

View File

@@ -0,0 +1,67 @@
{
"name": "@jridgewell/gen-mapping",
"version": "0.3.13",
"description": "Generate source maps",
"keywords": [
"source",
"map"
],
"main": "dist/gen-mapping.umd.js",
"module": "dist/gen-mapping.mjs",
"types": "types/gen-mapping.d.cts",
"files": [
"dist",
"src",
"types"
],
"exports": {
".": [
{
"import": {
"types": "./types/gen-mapping.d.mts",
"default": "./dist/gen-mapping.mjs"
},
"default": {
"types": "./types/gen-mapping.d.cts",
"default": "./dist/gen-mapping.umd.js"
}
},
"./dist/gen-mapping.umd.js"
],
"./package.json": "./package.json"
},
"scripts": {
"benchmark": "run-s build:code benchmark:*",
"benchmark:install": "cd benchmark && npm install",
"benchmark:only": "node --expose-gc benchmark/index.js",
"build": "run-s -n build:code build:types",
"build:code": "node ../../esbuild.mjs gen-mapping.ts",
"build:types": "run-s build:types:force build:types:emit build:types:mts",
"build:types:force": "rimraf tsconfig.build.tsbuildinfo",
"build:types:emit": "tsc --project tsconfig.build.json",
"build:types:mts": "node ../../mts-types.mjs",
"clean": "run-s -n clean:code clean:types",
"clean:code": "tsc --build --clean tsconfig.build.json",
"clean:types": "rimraf dist types",
"test": "run-s -n test:types test:only test:format",
"test:format": "prettier --check '{src,test}/**/*.ts'",
"test:only": "mocha",
"test:types": "eslint '{src,test}/**/*.ts'",
"lint": "run-s -n lint:types lint:format",
"lint:format": "npm run test:format -- --write",
"lint:types": "npm run test:types -- --fix",
"prepublishOnly": "npm run-s -n build test"
},
"homepage": "https://github.com/jridgewell/sourcemaps/tree/main/packages/gen-mapping",
"repository": {
"type": "git",
"url": "git+https://github.com/jridgewell/sourcemaps.git",
"directory": "packages/gen-mapping"
},
"author": "Justin Ridgewell <justin@ridgewell.name>",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
}

View File

@@ -0,0 +1,614 @@
import { SetArray, put, remove } from './set-array';
import {
encode,
// encodeGeneratedRanges,
// encodeOriginalScopes
} from '@jridgewell/sourcemap-codec';
import { TraceMap, decodedMappings } from '@jridgewell/trace-mapping';
import {
COLUMN,
SOURCES_INDEX,
SOURCE_LINE,
SOURCE_COLUMN,
NAMES_INDEX,
} from './sourcemap-segment';
import type { SourceMapInput } from '@jridgewell/trace-mapping';
// import type { OriginalScope, GeneratedRange } from '@jridgewell/sourcemap-codec';
import type { SourceMapSegment } from './sourcemap-segment';
import type {
DecodedSourceMap,
EncodedSourceMap,
Pos,
Mapping,
// BindingExpressionRange,
// OriginalPos,
// OriginalScopeInfo,
// GeneratedRangeInfo,
} from './types';
export type { DecodedSourceMap, EncodedSourceMap, Mapping };
export type Options = {
file?: string | null;
sourceRoot?: string | null;
};
const NO_NAME = -1;
/**
* Provides the state to generate a sourcemap.
*/
export class GenMapping {
declare private _names: SetArray<string>;
declare private _sources: SetArray<string>;
declare private _sourcesContent: (string | null)[];
declare private _mappings: SourceMapSegment[][];
// private declare _originalScopes: OriginalScope[][];
// private declare _generatedRanges: GeneratedRange[];
declare private _ignoreList: SetArray<number>;
declare file: string | null | undefined;
declare sourceRoot: string | null | undefined;
constructor({ file, sourceRoot }: Options = {}) {
this._names = new SetArray();
this._sources = new SetArray();
this._sourcesContent = [];
this._mappings = [];
// this._originalScopes = [];
// this._generatedRanges = [];
this.file = file;
this.sourceRoot = sourceRoot;
this._ignoreList = new SetArray();
}
}
interface PublicMap {
_names: GenMapping['_names'];
_sources: GenMapping['_sources'];
_sourcesContent: GenMapping['_sourcesContent'];
_mappings: GenMapping['_mappings'];
// _originalScopes: GenMapping['_originalScopes'];
// _generatedRanges: GenMapping['_generatedRanges'];
_ignoreList: GenMapping['_ignoreList'];
}
/**
* Typescript doesn't allow friend access to private fields, so this just casts the map into a type
* with public access modifiers.
*/
function cast(map: unknown): PublicMap {
return map as any;
}
/**
* A low-level API to associate a generated position with an original source position. Line and
* column here are 0-based, unlike `addMapping`.
*/
export function addSegment(
map: GenMapping,
genLine: number,
genColumn: number,
source?: null,
sourceLine?: null,
sourceColumn?: null,
name?: null,
content?: null,
): void;
export function addSegment(
map: GenMapping,
genLine: number,
genColumn: number,
source: string,
sourceLine: number,
sourceColumn: number,
name?: null,
content?: string | null,
): void;
export function addSegment(
map: GenMapping,
genLine: number,
genColumn: number,
source: string,
sourceLine: number,
sourceColumn: number,
name: string,
content?: string | null,
): void;
export function addSegment(
map: GenMapping,
genLine: number,
genColumn: number,
source?: string | null,
sourceLine?: number | null,
sourceColumn?: number | null,
name?: string | null,
content?: string | null,
): void {
return addSegmentInternal(
false,
map,
genLine,
genColumn,
source,
sourceLine,
sourceColumn,
name,
content,
);
}
/**
* A high-level API to associate a generated position with an original source position. Line is
* 1-based, but column is 0-based, due to legacy behavior in `source-map` library.
*/
export function addMapping(
map: GenMapping,
mapping: {
generated: Pos;
source?: null;
original?: null;
name?: null;
content?: null;
},
): void;
export function addMapping(
map: GenMapping,
mapping: {
generated: Pos;
source: string;
original: Pos;
name?: null;
content?: string | null;
},
): void;
export function addMapping(
map: GenMapping,
mapping: {
generated: Pos;
source: string;
original: Pos;
name: string;
content?: string | null;
},
): void;
export function addMapping(
map: GenMapping,
mapping: {
generated: Pos;
source?: string | null;
original?: Pos | null;
name?: string | null;
content?: string | null;
},
): void {
return addMappingInternal(false, map, mapping as Parameters<typeof addMappingInternal>[2]);
}
/**
* Same as `addSegment`, but will only add the segment if it generates useful information in the
* resulting map. This only works correctly if segments are added **in order**, meaning you should
* not add a segment with a lower generated line/column than one that came before.
*/
export const maybeAddSegment: typeof addSegment = (
map,
genLine,
genColumn,
source,
sourceLine,
sourceColumn,
name,
content,
) => {
return addSegmentInternal(
true,
map,
genLine,
genColumn,
source,
sourceLine,
sourceColumn,
name,
content,
);
};
/**
* Same as `addMapping`, but will only add the mapping if it generates useful information in the
* resulting map. This only works correctly if mappings are added **in order**, meaning you should
* not add a mapping with a lower generated line/column than one that came before.
*/
export const maybeAddMapping: typeof addMapping = (map, mapping) => {
return addMappingInternal(true, map, mapping as Parameters<typeof addMappingInternal>[2]);
};
/**
* Adds/removes the content of the source file to the source map.
*/
export function setSourceContent(map: GenMapping, source: string, content: string | null): void {
const {
_sources: sources,
_sourcesContent: sourcesContent,
// _originalScopes: originalScopes,
} = cast(map);
const index = put(sources, source);
sourcesContent[index] = content;
// if (index === originalScopes.length) originalScopes[index] = [];
}
export function setIgnore(map: GenMapping, source: string, ignore = true) {
const {
_sources: sources,
_sourcesContent: sourcesContent,
_ignoreList: ignoreList,
// _originalScopes: originalScopes,
} = cast(map);
const index = put(sources, source);
if (index === sourcesContent.length) sourcesContent[index] = null;
// if (index === originalScopes.length) originalScopes[index] = [];
if (ignore) put(ignoreList, index);
else remove(ignoreList, index);
}
/**
* Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects
* a sourcemap, or to JSON.stringify.
*/
export function toDecodedMap(map: GenMapping): DecodedSourceMap {
const {
_mappings: mappings,
_sources: sources,
_sourcesContent: sourcesContent,
_names: names,
_ignoreList: ignoreList,
// _originalScopes: originalScopes,
// _generatedRanges: generatedRanges,
} = cast(map);
removeEmptyFinalLines(mappings);
return {
version: 3,
file: map.file || undefined,
names: names.array,
sourceRoot: map.sourceRoot || undefined,
sources: sources.array,
sourcesContent,
mappings,
// originalScopes,
// generatedRanges,
ignoreList: ignoreList.array,
};
}
/**
* Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects
* a sourcemap, or to JSON.stringify.
*/
export function toEncodedMap(map: GenMapping): EncodedSourceMap {
const decoded = toDecodedMap(map);
return Object.assign({}, decoded, {
// originalScopes: decoded.originalScopes.map((os) => encodeOriginalScopes(os)),
// generatedRanges: encodeGeneratedRanges(decoded.generatedRanges as GeneratedRange[]),
mappings: encode(decoded.mappings as SourceMapSegment[][]),
});
}
/**
* Constructs a new GenMapping, using the already present mappings of the input.
*/
export function fromMap(input: SourceMapInput): GenMapping {
const map = new TraceMap(input);
const gen = new GenMapping({ file: map.file, sourceRoot: map.sourceRoot });
putAll(cast(gen)._names, map.names);
putAll(cast(gen)._sources, map.sources as string[]);
cast(gen)._sourcesContent = map.sourcesContent || map.sources.map(() => null);
cast(gen)._mappings = decodedMappings(map) as GenMapping['_mappings'];
// TODO: implement originalScopes/generatedRanges
if (map.ignoreList) putAll(cast(gen)._ignoreList, map.ignoreList);
return gen;
}
/**
* Returns an array of high-level mapping objects for every recorded segment, which could then be
* passed to the `source-map` library.
*/
export function allMappings(map: GenMapping): Mapping[] {
const out: Mapping[] = [];
const { _mappings: mappings, _sources: sources, _names: names } = cast(map);
for (let i = 0; i < mappings.length; i++) {
const line = mappings[i];
for (let j = 0; j < line.length; j++) {
const seg = line[j];
const generated = { line: i + 1, column: seg[COLUMN] };
let source: string | undefined = undefined;
let original: Pos | undefined = undefined;
let name: string | undefined = undefined;
if (seg.length !== 1) {
source = sources.array[seg[SOURCES_INDEX]];
original = { line: seg[SOURCE_LINE] + 1, column: seg[SOURCE_COLUMN] };
if (seg.length === 5) name = names.array[seg[NAMES_INDEX]];
}
out.push({ generated, source, original, name } as Mapping);
}
}
return out;
}
// This split declaration is only so that terser can elminiate the static initialization block.
function addSegmentInternal<S extends string | null | undefined>(
skipable: boolean,
map: GenMapping,
genLine: number,
genColumn: number,
source: S,
sourceLine: S extends string ? number : null | undefined,
sourceColumn: S extends string ? number : null | undefined,
name: S extends string ? string | null | undefined : null | undefined,
content: S extends string ? string | null | undefined : null | undefined,
): void {
const {
_mappings: mappings,
_sources: sources,
_sourcesContent: sourcesContent,
_names: names,
// _originalScopes: originalScopes,
} = cast(map);
const line = getIndex(mappings, genLine);
const index = getColumnIndex(line, genColumn);
if (!source) {
if (skipable && skipSourceless(line, index)) return;
return insert(line, index, [genColumn]);
}
// Sigh, TypeScript can't figure out sourceLine and sourceColumn aren't nullish if source
// isn't nullish.
assert<number>(sourceLine);
assert<number>(sourceColumn);
const sourcesIndex = put(sources, source);
const namesIndex = name ? put(names, name) : NO_NAME;
if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = content ?? null;
// if (sourcesIndex === originalScopes.length) originalScopes[sourcesIndex] = [];
if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) {
return;
}
return insert(
line,
index,
name
? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]
: [genColumn, sourcesIndex, sourceLine, sourceColumn],
);
}
function assert<T>(_val: unknown): asserts _val is T {
// noop.
}
function getIndex<T>(arr: T[][], index: number): T[] {
for (let i = arr.length; i <= index; i++) {
arr[i] = [];
}
return arr[index];
}
function getColumnIndex(line: SourceMapSegment[], genColumn: number): number {
let index = line.length;
for (let i = index - 1; i >= 0; index = i--) {
const current = line[i];
if (genColumn >= current[COLUMN]) break;
}
return index;
}
function insert<T>(array: T[], index: number, value: T) {
for (let i = array.length; i > index; i--) {
array[i] = array[i - 1];
}
array[index] = value;
}
function removeEmptyFinalLines(mappings: SourceMapSegment[][]) {
const { length } = mappings;
let len = length;
for (let i = len - 1; i >= 0; len = i, i--) {
if (mappings[i].length > 0) break;
}
if (len < length) mappings.length = len;
}
function putAll<T extends string | number>(setarr: SetArray<T>, array: T[]) {
for (let i = 0; i < array.length; i++) put(setarr, array[i]);
}
function skipSourceless(line: SourceMapSegment[], index: number): boolean {
// The start of a line is already sourceless, so adding a sourceless segment to the beginning
// doesn't generate any useful information.
if (index === 0) return true;
const prev = line[index - 1];
// If the previous segment is also sourceless, then adding another sourceless segment doesn't
// genrate any new information. Else, this segment will end the source/named segment and point to
// a sourceless position, which is useful.
return prev.length === 1;
}
function skipSource(
line: SourceMapSegment[],
index: number,
sourcesIndex: number,
sourceLine: number,
sourceColumn: number,
namesIndex: number,
): boolean {
// A source/named segment at the start of a line gives position at that genColumn
if (index === 0) return false;
const prev = line[index - 1];
// If the previous segment is sourceless, then we're transitioning to a source.
if (prev.length === 1) return false;
// If the previous segment maps to the exact same source position, then this segment doesn't
// provide any new position information.
return (
sourcesIndex === prev[SOURCES_INDEX] &&
sourceLine === prev[SOURCE_LINE] &&
sourceColumn === prev[SOURCE_COLUMN] &&
namesIndex === (prev.length === 5 ? prev[NAMES_INDEX] : NO_NAME)
);
}
function addMappingInternal<S extends string | null | undefined>(
skipable: boolean,
map: GenMapping,
mapping: {
generated: Pos;
source: S;
original: S extends string ? Pos : null | undefined;
name: S extends string ? string | null | undefined : null | undefined;
content: S extends string ? string | null | undefined : null | undefined;
},
) {
const { generated, source, original, name, content } = mapping;
if (!source) {
return addSegmentInternal(
skipable,
map,
generated.line - 1,
generated.column,
null,
null,
null,
null,
null,
);
}
assert<Pos>(original);
return addSegmentInternal(
skipable,
map,
generated.line - 1,
generated.column,
source as string,
original.line - 1,
original.column,
name,
content,
);
}
/*
export function addOriginalScope(
map: GenMapping,
data: {
start: Pos;
end: Pos;
source: string;
kind: string;
name?: string;
variables?: string[];
},
): OriginalScopeInfo {
const { start, end, source, kind, name, variables } = data;
const {
_sources: sources,
_sourcesContent: sourcesContent,
_originalScopes: originalScopes,
_names: names,
} = cast(map);
const index = put(sources, source);
if (index === sourcesContent.length) sourcesContent[index] = null;
if (index === originalScopes.length) originalScopes[index] = [];
const kindIndex = put(names, kind);
const scope: OriginalScope = name
? [start.line - 1, start.column, end.line - 1, end.column, kindIndex, put(names, name)]
: [start.line - 1, start.column, end.line - 1, end.column, kindIndex];
if (variables) {
scope.vars = variables.map((v) => put(names, v));
}
const len = originalScopes[index].push(scope);
return [index, len - 1, variables];
}
*/
// Generated Ranges
/*
export function addGeneratedRange(
map: GenMapping,
data: {
start: Pos;
isScope: boolean;
originalScope?: OriginalScopeInfo;
callsite?: OriginalPos;
},
): GeneratedRangeInfo {
const { start, isScope, originalScope, callsite } = data;
const {
_originalScopes: originalScopes,
_sources: sources,
_sourcesContent: sourcesContent,
_generatedRanges: generatedRanges,
} = cast(map);
const range: GeneratedRange = [
start.line - 1,
start.column,
0,
0,
originalScope ? originalScope[0] : -1,
originalScope ? originalScope[1] : -1,
];
if (originalScope?.[2]) {
range.bindings = originalScope[2].map(() => [[-1]]);
}
if (callsite) {
const index = put(sources, callsite.source);
if (index === sourcesContent.length) sourcesContent[index] = null;
if (index === originalScopes.length) originalScopes[index] = [];
range.callsite = [index, callsite.line - 1, callsite.column];
}
if (isScope) range.isScope = true;
generatedRanges.push(range);
return [range, originalScope?.[2]];
}
export function setEndPosition(range: GeneratedRangeInfo, pos: Pos) {
range[0][2] = pos.line - 1;
range[0][3] = pos.column;
}
export function addBinding(
map: GenMapping,
range: GeneratedRangeInfo,
variable: string,
expression: string | BindingExpressionRange,
) {
const { _names: names } = cast(map);
const bindings = (range[0].bindings ||= []);
const vars = range[1];
const index = vars!.indexOf(variable);
const binding = getIndex(bindings, index);
if (typeof expression === 'string') binding[0] = [put(names, expression)];
else {
const { start } = expression;
binding.push([put(names, expression.expression), start.line - 1, start.column]);
}
}
*/

View File

@@ -0,0 +1,82 @@
type Key = string | number | symbol;
/**
* SetArray acts like a `Set` (allowing only one occurrence of a string `key`), but provides the
* index of the `key` in the backing array.
*
* This is designed to allow synchronizing a second array with the contents of the backing array,
* like how in a sourcemap `sourcesContent[i]` is the source content associated with `source[i]`,
* and there are never duplicates.
*/
export class SetArray<T extends Key = Key> {
declare private _indexes: Record<T, number | undefined>;
declare array: readonly T[];
constructor() {
this._indexes = { __proto__: null } as any;
this.array = [];
}
}
interface PublicSet<T extends Key> {
array: T[];
_indexes: SetArray<T>['_indexes'];
}
/**
* Typescript doesn't allow friend access to private fields, so this just casts the set into a type
* with public access modifiers.
*/
function cast<T extends Key>(set: SetArray<T>): PublicSet<T> {
return set as any;
}
/**
* Gets the index associated with `key` in the backing array, if it is already present.
*/
export function get<T extends Key>(setarr: SetArray<T>, key: T): number | undefined {
return cast(setarr)._indexes[key];
}
/**
* Puts `key` into the backing array, if it is not already present. Returns
* the index of the `key` in the backing array.
*/
export function put<T extends Key>(setarr: SetArray<T>, key: T): number {
// The key may or may not be present. If it is present, it's a number.
const index = get(setarr, key);
if (index !== undefined) return index;
const { array, _indexes: indexes } = cast(setarr);
const length = array.push(key);
return (indexes[key] = length - 1);
}
/**
* Pops the last added item out of the SetArray.
*/
export function pop<T extends Key>(setarr: SetArray<T>): void {
const { array, _indexes: indexes } = cast(setarr);
if (array.length === 0) return;
const last = array.pop()!;
indexes[last] = undefined;
}
/**
* Removes the key, if it exists in the set.
*/
export function remove<T extends Key>(setarr: SetArray<T>, key: T): void {
const index = get(setarr, key);
if (index === undefined) return;
const { array, _indexes: indexes } = cast(setarr);
for (let i = index + 1; i < array.length; i++) {
const k = array[i];
array[i - 1] = k;
indexes[k]!--;
}
indexes[key] = undefined;
array.pop();
}

View File

@@ -0,0 +1,16 @@
type GeneratedColumn = number;
type SourcesIndex = number;
type SourceLine = number;
type SourceColumn = number;
type NamesIndex = number;
export type SourceMapSegment =
| [GeneratedColumn]
| [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn]
| [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex];
export const COLUMN = 0;
export const SOURCES_INDEX = 1;
export const SOURCE_LINE = 2;
export const SOURCE_COLUMN = 3;
export const NAMES_INDEX = 4;

View File

@@ -0,0 +1,61 @@
// import type { GeneratedRange, OriginalScope } from '@jridgewell/sourcemap-codec';
import type { SourceMapSegment } from './sourcemap-segment';
export interface SourceMapV3 {
file?: string | null;
names: readonly string[];
sourceRoot?: string;
sources: readonly (string | null)[];
sourcesContent?: readonly (string | null)[];
version: 3;
ignoreList?: readonly number[];
}
export interface EncodedSourceMap extends SourceMapV3 {
mappings: string;
// originalScopes: string[];
// generatedRanges: string;
}
export interface DecodedSourceMap extends SourceMapV3 {
mappings: readonly SourceMapSegment[][];
// originalScopes: readonly OriginalScope[][];
// generatedRanges: readonly GeneratedRange[];
}
export interface Pos {
line: number; // 1-based
column: number; // 0-based
}
export interface OriginalPos extends Pos {
source: string;
}
export interface BindingExpressionRange {
start: Pos;
expression: string;
}
// export type OriginalScopeInfo = [number, number, string[] | undefined];
// export type GeneratedRangeInfo = [GeneratedRange, string[] | undefined];
export type Mapping =
| {
generated: Pos;
source: undefined;
original: undefined;
name: undefined;
}
| {
generated: Pos;
source: string;
original: Pos;
name: string;
}
| {
generated: Pos;
source: string;
original: Pos;
name: undefined;
};

View File

@@ -0,0 +1,89 @@
import type { SourceMapInput } from '@jridgewell/trace-mapping';
import type { DecodedSourceMap, EncodedSourceMap, Pos, Mapping } from './types.cts';
export type { DecodedSourceMap, EncodedSourceMap, Mapping };
export type Options = {
file?: string | null;
sourceRoot?: string | null;
};
/**
* Provides the state to generate a sourcemap.
*/
export declare class GenMapping {
private _names;
private _sources;
private _sourcesContent;
private _mappings;
private _ignoreList;
file: string | null | undefined;
sourceRoot: string | null | undefined;
constructor({ file, sourceRoot }?: Options);
}
/**
* A low-level API to associate a generated position with an original source position. Line and
* column here are 0-based, unlike `addMapping`.
*/
export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source?: null, sourceLine?: null, sourceColumn?: null, name?: null, content?: null): void;
export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name?: null, content?: string | null): void;
export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name: string, content?: string | null): void;
/**
* A high-level API to associate a generated position with an original source position. Line is
* 1-based, but column is 0-based, due to legacy behavior in `source-map` library.
*/
export declare function addMapping(map: GenMapping, mapping: {
generated: Pos;
source?: null;
original?: null;
name?: null;
content?: null;
}): void;
export declare function addMapping(map: GenMapping, mapping: {
generated: Pos;
source: string;
original: Pos;
name?: null;
content?: string | null;
}): void;
export declare function addMapping(map: GenMapping, mapping: {
generated: Pos;
source: string;
original: Pos;
name: string;
content?: string | null;
}): void;
/**
* Same as `addSegment`, but will only add the segment if it generates useful information in the
* resulting map. This only works correctly if segments are added **in order**, meaning you should
* not add a segment with a lower generated line/column than one that came before.
*/
export declare const maybeAddSegment: typeof addSegment;
/**
* Same as `addMapping`, but will only add the mapping if it generates useful information in the
* resulting map. This only works correctly if mappings are added **in order**, meaning you should
* not add a mapping with a lower generated line/column than one that came before.
*/
export declare const maybeAddMapping: typeof addMapping;
/**
* Adds/removes the content of the source file to the source map.
*/
export declare function setSourceContent(map: GenMapping, source: string, content: string | null): void;
export declare function setIgnore(map: GenMapping, source: string, ignore?: boolean): void;
/**
* Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects
* a sourcemap, or to JSON.stringify.
*/
export declare function toDecodedMap(map: GenMapping): DecodedSourceMap;
/**
* Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects
* a sourcemap, or to JSON.stringify.
*/
export declare function toEncodedMap(map: GenMapping): EncodedSourceMap;
/**
* Constructs a new GenMapping, using the already present mappings of the input.
*/
export declare function fromMap(input: SourceMapInput): GenMapping;
/**
* Returns an array of high-level mapping objects for every recorded segment, which could then be
* passed to the `source-map` library.
*/
export declare function allMappings(map: GenMapping): Mapping[];
//# sourceMappingURL=gen-mapping.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"gen-mapping.d.ts","sourceRoot":"","sources":["../src/gen-mapping.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,89 @@
import type { SourceMapInput } from '@jridgewell/trace-mapping';
import type { DecodedSourceMap, EncodedSourceMap, Pos, Mapping } from './types.mts';
export type { DecodedSourceMap, EncodedSourceMap, Mapping };
export type Options = {
file?: string | null;
sourceRoot?: string | null;
};
/**
* Provides the state to generate a sourcemap.
*/
export declare class GenMapping {
private _names;
private _sources;
private _sourcesContent;
private _mappings;
private _ignoreList;
file: string | null | undefined;
sourceRoot: string | null | undefined;
constructor({ file, sourceRoot }?: Options);
}
/**
* A low-level API to associate a generated position with an original source position. Line and
* column here are 0-based, unlike `addMapping`.
*/
export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source?: null, sourceLine?: null, sourceColumn?: null, name?: null, content?: null): void;
export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name?: null, content?: string | null): void;
export declare function addSegment(map: GenMapping, genLine: number, genColumn: number, source: string, sourceLine: number, sourceColumn: number, name: string, content?: string | null): void;
/**
* A high-level API to associate a generated position with an original source position. Line is
* 1-based, but column is 0-based, due to legacy behavior in `source-map` library.
*/
export declare function addMapping(map: GenMapping, mapping: {
generated: Pos;
source?: null;
original?: null;
name?: null;
content?: null;
}): void;
export declare function addMapping(map: GenMapping, mapping: {
generated: Pos;
source: string;
original: Pos;
name?: null;
content?: string | null;
}): void;
export declare function addMapping(map: GenMapping, mapping: {
generated: Pos;
source: string;
original: Pos;
name: string;
content?: string | null;
}): void;
/**
* Same as `addSegment`, but will only add the segment if it generates useful information in the
* resulting map. This only works correctly if segments are added **in order**, meaning you should
* not add a segment with a lower generated line/column than one that came before.
*/
export declare const maybeAddSegment: typeof addSegment;
/**
* Same as `addMapping`, but will only add the mapping if it generates useful information in the
* resulting map. This only works correctly if mappings are added **in order**, meaning you should
* not add a mapping with a lower generated line/column than one that came before.
*/
export declare const maybeAddMapping: typeof addMapping;
/**
* Adds/removes the content of the source file to the source map.
*/
export declare function setSourceContent(map: GenMapping, source: string, content: string | null): void;
export declare function setIgnore(map: GenMapping, source: string, ignore?: boolean): void;
/**
* Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects
* a sourcemap, or to JSON.stringify.
*/
export declare function toDecodedMap(map: GenMapping): DecodedSourceMap;
/**
* Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects
* a sourcemap, or to JSON.stringify.
*/
export declare function toEncodedMap(map: GenMapping): EncodedSourceMap;
/**
* Constructs a new GenMapping, using the already present mappings of the input.
*/
export declare function fromMap(input: SourceMapInput): GenMapping;
/**
* Returns an array of high-level mapping objects for every recorded segment, which could then be
* passed to the `source-map` library.
*/
export declare function allMappings(map: GenMapping): Mapping[];
//# sourceMappingURL=gen-mapping.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"gen-mapping.d.ts","sourceRoot":"","sources":["../src/gen-mapping.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,33 @@
type Key = string | number | symbol;
/**
* SetArray acts like a `Set` (allowing only one occurrence of a string `key`), but provides the
* index of the `key` in the backing array.
*
* This is designed to allow synchronizing a second array with the contents of the backing array,
* like how in a sourcemap `sourcesContent[i]` is the source content associated with `source[i]`,
* and there are never duplicates.
*/
export declare class SetArray<T extends Key = Key> {
private _indexes;
array: readonly T[];
constructor();
}
/**
* Gets the index associated with `key` in the backing array, if it is already present.
*/
export declare function get<T extends Key>(setarr: SetArray<T>, key: T): number | undefined;
/**
* Puts `key` into the backing array, if it is not already present. Returns
* the index of the `key` in the backing array.
*/
export declare function put<T extends Key>(setarr: SetArray<T>, key: T): number;
/**
* Pops the last added item out of the SetArray.
*/
export declare function pop<T extends Key>(setarr: SetArray<T>): void;
/**
* Removes the key, if it exists in the set.
*/
export declare function remove<T extends Key>(setarr: SetArray<T>, key: T): void;
export {};
//# sourceMappingURL=set-array.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"set-array.d.ts","sourceRoot":"","sources":["../src/set-array.ts"],"names":[],"mappings":"AAAA,KAAK,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpC;;;;;;;GAOG;AACH,qBAAa,QAAQ,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG;IACvC,QAAgB,QAAQ,CAAgC;IAChD,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;;CAM7B;AAeD;;GAEG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,SAAS,CAElF;AAED;;;GAGG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,CAStE;AAED;;GAEG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAM5D;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,CAYvE"}

View File

@@ -0,0 +1,33 @@
type Key = string | number | symbol;
/**
* SetArray acts like a `Set` (allowing only one occurrence of a string `key`), but provides the
* index of the `key` in the backing array.
*
* This is designed to allow synchronizing a second array with the contents of the backing array,
* like how in a sourcemap `sourcesContent[i]` is the source content associated with `source[i]`,
* and there are never duplicates.
*/
export declare class SetArray<T extends Key = Key> {
private _indexes;
array: readonly T[];
constructor();
}
/**
* Gets the index associated with `key` in the backing array, if it is already present.
*/
export declare function get<T extends Key>(setarr: SetArray<T>, key: T): number | undefined;
/**
* Puts `key` into the backing array, if it is not already present. Returns
* the index of the `key` in the backing array.
*/
export declare function put<T extends Key>(setarr: SetArray<T>, key: T): number;
/**
* Pops the last added item out of the SetArray.
*/
export declare function pop<T extends Key>(setarr: SetArray<T>): void;
/**
* Removes the key, if it exists in the set.
*/
export declare function remove<T extends Key>(setarr: SetArray<T>, key: T): void;
export {};
//# sourceMappingURL=set-array.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"set-array.d.ts","sourceRoot":"","sources":["../src/set-array.ts"],"names":[],"mappings":"AAAA,KAAK,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpC;;;;;;;GAOG;AACH,qBAAa,QAAQ,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG;IACvC,QAAgB,QAAQ,CAAgC;IAChD,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;;CAM7B;AAeD;;GAEG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,SAAS,CAElF;AAED;;;GAGG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,CAStE;AAED;;GAEG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAM5D;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,CAYvE"}

View File

@@ -0,0 +1,13 @@
type GeneratedColumn = number;
type SourcesIndex = number;
type SourceLine = number;
type SourceColumn = number;
type NamesIndex = number;
export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex];
export declare const COLUMN = 0;
export declare const SOURCES_INDEX = 1;
export declare const SOURCE_LINE = 2;
export declare const SOURCE_COLUMN = 3;
export declare const NAMES_INDEX = 4;
export {};
//# sourceMappingURL=sourcemap-segment.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"sourcemap-segment.d.ts","sourceRoot":"","sources":["../src/sourcemap-segment.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AACzB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AAEzB,MAAM,MAAM,gBAAgB,GACxB,CAAC,eAAe,CAAC,GACjB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,CAAC,GACzD,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAE1E,eAAO,MAAM,MAAM,IAAI,CAAC;AACxB,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC"}

View File

@@ -0,0 +1,13 @@
type GeneratedColumn = number;
type SourcesIndex = number;
type SourceLine = number;
type SourceColumn = number;
type NamesIndex = number;
export type SourceMapSegment = [GeneratedColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn] | [GeneratedColumn, SourcesIndex, SourceLine, SourceColumn, NamesIndex];
export declare const COLUMN = 0;
export declare const SOURCES_INDEX = 1;
export declare const SOURCE_LINE = 2;
export declare const SOURCE_COLUMN = 3;
export declare const NAMES_INDEX = 4;
export {};
//# sourceMappingURL=sourcemap-segment.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"sourcemap-segment.d.ts","sourceRoot":"","sources":["../src/sourcemap-segment.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AACzB,KAAK,YAAY,GAAG,MAAM,CAAC;AAC3B,KAAK,UAAU,GAAG,MAAM,CAAC;AAEzB,MAAM,MAAM,gBAAgB,GACxB,CAAC,eAAe,CAAC,GACjB,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,CAAC,GACzD,CAAC,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;AAE1E,eAAO,MAAM,MAAM,IAAI,CAAC;AACxB,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAI,CAAC"}

View File

@@ -0,0 +1,44 @@
import type { SourceMapSegment } from './sourcemap-segment.cts';
export interface SourceMapV3 {
file?: string | null;
names: readonly string[];
sourceRoot?: string;
sources: readonly (string | null)[];
sourcesContent?: readonly (string | null)[];
version: 3;
ignoreList?: readonly number[];
}
export interface EncodedSourceMap extends SourceMapV3 {
mappings: string;
}
export interface DecodedSourceMap extends SourceMapV3 {
mappings: readonly SourceMapSegment[][];
}
export interface Pos {
line: number;
column: number;
}
export interface OriginalPos extends Pos {
source: string;
}
export interface BindingExpressionRange {
start: Pos;
expression: string;
}
export type Mapping = {
generated: Pos;
source: undefined;
original: undefined;
name: undefined;
} | {
generated: Pos;
source: string;
original: Pos;
name: string;
} | {
generated: Pos;
source: string;
original: Pos;
name: undefined;
};
//# sourceMappingURL=types.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACpC,cAAc,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC5C,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,MAAM,CAAC;CAGlB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAC;CAGzC;AAED,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,GAAG;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,GAAG,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD,MAAM,MAAM,OAAO,GACf;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;CACjB,GACD;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,GACD;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;CACjB,CAAC"}

View File

@@ -0,0 +1,44 @@
import type { SourceMapSegment } from './sourcemap-segment.mts';
export interface SourceMapV3 {
file?: string | null;
names: readonly string[];
sourceRoot?: string;
sources: readonly (string | null)[];
sourcesContent?: readonly (string | null)[];
version: 3;
ignoreList?: readonly number[];
}
export interface EncodedSourceMap extends SourceMapV3 {
mappings: string;
}
export interface DecodedSourceMap extends SourceMapV3 {
mappings: readonly SourceMapSegment[][];
}
export interface Pos {
line: number;
column: number;
}
export interface OriginalPos extends Pos {
source: string;
}
export interface BindingExpressionRange {
start: Pos;
expression: string;
}
export type Mapping = {
generated: Pos;
source: undefined;
original: undefined;
name: undefined;
} | {
generated: Pos;
source: string;
original: Pos;
name: string;
} | {
generated: Pos;
source: string;
original: Pos;
name: undefined;
};
//# sourceMappingURL=types.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACpC,cAAc,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC5C,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,MAAM,CAAC;CAGlB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAC;CAGzC;AAED,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,GAAG;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,GAAG,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD,MAAM,MAAM,OAAO,GACf;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;CACjB,GACD;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,GACD;IACE,SAAS,EAAE,GAAG,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;CACjB,CAAC"}

View File

@@ -0,0 +1,19 @@
Copyright 2024 Justin Ridgewell <justin@ridgewell.name>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,218 @@
# @jridgewell/remapping
> Remap sequential sourcemaps through transformations to point at the original source code
Remapping allows you to take the sourcemaps generated through transforming your code and "remap"
them to the original source locations. Think "my minified code, transformed with babel and bundled
with webpack", all pointing to the correct location in your original source code.
With remapping, none of your source code transformations need to be aware of the input's sourcemap,
they only need to generate an output sourcemap. This greatly simplifies building custom
transformations (think a find-and-replace).
## Installation
```sh
npm install @jridgewell/remapping
```
## Usage
```typescript
function remapping(
map: SourceMap | SourceMap[],
loader: (file: string, ctx: LoaderContext) => (SourceMap | null | undefined),
options?: { excludeContent: boolean, decodedMappings: boolean }
): SourceMap;
// LoaderContext gives the loader the importing sourcemap, tree depth, the ability to override the
// "source" location (where child sources are resolved relative to, or the location of original
// source), and the ability to override the "content" of an original source for inclusion in the
// output sourcemap.
type LoaderContext = {
readonly importer: string;
readonly depth: number;
source: string;
content: string | null | undefined;
}
```
`remapping` takes the final output sourcemap, and a `loader` function. For every source file pointer
in the sourcemap, the `loader` will be called with the resolved path. If the path itself represents
a transformed file (it has a sourcmap associated with it), then the `loader` should return that
sourcemap. If not, the path will be treated as an original, untransformed source code.
```js
// Babel transformed "helloworld.js" into "transformed.js"
const transformedMap = JSON.stringify({
file: 'transformed.js',
// 1st column of 2nd line of output file translates into the 1st source
// file, line 3, column 2
mappings: ';CAEE',
sources: ['helloworld.js'],
version: 3,
});
// Uglify minified "transformed.js" into "transformed.min.js"
const minifiedTransformedMap = JSON.stringify({
file: 'transformed.min.js',
// 0th column of 1st line of output file translates into the 1st source
// file, line 2, column 1.
mappings: 'AACC',
names: [],
sources: ['transformed.js'],
version: 3,
});
const remapped = remapping(
minifiedTransformedMap,
(file, ctx) => {
// The "transformed.js" file is an transformed file.
if (file === 'transformed.js') {
// The root importer is empty.
console.assert(ctx.importer === '');
// The depth in the sourcemap tree we're currently loading.
// The root `minifiedTransformedMap` is depth 0, and its source children are depth 1, etc.
console.assert(ctx.depth === 1);
return transformedMap;
}
// Loader will be called to load transformedMap's source file pointers as well.
console.assert(file === 'helloworld.js');
// `transformed.js`'s sourcemap points into `helloworld.js`.
console.assert(ctx.importer === 'transformed.js');
// This is a source child of `transformed`, which is a source child of `minifiedTransformedMap`.
console.assert(ctx.depth === 2);
return null;
}
);
console.log(remapped);
// {
// file: 'transpiled.min.js',
// mappings: 'AAEE',
// sources: ['helloworld.js'],
// version: 3,
// };
```
In this example, `loader` will be called twice:
1. `"transformed.js"`, the first source file pointer in the `minifiedTransformedMap`. We return the
associated sourcemap for it (its a transformed file, after all) so that sourcemap locations can
be traced through it into the source files it represents.
2. `"helloworld.js"`, our original, unmodified source code. This file does not have a sourcemap, so
we return `null`.
The `remapped` sourcemap now points from `transformed.min.js` into locations in `helloworld.js`. If
you were to read the `mappings`, it says "0th column of the first line output line points to the 1st
column of the 2nd line of the file `helloworld.js`".
### Multiple transformations of a file
As a convenience, if you have multiple single-source transformations of a file, you may pass an
array of sourcemap files in the order of most-recent transformation sourcemap first. Note that this
changes the `importer` and `depth` of each call to our loader. So our above example could have been
written as:
```js
const remapped = remapping(
[minifiedTransformedMap, transformedMap],
() => null
);
console.log(remapped);
// {
// file: 'transpiled.min.js',
// mappings: 'AAEE',
// sources: ['helloworld.js'],
// version: 3,
// };
```
### Advanced control of the loading graph
#### `source`
The `source` property can overridden to any value to change the location of the current load. Eg,
for an original source file, it allows us to change the location to the original source regardless
of what the sourcemap source entry says. And for transformed files, it allows us to change the
relative resolving location for child sources of the loaded sourcemap.
```js
const remapped = remapping(
minifiedTransformedMap,
(file, ctx) => {
if (file === 'transformed.js') {
// We pretend the transformed.js file actually exists in the 'src/' directory. When the nested
// source files are loaded, they will now be relative to `src/`.
ctx.source = 'src/transformed.js';
return transformedMap;
}
console.assert(file === 'src/helloworld.js');
// We could futher change the source of this original file, eg, to be inside a nested directory
// itself. This will be reflected in the remapped sourcemap.
ctx.source = 'src/nested/transformed.js';
return null;
}
);
console.log(remapped);
// {
// …,
// sources: ['src/nested/helloworld.js'],
// };
```
#### `content`
The `content` property can be overridden when we encounter an original source file. Eg, this allows
you to manually provide the source content of the original file regardless of whether the
`sourcesContent` field is present in the parent sourcemap. It can also be set to `null` to remove
the source content.
```js
const remapped = remapping(
minifiedTransformedMap,
(file, ctx) => {
if (file === 'transformed.js') {
// transformedMap does not include a `sourcesContent` field, so usually the remapped sourcemap
// would not include any `sourcesContent` values.
return transformedMap;
}
console.assert(file === 'helloworld.js');
// We can read the file to provide the source content.
ctx.content = fs.readFileSync(file, 'utf8');
return null;
}
);
console.log(remapped);
// {
// …,
// sourcesContent: [
// 'console.log("Hello world!")',
// ],
// };
```
### Options
#### excludeContent
By default, `excludeContent` is `false`. Passing `{ excludeContent: true }` will exclude the
`sourcesContent` field from the returned sourcemap. This is mainly useful when you want to reduce
the size out the sourcemap.
#### decodedMappings
By default, `decodedMappings` is `false`. Passing `{ decodedMappings: true }` will leave the
`mappings` field in a [decoded state](https://github.com/rich-harris/sourcemap-codec) instead of
encoding into a VLQ string.

View File

@@ -0,0 +1,144 @@
// src/build-source-map-tree.ts
import { TraceMap } from "@jridgewell/trace-mapping";
// src/source-map-tree.ts
import { GenMapping, maybeAddSegment, setIgnore, setSourceContent } from "@jridgewell/gen-mapping";
import { traceSegment, decodedMappings } from "@jridgewell/trace-mapping";
var SOURCELESS_MAPPING = /* @__PURE__ */ SegmentObject("", -1, -1, "", null, false);
var EMPTY_SOURCES = [];
function SegmentObject(source, line, column, name, content, ignore) {
return { source, line, column, name, content, ignore };
}
function Source(map, sources, source, content, ignore) {
return {
map,
sources,
source,
content,
ignore
};
}
function MapSource(map, sources) {
return Source(map, sources, "", null, false);
}
function OriginalSource(source, content, ignore) {
return Source(null, EMPTY_SOURCES, source, content, ignore);
}
function traceMappings(tree) {
const gen = new GenMapping({ file: tree.map.file });
const { sources: rootSources, map } = tree;
const rootNames = map.names;
const rootMappings = decodedMappings(map);
for (let i = 0; i < rootMappings.length; i++) {
const segments = rootMappings[i];
for (let j = 0; j < segments.length; j++) {
const segment = segments[j];
const genCol = segment[0];
let traced = SOURCELESS_MAPPING;
if (segment.length !== 1) {
const source2 = rootSources[segment[1]];
traced = originalPositionFor(
source2,
segment[2],
segment[3],
segment.length === 5 ? rootNames[segment[4]] : ""
);
if (traced == null) continue;
}
const { column, line, name, content, source, ignore } = traced;
maybeAddSegment(gen, i, genCol, source, line, column, name);
if (source && content != null) setSourceContent(gen, source, content);
if (ignore) setIgnore(gen, source, true);
}
}
return gen;
}
function originalPositionFor(source, line, column, name) {
if (!source.map) {
return SegmentObject(source.source, line, column, name, source.content, source.ignore);
}
const segment = traceSegment(source.map, line, column);
if (segment == null) return null;
if (segment.length === 1) return SOURCELESS_MAPPING;
return originalPositionFor(
source.sources[segment[1]],
segment[2],
segment[3],
segment.length === 5 ? source.map.names[segment[4]] : name
);
}
// src/build-source-map-tree.ts
function asArray(value) {
if (Array.isArray(value)) return value;
return [value];
}
function buildSourceMapTree(input, loader) {
const maps = asArray(input).map((m) => new TraceMap(m, ""));
const map = maps.pop();
for (let i = 0; i < maps.length; i++) {
if (maps[i].sources.length > 1) {
throw new Error(
`Transformation map ${i} must have exactly one source file.
Did you specify these with the most recent transformation maps first?`
);
}
}
let tree = build(map, loader, "", 0);
for (let i = maps.length - 1; i >= 0; i--) {
tree = MapSource(maps[i], [tree]);
}
return tree;
}
function build(map, loader, importer, importerDepth) {
const { resolvedSources, sourcesContent, ignoreList } = map;
const depth = importerDepth + 1;
const children = resolvedSources.map((sourceFile, i) => {
const ctx = {
importer,
depth,
source: sourceFile || "",
content: void 0,
ignore: void 0
};
const sourceMap = loader(ctx.source, ctx);
const { source, content, ignore } = ctx;
if (sourceMap) return build(new TraceMap(sourceMap, source), loader, source, depth);
const sourceContent = content !== void 0 ? content : sourcesContent ? sourcesContent[i] : null;
const ignored = ignore !== void 0 ? ignore : ignoreList ? ignoreList.includes(i) : false;
return OriginalSource(source, sourceContent, ignored);
});
return MapSource(map, children);
}
// src/source-map.ts
import { toDecodedMap, toEncodedMap } from "@jridgewell/gen-mapping";
var SourceMap = class {
constructor(map, options) {
const out = options.decodedMappings ? toDecodedMap(map) : toEncodedMap(map);
this.version = out.version;
this.file = out.file;
this.mappings = out.mappings;
this.names = out.names;
this.ignoreList = out.ignoreList;
this.sourceRoot = out.sourceRoot;
this.sources = out.sources;
if (!options.excludeContent) {
this.sourcesContent = out.sourcesContent;
}
}
toString() {
return JSON.stringify(this);
}
};
// src/remapping.ts
function remapping(input, loader, options) {
const opts = typeof options === "object" ? options : { excludeContent: !!options, decodedMappings: false };
const tree = buildSourceMapTree(input, loader);
return new SourceMap(traceMappings(tree), opts);
}
export {
remapping as default
};
//# sourceMappingURL=remapping.mjs.map

View File

@@ -0,0 +1,6 @@
{
"version": 3,
"sources": ["../src/build-source-map-tree.ts", "../src/source-map-tree.ts", "../src/source-map.ts", "../src/remapping.ts"],
"mappings": "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",
"names": ["source"]
}

View File

@@ -0,0 +1,212 @@
(function (global, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
factory(module, require('@jridgewell/gen-mapping'), require('@jridgewell/trace-mapping'));
module.exports = def(module);
} else if (typeof define === 'function' && define.amd) {
define(['module', '@jridgewell/gen-mapping', '@jridgewell/trace-mapping'], function(mod) {
factory.apply(this, arguments);
mod.exports = def(mod);
});
} else {
const mod = { exports: {} };
factory(mod, global.genMapping, global.traceMapping);
global = typeof globalThis !== 'undefined' ? globalThis : global || self;
global.remapping = def(mod);
}
function def(m) { return 'default' in m.exports ? m.exports.default : m.exports; }
})(this, (function (module, require_genMapping, require_traceMapping) {
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// umd:@jridgewell/trace-mapping
var require_trace_mapping = __commonJS({
"umd:@jridgewell/trace-mapping"(exports, module2) {
module2.exports = require_traceMapping;
}
});
// umd:@jridgewell/gen-mapping
var require_gen_mapping = __commonJS({
"umd:@jridgewell/gen-mapping"(exports, module2) {
module2.exports = require_genMapping;
}
});
// src/remapping.ts
var remapping_exports = {};
__export(remapping_exports, {
default: () => remapping
});
module.exports = __toCommonJS(remapping_exports);
// src/build-source-map-tree.ts
var import_trace_mapping2 = __toESM(require_trace_mapping());
// src/source-map-tree.ts
var import_gen_mapping = __toESM(require_gen_mapping());
var import_trace_mapping = __toESM(require_trace_mapping());
var SOURCELESS_MAPPING = /* @__PURE__ */ SegmentObject("", -1, -1, "", null, false);
var EMPTY_SOURCES = [];
function SegmentObject(source, line, column, name, content, ignore) {
return { source, line, column, name, content, ignore };
}
function Source(map, sources, source, content, ignore) {
return {
map,
sources,
source,
content,
ignore
};
}
function MapSource(map, sources) {
return Source(map, sources, "", null, false);
}
function OriginalSource(source, content, ignore) {
return Source(null, EMPTY_SOURCES, source, content, ignore);
}
function traceMappings(tree) {
const gen = new import_gen_mapping.GenMapping({ file: tree.map.file });
const { sources: rootSources, map } = tree;
const rootNames = map.names;
const rootMappings = (0, import_trace_mapping.decodedMappings)(map);
for (let i = 0; i < rootMappings.length; i++) {
const segments = rootMappings[i];
for (let j = 0; j < segments.length; j++) {
const segment = segments[j];
const genCol = segment[0];
let traced = SOURCELESS_MAPPING;
if (segment.length !== 1) {
const source2 = rootSources[segment[1]];
traced = originalPositionFor(
source2,
segment[2],
segment[3],
segment.length === 5 ? rootNames[segment[4]] : ""
);
if (traced == null) continue;
}
const { column, line, name, content, source, ignore } = traced;
(0, import_gen_mapping.maybeAddSegment)(gen, i, genCol, source, line, column, name);
if (source && content != null) (0, import_gen_mapping.setSourceContent)(gen, source, content);
if (ignore) (0, import_gen_mapping.setIgnore)(gen, source, true);
}
}
return gen;
}
function originalPositionFor(source, line, column, name) {
if (!source.map) {
return SegmentObject(source.source, line, column, name, source.content, source.ignore);
}
const segment = (0, import_trace_mapping.traceSegment)(source.map, line, column);
if (segment == null) return null;
if (segment.length === 1) return SOURCELESS_MAPPING;
return originalPositionFor(
source.sources[segment[1]],
segment[2],
segment[3],
segment.length === 5 ? source.map.names[segment[4]] : name
);
}
// src/build-source-map-tree.ts
function asArray(value) {
if (Array.isArray(value)) return value;
return [value];
}
function buildSourceMapTree(input, loader) {
const maps = asArray(input).map((m) => new import_trace_mapping2.TraceMap(m, ""));
const map = maps.pop();
for (let i = 0; i < maps.length; i++) {
if (maps[i].sources.length > 1) {
throw new Error(
`Transformation map ${i} must have exactly one source file.
Did you specify these with the most recent transformation maps first?`
);
}
}
let tree = build(map, loader, "", 0);
for (let i = maps.length - 1; i >= 0; i--) {
tree = MapSource(maps[i], [tree]);
}
return tree;
}
function build(map, loader, importer, importerDepth) {
const { resolvedSources, sourcesContent, ignoreList } = map;
const depth = importerDepth + 1;
const children = resolvedSources.map((sourceFile, i) => {
const ctx = {
importer,
depth,
source: sourceFile || "",
content: void 0,
ignore: void 0
};
const sourceMap = loader(ctx.source, ctx);
const { source, content, ignore } = ctx;
if (sourceMap) return build(new import_trace_mapping2.TraceMap(sourceMap, source), loader, source, depth);
const sourceContent = content !== void 0 ? content : sourcesContent ? sourcesContent[i] : null;
const ignored = ignore !== void 0 ? ignore : ignoreList ? ignoreList.includes(i) : false;
return OriginalSource(source, sourceContent, ignored);
});
return MapSource(map, children);
}
// src/source-map.ts
var import_gen_mapping2 = __toESM(require_gen_mapping());
var SourceMap = class {
constructor(map, options) {
const out = options.decodedMappings ? (0, import_gen_mapping2.toDecodedMap)(map) : (0, import_gen_mapping2.toEncodedMap)(map);
this.version = out.version;
this.file = out.file;
this.mappings = out.mappings;
this.names = out.names;
this.ignoreList = out.ignoreList;
this.sourceRoot = out.sourceRoot;
this.sources = out.sources;
if (!options.excludeContent) {
this.sourcesContent = out.sourcesContent;
}
}
toString() {
return JSON.stringify(this);
}
};
// src/remapping.ts
function remapping(input, loader, options) {
const opts = typeof options === "object" ? options : { excludeContent: !!options, decodedMappings: false };
const tree = buildSourceMapTree(input, loader);
return new SourceMap(traceMappings(tree), opts);
}
}));
//# sourceMappingURL=remapping.umd.js.map

View File

@@ -0,0 +1,6 @@
{
"version": 3,
"sources": ["umd:@jridgewell/trace-mapping", "umd:@jridgewell/gen-mapping", "../src/remapping.ts", "../src/build-source-map-tree.ts", "../src/source-map-tree.ts", "../src/source-map.ts"],
"mappings": "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",
"names": ["module", "module", "import_trace_mapping", "source", "import_gen_mapping"]
}

View File

@@ -0,0 +1,71 @@
{
"name": "@jridgewell/remapping",
"version": "2.3.5",
"description": "Remap sequential sourcemaps through transformations to point at the original source code",
"keywords": [
"source",
"map",
"remap"
],
"main": "dist/remapping.umd.js",
"module": "dist/remapping.mjs",
"types": "types/remapping.d.cts",
"files": [
"dist",
"src",
"types"
],
"exports": {
".": [
{
"import": {
"types": "./types/remapping.d.mts",
"default": "./dist/remapping.mjs"
},
"default": {
"types": "./types/remapping.d.cts",
"default": "./dist/remapping.umd.js"
}
},
"./dist/remapping.umd.js"
],
"./package.json": "./package.json"
},
"scripts": {
"benchmark": "run-s build:code benchmark:*",
"benchmark:install": "cd benchmark && npm install",
"benchmark:only": "node --expose-gc benchmark/index.js",
"build": "run-s -n build:code build:types",
"build:code": "node ../../esbuild.mjs remapping.ts",
"build:types": "run-s build:types:force build:types:emit build:types:mts",
"build:types:force": "rimraf tsconfig.build.tsbuildinfo",
"build:types:emit": "tsc --project tsconfig.build.json",
"build:types:mts": "node ../../mts-types.mjs",
"clean": "run-s -n clean:code clean:types",
"clean:code": "tsc --build --clean tsconfig.build.json",
"clean:types": "rimraf dist types",
"test": "run-s -n test:types test:only test:format",
"test:format": "prettier --check '{src,test}/**/*.ts'",
"test:only": "mocha",
"test:types": "eslint '{src,test}/**/*.ts'",
"lint": "run-s -n lint:types lint:format",
"lint:format": "npm run test:format -- --write",
"lint:types": "npm run test:types -- --fix",
"prepublishOnly": "npm run-s -n build test"
},
"homepage": "https://github.com/jridgewell/sourcemaps/tree/main/packages/remapping",
"repository": {
"type": "git",
"url": "git+https://github.com/jridgewell/sourcemaps.git",
"directory": "packages/remapping"
},
"author": "Justin Ridgewell <justin@ridgewell.name>",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"devDependencies": {
"source-map": "0.6.1"
}
}

View File

@@ -0,0 +1,89 @@
import { TraceMap } from '@jridgewell/trace-mapping';
import { OriginalSource, MapSource } from './source-map-tree';
import type { Sources, MapSource as MapSourceType } from './source-map-tree';
import type { SourceMapInput, SourceMapLoader, LoaderContext } from './types';
function asArray<T>(value: T | T[]): T[] {
if (Array.isArray(value)) return value;
return [value];
}
/**
* Recursively builds a tree structure out of sourcemap files, with each node
* being either an `OriginalSource` "leaf" or a `SourceMapTree` composed of
* `OriginalSource`s and `SourceMapTree`s.
*
* Every sourcemap is composed of a collection of source files and mappings
* into locations of those source files. When we generate a `SourceMapTree` for
* the sourcemap, we attempt to load each source file's own sourcemap. If it
* does not have an associated sourcemap, it is considered an original,
* unmodified source file.
*/
export default function buildSourceMapTree(
input: SourceMapInput | SourceMapInput[],
loader: SourceMapLoader,
): MapSourceType {
const maps = asArray(input).map((m) => new TraceMap(m, ''));
const map = maps.pop()!;
for (let i = 0; i < maps.length; i++) {
if (maps[i].sources.length > 1) {
throw new Error(
`Transformation map ${i} must have exactly one source file.\n` +
'Did you specify these with the most recent transformation maps first?',
);
}
}
let tree = build(map, loader, '', 0);
for (let i = maps.length - 1; i >= 0; i--) {
tree = MapSource(maps[i], [tree]);
}
return tree;
}
function build(
map: TraceMap,
loader: SourceMapLoader,
importer: string,
importerDepth: number,
): MapSourceType {
const { resolvedSources, sourcesContent, ignoreList } = map;
const depth = importerDepth + 1;
const children = resolvedSources.map((sourceFile: string | null, i: number): Sources => {
// The loading context gives the loader more information about why this file is being loaded
// (eg, from which importer). It also allows the loader to override the location of the loaded
// sourcemap/original source, or to override the content in the sourcesContent field if it's
// an unmodified source file.
const ctx: LoaderContext = {
importer,
depth,
source: sourceFile || '',
content: undefined,
ignore: undefined,
};
// Use the provided loader callback to retrieve the file's sourcemap.
// TODO: We should eventually support async loading of sourcemap files.
const sourceMap = loader(ctx.source, ctx);
const { source, content, ignore } = ctx;
// If there is a sourcemap, then we need to recurse into it to load its source files.
if (sourceMap) return build(new TraceMap(sourceMap, source), loader, source, depth);
// Else, it's an unmodified source file.
// The contents of this unmodified source file can be overridden via the loader context,
// allowing it to be explicitly null or a string. If it remains undefined, we fall back to
// the importing sourcemap's `sourcesContent` field.
const sourceContent =
content !== undefined ? content : sourcesContent ? sourcesContent[i] : null;
const ignored = ignore !== undefined ? ignore : ignoreList ? ignoreList.includes(i) : false;
return OriginalSource(source, sourceContent, ignored);
});
return MapSource(map, children);
}

View File

@@ -0,0 +1,42 @@
import buildSourceMapTree from './build-source-map-tree';
import { traceMappings } from './source-map-tree';
import SourceMap from './source-map';
import type { SourceMapInput, SourceMapLoader, Options } from './types';
export type {
SourceMapSegment,
EncodedSourceMap,
EncodedSourceMap as RawSourceMap,
DecodedSourceMap,
SourceMapInput,
SourceMapLoader,
LoaderContext,
Options,
} from './types';
export type { SourceMap };
/**
* Traces through all the mappings in the root sourcemap, through the sources
* (and their sourcemaps), all the way back to the original source location.
*
* `loader` will be called every time we encounter a source file. If it returns
* a sourcemap, we will recurse into that sourcemap to continue the trace. If
* it returns a falsey value, that source file is treated as an original,
* unmodified source file.
*
* Pass `excludeContent` to exclude any self-containing source file content
* from the output sourcemap.
*
* Pass `decodedMappings` to receive a SourceMap with decoded (instead of
* VLQ encoded) mappings.
*/
export default function remapping(
input: SourceMapInput | SourceMapInput[],
loader: SourceMapLoader,
options?: boolean | Options,
): SourceMap {
const opts =
typeof options === 'object' ? options : { excludeContent: !!options, decodedMappings: false };
const tree = buildSourceMapTree(input, loader);
return new SourceMap(traceMappings(tree), opts);
}

View File

@@ -0,0 +1,172 @@
import { GenMapping, maybeAddSegment, setIgnore, setSourceContent } from '@jridgewell/gen-mapping';
import { traceSegment, decodedMappings } from '@jridgewell/trace-mapping';
import type { TraceMap } from '@jridgewell/trace-mapping';
export type SourceMapSegmentObject = {
column: number;
line: number;
name: string;
source: string;
content: string | null;
ignore: boolean;
};
export type OriginalSource = {
map: null;
sources: Sources[];
source: string;
content: string | null;
ignore: boolean;
};
export type MapSource = {
map: TraceMap;
sources: Sources[];
source: string;
content: null;
ignore: false;
};
export type Sources = OriginalSource | MapSource;
const SOURCELESS_MAPPING = /* #__PURE__ */ SegmentObject('', -1, -1, '', null, false);
const EMPTY_SOURCES: Sources[] = [];
function SegmentObject(
source: string,
line: number,
column: number,
name: string,
content: string | null,
ignore: boolean,
): SourceMapSegmentObject {
return { source, line, column, name, content, ignore };
}
function Source(
map: TraceMap,
sources: Sources[],
source: '',
content: null,
ignore: false,
): MapSource;
function Source(
map: null,
sources: Sources[],
source: string,
content: string | null,
ignore: boolean,
): OriginalSource;
function Source(
map: TraceMap | null,
sources: Sources[],
source: string | '',
content: string | null,
ignore: boolean,
): Sources {
return {
map,
sources,
source,
content,
ignore,
} as any;
}
/**
* MapSource represents a single sourcemap, with the ability to trace mappings into its child nodes
* (which may themselves be SourceMapTrees).
*/
export function MapSource(map: TraceMap, sources: Sources[]): MapSource {
return Source(map, sources, '', null, false);
}
/**
* A "leaf" node in the sourcemap tree, representing an original, unmodified source file. Recursive
* segment tracing ends at the `OriginalSource`.
*/
export function OriginalSource(
source: string,
content: string | null,
ignore: boolean,
): OriginalSource {
return Source(null, EMPTY_SOURCES, source, content, ignore);
}
/**
* traceMappings is only called on the root level SourceMapTree, and begins the process of
* resolving each mapping in terms of the original source files.
*/
export function traceMappings(tree: MapSource): GenMapping {
// TODO: Eventually support sourceRoot, which has to be removed because the sources are already
// fully resolved. We'll need to make sources relative to the sourceRoot before adding them.
const gen = new GenMapping({ file: tree.map.file });
const { sources: rootSources, map } = tree;
const rootNames = map.names;
const rootMappings = decodedMappings(map);
for (let i = 0; i < rootMappings.length; i++) {
const segments = rootMappings[i];
for (let j = 0; j < segments.length; j++) {
const segment = segments[j];
const genCol = segment[0];
let traced: SourceMapSegmentObject | null = SOURCELESS_MAPPING;
// 1-length segments only move the current generated column, there's no source information
// to gather from it.
if (segment.length !== 1) {
const source = rootSources[segment[1]];
traced = originalPositionFor(
source,
segment[2],
segment[3],
segment.length === 5 ? rootNames[segment[4]] : '',
);
// If the trace is invalid, then the trace ran into a sourcemap that doesn't contain a
// respective segment into an original source.
if (traced == null) continue;
}
const { column, line, name, content, source, ignore } = traced;
maybeAddSegment(gen, i, genCol, source, line, column, name);
if (source && content != null) setSourceContent(gen, source, content);
if (ignore) setIgnore(gen, source, true);
}
}
return gen;
}
/**
* originalPositionFor is only called on children SourceMapTrees. It recurses down into its own
* child SourceMapTrees, until we find the original source map.
*/
export function originalPositionFor(
source: Sources,
line: number,
column: number,
name: string,
): SourceMapSegmentObject | null {
if (!source.map) {
return SegmentObject(source.source, line, column, name, source.content, source.ignore);
}
const segment = traceSegment(source.map, line, column);
// If we couldn't find a segment, then this doesn't exist in the sourcemap.
if (segment == null) return null;
// 1-length segments only move the current generated column, there's no source information
// to gather from it.
if (segment.length === 1) return SOURCELESS_MAPPING;
return originalPositionFor(
source.sources[segment[1]],
segment[2],
segment[3],
segment.length === 5 ? source.map.names[segment[4]] : name,
);
}

View File

@@ -0,0 +1,38 @@
import { toDecodedMap, toEncodedMap } from '@jridgewell/gen-mapping';
import type { GenMapping } from '@jridgewell/gen-mapping';
import type { DecodedSourceMap, EncodedSourceMap, Options } from './types';
/**
* A SourceMap v3 compatible sourcemap, which only includes fields that were
* provided to it.
*/
export default class SourceMap {
declare file?: string | null;
declare mappings: EncodedSourceMap['mappings'] | DecodedSourceMap['mappings'];
declare sourceRoot?: string;
declare names: string[];
declare sources: (string | null)[];
declare sourcesContent?: (string | null)[];
declare version: 3;
declare ignoreList: number[] | undefined;
constructor(map: GenMapping, options: Options) {
const out = options.decodedMappings ? toDecodedMap(map) : toEncodedMap(map);
this.version = out.version; // SourceMap spec says this should be first.
this.file = out.file;
this.mappings = out.mappings as SourceMap['mappings'];
this.names = out.names as SourceMap['names'];
this.ignoreList = out.ignoreList as SourceMap['ignoreList'];
this.sourceRoot = out.sourceRoot;
this.sources = out.sources as SourceMap['sources'];
if (!options.excludeContent) {
this.sourcesContent = out.sourcesContent as SourceMap['sourcesContent'];
}
}
toString(): string {
return JSON.stringify(this);
}
}

View File

@@ -0,0 +1,27 @@
import type { SourceMapInput } from '@jridgewell/trace-mapping';
export type {
SourceMapSegment,
DecodedSourceMap,
EncodedSourceMap,
} from '@jridgewell/trace-mapping';
export type { SourceMapInput };
export type LoaderContext = {
readonly importer: string;
readonly depth: number;
source: string;
content: string | null | undefined;
ignore: boolean | undefined;
};
export type SourceMapLoader = (
file: string,
ctx: LoaderContext,
) => SourceMapInput | null | undefined | void;
export type Options = {
excludeContent?: boolean;
decodedMappings?: boolean;
};

View File

@@ -0,0 +1,15 @@
import type { MapSource as MapSourceType } from './source-map-tree.cts';
import type { SourceMapInput, SourceMapLoader } from './types.cts';
/**
* Recursively builds a tree structure out of sourcemap files, with each node
* being either an `OriginalSource` "leaf" or a `SourceMapTree` composed of
* `OriginalSource`s and `SourceMapTree`s.
*
* Every sourcemap is composed of a collection of source files and mappings
* into locations of those source files. When we generate a `SourceMapTree` for
* the sourcemap, we attempt to load each source file's own sourcemap. If it
* does not have an associated sourcemap, it is considered an original,
* unmodified source file.
*/
export = function buildSourceMapTree(input: SourceMapInput | SourceMapInput[], loader: SourceMapLoader): MapSourceType;
//# sourceMappingURL=build-source-map-tree.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"build-source-map-tree.d.ts","sourceRoot":"","sources":["../src/build-source-map-tree.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAW,SAAS,IAAI,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAiB,MAAM,SAAS,CAAC;AAO9E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CACxC,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,EACxC,MAAM,EAAE,eAAe,GACtB,aAAa,CAkBf"}

View File

@@ -0,0 +1,15 @@
import type { MapSource as MapSourceType } from './source-map-tree.mts';
import type { SourceMapInput, SourceMapLoader } from './types.mts';
/**
* Recursively builds a tree structure out of sourcemap files, with each node
* being either an `OriginalSource` "leaf" or a `SourceMapTree` composed of
* `OriginalSource`s and `SourceMapTree`s.
*
* Every sourcemap is composed of a collection of source files and mappings
* into locations of those source files. When we generate a `SourceMapTree` for
* the sourcemap, we attempt to load each source file's own sourcemap. If it
* does not have an associated sourcemap, it is considered an original,
* unmodified source file.
*/
export default function buildSourceMapTree(input: SourceMapInput | SourceMapInput[], loader: SourceMapLoader): MapSourceType;
//# sourceMappingURL=build-source-map-tree.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"build-source-map-tree.d.ts","sourceRoot":"","sources":["../src/build-source-map-tree.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAW,SAAS,IAAI,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAiB,MAAM,SAAS,CAAC;AAO9E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CACxC,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,EACxC,MAAM,EAAE,eAAe,GACtB,aAAa,CAkBf"}

View File

@@ -0,0 +1,21 @@
import SourceMap from './source-map.cts';
import type { SourceMapInput, SourceMapLoader, Options } from './types.cts';
export type { SourceMapSegment, EncodedSourceMap, EncodedSourceMap as RawSourceMap, DecodedSourceMap, SourceMapInput, SourceMapLoader, LoaderContext, Options, } from './types.cts';
export type { SourceMap };
/**
* Traces through all the mappings in the root sourcemap, through the sources
* (and their sourcemaps), all the way back to the original source location.
*
* `loader` will be called every time we encounter a source file. If it returns
* a sourcemap, we will recurse into that sourcemap to continue the trace. If
* it returns a falsey value, that source file is treated as an original,
* unmodified source file.
*
* Pass `excludeContent` to exclude any self-containing source file content
* from the output sourcemap.
*
* Pass `decodedMappings` to receive a SourceMap with decoded (instead of
* VLQ encoded) mappings.
*/
export = function remapping(input: SourceMapInput | SourceMapInput[], loader: SourceMapLoader, options?: boolean | Options): SourceMap;
//# sourceMappingURL=remapping.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"remapping.d.ts","sourceRoot":"","sources":["../src/remapping.ts"],"names":[],"mappings":"AAEA,OAAO,SAAS,MAAM,cAAc,CAAC;AAErC,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACxE,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,IAAI,YAAY,EAChC,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,aAAa,EACb,OAAO,GACR,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,EACxC,MAAM,EAAE,eAAe,EACvB,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,GAC1B,SAAS,CAKX"}

View File

@@ -0,0 +1,21 @@
import SourceMap from './source-map.mts';
import type { SourceMapInput, SourceMapLoader, Options } from './types.mts';
export type { SourceMapSegment, EncodedSourceMap, EncodedSourceMap as RawSourceMap, DecodedSourceMap, SourceMapInput, SourceMapLoader, LoaderContext, Options, } from './types.mts';
export type { SourceMap };
/**
* Traces through all the mappings in the root sourcemap, through the sources
* (and their sourcemaps), all the way back to the original source location.
*
* `loader` will be called every time we encounter a source file. If it returns
* a sourcemap, we will recurse into that sourcemap to continue the trace. If
* it returns a falsey value, that source file is treated as an original,
* unmodified source file.
*
* Pass `excludeContent` to exclude any self-containing source file content
* from the output sourcemap.
*
* Pass `decodedMappings` to receive a SourceMap with decoded (instead of
* VLQ encoded) mappings.
*/
export default function remapping(input: SourceMapInput | SourceMapInput[], loader: SourceMapLoader, options?: boolean | Options): SourceMap;
//# sourceMappingURL=remapping.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"remapping.d.ts","sourceRoot":"","sources":["../src/remapping.ts"],"names":[],"mappings":"AAEA,OAAO,SAAS,MAAM,cAAc,CAAC;AAErC,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACxE,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,IAAI,YAAY,EAChC,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,aAAa,EACb,OAAO,GACR,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,EACxC,MAAM,EAAE,eAAe,EACvB,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,GAC1B,SAAS,CAKX"}

Some files were not shown because too many files have changed in this diff Show More