Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
Executable
+170
@@ -0,0 +1,170 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Laravel Timebank Database Backup Script
|
||||
# Backs up MySQL database with compression and rotation
|
||||
# Usage: ./backup-database.sh [backup_type]
|
||||
# backup_type: daily (default), weekly, monthly
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
BACKUP_ROOT_DIR="$PROJECT_ROOT/backups"
|
||||
LOG_FILE="$BACKUP_ROOT_DIR/backup.log"
|
||||
|
||||
# Create backup directories
|
||||
mkdir -p "$BACKUP_ROOT_DIR"/{database/{daily,weekly,monthly},logs}
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Load environment loader
|
||||
source "$SCRIPT_DIR/load-env.sh"
|
||||
|
||||
# Load environment variables
|
||||
if ! load_env "$PROJECT_ROOT/.env"; then
|
||||
log "ERROR: .env file not found in $PROJECT_ROOT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate required environment variables
|
||||
if [ -z "$DB_DATABASE" ] || [ -z "$DB_USERNAME" ] || [ -z "$DB_HOST" ]; then
|
||||
log "ERROR: Required database environment variables not found (DB_DATABASE, DB_USERNAME, DB_HOST)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set backup type (daily, weekly, monthly)
|
||||
BACKUP_TYPE="${1:-daily}"
|
||||
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
|
||||
BACKUP_DIR="$BACKUP_ROOT_DIR/database/$BACKUP_TYPE"
|
||||
BACKUP_FILE="$BACKUP_DIR/${DB_DATABASE}_${BACKUP_TYPE}_${TIMESTAMP}.sql"
|
||||
COMPRESSED_FILE="${BACKUP_FILE}.gz"
|
||||
|
||||
# Create backup type directory
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
log "Starting $BACKUP_TYPE database backup for $DB_DATABASE"
|
||||
|
||||
# Perform database backup
|
||||
log "Creating database dump..."
|
||||
|
||||
# Create MySQL configuration file for secure password handling
|
||||
MYSQL_CNF_FILE="/tmp/mysql_backup_$$.cnf"
|
||||
cat > "$MYSQL_CNF_FILE" <<EOF
|
||||
[mysqldump]
|
||||
host=${DB_HOST:-127.0.0.1}
|
||||
port=${DB_PORT:-3306}
|
||||
user=$DB_USERNAME
|
||||
password=$DB_PASSWORD
|
||||
EOF
|
||||
|
||||
# Set secure permissions on the config file
|
||||
chmod 600 "$MYSQL_CNF_FILE"
|
||||
|
||||
# Perform the backup using the config file.
|
||||
#
|
||||
# Failure is handled explicitly rather than left to `set -e`: when mysqldump
|
||||
# exits non-zero the shell would otherwise kill this script on the spot,
|
||||
# leaving a TRUNCATED .sql on disk, skipping the gzip below, and never
|
||||
# reaching the error branch. That is exactly what happened between at least
|
||||
# 2026-09-18 and 2026-09-22 - the dump died at its final step and left a
|
||||
# ~112 MB partial file that looked like a real backup and was rotated like
|
||||
# one. A backup that fails must not leave anything behind that can be
|
||||
# mistaken for a good one.
|
||||
#
|
||||
# --events is deliberately NOT passed. It requires the EVENT privilege, which
|
||||
# the application user does not have and should not be given: the schema
|
||||
# defines no MySQL events (Laravel schedules through cron instead), so the
|
||||
# flag only widened the app's privileges to satisfy the backup. If events are
|
||||
# ever introduced, add --events AND grant EVENT on that database - not the
|
||||
# other way round.
|
||||
set +e
|
||||
mysqldump \
|
||||
--defaults-extra-file="$MYSQL_CNF_FILE" \
|
||||
--single-transaction \
|
||||
--routines \
|
||||
--triggers \
|
||||
--add-drop-database \
|
||||
--databases "$DB_DATABASE" \
|
||||
> "$BACKUP_FILE" 2> "${BACKUP_FILE}.err"
|
||||
mysqldump_status=$?
|
||||
set -e
|
||||
|
||||
# Clean up the temporary config file
|
||||
rm -f "$MYSQL_CNF_FILE"
|
||||
|
||||
if [ $mysqldump_status -ne 0 ]; then
|
||||
log "ERROR: mysqldump failed (exit $mysqldump_status): $(tr '\n' ' ' < "${BACKUP_FILE}.err")"
|
||||
# Remove the partial dump: keeping it would let retention rotate a broken
|
||||
# backup as though it were valid.
|
||||
rm -f "$BACKUP_FILE" "${BACKUP_FILE}.err"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "${BACKUP_FILE}.err"
|
||||
|
||||
# A dump that does not end with mysqldump's completion marker is truncated,
|
||||
# whatever the exit status said.
|
||||
if ! tail -c 200 "$BACKUP_FILE" | grep -q "Dump completed"; then
|
||||
log "ERROR: dump is truncated (no completion marker) - discarding $BACKUP_FILE"
|
||||
rm -f "$BACKUP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Compress the backup
|
||||
log "Compressing backup..."
|
||||
gzip "$BACKUP_FILE"
|
||||
|
||||
# Verify the compressed backup exists and has content
|
||||
if [ -f "$COMPRESSED_FILE" ] && [ -s "$COMPRESSED_FILE" ]; then
|
||||
BACKUP_SIZE=$(du -h "$COMPRESSED_FILE" | cut -f1)
|
||||
log "SUCCESS: Database backup completed - $COMPRESSED_FILE ($BACKUP_SIZE)"
|
||||
else
|
||||
log "ERROR: Backup verification failed - $COMPRESSED_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Retention cleanup based on backup type
|
||||
cleanup_old_backups() {
|
||||
local backup_dir="$1"
|
||||
local keep_count="$2"
|
||||
local pattern="$3"
|
||||
|
||||
log "Cleaning up old $BACKUP_TYPE backups (keeping $keep_count most recent)"
|
||||
|
||||
# Remove old backups, keeping only the specified number
|
||||
ls -t "$backup_dir"/$pattern 2>/dev/null | tail -n +$((keep_count + 1)) | while read -r old_backup; do
|
||||
if [ -f "$backup_dir/$old_backup" ]; then
|
||||
rm "$backup_dir/$old_backup"
|
||||
log "Removed old backup: $old_backup"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Apply retention policy
|
||||
case "$BACKUP_TYPE" in
|
||||
daily)
|
||||
cleanup_old_backups "$BACKUP_DIR" 7 "${DB_DATABASE}_daily_*.sql.gz"
|
||||
;;
|
||||
weekly)
|
||||
cleanup_old_backups "$BACKUP_DIR" 4 "${DB_DATABASE}_weekly_*.sql.gz"
|
||||
;;
|
||||
monthly)
|
||||
cleanup_old_backups "$BACKUP_DIR" 12 "${DB_DATABASE}_monthly_*.sql.gz"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Generate backup report
|
||||
TOTAL_BACKUPS=$(find "$BACKUP_ROOT_DIR/database" -name "*.sql.gz" | wc -l)
|
||||
TOTAL_SIZE=$(du -sh "$BACKUP_ROOT_DIR/database" | cut -f1)
|
||||
|
||||
log "Backup summary: $TOTAL_BACKUPS total backups, $TOTAL_SIZE total size"
|
||||
log "$BACKUP_TYPE database backup completed successfully"
|
||||
|
||||
# Send notification
|
||||
if command -v mail >/dev/null 2>&1; then
|
||||
echo "Database backup completed successfully at $(date)" | mail -s "Timebank DB Backup Success" "${BACKUP_NOTIFY_EMAIL:-$USER@localhost}" 2>/dev/null || true
|
||||
fi
|
||||
Reference in new issue
Block a user