974 lines
26 KiB
Bash
974 lines
26 KiB
Bash
|
|
#!/bin/bash
|
||
|
|
# =============================================================================
|
||
|
|
# FundTracker Project Backup
|
||
|
|
# =============================================================================
|
||
|
|
# Backs up:
|
||
|
|
# 1. Complete FundTracker project/site files
|
||
|
|
# 2. FundTracker PostgreSQL database
|
||
|
|
#
|
||
|
|
# Creates ONE compressed archive containing both.
|
||
|
|
#
|
||
|
|
# Project files:
|
||
|
|
# /Kingdezignsnas16/fundtracker
|
||
|
|
#
|
||
|
|
# PostgreSQL:
|
||
|
|
# Container: fundtracker_db
|
||
|
|
# Database: fundtracker
|
||
|
|
# User: fundtracker
|
||
|
|
#
|
||
|
|
# Backup destination:
|
||
|
|
# /export/kingdezignsnas-16/Public/07 - Projects/NAS16/fundtracker
|
||
|
|
#
|
||
|
|
# Retention:
|
||
|
|
# Keeps the 10 most recent backups.
|
||
|
|
#
|
||
|
|
# Email:
|
||
|
|
# Uses OMV Postfix / Gmail relay via sendmail.
|
||
|
|
#
|
||
|
|
# Designed to run as ROOT from:
|
||
|
|
# OMV -> System -> Scheduled Jobs
|
||
|
|
#
|
||
|
|
# Backup destination: /export/kingdezignsnas-16/Public/07 - Projects/NAS16/fundtracker
|
||
|
|
# Script location: /usr/scripts/fundtracker-backup.sh
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
set -uo pipefail
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# CONFIGURATION
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
HOSTNAME_LABEL=$(hostname -s 2>/dev/null || echo "NAS16")
|
||
|
|
|
||
|
|
REPORT_TO="rufus.king@kingdezigns.com"
|
||
|
|
REPORT_FROM="backup-monitor@nas16.local"
|
||
|
|
|
||
|
|
# FundTracker project files
|
||
|
|
PROJECT_DIR="/Kingdezignsnas16/fundtracker"
|
||
|
|
|
||
|
|
# PostgreSQL container
|
||
|
|
DB_CONTAINER="fundtracker_db"
|
||
|
|
DB_NAME="fundtracker"
|
||
|
|
DB_USER="fundtracker"
|
||
|
|
|
||
|
|
# Backup destination
|
||
|
|
BACKUP_ROOT="/export/kingdezignsnas-16/Public/07 - Projects/NAS16/fundtracker"
|
||
|
|
|
||
|
|
# Temporary working directory
|
||
|
|
TMP_BASE="/tmp/fundtracker-backup"
|
||
|
|
|
||
|
|
# Keep this many completed backups
|
||
|
|
RETAIN_COUNT=10
|
||
|
|
|
||
|
|
# Date/time
|
||
|
|
DATE=$(date +%Y-%m-%d)
|
||
|
|
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
|
||
|
|
REPORT_TIME=$(date "+%Y-%m-%d %H:%M:%S")
|
||
|
|
|
||
|
|
ARCHIVE_NAME="fundtracker-backup-${TIMESTAMP}.tar.gz"
|
||
|
|
|
||
|
|
TMP_DIR="${TMP_BASE}/fundtracker-${TIMESTAMP}"
|
||
|
|
|
||
|
|
LOG_FILE="${BACKUP_ROOT}/backup.log"
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# STATUS TRACKING
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
OVERALL_STATUS="SUCCESS"
|
||
|
|
SECTION_ROWS_HTML=""
|
||
|
|
WARNING_COUNT=0
|
||
|
|
ERROR_COUNT=0
|
||
|
|
|
||
|
|
add_section_row() {
|
||
|
|
|
||
|
|
local section="$1"
|
||
|
|
local status="$2"
|
||
|
|
local detail="$3"
|
||
|
|
|
||
|
|
case "$status" in
|
||
|
|
|
||
|
|
OK)
|
||
|
|
BADGE="<span style=\"background:#d1fae5;color:#065f46;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;\">✓ OK</span>"
|
||
|
|
;;
|
||
|
|
|
||
|
|
WARNING)
|
||
|
|
BADGE="<span style=\"background:#fef3c7;color:#92400e;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;\">⚠ WARNING</span>"
|
||
|
|
WARNING_COUNT=$((WARNING_COUNT + 1))
|
||
|
|
|
||
|
|
[[ "$OVERALL_STATUS" == "SUCCESS" ]] &&
|
||
|
|
OVERALL_STATUS="WARNING"
|
||
|
|
;;
|
||
|
|
|
||
|
|
ERROR)
|
||
|
|
BADGE="<span style=\"background:#fee2e2;color:#991b1b;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;\">✗ ERROR</span>"
|
||
|
|
ERROR_COUNT=$((ERROR_COUNT + 1))
|
||
|
|
OVERALL_STATUS="FAILED"
|
||
|
|
;;
|
||
|
|
|
||
|
|
SKIPPED)
|
||
|
|
BADGE="<span style=\"background:#f3f4f6;color:#6b7280;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;\">— SKIPPED</span>"
|
||
|
|
;;
|
||
|
|
|
||
|
|
esac
|
||
|
|
|
||
|
|
SECTION_ROWS_HTML+="
|
||
|
|
<tr>
|
||
|
|
<td style=\"padding:9px 14px;border-bottom:1px solid #f3f4f6;font-size:13px;color:#111827;font-weight:600;\">$section</td>
|
||
|
|
<td style=\"padding:9px 14px;border-bottom:1px solid #f3f4f6;\">$BADGE</td>
|
||
|
|
<td style=\"padding:9px 14px;border-bottom:1px solid #f3f4f6;font-size:12px;color:#6b7280;\">$detail</td>
|
||
|
|
</tr>"
|
||
|
|
}
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# LOGGING
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
log() {
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||
|
|
}
|
||
|
|
|
||
|
|
log_section() {
|
||
|
|
|
||
|
|
echo "" | tee -a "$LOG_FILE"
|
||
|
|
echo "------------------------------------------------------------" | tee -a "$LOG_FILE"
|
||
|
|
log "$1"
|
||
|
|
echo "------------------------------------------------------------" | tee -a "$LOG_FILE"
|
||
|
|
}
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# INITIALIZE
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
mkdir -p "$BACKUP_ROOT"
|
||
|
|
|
||
|
|
log_section "FundTracker Backup Starting"
|
||
|
|
|
||
|
|
ARCHIVE_SIZE="N/A"
|
||
|
|
BACKUP_COUNT=0
|
||
|
|
FINAL_ARCHIVE="${BACKUP_ROOT}/${ARCHIVE_NAME}"
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# PRE-FLIGHT CHECK 1 — NAS16 STORAGE
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
if [ ! -d "/export/kingdezignsnas-16" ]; then
|
||
|
|
|
||
|
|
log "ERROR: /export/kingdezignsnas-16 is not mounted or accessible."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"NAS16 Storage" \
|
||
|
|
"ERROR" \
|
||
|
|
"/export/kingdezignsnas-16 is not accessible"
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
log "NAS16 storage is accessible."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"NAS16 Storage" \
|
||
|
|
"OK" \
|
||
|
|
"/export/kingdezignsnas-16 is accessible"
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# PRE-FLIGHT CHECK 2 — FUNDTRACKER PROJECT
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
if [ ! -d "$PROJECT_DIR" ]; then
|
||
|
|
|
||
|
|
log "ERROR: FundTracker project directory not found:"
|
||
|
|
log "$PROJECT_DIR"
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"FundTracker Files" \
|
||
|
|
"ERROR" \
|
||
|
|
"$PROJECT_DIR does not exist"
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
log "FundTracker project directory found."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"FundTracker Files" \
|
||
|
|
"OK" \
|
||
|
|
"$PROJECT_DIR is accessible"
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# PRE-FLIGHT CHECK 3 — DOCKER
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
if ! command -v docker >/dev/null 2>&1; then
|
||
|
|
|
||
|
|
log "ERROR: Docker command not found."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Docker" \
|
||
|
|
"ERROR" \
|
||
|
|
"Docker command is not available"
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Docker" \
|
||
|
|
"OK" \
|
||
|
|
"Docker command available"
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# PRE-FLIGHT CHECK 4 — DATABASE CONTAINER
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
if docker ps --format '{{.Names}}' 2>/dev/null |
|
||
|
|
grep -qx "$DB_CONTAINER"; then
|
||
|
|
|
||
|
|
log "PostgreSQL container is running: $DB_CONTAINER"
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"PostgreSQL Container" \
|
||
|
|
"OK" \
|
||
|
|
"$DB_CONTAINER is running"
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
log "ERROR: PostgreSQL container $DB_CONTAINER is not running."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"PostgreSQL Container" \
|
||
|
|
"ERROR" \
|
||
|
|
"$DB_CONTAINER is not running"
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# STOP HERE IF PRE-FLIGHT FAILED
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
if [ "$OVERALL_STATUS" = "FAILED" ]; then
|
||
|
|
|
||
|
|
log "Pre-flight checks failed. Backup will not proceed."
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
# Create temporary working directory
|
||
|
|
mkdir -p "$TMP_DIR"
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# SECTION 1 — DATABASE BACKUP
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
log_section "Backing up FundTracker PostgreSQL database"
|
||
|
|
|
||
|
|
DB_DIR="${TMP_DIR}/database"
|
||
|
|
|
||
|
|
mkdir -p "$DB_DIR"
|
||
|
|
|
||
|
|
DB_DUMP="${DB_DIR}/fundtracker.sql"
|
||
|
|
|
||
|
|
# pg_dump runs inside the PostgreSQL container.
|
||
|
|
#
|
||
|
|
# This is the same method documented by the FundTracker project itself.
|
||
|
|
#
|
||
|
|
# No database password is stored in this backup script.
|
||
|
|
|
||
|
|
if docker exec "$DB_CONTAINER" \
|
||
|
|
pg_dump \
|
||
|
|
-U "$DB_USER" \
|
||
|
|
"$DB_NAME" \
|
||
|
|
> "$DB_DUMP" 2>>"$LOG_FILE"; then
|
||
|
|
|
||
|
|
log "PostgreSQL dump completed successfully."
|
||
|
|
|
||
|
|
# Compress the SQL dump
|
||
|
|
if gzip "$DB_DUMP"; then
|
||
|
|
|
||
|
|
DB_SIZE=$(du -sh "${DB_DUMP}.gz" | cut -f1)
|
||
|
|
|
||
|
|
log "Database dump compressed."
|
||
|
|
log "Database backup size: $DB_SIZE"
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"PostgreSQL Database" \
|
||
|
|
"OK" \
|
||
|
|
"${DB_NAME} dumped and compressed · ${DB_SIZE}"
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
log "ERROR: Could not compress PostgreSQL dump."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"PostgreSQL Database" \
|
||
|
|
"ERROR" \
|
||
|
|
"pg_dump succeeded but gzip failed"
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
log "ERROR: PostgreSQL pg_dump failed."
|
||
|
|
|
||
|
|
rm -f "$DB_DUMP"
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"PostgreSQL Database" \
|
||
|
|
"ERROR" \
|
||
|
|
"pg_dump failed for database ${DB_NAME}"
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# SECTION 2 — SITE / PROJECT FILES
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
log_section "Backing up FundTracker project files"
|
||
|
|
|
||
|
|
SITE_DIR="${TMP_DIR}/fundtracker"
|
||
|
|
|
||
|
|
mkdir -p "$SITE_DIR"
|
||
|
|
|
||
|
|
if rsync -a "$PROJECT_DIR/" "$SITE_DIR/" 2>&1 |
|
||
|
|
tee -a "$LOG_FILE"; then
|
||
|
|
|
||
|
|
FILE_COUNT=$(find "$SITE_DIR" -type f 2>/dev/null | wc -l)
|
||
|
|
|
||
|
|
log "FundTracker project files copied successfully."
|
||
|
|
log "Files copied: $FILE_COUNT"
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"FundTracker Project Files" \
|
||
|
|
"OK" \
|
||
|
|
"${FILE_COUNT} file(s) backed up"
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
log "ERROR: rsync failed while backing up FundTracker files."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"FundTracker Project Files" \
|
||
|
|
"ERROR" \
|
||
|
|
"File backup failed"
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# SECTION 3 — MANIFEST
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
log_section "Creating backup manifest"
|
||
|
|
|
||
|
|
MANIFEST="${TMP_DIR}/MANIFEST.txt"
|
||
|
|
|
||
|
|
cat > "$MANIFEST" <<MANIFEST_EOF
|
||
|
|
=============================================================
|
||
|
|
FUNDTRACKER BACKUP MANIFEST
|
||
|
|
=============================================================
|
||
|
|
|
||
|
|
Host:
|
||
|
|
$(hostname)
|
||
|
|
|
||
|
|
Backup date:
|
||
|
|
$(date)
|
||
|
|
|
||
|
|
Project source:
|
||
|
|
${PROJECT_DIR}
|
||
|
|
|
||
|
|
Backup destination:
|
||
|
|
${BACKUP_ROOT}
|
||
|
|
|
||
|
|
Archive:
|
||
|
|
${ARCHIVE_NAME}
|
||
|
|
|
||
|
|
|
||
|
|
=============================================================
|
||
|
|
FUNDTRACKER PROJECT
|
||
|
|
=============================================================
|
||
|
|
|
||
|
|
Source:
|
||
|
|
${PROJECT_DIR}
|
||
|
|
|
||
|
|
The complete FundTracker project directory is stored under:
|
||
|
|
|
||
|
|
fundtracker/
|
||
|
|
|
||
|
|
|
||
|
|
=============================================================
|
||
|
|
POSTGRESQL DATABASE
|
||
|
|
=============================================================
|
||
|
|
|
||
|
|
Container:
|
||
|
|
${DB_CONTAINER}
|
||
|
|
|
||
|
|
Database:
|
||
|
|
${DB_NAME}
|
||
|
|
|
||
|
|
User:
|
||
|
|
${DB_USER}
|
||
|
|
|
||
|
|
Database backup:
|
||
|
|
database/fundtracker.sql.gz
|
||
|
|
|
||
|
|
|
||
|
|
=============================================================
|
||
|
|
RESTORE OVERVIEW
|
||
|
|
=============================================================
|
||
|
|
|
||
|
|
1. Extract this archive.
|
||
|
|
|
||
|
|
2. Restore the FundTracker project files to:
|
||
|
|
|
||
|
|
${PROJECT_DIR}
|
||
|
|
|
||
|
|
3. Start the FundTracker Docker Compose project.
|
||
|
|
|
||
|
|
4. Recreate / start the PostgreSQL container.
|
||
|
|
|
||
|
|
5. Restore the database using:
|
||
|
|
|
||
|
|
docker exec -i fundtracker_db \
|
||
|
|
psql -U fundtracker fundtracker \
|
||
|
|
< database/fundtracker.sql
|
||
|
|
|
||
|
|
|
||
|
|
=============================================================
|
||
|
|
MANIFEST COMPLETE
|
||
|
|
=============================================================
|
||
|
|
MANIFEST_EOF
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Backup Manifest" \
|
||
|
|
"OK" \
|
||
|
|
"MANIFEST.txt created"
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# SECTION 4 — CREATE ARCHIVE
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
log_section "Creating compressed backup archive"
|
||
|
|
|
||
|
|
cd "$TMP_BASE" || {
|
||
|
|
|
||
|
|
log "ERROR: Could not change to temporary backup directory."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Archive Creation" \
|
||
|
|
"ERROR" \
|
||
|
|
"Could not access temporary directory"
|
||
|
|
|
||
|
|
OVERALL_STATUS="FAILED"
|
||
|
|
}
|
||
|
|
|
||
|
|
if [ "$OVERALL_STATUS" != "FAILED" ]; then
|
||
|
|
|
||
|
|
if tar -czf "$FINAL_ARCHIVE" \
|
||
|
|
"$(basename "$TMP_DIR")" \
|
||
|
|
2>>"$LOG_FILE"; then
|
||
|
|
|
||
|
|
ARCHIVE_SIZE=$(du -sh "$FINAL_ARCHIVE" | cut -f1)
|
||
|
|
|
||
|
|
log "Archive created successfully."
|
||
|
|
log "Archive: $FINAL_ARCHIVE"
|
||
|
|
log "Size: $ARCHIVE_SIZE"
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Archive Creation" \
|
||
|
|
"OK" \
|
||
|
|
"${ARCHIVE_NAME} · ${ARCHIVE_SIZE}"
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
log "ERROR: tar failed while creating archive."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Archive Creation" \
|
||
|
|
"ERROR" \
|
||
|
|
"tar failed — archive was not created"
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# SECTION 5 — VERIFY ARCHIVE
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
if [ -f "$FINAL_ARCHIVE" ]; then
|
||
|
|
|
||
|
|
log_section "Verifying backup archive"
|
||
|
|
|
||
|
|
if tar -tzf "$FINAL_ARCHIVE" >/dev/null 2>&1; then
|
||
|
|
|
||
|
|
log "Archive verification successful."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Archive Verification" \
|
||
|
|
"OK" \
|
||
|
|
"Archive can be opened and read successfully"
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
log "ERROR: Archive verification failed."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Archive Verification" \
|
||
|
|
"ERROR" \
|
||
|
|
"tar could not read the completed archive"
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
log "ERROR: Final backup archive does not exist."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Archive Verification" \
|
||
|
|
"ERROR" \
|
||
|
|
"Final archive was not created"
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# SECTION 6 — CLEANUP
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
log_section "Cleaning up temporary files"
|
||
|
|
|
||
|
|
if rm -rf "$TMP_DIR"; then
|
||
|
|
|
||
|
|
log "Temporary backup files removed."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Temporary File Cleanup" \
|
||
|
|
"OK" \
|
||
|
|
"Temporary files removed"
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
log "WARNING: Could not completely remove temporary directory."
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Temporary File Cleanup" \
|
||
|
|
"WARNING" \
|
||
|
|
"Temporary directory could not be completely removed"
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# SECTION 7 — ROTATE OLD BACKUPS
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
log_section "Rotating old FundTracker backups"
|
||
|
|
|
||
|
|
BACKUP_LIST=$(ls -1t \
|
||
|
|
"${BACKUP_ROOT}"/fundtracker-backup-*.tar.gz \
|
||
|
|
2>/dev/null || true)
|
||
|
|
|
||
|
|
BACKUP_COUNT=$(echo "$BACKUP_LIST" | grep -c . || true)
|
||
|
|
|
||
|
|
DELETED_COUNT=0
|
||
|
|
|
||
|
|
if [ "$BACKUP_COUNT" -gt "$RETAIN_COUNT" ]; then
|
||
|
|
|
||
|
|
DELETE_LIST=$(echo "$BACKUP_LIST" |
|
||
|
|
tail -n +$((RETAIN_COUNT + 1)))
|
||
|
|
|
||
|
|
while IFS= read -r OLD_BACKUP; do
|
||
|
|
|
||
|
|
[ -z "$OLD_BACKUP" ] && continue
|
||
|
|
|
||
|
|
rm -f "$OLD_BACKUP"
|
||
|
|
|
||
|
|
log "Removed old backup: $(basename "$OLD_BACKUP")"
|
||
|
|
|
||
|
|
DELETED_COUNT=$((DELETED_COUNT + 1))
|
||
|
|
|
||
|
|
done <<< "$DELETE_LIST"
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Backup Rotation" \
|
||
|
|
"OK" \
|
||
|
|
"Kept ${RETAIN_COUNT} most recent · removed ${DELETED_COUNT} old backup(s)"
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
add_section_row \
|
||
|
|
"Backup Rotation" \
|
||
|
|
"OK" \
|
||
|
|
"${BACKUP_COUNT} / ${RETAIN_COUNT} backups retained"
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# FINAL STATUS
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
log_section "FundTracker Backup Complete"
|
||
|
|
|
||
|
|
log "Archive: ${FINAL_ARCHIVE:-N/A}"
|
||
|
|
log "Size: ${ARCHIVE_SIZE:-N/A}"
|
||
|
|
log "Status: ${OVERALL_STATUS}"
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# EMAIL STATUS BANNER
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
case "$OVERALL_STATUS" in
|
||
|
|
|
||
|
|
SUCCESS)
|
||
|
|
|
||
|
|
BADGE_HTML="<div style=\"background:#1a7f4b;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;\">✅ SUCCESS</div>"
|
||
|
|
|
||
|
|
BANNER_HTML="<tr><td style=\"background:#1a7f4b;padding:12px 32px;\"><span style=\"color:white;font-size:14px;font-weight:600;\">✅ FundTracker backup completed successfully — no action required.</span></td></tr>"
|
||
|
|
|
||
|
|
;;
|
||
|
|
|
||
|
|
WARNING)
|
||
|
|
|
||
|
|
BADGE_HTML="<div style=\"background:#b45309;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;\">⚠️ WARNING</div>"
|
||
|
|
|
||
|
|
BANNER_HTML="<tr><td style=\"background:#b45309;padding:12px 32px;\"><span style=\"color:white;font-size:14px;font-weight:600;\">⚠️ FundTracker backup completed with warnings — review recommended.</span></td></tr>"
|
||
|
|
|
||
|
|
;;
|
||
|
|
|
||
|
|
FAILED)
|
||
|
|
|
||
|
|
BADGE_HTML="<div style=\"background:#b91c1c;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;\">🚨 FAILED</div>"
|
||
|
|
|
||
|
|
BANNER_HTML="<tr><td style=\"background:#b91c1c;padding:12px 32px;\"><span style=\"color:white;font-size:14px;font-weight:600;\">🚨 FundTracker backup failed — immediate attention required!</span></td></tr>"
|
||
|
|
|
||
|
|
;;
|
||
|
|
|
||
|
|
esac
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# ARCHIVE CARD
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
if [ -f "${FINAL_ARCHIVE:-}" ]; then
|
||
|
|
|
||
|
|
ARCHIVE_CARD="
|
||
|
|
<tr><td style=\"padding:0 24px 8px;\">
|
||
|
|
<div style=\"background:white;border-radius:10px;border:1px solid #e5e7eb;padding:18px 22px;\">
|
||
|
|
|
||
|
|
<div style=\"font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;\">
|
||
|
|
🗜️ Backup Archive
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
|
||
|
|
<td style=\"width:50%;padding:8px 12px;background:#f9fafb;border-radius:6px;text-align:center;\">
|
||
|
|
<div style=\"font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;\">
|
||
|
|
Filename
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style=\"font-family:monospace;font-size:13px;font-weight:600;color:#111827;margin-top:4px;\">
|
||
|
|
${ARCHIVE_NAME}
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td style=\"width:4%;\"></td>
|
||
|
|
|
||
|
|
<td style=\"width:20%;padding:8px 12px;background:#f9fafb;border-radius:6px;text-align:center;\">
|
||
|
|
<div style=\"font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;\">
|
||
|
|
Size
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style=\"font-size:20px;font-weight:700;color:#111827;margin-top:2px;\">
|
||
|
|
${ARCHIVE_SIZE}
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td style=\"width:4%;\"></td>
|
||
|
|
|
||
|
|
<td style=\"width:20%;padding:8px 12px;background:#f9fafb;border-radius:6px;text-align:center;\">
|
||
|
|
<div style=\"font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;\">
|
||
|
|
Retained
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style=\"font-size:20px;font-weight:700;color:#111827;margin-top:2px;\">
|
||
|
|
${BACKUP_COUNT} / ${RETAIN_COUNT}
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
</table>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</td></tr>"
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
ARCHIVE_CARD=""
|
||
|
|
|
||
|
|
fi
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# HTML EMAIL
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
HTML_BODY=$(cat <<EOF
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="en">
|
||
|
|
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
|
|
</head>
|
||
|
|
|
||
|
|
<body style="margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
||
|
|
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0" style="background:#f1f5f9;padding:30px 0;">
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td align="center">
|
||
|
|
|
||
|
|
<table width="680" cellspacing="0" cellpadding="0" style="max-width:680px;width:100%;">
|
||
|
|
|
||
|
|
<!-- HEADER -->
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="background:#0f172a;border-radius:10px 10px 0 0;padding:28px 32px;">
|
||
|
|
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
|
||
|
|
<td>
|
||
|
|
|
||
|
|
<div style="font-size:11px;color:#94a3b8;letter-spacing:1px;text-transform:uppercase;">
|
||
|
|
FundTracker Backup Monitor
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style="font-size:26px;font-weight:800;color:white;margin-top:4px;">
|
||
|
|
💾 $HOSTNAME_LABEL
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style="font-size:13px;color:#94a3b8;margin-top:4px;">
|
||
|
|
/usr/scripts/fundtracker/fundtracker-backup.sh
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style="font-size:13px;color:#94a3b8;margin-top:4px;">
|
||
|
|
$REPORT_TIME | $WARNING_COUNT warning(s) · $ERROR_COUNT error(s)
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td align="right">
|
||
|
|
$BADGE_HTML
|
||
|
|
</td>
|
||
|
|
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
</table>
|
||
|
|
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<!-- STATUS BANNER -->
|
||
|
|
|
||
|
|
$BANNER_HTML
|
||
|
|
|
||
|
|
<!-- SECTION RESULTS -->
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="padding:28px 24px 8px;">
|
||
|
|
|
||
|
|
<div style="background:white;border-radius:10px;box-shadow:0 1px 6px rgba(0,0,0,.10);overflow:hidden;border:1px solid #e5e7eb;">
|
||
|
|
|
||
|
|
<div style="background:#f8fafc;padding:14px 22px;border-bottom:1px solid #e5e7eb;">
|
||
|
|
|
||
|
|
<span style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.5px;">
|
||
|
|
📋 Backup Section Results
|
||
|
|
</span>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||
|
|
|
||
|
|
<tr style="background:#f9fafb;">
|
||
|
|
|
||
|
|
<th style="text-align:left;padding:8px 14px;font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid #e5e7eb;width:30%;">
|
||
|
|
Section
|
||
|
|
</th>
|
||
|
|
|
||
|
|
<th style="text-align:left;padding:8px 14px;font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid #e5e7eb;width:15%;">
|
||
|
|
Status
|
||
|
|
</th>
|
||
|
|
|
||
|
|
<th style="text-align:left;padding:8px 14px;font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid #e5e7eb;">
|
||
|
|
Detail
|
||
|
|
</th>
|
||
|
|
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
$SECTION_ROWS_HTML
|
||
|
|
|
||
|
|
</table>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<!-- ARCHIVE CARD -->
|
||
|
|
|
||
|
|
$ARCHIVE_CARD
|
||
|
|
|
||
|
|
<!-- SUMMARY -->
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="padding:0 24px 28px;">
|
||
|
|
|
||
|
|
<div style="background:white;border-radius:10px;border:1px solid #e5e7eb;padding:18px 22px;">
|
||
|
|
|
||
|
|
<div style="font-size:13px;font-weight:700;color:#374151;margin-bottom:10px;text-transform:uppercase;letter-spacing:.5px;">
|
||
|
|
📊 Backup Summary
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Host</td>
|
||
|
|
<td style="font-size:13px;color:#111827;font-weight:600;">$HOSTNAME_LABEL</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Run Time</td>
|
||
|
|
<td style="font-size:13px;color:#111827;font-weight:600;">$REPORT_TIME</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Project Files</td>
|
||
|
|
<td style="font-family:monospace;font-size:11px;color:#111827;font-weight:600;">$PROJECT_DIR</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Database</td>
|
||
|
|
<td style="font-size:13px;color:#111827;font-weight:600;">PostgreSQL / $DB_NAME</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Destination</td>
|
||
|
|
<td style="font-family:monospace;font-size:11px;color:#111827;font-weight:600;">$BACKUP_ROOT</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Archive</td>
|
||
|
|
<td style="font-family:monospace;font-size:12px;color:#111827;font-weight:600;">$ARCHIVE_NAME</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Archive Size</td>
|
||
|
|
<td style="font-size:13px;color:#111827;font-weight:600;">$ARCHIVE_SIZE</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Backups on Disk</td>
|
||
|
|
<td style="font-size:13px;color:#111827;font-weight:600;">$BACKUP_COUNT of $RETAIN_COUNT max retained</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Warnings</td>
|
||
|
|
<td style="font-size:13px;font-weight:700;">$WARNING_COUNT</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Errors</td>
|
||
|
|
<td style="font-size:13px;font-weight:700;">$ERROR_COUNT</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Overall Status</td>
|
||
|
|
<td style="font-size:13px;font-weight:700;">$OVERALL_STATUS</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
</table>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
<!-- FOOTER -->
|
||
|
|
|
||
|
|
<tr>
|
||
|
|
<td style="background:#0f172a;border-radius:0 0 10px 10px;padding:16px 32px;text-align:center;">
|
||
|
|
|
||
|
|
<span style="font-size:11px;color:#64748b;">
|
||
|
|
Automated Backup Monitor | $HOSTNAME_LABEL | KingDezigns FundTracker
|
||
|
|
</span>
|
||
|
|
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
</table>
|
||
|
|
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
|
||
|
|
</table>
|
||
|
|
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
EOF
|
||
|
|
)
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# SEND EMAIL
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
SUBJECT="[FundTracker Backup] $HOSTNAME_LABEL — $OVERALL_STATUS | $REPORT_TIME"
|
||
|
|
|
||
|
|
{
|
||
|
|
echo "To: $REPORT_TO"
|
||
|
|
echo "From: $REPORT_FROM"
|
||
|
|
echo "Subject: $SUBJECT"
|
||
|
|
echo "MIME-Version: 1.0"
|
||
|
|
echo "Content-Type: text/html; charset=UTF-8"
|
||
|
|
echo ""
|
||
|
|
echo "$HTML_BODY"
|
||
|
|
} | sendmail -t
|
||
|
|
|
||
|
|
log "Email sent to $REPORT_TO — Overall status: $OVERALL_STATUS"
|
||
|
|
log "===== FundTracker backup script finished ====="
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# EXIT STATUS
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
if [ "$OVERALL_STATUS" = "FAILED" ]; then
|
||
|
|
exit 1
|
||
|
|
else
|
||
|
|
exit 0
|
||
|
|
fi
|
||
|
|
```
|
||
|
|
|