Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Automatically clean up old WordPress backup files to free up disk space.
**Features:**
- Scans all WordPress installations in Plesk vhosts
- Removes backups older than a specified number of days (default: 365 days)
- Always keeps a minimum number of most recent backups per domain (default: 3)
- Optional email report of backups found/removed per domain
- Dry-run mode to preview deletions without removing files
- Configurable retention period via environment variables
- Safe deletion with proper error handling
Expand Down Expand Up @@ -221,6 +223,17 @@ DAYS=180 ./remove-old-wordpress-backups/remove-wordpress-backups.sh
# Preview custom retention period before deleting
DAYS=180 ./remove-old-wordpress-backups/remove-wordpress-backups.sh --dry-run

# Always keep at least 5 most recent backups per domain, even if older than DAYS
MIN_KEEP=5 ./remove-old-wordpress-backups/remove-wordpress-backups.sh

# Email a per-domain report of backups found/removed (requires the 'mail' command)
EMAIL_TO="admin@example.com" ./remove-old-wordpress-backups/remove-wordpress-backups.sh

# Email the report via an SMTP relay when no local MTA is available (uses curl)
EMAIL_TO="admin@example.com" SMTP_SERVER="mail.example.com" SMTP_PORT=587 SMTP_SECURE=starttls \
SMTP_AUTH_USER="relay-user" SMTP_AUTH_PASS="relay-pass" \
./remove-old-wordpress-backups/remove-wordpress-backups.sh

# Run with auto-update enabled
AUTO_UPDATE=true ./remove-old-wordpress-backups/remove-wordpress-backups.sh

Expand Down Expand Up @@ -327,8 +340,19 @@ pci-dss-scan\pci-dss-scan.bat https://example.com
**Environment Variables:**
- `DAYS` - Number of days to keep backups (default: `365`)
- Example: `DAYS=180` keeps backups for 6 months
- `MIN_KEEP` - Minimum number of most recent backups to always keep per domain, regardless of age (default: `3`)
- Example: `MIN_KEEP=5` never deletes a domain's 5 newest backups even if older than `DAYS`
- `DRY_RUN` - Set to `true` to enable dry-run mode (default: `false`)
- Example: `DRY_RUN=true` previews deletions without removing files
- `EMAIL_TO` - Email address to receive the per-domain report (default: unset, no email sent)
- Uses the local `mail` command if available; falls back to the SMTP relay settings below via `curl` if not
- Report includes, per domain: backups found, backups removed (or would-remove in dry-run), and filenames removed
- `EMAIL_SUBJECT` - Subject line for the email report (default: `WordPress Backup Cleanup Report - <hostname>`)
- `SMTP_SERVER` - SMTP relay hostname, used only if the local `mail` command is unavailable (default: unset)
- `SMTP_PORT` - SMTP relay port (default: `25`)
- `SMTP_AUTH_USER` / `SMTP_AUTH_PASS` - SMTP credentials, leave unset for an unauthenticated relay (default: unset)
- `SMTP_SECURE` - SMTP security: blank for plain, `ssl` for implicit TLS (typically port 465), `starttls` for explicit STARTTLS (typically port 587) (default: blank)
- `SMTP_FROM` - Sender address (default: `plesk-monitor@<hostname>`)

**Command-line Options:**
- `--dry-run` or `-n` - Preview deletions without removing files
Expand Down
199 changes: 183 additions & 16 deletions remove-old-wordpress-backups/remove-wordpress-backups.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# - Scans all WordPress installations in Plesk vhosts for backup files
# - Removes backups older than specified retention period
# - Configurable retention via DAYS environment variable (default: 365 days)
# - Keeps a minimum number of most recent backups per domain regardless of age
# - Optional email report summarizing backups found/removed per domain
# - Dry-run mode to preview deletions without removing files
# - Safe deletion with proper error handling and validation
# - Detailed logging with timestamps
Expand All @@ -13,7 +15,16 @@
# Usage: ./remove-wordpress-backups.sh [--dry-run] [--update|--self-update] or DAYS=180 ./remove-wordpress-backups.sh
# Environment Variables:
# DAYS - Number of days to keep backups (default: 365)
# MIN_KEEP - Minimum number of most recent backups to keep per domain (default: 3)
# DRY_RUN - Set to "true" to enable dry-run mode (default: false)
# EMAIL_TO - Email address to send the per-domain report to (default: unset, no email sent)
# EMAIL_SUBJECT - Subject line for the email report (default: "WordPress Backup Cleanup Report - <hostname>")
# SMTP_SERVER - SMTP relay host, used via curl if local 'mail' command is unavailable (default: unset)
# SMTP_PORT - SMTP relay port (default: 25)
# SMTP_AUTH_USER - SMTP username, leave unset for unauthenticated relay (default: unset)
# SMTP_AUTH_PASS - SMTP password (default: unset)
# SMTP_SECURE - SMTP security: blank/"ssl"/"starttls" (default: blank/plain)
# SMTP_FROM - Sender address (default: plesk-monitor@<hostname>)
# AUTO_UPDATE - Set to "true" to enable automatic updates (default: false)
# UPDATE_CHECK_INTERVAL - Hours between update checks (default: 24)
# GITHUB_BRANCH - GitHub branch to update from (default: main)
Expand Down Expand Up @@ -169,9 +180,19 @@ fi
## CONFIGURATION
###
DAYS="${DAYS:-365}"
MIN_KEEP="${MIN_KEEP:-3}"
BACKUP_PATH="/var/www/vhosts/*/wordpress-backups"
FIND_CMD="/bin/find"
RM_CMD="/bin/rm"
EMAIL_TO="${EMAIL_TO:-}"
EMAIL_SUBJECT="${EMAIL_SUBJECT:-}"
# SMTP fallback for servers without a local MTA (used only if 'mail' command is unavailable)
SMTP_SERVER="${SMTP_SERVER:-}"
SMTP_PORT="${SMTP_PORT:-25}"
SMTP_AUTH_USER="${SMTP_AUTH_USER:-}"
SMTP_AUTH_PASS="${SMTP_AUTH_PASS:-}"
SMTP_SECURE="${SMTP_SECURE:-}"
SMTP_FROM="${SMTP_FROM:-}"

###
## FUNCTIONS
Expand All @@ -189,6 +210,12 @@ validate_configuration() {
log_message "ERROR: DAYS must be a positive integer (provided: ${DAYS})"
return 1
fi

# Validate MIN_KEEP parameter is a non-negative integer
if ! echo "${MIN_KEEP}" | grep -qE '^[0-9]+$'; then
log_message "ERROR: MIN_KEEP must be a non-negative integer (provided: ${MIN_KEEP})"
return 1
fi

# Verify required commands exist
if [ ! -x "${FIND_CMD}" ]; then
Expand All @@ -200,10 +227,144 @@ validate_configuration() {
log_message "ERROR: rm command not found at: ${RM_CMD}"
return 1
fi

# EMAIL_TO is optional, but if set we need a way to actually send it:
# either a local MTA via the 'mail' command, or SMTP relay settings for curl.
if [ -n "${EMAIL_TO}" ] && ! command -v mail >/dev/null 2>&1 && [ -z "${SMTP_SERVER}" ]; then
log_message "WARNING: EMAIL_TO is set but no 'mail' command or SMTP_SERVER was found. Report will not be emailed."
fi

return 0
}

# Per-domain scan results, populated by scan_domains(). Parallel arrays indexed by domain.
DOMAIN_NAMES=()
DOMAIN_FOUND=()
DOMAIN_REMOVED_FILES=() # newline-separated basenames of files removed (or eligible, in dry-run) for that domain
TO_DELETE=() # full paths of every file eligible for deletion, across all domains

# Function to scan every domain's backup directory: records how many backups
# exist, and which ones are eligible for deletion (older than DAYS, beyond the
# MIN_KEEP newest).
scan_domains() {
local dir domain total i removed_list
for dir in ${BACKUP_PATH}; do
[ -d "${dir}" ] || continue
domain=$(basename "$(dirname "${dir}")")

# Newest-first list of backup files in this domain's directory
local files=()
while IFS= read -r line; do
files+=("${line#* }")
done < <(${FIND_CMD} "${dir}" -maxdepth 1 -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn)

total=${#files[@]}
removed_list=""

# Anything beyond the MIN_KEEP newest is eligible if it's also older than DAYS
if [ "${total}" -gt "${MIN_KEEP}" ]; then
for ((i = MIN_KEEP; i < total; i++)); do
if [ -n "$(${FIND_CMD} "${files[$i]}" -mtime +"${DAYS}" 2>/dev/null)" ]; then
TO_DELETE+=("${files[$i]}")
removed_list+="$(basename "${files[$i]}")"$'\n'
fi
done
fi

DOMAIN_NAMES+=("${domain}")
DOMAIN_FOUND+=("${total}")
DOMAIN_REMOVED_FILES+=("${removed_list}")
done
}

# Function to build the plain-text per-domain report used both on-screen and in the email
build_report() {
local action="Removed"
[ "${DRY_RUN}" = "true" ] && action="Would remove (dry-run)"

local i domain found removed_files removed_count
for i in "${!DOMAIN_NAMES[@]}"; do
domain="${DOMAIN_NAMES[$i]}"
found="${DOMAIN_FOUND[$i]}"
removed_files="${DOMAIN_REMOVED_FILES[$i]}"
removed_count=0
[ -n "${removed_files}" ] && removed_count=$(printf '%s' "${removed_files}" | grep -c .)

echo "Domain: ${domain}"
echo " Backups found: ${found}"
echo " ${action}: ${removed_count}"
if [ "${removed_count}" -gt 0 ]; then
printf '%s' "${removed_files}" | sed '/^$/d;s/^/ - /'
fi
echo ""
done
}

# Function to send the report via an SMTP relay using curl, for servers with no local MTA.
# SMTP_SECURE: blank for plain, "ssl" for implicit TLS (typically port 465), "starttls" for
# explicit STARTTLS (typically port 587) — same convention as monitor-aspnet.bat.
send_via_smtp() {
local subject="$1" body="$2"
local from="${SMTP_FROM:-plesk-monitor@$(hostname -f 2>/dev/null || hostname)}"
local scheme="smtp"
[ "${SMTP_SECURE}" = "ssl" ] && scheme="smtps"

local msg_file
msg_file=$(mktemp)
{
echo "From: ${from}"
echo "To: ${EMAIL_TO}"
echo "Subject: ${subject}"
echo "Date: $(date -R)"
echo ""
echo "${body}"
} > "${msg_file}"

local -a curl_args=(--silent --show-error
--url "${scheme}://${SMTP_SERVER}:${SMTP_PORT}"
--mail-from "${from}" --mail-rcpt "${EMAIL_TO}"
--upload-file "${msg_file}")
[ "${SMTP_SECURE}" = "starttls" ] && curl_args+=(--ssl-reqd)
[ -n "${SMTP_AUTH_USER}" ] && curl_args+=(--user "${SMTP_AUTH_USER}:${SMTP_AUTH_PASS}")

local rc=0
curl "${curl_args[@]}" || rc=$?
rm -f "${msg_file}"
return "${rc}"
}

# Function to email the report when EMAIL_TO is configured. Prefers the local
# 'mail' command; falls back to a direct SMTP relay via curl when no local
# MTA is present but SMTP_SERVER is configured.
send_email_report() {
local body="$1"

[ -n "${EMAIL_TO}" ] || return 0

local subject="${EMAIL_SUBJECT:-WordPress Backup Cleanup Report - $(hostname -s 2>/dev/null || hostname)}"

if command -v mail >/dev/null 2>&1; then
if echo "${body}" | mail -s "${subject}" "${EMAIL_TO}"; then
log_message "Report emailed to ${EMAIL_TO}"
return 0
fi
log_message "WARNING: 'mail' command failed to send report to ${EMAIL_TO}"
return 1
fi

if [ -n "${SMTP_SERVER}" ]; then
if send_via_smtp "${subject}" "${body}"; then
log_message "Report emailed to ${EMAIL_TO} via SMTP (${SMTP_SERVER}:${SMTP_PORT})"
return 0
fi
log_message "WARNING: Failed to send email report to ${EMAIL_TO} via SMTP (${SMTP_SERVER}:${SMTP_PORT})"
return 1
fi

log_message "WARNING: Cannot send email report - no 'mail' command and no SMTP_SERVER configured."
return 1
}

# Function to remove old WordPress backups
remove_wordpress_backups() {
log_message "============================================================================"
Expand All @@ -213,6 +374,7 @@ remove_wordpress_backups() {
log_message "MODE: DRY-RUN (no files will be deleted)"
fi
log_message "Retention period: ${DAYS} days"
log_message "Minimum backups kept per domain: ${MIN_KEEP}"
log_message "Search path: ${BACKUP_PATH}"
echo ""

Expand All @@ -224,38 +386,43 @@ remove_wordpress_backups() {
return 0
fi

log_message "Scanning for backup files older than ${DAYS} days..."

# Count files before deletion
local file_count
file_count=$(${FIND_CMD} ${BACKUP_PATH} -type f -mtime +"${DAYS}" 2>/dev/null | wc -l) || file_count=0

log_message "Scanning for backup files older than ${DAYS} days (keeping ${MIN_KEEP} newest per domain)..."

scan_domains

local file_count=${#TO_DELETE[@]}
local report
report=$(build_report)

if [ "${file_count}" -eq 0 ]; then
log_message "No backup files older than ${DAYS} days found. Nothing to delete."
log_message "No eligible backup files found. Nothing to delete."
echo ""
echo "${report}"
send_email_report "${report}"
return 0
fi

log_message "Found ${file_count} backup file(s) to delete."
echo ""
echo "${report}"

# In dry-run mode, list files that would be deleted
# In dry-run mode, don't delete - just report what would happen
if [ "${DRY_RUN}" = "true" ]; then
echo ""
log_message "Files that would be deleted (dry-run mode):"
log_message "------------------------------------------------------------"
${FIND_CMD} ${BACKUP_PATH} -type f -mtime +"${DAYS}" -exec ls -lh {} \; 2>/dev/null | while read -r line; do
echo " $line"
done
echo ""
log_message "Dry-run complete. ${file_count} file(s) would be deleted."
log_message "Run without --dry-run flag to actually delete these files."
send_email_report "${report}"
return 0
fi

# Remove old backup files
if ${FIND_CMD} ${BACKUP_PATH} -type f -mtime +"${DAYS}" -exec ${RM_CMD} -f {} + 2>/dev/null; then
if printf '%s\0' "${TO_DELETE[@]}" | xargs -0 "${RM_CMD}" -f; then
log_message "Successfully removed ${file_count} old backup file(s)."
send_email_report "${report}"
else
log_message "ERROR: Failed to remove some backup files. Check permissions."
send_email_report "${report}

WARNING: one or more files could not be removed - check server permissions/logs."
return 1
fi

Expand Down
Loading