#!/usr/bin/env bash # Daily backup: SQLite DB + static/images → /root/backups/// # Prune backup folders older than RETENTION_DAYS (default 30). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$PROJECT_DIR" BACKUP_ROOT="${BACKUP_ROOT:-/root/backups}" RETENTION_DAYS="${RETENTION_DAYS:-30}" INSTANCE_NAME="${BACKUP_INSTANCE:-$(basename "$PROJECT_DIR")}" TZ_NAME="${BACKUP_TZ:-Asia/Shanghai}" log() { printf '[%s] %s\n' "$(TZ="$TZ_NAME" date '+%Y-%m-%d %H:%M:%S %Z')" "$*" } read_env_var() { local key="$1" local default="$2" local line if [[ ! -f .env ]]; then printf '%s' "$default" return fi line="$(grep -E "^${key}=" .env 2>/dev/null | tail -1 || true)" if [[ -z "$line" ]]; then printf '%s' "$default" return fi printf '%s' "${line#*=}" | tr -d '\r' } resolve_project_path() { local p="$1" if [[ "$p" == /* ]]; then printf '%s' "$p" else printf '%s' "$PROJECT_DIR/$p" fi } prune_old_backups() { local base="$BACKUP_ROOT/$INSTANCE_NAME" [[ -d "$base" ]] || return 0 local cutoff cutoff="$(TZ="$TZ_NAME" date -d "-${RETENTION_DAYS} days" +%Y-%m-%d 2>/dev/null || true)" if [[ -z "$cutoff" ]]; then find "$base" -mindepth 1 -maxdepth 1 -type d -mtime +"$RETENTION_DAYS" -print0 | xargs -r -0 rm -rf return 0 fi local dir name for dir in "$base"/*/; do [[ -d "$dir" ]] || continue name="$(basename "$dir")" [[ "$name" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || continue if [[ "$name" < "$cutoff" ]]; then log "prune: remove $dir (older than ${RETENTION_DAYS} days)" rm -rf "$dir" fi done } DB_REL="$(read_env_var DB_PATH crypto.db)" UPLOAD_REL="$(read_env_var UPLOAD_DIR static/images)" BACKUP_ROOT="$(read_env_var BACKUP_ROOT "$BACKUP_ROOT")" RETENTION_DAYS="$(read_env_var BACKUP_RETENTION_DAYS "$RETENTION_DAYS")" INSTANCE_NAME="$(read_env_var BACKUP_INSTANCE "$INSTANCE_NAME")" DB_PATH="$(resolve_project_path "$DB_REL")" UPLOAD_DIR="$(resolve_project_path "$UPLOAD_REL")" DATE_TAG="$(TZ="$TZ_NAME" date +%Y-%m-%d)" DEST="$BACKUP_ROOT/$INSTANCE_NAME/$DATE_TAG" if [[ ! -f "$DB_PATH" ]]; then log "error: database not found: $DB_PATH" exit 1 fi mkdir -p "$DEST" log "start backup instance=$INSTANCE_NAME dest=$DEST" if command -v sqlite3 >/dev/null 2>&1; then sqlite3 "$DB_PATH" ".backup '$DEST/crypto.db'" log "db: sqlite3 backup -> $DEST/crypto.db" else cp -a "$DB_PATH" "$DEST/crypto.db" log "db: cp -> $DEST/crypto.db (sqlite3 not installed)" fi if [[ -d "$UPLOAD_DIR" ]]; then tar -czf "$DEST/static_images.tar.gz" -C "$(dirname "$UPLOAD_DIR")" "$(basename "$UPLOAD_DIR")" log "images: $UPLOAD_DIR -> $DEST/static_images.tar.gz" else log "warn: upload dir missing, skip images: $UPLOAD_DIR" fi { echo "instance=$INSTANCE_NAME" echo "project_dir=$PROJECT_DIR" echo "backup_date=$DATE_TAG" echo "db_path=$DB_PATH" echo "upload_dir=$UPLOAD_DIR" } >"$DEST/manifest.txt" prune_old_backups log "done"