Your IP : 216.73.217.79


Current Path : /var/www/cesa.co.za/scripts/wordpress/
Upload File :
Current File : /var/www/cesa.co.za/scripts/wordpress/wp-files-backup.sh

#!/usr/bin/env bash
# Backup WordPress application files (core, themes, plugins, mu-plugins, drop-ins).
# Does NOT include uploaded media (wp-content/uploads) or the database.
#
# Purpose: roll back a broken plugin/theme/core upgrade without restoring media.
# Database backups are handled separately.
#
# =============================================================================
# HOW TO RUN THE BACKUP
# =============================================================================
# Run on the WordPress server (SSH). Archives go to /var/backups/wp-files by default.
#
# One-liner (recommended):
#
#      bash /var/www/cesa.co.za/scripts/wordpress/wp-files-backup.sh backup
#
# Same thing with explicit paths (must be on the SAME line as bash, or use export):
#
#      WP_PATH=/var/www/cesa.co.za BACKUP_DIR=/var/backups/wp-files \
#        bash /var/www/cesa.co.za/scripts/wordpress/wp-files-backup.sh backup
#
# Or export first, then run:
#
#      export WP_PATH=/var/www/cesa.co.za
#      export BACKUP_DIR=/var/backups/wp-files
#      bash /var/www/cesa.co.za/scripts/wordpress/wp-files-backup.sh backup
#
# NOTE: These do NOT pass variables into the script (child process never sees them):
#      WP_PATH=/var/www/cesa.co.za          # assignment only — not exported
#      BACKUP_DIR=/var/backups/wp-files     # assignment only — not exported
#      bash .../wp-files-backup.sh backup  # script ignores the vars above
#
# First-time setup (once, as root if needed):
#
#      sudo mkdir -p /var/backups/wp-files
#      sudo chown otto:otto /var/backups/wp-files   # or whatever deploy user
#      sudo chmod 750 /var/backups/wp-files
#
# Output:
#   - Archive:  /var/backups/wp-files/cesa-wp-files-<host>-<YYYYMMDD-HHMMSS>.tar.gz
#   - Manifest: same name with .txt (paths, WP version, sha256)
#   - Keeps the newest KEEP archives (default 10; set KEEP=0 to keep all)
#
# =============================================================================
# HOW TO RESTORE (after a bad plugin upgrade)
# =============================================================================
# Overwrites core / themes / plugins / mu-plugins from the archive.
# Does NOT touch wp-content/uploads or the database.
#
#      bash /var/www/cesa.co.za/scripts/wordpress/wp-files-backup.sh restore \
#        /var/backups/wp-files/cesa-wp-files-HOST-20260720-144800.tar.gz
#
#    Non-interactive:
#
#      FORCE=1 bash /var/www/cesa.co.za/scripts/wordpress/wp-files-backup.sh restore \
#        /var/backups/wp-files/cesa-wp-files-HOST-20260720-144800.tar.gz
#
# Env:
#   WP_PATH     WordPress root (default: /var/www/cesa.co.za if present, else cwd)
#   BACKUP_DIR  Where archives are written (default: /var/backups/wp-files)
#   KEEP        Number of archives to retain (default: 10; 0 = keep all)
#   LABEL       Optional label in the archive name (default: short hostname)
#   FORCE=1     Skip restore confirmation prompt

set -euo pipefail

CMD="${1:-backup}"
if [[ -z "${WP_PATH:-}" ]]; then
  if [[ -f /var/www/cesa.co.za/wp-config.php ]]; then
    WP_PATH=/var/www/cesa.co.za
  else
    WP_PATH="$(pwd)"
  fi
fi
BACKUP_DIR="${BACKUP_DIR:-/var/backups/wp-files}"
KEEP="${KEEP:-10}"
LABEL="${LABEL:-$(hostname -s 2>/dev/null || echo wp)}"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"

die() {
  echo "ERROR: $*" >&2
  exit 1
}

require_wp_root() {
  [[ -f "$WP_PATH/wp-config.php" ]] || die "wp-config.php not found in WP_PATH=$WP_PATH"
  [[ -d "$WP_PATH/wp-admin" && -d "$WP_PATH/wp-includes" ]] || die "Does not look like a WordPress root: $WP_PATH"
}

resolve_backup_dir() {
  if ! mkdir -p "$BACKUP_DIR" 2>/dev/null; then
    die "Cannot create BACKUP_DIR=$BACKUP_DIR (permission denied). Run once:
  sudo mkdir -p $BACKUP_DIR
  sudo chown $(id -un):$(id -gn) $BACKUP_DIR
  sudo chmod 750 $BACKUP_DIR"
  fi
  [[ -w "$BACKUP_DIR" ]] || die "BACKUP_DIR=$BACKUP_DIR is not writable by $(id -un)"
  BACKUP_DIR="$(cd "$BACKUP_DIR" && pwd)"
}

wp_version() {
  if [[ -f "$WP_PATH/wp-includes/version.php" ]]; then
    sed -n "s/^\\\$wp_version *= *'\\([^']*\\)'.*/\\1/p" "$WP_PATH/wp-includes/version.php" | head -1
  fi
}

prune_old_backups() {
  local keep="$1"
  [[ "$keep" =~ ^[0-9]+$ ]] || return 0
  [[ "$keep" -eq 0 ]] && return 0

  mapfile -t archives < <(ls -1t "$BACKUP_DIR"/cesa-wp-files-*.tar.gz 2>/dev/null || true)
  local i
  for ((i = keep; i < ${#archives[@]}; i++)); do
    echo "    prune: ${archives[$i]}"
    rm -f "${archives[$i]}"
    rm -f "${archives[$i]%.tar.gz}.txt" 2>/dev/null || true
  done
}

# Optional root files — only add those that exist so tar does not fail.
collect_root_files() {
  local -a candidates=(
    index.php
    wp-activate.php
    wp-blog-header.php
    wp-comments-post.php
    wp-config.php
    wp-config-sample.php
    wp-cron.php
    wp-links-opml.php
    wp-load.php
    wp-login.php
    wp-mail.php
    wp-settings.php
    wp-signup.php
    wp-trackback.php
    xmlrpc.php
    license.txt
    readme.html
    .htaccess
  )
  ROOT_FILES=()
  local f
  for f in "${candidates[@]}"; do
    [[ -e "$WP_PATH/$f" ]] && ROOT_FILES+=("$f")
  done
}

do_backup() {
  require_wp_root
  WP_PATH="$(cd "$WP_PATH" && pwd)"
  resolve_backup_dir
  collect_root_files

  local archive="$BACKUP_DIR/cesa-wp-files-${LABEL}-${TIMESTAMP}.tar.gz"
  local manifest="$BACKUP_DIR/cesa-wp-files-${LABEL}-${TIMESTAMP}.txt"
  local ver
  ver="$(wp_version || true)"

  echo "==> WordPress path : $WP_PATH"
  echo "==> Backup dir     : $BACKUP_DIR"
  echo "==> Archive        : $archive"
  echo "==> WP version     : ${ver:-unknown}"
  echo "==> Excluding      : uploads, cache, upgrade/temp, logs, SQL/zip dumps"

  local -a excludes=(
    --exclude='wp-content/uploads'
    --exclude='wp-content/Uploads'
    --exclude='wp-content/cache'
    --exclude='wp-content/upgrade'
    --exclude='wp-content/upgrade-temp-backup'
    --exclude='wp-content/temp'
    --exclude='wp-content/tmp'
    --exclude='wp-content/backups'
    --exclude='wp-content/backup*'
    --exclude='wp-content/updraft'
    --exclude='wp-content/ai1wm-backups'
    --exclude='wp-content/wflogs'
    --exclude='wp-content/debug.log'
    --exclude='wp-content/**/*.log'
    --exclude='*.sql'
    --exclude='*.sql.gz'
    --exclude='node_modules'
    --exclude='.git'
  )

  # If BACKUP_DIR lives under WP_PATH, exclude it from the archive
  if [[ "$BACKUP_DIR" == "$WP_PATH"/* ]]; then
    excludes+=(--exclude="${BACKUP_DIR#"$WP_PATH"/}")
  fi

  local -a tar_extra=()
  if tar --help 2>&1 | grep -q -- '--warning='; then
    tar_extra+=(--warning=no-file-changed)
  fi

  # Core + full wp-content tree with uploads/caches excluded above.
  # Exit 1 from tar (file changed while reading) is tolerated on busy sites.
  set +e
  tar "${tar_extra[@]}" -czf "$archive" \
    "${excludes[@]}" \
    -C "$WP_PATH" \
    wp-admin \
    wp-includes \
    wp-content \
    "${ROOT_FILES[@]}"
  local tar_rc=$?
  set -e
  # GNU tar: 0 ok, 1 some files changed/differed while reading
  if [[ "$tar_rc" -ne 0 && "$tar_rc" -ne 1 ]]; then
    die "tar failed with exit code $tar_rc"
  fi

  [[ -s "$archive" ]] || die "Archive was not created or is empty: $archive"

  {
    echo "CESA WordPress file backup (no uploads, no database)"
    echo "created:     $(date -Is)"
    echo "host:        $(hostname -f 2>/dev/null || hostname)"
    echo "wp_path:     $WP_PATH"
    echo "wp_version:  ${ver:-unknown}"
    echo "archive:     $archive"
    echo "sha256:      $(sha256sum "$archive" | awk '{print $1}')"
    echo "size_bytes:  $(wc -c <"$archive" | tr -d ' ')"
    echo ""
    echo "Included: WordPress core, wp-config.php, .htaccess (if present),"
    echo "          wp-content/themes, plugins, mu-plugins, languages, drop-ins"
    echo "Excluded: wp-content/uploads, caches, upgrade temps, logs, SQL dumps"
    echo ""
    echo "Restore:"
    echo "  WP_PATH=$WP_PATH bash $0 restore $archive"
  } >"$manifest"

  prune_old_backups "$KEEP"

  echo ""
  echo "Done."
  echo "  archive : $archive"
  echo "  manifest: $manifest"
  echo "  size    : $(du -h "$archive" | awk '{print $1}')"
}

do_restore() {
  local archive="${2:-}"
  [[ -n "$archive" ]] || die "Usage: $0 restore /path/to/cesa-wp-files-....tar.gz"
  [[ -f "$archive" ]] || die "Archive not found: $archive"

  require_wp_root
  WP_PATH="$(cd "$WP_PATH" && pwd)"
  archive="$(cd "$(dirname "$archive")" && pwd)/$(basename "$archive")"

  echo "==> Restore WordPress files (core / themes / plugins) from:"
  echo "    $archive"
  echo "==> Target WP_PATH: $WP_PATH"
  echo "==> Uploads and database are NOT touched."
  echo ""
  echo "This will overwrite matching files under WP_PATH."
  if [[ -t 0 && "${FORCE:-}" != "1" ]]; then
    read -r -p "Type YES to continue: " confirm
    [[ "$confirm" == "YES" ]] || die "Aborted."
  fi

  tar -xzf "$archive" -C "$WP_PATH"

  echo ""
  echo "Restore complete."
  echo "  Next: clear object/page caches; verify Plugins / Themes."
  echo "  Database was not restored — use your DB backup if needed."
}

usage() {
  # Print the leading comment block (instructions) until the first non-comment line.
  awk '
    NR == 1 { next }
    /^#/ { sub(/^# ?/, ""); print; next }
    { exit }
  ' "$0"
}

case "$CMD" in
  backup)
    do_backup
    ;;
  restore)
    do_restore "$@"
    ;;
  -h|--help|help)
    usage
    ;;
  *)
    die "Unknown command: $CMD (use backup|restore|help)"
    ;;
esac