Skip to content

Commit c603d76

Browse files
committed
fix: improve backup robustness and custom stack path support
1 parent 404768e commit c603d76

File tree

5 files changed

+158
-34
lines changed

5 files changed

+158
-34
lines changed

.github/workflows/validate.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,13 @@ jobs:
1616
bash -n backup.sh
1717
bash -n restore.sh
1818
bash -n install.sh
19+
20+
- name: Smoke test backup/restore scripts
21+
run: |
22+
TMP_MEDIA="$(mktemp -d)"
23+
mkdir -p "$TMP_MEDIA/config/sonarr" "$TMP_MEDIA/logs"
24+
printf '<Config>ok</Config>\n' > "$TMP_MEDIA/config/sonarr/config.xml"
25+
printf 'db\n' > "$TMP_MEDIA/config/sonarr/sonarr.db"
26+
printf 'WIREGUARD_PRIVATE_KEY=secret\n' > "$TMP_MEDIA/.env"
27+
bash backup.sh --path "$TMP_MEDIA" --stack-dir "$TMP_MEDIA" --keep 1
28+
bash restore.sh --list --path "$TMP_MEDIA"

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ This tool backs up configs, databases, and compose files every night. When somet
3434
| **Environment** | .env (sensitive values redacted) |
3535
| **Container state** | Snapshot of running containers at backup time |
3636

37-
Backups are compressed tarballs stored in `~/Media/backups/`. Old backups are pruned automatically (default: keep 14 days).
37+
Backups are compressed tarballs stored in `~/Media/backups/` (or your custom `--path`). Old backups are pruned automatically (default: keep 14 days).
3838

3939
## See It In Action
4040

@@ -70,6 +70,7 @@ Customize the schedule:
7070

7171
```bash
7272
bash install.sh --hour 3 # run at 3am instead
73+
bash install.sh --path /Volumes/External/Media --keep 30
7374
```
7475

7576
Remove the scheduled backup:
@@ -110,6 +111,15 @@ All scripts default to `~/Media`. Use `--path` for a different location:
110111
```bash
111112
bash backup.sh --path /Volumes/External/Media
112113
bash restore.sh --latest --path /Volumes/External/Media
114+
bash install.sh --path /Volumes/External/Media --keep 30
115+
```
116+
117+
If your stack repo is not in the same directory as your media library, also set `--stack-dir`:
118+
119+
```bash
120+
bash backup.sh --path /Volumes/External/Media --stack-dir ~/mac-media-stack
121+
bash restore.sh --latest --path /Volumes/External/Media --stack-dir ~/mac-media-stack
122+
bash install.sh --path /Volumes/External/Media --stack-dir ~/mac-media-stack
113123
```
114124

115125
## Works With

backup.sh

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ CYAN='\033[0;36m'
1010
NC='\033[0m'
1111

1212
MEDIA_DIR="$HOME/Media"
13+
STACK_DIR="$MEDIA_DIR"
14+
STACK_DIR_SET=false
1315
KEEP_DAYS=14
1416

1517
usage() {
@@ -18,8 +20,9 @@ usage() {
1820
echo "Back up your *arr Docker stack configs, databases, and compose files."
1921
echo ""
2022
echo "Options:"
21-
echo " --path DIR Media directory (default: ~/Media)"
22-
echo " --keep DAYS Days of backups to keep (default: 14)"
23+
echo " --path DIR Media directory (default: ~/Media)"
24+
echo " --stack-dir DIR Stack directory with docker-compose/.env (default: --path)"
25+
echo " --keep DAYS Days of backups to keep (default: 14)"
2326
echo " --help Show this help"
2427
exit "${1:-0}"
2528
}
@@ -34,6 +37,15 @@ while [[ $# -gt 0 ]]; do
3437
MEDIA_DIR="$2"
3538
shift 2
3639
;;
40+
--stack-dir)
41+
if [[ $# -lt 2 ]]; then
42+
echo -e "${RED}ERR${NC} Missing value for --stack-dir"
43+
usage 1
44+
fi
45+
STACK_DIR="$2"
46+
STACK_DIR_SET=true
47+
shift 2
48+
;;
3749
--keep)
3850
if [[ $# -lt 2 ]]; then
3951
echo -e "${RED}ERR${NC} Missing value for --keep"
@@ -48,6 +60,10 @@ while [[ $# -gt 0 ]]; do
4860
done
4961

5062
MEDIA_DIR="${MEDIA_DIR/#\~/$HOME}"
63+
if [[ "$STACK_DIR_SET" != true ]]; then
64+
STACK_DIR="$MEDIA_DIR"
65+
fi
66+
STACK_DIR="${STACK_DIR/#\~/$HOME}"
5167
BACKUP_DIR="$MEDIA_DIR/backups"
5268
LOG_DIR="$MEDIA_DIR/logs"
5369
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
@@ -66,6 +82,7 @@ echo -e "${CYAN} Mac Media Stack Backup${NC}"
6682
echo -e "${CYAN}==============================${NC}"
6783
echo ""
6884
echo -e "${CYAN}INF${NC} Media directory: $MEDIA_DIR"
85+
echo -e "${CYAN}INF${NC} Stack directory: $STACK_DIR"
6986
echo -e "${CYAN}INF${NC} Timestamp: $TIMESTAMP"
7087
echo ""
7188

@@ -75,6 +92,11 @@ if [[ ! -d "$MEDIA_DIR" ]]; then
7592
exit 1
7693
fi
7794

95+
if [[ ! -d "$STACK_DIR" ]]; then
96+
echo -e "${RED}ERR${NC} Stack directory not found: $STACK_DIR"
97+
exit 1
98+
fi
99+
78100
if ! [[ "$KEEP_DAYS" =~ ^[0-9]+$ ]]; then
79101
echo -e "${RED}ERR${NC} --keep must be a whole number"
80102
exit 1
@@ -145,14 +167,14 @@ echo ""
145167
# ==============================
146168
echo -e "${CYAN}--- Compose File ---${NC}"
147169

148-
if [[ -f "$MEDIA_DIR/docker-compose.yml" ]]; then
149-
cp "$MEDIA_DIR/docker-compose.yml" "$BACKUP_STAGING/"
170+
if [[ -f "$STACK_DIR/docker-compose.yml" ]]; then
171+
cp "$STACK_DIR/docker-compose.yml" "$BACKUP_STAGING/"
150172
echo -e "${GREEN}OK${NC} Copied docker-compose.yml"
151-
elif [[ -f "$MEDIA_DIR/docker-compose.yaml" ]]; then
152-
cp "$MEDIA_DIR/docker-compose.yaml" "$BACKUP_STAGING/"
173+
elif [[ -f "$STACK_DIR/docker-compose.yaml" ]]; then
174+
cp "$STACK_DIR/docker-compose.yaml" "$BACKUP_STAGING/"
153175
echo -e "${GREEN}OK${NC} Copied docker-compose.yaml"
154176
else
155-
echo -e "${YELLOW}WRN${NC} No docker-compose file found in $MEDIA_DIR"
177+
echo -e "${YELLOW}WRN${NC} No docker-compose file found in $STACK_DIR"
156178
fi
157179
echo ""
158180

@@ -161,12 +183,12 @@ echo ""
161183
# ==============================
162184
echo -e "${CYAN}--- Environment File ---${NC}"
163185

164-
if [[ -f "$MEDIA_DIR/.env" ]]; then
165-
grep -ivE '(password|key|secret|token)' "$MEDIA_DIR/.env" > "$BACKUP_STAGING/.env.redacted" 2>/dev/null || true
166-
REDACTED=$(grep -ciE '(password|key|secret|token)' "$MEDIA_DIR/.env" 2>/dev/null || echo "0")
186+
if [[ -f "$STACK_DIR/.env" ]]; then
187+
grep -ivE '(password|key|secret|token)' "$STACK_DIR/.env" > "$BACKUP_STAGING/.env.redacted" 2>/dev/null || true
188+
REDACTED=$(grep -ciE '(password|key|secret|token)' "$STACK_DIR/.env" 2>/dev/null || echo "0")
167189
echo -e "${GREEN}OK${NC} Copied .env ($REDACTED sensitive line(s) redacted)"
168190
else
169-
echo -e "${YELLOW}WRN${NC} No .env file found"
191+
echo -e "${YELLOW}WRN${NC} No .env file found in $STACK_DIR"
170192
fi
171193
echo ""
172194

@@ -176,11 +198,21 @@ echo ""
176198
echo -e "${CYAN}--- Container State ---${NC}"
177199

178200
if command -v docker &>/dev/null; then
179-
if docker compose ls &>/dev/null 2>&1; then
180-
(cd "$MEDIA_DIR" && docker compose ps 2>/dev/null) > "$BACKUP_STAGING/container-state.txt" || true
181-
echo -e "${GREEN}OK${NC} Captured container state"
201+
COMPOSE_STATE_FILE=""
202+
if [[ -f "$STACK_DIR/docker-compose.yml" ]]; then
203+
COMPOSE_STATE_FILE="$STACK_DIR/docker-compose.yml"
204+
elif [[ -f "$STACK_DIR/docker-compose.yaml" ]]; then
205+
COMPOSE_STATE_FILE="$STACK_DIR/docker-compose.yaml"
206+
fi
207+
208+
if [[ -n "$COMPOSE_STATE_FILE" ]] && docker compose ls &>/dev/null 2>&1; then
209+
if (cd "$STACK_DIR" && docker compose ps 2>/dev/null) > "$BACKUP_STAGING/container-state.txt"; then
210+
echo -e "${GREEN}OK${NC} Captured container state"
211+
else
212+
echo -e "${YELLOW}WRN${NC} Could not capture container state from $STACK_DIR"
213+
fi
182214
else
183-
echo -e "${YELLOW}WRN${NC} Docker Compose not available, skipping container state"
215+
echo -e "${YELLOW}WRN${NC} Compose file not found or Docker Compose unavailable; skipping container state"
184216
fi
185217
else
186218
echo -e "${YELLOW}WRN${NC} Docker not found, skipping container state"

install.sh

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ CYAN='\033[0;36m'
1010
NC='\033[0m'
1111

1212
HOUR=2
13+
KEEP_DAYS=14
1314
UNINSTALL=false
1415
LABEL="com.mac-media-stack.backup"
1516
PLIST_DIR="$HOME/Library/LaunchAgents"
1617
PLIST_PATH="$PLIST_DIR/$LABEL.plist"
1718
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
1819
BACKUP_SCRIPT="$SCRIPT_DIR/backup.sh"
1920
MEDIA_DIR="$HOME/Media"
21+
STACK_DIR="$MEDIA_DIR"
22+
STACK_DIR_SET=false
2023
LOG_DIR="$MEDIA_DIR/logs"
2124

2225
usage() {
@@ -25,9 +28,12 @@ usage() {
2528
echo "Install or remove scheduled nightly backups via launchd."
2629
echo ""
2730
echo "Options:"
28-
echo " --hour HOUR Hour to run backup (0-23, default: 2)"
29-
echo " --uninstall Remove the scheduled backup"
30-
echo " --help Show this help"
31+
echo " --hour HOUR Hour to run backup (0-23, default: 2)"
32+
echo " --path DIR Media directory passed to backup.sh (default: ~/Media)"
33+
echo " --stack-dir DIR Stack directory passed to backup.sh (default: --path)"
34+
echo " --keep DAYS Backup retention passed to backup.sh (default: 14)"
35+
echo " --uninstall Remove the scheduled backup"
36+
echo " --help Show this help"
3137
echo ""
3238
echo "Examples:"
3339
echo " bash install.sh # Install nightly backup at 2am"
@@ -46,12 +52,44 @@ while [[ $# -gt 0 ]]; do
4652
HOUR="$2"
4753
shift 2
4854
;;
55+
--path)
56+
if [[ $# -lt 2 ]]; then
57+
echo -e "${RED}ERR${NC} Missing value for --path"
58+
usage 1
59+
fi
60+
MEDIA_DIR="$2"
61+
shift 2
62+
;;
63+
--stack-dir)
64+
if [[ $# -lt 2 ]]; then
65+
echo -e "${RED}ERR${NC} Missing value for --stack-dir"
66+
usage 1
67+
fi
68+
STACK_DIR="$2"
69+
STACK_DIR_SET=true
70+
shift 2
71+
;;
72+
--keep)
73+
if [[ $# -lt 2 ]]; then
74+
echo -e "${RED}ERR${NC} Missing value for --keep"
75+
usage 1
76+
fi
77+
KEEP_DAYS="$2"
78+
shift 2
79+
;;
4980
--uninstall) UNINSTALL=true; shift ;;
5081
--help) usage ;;
5182
*) echo -e "${RED}ERR${NC} Unknown option: $1"; exit 1 ;;
5283
esac
5384
done
5485

86+
MEDIA_DIR="${MEDIA_DIR/#\~/$HOME}"
87+
if [[ "$STACK_DIR_SET" != true ]]; then
88+
STACK_DIR="$MEDIA_DIR"
89+
fi
90+
STACK_DIR="${STACK_DIR/#\~/$HOME}"
91+
LOG_DIR="$MEDIA_DIR/logs"
92+
5593
echo ""
5694
echo -e "${CYAN}==============================${NC}"
5795
echo -e "${CYAN} Mac Media Stack Backup${NC}"
@@ -101,6 +139,11 @@ if [[ "$HOUR" -lt 0 || "$HOUR" -gt 23 ]]; then
101139
exit 1
102140
fi
103141

142+
if ! [[ "$KEEP_DAYS" =~ ^[0-9]+$ ]]; then
143+
echo -e "${RED}ERR${NC} --keep must be a whole number"
144+
exit 1
145+
fi
146+
104147
# ==============================
105148
# Create directories
106149
# ==============================
@@ -111,6 +154,9 @@ mkdir -p "$PLIST_DIR" "$LOG_DIR"
111154
# ==============================
112155
echo -e "${CYAN}INF${NC} Backup script: $BACKUP_SCRIPT"
113156
echo -e "${CYAN}INF${NC} Schedule: daily at ${HOUR}:00"
157+
echo -e "${CYAN}INF${NC} Media directory: $MEDIA_DIR"
158+
echo -e "${CYAN}INF${NC} Stack directory: $STACK_DIR"
159+
echo -e "${CYAN}INF${NC} Retention: $KEEP_DAYS day(s)"
114160
echo ""
115161

116162
cat > "$PLIST_PATH" <<EOF
@@ -124,6 +170,12 @@ cat > "$PLIST_PATH" <<EOF
124170
<array>
125171
<string>/bin/bash</string>
126172
<string>$BACKUP_SCRIPT</string>
173+
<string>--path</string>
174+
<string>$MEDIA_DIR</string>
175+
<string>--stack-dir</string>
176+
<string>$STACK_DIR</string>
177+
<string>--keep</string>
178+
<string>$KEEP_DAYS</string>
127179
</array>
128180
<key>StartCalendarInterval</key>
129181
<dict>

0 commit comments

Comments
 (0)