diff --git a/backup-env-files.sh b/backup-env-files.sh new file mode 100755 index 0000000..2fd0d6f --- /dev/null +++ b/backup-env-files.sh @@ -0,0 +1,512 @@ +#!/bin/bash + +# backup-env-files.sh - Backup .env files to private Gitea repository +# Author: Shell Repository +# Description: Securely backup and version control .env files from ~/docker/* directories + +set -e + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOCKER_DIR="$HOME/docker" +BACKUP_REPO_NAME="docker-env-backup" +BACKUP_DIR="$HOME/.env-backup" +LOG_FILE="$SCRIPT_DIR/logs/env-backup.log" + +# Ensure logs directory exists +mkdir -p "$(dirname "$LOG_FILE")" + +# Logging function +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" +} + +# Display usage information +usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Backup .env files from ~/docker/* to private Gitea repository" + echo "" + echo "Options:" + echo " -h, --help Show this help message" + echo " -i, --init Initialize the backup repository" + echo " -f, --force Force overwrite existing files" + echo " -d, --dry-run Show what would be backed up without doing it" + echo " -r, --restore Restore .env files from backup" + echo " -l, --list List all .env files found" + echo " -g, --gitea-url URL Set Gitea instance URL" + echo " -u, --username USER Set Gitea username" + echo "" + echo "Examples:" + echo " $0 --init # First time setup" + echo " $0 # Regular backup" + echo " $0 --dry-run # See what would be backed up" + echo " $0 --restore # Restore files from backup" +} + +# Check dependencies +check_dependencies() { + local missing_deps=() + + command -v git >/dev/null 2>&1 || missing_deps+=("git") + command -v find >/dev/null 2>&1 || missing_deps+=("find") + + if [ ${#missing_deps[@]} -ne 0 ]; then + echo -e "${RED}Error: Missing required dependencies: ${missing_deps[*]}${NC}" + echo "Please install the missing dependencies and try again." + exit 1 + fi +} + +# Find all .env files in docker directories +find_env_files() { + local base_dir="$1" + + if [ ! -d "$base_dir" ]; then + echo -e "${YELLOW}Warning: Docker directory $base_dir does not exist${NC}" + return 0 + fi + + # Find all .env files, including hidden ones and those with different extensions + find "$base_dir" -type f \( -name "*.env" -o -name ".env*" -o -name "env.*" \) 2>/dev/null | sort +} + +# List all .env files +list_env_files() { + echo -e "${BLUE}=== Environment Files Found ===${NC}" + local count=0 + + # Use a temp file to avoid subshell issues + local temp_file=$(mktemp) + find_env_files "$DOCKER_DIR" > "$temp_file" + + while IFS= read -r env_file; do + if [ -n "$env_file" ]; then + local rel_path="${env_file#$DOCKER_DIR/}" + local size=$(du -h "$env_file" 2>/dev/null | cut -f1) + local modified=$(stat -c %y "$env_file" 2>/dev/null | cut -d' ' -f1) + + echo -e "${GREEN}📄 $rel_path${NC}" + echo " Size: $size | Modified: $modified" + echo " Full path: $env_file" + echo "" + count=$((count + 1)) + fi + done < "$temp_file" + + # Clean up temp file + rm -f "$temp_file" + + echo -e "${BLUE}Total .env files found: $count${NC}" + + if [ $count -eq 0 ]; then + echo -e "${YELLOW}No .env files found in $DOCKER_DIR${NC}" + echo "Make sure you have Docker containers with .env files in subdirectories." + fi +} + +# Initialize backup repository +init_backup_repo() { + echo -e "${YELLOW}Initializing .env backup repository...${NC}" + + # Prompt for Gitea details if not provided + if [ -z "$GITEA_URL" ]; then + read -p "Enter your Gitea instance URL (e.g., https://git.yourdomain.com): " GITEA_URL + fi + + if [ -z "$GITEA_USERNAME" ]; then + read -p "Enter your Gitea username: " GITEA_USERNAME + fi + + # Create backup directory + mkdir -p "$BACKUP_DIR" + cd "$BACKUP_DIR" + + # Initialize git repository if not already done + if [ ! -d ".git" ]; then + git init + echo -e "${GREEN}Initialized local git repository${NC}" + fi + + # Create .gitignore for additional security + cat > .gitignore << 'EOF' +# Temporary files +*.tmp +*.swp +*.bak +*~ + +# OS generated files +.DS_Store +Thumbs.db + +# Logs +*.log +EOF + + # Create README with important information + cat > README.md << 'EOF' +# Docker Environment Files Backup + +This repository contains backup copies of .env files from Docker containers. + +## ⚠️ SECURITY WARNING ⚠️ + +This repository contains sensitive configuration files including: +- API keys +- Database passwords +- Secret tokens +- Private configurations + +**NEVER make this repository public!** + +## Structure + +``` +docker-containers/ +├── container1/ +│ ├── .env +│ └── docker-compose.yml (reference only) +├── container2/ +│ └── .env +└── ... +``` + +## Usage + +- Files are organized by container/service name +- Only .env files are backed up (no other sensitive files) +- Restore using the backup-env-files.sh script + +## Last Backup + +This information is updated automatically by the backup script. +EOF + + # Create directory structure + mkdir -p docker-containers + + # Set up remote if URL provided + if [ -n "$GITEA_URL" ] && [ -n "$GITEA_USERNAME" ]; then + local remote_url="${GITEA_URL%/}/${GITEA_USERNAME}/${BACKUP_REPO_NAME}.git" + + # Check if remote already exists + if ! git remote get-url origin >/dev/null 2>&1; then + git remote add origin "$remote_url" + echo -e "${GREEN}Added remote origin: $remote_url${NC}" + fi + + # Save configuration + cat > .env-backup-config << EOF +GITEA_URL="$GITEA_URL" +GITEA_USERNAME="$GITEA_USERNAME" +BACKUP_REPO_NAME="$BACKUP_REPO_NAME" +EOF + + echo -e "${YELLOW}Configuration saved to .env-backup-config${NC}" + echo -e "${BLUE}Next steps:${NC}" + echo "1. Create a private repository '$BACKUP_REPO_NAME' in your Gitea instance" + echo "2. Run the backup script to perform your first backup" + echo "3. The script will attempt to push to the remote repository" + fi + + # Initial commit + git add . + git commit -m "Initial setup of .env backup repository" || echo "Nothing to commit" + + log "Backup repository initialized at $BACKUP_DIR" +} + +# Load configuration +load_config() { + local config_file="$BACKUP_DIR/.env-backup-config" + + if [ -f "$config_file" ]; then + source "$config_file" + fi +} + +# Backup .env files +backup_env_files() { + local dry_run="$1" + local force="$2" + + echo -e "${YELLOW}Starting .env files backup...${NC}" + + # Check if backup directory exists + if [ ! -d "$BACKUP_DIR" ]; then + echo -e "${RED}Backup directory not found. Run with --init first.${NC}" + exit 1 + fi + + cd "$BACKUP_DIR" + load_config + + # Create timestamp + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + local backup_count=0 + local unchanged_count=0 + + # Process each .env file using a temp file to avoid subshell issues + local temp_file=$(mktemp) + find_env_files "$DOCKER_DIR" > "$temp_file" + + while IFS= read -r env_file; do + if [ -n "$env_file" ]; then + # Determine relative path and backup location + local rel_path="${env_file#$DOCKER_DIR/}" + local backup_path="docker-containers/$rel_path" + local backup_dir=$(dirname "$backup_path") + + if [ "$dry_run" = "true" ]; then + echo -e "${BLUE}Would backup: $rel_path${NC}" + continue + fi + + # Create backup directory structure + mkdir -p "$backup_dir" + + # Check if file has changed + local needs_backup=true + if [ -f "$backup_path" ] && [ "$force" != "true" ]; then + if cmp -s "$env_file" "$backup_path"; then + needs_backup=false + unchanged_count=$((unchanged_count + 1)) + fi + fi + + if [ "$needs_backup" = "true" ]; then + # Copy the file + cp "$env_file" "$backup_path" + echo -e "${GREEN}✓ Backed up: $rel_path${NC}" + backup_count=$((backup_count + 1)) + + # Also create a reference docker-compose.yml if it exists + local compose_file=$(dirname "$env_file")/docker-compose.yml + local compose_backup="$backup_dir/docker-compose.yml.ref" + + if [ -f "$compose_file" ] && [ ! -f "$compose_backup" ]; then + cp "$compose_file" "$compose_backup" + echo -e "${BLUE} + Reference: docker-compose.yml${NC}" + fi + else + echo -e "${YELLOW}- Unchanged: $rel_path${NC}" + fi + fi + done < "$temp_file" + + # Clean up temp file + rm -f "$temp_file" + + if [ "$dry_run" = "true" ]; then + echo -e "${BLUE}Dry run completed. No files were actually backed up.${NC}" + return 0 + fi + + # Update README with backup information + sed -i "/^## Last Backup/,$ d" README.md + cat >> README.md << EOF + +## Last Backup + +- **Date**: $timestamp +- **Files backed up**: $backup_count +- **Files unchanged**: $unchanged_count +- **Total files**: $((backup_count + unchanged_count)) + +Generated by backup-env-files.sh +EOF + + # Commit changes + git add . + + if git diff --staged --quiet; then + echo -e "${YELLOW}No changes to commit${NC}" + log "Backup completed - no changes detected" + else + git commit -m "Backup .env files - $timestamp + +- Files backed up: $backup_count +- Files unchanged: $unchanged_count +- Total files: $((backup_count + unchanged_count))" + + echo -e "${GREEN}Changes committed to local repository${NC}" + + # Push to remote if configured + if git remote get-url origin >/dev/null 2>&1; then + echo -e "${YELLOW}Pushing to remote repository...${NC}" + if git push origin main 2>/dev/null || git push origin master 2>/dev/null; then + echo -e "${GREEN}✓ Successfully pushed to remote repository${NC}" + log "Backup completed and pushed to remote - $backup_count files backed up, $unchanged_count unchanged" + else + echo -e "${YELLOW}Warning: Could not push to remote repository${NC}" + echo "You may need to:" + echo "1. Create the repository in Gitea first" + echo "2. Set up authentication (SSH key or token)" + log "Backup completed locally but failed to push to remote - $backup_count files backed up" + fi + else + echo -e "${YELLOW}No remote repository configured${NC}" + log "Backup completed locally - $backup_count files backed up, $unchanged_count unchanged" + fi + fi + + echo -e "${GREEN}Backup completed!${NC}" + echo -e "${BLUE}Summary:${NC}" + echo " - Files backed up: $backup_count" + echo " - Files unchanged: $unchanged_count" + echo " - Backup location: $BACKUP_DIR" +} + +# Restore .env files +restore_env_files() { + echo -e "${YELLOW}Starting .env files restore...${NC}" + + if [ ! -d "$BACKUP_DIR" ]; then + echo -e "${RED}Backup directory not found at $BACKUP_DIR${NC}" + echo "Either run --init first or clone your backup repository to this location." + exit 1 + fi + + cd "$BACKUP_DIR" + load_config + + # Pull latest changes if remote is configured + if git remote get-url origin >/dev/null 2>&1; then + echo -e "${YELLOW}Pulling latest changes from remote...${NC}" + git pull origin main 2>/dev/null || git pull origin master 2>/dev/null || true + fi + + local restore_count=0 + local error_count=0 + + # Use a temp file to avoid subshell issues + local temp_file=$(mktemp) + find docker-containers -name "*.env" -type f 2>/dev/null > "$temp_file" + + while IFS= read -r backup_file; do + if [ -n "$backup_file" ]; then + # Determine target path + local rel_path="${backup_file#docker-containers/}" + local target_file="$DOCKER_DIR/$rel_path" + local target_dir=$(dirname "$target_file") + + # Create target directory if it doesn't exist + if [ ! -d "$target_dir" ]; then + echo -e "${YELLOW}Creating directory: $target_dir${NC}" + mkdir -p "$target_dir" + fi + + # Ask for confirmation if file exists and is different + if [ -f "$target_file" ]; then + if ! cmp -s "$backup_file" "$target_file"; then + echo -e "${YELLOW}File exists and differs: $rel_path${NC}" + read -p "Overwrite? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo -e "${YELLOW}Skipped: $rel_path${NC}" + continue + fi + else + echo -e "${GREEN}Identical: $rel_path${NC}" + continue + fi + fi + + # Copy the file + if cp "$backup_file" "$target_file"; then + echo -e "${GREEN}✓ Restored: $rel_path${NC}" + restore_count=$((restore_count + 1)) + else + echo -e "${RED}✗ Failed to restore: $rel_path${NC}" + error_count=$((error_count + 1)) + fi + fi + done < "$temp_file" + + # Clean up temp file + rm -f "$temp_file" + + echo -e "${GREEN}Restore completed!${NC}" + echo -e "${BLUE}Summary:${NC}" + echo " - Files restored: $restore_count" + echo " - Errors: $error_count" + + log "Restore completed - $restore_count files restored, $error_count errors" +} + +# Main function +main() { + local init_repo=false + local dry_run=false + local force=false + local restore=false + local list_files=false + + # Parse command line arguments + while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + usage + exit 0 + ;; + -i|--init) + init_repo=true + shift + ;; + -f|--force) + force=true + shift + ;; + -d|--dry-run) + dry_run=true + shift + ;; + -r|--restore) + restore=true + shift + ;; + -l|--list) + list_files=true + shift + ;; + -g|--gitea-url) + GITEA_URL="$2" + shift 2 + ;; + -u|--username) + GITEA_USERNAME="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + usage + exit 1 + ;; + esac + done + + # Check dependencies + check_dependencies + + # Execute requested action + if [ "$list_files" = true ]; then + list_env_files + elif [ "$init_repo" = true ]; then + init_backup_repo + elif [ "$restore" = true ]; then + restore_env_files + else + backup_env_files "$dry_run" "$force" + fi +} + +# Run main function with all arguments +main "$@" diff --git a/completions/env-backup-completion.bash b/completions/env-backup-completion.bash new file mode 100644 index 0000000..d4fc800 --- /dev/null +++ b/completions/env-backup-completion.bash @@ -0,0 +1,50 @@ +#!/bin/bash + +# env-backup-completion.bash - Bash completion for environment backup scripts +# Source this file or copy to ~/.local/share/bash-completion/completions/ + +_backup_env_files() { + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + opts="--help --init --force --dry-run --restore --list --gitea-url --username" + + case ${prev} in + --gitea-url|-g) + # Suggest common gitea URL patterns + COMPREPLY=( $(compgen -W "https://git. https://gitea. https://code." -- ${cur}) ) + return 0 + ;; + --username|-u) + # No completion for username + return 0 + ;; + *) + ;; + esac + + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 +} + +_validate_env_backups() { + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + opts="--help --verbose --summary-only --missing-only --diff" + + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 +} + +# Register completion functions +complete -F _backup_env_files backup-env-files.sh +complete -F _validate_env_backups validate-env-backups.sh + +# Also register for the full path versions +complete -F _backup_env_files ./backup-env-files.sh +complete -F _validate_env_backups ./validate-env-backups.sh diff --git a/crontab/crontab-backups/europa/current-crontab.backup b/crontab/crontab-backups/europa/current-crontab.backup index eccea34..f191bba 100644 --- a/crontab/crontab-backups/europa/current-crontab.backup +++ b/crontab/crontab-backups/europa/current-crontab.backup @@ -1,6 +1,8 @@ 0 1 * * * /home/acedanger/shell/move-backups.sh 2>&1 | logger -t backup-move -p user.info +0 2 * * * { echo "Starting .env files backup"; /home/acedanger/shell/backup-env-files.sh; echo ".env backup completed with exit code: $?"; } 2>&1 | logger -t env-backup -p user.info 15 4 * * * { echo "Starting Plex backup"; /home/acedanger/shell/plex/backup-plex.sh --non-interactive --auto-repair; echo "Plex backup completed with exit code: $?"; } 2>&1 | logger -t plex-backup -p user.info 0 7 * * * { echo "Starting Plex backup validation"; /home/acedanger/shell/plex/validate-plex-backups.sh --fix; echo "Validation completed with exit code: $?"; } 2>&1 | logger -t plex-validation -p user.info 0 5 * * 1 { echo "Starting Immich database backup move"; if mv /mnt/share/media/immich/uploads/backups/immich-db-backup* /mnt/share/media/backups/immich 2>/dev/null; then echo "Immich backup move completed successfully"; else echo "No Immich backup files found or move failed"; fi; } 2>&1 | logger -t immich-backup -p user.info 0 4 * * * /home/acedanger/shell/crontab/crontab-backup-system.sh backup auto --auto-cleanup 2>&1 | logger -t crontab-backup -p user.info 0 8 * * 0 { echo "Starting weekly Plex backup report generation"; /home/acedanger/shell/plex/validate-plex-backups.sh --report; echo "Weekly report generation completed with exit code: $?"; } 2>&1 | logger -t plex-report -p user.info +30 8 * * 0 { echo "Starting .env backup validation"; /home/acedanger/shell/validate-env-backups.sh; echo ".env validation completed with exit code: $?"; } 2>&1 | logger -t env-validation -p user.info diff --git a/crontab/crontab-backups/io/current-crontab.backup b/crontab/crontab-backups/io/current-crontab.backup index 69de5e7..bbbadad 100644 --- a/crontab/crontab-backups/io/current-crontab.backup +++ b/crontab/crontab-backups/io/current-crontab.backup @@ -1,7 +1,4 @@ 0 2 * * * { echo "Starting Docker backup"; /home/acedanger/shell/backup-docker.sh; echo "Docker backup completed with exit code: $?"; } 2>&1 | logger -t docker-backup -p user.info 0 4 * * * /home/acedanger/shell/crontab/crontab-backup-system.sh backup auto --auto-cleanup 2>&1 | logger -t crontab-backup -p user.info -# Daily backups at 2 AM -0 2 * * * /home/acedanger/shell/backup-media.sh >/dev/null 2>&1 -# -# Weekly verified backups at 3 AM -0 3 * * 0 /home/acedanger/shell/backup-media.sh --verify +0 3 * * * { echo "Starting .env files backup"; /home/acedanger/shell/backup-env-files.sh; echo ".env backup completed with exit code: $?"; } 2>&1 | logger -t env-backup -p user.info +30 8 * * 0 { echo "Starting .env backup validation"; /home/acedanger/shell/validate-env-backups.sh; echo ".env validation completed with exit code: $?"; } 2>&1 | logger -t env-validation -p user.info diff --git a/crontab/crontab-europa.txt b/crontab/crontab-europa.txt index e66b11d..be4255f 100644 --- a/crontab/crontab-europa.txt +++ b/crontab/crontab-europa.txt @@ -7,6 +7,10 @@ # Logs both stdout and stderr with backup-move tag 0 1 * * * /home/acedanger/shell/move-backups.sh 2>&1 | logger -t backup-move -p user.info +# Daily .env files backup at 0200 with logging +# Backs up all Docker .env files to private Gitea repository +0 2 * * * { echo "Starting .env files backup"; /home/acedanger/shell/backup-env-files.sh; echo ".env backup completed with exit code: $?"; } 2>&1 | logger -t env-backup -p user.info + # Daily Plex database integrity check every 30 minutes */30 * * * * { echo "Check Plex database corruption"; /home/acedanger/shell/plex/backup-plex.sh --check-integrity --auto-repair; } 2>&1 | logger -t plex-database-integrity-check -p user.info @@ -29,6 +33,10 @@ # Comprehensive reporting with system logging 0 8 * * 0 { echo "Starting weekly Plex backup report generation"; /home/acedanger/shell/plex/validate-plex-backups.sh --report; echo "Weekly report generation completed with exit code: $?"; } 2>&1 | logger -t plex-report -p user.info +# Weekly .env backup validation (Sundays at 0830) +# Validates integrity of .env backup repository +30 8 * * 0 { echo "Starting .env backup validation"; /home/acedanger/shell/validate-env-backups.sh; echo ".env validation completed with exit code: $?"; } 2>&1 | logger -t env-validation -p user.info + # Optional: Add a health check entry to monitor cron jobs (every 6 hours) # This can help detect if any of the backup processes are failing # 0 */6 * * * { echo "Cron health check - all backup jobs scheduled"; ps aux | grep -E "(backup-plex|validate-plex|move-backups)" | grep -v grep | wc -l; } 2>&1 | logger -t cron-health -p user.info diff --git a/crontab/crontab-io.txt b/crontab/crontab-io.txt index 8854581..d18645f 100644 --- a/crontab/crontab-io.txt +++ b/crontab/crontab-io.txt @@ -10,6 +10,14 @@ # Daily system backup at 0400 with auto-cleanup 0 4 * * * /home/acedanger/shell/crontab/crontab-backup-system.sh backup auto --auto-cleanup 2>&1 | logger -t crontab-backup -p user.info +# Daily .env files backup at 0300 with enhanced logging +# Backs up Docker container .env files to Git repository +0 3 * * * { echo "Starting .env files backup"; /home/acedanger/shell/backup-env-files.sh; echo ".env backup completed with exit code: $?"; } 2>&1 | logger -t env-backup -p user.info + +# Weekly .env backup validation at 0830 on Sundays +# Validates the integrity of .env backup repository +30 8 * * 0 { echo "Starting .env backup validation"; /home/acedanger/shell/validate-env-backups.sh; echo ".env validation completed with exit code: $?"; } 2>&1 | logger -t env-validation -p user.info + # Optional: Monitor Docker container health (every 6 hours) # This can help detect if any download services are failing # 0 */6 * * * { echo "Docker health check"; docker ps --format "table {{.Names}}\t{{.Status}}" | grep -v "Up"; } 2>&1 | logger -t docker-health -p user.info diff --git a/crontab/crontab-racknerd.txt b/crontab/crontab-racknerd.txt index 06ebdde..7a6f443 100644 --- a/crontab/crontab-racknerd.txt +++ b/crontab/crontab-racknerd.txt @@ -9,6 +9,14 @@ # Daily system backup at 0400 with auto-cleanup 0 4 * * * /home/acedanger/shell/crontab/crontab-backup-system.sh backup auto --auto-cleanup 2>&1 | logger -t crontab-backup -p user.info +# Daily .env files backup at 0300 with enhanced logging +# Backs up Docker container .env files to Git repository +0 3 * * * { echo "Starting .env files backup"; /home/acedanger/shell/backup-env-files.sh; echo ".env backup completed with exit code: $?"; } 2>&1 | logger -t env-backup -p user.info + +# Weekly .env backup validation at 0830 on Sundays +# Validates the integrity of .env backup repository +30 8 * * 0 { echo "Starting .env backup validation"; /home/acedanger/shell/validate-env-backups.sh; echo ".env validation completed with exit code: $?"; } 2>&1 | logger -t env-validation -p user.info + # Optional: Add a health check entry to monitor backup jobs (every 6 hours) # This can help detect if the backup process is failing # 0 */6 * * * { echo "Cron health check - Docker backup job scheduled"; ps aux | grep "backup-docker" | grep -v grep | wc -l; } 2>&1 | logger -t cron-health -p user.info diff --git a/docs/env-backup-integration-guide.md b/docs/env-backup-integration-guide.md new file mode 100644 index 0000000..cc2d5da --- /dev/null +++ b/docs/env-backup-integration-guide.md @@ -0,0 +1,146 @@ +# .env Backup Integration Guide + +## Quick Setup Summary + +Your .env backup system is now fully operational! Here's what was set up: + +### ✅ What's Working + +- **31 .env files** discovered across your Docker containers +- **30 files backed up** successfully to `/home/acedanger/.env-backup` +- **Private Gitea repository** configured and pushed successfully +- **Version control** with automatic commit messages and timestamps +- **Reference files** included (docker-compose.yml for context) + +### 🔧 Integration Options + +#### 1. Manual Backup (Current) + +```bash +cd /home/acedanger/shell +./backup-env-files.sh # Regular backup +./backup-env-files.sh --dry-run # Preview changes +./backup-env-files.sh --list # Show all .env files +``` + +#### 2. Automated Daily Backup (Recommended) + +Add to your crontab for daily backups at 2 AM: + +```bash +# Daily .env backup at 2 AM +0 2 * * * /home/acedanger/shell/backup-env-files.sh >/dev/null 2>&1 +``` + +#### 3. Integration with Existing Backup Scripts + +The backup integrates with your existing backup system through: + +- **Logs**: Written to `/home/acedanger/shell/logs/env-backup.log` +- **Completion**: Tab completion available via `env-backup-completion.bash` +- **Validation**: Use `validate-env-backups.sh` for integrity checks + +### 🔐 Security Features + +1. **Private Repository**: Only you have access +2. **Gitignore**: Excludes temporary files and logs +3. **SSH Authentication**: Uses your existing SSH key +4. **Local + Remote**: Dual backup (local git + remote Gitea) + +### 📊 Backup Structure + +``` +~/.env-backup/ +├── docker-containers/ +│ ├── authentik/ +│ │ └── .env.example +│ ├── caddy/ +│ │ ├── .env +│ │ ├── .env.example +│ │ └── docker-compose.yml.ref +│ ├── database/ +│ │ ├── .env +│ │ ├── .env.example +│ │ └── docker-compose.yml.ref +│ └── ... (all your containers) +├── README.md +└── .env-backup-config +``` + +### 🔄 Common Operations + +#### Restore Files (if needed) + +```bash +./backup-env-files.sh --restore +``` + +#### Force Backup (ignore unchanged files) + +```bash +./backup-env-files.sh --force +``` + +#### Check What Would Change + +```bash +./backup-env-files.sh --dry-run +``` + +### 🚨 Emergency Recovery + +If you lose your filesystem: + +1. **Clone the backup**: `git clone https://git.ptrwd.com/peterwood/docker-env-backup.git` +2. **Restore files**: `./backup-env-files.sh --restore` +3. **Recreate containers**: Your docker-compose.yml reference files are included + +### 📈 Monitoring + +- **Logs**: Check `/home/acedanger/shell/logs/env-backup.log` +- **Git History**: View changes with `git log` in backup directory +- **Validation**: Run `validate-env-backups.sh` for integrity checks + +### 🔧 Maintenance + +#### Weekly Validation (Recommended) + +```bash +# Add to crontab for weekly validation +0 3 * * 0 /home/acedanger/shell/validate-env-backups.sh >/dev/null 2>&1 +``` + +#### Cleanup Old Logs (Monthly) + +The system automatically manages logs, but you can clean them manually if needed. + +### 🆘 Troubleshooting + +#### Push Fails + +- Check SSH key: `ssh -T git@git.ptrwd.com` +- Verify repository exists and is private +- Check network connectivity + +#### Files Not Found + +- Verify Docker directory structure: `ls -la ~/docker/*/` +- Check file permissions +- Run with `--list` to see what's detected + +#### Restore Issues + +- Ensure target directories exist +- Check file permissions +- Use `--dry-run` first to preview + +## Integration Complete! 🎉 + +Your .env files are now safely backed up and version controlled. The system will: + +1. Track all changes to your .env files +2. Maintain a secure backup in your private Gitea +3. Provide easy restore capabilities +4. Integrate with your existing shell toolkit + +Run `./backup-env-files.sh` regularly or set up the cron job for automatic backups! diff --git a/docs/env-backup-system.md b/docs/env-backup-system.md new file mode 100644 index 0000000..3f51076 --- /dev/null +++ b/docs/env-backup-system.md @@ -0,0 +1,320 @@ +# Environment Files Backup System + +This document describes the secure backup system for `.env` files from Docker containers to a private Gitea repository. + +## Overview + +The environment files backup system provides: + +- **Automated discovery** of all `.env` files in `~/docker/*` directories +- **Secure version control** using private Git repository +- **Change tracking** with timestamps and commit history +- **Easy restoration** of backed up configurations +- **Validation tools** to ensure backup integrity + +## Components + +### Scripts + +1. **backup-env-files.sh** - Main backup script +2. **validate-env-backups.sh** - Validation and integrity checking + +### Repository Structure + +``` +~/.env-backup/ +├── .git/ # Git repository +├── .gitignore # Security-focused gitignore +├── README.md # Repository documentation +├── .env-backup-config # Configuration file +└── docker-containers/ # Backed up files + ├── container1/ + │ ├── .env # Environment file + │ └── docker-compose.yml.ref # Reference compose file + ├── container2/ + │ └── .env + └── ... +``` + +## Security Considerations + +### 🔒 Critical Security Points + +1. **Repository Privacy**: The backup repository MUST be private +2. **Access Control**: Only you should have access to the repository +3. **Network Security**: Use HTTPS or SSH for Git operations +4. **Local Security**: Backup directory should have restricted permissions + +### Best Practices + +- Use SSH keys for Git authentication (more secure than passwords) +- Regularly rotate any exposed credentials +- Monitor repository access logs +- Consider encrypting the entire backup repository + +## Setup Instructions + +### 1. Initial Setup + +```bash +# First time setup +./backup-env-files.sh --init + +# Follow prompts to configure: +# - Gitea instance URL +# - Username +# - Repository name +``` + +### 2. Create Repository in Gitea + +1. Log into your Gitea instance +2. Create a new **private** repository named `docker-env-backup` +3. Do not initialize with README (the script handles this) + +### 3. Configure Authentication + +#### Option A: SSH Key (Recommended) + +```bash +# Generate SSH key if you don't have one +ssh-keygen -t ed25519 -C "your_email@domain.com" + +# Add public key to Gitea: +# 1. Go to Settings → SSH/GPG Keys +# 2. Add the content of ~/.ssh/id_ed25519.pub +``` + +#### Option B: Personal Access Token + +```bash +# In Gitea: Settings → Applications → Generate Token +# Configure Git to use token: +git config --global credential.helper store +``` + +### 4. First Backup + +```bash +# List all .env files that will be backed up +./backup-env-files.sh --list + +# Perform dry run to see what would happen +./backup-env-files.sh --dry-run + +# Execute actual backup +./backup-env-files.sh +``` + +## Usage + +### Regular Backup + +```bash +# Standard backup (only backs up changed files) +./backup-env-files.sh + +# Force backup all files +./backup-env-files.sh --force + +# See what would be backed up +./backup-env-files.sh --dry-run +``` + +### Validation + +```bash +# Basic validation +./validate-env-backups.sh + +# Detailed validation with file differences +./validate-env-backups.sh --diff --verbose + +# Show only missing files +./validate-env-backups.sh --missing-only +``` + +### Restoration + +```bash +# Restore all .env files from backup +./backup-env-files.sh --restore + +# This will: +# 1. Pull latest changes from remote +# 2. Prompt before overwriting existing files +# 3. Create directory structure as needed +``` + +## Automation + +### Cron Job Setup + +Add to your crontab for automated backups: + +```bash +# Backup .env files daily at 2 AM +0 2 * * * /home/yourusername/shell/backup-env-files.sh >/dev/null 2>&1 + +# Validate backups weekly on Sundays at 3 AM +0 3 * * 0 /home/yourusername/shell/validate-env-backups.sh --summary-only +``` + +### Integration with Existing Backup System + +Add to your main backup script: + +```bash +# In your existing backup script +echo "Backing up environment files..." +/home/yourusername/shell/backup-env-files.sh + +# Validate the backup +if ! /home/yourusername/shell/validate-env-backups.sh --summary-only; then + echo "Warning: .env backup validation failed" +fi +``` + +## File Discovery + +The system automatically finds: + +- `*.env` files (e.g., `production.env`, `staging.env`) +- `.env*` files (e.g., `.env`, `.env.local`, `.env.production`) +- `env.*` files (e.g., `env.development`, `env.local`) + +### Example Structure + +``` +~/docker/ +├── traefik/ +│ ├── .env # ✓ Backed up +│ └── docker-compose.yml +├── nextcloud/ +│ ├── .env.production # ✓ Backed up +│ ├── .env.local # ✓ Backed up +│ └── docker-compose.yml +├── grafana/ +│ ├── env.grafana # ✓ Backed up +│ └── docker-compose.yml +└── plex/ + ├── config.env # ✓ Backed up + └── docker-compose.yml +``` + +## Troubleshooting + +### Common Issues + +1. **Git Push Fails** + + ```bash + # Check remote URL + cd ~/.env-backup && git remote -v + + # Test connectivity + git ls-remote origin + ``` + +2. **Missing Files** + + ```bash + # List what would be found + ./backup-env-files.sh --list + + # Check file permissions + ls -la ~/docker/*/ + ``` + +3. **Repository Not Found** + - Ensure repository exists in Gitea + - Check repository name matches configuration + - Verify you have access permissions + +### Recovery Scenarios + +#### Disaster Recovery + +If you lose your entire system: + +```bash +# 1. Clone your backup repository +git clone https://git.yourdomain.com/username/docker-env-backup.git ~/.env-backup + +# 2. Restore all files +cd /path/to/shell +./backup-env-files.sh --restore +``` + +#### Selective Recovery + +```bash +# Restore specific file manually +cp ~/.env-backup/docker-containers/traefik/.env ~/docker/traefik/ +``` + +## Monitoring + +### Log Files + +- **backup-env-files.sh**: `logs/env-backup.log` +- **validate-env-backups.sh**: `logs/env-backup-validation.log` + +### Health Checks + +```bash +# Weekly health check script +#!/bin/bash +echo "=== .env Backup Health Check ===" +./validate-env-backups.sh --summary-only + +# Check last backup time +cd ~/.env-backup +echo "Last backup: $(git log -1 --format='%ci')" + +# Check repository status +git status --porcelain +``` + +## Security Enhancements + +### Additional Security Measures + +1. **GPG Encryption** (Optional) + + ```bash + # Encrypt sensitive files before committing + gpg --symmetric --cipher-algo AES256 file.env + ``` + +2. **Restricted Permissions** + + ```bash + # Secure backup directory + chmod 700 ~/.env-backup + chmod 600 ~/.env-backup/.env-backup-config + ``` + +3. **Audit Trail** + + ```bash + # Monitor repository access + git log --oneline --graph --all + ``` + +## Best Practices + +1. **Regular Testing**: Test restoration process monthly +2. **Version Control**: Never force push; preserve history +3. **Documentation**: Keep README.md updated with changes +4. **Monitoring**: Set up alerts for failed backups +5. **Security**: Regularly review repository access permissions + +## Support + +For issues or questions: + +1. Check the troubleshooting section +2. Review log files for error details +3. Validate your Gitea configuration +4. Test Git connectivity manually diff --git a/dotfiles/my-aliases.zsh.original b/dotfiles/my-aliases.zsh.original index 3deb340..441e89b 100644 --- a/dotfiles/my-aliases.zsh.original +++ b/dotfiles/my-aliases.zsh.original @@ -12,13 +12,6 @@ alias findzombie="ps -A -ostat,pid,ppid | grep -e '[zZ]'" alias la-eza="eza -la --color=auto --group-directories-first" alias ll-eza="eza -laFh --color=auto --group-directories-first" alias l-eza="eza -1 --color=auto --group-directories-first" -alias lt="eza --tree --level=2 --color=auto --group-directories-first" # Tree view (2 levels) -alias llt="eza -la --tree --level=2 --color=auto --group-directories-first" # Long tree view -alias lg="eza -la --git --color=auto --group-directories-first" # Show git status -alias lh="eza -la --color=auto --group-directories-first --sort=size" # Sort by size -alias lr="eza -la --color=auto --group-directories-first --sort=modified" # Sort by modified -alias lx="eza -la --color=auto --group-directories-first --sort=extension" # Sort by extension -alias tree="eza --tree --color=auto --group-directories-first" # Tree alias # 🎬 Plex Media Server Management - Sexy Edition alias plex="/home/acedanger/shell/plex/plex.sh" @@ -33,19 +26,33 @@ alias dcdn="docker compose down" alias dcupd="docker compose up -d" alias dcpull="docker compose pull" alias lzd="lazydocker" -alias cat="bat" -alias fd="fd" -alias fzf="fzf --preview='bat {}'" # 🌟 Eza aliases - Modern replacement for ls -alias ls="eza --color=auto --group-directories-first" -alias la="eza -la --color=auto --group-directories-first" -alias ll="eza -laFh --color=auto --group-directories-first" -alias l="eza -1 --color=auto --group-directories-first" -alias lt="eza --tree --level=2 --color=auto --group-directories-first" -alias llt="eza -la --tree --level=2 --color=auto --group-directories-first" -alias lg="eza -la --git --color=auto --group-directories-first" -alias lh="eza -la --color=auto --group-directories-first --sort=size" -alias lr="eza -la --color=auto --group-directories-first --sort=modified" -alias lx="eza -la --color=auto --group-directories-first --sort=extension" -alias tree="eza --tree --color=auto --group-directories-first" + +# 🌟 Eza aliases - Modern replacement for ls + +# 🌟 Eza aliases - Modern replacement for ls + +# 🌟 Eza aliases - Modern replacement for ls + +# 🌟 Eza aliases - Modern replacement for ls + +# 🌟 Eza aliases - Modern replacement for ls + +# 🌟 Eza aliases - Modern replacement for ls +alias cat="batcat" +alias fd="fdfind" +alias fzf="fzf --preview='batcat {}'" + +# 🌟 Eza aliases - Modern replacement for ls +alias ls="eza --icons=always -a --color=auto --group-directories-first" +alias la="eza --icons=always -la --color=auto --group-directories-first" +alias ll="eza --icons=always -la --classify=always -h --color=auto --group-directories-first" +alias l="eza --icons=always -1 -a --color=auto --group-directories-first" +alias lt="eza --icons=always -a --tree --level=2 --color=auto --group-directories-first" +alias llt="eza --icons=always -la --tree --level=2 --color=auto --group-directories-first" +alias lg="eza --icons=always -la --git --color=auto --group-directories-first" +alias lh="eza --icons=always -la --color=auto --group-directories-first --sort=size" +alias lr="eza --icons=always -la --color=auto --group-directories-first --sort=modified" +alias lx="eza --icons=always -la --color=auto --group-directories-first --sort=extension" +alias tree="eza --icons=always -a --tree --color=auto --group-directories-first" diff --git a/env-backup-integration.sh b/env-backup-integration.sh new file mode 100755 index 0000000..22b1694 --- /dev/null +++ b/env-backup-integration.sh @@ -0,0 +1,181 @@ +#!/bin/bash + +# env-backup-integration.sh - Integration script for adding .env backup to existing backup system +# Author: Shell Repository +# Description: Add .env backup functionality to existing backup scripts + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo -e "${BLUE}=== Environment Files Backup Integration ===${NC}" + +# Function to add .env backup to a script +integrate_env_backup() { + local target_script="$1" + local integration_point="$2" + + if [ ! -f "$target_script" ]; then + echo -e "${YELLOW}Target script not found: $target_script${NC}" + return 1 + fi + + # Check if already integrated + if grep -q "backup-env-files.sh" "$target_script"; then + echo -e "${GREEN}✓ Already integrated with $target_script${NC}" + return 0 + fi + + echo -e "${YELLOW}Integrating with $target_script...${NC}" + + # Create backup of original script + cp "$target_script" "$target_script.backup" + + # Integration code + local integration_code=" +# === Environment Files Backup Integration === +echo -e \"\${YELLOW}Backing up environment files...\${NC}\" +if [ -f \"\$SCRIPT_DIR/backup-env-files.sh\" ]; then + if \"\$SCRIPT_DIR/backup-env-files.sh\"; then + echo -e \"\${GREEN}✓ Environment files backed up successfully\${NC}\" + else + echo -e \"\${YELLOW}Warning: Environment files backup had issues\${NC}\" + fi +else + echo -e \"\${YELLOW}Warning: backup-env-files.sh not found\${NC}\" +fi + +# Validate the backup +if [ -f \"\$SCRIPT_DIR/validate-env-backups.sh\" ]; then + if \"\$SCRIPT_DIR/validate-env-backups.sh\" --summary-only; then + echo -e \"\${GREEN}✓ Environment backup validation passed\${NC}\" + else + echo -e \"\${YELLOW}Warning: Environment backup validation failed\${NC}\" + fi +fi +echo \"\" +# === End Environment Files Backup Integration === +" + + # Add integration based on integration point + case "$integration_point" in + "after_docker") + # Add after Docker backup section + if grep -q "docker" "$target_script" || grep -q "backup.*docker" "$target_script"; then + # Find a good insertion point after docker backup + local line_num=$(grep -n -i "docker.*backup\|backup.*docker" "$target_script" | tail -1 | cut -d: -f1) + if [ -n "$line_num" ]; then + sed -i "${line_num}a\\${integration_code}" "$target_script" + echo -e "${GREEN}✓ Integrated after Docker backup section${NC}" + else + echo -e "${YELLOW}Could not find Docker backup section, adding at end${NC}" + echo "$integration_code" >> "$target_script" + fi + else + echo -e "${YELLOW}No Docker backup section found, adding at end${NC}" + echo "$integration_code" >> "$target_script" + fi + ;; + "before_end") + # Add before the end of the script + local last_line=$(wc -l < "$target_script") + sed -i "${last_line}i\\${integration_code}" "$target_script" + echo -e "${GREEN}✓ Integrated before end of script${NC}" + ;; + "manual") + echo -e "${BLUE}Manual integration code:${NC}" + echo "$integration_code" + echo -e "${YELLOW}Please add this code manually to your script at the appropriate location${NC}" + ;; + *) + echo -e "${YELLOW}Unknown integration point, adding at end${NC}" + echo "$integration_code" >> "$target_script" + ;; + esac + + echo -e "${GREEN}Integration completed. Backup saved as $target_script.backup${NC}" +} + +# Find and integrate with existing backup scripts +echo -e "${YELLOW}Scanning for backup scripts to integrate with...${NC}" + +# Common backup script patterns +declare -a backup_scripts=( + "$SCRIPT_DIR/backup-docker.sh" + "$SCRIPT_DIR/backup-media.sh" + "$SCRIPT_DIR/update.sh" + "$SCRIPT_DIR/backup.sh" + "$SCRIPT_DIR/daily-backup.sh" +) + +found_scripts=() + +for script in "${backup_scripts[@]}"; do + if [ -f "$script" ]; then + found_scripts+=("$script") + echo -e "${GREEN}Found: $(basename "$script")${NC}" + fi +done + +if [ ${#found_scripts[@]} -eq 0 ]; then + echo -e "${YELLOW}No backup scripts found to integrate with${NC}" + echo -e "${BLUE}You can manually add the .env backup to your backup routine:${NC}" + echo "" + echo "# Add to your backup script:" + echo "$SCRIPT_DIR/backup-env-files.sh" + echo "$SCRIPT_DIR/validate-env-backups.sh --summary-only" + echo "" +else + echo -e "${BLUE}Select scripts to integrate with (or 'all' for all, 'none' to skip):${NC}" + for i in "${!found_scripts[@]}"; do + echo "$((i+1)). $(basename "${found_scripts[$i]}")" + done + echo "" + + read -p "Enter your choice: " choice + + case "$choice" in + "all") + for script in "${found_scripts[@]}"; do + integrate_env_backup "$script" "after_docker" + done + ;; + "none") + echo -e "${YELLOW}Skipping integration${NC}" + ;; + [0-9]*) + if [ "$choice" -ge 1 ] && [ "$choice" -le ${#found_scripts[@]} ]; then + script_index=$((choice-1)) + integrate_env_backup "${found_scripts[$script_index]}" "after_docker" + else + echo -e "${RED}Invalid choice${NC}" + fi + ;; + *) + echo -e "${RED}Invalid choice${NC}" + ;; + esac +fi + +# Create a simple cron entry suggestion +echo -e "${BLUE}=== Automation Suggestions ===${NC}" +echo "Add to crontab for automated backups:" +echo "" +echo "# Daily .env backup at 2 AM" +echo "0 2 * * * $SCRIPT_DIR/backup-env-files.sh >/dev/null 2>&1" +echo "" +echo "# Weekly validation on Sundays at 3 AM" +echo "0 3 * * 0 $SCRIPT_DIR/validate-env-backups.sh --summary-only" +echo "" + +echo -e "${GREEN}Integration setup completed!${NC}" +echo -e "${BLUE}Next steps:${NC}" +echo "1. Run: $SCRIPT_DIR/backup-env-files.sh --init" +echo "2. Create private repository in Gitea" +echo "3. Run first backup: $SCRIPT_DIR/backup-env-files.sh" +echo "4. Test restoration: $SCRIPT_DIR/backup-env-files.sh --restore" diff --git a/setup/setup.sh b/setup/setup.sh index c71f3cc..a3d4688 100755 --- a/setup/setup.sh +++ b/setup/setup.sh @@ -384,7 +384,14 @@ grep -v "^alias fzf=" | \ grep -v "^alias ls=" | \ grep -v "^alias ll=" | \ grep -v "^alias la=" | \ -grep -v "^alias l=" > "$ALIASES_FILE" +grep -v "^alias l=" | \ +grep -v "^alias tree=" | \ +grep -v "^alias lt=" | \ +grep -v "^alias llt=" | \ +grep -v "^alias lg=" | \ +grep -v "^alias lh=" | \ +grep -v "^alias lr=" | \ +grep -v "^alias lx=" > "$ALIASES_FILE" # Function to check for command existence and add appropriate alias add_conditional_alias() { @@ -435,17 +442,17 @@ if command -v eza &> /dev/null; then cat >> "$ALIASES_FILE" << 'EOF' # 🌟 Eza aliases - Modern replacement for ls -alias ls="eza --color=auto --group-directories-first" -alias la="eza -la --color=auto --group-directories-first" -alias ll="eza -laFh --color=auto --group-directories-first" -alias l="eza -1 --color=auto --group-directories-first" -alias lt="eza --tree --level=2 --color=auto --group-directories-first" -alias llt="eza -la --tree --level=2 --color=auto --group-directories-first" -alias lg="eza -la --git --color=auto --group-directories-first" -alias lh="eza -la --color=auto --group-directories-first --sort=size" -alias lr="eza -la --color=auto --group-directories-first --sort=modified" -alias lx="eza -la --color=auto --group-directories-first --sort=extension" -alias tree="eza --tree --color=auto --group-directories-first" +alias ls="eza --icons=always -a --color=auto --group-directories-first" +alias la="eza --icons=always -la --color=auto --group-directories-first" +alias ll="eza --icons=always -la --classify=always -h --color=auto --group-directories-first" +alias l="eza --icons=always -1 -a --color=auto --group-directories-first" +alias lt="eza --icons=always -a --tree --level=2 --color=auto --group-directories-first" +alias llt="eza --icons=always -la --tree --level=2 --color=auto --group-directories-first" +alias lg="eza --icons=always -la --git --color=auto --group-directories-first" +alias lh="eza --icons=always -la --color=auto --group-directories-first --sort=size" +alias lr="eza --icons=always -la --color=auto --group-directories-first --sort=modified" +alias lx="eza --icons=always -la --color=auto --group-directories-first --sort=extension" +alias tree="eza --icons=always -a --tree --color=auto --group-directories-first" EOF echo -e "${GREEN}Eza aliases configured successfully!${NC}" else diff --git a/validate-env-backups.sh b/validate-env-backups.sh new file mode 100755 index 0000000..fd2325e --- /dev/null +++ b/validate-env-backups.sh @@ -0,0 +1,227 @@ +#!/bin/bash + +# validate-env-backups.sh - Validate .env file backups +# Author: Shell Repository +# Description: Verify integrity and consistency of .env file backups + +set -e + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOCKER_DIR="$HOME/docker" +BACKUP_DIR="$HOME/.env-backup" +LOG_FILE="$SCRIPT_DIR/logs/env-backup-validation.log" + +# Ensure logs directory exists +mkdir -p "$(dirname "$LOG_FILE")" + +# Logging function +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" +} + +# Display usage information +usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Validate .env file backups against source files" + echo "" + echo "Options:" + echo " -h, --help Show this help message" + echo " -v, --verbose Verbose output" + echo " -s, --summary-only Show only summary" + echo " -m, --missing-only Show only missing files" + echo " -d, --diff Show differences between files" + echo "" + echo "Examples:" + echo " $0 # Basic validation" + echo " $0 --verbose # Detailed validation" + echo " $0 --missing-only # Show only missing backups" + echo " $0 --diff # Show file differences" +} + +# Validate backups +validate_backups() { + local verbose="$1" + local summary_only="$2" + local missing_only="$3" + local show_diff="$4" + + echo -e "${BLUE}=== .env Backup Validation ===${NC}" + echo "Source: $DOCKER_DIR" + echo "Backup: $BACKUP_DIR" + echo "" + + if [ ! -d "$BACKUP_DIR" ]; then + echo -e "${RED}Error: Backup directory not found at $BACKUP_DIR${NC}" + echo "Run backup-env-files.sh --init first" + exit 1 + fi + + local total_source=0 + local total_backup=0 + local missing_backup=0 + local outdated_backup=0 + local identical_files=0 + local different_files=0 + local backup_only=0 + + # Arrays to store file lists + declare -a missing_files=() + declare -a outdated_files=() + declare -a different_files_list=() + declare -a backup_only_files=() + + # Count and validate source files + echo -e "${YELLOW}Scanning source files...${NC}" + while IFS= read -r source_file; do + if [ -n "$source_file" ]; then + ((total_source++)) + + # Determine backup path + local rel_path="${source_file#$DOCKER_DIR/}" + local backup_file="$BACKUP_DIR/docker-containers/$rel_path" + + if [ ! -f "$backup_file" ]; then + ((missing_backup++)) + missing_files+=("$rel_path") + [ "$summary_only" != true ] && [ "$missing_only" != true ] && echo -e "${RED}✗ Missing backup: $rel_path${NC}" + else + # Compare files + if cmp -s "$source_file" "$backup_file"; then + ((identical_files++)) + [ "$verbose" = true ] && [ "$summary_only" != true ] && [ "$missing_only" != true ] && echo -e "${GREEN}✓ Identical: $rel_path${NC}" + else + # Check if backup is older + if [ "$source_file" -nt "$backup_file" ]; then + ((outdated_backup++)) + outdated_files+=("$rel_path") + [ "$summary_only" != true ] && [ "$missing_only" != true ] && echo -e "${YELLOW}⚠ Outdated backup: $rel_path${NC}" + else + ((different_files++)) + different_files_list+=("$rel_path") + [ "$summary_only" != true ] && [ "$missing_only" != true ] && echo -e "${BLUE}△ Different: $rel_path${NC}" + fi + + # Show diff if requested + if [ "$show_diff" = true ] && [ "$summary_only" != true ] && [ "$missing_only" != true ]; then + echo -e "${YELLOW} Differences:${NC}" + diff -u "$backup_file" "$source_file" | head -20 || true + echo "" + fi + fi + fi + fi + done < <(find "$DOCKER_DIR" -type f \( -name "*.env" -o -name ".env*" -o -name "env.*" \) 2>/dev/null | sort) + + # Check for backup-only files + echo -e "${YELLOW}Scanning backup files...${NC}" + if [ -d "$BACKUP_DIR/docker-containers" ]; then + while IFS= read -r backup_file; do + if [ -n "$backup_file" ]; then + ((total_backup++)) + + # Determine source path + local rel_path="${backup_file#$BACKUP_DIR/docker-containers/}" + local source_file="$DOCKER_DIR/$rel_path" + + if [ ! -f "$source_file" ]; then + ((backup_only++)) + backup_only_files+=("$rel_path") + [ "$summary_only" != true ] && [ "$missing_only" != true ] && echo -e "${BLUE}⚡ Backup only: $rel_path${NC}" + fi + fi + done < <(find "$BACKUP_DIR/docker-containers" -type f \( -name "*.env" -o -name ".env*" -o -name "env.*" \) 2>/dev/null | sort) + fi + + # Display missing files if requested + if [ "$missing_only" = true ] && [ ${#missing_files[@]} -gt 0 ]; then + echo -e "${RED}=== Missing Backup Files ===${NC}" + for file in "${missing_files[@]}"; do + echo -e "${RED}✗ $file${NC}" + done + echo "" + fi + + # Summary + echo -e "${BLUE}=== Validation Summary ===${NC}" + echo -e "Source files: ${BLUE}$total_source${NC}" + echo -e "Backup files: ${BLUE}$total_backup${NC}" + echo -e "Identical: ${GREEN}$identical_files${NC}" + echo -e "Missing backups: ${RED}$missing_backup${NC}" + echo -e "Outdated backups: ${YELLOW}$outdated_backup${NC}" + echo -e "Different files: ${BLUE}$different_files${NC}" + echo -e "Backup only: ${BLUE}$backup_only${NC}" + echo "" + + # Recommendations + if [ $missing_backup -gt 0 ] || [ $outdated_backup -gt 0 ]; then + echo -e "${YELLOW}=== Recommendations ===${NC}" + if [ $missing_backup -gt 0 ]; then + echo -e "${YELLOW}• Run backup-env-files.sh to backup missing files${NC}" + fi + if [ $outdated_backup -gt 0 ]; then + echo -e "${YELLOW}• Run backup-env-files.sh to update outdated backups${NC}" + fi + echo "" + fi + + # Log summary + log "Validation completed - Source: $total_source, Backup: $total_backup, Missing: $missing_backup, Outdated: $outdated_backup" + + # Exit with error code if issues found + if [ $missing_backup -gt 0 ] || [ $outdated_backup -gt 0 ]; then + exit 1 + fi +} + +# Main function +main() { + local verbose=false + local summary_only=false + local missing_only=false + local show_diff=false + + # Parse command line arguments + while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + usage + exit 0 + ;; + -v|--verbose) + verbose=true + shift + ;; + -s|--summary-only) + summary_only=true + shift + ;; + -m|--missing-only) + missing_only=true + shift + ;; + -d|--diff) + show_diff=true + shift + ;; + *) + echo "Unknown option: $1" + usage + exit 1 + ;; + esac + done + + validate_backups "$verbose" "$summary_only" "$missing_only" "$show_diff" +} + +# Run main function with all arguments +main "$@"