#!/bin/sh set -eu backup_kind="${1:-daily}" case "$backup_kind" in daily|weekly|monthly) ;; *) echo "用法: BACKUP_ENCRYPTION_PASSWORD=... sh scripts/backup.sh [daily|weekly|monthly]" >&2; exit 2 ;; esac backup_password="${BACKUP_ENCRYPTION_PASSWORD:-}" if [ "${#backup_password}" -lt 20 ]; then echo "BACKUP_ENCRYPTION_PASSWORD 至少需要 20 个字符" >&2 exit 2 fi timestamp="$(date +%Y%m%d-%H%M%S)-$$" archive_name="huiyu-${backup_kind}-${timestamp}.tar.gz.enc" work_dir="/app/data/backups/work-${timestamp}" backup_dir="${BACKUP_HOST_DIR:-./backups}" case "$backup_dir" in ''|'/'|'.'|'..') echo "BACKUP_HOST_DIR 不能是空目录、根目录或当前目录" >&2; exit 2 ;; esac docker compose exec -T \ -e BACKUP_WORK_DIR="$work_dir" \ -e BACKUP_ARCHIVE_NAME="$archive_name" \ -e BACKUP_ENCRYPTION_PASSWORD="$BACKUP_ENCRYPTION_PASSWORD" \ web sh -eu -c ' mkdir -p "$BACKUP_WORK_DIR" /app/offsite-backups node -e " const Database = require(\"better-sqlite3\"); const db = new Database(\"/app/data/db/huiyu.sqlite\"); db.backup(process.env.BACKUP_WORK_DIR + \"/huiyu.sqlite\") .then(() => db.close()) .catch(error => { console.error(error); db.close(); process.exit(1); }); " cp -R /app/data/uploads "$BACKUP_WORK_DIR/uploads" tar -czf "$BACKUP_WORK_DIR/bundle.tar.gz" -C "$BACKUP_WORK_DIR" huiyu.sqlite uploads openssl enc -aes-256-cbc -salt -pbkdf2 -iter 200000 \ -pass env:BACKUP_ENCRYPTION_PASSWORD \ -in "$BACKUP_WORK_DIR/bundle.tar.gz" \ -out "/app/offsite-backups/$BACKUP_ARCHIVE_NAME" openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \ -pass env:BACKUP_ENCRYPTION_PASSWORD \ -in "/app/offsite-backups/$BACKUP_ARCHIVE_NAME" | tar -tzf - >/dev/null rm -rf "$BACKUP_WORK_DIR" ' find "$backup_dir" -type f -name 'huiyu-daily-*.tar.gz.enc' -mtime +7 -delete find "$backup_dir" -type f -name 'huiyu-weekly-*.tar.gz.enc' -mtime +28 -delete find "$backup_dir" -type f -name 'huiyu-monthly-*.tar.gz.enc' -mtime +186 -delete echo "备份完成并校验: $backup_dir/$archive_name"