-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackup.sh
More file actions
201 lines (149 loc) · 7.45 KB
/
Copy pathbackup.sh
File metadata and controls
201 lines (149 loc) · 7.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#!/usr/bin/env bash
# =============================================================================
# TeslaMate PostgreSQL Backup → Filen Cloud
# =============================================================================
# This script:
# 1. Dumps the TeslaMate PostgreSQL database (inside Docker)
# 2. Compresses with gzip (timestamped filename)
# 3. Uploads to Filen cloud using the Filen CLI
# 4. Rotates local backups (keeps last N days)
# 5. Logs everything and sends optional notifications
#
# Prerequisites:
# - Docker + docker compose running TeslaMate
# - Filen CLI installed (curl -sL https://filen.io/cli.sh | bash)
# - FILEN_EMAIL and FILEN_PASSWORD set in .env
# =============================================================================
set -euo pipefail
# ─── Configuration ───────────────────────────────────────────────────────────
# Docker compose project directory (where your docker-compose.yml lives)
COMPOSE_DIR="${TESLAMATE_COMPOSE_DIR:-$HOME/teslamate}"
# Docker compose service name for the database
DB_SERVICE="${TESLAMATE_DB_SERVICE:-database}"
# PostgreSQL credentials (should match your docker-compose.yml)
DB_USER="${TESLAMATE_DB_USER:-teslamate}"
DB_NAME="${TESLAMATE_DB_NAME:-teslamate}"
# Local backup directory
LOCAL_BACKUP_DIR="${TESLAMATE_BACKUP_DIR:-$HOME/teslamate-backups}"
# Filen remote path
FILEN_REMOTE_PATH="${FILEN_REMOTE_PATH:-/teslamate-backups}"
# How many days of local backups to keep
LOCAL_RETENTION_DAYS="${LOCAL_RETENTION_DAYS:-7}"
# How many days of Filen backups to keep (0 = keep all)
FILEN_RETENTION_DAYS="${FILEN_RETENTION_DAYS:-30}"
# Filen CLI binary location
FILEN_CLI="${FILEN_CLI:-filen}"
# Filen credentials (set these in .env, never commit them)
FILEN_EMAIL="${FILEN_EMAIL:-}"
FILEN_PASSWORD="${FILEN_PASSWORD:-}"
# Log file
LOG_FILE="${TESLAMATE_BACKUP_LOG:-$HOME/teslamate-backups/backup.log}"
# Optional: webhook URL for notifications (Telegram, Pushover, ntfy, etc.)
NOTIFY_URL="${NOTIFY_URL:-}"
# ─── Helper: build Filen CLI command with auth ──────────────────────────────
filen_cmd() {
"$FILEN_CLI" --skip-update --quiet --email "$FILEN_EMAIL" --password "$FILEN_PASSWORD" "$@"
}
# ─── Functions ───────────────────────────────────────────────────────────────
log() {
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $*" | tee -a "$LOG_FILE"
}
notify() {
if [[ -n "$NOTIFY_URL" ]]; then
curl -sf -d "$1" "$NOTIFY_URL" >/dev/null 2>&1 || true
fi
}
cleanup_on_error() {
log "ERROR: Backup failed! See log for details."
notify "❌ TeslaMate backup FAILED at $(date '+%H:%M'). Check logs on VPS."
exit 1
}
trap cleanup_on_error ERR
# ─── Pre-flight checks ──────────────────────────────────────────────────────
# Ensure local backup dir exists
mkdir -p "$LOCAL_BACKUP_DIR"
# Ensure log file exists
touch "$LOG_FILE"
log "═══ TeslaMate Backup Starting ═══"
# Check Filen credentials are set
if [[ -z "$FILEN_EMAIL" || -z "$FILEN_PASSWORD" ]]; then
log "ERROR: FILEN_EMAIL and FILEN_PASSWORD must be set in .env"
exit 1
fi
# Check Docker is running
if ! docker info >/dev/null 2>&1; then
log "ERROR: Docker is not running or not accessible"
exit 1
fi
# Check compose directory exists
if [[ ! -f "$COMPOSE_DIR/docker-compose.yml" ]] && [[ ! -f "$COMPOSE_DIR/compose.yml" ]]; then
log "ERROR: No docker-compose.yml found in $COMPOSE_DIR"
exit 1
fi
# Check Filen CLI is available
if ! command -v "$FILEN_CLI" >/dev/null 2>&1; then
log "ERROR: Filen CLI not found. Install with: curl -sL https://filen.io/cli.sh | bash"
exit 1
fi
# ─── Step 1: Database Dump ───────────────────────────────────────────────────
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
DUMP_FILENAME="teslamate_${TIMESTAMP}.sql.gz"
DUMP_PATH="${LOCAL_BACKUP_DIR}/${DUMP_FILENAME}"
log "Dumping database..."
cd "$COMPOSE_DIR"
docker compose exec -T "$DB_SERVICE" \
pg_dump -U "$DB_USER" -d "$DB_NAME" \
--no-owner --no-acl \
--format=plain \
| gzip -9 > "$DUMP_PATH"
DUMP_SIZE=$(du -h "$DUMP_PATH" | cut -f1)
log "Database dump complete: $DUMP_FILENAME ($DUMP_SIZE)"
# ─── Step 2: Upload to Filen ────────────────────────────────────────────────
log "Uploading to Filen: ${FILEN_REMOTE_PATH}/"
filen_cmd upload "$DUMP_PATH" "${FILEN_REMOTE_PATH}/"
log "Upload complete"
# ─── Step 3: Verify upload ──────────────────────────────────────────────────
log "Verifying upload..."
if filen_cmd ls "${FILEN_REMOTE_PATH}/" 2>&1 | grep -q "$DUMP_FILENAME"; then
log "✓ Upload verified on Filen"
else
log "WARNING: Could not verify upload (file may still be processing)"
fi
# ─── Step 4: Rotate local backups ───────────────────────────────────────────
log "Rotating local backups (keeping last ${LOCAL_RETENTION_DAYS} days)..."
DELETED_LOCAL=0
find "$LOCAL_BACKUP_DIR" -name "teslamate_*.sql.gz" -type f -mtime +"$LOCAL_RETENTION_DAYS" | while read -r old_file; do
rm -f "$old_file"
log " Deleted local: $(basename "$old_file")"
DELETED_LOCAL=$((DELETED_LOCAL + 1))
done
LOCAL_COUNT=$(find "$LOCAL_BACKUP_DIR" -name "teslamate_*.sql.gz" -type f | wc -l)
log "Local backups remaining: $LOCAL_COUNT"
# ─── Step 5: Rotate Filen backups (optional) ────────────────────────────────
if [[ "$FILEN_RETENTION_DAYS" -gt 0 ]]; then
log "Rotating Filen backups (keeping last ${FILEN_RETENTION_DAYS} days)..."
# Calculate the cutoff date
CUTOFF_DATE=$(date -d "-${FILEN_RETENTION_DAYS} days" '+%Y%m%d' 2>/dev/null || \
date -v-${FILEN_RETENTION_DAYS}d '+%Y%m%d' 2>/dev/null || echo "")
if [[ -n "$CUTOFF_DATE" ]]; then
# List remote files and delete old ones
filen_cmd ls "${FILEN_REMOTE_PATH}/" 2>/dev/null | \
grep -oP 'teslamate_\d{8}_\d{6}\.sql\.gz' | while read -r remote_file; do
# Extract date from filename: teslamate_YYYYMMDD_HHMMSS.sql.gz
FILE_DATE=$(echo "$remote_file" | grep -oP '\d{8}' | head -1)
if [[ "$FILE_DATE" < "$CUTOFF_DATE" ]]; then
log " Deleting remote: $remote_file"
filen_cmd rm "${FILEN_REMOTE_PATH}/${remote_file}" 2>/dev/null || true
fi
done
fi
fi
# ─── Done ────────────────────────────────────────────────────────────────────
log "═══ Backup Complete ═══"
log " File: $DUMP_FILENAME"
log " Size: $DUMP_SIZE"
log " Local: $DUMP_PATH"
log " Remote: ${FILEN_REMOTE_PATH}/${DUMP_FILENAME}"
notify "✅ TeslaMate backup OK: ${DUMP_FILENAME} (${DUMP_SIZE})"