@@ -25,45 +25,163 @@ set -euo pipefail
2525# SOFTWARE.
2626
2727# Load env file
28- export $( cat .env | xargs)
28+ if [[ ! -f .env ]]; then
29+ echo " Missing .env file" >&2
30+ exit 1
31+ fi
32+
33+ set -a
34+ # shellcheck disable=SC1091
35+ . ./.env
36+ set +a
37+
38+ : " ${db_host:? db_host is required} "
39+ : " ${db_user:? db_user is required} "
40+ : " ${db_pass:? db_pass is required} "
41+ : " ${max_parallel:? max_parallel is required} "
42+ : " ${max_backups:? max_backups is required} "
43+ : " ${backup_dir:? backup_dir is required} "
44+
45+ max_parallel_cap=" ${max_parallel_cap:- 16} "
46+ mysql_connection_reserve=" ${mysql_connection_reserve:- 20} "
47+
48+ if ! [[ " $max_backups " =~ ^[0-9]+$ ]]; then
49+ echo " max_backups must be a non-negative integer" >&2
50+ exit 1
51+ fi
52+
53+ if ! [[ " $max_parallel_cap " =~ ^[0-9]+$ ]] || (( max_parallel_cap < 1 )) ; then
54+ echo " max_parallel_cap must be an integer >= 1" >&2
55+ exit 1
56+ fi
57+
58+ if ! [[ " $mysql_connection_reserve " =~ ^[0-9]+$ ]]; then
59+ echo " mysql_connection_reserve must be a non-negative integer" >&2
60+ exit 1
61+ fi
2962
3063# Config
31- timestamp=$( date +" %Y%m%d-%H%M%S" )
64+ timestamp=" $( date +" %Y%m%d-%H%M%S" ) "
3265
3366# Make sure backup directory exists
3467mkdir -p " $backup_dir /$timestamp "
3568
69+ mysql_cnf=" $( mktemp " ${TMPDIR:-/ tmp} /mysql-backup-XXXXXX.cnf" ) "
70+ chmod 600 " $mysql_cnf "
71+ cat > " $mysql_cnf " << EOF
72+ [client]
73+ host=$db_host
74+ user=$db_user
75+ password=$db_pass
76+ EOF
77+ trap ' rm -f "$mysql_cnf"' EXIT
78+
79+ unset db_pass
80+
81+ resolve_parallel_jobs () {
82+ local max_parallel_value=" $1 "
83+ local max_parallel_cap_value=" $2 "
84+ local connection_reserve=" $3 "
85+ local mysql_cnf_path=" $4 "
86+
87+ if [[ " $max_parallel_value " != " auto" ]]; then
88+ if ! [[ " $max_parallel_value " =~ ^[0-9]+$ ]] || (( max_parallel_value < 1 )) ; then
89+ echo " max_parallel must be a positive integer or 'auto'" >&2
90+ return 1
91+ fi
92+ echo " $max_parallel_value "
93+ return 0
94+ fi
95+
96+ local cpu_count
97+ cpu_count=" $( nproc 2> /dev/null || getconf _NPROCESSORS_ONLN 2> /dev/null || sysctl -n hw.logicalcpu 2> /dev/null || echo 1) "
98+ if ! [[ " $cpu_count " =~ ^[0-9]+$ ]] || (( cpu_count < 1 )) ; then
99+ cpu_count=1
100+ fi
101+
102+ local db_max_connections
103+ db_max_connections=" $( mysql --defaults-extra-file=" $mysql_cnf_path " -Nse " SELECT @@max_connections;" 2> /dev/null || true) "
104+ if ! [[ " $db_max_connections " =~ ^[0-9]+$ ]] || (( db_max_connections < 1 )) ; then
105+ echo " Could not determine MySQL max_connections for auto max_parallel" >&2
106+ return 1
107+ fi
108+
109+ local connection_budget=$(( db_max_connections - connection_reserve))
110+ if (( connection_budget < 1 )) ; then
111+ connection_budget=1
112+ fi
113+
114+ local jobs=" $cpu_count "
115+ if (( connection_budget < jobs)) ; then
116+ jobs=" $connection_budget "
117+ fi
118+ if (( max_parallel_cap_value < jobs)) ; then
119+ jobs=" $max_parallel_cap_value "
120+ fi
121+ if (( jobs < 1 )) ; then
122+ jobs=1
123+ fi
124+
125+ echo " $jobs "
126+ }
127+
36128# Function to cleanup old backups
37129cleanup_old_backups () {
38- local backup_dir=" $1 "
39- local backups=($( ls -r -t " $backup_dir " | grep " ^[0-9]*-[0-9]*$" ) )
40- local num_backups=${# backups[@]}
41-
42- if [ $num_backups -gt $max_backups ]; then
43- local num_to_delete=$(( num_backups - max_backups))
44- for (( i = 0 ; i < num_to_delete; i++ )) ; do
45- echo " Deleting old backup: ${backups[$i]} "
46- rm -r " $backup_dir /${backups[$i]} "
47- done
130+ local backup_root=" $1 "
131+ local keep_count=" $2 "
132+ local -a backups=()
133+
134+ if [[ -z " $backup_root " || " $backup_root " == " /" ]]; then
135+ echo " Invalid backup_dir for cleanup: '$backup_root '" >&2
136+ return 1
137+ fi
138+
139+ if [[ ! -d " $backup_root " ]]; then
140+ echo " Backup directory does not exist for cleanup: $backup_root " >&2
141+ return 1
142+ fi
143+
144+ local dir_name
145+ for dir_name in " $backup_root " /* ; do
146+ [[ -d " $dir_name " ]] || continue
147+ dir_name=" $( basename " $dir_name " ) "
148+ [[ " $dir_name " =~ ^[0-9]{8}-[0-9]{6}$ ]] || continue
149+ backups+=(" $dir_name " )
150+ done
151+
152+ if (( ${# backups[@]} == 0 )) ; then
153+ return 0
48154 fi
155+
156+ IFS=$' \n ' backups=($( printf ' %s\n' " ${backups[@]} " | sort -r) )
157+ unset IFS
158+ local num_backups=" ${# backups[@]} "
159+
160+ if (( num_backups <= keep_count)) ; then
161+ return 0
162+ fi
163+
164+ local i
165+ for (( i = keep_count; i < num_backups; i++ )) ; do
166+ echo " Deleting old backup: ${backups[$i]} "
167+ rm -rf -- " $backup_root /${backups[$i]} "
168+ done
49169}
50170
51- # Define function for DB Export
171+ # Define function for DB export
52172backup_db () {
53173 local db=" $1 "
54- local db_user=" $2 "
55- local db_pass=" $3 "
56- local db_host=" $4 "
57- local timestamp=" $5 "
58- local backup_dir=" $6 "
174+ local backup_timestamp=" $2 "
175+ local backup_root=" $3 "
176+ local mysql_cnf_path=" $4 "
59177
60178 echo " Backing up $db "
61179 if mysqldump \
62- --user=" $db_user " --password=" $db_pass " \
63- --host=" $db_host " \
180+ --defaults-extra-file=" $mysql_cnf_path " \
64181 --single-transaction \
65182 --skip-lock-tables \
66- " $db " | gzip > " $backup_dir /$timestamp /$db .sql.gz" ; then
183+ --databases " $db " \
184+ | gzip > " $backup_root /$backup_timestamp /$db .sql.gz" ; then
67185 echo " Backup of $db completed"
68186 else
69187 echo " Backup of $db failed" >&2
@@ -72,15 +190,30 @@ backup_db() {
72190}
73191export -f backup_db
74192
193+ parallel_jobs=" $( resolve_parallel_jobs " $max_parallel " " $max_parallel_cap " " $mysql_connection_reserve " " $mysql_cnf " ) "
194+ echo " Using $parallel_jobs parallel job(s)"
195+
75196# Get list of all databases
76- databases=$( mysql -h $db_host -u $db_user --password=" $db_pass " -e " SHOW DATABASES;" | grep -Ev " (Database|information_schema|performance_schema|mysql|sys|vapor)" )
197+ databases=()
198+ while IFS= read -r db; do
199+ [[ -n " $db " ]] && databases+=(" $db " )
200+ done < <(
201+ mysql --defaults-extra-file=" $mysql_cnf " -Nse " SHOW DATABASES;" \
202+ | grep -Ev " ^(information_schema|performance_schema|mysql|sys|vapor)$" || true
203+ )
77204
78205# Run export job
79- echo " $databases " | parallel --halt soon,fail=1 -j " $max_parallel " backup_db {} " $db_user " " $db_pass " " $db_host " " $timestamp " " $backup_dir "
206+ if (( ${# databases[@]} == 0 )) ; then
207+ echo " No user databases found for backup"
208+ else
209+ printf ' %s\n' " ${databases[@]} " \
210+ | parallel --halt soon,fail=1 -j " $parallel_jobs " \
211+ backup_db {} " $timestamp " " $backup_dir " " $mysql_cnf "
212+ fi
80213
81214# Clean up old backups
82215echo " Cleaning up old backups"
83- cleanup_old_backups $backup_dir
216+ cleanup_old_backups " $backup_dir " " $max_backups "
84217
85218# Done
86219echo -e " \n\nBackup completed\n"
0 commit comments