#!/bin/bash # ---------------------------------------------------- # Command: _do # Description: A collection of useful command-line utilities for managing WordPress sites. # Author: Austin Ginder # License: MIT # ---------------------------------------------------- # --- Global Variables --- CAPTAINCORE_DO_VERSION="1.2" GUM_VERSION="0.14.4" CWEBP_VERSION="1.5.0" RCLONE_VERSION="1.69.3" GIT_VERSION="2.50.0" GUM_CMD="" CWEBP_CMD="" IDENTIFY_CMD="" WP_CLI_CMD="" RESTIC_CMD="" # --- Helper Functions --- # ---------------------------------------------------- # Intelligently finds or creates a private directory. # Sets a global variable CAPTAINCORE_PRIVATE_DIR and echoes the path. # ---------------------------------------------------- function _get_private_dir() { # Return immediately if already found if [[ -n "$CAPTAINCORE_PRIVATE_DIR" ]]; then echo "$CAPTAINCORE_PRIVATE_DIR" return 0 fi local wp_root="" local parent_dir="" # --- Tier 1: Preferred WP-CLI Method (if in a WP directory) --- # Check if wp-cli is set up and if we are in a WP installation. if setup_wp_cli && "$WP_CLI_CMD" core is-installed --quiet 2>/dev/null; then local wp_config_path wp_config_path=$("$WP_CLI_CMD" config path --quiet 2>/dev/null) if [ -n "$wp_config_path" ] && [ -f "$wp_config_path" ]; then wp_root=$(dirname "$wp_config_path") parent_dir=$(dirname "$wp_root") # --- WPE Specific Checks --- # A. Check for _wpeprivate inside the WP root directory first (most common). if [ -d "${wp_root}/_wpeprivate" ]; then CAPTAINCORE_PRIVATE_DIR="${wp_root}/_wpeprivate" echo "$CAPTAINCORE_PRIVATE_DIR" return 0 fi # B. Check for _wpeprivate in the parent directory. if [ -d "${parent_dir}/_wpeprivate" ]; then CAPTAINCORE_PRIVATE_DIR="${parent_dir}/_wpeprivate" echo "$CAPTAINCORE_PRIVATE_DIR" return 0 fi # --- Standard Checks (relative to WP root's parent) --- # Check for a standard ../private directory if [ -d "${parent_dir}/private" ]; then CAPTAINCORE_PRIVATE_DIR="${parent_dir}/private" echo "$CAPTAINCORE_PRIVATE_DIR" return 0 fi # Try to create a ../private directory, suppressing errors if mkdir -p "${parent_dir}/private" 2>/dev/null; then CAPTAINCORE_PRIVATE_DIR="${parent_dir}/private" echo "$CAPTAINCORE_PRIVATE_DIR" return 0 fi # Fallback to ../tmp if it exists if [ -d "${parent_dir}/tmp" ]; then CAPTAINCORE_PRIVATE_DIR="${parent_dir}/tmp" echo "$CAPTAINCORE_PRIVATE_DIR" return 0 fi fi fi # --- Tier 2: Manual Fallback (if WP-CLI fails or not in a WP install) --- local current_dir current_dir=$(pwd) # WPE check in current directory if [ -d "${current_dir}/_wpeprivate" ]; then CAPTAINCORE_PRIVATE_DIR="${current_dir}/_wpeprivate" echo "$CAPTAINCORE_PRIVATE_DIR" return 0 fi # Relative private check if [ -d "../private" ]; then CAPTAINCORE_PRIVATE_DIR=$(cd ../private && pwd) echo "$CAPTAINCORE_PRIVATE_DIR" return 0 fi # Attempt to create relative private, suppressing errors if mkdir -p "../private" 2>/dev/null; then CAPTAINCORE_PRIVATE_DIR=$(cd ../private && pwd) echo "$CAPTAINCORE_PRIVATE_DIR" return 0 fi # Relative tmp check if [ -d "../tmp" ]; then CAPTAINCORE_PRIVATE_DIR=$(cd ../tmp && pwd) echo "$CAPTAINCORE_PRIVATE_DIR" return 0 fi # --- Tier 3: Last Resort Fallback to Home Directory --- # Suppress errors in case $HOME is not writable if mkdir -p "$HOME/private" 2>/dev/null; then CAPTAINCORE_PRIVATE_DIR="$HOME/private" echo "$CAPTAINCORE_PRIVATE_DIR" return 0 fi echo "Error: Could not find or create a suitable private, writable directory." >&2 return 1 } # ---------------------------------------------------- # Checks for and installs 'gum' if not present. Sets GUM_CMD on success. # ---------------------------------------------------- function setup_gum() { # Return if already found if [[ -n "$GUM_CMD" ]]; then return 0; fi # If gum is already in the PATH, we're good to go. if command -v gum &> /dev/null; then GUM_CMD="gum" return 0 fi # Find the private directory for storing tools local private_dir if ! private_dir=$(_get_private_dir); then echo "Error: Cannot find a writable directory to install gum." >&2 return 1 fi # Find the executable inside the private directory if it's already installed local existing_executable existing_executable=$(find "$private_dir" -name gum -type f 2>/dev/null | head -n 1) if [ -n "$existing_executable" ] && [ -x "$existing_executable" ] && "$existing_executable" --version &> /dev/null; then GUM_CMD="$existing_executable" return 0 fi echo "Required tool 'gum' not found. Installing to '${private_dir}'..." >&2 local original_dir; original_dir=$(pwd) cd "$private_dir" || { echo "Error: Could not enter private directory '${private_dir}'." >&2; return 1; } local gum_dir_name="gum_${GUM_VERSION}_Linux_x86_64" local gum_tarball="${gum_dir_name}.tar.gz" if ! curl -sSL "https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/${gum_tarball}" -o "${gum_tarball}"; then echo "Error: Failed to download gum." >&2; cd "$original_dir" > /dev/null 2>&1; return 1; fi # Find the path of the 'gum' binary WITHIN the tarball before extracting local gum_path_in_tar gum_path_in_tar=$(tar -tf "${gum_tarball}" | grep '/gum$' | head -n 1) if [ -z "$gum_path_in_tar" ]; then echo "Error: Could not find 'gum' executable within the downloaded tarball." >&2 rm -f "${gum_tarball}"; cd "$original_dir" > /dev/null 2>&1; return 1; fi # Now extract the tarball if ! tar -xf "${gum_tarball}"; then echo "Error: Failed to extract gum from tarball." >&2 rm -f "${gum_tarball}"; cd "$original_dir" > /dev/null 2>&1; return 1; fi rm -f "${gum_tarball}" # The full path to the executable is the private dir + the path from the tarball local gum_executable="${private_dir}/${gum_path_in_tar}" if [ -f "$gum_executable" ]; then chmod +x "$gum_executable" else echo "Error: gum executable not found at expected path after extraction: ${gum_executable}" >&2 cd "$original_dir" > /dev/null 2>&1; return 1; fi # Final check if [ -x "$gum_executable" ] && "$gum_executable" --version &> /dev/null; then echo "'gum' installed successfully." >&2 GUM_CMD="$gum_executable" else echo "Error: gum installation failed. The binary at ${gum_executable} might not be executable or compatible." >&2 cd "$original_dir" > /dev/null 2>&1; return 1; fi cd "$original_dir" > /dev/null 2>&1; return 0; } # ---------------------------------------------------- # Checks for and installs 'cwebp' if not present. Sets CWEBP_CMD on success. # ---------------------------------------------------- function setup_cwebp() { # Return if already found if [[ -n "$CWEBP_CMD" ]]; then return 0; fi if command -v cwebp &> /dev/null; then CWEBP_CMD="cwebp"; return 0; fi local private_dir if ! private_dir=$(_get_private_dir); then echo "Error: Cannot find a writable directory to install cwebp." >&2; return 1; fi local existing_executable existing_executable=$(find "$private_dir" -name cwebp -type f 2>/dev/null | head -n 1) if [ -n "$existing_executable" ] && [ -x "$existing_executable" ] && "$existing_executable" -version &> /dev/null; then CWEBP_CMD="$existing_executable"; return 0; fi echo "Required tool 'cwebp' not found. Installing to '${private_dir}'..." >&2 local original_dir; original_dir=$(pwd) cd "$private_dir" || { echo "Error: Could not enter private directory '${private_dir}'." >&2; return 1; } local cwebp_dir_name="libwebp-${CWEBP_VERSION}-linux-x86-64" local cwebp_tarball="${cwebp_dir_name}.tar.gz" if ! curl -sSL "https://storage.googleapis.com/downloads.webmproject.org/releases/webp/${cwebp_tarball}" -o "${cwebp_tarball}"; then echo "Error: Failed to download cwebp." >&2; cd "$original_dir" > /dev/null 2>&1; return 1; fi local cwebp_path_in_tar cwebp_path_in_tar=$(tar -tf "${cwebp_tarball}" | grep '/bin/cwebp$' | head -n 1) if [ -z "$cwebp_path_in_tar" ]; then echo "Error: Could not find 'cwebp' executable within the downloaded tarball." >&2 rm -f "${cwebp_tarball}"; cd "$original_dir" > /dev/null 2>&1; return 1; fi if ! tar -xzf "${cwebp_tarball}"; then echo "Error: Failed to extract cwebp." >&2; rm -f "${cwebp_tarball}"; cd "$original_dir" > /dev/null 2>&1; return 1; fi rm -f "${cwebp_tarball}" local cwebp_executable="${private_dir}/${cwebp_path_in_tar}" if [ -f "$cwebp_executable" ]; then chmod +x "$cwebp_executable"; else echo "Error: cwebp executable not found at expected path: ${cwebp_executable}" >&2; cd "$original_dir" > /dev/null 2>&1; return 1; fi if [ -x "$cwebp_executable" ] && "$cwebp_executable" -version &> /dev/null; then echo "'cwebp' installed successfully." >&2; CWEBP_CMD="$cwebp_executable"; else echo "Error: cwebp installation failed." >&2; cd "$original_dir" > /dev/null 2>&1; return 1; fi cd "$original_dir" > /dev/null 2>&1; return 0; } # ---------------------------------------------------- # Checks for and installs ImageMagick if not present. Sets IDENTIFY_CMD on success. # ---------------------------------------------------- function setup_imagemagick() { # Return if already found if [[ -n "$IDENTIFY_CMD" ]]; then return 0 fi # If identify is already in the PATH, we're good to go. if command -v identify &> /dev/null; then IDENTIFY_CMD="identify" return 0 fi local private_dir if ! private_dir=$(_get_private_dir); then # Error message is handled by the helper function return 1 fi # Define the path where the extracted binary should be local identify_executable="${private_dir}/squashfs-root/usr/bin/identify" # Check if we have already extracted it if [ -f "$identify_executable" ] && "$identify_executable" -version &> /dev/null; then IDENTIFY_CMD="$identify_executable" return 0 fi # If not found, download and extract the AppImage echo "Required tool 'identify' not found. Sideloading via AppImage extraction..." >&2 local imagemagick_appimage_path="${private_dir}/ImageMagick.AppImage" # Let's use the 'gcc' version as it's a common compiler toolchain for Linux local appimage_url="https://github.com/ImageMagick/ImageMagick/releases/download/7.1.1-47/ImageMagick-82572af-gcc-x86_64.AppImage" echo "Downloading from ${appimage_url}..." >&2 if ! wget --quiet "$appimage_url" -O "$imagemagick_appimage_path"; then echo "Error: Failed to download the ImageMagick AppImage." >&2 rm -f "$imagemagick_appimage_path" # Clean up partial download return 1 fi chmod +x "$imagemagick_appimage_path" # --- EXTRACTION STEP --- # This is the key change to work around the FUSE error. echo "Extracting AppImage..." >&2 # Change into the private directory to contain the extraction cd "$private_dir" || { echo "Error: Could not enter private directory." >&2; return 1; } # Run the extraction. This creates a 'squashfs-root' directory. if ! ./ImageMagick.AppImage --appimage-extract >/dev/null; then echo "Error: Failed to extract the ImageMagick AppImage." >&2 # Clean up on failure rm -f "ImageMagick.AppImage" rm -rf "squashfs-root" cd - > /dev/null return 1 fi # We don't need the AppImage file anymore after extraction rm -f "ImageMagick.AppImage" # Return to the original directory cd - > /dev/null # Final check if [ -f "$identify_executable" ] && "$identify_executable" -version &> /dev/null; then echo "'identify' binary extracted successfully to ${private_dir}/squashfs-root/" >&2 IDENTIFY_CMD="$identify_executable" else echo "Error: ImageMagick extraction failed. Could not find the 'identify' executable." >&2 return 1 fi } # ---------------------------------------------------- # Checks for and installs 'rclone' if not present. Sets RCLONE_CMD on success. # ---------------------------------------------------- function setup_rclone() { # Return if already found if [[ -n "$RCLONE_CMD" ]]; then return 0; fi if command -v rclone &> /dev/null; then RCLONE_CMD="rclone"; return 0; fi if ! command -v unzip &>/dev/null; then echo "Error: 'unzip' command is required for rclone installation." >&2; return 1; fi local private_dir if ! private_dir=$(_get_private_dir); then echo "Error: Cannot find a writable directory to install rclone." >&2; return 1; fi local existing_executable existing_executable=$(find "$private_dir" -name rclone -type f 2>/dev/null | head -n 1) if [ -n "$existing_executable" ] && [ -x "$existing_executable" ] && "$existing_executable" --version &> /dev/null; then RCLONE_CMD="$existing_executable"; return 0; fi echo "Required tool 'rclone' not found. Installing to '${private_dir}'..." >&2 local original_dir; original_dir=$(pwd) cd "$private_dir" || { echo "Error: Could not enter private directory '${private_dir}'." >&2; return 1; } local rclone_zip="rclone-v${RCLONE_VERSION}-linux-amd64.zip" if ! curl -sSL "https://github.com/rclone/rclone/releases/download/v${RCLONE_VERSION}/${rclone_zip}" -o "${rclone_zip}"; then echo "Error: Failed to download rclone." >&2; cd "$original_dir" > /dev/null 2>&1; return 1; fi local rclone_path_in_zip rclone_path_in_zip=$(unzip -l "${rclone_zip}" | grep '/rclone$' | awk '{print $4}' | head -n 1) if [ -z "$rclone_path_in_zip" ]; then echo "Error: Could not find 'rclone' executable within the downloaded zip." >&2 rm -f "${rclone_zip}"; cd "$original_dir" > /dev/null 2>&1; return 1; fi if ! unzip -q -o "${rclone_zip}"; then echo "Error: Failed to extract rclone." >&2; rm -f "${rclone_zip}"; cd "$original_dir" > /dev/null 2>&1; return 1; fi rm -f "${rclone_zip}" local rclone_executable="${private_dir}/${rclone_path_in_zip}" if [ -f "$rclone_executable" ]; then chmod +x "$rclone_executable"; else echo "Error: rclone executable not found at expected path: ${rclone_executable}" >&2; cd "$original_dir" > /dev/null 2>&1; return 1; fi if [ -x "$rclone_executable" ] && "$rclone_executable" --version &> /dev/null; then echo "'rclone' installed successfully." >&2; RCLONE_CMD="$rclone_executable"; else echo "Error: rclone installation failed." >&2; cd "$original_dir" > /dev/null 2>&1; return 1; fi cd "$original_dir" > /dev/null 2>&1; return 0; } # ---------------------------------------------------- # Checks for and installs 'restic' if not present. # Sets RESTIC_CMD on success. # ---------------------------------------------------- function setup_restic() { # Return if already found if [[ -n "$RESTIC_CMD" ]]; then return 0; fi # If restic is already in the PATH, we're good. if command -v restic &> /dev/null; then RESTIC_CMD="restic" return 0 fi # Check for local installation in private dir local restic_executable="$HOME/private/restic" if [ -f "$restic_executable" ] && "$restic_executable" version &> /dev/null; then RESTIC_CMD="$restic_executable" return 0 fi # If not found, download it echo "Required tool 'restic' not found. Installing..." >&2 if ! command -v bunzip2 &>/dev/null; then echo "Error: 'bunzip2' command is required for installation." >&2; return 1; fi mkdir -p "$HOME/private" cd "$HOME/private" || { echo "Error: Could not enter ~/private." >&2; return 1; } local restic_version="0.18.0" local restic_archive="restic_${restic_version}_linux_amd64.bz2" if ! curl -sL "https://github.com/restic/restic/releases/download/v${restic_version}/${restic_archive}" -o "${restic_archive}"; then echo "Error: Failed to download restic." >&2 cd - > /dev/null return 1 fi # Decompress and extract the binary bunzip2 -c "${restic_archive}" > restic_temp && mv restic_temp restic rm -f "${restic_archive}" chmod +x restic # Final check if [ -f "$restic_executable" ] && "$restic_executable" version &> /dev/null; then echo "'restic' installed successfully." >&2 RESTIC_CMD="$restic_executable" else echo "Error: restic installation failed." >&2 cd - > /dev/null return 1 fi cd - > /dev/null } # ---------------------------------------------------- # Checks for and installs 'git' if not present. Sets GIT_CMD on success. # ---------------------------------------------------- function setup_git() { # Return if already found if [[ -n "$GIT_CMD" ]]; then return 0 fi # If git is already in the PATH, we're good to go. if command -v git &> /dev/null; then GIT_CMD="git" return 0 fi # --- Sideloading Logic --- echo "Required tool 'git' not found. Attempting to sideload..." >&2 local private_dir if ! private_dir=$(_get_private_dir); then return 1 fi local git_executable="${private_dir}/git/usr/bin/git" # Check if git has already been sideloaded if [ -f "$git_executable" ] && "$git_executable" --version &> /dev/null; then echo "'git' found in private directory." >&2 GIT_CMD="$git_executable" return 0 fi # Check for wget and dpkg-deb, which are required for sideloading if ! command -v wget &> /dev/null || ! command -v dpkg-deb &> /dev/null; then echo "❌ Error: 'wget' and 'dpkg-deb' are required to sideload git." >&2 return 1 fi # Determine OS distribution and version if [ -f /etc/os-release ]; then . /etc/os-release else echo "❌ Error: Cannot determine OS distribution. /etc/os-release not found." >&2 return 1 fi if [[ "$ID" != "ubuntu" ]]; then echo "❌ Error: Sideloading git is currently only supported on Ubuntu." >&2 return 1 fi # Construct the download URL for the git package # This example uses a recent stable version. You may need to update the version number periodically. local git_version="2.47.1-0ppa1~ubuntu16.04.1" local git_deb_url="https://launchpad.net/~git-core/+archive/ubuntu/candidate/+build/29298725/+files/git_${git_version}_amd64.deb" local git_deb_file="${private_dir}/git_latest.deb" echo "Downloading git from ${git_deb_url}..." >&2 if ! wget -q -O "$git_deb_file" "$git_deb_url"; then echo "❌ Error: Failed to download git .deb package." >&2 rm -f "$git_deb_file" return 1 fi echo "Extracting git package..." >&2 local extract_dir="${private_dir}/git" mkdir -p "$extract_dir" if ! dpkg-deb -x "$git_deb_file" "$extract_dir"; then echo "❌ Error: Failed to extract git .deb package." >&2 rm -rf "$extract_dir" rm -f "$git_deb_file" return 1 fi # Clean up the downloaded .deb file rm -f "$git_deb_file" # Final check if [ -f "$git_executable" ] && "$git_executable" --version &> /dev/null; then echo "'git' sideloaded successfully." >&2 GIT_CMD="$git_executable" return 0 else echo "❌ Error: git sideloading failed. The git binary is not available after extraction." >&2 return 1 fi } # ---------------------------------------------------- # Checks for and finds the 'wp' command. Sets WP_CLI_CMD on success. # ---------------------------------------------------- function setup_wp_cli() { # Return if already found if [[ -n "$WP_CLI_CMD" ]]; then return 0; fi # 1. Check if 'wp' is already in the PATH (covers interactive shells) if command -v wp &> /dev/null; then WP_CLI_CMD="wp" return 0 fi # 2. If not in PATH, check common absolute paths for cron environments local common_paths=( "/usr/local/bin/wp" "$HOME/bin/wp" "/opt/wp-cli/wp" ) for path in "${common_paths[@]}"; do if [ -x "$path" ]; then WP_CLI_CMD="$path" return 0 fi done # 3. If still not found, error out echo "❌ Error: 'wp' command not found. Please ensure WP-CLI is installed and in your PATH." >&2 return 1 } # ---------------------------------------------------- # (Helper) Uses PHP to check if a file is a WebP image. # ---------------------------------------------------- function _is_webp_php() { local file_path="$1" if [ -z "$file_path" ]; then return 1 # Return false if no file path is provided fi # IMAGETYPE_WEBP has a constant value of 18 in PHP. # We will embed the file_path directly into the PHP string. local php_code=" \$file_to_check = '${file_path}'; if (!file_exists(\$file_to_check)) { // Silently exit if file doesn't exist, to avoid warnings. exit(1); } if (function_exists('exif_imagetype')) { // The @ suppresses warnings for unsupported file types. \$image_type = @exif_imagetype(\$file_to_check); if (\$image_type === 18) { // 18 is the constant for IMAGETYPE_WEBP exit(0); // Exit with success code (true) } } exit(1); // Exit with failure code (false) " if ! setup_wp_cli; then if command -v php &> /dev/null; then php -r "\$file_path='${file_path}'; ${php_code}" return $? fi return 1 fi # Execute 'wp eval' with the self-contained code. No extra arguments are needed. "$WP_CLI_CMD" eval "$php_code" return $? } # ---------------------------------------------------- # (Primary Checker) Checks if a file is WebP, using identify or PHP fallback. # ---------------------------------------------------- function _is_webp() { # Determine which method to use, but only do it once. if [[ -z "$IDENTIFY_METHOD" ]]; then if command -v identify &> /dev/null; then export IDENTIFY_METHOD="identify" else export IDENTIFY_METHOD="php" fi fi # Execute the chosen method if [[ "$IDENTIFY_METHOD" == "identify" ]]; then # Return false if the file doesn't exist to prevent errors. if [ ! -f "$1" ]; then return 1; fi if [[ "$(identify -format "%m" "$1")" == "WEBP" ]]; then return 0 # It is a WebP file else return 1 # It is not a WebP file fi else # Fallback to PHP if _is_webp_php "$1"; then return 0 # It is a WebP file else return 1 # It is not a WebP file fi fi } # ---------------------------------------------------- # Displays detailed help for a specific command. # ---------------------------------------------------- function show_command_help() { local cmd="$1" # If no command is specified, show the general usage. if [ -z "$cmd" ]; then show_usage return fi # Display help text based on the command provided. case "$cmd" in backup) echo "Creates a full backup (files + DB) of a WordPress site." echo echo "Usage: _do backup " ;; checkpoint) echo "Manages versioned checkpoints of a WordPress installation's manifest." echo echo "Usage: _do checkpoint [arguments]" echo echo "Subcommands:" echo " create Creates a new checkpoint of the current plugin/theme/core manifest." echo " list Lists available checkpoints from the generated list to inspect." echo " list-generate Generates a detailed list of all checkpoints for fast viewing." echo " revert [] Reverts the site to the specified checkpoint hash." echo " show Retrieves the details for a specific checkpoint hash." echo " latest Gets the hash of the most recent checkpoint." ;; clean) echo "Cleans up unused WordPress components or analyzes disk usage." echo echo "Usage: _do clean " echo echo "Subcommands:" echo " plugins Deletes all inactive plugins." echo " themes Deletes all inactive themes except for the latest default WordPress theme." echo " disk Provides an interactive disk usage analysis for the current directory." ;; cron) echo "Manages scheduled tasks (cron jobs) for this script." echo echo "Usage: _do cron [arguments]" echo echo "Subcommands:" echo " enable Adds a job to the system crontab to run '_do cron run' every 10 minutes." echo " list Lists all scheduled commands." echo " run Executes any scheduled commands that are due." echo " add \"\" \"