|
1 | | -#!/bin/bash |
| 1 | +#!/usr/bin/env bash |
2 | 2 |
|
3 | 3 | # requirements |
4 | 4 | # ~/log, ~/backups, ~/path/to/example.com/public |
5 | 5 |
|
6 | | -# version - 5.2.1 |
| 6 | +version=6.0.0 |
7 | 7 |
|
8 | 8 | ### Variables - Please do not add trailing slash in the PATHs |
9 | 9 |
|
| 10 | +# auto delete older backups after certain number days |
| 11 | +# configurable using -k|--keepfor <days> |
| 12 | +AUTODELETEAFTER=7 |
| 13 | + |
10 | 14 | # where to store the database backups? |
11 | 15 | BACKUP_PATH=${HOME}/backups/db-backups |
12 | | -ENCRYPTED_BACKUP_PATH=${HOME}/backups/encrypted-db-backups |
13 | 16 |
|
14 | | -# the script assumes your sites are stored like ~/sites/example.com/public, ~/sites/example.net/public, ~/sites/example.org/public and so on. |
15 | | -# if you have a different pattern, such as ~/app/example.com/public, please change the following to fit the server environment! |
| 17 | +# a passphrase for encryption, in order to being able to use almost any special characters use "" |
| 18 | +# it's best to configure it in ~/.envrc file |
| 19 | +PASSPHRASE= |
| 20 | + |
| 21 | +# the script assumes your sites are stored like ~/sites/example.com, ~/sites/example.net, ~/sites/example.org and so on. |
| 22 | +# if you have a different pattern, such as ~/app/example.com, please change the following to fit the server environment! |
16 | 23 | SITES_PATH=${HOME}/sites |
17 | 24 |
|
18 | | -# if WP is in a sub-directory, please leave this empty! |
| 25 | +#-------- Do NOT Edit Below This Line --------# |
| 26 | + |
| 27 | +log_file=${HOME}/log/backups.log |
| 28 | +exec > >(tee -a "${log_file}") |
| 29 | +exec 2> >(tee -a "${log_file}" >&2) |
| 30 | + |
| 31 | +# Variables defined later in the script |
| 32 | +success_alert= |
| 33 | +custom_email= |
| 34 | +custom_wp_path= |
| 35 | +BUCKET_NAME= |
| 36 | +DOMAIN= |
19 | 37 | PUBLIC_DIR=public |
20 | 38 |
|
21 | | -# a passphrase for encryption, in order to being able to use almost any special characters use "" |
22 | | -PASSPHRASE= |
| 39 | +# get environment variables, if exists |
| 40 | +[ -f "$HOME/.envrc" ] && source ~/.envrc |
| 41 | +[ -f "$HOME/.env" ] && source ~/.env |
23 | 42 |
|
24 | | -# auto delete older backups after certain number days - default 60. YMMV |
25 | | -AUTODELETEAFTER=30 |
| 43 | +print_help() { |
| 44 | + printf '%s\n' "Take a database backup" |
| 45 | + echo |
| 46 | + printf 'Usage: %s [-b <name>] [-k <days>] [-e <email-address>] [-s] [-p <WP path>] [-v] [-h] example.com\n' "$0" |
| 47 | + echo |
| 48 | + printf '\t%s\t%s\n' "-b, --bucket" "Name of the bucket for offsite backup (default: none)" |
| 49 | + printf '\t%s\t%s\n' "-k, --keepfor" "# of days to keep the local backups (default: 7)" |
| 50 | + printf '\t%s\t%s\n' "-e, --email" "Email to send success/failures alerts (default: root@localhost)" |
| 51 | + printf '\t%s\t%s\n' "-s, --success" "Alert on successful backup too (default: alert only on failures)" |
| 52 | + printf '\t%s\t%s\n' "-p, --path" "Path to WP files (default: ~/sites/example.com/public or ~/public_html for cPanel)" |
| 53 | + echo |
| 54 | + printf '\t%s\t%s\n' "-v, --version" "Prints the version info" |
| 55 | + printf '\t%s\t%s\n' "-h, --help" "Prints help" |
| 56 | + |
| 57 | + echo |
| 58 | + echo "For more info, changelog and documentation... https://github.com/pothi/backup-wordpress" |
| 59 | +} |
| 60 | + |
| 61 | +# https://stackoverflow.com/a/62616466/1004587 |
| 62 | +# Convenience functions. |
| 63 | +EOL=$(printf '\1\3\3\7') |
| 64 | +opt= |
| 65 | +usage_error () { echo >&2 "$(basename $0): $1"; exit 2; } |
| 66 | +assert_argument () { test "$1" != "$EOL" || usage_error "$2 requires an argument"; } |
| 67 | + |
| 68 | +# One loop, nothing more. |
| 69 | +if [ "$#" != 0 ]; then |
| 70 | + set -- "$@" "$EOL" |
| 71 | + while [ "$1" != "$EOL" ]; do |
| 72 | + opt="$1"; shift |
| 73 | + case "$opt" in |
| 74 | + |
| 75 | + # Your options go here. |
| 76 | + -v|--version) echo $version; exit 0;; |
| 77 | + -V) echo $version; exit 0;; |
| 78 | + -h|--help) print_help; exit 0;; |
| 79 | + -b|--bucket) assert_argument "$1" "$opt"; BUCKET_NAME="$1"; shift;; |
| 80 | + -k|--keepfor) assert_argument "$1" "$opt"; AUTODELETEAFTER="$1"; shift;; |
| 81 | + -p|--path) assert_argument "$1" "$opt"; custom_wp_path="$1"; shift;; |
| 82 | + -e|--email) assert_argument "$1" "$opt"; custom_email="$1"; shift;; |
| 83 | + -s|--success) success_alert=1;; |
| 84 | + |
| 85 | + # Arguments processing. You may remove any unneeded line after the 1st. |
| 86 | + -|''|[!-]*) set -- "$@" "$opt";; # positional argument, rotate to the end |
| 87 | + --*=*) set -- "${opt%%=*}" "${opt#*=}" "$@";; # convert '--name=arg' to '--name' 'arg' |
| 88 | + -[!-]?*) set -- $(echo "${opt#-}" | sed 's/\(.\)/ -\1/g') "$@";; # convert '-abc' to '-a' '-b' '-c' |
| 89 | + --) while [ "$1" != "$EOL" ]; do set -- "$@" "$1"; shift; done;; # process remaining arguments as positional |
| 90 | + -*) usage_error "unknown option: '$opt'";; # catch misspelled options |
| 91 | + *) usage_error "this should NEVER happen ($opt)";; # sanity test for previous patterns |
| 92 | + |
| 93 | + esac |
| 94 | + done |
| 95 | + shift # $EOL |
| 96 | +fi |
26 | 97 |
|
27 | | -# You may hard-code the domain name |
28 | | -DOMAIN= |
| 98 | +# Do something cool with "$@"... \o/ |
29 | 99 |
|
30 | | -# AWS Variable can be hard-coded here |
31 | | -AWS_S3_BUCKET_NAME= |
| 100 | +# Get example.com |
| 101 | +if [ "$#" -gt 0 ]; then |
| 102 | + DOMAIN=$1 |
| 103 | + shift |
| 104 | +else |
| 105 | + print_help |
| 106 | + exit 2 |
| 107 | +fi |
32 | 108 |
|
33 | | -#-------- Do NOT Edit Below This Line --------# |
| 109 | +# compatibility with old syntax to get bucket name |
| 110 | +# To be removed in the future |
| 111 | +if [ "$#" -gt 0 ]; then |
| 112 | + BUCKET_NAME=$1 |
| 113 | + echo "You are using old syntax." |
| 114 | + print_help |
| 115 | + shift |
| 116 | +fi |
| 117 | + |
| 118 | +# unwanted argument/s |
| 119 | +if [ "$#" -gt 0 ]; then |
| 120 | + print_help |
| 121 | + exit 2 |
| 122 | +fi |
34 | 123 |
|
35 | 124 | # to capture non-zero exit code in the pipeline |
36 | 125 | set -o pipefail |
37 | 126 |
|
38 | 127 | # attempt to create log directory if it doesn't exist |
39 | | -[ -d "${HOME}/log" ] || mkdir -p ${HOME}/log |
40 | | -if [ "$?" -ne "0" ]; then |
41 | | - echo "Log directory not found at ~/log. This script can't create it, either!" |
42 | | - echo 'You may create it manually and re-run this script.' |
43 | | - exit 1 |
| 128 | +if [ ! -d "${HOME}/log" ]; then |
| 129 | + if ! mkdir -p "${HOME}"/log; then |
| 130 | + echo "Log directory not found at ~/log. This script can't create it, either!" |
| 131 | + echo 'You may create it manually and re-run this script.' |
| 132 | + exit 1 |
| 133 | + fi |
44 | 134 | fi |
45 | 135 | # attempt to create the backups directory, if it doesn't exist |
46 | | -[ -d "$BACKUP_PATH" ] || mkdir -p $BACKUP_PATH |
47 | | -if [ "$?" -ne "0" ]; then |
48 | | - echo "BACKUP_PATH is not found at $BACKUP_PATH. This script can't create it, either!" |
49 | | - echo 'You may create it manually and re-run this script.' |
50 | | - exit 1 |
51 | | -fi |
52 | | -# if passphrase is supplied, attempt to create backups directory for encrypt backups, if it doesn't exist |
53 | | -if [ -n "$PASSPHRASE" ]; then |
54 | | - [ -d "$ENCRYPTED_BACKUP_PATH" ] || mkdir -p $ENCRYPTED_BACKUP_PATH |
55 | | - if [ "$?" -ne "0" ]; then |
56 | | - echo "ENCRYPTED_BACKUP_PATH Is not found at $ENCRYPTED_BACKUP_PATH. This script can't create it, either!" |
| 136 | +if [ ! -d "$BACKUP_PATH" ]; then |
| 137 | + if ! mkdir -p "$BACKUP_PATH"; then |
| 138 | + echo "BACKUP_PATH is not found at $BACKUP_PATH. This script can't create it, either!" |
57 | 139 | echo 'You may create it manually and re-run this script.' |
58 | 140 | exit 1 |
59 | 141 | fi |
60 | 142 | fi |
61 | 143 |
|
62 | | -log_file=${HOME}/log/backups.log |
63 | | -exec > >(tee -a ${log_file} ) |
64 | | -exec 2> >(tee -a ${log_file} >&2) |
65 | | - |
66 | 144 | export PATH=~/bin:~/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin |
67 | 145 |
|
68 | | -declare -r script_name=$(basename "$0") |
69 | | -declare -r timestamp=$(date +%F_%H-%M-%S) |
70 | | -declare -r wp_cli=`which wp` |
71 | | -declare -r aws_cli=`which aws` |
72 | | - |
73 | | -if [ -z "$wp_cli" ]; then |
74 | | - echo "wp-cli is not found in $PATH. Exiting." |
75 | | - exit 1 |
76 | | -fi |
77 | | - |
78 | | -if [ -z "$aws_cli" ]; then |
79 | | - echo "aws-cli is not found in $PATH. Exiting." |
80 | | - exit 1 |
81 | | -fi |
| 146 | +script_name=$(basename "$0") |
| 147 | +timestamp=$(date +%F_%H-%M-%S) |
82 | 148 |
|
83 | | -cPanel=$(/usr/local/cpanel/cpanel -V 2>/dev/null) |
84 | | -if [ ! -z "$cPanel" ]; then |
85 | | - SITES_PATH=$HOME |
86 | | - PUBLIC_DIR=public_html |
87 | | -fi |
88 | | - |
89 | | -echo "'$script_name' started on... $(date +%c)" |
| 149 | +command -v wp >/dev/null || { echo >&2 "wp cli is not found in $PATH. Exiting."; exit 1; } |
| 150 | +command -v aws >/dev/null || { echo >&2 "aws cli is not found in $PATH. Exiting."; exit 1; } |
| 151 | +command -v mail >/dev/null || echo >&2 "[WARNING]: 'mail' command is not found in $PATH; Alerts will not be sent!" |
90 | 152 |
|
91 | | -let AUTODELETEAFTER-- |
92 | | - |
93 | | -# get environment variables, if exists |
94 | | -[ -f "$HOME/.envrc" ] && source ~/.envrc |
95 | | -[ -f "$HOME/.env" ] && source ~/.env |
| 153 | +((AUTODELETEAFTER--)) |
96 | 154 |
|
97 | 155 | # check for the variable/s in three places |
98 | 156 | # 1 - hard-coded value |
99 | 157 | # 2 - optional parameter while invoking the script |
100 | 158 | # 3 - environment files |
101 | 159 |
|
102 | | -if [ "$DOMAIN" == "" ]; then |
103 | | - if [ "$1" == "" ]; then |
104 | | - if [ "$WP_DOMAIN" != "" ]; then |
105 | | - DOMAIN=$WP_DOMAIN |
106 | | - else |
107 | | - echo "Usage $script_name example.com (S3 bucket name)"; exit 1 |
108 | | - fi |
109 | | - else |
110 | | - DOMAIN=$1 |
111 | | - fi |
| 160 | +alertEmail=${custom_email:-${BACKUP_ADMIN_EMAIL:-${ADMIN_EMAIL:-"root@localhost"}}} |
| 161 | + |
| 162 | +# Define paths |
| 163 | +# cPanel uses a different directory structure |
| 164 | +# dir_to_backup and db_dump are used only in full-backup.sh |
| 165 | +cPanel=$(/usr/local/cpanel/cpanel -V 2>/dev/null) |
| 166 | +if [ "$cPanel" ]; then |
| 167 | + SITES_PATH=$HOME |
| 168 | + PUBLIC_DIR=public_html |
| 169 | + WP_PATH=${SITES_PATH}/${PUBLIC_DIR} |
| 170 | + # dir_to_backup=public_html |
| 171 | + # db_dump=${WP_PATH}/db.sql |
| 172 | +else |
| 173 | + WP_PATH=${SITES_PATH}/${DOMAIN}/${PUBLIC_DIR} |
| 174 | + # dir_to_backup=${DOMAIN} |
| 175 | + # db_dump=${SITES_PATH}/${DOMAIN}/db.sql |
112 | 176 | fi |
113 | 177 |
|
114 | | -if [ "$BUCKET_NAME" == "" ]; then |
115 | | - if [ "$2" != "" ]; then |
116 | | - BUCKET_NAME=$2 |
117 | | - elif [ "$AWS_S3_BUCKET_NAME" != "" ]; then |
118 | | - BUCKET_NAME=$AWS_S3_BUCKET_NAME |
119 | | - fi |
| 178 | +if [ "$custom_wp_path" ]; then |
| 179 | + WP_PATH="$custom_wp_path" |
| 180 | + DB_OUTPUT_FILE_NAME=${custom_wp_path}/db.sql |
120 | 181 | fi |
121 | 182 |
|
122 | | -# WordPress root |
123 | | -WP_PATH=${SITES_PATH}/${DOMAIN}/${PUBLIC_DIR} |
124 | | -# For cPanel - main site |
125 | | -[ ! -d "$WP_PATH" ] && WP_PATH=${SITES_PATH}/${PUBLIC_DIR} |
126 | 183 | [ ! -d "$WP_PATH" ] && echo "WordPress is not found at $WP_PATH" && exit 1 |
127 | 184 |
|
| 185 | +echo "'$script_name' started on... $(date +%c)" |
| 186 | + |
128 | 187 | # convert forward slash found in sub-directories to hyphen |
129 | 188 | # ex: example.com/test would become example.com-test |
130 | | -DOMAIN_FULL_PATH=$(echo $DOMAIN | awk '{gsub(/\//,"-")}; 1') |
| 189 | +DOMAIN_FULL_PATH=$(echo "$DOMAIN" | awk '{gsub(/\//,"-")}; 1') |
131 | 190 |
|
132 | 191 | DB_OUTPUT_FILE_NAME=${BACKUP_PATH}/${DOMAIN_FULL_PATH}-${timestamp}.sql.gz |
133 | | -ENCRYPTED_DB_OUTPUT_FILE_NAME=${ENCRYPTED_BACKUP_PATH}/${DOMAIN_FULL_PATH}-${timestamp}.sql.gz |
134 | 192 | DB_LATEST_FILE_NAME=${BACKUP_PATH}/${DOMAIN_FULL_PATH}-latest.sql.gz |
135 | 193 |
|
136 | 194 | # take actual DB backup |
137 | | -if [ -f "$wp_cli" ]; then |
138 | | - $wp_cli --path=${WP_PATH} transient delete --all |
139 | | - $wp_cli --path=${WP_PATH} db export --no-tablespaces=true --add-drop-table - | gzip > $DB_OUTPUT_FILE_NAME |
140 | | - if [ "$?" != "0" ]; then |
141 | | - echo; echo 'Something went wrong while taking local backup!' |
142 | | - [ -f $DB_OUTPUT_FILE_NAME ] && rm -f $DB_OUTPUT_FILE_NAME |
143 | | - fi |
144 | | - |
145 | | - [ -L $DB_LATEST_FILE_NAME ] && rm $DB_LATEST_FILE_NAME |
146 | | - if [ -n "$PASSPHRASE" ] ; then |
147 | | - gpg --symmetric --passphrase $PASSPHRASE --batch -o ${ENCRYPTED_DB_OUTPUT_FILE_NAME} $DB_OUTPUT_FILE_NAME |
148 | | - [ -f $DB_OUTPUT_FILE_NAME ] && rm -f $DB_OUTPUT_FILE_NAME |
149 | | - ln -s $ENCRYPTED_DB_OUTPUT_FILE_NAME $DB_LATEST_FILE_NAME |
150 | | - else |
151 | | - ln -s $DB_OUTPUT_FILE_NAME $DB_LATEST_FILE_NAME |
152 | | - fi |
| 195 | +wp --path="${WP_PATH}" transient delete --all |
| 196 | +if [ -n "$PASSPHRASE" ] ; then |
| 197 | + DB_OUTPUT_FILE_NAME="${DB_OUTPUT_FILE_NAME}".gpg |
| 198 | + wp --path="${WP_PATH}" db export --no-tablespaces=true --add-drop-table - | gzip | gpg --symmetric --passphrase "$PASSPHRASE" --batch -o "$DB_OUTPUT_FILE_NAME" |
153 | 199 | else |
154 | | - echo 'Please install wp-cli and re-run this script'; exit 1; |
| 200 | + wp --path="${WP_PATH}" db export --no-tablespaces=true --add-drop-table - | gzip > "$DB_OUTPUT_FILE_NAME" |
| 201 | +fi |
| 202 | +if [ "$?" != "0" ]; then |
| 203 | + msg='[Error] Something went wrong while taking DB backup!' |
| 204 | + echo; echo "$msg"; echo |
| 205 | + echo "$msg" | mail -s 'DB Backup Failure' "$alertEmail" |
| 206 | + [ -f "$DB_OUTPUT_FILE_NAME" ] && rm -f "$DB_OUTPUT_FILE_NAME" |
| 207 | + exit 1 |
155 | 208 | fi |
156 | 209 |
|
157 | | -# external backup |
158 | | -if [ "$BUCKET_NAME" != "" ]; then |
159 | | - if [ -z "$PASSPHRASE" ] ; then |
160 | | - $aws_cli s3 cp $DB_OUTPUT_FILE_NAME s3://$BUCKET_NAME/${DOMAIN_FULL_PATH}/db-backups/ --only-show-errors |
161 | | - else |
162 | | - $aws_cli s3 cp $ENCRYPTED_DB_OUTPUT_FILE_NAME s3://$BUCKET_NAME/${DOMAIN_FULL_PATH}/encrypted-db-backups/ --only-show-errors |
163 | | - fi |
164 | | - if [ "$?" != "0" ]; then |
165 | | - echo; echo 'Something went wrong while taking offsite backup'; |
166 | | - echo "Check $LOG_FILE for any log info"; echo |
| 210 | +[ -L "$DB_LATEST_FILE_NAME" ] && rm "$DB_LATEST_FILE_NAME" |
| 211 | +ln -s "$DB_OUTPUT_FILE_NAME" "$DB_LATEST_FILE_NAME" |
| 212 | + |
| 213 | +# send the backup offsite |
| 214 | +if [ "$BUCKET_NAME" ]; then |
| 215 | + cmd="aws s3 cp $DB_OUTPUT_FILE_NAME s3://$BUCKET_NAME/${DOMAIN_FULL_PATH}/db-backups/ --only-show-errors" |
| 216 | + if $cmd; then |
| 217 | + msg='Offsite backup successful.' |
| 218 | + echo; echo "$msg"; echo |
| 219 | + [ "$success_alert" ] && echo "$msg" | mail -s 'Offsite Backup Info' "$alertEmail" |
167 | 220 | else |
168 | | - echo; echo 'Offsite backup successful'; echo |
| 221 | + msg='[Error] Something went wrong while taking offsite backup.' |
| 222 | + echo; echo "$msg"; echo |
| 223 | + echo "$msg" | mail -s 'Offsite Backup Info' "$alertEmail" |
169 | 224 | fi |
170 | 225 | fi |
171 | 226 |
|
172 | | -# Auto delete backups |
173 | | -[ -d "$BACKUP_PATH" ] && find $BACKUP_PATH -type f -mtime +$AUTODELETEAFTER -exec rm {} \; |
174 | | -[ -d $ENCRYPTED_BACKUP_PATH ] && find $ENCRYPTED_BACKUP_PATH -type f -mtime +$AUTODELETEAFTER -exec rm {} \; |
| 227 | +# Auto delete backups |
| 228 | +find -L "$BACKUP_PATH" -type f -mtime +$AUTODELETEAFTER -exec rm {} \; |
175 | 229 |
|
176 | | -if [ -z "$PASSPHRASE" ] ; then |
177 | | - echo; echo 'DB backup is done without encryption: '${DB_LATEST_FILE_NAME}' -> '${DB_OUTPUT_FILE_NAME}; echo |
178 | | -else |
179 | | - echo; echo 'DB backup is done encrypted: '${DB_LATEST_FILE_NAME}' -> '${ENCRYPTED_DB_OUTPUT_FILE_NAME}; echo |
180 | | -fi |
| 230 | +echo "Database backup is done; please check the latest backup in '${BACKUP_PATH}'." |
| 231 | +echo "Latest backup is at ${DB_OUTPUT_FILE_NAME}" |
181 | 232 |
|
182 | 233 | echo "Script ended on... $(date +%c)" |
| 234 | +echo |
183 | 235 |
|
0 commit comments