Skip to content

Commit 22f7d0e

Browse files
authored
Create README.md
0 parents  commit 22f7d0e

File tree

1 file changed

+381
-0
lines changed

1 file changed

+381
-0
lines changed

README.md

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
# Automated rsync Backup Script
2+
3+
This script is for automating backups of a local directory to a remote server (like a Hetzner Storage Box) using `rsync` over SSH.
4+
5+
---
6+
7+
## Features
8+
9+
- **External Configuration**: All settings (paths, hosts) and credentials (API tokens) are stored in separate files, not in the script.
10+
- **Portable**: The entire backup setup (script + configs) can be moved to a new server by copying one directory.
11+
- **ntfy Notifications**: Sends detailed success or failure notifications to a specified ntfy topic.
12+
- **Robust Error Handling**: Uses `set -Euo pipefail` and a global `ERR` trap to catch and report any unexpected errors.
13+
- **Locking**: Prevents multiple instances from running simultaneously using `flock`. Essential for cron jobs.
14+
- **Log Rotation**: Automatically rotates the log file when it exceeds a defined size.
15+
- **Special Modes**: Includes modes for `--dry-run`, integrity checking (`--checksum`), and getting a quick summary (`--summary`).
16+
- **Prerequisite Checks**: Verifies that all required commands and SSH connectivity are working before running.
17+
18+
---
19+
20+
## File Structure
21+
22+
All files should be placed in a single directory (e.g., `/root/scripts/backup`).
23+
24+
```
25+
26+
/root/scripts/backup/
27+
├── backup_script.sh (The main script)
28+
├── backup_rsync.conf (Your main backup settings)
29+
├── credentials.conf (Your secret token and ntfy URL)
30+
└── rsync_exclude.txt (Files and patterns to exclude)
31+
32+
````
33+
34+
---
35+
36+
## Setup Instructions
37+
38+
Follow these steps to get the backup system running.
39+
40+
### 1. Prerequisites
41+
42+
First, ensure the required tools are installed. On Debian/Ubuntu, you can run:
43+
```sh
44+
sudo apt-get update && sudo apt-get install rsync curl coreutils util-linux
45+
````
46+
47+
*(coreutils provides `numfmt`, `stat`, etc. and util-linux provides `flock`)*
48+
49+
### 2\. Passwordless SSH Login
50+
51+
The script needs to log into the Hetzner Storage Box without a password.
52+
53+
- **Generate an SSH key** on your server if you don't have one:
54+
55+
```sh
56+
ssh-keygen -t rsa -b 4096
57+
```
58+
59+
(Just press Enter through all the prompts).
60+
61+
- **Copy your public key** to the Hetzner Storage Box. First, view your public key:
62+
63+
```sh
64+
cat ~/.ssh/id_rsa.pub
65+
```
66+
67+
- Go to your Hetzner Robot panel, select your Storage Box, and paste the entire public key content into the "SSH Keys" section.
68+
69+
- **Test the connection**. Replace `u444300` and the hostname with your own details.
70+
71+
```sh
72+
ssh -p 23 [email protected] 'echo "Connection successful"'
73+
```
74+
75+
If this works without asking for a password, you are ready.
76+
77+
### 3\. Place and Configure Files
78+
79+
1. Create your script directory: `mkdir -p /root/scripts/backup && cd /root/scripts/backup`
80+
2. Create the four files (`backup_script.sh`, `backup_rsync.conf`, `credentials.conf`, `rsync_exclude.txt`) in this directory using the content provided below.
81+
3. **Make the script executable**:
82+
```sh
83+
chmod +x backup_script.sh
84+
```
85+
4. **Set secure permissions** for your credentials:
86+
```sh
87+
chmod 600 credentials.conf
88+
```
89+
5. Edit `backup_rsync.conf` and `credentials.conf` to match your server paths, Hetzner details, and ntfy topic.
90+
6. Edit `rsync_exclude.txt` to list any files or directories you wish to skip.
91+
92+
### 4\. Set up a Cron Job
93+
94+
To run the backup automatically, edit the root crontab.
95+
96+
- Open the crontab editor:
97+
```sh
98+
crontab -e
99+
```
100+
- Add a line to schedule the script. This example runs the backup every day at 3:00 AM.
101+
```crontab
102+
# Run the rsync backup every day at 3:00 AM
103+
0 3 * * * /root/scripts/backup/backup_script.sh >/dev/null 2>&1
104+
```
105+
*(Redirecting output to `/dev/null` is fine since the script handles its own logging and notifications).*
106+
107+
-----
108+
109+
## Usage
110+
111+
- **Run Manually**: `cd /root/scripts/backup && ./backup_script.sh`
112+
- **Dry Run** (see what would change without doing anything): `./backup_script.sh --dry-run`
113+
- **Check Integrity** (compare local and remote files): `./backup_script.sh --checksum`
114+
- **Get Mismatch Count**: `./backup_script.sh --summary`
115+
116+
The log file is located at `/var/log/backup_rsync.log` by default.
117+
118+
-----
119+
120+
### **`backup_script.sh`**
121+
122+
```bash
123+
124+
#!/bin/bash
125+
126+
# =================================================================
127+
# SCRIPT INITIALIZATION & SETUP
128+
# =================================================================
129+
set -Euo pipefail
130+
umask 077
131+
132+
# --- Determine script's location to load local config files ---
133+
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
134+
135+
# --- Source Configuration Files ---
136+
# Non-sensitive settings from the script's directory
137+
if [ -f "${SCRIPT_DIR}/backup_rsync.conf" ]; then
138+
source "${SCRIPT_DIR}/backup_rsync.conf"
139+
else
140+
echo "FATAL: Main configuration file backup_rsync.conf not found in ${SCRIPT_DIR}." >&2
141+
exit 1
142+
fi
143+
# Sensitive credentials from the script's directory
144+
if [ -f "${SCRIPT_DIR}/credentials.conf" ]; then
145+
source "${SCRIPT_DIR}/credentials.conf"
146+
else
147+
echo "FATAL: Credentials file credentials.conf not found in ${SCRIPT_DIR}." >&2
148+
exit 1
149+
fi
150+
151+
152+
# =================================================================
153+
# SCRIPT CONFIGURATION (STATIC)
154+
# =================================================================
155+
# These values are less likely to change or are derived from config
156+
REMOTE_TARGET="${HETZNER_BOX}:${BOX_DIR}"
157+
LOCK_FILE="/tmp/backup_rsync.lock"
158+
MAX_LOG_SIZE=10485760 # 10 MB in bytes
159+
160+
RSYNC_OPTS=(
161+
-avz
162+
--stats
163+
--delete
164+
--partial
165+
--timeout=60
166+
--exclude-from="$EXCLUDE_FROM"
167+
-e "ssh -p $SSH_PORT"
168+
)
169+
170+
# =================================================================
171+
# HELPER FUNCTIONS
172+
# =================================================================
173+
174+
send_ntfy() {
175+
local title="$1"
176+
local tags="$2"
177+
local priority="${3:-default}"
178+
local message="$4"
179+
# Check for token and URL to prevent curl errors
180+
if [ -z "${NTFY_TOKEN:-}" ] || [ -z "${NTFY_URL:-}" ]; then return; fi
181+
curl -s -u ":$NTFY_TOKEN" \
182+
-H "Title: ${title}" \
183+
-H "Tags: ${tags}" \
184+
-H "Priority: ${priority}" \
185+
-d "$message" \
186+
"$NTFY_URL" > /dev/null 2>> "$LOG_FILE"
187+
}
188+
189+
run_integrity_check() {
190+
local rsync_check_opts=(
191+
-ainc
192+
--delete
193+
--exclude-from="$EXCLUDE_FROM"
194+
--out-format="%n"
195+
-e "ssh -p $SSH_PORT"
196+
)
197+
LC_ALL=C rsync "${rsync_check_opts[@]}" "$LOCAL_DIR" "$REMOTE_TARGET" 2>> "$LOG_FILE"
198+
}
199+
200+
format_backup_stats() {
201+
local rsync_output="$1"
202+
local bytes
203+
bytes=$(echo "$rsync_output" | grep 'Total transferred file size' | awk '{gsub(/,/, ""); print $5}')
204+
205+
if [[ "$bytes" =~ ^[0-9]+$ && "$bytes" -gt 0 ]]; then
206+
printf "Data Transferred: %s" "$(numfmt --to=iec-i --suffix=B --format="%.2f" "$bytes")"
207+
else
208+
printf "Data Transferred: 0 B (No changes)"
209+
fi
210+
}
211+
212+
# =================================================================
213+
# PRE-FLIGHT CHECKS & SETUP
214+
# =================================================================
215+
216+
trap 'send_ntfy "❌ Backup Crashed: ${HOSTNAME}" "x" "high" "Backup script terminated unexpectedly. Check log: ${LOG_FILE}"' ERR
217+
218+
REQUIRED_CMDS=(rsync curl flock hostname date stat mv touch awk numfmt grep)
219+
for cmd in "${REQUIRED_CMDS[@]}"; do
220+
if ! command -v "$cmd" &>/dev/null; then
221+
echo "FATAL: Required command '$cmd' not found. Please install it." >&2
222+
trap - ERR
223+
exit 10
224+
fi
225+
done
226+
227+
if ! ssh -p "$SSH_PORT" -o BatchMode=yes -o ConnectTimeout=10 "$HETZNER_BOX" 'exit' 2>/dev/null; then
228+
send_ntfy "❌ SSH FAILED: ${HOSTNAME}" "x" "high" "Unable to SSH into $HETZNER_BOX. Check keys and connectivity."
229+
trap - ERR
230+
exit 6
231+
fi
232+
233+
if ! [ -f "$EXCLUDE_FROM" ]; then
234+
send_ntfy "❌ Backup FAILED: ${HOSTNAME}" "x" "high" "FATAL: Exclude file not found at $EXCLUDE_FROM"
235+
trap - ERR
236+
exit 3
237+
fi
238+
if [[ "$LOCAL_DIR" != */ ]]; then
239+
send_ntfy "❌ Backup FAILED: ${HOSTNAME}" "x" "high" "FATAL: LOCAL_DIR must end with a trailing slash ('/')"
240+
trap - ERR
241+
exit 2
242+
fi
243+
244+
# =================================================================
245+
# SCRIPT EXECUTION
246+
# =================================================================
247+
248+
HOSTNAME=$(hostname -s)
249+
250+
if [[ "${1:-}" ]]; then
251+
trap - ERR
252+
case "${1}" in
253+
--dry-run)
254+
echo "--- DRY RUN MODE ACTIVATED ---"
255+
rsync "${RSYNC_OPTS[@]}" --dry-run "$LOCAL_DIR" "$REMOTE_TARGET"
256+
echo "--- DRY RUN COMPLETED ---"
257+
exit 0
258+
;;
259+
--checksum)
260+
echo "--- INTEGRITY CHECK MODE ACTIVATED ---"
261+
FILE_DISCREPANCIES=$(run_integrity_check)
262+
if [ -z "$FILE_DISCREPANCIES" ]; then
263+
send_ntfy "✅ Backup Integrity OK: ${HOSTNAME}" "white_check_mark" "default" "Checksum validation passed. No discrepancies found."
264+
else
265+
ISSUE_LIST=$(echo "${FILE_DISCREPANCIES}" | head -n 10)
266+
printf -v FAILURE_MSG "Backup integrity check FAILED.\n\nFirst 10 differing files:\n%s\n\nCheck log for full details." "${ISSUE_LIST}"
267+
send_ntfy "❌ Backup Integrity FAILED: ${HOSTNAME}" "x" "high" "${FAILURE_MSG}"
268+
fi
269+
exit 0
270+
;;
271+
--summary)
272+
echo "--- INTEGRITY SUMMARY MODE ---"
273+
MISMATCH_COUNT=$(run_integrity_check | wc -l)
274+
printf "🚨 Total files with checksum mismatches: %d\n" "$MISMATCH_COUNT"
275+
exit 0
276+
;;
277+
esac
278+
fi
279+
280+
exec 200>"$LOCK_FILE"
281+
flock -n 200 || { echo "Another instance is running, exiting."; exit 5; }
282+
283+
if [ -f "$LOG_FILE" ] && [ "$(stat -c%s "$LOG_FILE")" -gt "$MAX_LOG_SIZE" ]; then
284+
mv "$LOG_FILE" "${LOG_FILE}.$(date +%Y%m%d_%H%M%S)"
285+
touch "$LOG_FILE"
286+
fi
287+
288+
echo "============================================================" >> "$LOG_FILE"
289+
echo "[$HOSTNAME] [$(date '+%Y-%m-%d %H:%M:%S')] Starting rsync backup" >> "$LOG_FILE"
290+
291+
# --- Execute Backup & Capture Output ---
292+
RSYNC_OUTPUT=$(rsync "${RSYNC_OPTS[@]}" "$LOCAL_DIR" "$REMOTE_TARGET" 2>&1)
293+
RSYNC_EXIT_CODE=$?
294+
295+
# Log the full output from the command
296+
echo "$RSYNC_OUTPUT" >> "$LOG_FILE"
297+
298+
if [ $RSYNC_EXIT_CODE -eq 0 ]; then
299+
trap - ERR
300+
echo "[$HOSTNAME] [$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: rsync completed." >> "$LOG_FILE"
301+
BACKUP_STATS=$(format_backup_stats "$RSYNC_OUTPUT")
302+
printf -v SUCCESS_MSG "rsync backup completed successfully.\n\n%s" "${BACKUP_STATS}"
303+
send_ntfy "✅ Backup SUCCESS: ${HOSTNAME}" "white_check_mark" "default" "${SUCCESS_MSG}"
304+
else
305+
EXIT_CODE=$RSYNC_EXIT_CODE
306+
trap - ERR
307+
echo "[$HOSTNAME] [$(date '+%Y-%m-%d %H:%M:%S')] FAILED: rsync exited with code: $EXIT_CODE." >> "$LOG_FILE"
308+
send_ntfy "❌ Backup FAILED: ${HOSTNAME}" "x" "high" "rsync failed on ${HOSTNAME} with exit code ${EXIT_CODE}. Check log for details."
309+
fi
310+
311+
echo "======================= Run Finished =======================" >> "$LOG_FILE"
312+
echo "" >> "$LOG_FILE"
313+
314+
````
315+
316+
-----
317+
318+
### **`backup_rsync.conf`**
319+
320+
```bash
321+
# Configuration for the rsync backup script
322+
323+
# --- Source and Destination ---
324+
# IMPORTANT: LOCAL_DIR must end with a trailing slash!
325+
LOCAL_DIR="/home/user/"
326+
327+
# Directory on the remote storage box
328+
BOX_DIR="/home/myvps/"
329+
330+
# Hetzner Storage Box details (username and hostname)
331+
HETZNER_BOX="[email protected]"
332+
SSH_PORT="23"
333+
334+
# --- Logging ---
335+
LOG_FILE="/var/log/backup_rsync.log"
336+
337+
# --- Exclude File ---
338+
# This line uses the SCRIPT_DIR variable from the main script
339+
# to locate rsync_exclude.txt in the same directory.
340+
EXCLUDE_FROM="${SCRIPT_DIR}/rsync_exclude.txt"
341+
342+
```
343+
344+
-----
345+
346+
### **`credentials.conf`**
347+
348+
```ini
349+
# Sensitive information for the backup script
350+
# !! IMPORTANT !! Set permissions to 600: chmod 600 credentials.conf
351+
352+
NTFY_TOKEN="tk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
353+
NTFY_URL="https://ntfy.sh/your-private-topic-name"
354+
355+
```
356+
357+
-----
358+
359+
### **`rsync_exclude.txt`**
360+
361+
```
362+
# List of files/directories to exclude from backup, one per line.
363+
# See 'man rsync' for pattern matching rules.
364+
365+
# Common cache and temporary files
366+
.cache/
367+
/tmp/
368+
*.tmp
369+
*.bak
370+
*.swp
371+
372+
# Specific application caches/dependencies
373+
/node_modules/
374+
/vendor/
375+
__pycache__/
376+
377+
# System files that shouldn't be backed up
378+
/lost+found/
379+
.DS_Store
380+
Thumbs.db
381+
```

0 commit comments

Comments
 (0)