mirror of
https://github.com/frappe/frappe_docker.git
synced 2026-06-18 14:15:09 +00:00
225 lines
No EOL
8.2 KiB
Bash
Executable file
225 lines
No EOL
8.2 KiB
Bash
Executable file
#!/bin/bash
|
|
|
|
# Backup ERPNext Site Script
|
|
# Usage: ./backup-site.sh <site-name> [options]
|
|
|
|
set -euo pipefail
|
|
|
|
# Colors
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
|
|
|
|
# Helper functions
|
|
echo_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
echo_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
echo_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
echo_debug() { [[ "${DEBUG:-}" == "1" ]] && echo -e "${BLUE}[DEBUG]${NC} $1" || true; }
|
|
log_action() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "/tmp/erpnext-backup-$(date '+%Y%m%d').log"; }
|
|
|
|
# Cleanup on error
|
|
cleanup() {
|
|
local exit_code=$?
|
|
[[ $exit_code -ne 0 ]] && echo_error "Script failed with exit code $exit_code" && log_action "FAILED: ${SITE_NAME:-unknown}"
|
|
exit $exit_code
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# Configuration
|
|
PROJECT_NAME="${PROJECT_NAME:-erpnext-production}"
|
|
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}"
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PRODUCTION_DIR="$(dirname "$SCRIPT_DIR")"
|
|
|
|
# Docker helpers
|
|
dc_exec() { docker compose --project-name "$PROJECT_NAME" exec backend "$@"; }
|
|
dc_cmd() { docker compose --project-name "$PROJECT_NAME" "$@"; }
|
|
|
|
cd "$PRODUCTION_DIR"
|
|
|
|
# Help function
|
|
show_help() {
|
|
cat << EOF
|
|
Usage: $0 <site-name> [options]
|
|
|
|
Options:
|
|
--with-files Include files in backup
|
|
--compress Compress the backup
|
|
--auto-copy Copy backups to host ./backups/
|
|
--cleanup-old Remove backups older than BACKUP_RETENTION_DAYS
|
|
--encrypt Encrypt with GPG (requires BACKUP_PASSPHRASE env var)
|
|
--debug Enable debug output
|
|
-h, --help Show this help
|
|
|
|
Environment Variables:
|
|
PROJECT_NAME Docker project name (default: erpnext-production)
|
|
BACKUP_RETENTION_DAYS Keep backups for N days (default: 30)
|
|
BACKUP_PASSPHRASE GPG encryption passphrase
|
|
AUTO_COPY Auto-copy to host (set to 1)
|
|
CLEANUP_OLD Auto-cleanup old backups (set to 1)
|
|
|
|
Examples:
|
|
$0 erp.example.com --with-files --auto-copy
|
|
BACKUP_PASSPHRASE='secret' $0 erp.example.com --encrypt --auto-copy
|
|
|
|
Decryption:
|
|
gpg --decrypt backup-file.gpg > backup-file
|
|
EOF
|
|
}
|
|
|
|
# Parse arguments
|
|
SITE_NAME="" WITH_FILES="" COMPRESS=""
|
|
AUTO_COPY="${AUTO_COPY:-0}" CLEANUP_OLD="${CLEANUP_OLD:-0}" ENCRYPT="${ENCRYPT:-0}"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-h|--help) show_help; exit 0 ;;
|
|
--with-files) WITH_FILES="--with-files"; shift ;;
|
|
--compress) COMPRESS="--compress"; shift ;;
|
|
--auto-copy) AUTO_COPY=1; shift ;;
|
|
--cleanup-old) CLEANUP_OLD=1; shift ;;
|
|
--encrypt) ENCRYPT=1; shift ;;
|
|
--debug) DEBUG=1; shift ;;
|
|
*) [[ -z "$SITE_NAME" ]] && SITE_NAME="$1" || { echo_error "Unknown: $1"; show_help; exit 1; }; shift ;;
|
|
esac
|
|
done
|
|
|
|
# Get site name if not provided
|
|
if [[ -z "$SITE_NAME" ]]; then
|
|
echo_warn "Site name required"
|
|
read -p "Enter site name (e.g., erp.example.com): " SITE_NAME
|
|
fi
|
|
|
|
[[ -z "$SITE_NAME" ]] && { echo_error "Site name cannot be empty"; exit 1; }
|
|
|
|
# Validate site name format
|
|
if [[ ! "$SITE_NAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$ ]]; then
|
|
echo_warn "Site name format looks unusual: $SITE_NAME"
|
|
read -p "Continue? (y/N): " -n 1 -r; echo
|
|
[[ ! $REPLY =~ ^[Yy]$ ]] && exit 1
|
|
fi
|
|
|
|
echo_debug "Site: $SITE_NAME, Project: $PROJECT_NAME"
|
|
|
|
# Check encryption requirements
|
|
if [[ "$ENCRYPT" == "1" ]]; then
|
|
[[ -z "${BACKUP_PASSPHRASE:-}" ]] && { echo_error "BACKUP_PASSPHRASE not set"; exit 1; }
|
|
command -v gpg >/dev/null 2>&1 || { echo_error "GPG not installed"; exit 1; }
|
|
echo_debug "Encryption ready"
|
|
fi
|
|
|
|
# Validate environment
|
|
[[ ! -f "production.yaml" ]] && { echo_error "production.yaml not found"; exit 1; }
|
|
docker info >/dev/null 2>&1 || { echo_error "Docker not running"; exit 1; }
|
|
dc_exec echo "test" >/dev/null 2>&1 || { echo_error "Backend container not running"; exit 1; }
|
|
|
|
# Verify site exists
|
|
echo_info "Verifying site: $SITE_NAME"
|
|
if ! dc_exec bench --site "$SITE_NAME" list-apps >/dev/null 2>&1; then
|
|
echo_error "Site '$SITE_NAME' not found"
|
|
echo_info "Available sites:"
|
|
dc_exec find /home/frappe/frappe-bench/sites -maxdepth 1 -type d \( -name "*.local" -o -name "*.*" \) | \
|
|
sed 's|.*/||' | grep -v "^$" || echo " None found"
|
|
exit 1
|
|
fi
|
|
|
|
# Create backup
|
|
echo_info "Creating backup for: $SITE_NAME"
|
|
log_action "STARTED: $SITE_NAME"
|
|
BACKUP_TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
|
|
|
|
if ! dc_exec bench --site "$SITE_NAME" backup $WITH_FILES $COMPRESS; then
|
|
echo_error "Backup command failed!"
|
|
log_action "FAILED: Backup command"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify backup files
|
|
BACKUP_PATH="/home/frappe/frappe-bench/sites/$SITE_NAME/private/backups"
|
|
BACKUP_FILES=$(dc_exec find "$BACKUP_PATH" -type f -mmin -2 2>/dev/null | tr -d '\r' || echo "")
|
|
|
|
if [[ -z "$BACKUP_FILES" ]]; then
|
|
echo_error "No backup files found!"
|
|
log_action "FAILED: No files"
|
|
exit 1
|
|
fi
|
|
|
|
# Display backup info
|
|
BACKUP_COUNT=$(echo "$BACKUP_FILES" | wc -l)
|
|
echo_info "✓ Created $BACKUP_COUNT file(s)"
|
|
echo_info "Files:"
|
|
|
|
TOTAL_SIZE=0
|
|
while IFS= read -r file; do
|
|
[[ -z "$file" ]] && continue
|
|
SIZE_BYTES=$(dc_exec stat -c%s "$file" 2>/dev/null | tr -d '\r' || echo "0")
|
|
SIZE_HR=$(numfmt --to=iec-i --suffix=B "$SIZE_BYTES" 2>/dev/null || echo "$SIZE_BYTES bytes")
|
|
echo_info " - $(basename "$file") ($SIZE_HR)"
|
|
TOTAL_SIZE=$((TOTAL_SIZE + SIZE_BYTES))
|
|
done <<< "$BACKUP_FILES"
|
|
|
|
[[ $TOTAL_SIZE -gt 0 ]] && echo_info "Total size: $(numfmt --to=iec-i --suffix=B "$TOTAL_SIZE" 2>/dev/null || echo "$TOTAL_SIZE bytes")"
|
|
|
|
log_action "SUCCESS: $BACKUP_COUNT files, $TOTAL_SIZE bytes"
|
|
|
|
# Auto-copy to host
|
|
if [[ "$AUTO_COPY" == "1" ]]; then
|
|
echo_info "Copying to host ./backups/"
|
|
mkdir -p ./backups
|
|
|
|
BACKEND_CONTAINER=$(dc_cmd ps -q backend)
|
|
if docker cp "${BACKEND_CONTAINER}:${BACKUP_PATH}/." ./backups/ 2>/dev/null; then
|
|
echo_info "✓ Copied to ./backups/"
|
|
|
|
# Encrypt if requested
|
|
if [[ "$ENCRYPT" == "1" ]]; then
|
|
echo_info "Encrypting backups..."
|
|
TIMESTAMP_PREFIX="${BACKUP_TIMESTAMP:0:12}"
|
|
RECENT_BACKUPS=$(find ./backups -type f -mmin -1 -name "${TIMESTAMP_PREFIX}*" ! -name "*.gpg" 2>/dev/null || true)
|
|
|
|
if [[ -n "$RECENT_BACKUPS" ]]; then
|
|
ENCRYPTED_COUNT=0
|
|
while IFS= read -r backup_file; do
|
|
[[ ! -f "$backup_file" ]] && continue
|
|
|
|
if echo "$BACKUP_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
|
--symmetric --cipher-algo AES256 -o "${backup_file}.gpg" "$backup_file" 2>/dev/null; then
|
|
|
|
[[ -f "${backup_file}.gpg" ]] && rm -f "$backup_file" && ((ENCRYPTED_COUNT++))
|
|
echo_info " ✓ Encrypted: $(basename "${backup_file}.gpg")"
|
|
else
|
|
echo_warn " ✗ Encryption failed: $(basename "$backup_file")"
|
|
fi
|
|
done <<< "$RECENT_BACKUPS"
|
|
|
|
echo_info "✓ Encrypted $ENCRYPTED_COUNT file(s)"
|
|
log_action "SUCCESS: Encrypted $ENCRYPTED_COUNT files"
|
|
fi
|
|
fi
|
|
|
|
# Show latest files
|
|
echo_info "Latest in ./backups/:"
|
|
ls -lht ./backups/ | head -n 6 || true
|
|
else
|
|
echo_warn "Copy failed. Files remain in container."
|
|
fi
|
|
else
|
|
echo_info "Backup location: ${BACKUP_PATH}/"
|
|
echo_info "To copy: docker cp \$(docker compose -p $PROJECT_NAME ps -q backend):${BACKUP_PATH}/. ./backups/"
|
|
fi
|
|
|
|
# Cleanup old backups
|
|
if [[ "$CLEANUP_OLD" == "1" ]] && [[ "$BACKUP_RETENTION_DAYS" -gt 0 ]]; then
|
|
echo_info "Cleaning backups older than $BACKUP_RETENTION_DAYS days"
|
|
|
|
# Container cleanup
|
|
dc_exec find "$BACKUP_PATH" -type f -mtime +$BACKUP_RETENTION_DAYS -delete 2>/dev/null && \
|
|
echo_info "✓ Container cleanup done" || echo_warn "Container cleanup failed"
|
|
|
|
# Host cleanup
|
|
if [[ -d "./backups" ]]; then
|
|
DELETED=$(find "./backups" -type f -mtime +$BACKUP_RETENTION_DAYS 2>/dev/null | wc -l)
|
|
[[ "$DELETED" -gt 0 ]] && find "./backups" -type f -mtime +$BACKUP_RETENTION_DAYS -delete 2>/dev/null
|
|
echo_info "✓ Host cleanup: removed $DELETED file(s)"
|
|
fi
|
|
fi
|
|
|
|
echo_info "✓ Backup completed successfully!" |