diff --git a/USAGE.md b/USAGE.md index 364e5c3..5d7faa2 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1216,40 +1216,8 @@ Next scheduled operations: With multi-job support, each history record includes which job performed the operation: ```bash -# View history for specific job +# View history for a specific job sudo backupd job show production # Includes recent history - -# Filter history by job (programmatic) -# In scripts, use the get_job_history function: -source /etc/backupd/lib/history.sh -get_job_history "production" "all" 20 # Last 20 ops for production job -get_job_history "staging" "database" 10 # Last 10 DB backups for staging -``` - -**Job history summary:** - -```bash -# Get summary statistics for all jobs (programmatic) -source /etc/backupd/lib/history.sh -get_history_by_jobs -``` - -Returns: -```json -{ - "default": { - "total": 45, - "success": 43, - "failed": 2, - "last_backup": "2026-01-07T03:00:46+00:00" - }, - "production": { - "total": 20, - "success": 20, - "failed": 0, - "last_backup": "2026-01-07T02:00:15+00:00" - } -} ``` --- diff --git a/install.sh b/install.sh old mode 100755 new mode 100644 index 9f276ee..b8a3258 --- a/install.sh +++ b/install.sh @@ -180,37 +180,6 @@ pkg_install() { esac } -# Get the install command hint for user-facing error messages -get_install_hint() { - local package="$1" - local pm - pm=$(detect_package_manager) - - case "$pm" in - apt) - echo "sudo apt install $package" - ;; - dnf) - echo "sudo dnf install $package" - ;; - yum) - echo "sudo yum install $package" - ;; - pacman) - echo "sudo pacman -S $package" - ;; - apk) - echo "sudo apk add $package" - ;; - zypper) - echo "sudo zypper install $package" - ;; - *) - echo "Install '$package' using your system's package manager" - ;; - esac -} - print_banner() { echo -e "${CYAN}" echo "╔═══════════════════════════════════════════════════════════╗" diff --git a/lib/core.sh b/lib/core.sh old mode 100755 new mode 100644 index 2795f86..e55eb32 --- a/lib/core.sh +++ b/lib/core.sh @@ -63,19 +63,6 @@ if [[ -z "${BACKUPD_TRAP_SET:-}" ]]; then BACKUPD_TRAP_SET=1 fi -# Helper to run a command and track its PID for cleanup -# Usage: run_tracked_command restic check -r "$repo" -run_tracked_command() { - "$@" & - local pid=$! - BACKUPD_CHILD_PIDS+=("$pid") - wait "$pid" - local exit_code=$? - # Remove PID from tracking array - BACKUPD_CHILD_PIDS=("${BACKUPD_CHILD_PIDS[@]/$pid/}") - return $exit_code -} - # ---------- Package Manager Functions ---------- # Package manager detection (cached) @@ -292,19 +279,6 @@ json_output() { echo "$1" } -# Build simple JSON key-value pair - usage: json_kv "key" "value" -json_kv() { - local key="$1" - local value="$2" - # Escape special characters in value - value="${value//\\/\\\\}" - value="${value//\"/\\\"}" - value="${value//$'\n'/\\n}" - value="${value//$'\r'/\\r}" - value="${value//$'\t'/\\t}" - printf '"%s": "%s"' "$key" "$value" -} - # Check if JSON output mode is enabled is_json_output() { [[ "${JSON_OUTPUT:-0}" -eq 1 ]] @@ -429,54 +403,6 @@ validate_password() { # ---------- System Check Functions ---------- -# Check available disk space (in MB) -check_disk_space() { - local path="$1" - local required_mb="${2:-1000}" # Default 1GB - - local available_mb - available_mb=$(df -m "$path" 2>/dev/null | awk 'NR==2 {print $4}') - - if [[ -z "$available_mb" ]]; then - print_warning "Could not check disk space - proceeding anyway" - log_warn "df output could not be parsed for path: $path" - return 0 - fi - - if [[ "$available_mb" -lt "$required_mb" ]]; then - print_error "Insufficient disk space. Available: ${available_mb}MB, Required: ${required_mb}MB" - return 1 - fi - - return 0 -} - -# Check network connectivity (supports curl/wget fallback) -# BACKUPD-007 FIX: Add wget fallback for systems without curl -check_network() { - local host="${1:-1.1.1.1}" - local timeout="${2:-5}" - - # Try curl first (ICMP often blocked on servers) - if command -v curl &>/dev/null; then - if curl -s --connect-timeout "$timeout" "https://www.google.com" &>/dev/null; then - return 0 - fi - elif command -v wget &>/dev/null; then - if wget -q --timeout="$timeout" -O /dev/null "https://www.google.com" 2>/dev/null; then - return 0 - fi - fi - - # Fallback to ping - if ! ping -c 1 -W "$timeout" "$host" &>/dev/null; then - print_error "No network connectivity" - return 1 - fi - - return 0 -} - # Download file with curl/wget fallback # Usage: download_to_file URL OUTPUT_FILE [TIMEOUT] # BACKUPD-007 FIX: Support both curl and wget for maximum compatibility @@ -510,26 +436,6 @@ fetch_url() { fi } -# ---------- MySQL Helper Functions ---------- - -# Create MySQL credentials file (more secure than command line) -create_mysql_auth_file() { - local user="$1" - local pass="$2" - local auth_file - - auth_file="$(mktemp)" - chmod 600 "$auth_file" - - cat > "$auth_file" << EOF -[client] -user=$user -password=$pass -EOF - - echo "$auth_file" -} - # ---------- Logging Functions ---------- # Maximum log file size (10MB) @@ -583,24 +489,6 @@ create_secure_temp() { echo "$temp_dir" } -# Safe file write (atomic) -safe_write_file() { - local target="$1" - local content="$2" - local temp_file - - temp_file="$(mktemp "${target}.XXXXXXXXXX")" - - if echo "$content" > "$temp_file" 2>/dev/null; then - chmod 600 "$temp_file" - mv "$temp_file" "$target" - return 0 - else - rm -f "$temp_file" - return 1 - fi -} - # ---------- Panel Detection Functions ---------- # Panel definitions: name|pattern|webroot_subdir|detection_method @@ -804,11 +692,6 @@ get_panel_info() { esac } -# Get all panel keys -get_all_panel_keys() { - echo "${!PANEL_DEFINITIONS[@]}" | tr ' ' '\n' | sort -} - # Count sites for a given pattern count_sites_for_pattern() { local pattern="$1" @@ -821,60 +704,6 @@ count_sites_for_pattern() { echo "$count" } -# ---------- Site Naming Functions ---------- - -# Get site name/URL from various app types -get_site_name() { - local site_path="$1" - local owner="${2:-www-data}" - local name="" - - # 1. WordPress: wp option get siteurl - if [[ -f "$site_path/wp-config.php" ]]; then - if su -l -s /bin/bash "$owner" -c "command -v wp >/dev/null 2>&1" 2>/dev/null; then - name="$(su -l -s /bin/bash "$owner" -c "cd '$site_path' && wp option get siteurl 2>/dev/null" 2>/dev/null || true)" - fi - if [[ -z "$name" ]]; then - name="$(grep -E "define\s*\(\s*['\"]WP_HOME['\"]" "$site_path/wp-config.php" 2>/dev/null | head -1 | sed -E "s/.*['\"]https?:\/\/([^'\"]+)['\"].*/https:\/\/\1/" || true)" - fi - [[ -n "$name" ]] && echo "$name" && return 0 - fi - - # 2. Laravel: APP_URL from .env - if [[ -f "$site_path/.env" ]] && [[ -f "$site_path/artisan" ]]; then - name="$(grep -E "^APP_URL=" "$site_path/.env" 2>/dev/null | cut -d'=' -f2 | tr -d '"' | tr -d "'" || true)" - [[ -n "$name" ]] && echo "$name" && return 0 - fi - - # 3. Node.js: name from package.json - if [[ -f "$site_path/package.json" ]]; then - name="$(grep -E '"name"\s*:' "$site_path/package.json" 2>/dev/null | head -1 | sed -E 's/.*"name"\s*:\s*"([^"]+)".*/\1/' || true)" - [[ -n "$name" ]] && echo "$name" && return 0 - fi - - # 4. Generic: try to extract from nginx/apache configs in common locations - # Check for server_name in nginx configs - if [[ -d "/etc/nginx/sites-enabled" ]]; then - local nginx_name - nginx_name="$(grep -rh "server_name" /etc/nginx/sites-enabled/ 2>/dev/null | grep -i "$(basename "$site_path")" | head -1 | awk '{print $2}' | tr -d ';' || true)" - [[ -n "$nginx_name" && "$nginx_name" != "_" ]] && echo "$nginx_name" && return 0 - fi - - # 5. Fallback: use folder name - echo "$(basename "$site_path")" -} - -# Sanitize name for use as filename -sanitize_for_filename() { - local s="$1" - s="$(echo -n "$s" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')" - s="${s//:\/\//__}"; s="${s//\//__}" - s="$(echo -n "$s" | sed -E 's/[^a-z0-9._-]+/_/g')" - s="${s%.}" - [[ -z "$s" ]] && s="unknown-site" - printf "%s" "$s" -} - # ---------- Dependency Installation ---------- # Install rclone with SHA256 verification diff --git a/lib/crypto.sh b/lib/crypto.sh old mode 100755 new mode 100644 index 8f2f7aa..7308f80 --- a/lib/crypto.sh +++ b/lib/crypto.sh @@ -435,28 +435,3 @@ migrate_secrets() { echo "Migration complete!" return 0 } - -# Check if migration is recommended -migration_recommended() { - local secrets_dir="$1" - local current_version best_version - - current_version="$(get_crypto_version "$secrets_dir")" - best_version="$(get_best_crypto_version)" - - [[ "$current_version" -lt "$best_version" ]] -} - -# Get migration recommendation message -get_migration_recommendation() { - local secrets_dir="$1" - local current_version best_version - - current_version="$(get_crypto_version "$secrets_dir")" - best_version="$(get_best_crypto_version)" - - if [[ "$current_version" -lt "$best_version" ]]; then - echo "Encryption upgrade available: $(get_crypto_name "$current_version") → $(get_crypto_name "$best_version")" - echo "Run 'backupd --migrate-encryption' to upgrade" - fi -} diff --git a/lib/generators.sh b/lib/generators.sh old mode 100755 new mode 100644 index 48d0cb9..c904481 --- a/lib/generators.sh +++ b/lib/generators.sh @@ -2111,41 +2111,3 @@ regenerate_all_job_scripts() { return 0 } - -# Generate scripts for default job from global config -# Used for backward compatibility during migration -# Usage: generate_default_job_scripts_from_config -generate_default_job_scripts_from_config() { - local install_dir="${INSTALL_DIR:-/etc/backupd}" - - # Source jobs module - if ! declare -f job_exists &>/dev/null; then - source "$LIB_DIR/jobs.sh" 2>/dev/null || source "$install_dir/lib/jobs.sh" 2>/dev/null || { - print_error "Jobs module not found" - return 1 - } - fi - - # Create default job if it doesn't exist - if ! job_exists "default"; then - create_job "default" || return 1 - fi - - # Copy global config values to default job - local config_file="$install_dir/.config" - if [[ -f "$config_file" ]]; then - local keys=("RCLONE_REMOTE" "RCLONE_DB_PATH" "RCLONE_FILES_PATH" "RETENTION_DAYS" "RETENTION_DESC" - "WEB_PATH_PATTERN" "WEBROOT_SUBDIR" "DO_DATABASE" "DO_FILES" "PANEL_KEY") - local key value - - for key in "${keys[@]}"; do - value="$(grep "^${key}=" "$config_file" 2>/dev/null | cut -d'=' -f2- | tr -d '"')" || value="" - if [[ -n "$value" ]]; then - save_job_config "default" "$key" "$value" - fi - done - fi - - # Generate scripts for default job - generate_job_scripts "default" -} diff --git a/lib/history.sh b/lib/history.sh index ab18f50..2fcf932 100644 --- a/lib/history.sh +++ b/lib/history.sh @@ -112,70 +112,3 @@ get_next_backup_times() { local verify_full_next=$(echo "$timer_out" | grep "backupd-verify-full.timer" | awk '{print $1, $2, $3}') echo "{\"database\":\"${db_next:-not scheduled}\",\"files\":\"${files_next:-not scheduled}\",\"verify_quick\":\"${verify_next:-not scheduled}\",\"verify_full\":\"${verify_full_next:-not scheduled}\"}" } - -# Get history records filtered by job name (v3.1.0) -# Usage: get_job_history "production" "all" 20 -get_job_history() { - local job_name="${1:-default}" type="${2:-all}" limit="${3:-20}" - [[ ! -f "$HISTORY_FILE" ]] && echo "[]" && return 0 - - local records - # Filter by job name first - local job_filtered - job_filtered=$(grep "\"job\":\"$job_name\"" "$HISTORY_FILE" 2>/dev/null || echo "") - - [[ -z "$job_filtered" ]] && echo "[]" && return 0 - - case "$type" in - all) - records=$(echo "$job_filtered" | tail -n "$limit" | tac) - ;; - backup|backups) - records=$(echo "$job_filtered" | grep -E '"type":"(database|files)"' | tail -n "$limit" | tac) - ;; - verify|verifications) - records=$(echo "$job_filtered" | grep -E '"type":"verify_(quick|full)"' | tail -n "$limit" | tac) - ;; - *) - records=$(echo "$job_filtered" | grep "\"type\":\"$type\"" | tail -n "$limit" | tac) - ;; - esac - - echo "["; local first=1 - while IFS= read -r line; do - [[ -z "$line" ]] && continue - [[ $first -eq 0 ]] && echo "," - echo " $line"; first=0 - done <<< "$records" - echo "]" -} - -# Get history summary by job (v3.1.0) -# Returns summary stats for each job -get_history_by_jobs() { - [[ ! -f "$HISTORY_FILE" ]] && echo "{}" && return 0 - - # Get unique job names from history - local jobs - jobs=$(grep -oE '"job":"[^"]*"' "$HISTORY_FILE" 2>/dev/null | sort -u | cut -d'"' -f4) - - echo "{" - local first=1 - while IFS= read -r job; do - [[ -z "$job" ]] && continue - [[ $first -eq 0 ]] && echo "," - first=0 - - local total success failed - total=$(grep "\"job\":\"$job\"" "$HISTORY_FILE" | wc -l) - success=$(grep "\"job\":\"$job\"" "$HISTORY_FILE" | grep '"status":"success"' | wc -l) - failed=$(grep "\"job\":\"$job\"" "$HISTORY_FILE" | grep '"status":"failed"' | wc -l) - - local last_backup - last_backup=$(grep "\"job\":\"$job\"" "$HISTORY_FILE" | grep -E '"type":"(database|files)"' | tail -1 | grep -oE '"started_at":"[^"]*"' | cut -d'"' -f4) - - echo -n " \"$job\": {\"total\": $total, \"success\": $success, \"failed\": $failed, \"last_backup\": \"${last_backup:-never}\"}" - done <<< "$jobs" - echo - echo "}" -} diff --git a/tests/unit/core.bats b/tests/unit/core.bats index 84b3775..bbef90a 100644 --- a/tests/unit/core.bats +++ b/tests/unit/core.bats @@ -135,43 +135,6 @@ setup() { assert_failure } -# ---------- sanitize_for_filename() ---------- - -@test "sanitize_for_filename lowercases and preserves dots" { - run sanitize_for_filename "MyDomain.COM" - assert_output "mydomain.com" -} - -@test "sanitize_for_filename replaces :// with __ and preserves dots" { - run sanitize_for_filename "https://example.com" - assert_output "https__example.com" -} - -@test "sanitize_for_filename replaces slashes with __" { - run sanitize_for_filename "path/to/dir" - assert_output "path__to__dir" -} - -@test "sanitize_for_filename removes whitespace" { - run sanitize_for_filename "my domain" - assert_output "mydomain" -} - -@test "sanitize_for_filename replaces special chars with underscore" { - run sanitize_for_filename "site@host:8080" - assert_output "site_host_8080" -} - -@test "sanitize_for_filename returns unknown-site for empty input" { - run sanitize_for_filename "" - assert_output "unknown-site" -} - -@test "sanitize_for_filename removes trailing dots" { - run sanitize_for_filename "example.com." - assert_output "example.com" -} - # ---------- verify_file_integrity (removed in v3.2.4) ---------- @test "verify_file_integrity is removed (dead code referenced gpg)" {