05/19 Enhance codes 4

This commit is contained in:
2026-05-19 10:35:34 -04:00
parent 3cdcbbe8f7
commit abea19d9dd
4 changed files with 460 additions and 53 deletions
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# /home/spuser/PassKeeper/scripts/restore_db.sh
#
# Restore a PassKeeper MySQL backup created by backup_db.sh.
#
# SAFETY FEATURES:
# - Requires explicit confirmation before overwriting the live database.
# - Creates a pre-restore safety dump of the current DB before touching it.
# - Verifies the backup file is a valid gzip before proceeding.
# - Stops the PassKeeper service before restore, restarts it after.
# - Runs flask db upgrade after restore to ensure the schema is current.
#
# Usage:
# bash scripts/restore_db.sh /path/to/passkeeper_20260101_020000.sql.gz
#
# Dry-run (verify only, no changes):
# DRY_RUN=1 bash scripts/restore_db.sh /path/to/backup.sql.gz
set -euo pipefail
# ── Configuration ──────────────────────────────────────────────────────────────
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
PROJECT_DIR="$(realpath "$SCRIPT_DIR/..")"
ENV_FILE="$PROJECT_DIR/.env"
SERVICE_NAME="${PASSKEEPER_SERVICE:-passkeeper}"
BACKUP_DIR="${BACKUP_DIR:-/home/spuser/backups/passkeeper}"
LOG_FILE="${BACKUP_DIR}/restore.log"
DRY_RUN="${DRY_RUN:-0}"
# ── Helpers ────────────────────────────────────────────────────────────────────
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
die() { log "ERROR $*"; exit 1; }
info() { log "INFO $*"; }
# ── Argument check ─────────────────────────────────────────────────────────────
[[ $# -lt 1 ]] && die "Usage: $0 <backup_file.sql.gz>"
BACKUP_FILE="$(realpath "$1")"
[[ -f "$BACKUP_FILE" ]] || die "Backup file not found: $BACKUP_FILE"
# ── Load .env ─────────────────────────────────────────────────────────────────
[[ -f "$ENV_FILE" ]] || die ".env file not found at $ENV_FILE"
set -o allexport
# shellcheck disable=SC1090
source "$ENV_FILE"
set +o allexport
DB_HOST="${MYSQL_HOST:-127.0.0.1}"
DB_PORT="${MYSQL_PORT:-3306}"
DB_USER="${MYSQL_USER:-passkeeper}"
DB_PASS="${MYSQL_PASSWORD}"
DB_NAME="${MYSQL_DB:-passkeeper}"
mkdir -p "$BACKUP_DIR"
info "=== PassKeeper database restore ==="
info "Backup file : $BACKUP_FILE"
info "Target DB : $DB_NAME @ $DB_HOST:$DB_PORT"
info "Service : $SERVICE_NAME"
[[ "$DRY_RUN" == "1" ]] && info "MODE : DRY RUN — no changes will be made"
# ── Validate backup file ───────────────────────────────────────────────────────
info "Validating backup file integrity…"
if ! gzip -t "$BACKUP_FILE" 2>/dev/null; then
die "Backup file failed gzip integrity check: $BACKUP_FILE"
fi
BACKUP_SIZE="$(du -sh "$BACKUP_FILE" | cut -f1)"
info "Backup size : $BACKUP_SIZE — gzip OK"
# ── Confirmation ───────────────────────────────────────────────────────────────
if [[ "$DRY_RUN" != "1" ]]; then
echo ""
echo " ⚠️ WARNING: This will OVERWRITE the live database '$DB_NAME'."
echo " All current data will be replaced with the contents of:"
echo " $BACKUP_FILE"
echo ""
read -r -p " Type 'yes' to continue: " CONFIRM
[[ "$CONFIRM" == "yes" ]] || { info "Restore cancelled by user."; exit 0; }
fi
[[ "$DRY_RUN" == "1" ]] && { info "Dry run complete — backup file is valid."; exit 0; }
# ── Pre-restore safety dump ────────────────────────────────────────────────────
SAFETY_TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
SAFETY_FILE="${BACKUP_DIR}/pre_restore_safety_${SAFETY_TIMESTAMP}.sql.gz"
info "Creating pre-restore safety dump → $SAFETY_FILE"
MYSQL_PWD="$DB_PASS" mysqldump \
--host="$DB_HOST" \
--port="$DB_PORT" \
--user="$DB_USER" \
--single-transaction \
--no-tablespaces \
"$DB_NAME" \
| gzip -9 > "$SAFETY_FILE" \
|| die "Safety dump failed — aborting restore. Database is unchanged."
info "Safety dump complete: $(du -sh "$SAFETY_FILE" | cut -f1)"
# ── Stop service ───────────────────────────────────────────────────────────────
info "Stopping $SERVICE_NAME"
sudo systemctl stop "$SERVICE_NAME" || die "Failed to stop $SERVICE_NAME"
info "$SERVICE_NAME stopped"
RESTORE_OK=0
# ── Restore ────────────────────────────────────────────────────────────────────
info "Restoring database from backup…"
if MYSQL_PWD="$DB_PASS" gunzip -c "$BACKUP_FILE" | mysql \
--host="$DB_HOST" \
--port="$DB_PORT" \
--user="$DB_USER" \
"$DB_NAME"; then
info "Database restore complete"
RESTORE_OK=1
else
log "ERROR Database restore failed — attempting to roll back from safety dump"
if MYSQL_PWD="$DB_PASS" gunzip -c "$SAFETY_FILE" | mysql \
--host="$DB_HOST" \
--port="$DB_PORT" \
--user="$DB_USER" \
"$DB_NAME"; then
log "WARN Rolled back to pre-restore state from $SAFETY_FILE"
else
log "ERROR Rollback also failed — database may be in an inconsistent state"
log "ERROR Manual recovery required. Safety dump: $SAFETY_FILE"
fi
fi
# ── Run migrations ─────────────────────────────────────────────────────────────
if [[ "$RESTORE_OK" == "1" ]]; then
info "Running flask db upgrade to ensure schema is current…"
cd "$PROJECT_DIR"
if source .venv/bin/activate 2>/dev/null; then
flask db upgrade && info "flask db upgrade OK" || \
log "WARN flask db upgrade failed — check migrations manually"
deactivate 2>/dev/null || true
else
log "WARN Could not activate .venv — skipping flask db upgrade"
log "WARN Run 'flask db upgrade' manually before restarting the service"
fi
fi
# ── Restart service ────────────────────────────────────────────────────────────
info "Starting $SERVICE_NAME"
sudo systemctl start "$SERVICE_NAME" && info "$SERVICE_NAME started" || \
die "$SERVICE_NAME failed to start — check 'journalctl -u $SERVICE_NAME -n 50'"
# ── Summary ────────────────────────────────────────────────────────────────────
echo ""
if [[ "$RESTORE_OK" == "1" ]]; then
info "=== Restore finished successfully ==="
info "Restored from : $BACKUP_FILE"
info "Safety dump : $SAFETY_FILE (kept for 24 h — delete manually when satisfied)"
else
die "=== Restore FAILED — see log for details: $LOG_FILE ==="
fi