#!/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 "$@"