Skip to content

Commit

Permalink
Version 6!
Browse files Browse the repository at this point in the history
  • Loading branch information
pothi committed Feb 16, 2023
1 parent 7374720 commit 289f820
Show file tree
Hide file tree
Showing 5 changed files with 552 additions and 327 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ There are plenty of plugins available to take backups within WordPress. However,
- Automatic deletion of local backups.
- Support for sub-directory installation of WordPress!
- Support for simple encryption using GnuPG
- Alert via email when the offsite backup fails (and succeeds)

## Roadmap

Expand Down Expand Up @@ -59,6 +60,8 @@ There are plenty of plugins available to take backups within WordPress. However,
/path/to/db-backup.sh example3.com
```

For more usage options, please run `/path/to/db-backup.sh -h`.

The above is applicable to all the scripts!

## Contributors
Expand Down
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
version: 6.0.0
- date: 2023-02-16
- pass important variables as arguments. No breaking changes. Old usage format still works.
- change the default location of DB backup inside a full backup
- fix and simplify excludes in full backup.

version: 5.3.0
- date: 2023-02-03
- alert upon success
Expand Down
290 changes: 171 additions & 119 deletions db-backup.sh
Original file line number Diff line number Diff line change
@@ -1,183 +1,235 @@
#!/bin/bash
#!/usr/bin/env bash

# requirements
# ~/log, ~/backups, ~/path/to/example.com/public

# version - 5.2.1
version=6.0.0

### Variables - Please do not add trailing slash in the PATHs

# auto delete older backups after certain number days
# configurable using -k|--keepfor <days>
AUTODELETEAFTER=7

# where to store the database backups?
BACKUP_PATH=${HOME}/backups/db-backups
ENCRYPTED_BACKUP_PATH=${HOME}/backups/encrypted-db-backups

# the script assumes your sites are stored like ~/sites/example.com/public, ~/sites/example.net/public, ~/sites/example.org/public and so on.
# if you have a different pattern, such as ~/app/example.com/public, please change the following to fit the server environment!
# a passphrase for encryption, in order to being able to use almost any special characters use ""
# it's best to configure it in ~/.envrc file
PASSPHRASE=

# the script assumes your sites are stored like ~/sites/example.com, ~/sites/example.net, ~/sites/example.org and so on.
# if you have a different pattern, such as ~/app/example.com, please change the following to fit the server environment!
SITES_PATH=${HOME}/sites

# if WP is in a sub-directory, please leave this empty!
#-------- Do NOT Edit Below This Line --------#

log_file=${HOME}/log/backups.log
exec > >(tee -a "${log_file}")
exec 2> >(tee -a "${log_file}" >&2)

# Variables defined later in the script
success_alert=
custom_email=
custom_wp_path=
BUCKET_NAME=
DOMAIN=
PUBLIC_DIR=public

# a passphrase for encryption, in order to being able to use almost any special characters use ""
PASSPHRASE=
# get environment variables, if exists
[ -f "$HOME/.envrc" ] && source ~/.envrc
[ -f "$HOME/.env" ] && source ~/.env

# auto delete older backups after certain number days - default 60. YMMV
AUTODELETEAFTER=30
print_help() {
printf '%s\n' "Take a database backup"
echo
printf 'Usage: %s [-b <name>] [-k <days>] [-e <email-address>] [-s] [-p <WP path>] [-v] [-h] example.com\n' "$0"
echo
printf '\t%s\t%s\n' "-b, --bucket" "Name of the bucket for offsite backup (default: none)"
printf '\t%s\t%s\n' "-k, --keepfor" "# of days to keep the local backups (default: 7)"
printf '\t%s\t%s\n' "-e, --email" "Email to send success/failures alerts (default: root@localhost)"
printf '\t%s\t%s\n' "-s, --success" "Alert on successful backup too (default: alert only on failures)"
printf '\t%s\t%s\n' "-p, --path" "Path to WP files (default: ~/sites/example.com/public or ~/public_html for cPanel)"
echo
printf '\t%s\t%s\n' "-v, --version" "Prints the version info"
printf '\t%s\t%s\n' "-h, --help" "Prints help"

echo
echo "For more info, changelog and documentation... https://github.com/pothi/backup-wordpress"
}

# https://stackoverflow.com/a/62616466/1004587
# Convenience functions.
EOL=$(printf '\1\3\3\7')
opt=
usage_error () { echo >&2 "$(basename $0): $1"; exit 2; }
assert_argument () { test "$1" != "$EOL" || usage_error "$2 requires an argument"; }

# One loop, nothing more.
if [ "$#" != 0 ]; then
set -- "$@" "$EOL"
while [ "$1" != "$EOL" ]; do
opt="$1"; shift
case "$opt" in

# Your options go here.
-v|--version) echo $version; exit 0;;
-V) echo $version; exit 0;;
-h|--help) print_help; exit 0;;
-b|--bucket) assert_argument "$1" "$opt"; BUCKET_NAME="$1"; shift;;
-k|--keepfor) assert_argument "$1" "$opt"; AUTODELETEAFTER="$1"; shift;;
-p|--path) assert_argument "$1" "$opt"; custom_wp_path="$1"; shift;;
-e|--email) assert_argument "$1" "$opt"; custom_email="$1"; shift;;
-s|--success) success_alert=1;;

# Arguments processing. You may remove any unneeded line after the 1st.
-|''|[!-]*) set -- "$@" "$opt";; # positional argument, rotate to the end
--*=*) set -- "${opt%%=*}" "${opt#*=}" "$@";; # convert '--name=arg' to '--name' 'arg'
-[!-]?*) set -- $(echo "${opt#-}" | sed 's/\(.\)/ -\1/g') "$@";; # convert '-abc' to '-a' '-b' '-c'
--) while [ "$1" != "$EOL" ]; do set -- "$@" "$1"; shift; done;; # process remaining arguments as positional
-*) usage_error "unknown option: '$opt'";; # catch misspelled options
*) usage_error "this should NEVER happen ($opt)";; # sanity test for previous patterns

esac
done
shift # $EOL
fi

# You may hard-code the domain name
DOMAIN=
# Do something cool with "$@"... \o/

# AWS Variable can be hard-coded here
AWS_S3_BUCKET_NAME=
# Get example.com
if [ "$#" -gt 0 ]; then
DOMAIN=$1
shift
else
print_help
exit 2
fi

#-------- Do NOT Edit Below This Line --------#
# compatibility with old syntax to get bucket name
# To be removed in the future
if [ "$#" -gt 0 ]; then
BUCKET_NAME=$1
echo "You are using old syntax."
print_help
shift
fi

# unwanted argument/s
if [ "$#" -gt 0 ]; then
print_help
exit 2
fi

# to capture non-zero exit code in the pipeline
set -o pipefail

# attempt to create log directory if it doesn't exist
[ -d "${HOME}/log" ] || mkdir -p ${HOME}/log
if [ "$?" -ne "0" ]; then
echo "Log directory not found at ~/log. This script can't create it, either!"
echo 'You may create it manually and re-run this script.'
exit 1
if [ ! -d "${HOME}/log" ]; then
if ! mkdir -p "${HOME}"/log; then
echo "Log directory not found at ~/log. This script can't create it, either!"
echo 'You may create it manually and re-run this script.'
exit 1
fi
fi
# attempt to create the backups directory, if it doesn't exist
[ -d "$BACKUP_PATH" ] || mkdir -p $BACKUP_PATH
if [ "$?" -ne "0" ]; then
echo "BACKUP_PATH is not found at $BACKUP_PATH. This script can't create it, either!"
echo 'You may create it manually and re-run this script.'
exit 1
fi
# if passphrase is supplied, attempt to create backups directory for encrypt backups, if it doesn't exist
if [ -n "$PASSPHRASE" ]; then
[ -d "$ENCRYPTED_BACKUP_PATH" ] || mkdir -p $ENCRYPTED_BACKUP_PATH
if [ "$?" -ne "0" ]; then
echo "ENCRYPTED_BACKUP_PATH Is not found at $ENCRYPTED_BACKUP_PATH. This script can't create it, either!"
if [ ! -d "$BACKUP_PATH" ]; then
if ! mkdir -p "$BACKUP_PATH"; then
echo "BACKUP_PATH is not found at $BACKUP_PATH. This script can't create it, either!"
echo 'You may create it manually and re-run this script.'
exit 1
fi
fi

log_file=${HOME}/log/backups.log
exec > >(tee -a ${log_file} )
exec 2> >(tee -a ${log_file} >&2)

export PATH=~/bin:~/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin

declare -r script_name=$(basename "$0")
declare -r timestamp=$(date +%F_%H-%M-%S)
declare -r wp_cli=`which wp`
declare -r aws_cli=`which aws`

if [ -z "$wp_cli" ]; then
echo "wp-cli is not found in $PATH. Exiting."
exit 1
fi

if [ -z "$aws_cli" ]; then
echo "aws-cli is not found in $PATH. Exiting."
exit 1
fi
script_name=$(basename "$0")
timestamp=$(date +%F_%H-%M-%S)

cPanel=$(/usr/local/cpanel/cpanel -V 2>/dev/null)
if [ ! -z "$cPanel" ]; then
SITES_PATH=$HOME
PUBLIC_DIR=public_html
fi

echo "'$script_name' started on... $(date +%c)"
command -v wp >/dev/null || { echo >&2 "wp cli is not found in $PATH. Exiting."; exit 1; }
command -v aws >/dev/null || { echo >&2 "aws cli is not found in $PATH. Exiting."; exit 1; }
command -v mail >/dev/null || echo >&2 "[WARNING]: 'mail' command is not found in $PATH; Alerts will not be sent!"

let AUTODELETEAFTER--

# get environment variables, if exists
[ -f "$HOME/.envrc" ] && source ~/.envrc
[ -f "$HOME/.env" ] && source ~/.env
((AUTODELETEAFTER--))

# check for the variable/s in three places
# 1 - hard-coded value
# 2 - optional parameter while invoking the script
# 3 - environment files

if [ "$DOMAIN" == "" ]; then
if [ "$1" == "" ]; then
if [ "$WP_DOMAIN" != "" ]; then
DOMAIN=$WP_DOMAIN
else
echo "Usage $script_name example.com (S3 bucket name)"; exit 1
fi
else
DOMAIN=$1
fi
alertEmail=${custom_email:-${BACKUP_ADMIN_EMAIL:-${ADMIN_EMAIL:-"root@localhost"}}}

# Define paths
# cPanel uses a different directory structure
# dir_to_backup and db_dump are used only in full-backup.sh
cPanel=$(/usr/local/cpanel/cpanel -V 2>/dev/null)
if [ "$cPanel" ]; then
SITES_PATH=$HOME
PUBLIC_DIR=public_html
WP_PATH=${SITES_PATH}/${PUBLIC_DIR}
# dir_to_backup=public_html
# db_dump=${WP_PATH}/db.sql
else
WP_PATH=${SITES_PATH}/${DOMAIN}/${PUBLIC_DIR}
# dir_to_backup=${DOMAIN}
# db_dump=${SITES_PATH}/${DOMAIN}/db.sql
fi

if [ "$BUCKET_NAME" == "" ]; then
if [ "$2" != "" ]; then
BUCKET_NAME=$2
elif [ "$AWS_S3_BUCKET_NAME" != "" ]; then
BUCKET_NAME=$AWS_S3_BUCKET_NAME
fi
if [ "$custom_wp_path" ]; then
WP_PATH="$custom_wp_path"
DB_OUTPUT_FILE_NAME=${custom_wp_path}/db.sql
fi

# WordPress root
WP_PATH=${SITES_PATH}/${DOMAIN}/${PUBLIC_DIR}
# For cPanel - main site
[ ! -d "$WP_PATH" ] && WP_PATH=${SITES_PATH}/${PUBLIC_DIR}
[ ! -d "$WP_PATH" ] && echo "WordPress is not found at $WP_PATH" && exit 1

echo "'$script_name' started on... $(date +%c)"

# convert forward slash found in sub-directories to hyphen
# ex: example.com/test would become example.com-test
DOMAIN_FULL_PATH=$(echo $DOMAIN | awk '{gsub(/\//,"-")}; 1')
DOMAIN_FULL_PATH=$(echo "$DOMAIN" | awk '{gsub(/\//,"-")}; 1')

DB_OUTPUT_FILE_NAME=${BACKUP_PATH}/${DOMAIN_FULL_PATH}-${timestamp}.sql.gz
ENCRYPTED_DB_OUTPUT_FILE_NAME=${ENCRYPTED_BACKUP_PATH}/${DOMAIN_FULL_PATH}-${timestamp}.sql.gz
DB_LATEST_FILE_NAME=${BACKUP_PATH}/${DOMAIN_FULL_PATH}-latest.sql.gz

# take actual DB backup
if [ -f "$wp_cli" ]; then
$wp_cli --path=${WP_PATH} transient delete --all
$wp_cli --path=${WP_PATH} db export --no-tablespaces=true --add-drop-table - | gzip > $DB_OUTPUT_FILE_NAME
if [ "$?" != "0" ]; then
echo; echo 'Something went wrong while taking local backup!'
[ -f $DB_OUTPUT_FILE_NAME ] && rm -f $DB_OUTPUT_FILE_NAME
fi

[ -L $DB_LATEST_FILE_NAME ] && rm $DB_LATEST_FILE_NAME
if [ -n "$PASSPHRASE" ] ; then
gpg --symmetric --passphrase $PASSPHRASE --batch -o ${ENCRYPTED_DB_OUTPUT_FILE_NAME} $DB_OUTPUT_FILE_NAME
[ -f $DB_OUTPUT_FILE_NAME ] && rm -f $DB_OUTPUT_FILE_NAME
ln -s $ENCRYPTED_DB_OUTPUT_FILE_NAME $DB_LATEST_FILE_NAME
else
ln -s $DB_OUTPUT_FILE_NAME $DB_LATEST_FILE_NAME
fi
wp --path="${WP_PATH}" transient delete --all
if [ -n "$PASSPHRASE" ] ; then
DB_OUTPUT_FILE_NAME="${DB_OUTPUT_FILE_NAME}".gpg
wp --path="${WP_PATH}" db export --no-tablespaces=true --add-drop-table - | gzip | gpg --symmetric --passphrase "$PASSPHRASE" --batch -o "$DB_OUTPUT_FILE_NAME"
else
echo 'Please install wp-cli and re-run this script'; exit 1;
wp --path="${WP_PATH}" db export --no-tablespaces=true --add-drop-table - | gzip > "$DB_OUTPUT_FILE_NAME"
fi
if [ "$?" != "0" ]; then
msg='[Error] Something went wrong while taking DB backup!'
echo; echo "$msg"; echo
echo "$msg" | mail -s 'DB Backup Failure' "$alertEmail"
[ -f "$DB_OUTPUT_FILE_NAME" ] && rm -f "$DB_OUTPUT_FILE_NAME"
exit 1
fi

# external backup
if [ "$BUCKET_NAME" != "" ]; then
if [ -z "$PASSPHRASE" ] ; then
$aws_cli s3 cp $DB_OUTPUT_FILE_NAME s3://$BUCKET_NAME/${DOMAIN_FULL_PATH}/db-backups/ --only-show-errors
else
$aws_cli s3 cp $ENCRYPTED_DB_OUTPUT_FILE_NAME s3://$BUCKET_NAME/${DOMAIN_FULL_PATH}/encrypted-db-backups/ --only-show-errors
fi
if [ "$?" != "0" ]; then
echo; echo 'Something went wrong while taking offsite backup';
echo "Check $LOG_FILE for any log info"; echo
[ -L "$DB_LATEST_FILE_NAME" ] && rm "$DB_LATEST_FILE_NAME"
ln -s "$DB_OUTPUT_FILE_NAME" "$DB_LATEST_FILE_NAME"

# send the backup offsite
if [ "$BUCKET_NAME" ]; then
cmd="aws s3 cp $DB_OUTPUT_FILE_NAME s3://$BUCKET_NAME/${DOMAIN_FULL_PATH}/db-backups/ --only-show-errors"
if $cmd; then
msg='Offsite backup successful.'
echo; echo "$msg"; echo
[ "$success_alert" ] && echo "$msg" | mail -s 'Offsite Backup Info' "$alertEmail"
else
echo; echo 'Offsite backup successful'; echo
msg='[Error] Something went wrong while taking offsite backup.'
echo; echo "$msg"; echo
echo "$msg" | mail -s 'Offsite Backup Info' "$alertEmail"
fi
fi

# Auto delete backups
[ -d "$BACKUP_PATH" ] && find $BACKUP_PATH -type f -mtime +$AUTODELETEAFTER -exec rm {} \;
[ -d $ENCRYPTED_BACKUP_PATH ] && find $ENCRYPTED_BACKUP_PATH -type f -mtime +$AUTODELETEAFTER -exec rm {} \;
# Auto delete backups
find -L "$BACKUP_PATH" -type f -mtime +$AUTODELETEAFTER -exec rm {} \;

if [ -z "$PASSPHRASE" ] ; then
echo; echo 'DB backup is done without encryption: '${DB_LATEST_FILE_NAME}' -> '${DB_OUTPUT_FILE_NAME}; echo
else
echo; echo 'DB backup is done encrypted: '${DB_LATEST_FILE_NAME}' -> '${ENCRYPTED_DB_OUTPUT_FILE_NAME}; echo
fi
echo "Database backup is done; please check the latest backup in '${BACKUP_PATH}'."
echo "Latest backup is at ${DB_OUTPUT_FILE_NAME}"

echo "Script ended on... $(date +%c)"
echo

Loading

0 comments on commit 289f820

Please sign in to comment.