{
"tool_response": {
"filePath": "\/tmp\/backup-prod-to-ki.sh",
"oldString": "#------------------------------------------------------------------------------\n# PHASE 6: \/var\/www\/ inkrementell\n#------------------------------------------------------------------------------\nrsync_www() {\n log \"--- Phase 6: \/var\/www\/ inkrementell ---\"\n\n rsync -az --delete \\\n --timeout=7200 \\\n --exclude='.git\/' \\\n --exclude='node_modules\/' \\\n --exclude='vendor\/' \\\n --exclude='__pycache__\/' \\\n --exclude='.cache\/' \\\n --exclude='*.log' \\\n --exclude='logs\/' \\\n --exclude='demo.karlkratz.de\/' \\\n \/var\/www\/ \\\n \"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_INCR}\/www\/\" \\\n 2>>\"$LOG_FILE\" \\\n && log \"\/var\/www\/ rsync abgeschlossen\" \\\n || record_error \"rsync \/var\/www\/ fehlgeschlagen\"\n}",
"newString": "#------------------------------------------------------------------------------\n# PHASE 6: \/var\/www\/ inkrementell (parallel, 5 gleichzeitig)\n#------------------------------------------------------------------------------\nrsync_www_dir() {\n local dir=\"$1\"\n local name\n name=$(basename \"$dir\")\n rsync -az --delete \\\n --timeout=7200 \\\n --exclude='.git\/' \\\n --exclude='node_modules\/' \\\n --exclude='vendor\/' \\\n --exclude='__pycache__\/' \\\n --exclude='.cache\/' \\\n --exclude='*.log' \\\n --exclude='logs\/' \\\n \"$dir\/\" \\\n \"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_INCR}\/www\/${name}\/\" \\\n 2>>\"$LOG_FILE\" \\\n && echo \"[$(date '+%Y-%m-%d %H:%M:%S')] rsync OK: ${name}\" >> \"$LOG_FILE\" \\\n || echo \"[$(date '+%Y-%m-%d %H:%M:%S')] FEHLER: rsync ${name}\" >> \"$LOG_FILE\"\n}\nexport -f rsync_www_dir\nexport REMOTE_USER REMOTE_HOST REMOTE_INCR LOG_FILE\n\nrsync_www() {\n log \"--- Phase 6: \/var\/www\/ inkrementell (parallel, max 5) ---\"\n\n # Toplevel-Dateien einzeln\n rsync -az --timeout=300 \\\n --exclude='*\/' \\\n \/var\/www\/ \\\n \"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_INCR}\/www\/\" \\\n 2>>\"$LOG_FILE\"\n\n # Unterverzeichnisse parallel (max 5), demo excluded\n find \/var\/www -maxdepth 1 -mindepth 1 -type d \\\n ! -name 'demo.karlkratz.de' \\\n | xargs -P5 -I{} bash -c 'rsync_www_dir \"$@\"' _ {}\n\n local fail_count\n fail_count=$(grep -c \"FEHLER: rsync \" \"$LOG_FILE\" 2>\/dev\/null || echo 0)\n if [ \"$fail_count\" -gt 0 ]; then\n record_error \"rsync \/var\/www\/: ${fail_count} Verzeichnisse fehlgeschlagen\"\n fi\n log \"\/var\/www\/ rsync abgeschlossen (${fail_count} Fehler)\"\n}",
"originalFile": "#!\/bin\/bash\n#==============================================================================\n# Datensicherung: prod.karlkratz.com → ki\/st (88.198.50.199)\n# Taeglich um 02:00 via cron\n# Version: 1.0 (2026-02-19)\n#==============================================================================\nset -uo pipefail\n\n#------------------------------------------------------------------------------\n# KONFIGURATION\n#------------------------------------------------------------------------------\nBACKUP_DATE=$(date +%Y-%m-%d)\nREMOTE_HOST=\"88.198.50.199\"\nREMOTE_USER=\"root\"\nREMOTE_BASE=\"\/backup\/prod\"\nREMOTE_DAILY=\"${REMOTE_BASE}\/daily\/${BACKUP_DATE}\"\nREMOTE_INCR=\"${REMOTE_BASE}\/incremental\"\nLOCAL_STAGING=\"\/var\/backup\/staging\"\nLOG_FILE=\"\/var\/log\/backup-prod-to-ki.log\"\nLOCK_FILE=\"\/var\/run\/backup-prod-to-ki.lock\"\nRETENTION_DAYS=7\nMAIL_RECIPIENT=\"i@karlkratz.de\"\nGPG_PASSPHRASE_FILE=\"\/root\/.backup-gpg-passphrase\"\n\n#------------------------------------------------------------------------------\n# HILFSFUNKTIONEN\n#------------------------------------------------------------------------------\nlog() {\n echo \"[$(date '+%Y-%m-%d %H:%M:%S')] $1\" | tee -a \"$LOG_FILE\"\n}\n\nlog_error() {\n echo \"[$(date '+%Y-%m-%d %H:%M:%S')] FEHLER: $1\" | tee -a \"$LOG_FILE\" >&2\n}\n\nERRORS=()\nrecord_error() {\n ERRORS+=(\"$1\")\n log_error \"$1\"\n}\n\nacquire_lock() {\n if [ -f \"$LOCK_FILE\" ]; then\n local pid\n pid=$(cat \"$LOCK_FILE\" 2>\/dev\/null)\n if [ -n \"$pid\" ] && kill -0 \"$pid\" 2>\/dev\/null; then\n log_error \"Backup laeuft bereits (PID: $pid). Abbruch.\"\n exit 1\n fi\n log \"Verwaiste Lock-Datei gefunden, wird entfernt.\"\n rm -f \"$LOCK_FILE\"\n fi\n echo $$ > \"$LOCK_FILE\"\n trap 'rm -f \"$LOCK_FILE\"; rm -rf \"$LOCAL_STAGING\"' EXIT\n}\n\n#------------------------------------------------------------------------------\n# PHASE 1: MariaDB (45 Datenbanken, einzeln)\n#------------------------------------------------------------------------------\nbackup_mariadb() {\n log \"--- Phase 1a: MariaDB ---\"\n local db_dir=\"${LOCAL_STAGING}\/databases\/mariadb\"\n mkdir -p \"$db_dir\"\n\n local DB_LIST=(\n admin admin_auth agent anachroma_pipeline apache_log_db\n backup_restore bic claudia_grajek_de code_documentation\n code_intelligence codequality content_pipeline doc2vector\n freund freund_lexoffice_369wohlbefinden freund_lexoffice_karlscore\n freund_pipeline karlkratz_de karlkratz_de_dev karlkratz_semantic\n karlscore_net ki_db ki_protocol kiebook kigem_rag kigemeinschaft\n kiglove kiseminar lisa_sundermeyer_de nevoteam nextcloud\n ocr_rechnung payment_system pdf_import ragdemo ragdemo1\n raum_events sprechstunde_physio system_karlkratz_de t_anachroma\n telegram_bot_karlkratz tracking vmail\n )\n\n local ok=0 fail=0\n for db in \"${DB_LIST[@]}\"; do\n if mysqldump --single-transaction --routines --triggers --events \\\n --quick --lock-tables=false \"$db\" 2>\/dev\/null | gzip -9 > \"${db_dir}\/${db}.sql.gz\"; then\n # Verify dump is not empty (gzip header is ~20 bytes)\n local size\n size=$(stat -c%s \"${db_dir}\/${db}.sql.gz\" 2>\/dev\/null || echo 0)\n if [ \"$size\" -gt 50 ]; then\n ok=$((ok + 1))\n else\n record_error \"MariaDB: Dump leer fuer $db\"\n rm -f \"${db_dir}\/${db}.sql.gz\"\n fail=$((fail + 1))\n fi\n else\n record_error \"MariaDB: Dump fehlgeschlagen fuer $db\"\n fail=$((fail + 1))\n fi\n done\n\n # Grants sichern\n mysql -N -e \"SELECT CONCAT('SHOW GRANTS FOR ''',user,'''@''',host,''';') FROM mysql.user WHERE user NOT IN ('root','mariadb.sys','')\" 2>\/dev\/null \\\n | mysql -N 2>\/dev\/null | sed 's\/$\/;\/' | gzip -9 > \"${db_dir}\/_grants.sql.gz\" 2>\/dev\/null\n\n log \"MariaDB: ${ok}\/${#DB_LIST[@]} OK, ${fail} Fehler\"\n}\n\n#------------------------------------------------------------------------------\n# PHASE 1b: Redis\n#------------------------------------------------------------------------------\nbackup_redis() {\n log \"--- Phase 1b: Redis ---\"\n local redis_dir=\"${LOCAL_STAGING}\/databases\/redis\"\n mkdir -p \"$redis_dir\"\n\n redis-cli BGSAVE >\/dev\/null 2>&1\n sleep 3\n\n local rdb_dir\n rdb_dir=$(redis-cli CONFIG GET dir 2>\/dev\/null | tail -1)\n local rdb_file\n rdb_file=$(redis-cli CONFIG GET dbfilename 2>\/dev\/null | tail -1)\n\n if [ -f \"${rdb_dir}\/${rdb_file}\" ]; then\n cp \"${rdb_dir}\/${rdb_file}\" \"${redis_dir}\/dump.rdb\"\n gzip -9 \"${redis_dir}\/dump.rdb\"\n log \"Redis: $(du -sh \"${redis_dir}\/dump.rdb.gz\" | cut -f1)\"\n else\n record_error \"Redis: RDB nicht gefunden (${rdb_dir}\/${rdb_file})\"\n fi\n}\n\n#------------------------------------------------------------------------------\n# PHASE 1c: Qdrant (API Snapshots)\n#------------------------------------------------------------------------------\nbackup_qdrant() {\n log \"--- Phase 1c: Qdrant ---\"\n local qdrant_dir=\"${LOCAL_STAGING}\/databases\/qdrant\"\n mkdir -p \"$qdrant_dir\"\n\n local collections\n collections=$(curl -sf http:\/\/localhost:6333\/collections 2>\/dev\/null \\\n | python3 -c \"import sys,json; [print(c['name']) for c in json.load(sys.stdin)['result']['collections']]\" 2>\/dev\/null)\n\n if [ -z \"$collections\" ]; then\n record_error \"Qdrant: API nicht erreichbar oder keine Collections\"\n return\n fi\n\n local ok=0\n while IFS= read -r coll; do\n [ -z \"$coll\" ] && continue\n local snap_result\n snap_result=$(curl -sf -X POST \"http:\/\/localhost:6333\/collections\/${coll}\/snapshots\" 2>\/dev\/null)\n local snap_name\n snap_name=$(echo \"$snap_result\" | python3 -c \"import sys,json; print(json.load(sys.stdin)['result']['name'])\" 2>\/dev\/null)\n\n if [ -n \"$snap_name\" ]; then\n curl -sf -o \"${qdrant_dir}\/${coll}.snapshot\" \\\n \"http:\/\/localhost:6333\/collections\/${coll}\/snapshots\/${snap_name}\" 2>\/dev\/null\n curl -sf -X DELETE \"http:\/\/localhost:6333\/collections\/${coll}\/snapshots\/${snap_name}\" >\/dev\/null 2>&1\n ok=$((ok + 1))\n else\n record_error \"Qdrant: Snapshot fehlgeschlagen fuer ${coll}\"\n fi\n done <<< \"$collections\"\n\n log \"Qdrant: ${ok} Snapshots erstellt\"\n}\n\n#------------------------------------------------------------------------------\n# PHASE 1d: ArangoDB\n#------------------------------------------------------------------------------\nbackup_arangodb() {\n log \"--- Phase 1d: ArangoDB ---\"\n local arango_dir=\"${LOCAL_STAGING}\/databases\/arangodb\"\n mkdir -p \"$arango_dir\"\n\n if command -v arangodump &>\/dev\/null; then\n if arangodump --output-directory \"$arango_dir\" --overwrite true --compress-output true 2>>\"$LOG_FILE\"; then\n log \"ArangoDB: Dump erstellt\"\n else\n record_error \"ArangoDB: Dump fehlgeschlagen\"\n fi\n else\n record_error \"ArangoDB: arangodump nicht installiert\"\n fi\n}\n\n#------------------------------------------------------------------------------\n# PHASE 1e: ChromaDB\n#------------------------------------------------------------------------------\nbackup_chromadb() {\n log \"--- Phase 1e: ChromaDB ---\"\n local chroma_dir=\"${LOCAL_STAGING}\/databases\/chromadb\"\n mkdir -p \"$chroma_dir\"\n\n if [ -d \/var\/www\/chromadb ]; then\n tar czf \"${chroma_dir}\/chromadb-data.tar.gz\" \\\n --exclude='*.log' \\\n -C \/var\/www chromadb 2>>\"$LOG_FILE\" \\\n && log \"ChromaDB: $(du -sh \"${chroma_dir}\/chromadb-data.tar.gz\" | cut -f1)\" \\\n || record_error \"ChromaDB: tar fehlgeschlagen\"\n else\n log \"ChromaDB: \/var\/www\/chromadb nicht vorhanden (uebersprungen)\"\n fi\n}\n\n#------------------------------------------------------------------------------\n# PHASE 2: E-Mail\n#------------------------------------------------------------------------------\nbackup_mail() {\n log \"--- Phase 2: E-Mail ---\"\n local mail_dir=\"${LOCAL_STAGING}\/mail\"\n mkdir -p \"$mail_dir\"\n\n if [ -d \/var\/vmail ]; then\n tar czf \"${mail_dir}\/vmail.tar.gz\" -C \/var vmail 2>>\"$LOG_FILE\" \\\n && log \"vmail: $(du -sh \"${mail_dir}\/vmail.tar.gz\" | cut -f1)\" \\\n || record_error \"Mail: vmail tar fehlgeschlagen\"\n fi\n\n if [ -d \/var\/mail ] && [ \"$(ls -A \/var\/mail 2>\/dev\/null)\" ]; then\n tar czf \"${mail_dir}\/mail.tar.gz\" -C \/var mail 2>>\"$LOG_FILE\" \\\n || record_error \"Mail: \/var\/mail tar fehlgeschlagen\"\n fi\n}\n\n#------------------------------------------------------------------------------\n# PHASE 3: Konfigurationsdateien\n#------------------------------------------------------------------------------\nbackup_configs() {\n log \"--- Phase 3: Konfigurationen ---\"\n local conf_dir=\"${LOCAL_STAGING}\/configs\"\n mkdir -p \"$conf_dir\"\n\n declare -A CONFIGS=(\n [\"apache2\"]=\"\/etc\/apache2\"\n [\"php\"]=\"\/etc\/php\"\n [\"mysql\"]=\"\/etc\/mysql\"\n [\"redis\"]=\"\/etc\/redis\"\n [\"postfix\"]=\"\/etc\/postfix\"\n [\"dovecot\"]=\"\/etc\/dovecot\"\n [\"opendkim\"]=\"\/etc\/opendkim\"\n [\"rspamd\"]=\"\/etc\/rspamd\"\n [\"fail2ban\"]=\"\/etc\/fail2ban\"\n [\"nftables\"]=\"\/etc\/nftables\"\n [\"monit\"]=\"\/etc\/monit\"\n [\"ipset\"]=\"\/etc\/ipset\"\n [\"cron.d\"]=\"\/etc\/cron.d\"\n [\"letsencrypt\"]=\"\/etc\/letsencrypt\"\n [\"nats\"]=\"\/etc\/nats\"\n [\"qdrant\"]=\"\/etc\/qdrant\"\n )\n\n local ok=0\n for name in \"${!CONFIGS[@]}\"; do\n local dir=\"${CONFIGS[$name]}\"\n if [ -d \"$dir\" ]; then\n tar czf \"${conf_dir}\/${name}.tar.gz\" \\\n --exclude='*.log' \\\n -C \"$(dirname \"$dir\")\" \"$(basename \"$dir\")\" 2>\/dev\/null \\\n && ok=$((ok + 1)) \\\n || record_error \"Config: ${name} tar fehlgeschlagen\"\n fi\n done\n\n # Custom systemd units\n local sd_tmp=\"${conf_dir}\/_systemd_tmp\"\n mkdir -p \"$sd_tmp\"\n find \/etc\/systemd\/system\/ -maxdepth 1 \\( -name \"*.service\" -o -name \"*.timer\" \\) \\\n ... [TRUNCATED-12bf58cda5165688]",
"structuredPatch": [
{
"oldStart": 338,
"oldLines": 11,
"newStart": 338,
"newLines": 12,
"lines": [
" }",
" ",
" #------------------------------------------------------------------------------",
"-# PHASE 6: \/var\/www\/ inkrementell",
"+# PHASE 6: \/var\/www\/ inkrementell (parallel, 5 gleichzeitig)",
" #------------------------------------------------------------------------------",
"-rsync_www() {",
"- log \"--- Phase 6: \/var\/www\/ inkrementell ---\"",
"-",
"+rsync_www_dir() {",
"+ local dir=\"$1\"",
"+ local name",
"+ name=$(basename \"$dir\")",
" rsync -az --delete \\",
" --timeout=7200 \\",
" --exclude='.git\/' \\"
]
},
{
"oldStart": 352,
"oldLines": 12,
"newStart": 353,
"newLines": 36,
"lines": [
" --exclude='.cache\/' \\",
" --exclude='*.log' \\",
" --exclude='logs\/' \\",
"- --exclude='demo.karlkratz.de\/' \\",
"+ \"$dir\/\" \\",
"+ \"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_INCR}\/www\/${name}\/\" \\",
"+ 2>>\"$LOG_FILE\" \\",
"+ && echo \"[$(date '+%Y-%m-%d %H:%M:%S')] rsync OK: ${name}\" >> \"$LOG_FILE\" \\",
"+ || echo \"[$(date '+%Y-%m-%d %H:%M:%S')] FEHLER: rsync ${name}\" >> \"$LOG_FILE\"",
"+}",
"+export -f rsync_www_dir",
"+export REMOTE_USER REMOTE_HOST REMOTE_INCR LOG_FILE",
"+",
"+rsync_www() {",
"+ log \"--- Phase 6: \/var\/www\/ inkrementell (parallel, max 5) ---\"",
"+",
"+ # Toplevel-Dateien einzeln",
"+ rsync -az --timeout=300 \\",
"+ --exclude='*\/' \\",
" \/var\/www\/ \\",
" \"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_INCR}\/www\/\" \\",
"- 2>>\"$LOG_FILE\" \\",
"- && log \"\/var\/www\/ rsync abgeschlossen\" \\",
"- || record_error \"rsync \/var\/www\/ fehlgeschlagen\"",
"+ 2>>\"$LOG_FILE\"",
"+",
"+ # Unterverzeichnisse parallel (max 5), demo excluded",
"+ find \/var\/www -maxdepth 1 -mindepth 1 -type d \\",
"+ ! -name 'demo.karlkratz.de' \\",
"+ | xargs -P5 -I{} bash -c 'rsync_www_dir \"$@\"' _ {}",
"+",
"+ local fail_count",
"+ fail_count=$(grep -c \"FEHLER: rsync \" \"$LOG_FILE\" 2>\/dev\/null || echo 0)",
"+ if [ \"$fail_count\" -gt 0 ]; then",
"+ record_error \"rsync \/var\/www\/: ${fail_count} Verzeichnisse fehlgeschlagen\"",
"+ fi",
"+ log \"\/var\/www\/ rsync abgeschlossen (${fail_count} Fehler)\"",
" }",
" ",
" #------------------------------------------------------------------------------"
]
}
],
"userModified": false,
"replaceAll": false
}
}