Skip to content

Commit f52cb45

Browse files
committed
Harden backup and restore reliability paths
1 parent 7aab431 commit f52cb45

4 files changed

Lines changed: 108 additions & 28 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ This tool backs up configs, databases, and compose files every night. When somet
2929
| Item | Details |
3030
|------|---------|
3131
| **Config files** | config.xml, config.yml, settings.json, *.conf from each service |
32-
| **Databases** | All *.db files (Sonarr, Radarr, Prowlarr, Lidarr, Bazarr, etc.) |
32+
| **Databases** | All *.db files (Sonarr, Radarr, Prowlarr, Lidarr, Bazarr, etc.), snapshot-backed with sqlite3 when available |
3333
| **Compose file** | docker-compose.yml |
3434
| **Environment** | .env (sensitive values redacted) |
3535
| **Container state** | Snapshot of running containers at backup time |
@@ -85,6 +85,7 @@ bash restore.sh 20260222-020000
8585
```
8686

8787
The restore process stops your containers, copies configs and databases back, restarts everything, and runs a health check.
88+
If container restart fails, the script exits non-zero and prints clear remediation steps instead of claiming success.
8889

8990
**Note:** .env values are redacted in backups for security. After restoring, check your .env file and re-add any secrets if needed.
9091

backup.sh

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,27 @@ usage() {
2121
echo " --path DIR Media directory (default: ~/Media)"
2222
echo " --keep DAYS Days of backups to keep (default: 14)"
2323
echo " --help Show this help"
24-
exit 0
24+
exit "${1:-0}"
2525
}
2626

2727
while [[ $# -gt 0 ]]; do
2828
case "$1" in
29-
--path) MEDIA_DIR="$2"; shift 2 ;;
30-
--keep) KEEP_DAYS="$2"; shift 2 ;;
29+
--path)
30+
if [[ $# -lt 2 ]]; then
31+
echo -e "${RED}ERR${NC} Missing value for --path"
32+
usage 1
33+
fi
34+
MEDIA_DIR="$2"
35+
shift 2
36+
;;
37+
--keep)
38+
if [[ $# -lt 2 ]]; then
39+
echo -e "${RED}ERR${NC} Missing value for --keep"
40+
usage 1
41+
fi
42+
KEEP_DAYS="$2"
43+
shift 2
44+
;;
3145
--help) usage ;;
3246
*) echo -e "${RED}ERR${NC} Unknown option: $1"; exit 1 ;;
3347
esac
@@ -61,7 +75,12 @@ if [[ ! -d "$MEDIA_DIR" ]]; then
6175
exit 1
6276
fi
6377

64-
mkdir -p "$BACKUP_DIR" "$LOG_DIR" "$BACKUP_STAGING"
78+
if ! [[ "$KEEP_DAYS" =~ ^[0-9]+$ ]]; then
79+
echo -e "${RED}ERR${NC} --keep must be a whole number"
80+
exit 1
81+
fi
82+
83+
mkdir -p "$BACKUP_DIR" "$LOG_DIR" "$BACKUP_STAGING" "$BACKUP_STAGING/configs" "$BACKUP_STAGING/databases"
6584

6685
# ==============================
6786
# Config files
@@ -91,13 +110,30 @@ echo ""
91110
# ==============================
92111
echo -e "${CYAN}--- Databases ---${NC}"
93112

113+
# Use sqlite online backup when available to avoid inconsistent hot copies.
114+
backup_db_file() {
115+
local src="$1"
116+
local dst="$2"
117+
local rel="$3"
118+
119+
if command -v sqlite3 &>/dev/null; then
120+
if sqlite3 "$src" ".timeout 5000" ".backup \"$dst\"" >/dev/null 2>&1; then
121+
echo -e "${GREEN}OK${NC} Snapshot database: $rel"
122+
return 0
123+
fi
124+
echo -e "${YELLOW}WRN${NC} sqlite3 snapshot failed for $rel, falling back to file copy"
125+
fi
126+
127+
cp "$src" "$dst"
128+
echo -e "${GREEN}OK${NC} Copied database: $rel"
129+
}
130+
94131
find "$MEDIA_DIR" -maxdepth 3 -type f -name "*.db" \
95132
! -path "*/backups/*" ! -path "*/logs/*" 2>/dev/null | while read -r file; do
96133
rel_path="${file#$MEDIA_DIR/}"
97134
dest_dir="$BACKUP_STAGING/databases/$(dirname "$rel_path")"
98135
mkdir -p "$dest_dir"
99-
cp "$file" "$dest_dir/"
100-
echo -e "${GREEN}OK${NC} Copied database: $rel_path"
136+
backup_db_file "$file" "$dest_dir/$(basename "$file")" "$rel_path"
101137
done
102138

103139
DB_COUNT=$(find "$BACKUP_STAGING/databases" -type f 2>/dev/null | wc -l | tr -d ' ')
@@ -166,11 +202,9 @@ echo ""
166202
# ==============================
167203
echo -e "${CYAN}--- Pruning Old Backups ---${NC}"
168204

169-
PRUNED=0
170205
find "$BACKUP_DIR" -name "backup-*.tar.gz" -type f -mtime +"$KEEP_DAYS" 2>/dev/null | while read -r old; do
171206
rm -f "$old"
172207
echo -e "${YELLOW}DEL${NC} Removed $(basename "$old")"
173-
PRUNED=$((PRUNED + 1))
174208
done
175209

176210
REMAINING=$(find "$BACKUP_DIR" -name "backup-*.tar.gz" -type f 2>/dev/null | wc -l | tr -d ' ')

install.sh

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,19 @@ usage() {
3333
echo " bash install.sh # Install nightly backup at 2am"
3434
echo " bash install.sh --hour 3 # Install nightly backup at 3am"
3535
echo " bash install.sh --uninstall # Remove scheduled backup"
36-
exit 0
36+
exit "${1:-0}"
3737
}
3838

3939
while [[ $# -gt 0 ]]; do
4040
case "$1" in
41-
--hour) HOUR="$2"; shift 2 ;;
41+
--hour)
42+
if [[ $# -lt 2 ]]; then
43+
echo -e "${RED}ERR${NC} Missing value for --hour"
44+
usage 1
45+
fi
46+
HOUR="$2"
47+
shift 2
48+
;;
4249
--uninstall) UNINSTALL=true; shift ;;
4350
--help) usage ;;
4451
*) echo -e "${RED}ERR${NC} Unknown option: $1"; exit 1 ;;
@@ -84,6 +91,11 @@ if [[ ! -f "$BACKUP_SCRIPT" ]]; then
8491
exit 1
8592
fi
8693

94+
if ! [[ "$HOUR" =~ ^[0-9]+$ ]]; then
95+
echo -e "${RED}ERR${NC} Hour must be a whole number between 0 and 23"
96+
exit 1
97+
fi
98+
8799
if [[ "$HOUR" -lt 0 || "$HOUR" -gt 23 ]]; then
88100
echo -e "${RED}ERR${NC} Hour must be between 0 and 23"
89101
exit 1

restore.sh

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,21 @@ usage() {
3030
echo " bash restore.sh --latest"
3131
echo " bash restore.sh 20260222-020000"
3232
echo " bash restore.sh backup-20260222-020000.tar.gz"
33-
exit 0
33+
exit "${1:-0}"
3434
}
3535

3636
while [[ $# -gt 0 ]]; do
3737
case "$1" in
3838
--list) LIST_MODE=true; shift ;;
3939
--latest) LATEST_MODE=true; shift ;;
40-
--path) MEDIA_DIR="$2"; shift 2 ;;
40+
--path)
41+
if [[ $# -lt 2 ]]; then
42+
echo -e "${RED}ERR${NC} Missing value for --path"
43+
usage 1
44+
fi
45+
MEDIA_DIR="$2"
46+
shift 2
47+
;;
4148
--help) usage ;;
4249
-*) echo -e "${RED}ERR${NC} Unknown option: $1"; exit 1 ;;
4350
*) BACKUP_TARGET="$1"; shift ;;
@@ -63,15 +70,15 @@ if $LIST_MODE; then
6370
echo ""
6471

6572
FOUND=0
66-
for f in $(ls -t "$BACKUP_DIR"/backup-*.tar.gz 2>/dev/null); do
73+
while IFS= read -r f; do
6774
name=$(basename "$f")
6875
size=$(du -sh "$f" | cut -f1)
6976
date_part=$(echo "$name" | sed 's/backup-\([0-9]*\)-\([0-9]*\).*/\1/')
7077
time_part=$(echo "$name" | sed 's/backup-[0-9]*-\([0-9]*\).*/\1/')
7178
formatted_date="${date_part:0:4}-${date_part:4:2}-${date_part:6:2} ${time_part:0:2}:${time_part:2:2}:${time_part:4:2}"
7279
echo -e " ${GREEN}$name${NC} ($size) $formatted_date"
7380
FOUND=$((FOUND + 1))
74-
done
81+
done < <(ls -1t "$BACKUP_DIR"/backup-*.tar.gz 2>/dev/null)
7582

7683
if [[ $FOUND -eq 0 ]]; then
7784
echo -e " ${YELLOW}No backups found${NC}"
@@ -142,8 +149,12 @@ trap cleanup EXIT
142149
# ==============================
143150
echo -e "${CYAN}--- Extracting ---${NC}"
144151
tar -xzf "$BACKUP_FILE" -C "$TEMP_DIR"
145-
BACKUP_ROOT=$(ls "$TEMP_DIR")
146-
EXTRACTED="$TEMP_DIR/$BACKUP_ROOT"
152+
BACKUP_ROOT=$(find "$TEMP_DIR" -mindepth 1 -maxdepth 1 -type d | head -1)
153+
if [[ -z "$BACKUP_ROOT" || ! -d "$BACKUP_ROOT" ]]; then
154+
echo -e "${RED}ERR${NC} Backup archive did not contain a valid root directory"
155+
exit 1
156+
fi
157+
EXTRACTED="$BACKUP_ROOT"
147158
echo -e "${GREEN}OK${NC} Extracted to temp directory"
148159
echo ""
149160

@@ -152,9 +163,19 @@ echo ""
152163
# ==============================
153164
echo -e "${CYAN}--- Stopping Containers ---${NC}"
154165

155-
if command -v docker &>/dev/null && [[ -f "$MEDIA_DIR/docker-compose.yml" || -f "$MEDIA_DIR/docker-compose.yaml" ]]; then
156-
(cd "$MEDIA_DIR" && docker compose stop 2>/dev/null) || true
157-
echo -e "${GREEN}OK${NC} Containers stopped"
166+
STOP_COMPOSE_FILE=""
167+
if [[ -f "$MEDIA_DIR/docker-compose.yml" ]]; then
168+
STOP_COMPOSE_FILE="$MEDIA_DIR/docker-compose.yml"
169+
elif [[ -f "$MEDIA_DIR/docker-compose.yaml" ]]; then
170+
STOP_COMPOSE_FILE="$MEDIA_DIR/docker-compose.yaml"
171+
fi
172+
173+
if command -v docker &>/dev/null && [[ -n "$STOP_COMPOSE_FILE" ]]; then
174+
if (cd "$MEDIA_DIR" && docker compose stop >/dev/null 2>&1); then
175+
echo -e "${GREEN}OK${NC} Containers stopped"
176+
else
177+
echo -e "${YELLOW}WRN${NC} Failed to stop containers cleanly, continuing restore"
178+
fi
158179
else
159180
echo -e "${YELLOW}WRN${NC} No compose file or Docker not found, skipping container stop"
160181
fi
@@ -239,15 +260,27 @@ echo ""
239260
# ==============================
240261
echo -e "${CYAN}--- Restarting Containers ---${NC}"
241262

242-
if command -v docker &>/dev/null && [[ -f "$MEDIA_DIR/docker-compose.yml" || -f "$MEDIA_DIR/docker-compose.yaml" ]]; then
243-
(cd "$MEDIA_DIR" && docker compose up -d 2>/dev/null) || true
244-
echo -e "${GREEN}OK${NC} Containers started"
245-
echo ""
263+
START_COMPOSE_FILE=""
264+
if [[ -f "$MEDIA_DIR/docker-compose.yml" ]]; then
265+
START_COMPOSE_FILE="$MEDIA_DIR/docker-compose.yml"
266+
elif [[ -f "$MEDIA_DIR/docker-compose.yaml" ]]; then
267+
START_COMPOSE_FILE="$MEDIA_DIR/docker-compose.yaml"
268+
fi
269+
270+
if command -v docker &>/dev/null && [[ -n "$START_COMPOSE_FILE" ]]; then
271+
if (cd "$MEDIA_DIR" && docker compose up -d >/dev/null 2>&1); then
272+
echo -e "${GREEN}OK${NC} Containers started"
273+
echo ""
246274

247-
# Health check
248-
echo -e "${CYAN}--- Health Check ---${NC}"
249-
sleep 3
250-
(cd "$MEDIA_DIR" && docker compose ps 2>/dev/null) || true
275+
# Health check
276+
echo -e "${CYAN}--- Health Check ---${NC}"
277+
sleep 3
278+
(cd "$MEDIA_DIR" && docker compose ps 2>/dev/null) || true
279+
else
280+
echo -e "${RED}ERR${NC} Restore completed, but container restart failed."
281+
echo -e "${RED}ERR${NC} Check compose syntax/secrets and run: (cd \"$MEDIA_DIR\" && docker compose up -d)"
282+
exit 1
283+
fi
251284
else
252285
echo -e "${YELLOW}WRN${NC} No compose file or Docker not found, skipping restart"
253286
fi

0 commit comments

Comments
 (0)