Skip to content

Commit ad9323e

Browse files
committed
feat: harden backup script runtime and credential handling
Closes #3 Closes #4 Closes #5 Closes #7
1 parent 022985c commit ad9323e

File tree

2 files changed

+165
-27
lines changed

2 files changed

+165
-27
lines changed

.env.example

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ db_host=""
33
db_user=""
44
db_pass=""
55

6-
# Maxiumum number of parallel jobs
7-
max_parallel=10
6+
# Maximum number of parallel jobs.
7+
# Use an integer (e.g. 10) or "auto" for dynamic sizing.
8+
max_parallel=auto
9+
10+
# Only used when max_parallel=auto
11+
max_parallel_cap=16
12+
mysql_connection_reserve=20
813

914
# Keeping the last 5 backups
1015
max_backups=5
1116

1217
# Backup directory
13-
backup_dir="backup/"
18+
backup_dir="backup/"

dump.sh

Lines changed: 157 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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
3467
mkdir -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
37129
cleanup_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
52172
backup_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
}
73191
export -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
82215
echo "Cleaning up old backups"
83-
cleanup_old_backups $backup_dir
216+
cleanup_old_backups "$backup_dir" "$max_backups"
84217

85218
# Done
86219
echo -e "\n\nBackup completed\n"

0 commit comments

Comments
 (0)