Backup-Home-docker.tar.gz.sh
· 23 KiB · Bash
Исходник
#!/bin/bash
# Backup complet Docker + (CasaOS si présent) + Portainer
# + export docker-compose + rapport HTML + index
# + last/ + rotation + publication WEB (HTML only)
#############################
# CONFIG
#############################
SOURCE_DIR="/home/docker"
# Base des backups datés
BACKUP_DIR="/home/backup/docker/compose"
MAX_BACKUPS=3
DATE_RAW="$(date +'%Y%m%d_%H%M%S')"
DATE_DIR="$(date +'%Y.%m.%d_%H-%M-%S')"
# Dossier daté du run courant
RUN_DIR="$BACKUP_DIR/$DATE_DIR"
# Dossier "dernier backup"
LAST_DIR="$BACKUP_DIR/last"
# Archives dans le dossier daté
BACKUP_FILE="$RUN_DIR/docker_backup.tar.gz"
CASAOS_FULL_TAR="$RUN_DIR/casaos_full_backup.tar.gz"
# Dossier de configs / compose dans le run
CONFIG_DIR="$RUN_DIR"
# Logs
LOG_FILE="/var/log/docker_backup.log"
# Rapports HTML
HTML_REPORT="$RUN_DIR/report.html"
HTML_REPORT_LAST="$LAST_DIR/report.html"
HTML_INDEX_LAST="$LAST_DIR/index.html"
# Publication WEB (HTML uniquement)
WEB_PUBLIC_DIR="/home/www-adm1n/docker_compose"
# Dossiers à exclure (chemins relatifs à $SOURCE_DIR)
EXCLUDE_DIRS=(
"docker_var_lib"
"1_Backups"
"casaos_data/*"
"nginx_proxy/data/logs/*"
"yt-dl/video/*"
)
#############################
# MODE TTY / CRON
#############################
# Si pas de terminal (cron), on désactive les couleurs
if [ -t 1 ]; then
USE_COLORS=1
else
USE_COLORS=0
fi
#############################
# COULEURS
#############################
if [ "$USE_COLORS" = "1" ]; then
NC="\e[0m"
GREEN="\e[32m"
YELLOW="\e[33m"
CYAN="\e[36m"
RED="\e[31m"
else
NC=""
GREEN=""
YELLOW=""
CYAN=""
RED=""
fi
#############################
# DÉTECTION CASAOS
#############################
CASAOS_PRESENT=0
if [ -d "/var/lib/casaos" ] || [ -d "$SOURCE_DIR/casaos_data" ] || command -v casaos >/dev/null 2>&1; then
CASAOS_PRESENT=1
fi
#############################
# FONCTIONS
#############################
log_message() {
local msg="$1"
local now
now="$(date +'%Y-%m-%d %H:%M:%S')"
echo "$now - $msg" >> "$LOG_FILE"
echo -e "$now - $msg"
}
format_time() {
local t=$1
((h=t/3600))
((m=(t%3600)/60))
((s=t%60))
printf "%02d:%02d:%02d\n" $h $m $s
}
check_pigz() {
if ! command -v pigz >/dev/null 2>&1; then
log_message "pigz n'est pas installé."
if [ "$USE_COLORS" = "1" ]; then
echo -e "${YELLOW}pigz n'est pas installé. Voulez-vous l’installer ? (y/n)${NC}"
read -r rep
if [ "$rep" = "y" ]; then
sudo apt update && sudo apt install -y pigz || {
echo -e "${RED}Échec installation pigz. Abandon.${NC}"
exit 1
}
else
echo -e "${RED}pigz requis. Abandon.${NC}"
exit 1
fi
else
echo "ERREUR: pigz requis (mode cron non-interactif)."
exit 1
fi
fi
}
rotate_backups() {
log_message "Rotation backups (max $MAX_BACKUPS)..."
# Liste uniquement les dossiers datés, ignore last/
mapfile -t backups_dirs < <(
find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d \
! -name "last" \
-printf "%T@ %p\n" 2>/dev/null \
| sort -nr \
| awk '{print $2}'
)
if [ "${#backups_dirs[@]}" -gt "$MAX_BACKUPS" ]; then
for d in "${backups_dirs[@]:$MAX_BACKUPS}"; do
log_message "Suppression ancien backup : $d"
rm -rf "$d"
done
fi
}
get_size() {
du -sh "$1" 2>/dev/null | awk '{print $1}'
}
html_escape() {
sed -e 's/&/\&/g' \
-e 's/</\</g' \
-e 's/>/\>/g'
}
html_line() {
# $1=label $2=value $3=emoji $4=class
echo "<div class='row $4'><span class='emoji'>$3</span><span class='label'>$1</span><span class='value'>$2</span></div>" >> "$HTML_REPORT"
}
html_container_table() {
local dirs
dirs=$(find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -type d \
! -path "$SOURCE_DIR/casaos_data" \
! -path "$SOURCE_DIR/docker_var_lib" \
! -path "$SOURCE_DIR/portainer" \
2>/dev/null)
if [ -z "$dirs" ]; then
echo "<div class='empty'>Aucun dossier Docker trouvé.</div>" >> "$HTML_REPORT"
return
fi
echo "<div class='tblwrap'><table class='tbl'>" >> "$HTML_REPORT"
echo "<thead><tr><th>📦 Container</th><th>📏 Taille</th><th>📁 Chemin</th></tr></thead>" >> "$HTML_REPORT"
echo "<tbody>" >> "$HTML_REPORT"
# tri par taille desc
du -sh $dirs 2>/dev/null | sort -hr | while read -r size path; do
name="$(basename "$path")"
esc_path="$(echo "$path" | html_escape)"
echo "<tr><td><b>$name</b></td><td>$size</td><td><code>$esc_path</code></td></tr>" >> "$HTML_REPORT"
done
echo "</tbody></table></div>" >> "$HTML_REPORT"
}
generate_index_html() {
local base_dir="$1"
local output_file="$2"
cat > "$output_file" <<EOF
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Index Backups Docker</title>
<style>
body{font-family:Arial,sans-serif;background:#0b1220;color:#e5e7eb;margin:0;padding:24px}
.card{background:#111827;border:1px solid #1f2937;border-radius:12px;padding:16px;max-width:980px;margin:auto}
h1{margin:0 0 12px;font-size:22px}
a{color:#93c5fd;text-decoration:none}
a:hover{text-decoration:underline}
hr{border:0;border-top:1px solid #1f2937;margin:12px 0}
.toolbar{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:12px}
.btn{display:inline-block;padding:8px 12px;border-radius:10px;background:#0f172a;border:1px solid #1f2937}
.btn:hover{background:#111c33}
.item{padding:10px;border-radius:12px;margin:8px 0;background:#0f172a;border:1px solid #1f2937}
.row{display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap}
.left{display:flex;gap:10px;align-items:center}
.badge{font-size:12px;padding:4px 8px;border-radius:999px;background:#1f2937}
.badge-ok{background:#052e16;color:#86efac;border:1px solid #14532d}
.badge-warn{background:#451a03;color:#fcd34d;border:1px solid #92400e}
.meta{color:#94a3b8;font-size:12px;margin-top:6px}
small{color:#94a3b8}
.note{color:#94a3b8;font-size:12px;margin-top:10px}
</style>
</head>
<body>
<div class="card">
<h1>📦 Index des Backups Docker</h1>
<small>Dossier backups: $base_dir</small>
<div class="toolbar">
<a class="btn" href="./report.html">📌 Ouvrir le LAST</a>
</div>
<div class="note">
ℹ️ Les backups datés ne sont pas exposés sur le web (sécurité).
Seul le rapport <b>LAST</b> est accessible.
</div>
<hr>
EOF
# Liste dossiers datés (ignore last)
find "$base_dir" -mindepth 1 -maxdepth 1 -type d ! -name "last" 2>/dev/null \
| sort -r \
| while read -r d; do
name="$(basename "$d")"
tarfile="$d/docker_backup.tar.gz"
report="$d/report.html"
mtime="$(date -r "$d" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo 'N/A')"
if [ -f "$tarfile" ]; then
tarsize="$(du -sh "$tarfile" 2>/dev/null | awk '{print $1}')"
else
tarsize="N/A"
fi
if [ -f "$report" ]; then
status="OK"
badge="badge-ok"
else
status="missing report"
badge="badge-warn"
fi
cat >> "$output_file" <<EOF
<div class="item">
<div class="row">
<div class="left">
<span>📄</span>
<span><b>$name</b></span>
</div>
<span class="badge $badge">$status</span>
</div>
<div class="meta">🕒 $mtime — 🗜️ docker_backup.tar.gz : <b>$tarsize</b></div>
</div>
EOF
done
cat >> "$output_file" <<EOF
</div>
</body>
</html>
EOF
}
#############################
# DÉBUT SCRIPT
#############################
SECONDS=0
check_pigz
mkdir -p "$RUN_DIR"
mkdir -p "$BACKUP_DIR"
touch "$LOG_FILE"
echo -e "${CYAN}=== Backup Docker + Configs (Portainer / CasaOS) ===${NC}"
log_message "Début sauvegarde..."
log_message "Dossier backup (run) : $RUN_DIR"
log_message "Dossier backup (last): $LAST_DIR"
log_message "CasaOS détecté : $CASAOS_PRESENT"
##############################################
# 1) Archive Docker
##############################################
log_message "Création archive Docker : $BACKUP_FILE"
EXCLUDE_ARGS=(
"--exclude=*.log"
"--exclude=.composer"
"--exclude=.aptitude"
"--exclude=.cache"
"--exclude=.cmake"
"--exclude=.yarn"
"--exclude=.w3m"
"--exclude=.pip"
"--exclude=.pm2"
"--exclude=.pm"
"--exclude=.bundle"
"--exclude=.gem"
"--exclude=.cpan"
"--exclude=.cpanm"
"--exclude=.git"
"--exclude=.local"
"--exclude=.npm"
"--exclude=.nvm"
"--exclude=.rvm"
"--exclude=node_modules"
"--exclude=lost+found"
)
for d in "${EXCLUDE_DIRS[@]}"; do
EXCLUDE_ARGS+=( "--exclude=$d" )
done
tar -cf "$BACKUP_FILE" -I pigz \
--directory="$SOURCE_DIR" \
"${EXCLUDE_ARGS[@]}" \
. 2>> "$LOG_FILE"
if [ $? -ne 0 ]; then
log_message "ERREUR lors de la création du tar Docker."
echo -e "${RED}Erreur lors de la création de l’archive Docker.${NC}"
exit 1
fi
##############################################
# 2) Extraction docker-compose CasaOS (si présent)
##############################################
if [ "$CASAOS_PRESENT" = "1" ]; then
log_message "Extraction des docker-compose CasaOS..."
if [ -d "$SOURCE_DIR/casaos_data" ]; then
find "$SOURCE_DIR/casaos_data" -maxdepth 3 -type f -name "docker-compose.y*ml" | while read -r file; do
container_name=$(basename "$(dirname "$file")")
dest_dir="$CONFIG_DIR/casaos/$container_name"
mkdir -p "$dest_dir"
cp "$file" "$dest_dir/"
log_message " [CasaOS] compose : $file"
done
fi
else
log_message "CasaOS non détecté → ignoré."
fi
##############################################
# 3) Extraction docker-compose Docker (hors CasaOS)
##############################################
log_message "Extraction des docker-compose Docker..."
find "$SOURCE_DIR" -mindepth 2 -maxdepth 2 -type f -name "docker-compose.y*ml" \
! -path "$SOURCE_DIR/casaos_data/*" \
! -path "$SOURCE_DIR/docker_var_lib/*" \
2>/dev/null | while read -r file; do
container_name=$(basename "$(dirname "$file")")
dest_dir="$CONFIG_DIR/Compose/$container_name"
mkdir -p "$dest_dir"
cp "$file" "$dest_dir/"
log_message " [Docker] compose : $file"
done
##############################################
# 4) Backup config Portainer (si volume présent)
##############################################
PORTAINER_CONFIG_DIR="$CONFIG_DIR/portainer"
PORTAINER_FILES=0
if docker volume inspect portainer_data >/dev/null 2>&1; then
log_message "Sauvegarde config Portainer..."
mkdir -p "$PORTAINER_CONFIG_DIR"
cp -r /var/lib/docker/volumes/portainer_data/_data/* "$PORTAINER_CONFIG_DIR/" 2>>"$LOG_FILE"
PORTAINER_FILES=$(find "$PORTAINER_CONFIG_DIR" -type f 2>/dev/null | wc -l)
else
PORTAINER_CONFIG_DIR=""
fi
##############################################
# 5) Backup CasaOS (config + FULL TAR) si présent
##############################################
CASAOS_CONFIG_DIR="$CONFIG_DIR/casaos/full_config"
if [ "$CASAOS_PRESENT" = "1" ]; then
if [ -d "/var/lib/casaos" ]; then
log_message "Sauvegarde config CasaOS full..."
mkdir -p "$CASAOS_CONFIG_DIR"
cp -r /var/lib/casaos/* "$CASAOS_CONFIG_DIR/" 2>>"$LOG_FILE"
fi
CASAOS_TAR_SOURCES=()
[ -d "/var/lib/casaos" ] && CASAOS_TAR_SOURCES+=( "/var/lib/casaos" )
[ -d "$SOURCE_DIR/casaos_data" ] && CASAOS_TAR_SOURCES+=( "$SOURCE_DIR/casaos_data" )
if [ "${#CASAOS_TAR_SOURCES[@]}" -gt 0 ]; then
log_message "Création archive CasaOS FULL : $CASAOS_FULL_TAR"
tar -cf "$CASAOS_FULL_TAR" -I pigz "${CASAOS_TAR_SOURCES[@]}" 2>>"$LOG_FILE"
fi
fi
##############################################
# 6) Stats + rotation
##############################################
CASAOS_COMPOSE_BACKUP=0
if [ "$CASAOS_PRESENT" = "1" ]; then
CASAOS_COMPOSE_BACKUP=$(find "$CONFIG_DIR/casaos" -maxdepth 3 -type f -name "docker-compose.y*ml" 2>/dev/null | wc -l)
fi
DOCKER_COMPOSE_BACKUP=$(find "$CONFIG_DIR/Compose" -type f -name "docker-compose.y*ml" 2>/dev/null | wc -l)
SOURCE_COMPOSE_COUNT=$(find "$SOURCE_DIR" -mindepth 2 -maxdepth 2 -type f -name "docker-compose.y*ml" \
! -path "$SOURCE_DIR/casaos_data/*" \
! -path "$SOURCE_DIR/docker_var_lib/*" \
2>/dev/null | wc -l)
CONTAINERS_DOCKER=$(find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -type d \
! -path "$SOURCE_DIR/casaos_data" \
! -path "$SOURCE_DIR/docker_var_lib" \
! -path "$SOURCE_DIR/portainer" 2>/dev/null | wc -l)
rotate_backups
TOTAL_BACKUP_SETS=$(find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d ! -name "last" 2>/dev/null | wc -l)
mapfile -t CURRENT_SETS < <(find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d ! -name "last" 2>/dev/null | sort)
OLDEST_DIR="${CURRENT_SETS[0]}"
NEWEST_DIR="${CURRENT_SETS[${#CURRENT_SETS[@]}-1]}"
DURATION="$(format_time $SECONDS)"
TAR_SIZE="$(get_size "$BACKUP_FILE")"
CASAOS_FULL_TAR_SIZE=""
[ -f "$CASAOS_FULL_TAR" ] && CASAOS_FULL_TAR_SIZE="$(get_size "$CASAOS_FULL_TAR")"
PORTAINER_CONFIG_SIZE=""
[ -n "$PORTAINER_CONFIG_DIR" ] && [ -d "$PORTAINER_CONFIG_DIR" ] && PORTAINER_CONFIG_SIZE="$(get_size "$PORTAINER_CONFIG_DIR")"
##############################################
# 7) Génération rapport HTML (run daté)
##############################################
cat > "$HTML_REPORT" <<EOF
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Backup Docker Report - $DATE_DIR</title>
<style>
:root{
--bg:#0b1220;
--card:#111827;
--border:#1f2937;
--muted:#94a3b8;
--text:#e5e7eb;
--blue:#93c5fd;
--ok:#22c55e;
--warn:#f59e0b;
--bad:#ef4444;
--row:#0f172a;
--row2:#0c1428;
}
*{box-sizing:border-box}
body{font-family:Arial,system-ui,sans-serif;background:var(--bg);color:var(--text);margin:0;padding:22px}
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;max-width:1100px;margin:auto}
h1{margin:0 0 8px;font-size:22px}
small{color:var(--muted)}
hr{border:0;border-top:1px solid var(--border);margin:14px 0}
h2{margin:16px 0 8px;font-size:15px;color:var(--blue)}
.row{display:flex;gap:12px;padding:10px;border-radius:12px;align-items:center}
.row:nth-child(odd){background:var(--row)}
.row:nth-child(even){background:var(--row2)}
.label{flex:1;color:#cbd5e1}
.value{font-weight:bold;color:#f8fafc}
.emoji{width:26px;display:inline-block}
.ok{border-left:4px solid var(--ok)}
.warn{border-left:4px solid var(--warn)}
.bad{border-left:4px solid var(--bad)}
.info{border-left:4px solid #3b82f6}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
@media (max-width:900px){.grid{grid-template-columns:1fr}}
.tblwrap{overflow:auto;border:1px solid var(--border);border-radius:14px}
.tbl{width:100%;border-collapse:collapse;min-width:720px}
.tbl th,.tbl td{padding:10px;border-bottom:1px solid var(--border);text-align:left;vertical-align:top}
.tbl th{color:#cbd5e1;background:#0f172a;position:sticky;top:0}
.tbl tr:hover td{background:#0a1020}
code{color:#cbd5e1}
.empty{padding:10px;color:var(--muted)}
.footer{margin-top:14px;color:var(--muted);font-size:12px}
.badge{display:inline-block;padding:3px 8px;border-radius:999px;border:1px solid var(--border);font-size:12px;margin-left:8px}
.badge-ok{background:#052e16;color:#86efac;border-color:#14532d}
.badge-warn{background:#451a03;color:#fcd34d;border-color:#92400e}
</style>
</head>
<body>
<div class="card">
<h1>📦 Backup Docker / CasaOS / Portainer</h1>
<small>📅 $DATE_DIR — 🕒 $(date +'%Y-%m-%d %H:%M:%S')</small>
<hr>
<h2>🔧 Global</h2>
EOF
# lignes global
html_line "Dossier backup courant" "$(echo "$RUN_DIR" | html_escape)" "📁" "info"
html_line "Durée totale" "$DURATION" "⏱️" "info"
html_line "Nombre de sets de backup" "$TOTAL_BACKUP_SETS" "🗂️" "info"
echo "<h2>🐳 Docker</h2>" >> "$HTML_REPORT"
html_line "Containers détectés" "$CONTAINERS_DOCKER" "🧱" "ok"
html_line "Compose source" "$SOURCE_COMPOSE_COUNT" "📄" "info"
html_line "Compose backup" "$DOCKER_COMPOSE_BACKUP" "📦" "info"
html_line "Archive Docker (.tar.gz)" "$(echo "$BACKUP_FILE ($TAR_SIZE)" | html_escape)" "🗜️" "ok"
echo "<h2>🏠 CasaOS</h2>" >> "$HTML_REPORT"
if [ "$CASAOS_PRESENT" = "1" ]; then
html_line "CasaOS détecté" "OUI" "✅" "ok"
html_line "Compose CasaOS backup" "$CASAOS_COMPOSE_BACKUP" "📄" "info"
if [ -f "$CASAOS_FULL_TAR" ]; then
html_line "Archive CasaOS FULL (.tar.gz)" "$(echo "$CASAOS_FULL_TAR ($CASAOS_FULL_TAR_SIZE)" | html_escape)" "🗜️" "ok"
else
html_line "Archive CasaOS FULL (.tar.gz)" "Non générée" "⚠️" "warn"
fi
else
html_line "CasaOS détecté" "NON (ignoré)" "🚫" "warn"
fi
echo "<h2>🧭 Portainer</h2>" >> "$HTML_REPORT"
html_line "Fichiers config Portainer" "$PORTAINER_FILES" "📁" "info"
[ -n "$PORTAINER_CONFIG_SIZE" ] && html_line "Taille config Portainer" "$PORTAINER_CONFIG_SIZE" "📏" "info"
echo "<h2>📊 Containers / Tailles (dossiers dans $SOURCE_DIR)</h2>" >> "$HTML_REPORT"
html_container_table
cat >> "$HTML_REPORT" <<EOF
<div class="footer">
✅ Rapport généré automatiquement par le script de backup.<br>
⚠️ Ne partage pas ce rapport publiquement : il révèle des chemins et des infos de structure.
</div>
</div>
</body>
</html>
EOF
log_message "Rapport HTML généré : $HTML_REPORT"
##################
##############################################
# 8) MAJ last/ (SANS copier les archives .gz)
##############################################
log_message "Mise à jour du dossier LAST : $LAST_DIR"
rm -rf "$LAST_DIR"
mkdir -p "$LAST_DIR"
# HTML
cp -f "$HTML_REPORT" "$LAST_DIR/report.html"
# Compose / configs (pas d'archives)
[ -d "$CONFIG_DIR/Compose" ] && cp -a "$CONFIG_DIR/Compose" "$LAST_DIR/"
[ -d "$CONFIG_DIR/portainer" ] && cp -a "$CONFIG_DIR/portainer" "$LAST_DIR/"
[ "$CASAOS_PRESENT" = "1" ] && [ -d "$CONFIG_DIR/casaos" ] && cp -a "$CONFIG_DIR/casaos" "$LAST_DIR/"
# Index HTML last/
generate_index_html "$BACKUP_DIR" "$HTML_INDEX_LAST"
log_message "LAST mis à jour ✅"
log_message "Dernier rapport HTML : $HTML_REPORT_LAST"
log_message "Index HTML last : $HTML_INDEX_LAST"
##############################################
# 9) Publication WEB (HTML UNIQUEMENT)
##############################################
log_message "Publication web (HTML only) vers : $WEB_PUBLIC_DIR"
rm -rf "$WEB_PUBLIC_DIR"
mkdir -p "$WEB_PUBLIC_DIR"
cp -f "$LAST_DIR/report.html" "$WEB_PUBLIC_DIR/report.html"
cp -f "$LAST_DIR/index.html" "$WEB_PUBLIC_DIR/" 2>/dev/null || true
log_message "Publication web OK ✅"
##############################################
# 10) Rapport CONSOLE
##############################################
echo -e "${GREEN}===== BACKUP TERMINÉ ✅ =====${NC}"
##############################################
# LISTE DES DOSSIERS DOCKER PAR TAILLE (ASCII)
##############################################
echo ""
echo -e "${CYAN}----- LISTE/TAILLE DES RÉPERTOIRES DOCKER -----${NC}"
DOCKER_DIRS=$(find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -type d \
! -path "$SOURCE_DIR/casaos_data" \
! -path "$SOURCE_DIR/docker_var_lib" \
! -path "$SOURCE_DIR/portainer" \
2>/dev/null)
if [ -z "$DOCKER_DIRS" ]; then
echo -e "${YELLOW}(Aucun dossier Docker trouvé)${NC}"
else
echo -e "${CYAN}┌──────────────────────────┬─────────┬────────────────────────────────────────┐${NC}"
printf "${CYAN}│ %-24s │ %-7s │ %-38s │${NC}\n" "Container" "Taille" "Chemin"
echo -e "${CYAN}├──────────────────────────┼─────────┼────────────────────────────────────────┤${NC}"
# du + tri par taille desc
du -sh $DOCKER_DIRS 2>/dev/null | sort -hr | while read -r size path; do
name=$(basename "$path")
printf "│ %-24s │ %-7s │ %-38s │\n" "$name" "$size" "$path"
done
echo -e "${CYAN}└──────────────────────────┴─────────┴────────────────────────────────────────┘${NC}"
fi
##############################################
echo -e "${CYAN}Dossier backup courant : ${YELLOW}$RUN_DIR${NC}"
echo ""
echo -e "${CYAN}----- DOCKER -----${NC}"
echo "Containers Docker détectés : $CONTAINERS_DOCKER"
echo "Compose source (Docker) : $SOURCE_COMPOSE_COUNT"
echo "Compose backup (Docker) : $DOCKER_COMPOSE_BACKUP"
echo "Archive Docker (run) : $BACKUP_FILE ($TAR_SIZE)"
echo ""
echo -e "${CYAN}----- CASAOS -----${NC}"
if [ "$CASAOS_PRESENT" = "1" ]; then
echo "CasaOS détecté : OUI"
echo "Compose CasaOS backup : $CASAOS_COMPOSE_BACKUP"
[ -f "$CASAOS_FULL_TAR" ] && echo "Archive CasaOS FULL (run) : $CASAOS_FULL_TAR ($CASAOS_FULL_TAR_SIZE)"
else
echo "CasaOS détecté : NON (ignoré)"
fi
echo ""
echo -e "${CYAN}----- PORTAINER -----${NC}"
echo "Fichiers config Portainer : $PORTAINER_FILES"
[ -n "$PORTAINER_CONFIG_SIZE" ] && echo "Taille Portainer (run) : $PORTAINER_CONFIG_SIZE"
echo ""
echo -e "${CYAN}----- GLOBAL -----${NC}"
echo "Sets de backup : $TOTAL_BACKUP_SETS"
echo "Oldest backup : ${OLDEST_DIR:-N/A}"
echo "Newest backup : ${NEWEST_DIR:-N/A}"
echo "Durée totale : $DURATION"
echo ""
echo "HTML (run) : $HTML_REPORT"
echo "HTML (last) : $HTML_REPORT_LAST"
echo "WEB index : $WEB_PUBLIC_DIR/index.html"
echo "WEB report : $WEB_PUBLIC_DIR/report.html"
##############################################
# JSON optionnel
##############################################
if [ "$1" = "--json" ]; then
echo ""
echo "{"
echo " \"date_raw\": \"$DATE_RAW\","
echo " \"date_dir\": \"$DATE_DIR\","
echo " \"run_dir\": \"$RUN_DIR\","
echo " \"last_dir\": \"$LAST_DIR\","
echo " \"docker_backup_file\": \"$BACKUP_FILE\","
echo " \"docker_backup_size\": \"$TAR_SIZE\","
echo " \"casaos_present\": $CASAOS_PRESENT,"
echo " \"casaos_full_backup_file\": \"${CASAOS_FULL_TAR:-}\","
echo " \"casaos_full_backup_size\": \"${CASAOS_FULL_TAR_SIZE:-}\","
echo " \"portainer_files\": $PORTAINER_FILES,"
echo " \"backup_sets\": $TOTAL_BACKUP_SETS,"
echo " \"duration\": \"$DURATION\","
echo " \"html_report_run\": \"$HTML_REPORT\","
echo " \"html_report_last\": \"$HTML_REPORT_LAST\","
echo " \"web_index\": \"$WEB_PUBLIC_DIR/index.html\","
echo " \"web_report\": \"$WEB_PUBLIC_DIR/report.html\""
echo "}"
fi
| 1 | #!/bin/bash |
| 2 | |
| 3 | # Backup complet Docker + (CasaOS si présent) + Portainer |
| 4 | # + export docker-compose + rapport HTML + index |
| 5 | # + last/ + rotation + publication WEB (HTML only) |
| 6 | |
| 7 | ############################# |
| 8 | # CONFIG |
| 9 | ############################# |
| 10 | |
| 11 | SOURCE_DIR="/home/docker" |
| 12 | |
| 13 | # Base des backups datés |
| 14 | BACKUP_DIR="/home/backup/docker/compose" |
| 15 | MAX_BACKUPS=3 |
| 16 | |
| 17 | DATE_RAW="$(date +'%Y%m%d_%H%M%S')" |
| 18 | DATE_DIR="$(date +'%Y.%m.%d_%H-%M-%S')" |
| 19 | |
| 20 | # Dossier daté du run courant |
| 21 | RUN_DIR="$BACKUP_DIR/$DATE_DIR" |
| 22 | |
| 23 | # Dossier "dernier backup" |
| 24 | LAST_DIR="$BACKUP_DIR/last" |
| 25 | |
| 26 | # Archives dans le dossier daté |
| 27 | BACKUP_FILE="$RUN_DIR/docker_backup.tar.gz" |
| 28 | CASAOS_FULL_TAR="$RUN_DIR/casaos_full_backup.tar.gz" |
| 29 | |
| 30 | # Dossier de configs / compose dans le run |
| 31 | CONFIG_DIR="$RUN_DIR" |
| 32 | |
| 33 | # Logs |
| 34 | LOG_FILE="/var/log/docker_backup.log" |
| 35 | |
| 36 | # Rapports HTML |
| 37 | HTML_REPORT="$RUN_DIR/report.html" |
| 38 | HTML_REPORT_LAST="$LAST_DIR/report.html" |
| 39 | HTML_INDEX_LAST="$LAST_DIR/index.html" |
| 40 | |
| 41 | # Publication WEB (HTML uniquement) |
| 42 | WEB_PUBLIC_DIR="/home/www-adm1n/docker_compose" |
| 43 | |
| 44 | # Dossiers à exclure (chemins relatifs à $SOURCE_DIR) |
| 45 | EXCLUDE_DIRS=( |
| 46 | "docker_var_lib" |
| 47 | "1_Backups" |
| 48 | "casaos_data/*" |
| 49 | "nginx_proxy/data/logs/*" |
| 50 | "yt-dl/video/*" |
| 51 | ) |
| 52 | |
| 53 | ############################# |
| 54 | # MODE TTY / CRON |
| 55 | ############################# |
| 56 | |
| 57 | # Si pas de terminal (cron), on désactive les couleurs |
| 58 | if [ -t 1 ]; then |
| 59 | USE_COLORS=1 |
| 60 | else |
| 61 | USE_COLORS=0 |
| 62 | fi |
| 63 | |
| 64 | ############################# |
| 65 | # COULEURS |
| 66 | ############################# |
| 67 | if [ "$USE_COLORS" = "1" ]; then |
| 68 | NC="\e[0m" |
| 69 | GREEN="\e[32m" |
| 70 | YELLOW="\e[33m" |
| 71 | CYAN="\e[36m" |
| 72 | RED="\e[31m" |
| 73 | else |
| 74 | NC="" |
| 75 | GREEN="" |
| 76 | YELLOW="" |
| 77 | CYAN="" |
| 78 | RED="" |
| 79 | fi |
| 80 | |
| 81 | ############################# |
| 82 | # DÉTECTION CASAOS |
| 83 | ############################# |
| 84 | |
| 85 | CASAOS_PRESENT=0 |
| 86 | if [ -d "/var/lib/casaos" ] || [ -d "$SOURCE_DIR/casaos_data" ] || command -v casaos >/dev/null 2>&1; then |
| 87 | CASAOS_PRESENT=1 |
| 88 | fi |
| 89 | |
| 90 | ############################# |
| 91 | # FONCTIONS |
| 92 | ############################# |
| 93 | |
| 94 | log_message() { |
| 95 | local msg="$1" |
| 96 | local now |
| 97 | now="$(date +'%Y-%m-%d %H:%M:%S')" |
| 98 | echo "$now - $msg" >> "$LOG_FILE" |
| 99 | echo -e "$now - $msg" |
| 100 | } |
| 101 | |
| 102 | format_time() { |
| 103 | local t=$1 |
| 104 | ((h=t/3600)) |
| 105 | ((m=(t%3600)/60)) |
| 106 | ((s=t%60)) |
| 107 | printf "%02d:%02d:%02d\n" $h $m $s |
| 108 | } |
| 109 | |
| 110 | check_pigz() { |
| 111 | if ! command -v pigz >/dev/null 2>&1; then |
| 112 | log_message "pigz n'est pas installé." |
| 113 | |
| 114 | if [ "$USE_COLORS" = "1" ]; then |
| 115 | echo -e "${YELLOW}pigz n'est pas installé. Voulez-vous l’installer ? (y/n)${NC}" |
| 116 | read -r rep |
| 117 | if [ "$rep" = "y" ]; then |
| 118 | sudo apt update && sudo apt install -y pigz || { |
| 119 | echo -e "${RED}Échec installation pigz. Abandon.${NC}" |
| 120 | exit 1 |
| 121 | } |
| 122 | else |
| 123 | echo -e "${RED}pigz requis. Abandon.${NC}" |
| 124 | exit 1 |
| 125 | fi |
| 126 | else |
| 127 | echo "ERREUR: pigz requis (mode cron non-interactif)." |
| 128 | exit 1 |
| 129 | fi |
| 130 | fi |
| 131 | } |
| 132 | |
| 133 | rotate_backups() { |
| 134 | log_message "Rotation backups (max $MAX_BACKUPS)..." |
| 135 | |
| 136 | # Liste uniquement les dossiers datés, ignore last/ |
| 137 | mapfile -t backups_dirs < <( |
| 138 | find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d \ |
| 139 | ! -name "last" \ |
| 140 | -printf "%T@ %p\n" 2>/dev/null \ |
| 141 | | sort -nr \ |
| 142 | | awk '{print $2}' |
| 143 | ) |
| 144 | |
| 145 | if [ "${#backups_dirs[@]}" -gt "$MAX_BACKUPS" ]; then |
| 146 | for d in "${backups_dirs[@]:$MAX_BACKUPS}"; do |
| 147 | log_message "Suppression ancien backup : $d" |
| 148 | rm -rf "$d" |
| 149 | done |
| 150 | fi |
| 151 | } |
| 152 | |
| 153 | get_size() { |
| 154 | du -sh "$1" 2>/dev/null | awk '{print $1}' |
| 155 | } |
| 156 | |
| 157 | html_escape() { |
| 158 | sed -e 's/&/\&/g' \ |
| 159 | -e 's/</\</g' \ |
| 160 | -e 's/>/\>/g' |
| 161 | } |
| 162 | |
| 163 | html_line() { |
| 164 | # $1=label $2=value $3=emoji $4=class |
| 165 | echo "<div class='row $4'><span class='emoji'>$3</span><span class='label'>$1</span><span class='value'>$2</span></div>" >> "$HTML_REPORT" |
| 166 | } |
| 167 | |
| 168 | |
| 169 | html_container_table() { |
| 170 | local dirs |
| 171 | dirs=$(find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -type d \ |
| 172 | ! -path "$SOURCE_DIR/casaos_data" \ |
| 173 | ! -path "$SOURCE_DIR/docker_var_lib" \ |
| 174 | ! -path "$SOURCE_DIR/portainer" \ |
| 175 | 2>/dev/null) |
| 176 | |
| 177 | if [ -z "$dirs" ]; then |
| 178 | echo "<div class='empty'>Aucun dossier Docker trouvé.</div>" >> "$HTML_REPORT" |
| 179 | return |
| 180 | fi |
| 181 | |
| 182 | echo "<div class='tblwrap'><table class='tbl'>" >> "$HTML_REPORT" |
| 183 | echo "<thead><tr><th>📦 Container</th><th>📏 Taille</th><th>📁 Chemin</th></tr></thead>" >> "$HTML_REPORT" |
| 184 | echo "<tbody>" >> "$HTML_REPORT" |
| 185 | |
| 186 | # tri par taille desc |
| 187 | du -sh $dirs 2>/dev/null | sort -hr | while read -r size path; do |
| 188 | name="$(basename "$path")" |
| 189 | esc_path="$(echo "$path" | html_escape)" |
| 190 | echo "<tr><td><b>$name</b></td><td>$size</td><td><code>$esc_path</code></td></tr>" >> "$HTML_REPORT" |
| 191 | done |
| 192 | |
| 193 | echo "</tbody></table></div>" >> "$HTML_REPORT" |
| 194 | } |
| 195 | |
| 196 | |
| 197 | generate_index_html() { |
| 198 | local base_dir="$1" |
| 199 | local output_file="$2" |
| 200 | |
| 201 | cat > "$output_file" <<EOF |
| 202 | <!doctype html> |
| 203 | <html lang="fr"> |
| 204 | <head> |
| 205 | <meta charset="utf-8"> |
| 206 | <title>Index Backups Docker</title> |
| 207 | <style> |
| 208 | body{font-family:Arial,sans-serif;background:#0b1220;color:#e5e7eb;margin:0;padding:24px} |
| 209 | .card{background:#111827;border:1px solid #1f2937;border-radius:12px;padding:16px;max-width:980px;margin:auto} |
| 210 | h1{margin:0 0 12px;font-size:22px} |
| 211 | a{color:#93c5fd;text-decoration:none} |
| 212 | a:hover{text-decoration:underline} |
| 213 | hr{border:0;border-top:1px solid #1f2937;margin:12px 0} |
| 214 | .toolbar{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:12px} |
| 215 | .btn{display:inline-block;padding:8px 12px;border-radius:10px;background:#0f172a;border:1px solid #1f2937} |
| 216 | .btn:hover{background:#111c33} |
| 217 | .item{padding:10px;border-radius:12px;margin:8px 0;background:#0f172a;border:1px solid #1f2937} |
| 218 | .row{display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap} |
| 219 | .left{display:flex;gap:10px;align-items:center} |
| 220 | .badge{font-size:12px;padding:4px 8px;border-radius:999px;background:#1f2937} |
| 221 | .badge-ok{background:#052e16;color:#86efac;border:1px solid #14532d} |
| 222 | .badge-warn{background:#451a03;color:#fcd34d;border:1px solid #92400e} |
| 223 | .meta{color:#94a3b8;font-size:12px;margin-top:6px} |
| 224 | small{color:#94a3b8} |
| 225 | .note{color:#94a3b8;font-size:12px;margin-top:10px} |
| 226 | </style> |
| 227 | </head> |
| 228 | <body> |
| 229 | <div class="card"> |
| 230 | <h1>📦 Index des Backups Docker</h1> |
| 231 | <small>Dossier backups: $base_dir</small> |
| 232 | |
| 233 | <div class="toolbar"> |
| 234 | <a class="btn" href="./report.html">📌 Ouvrir le LAST</a> |
| 235 | </div> |
| 236 | |
| 237 | <div class="note"> |
| 238 | ℹ️ Les backups datés ne sont pas exposés sur le web (sécurité). |
| 239 | Seul le rapport <b>LAST</b> est accessible. |
| 240 | </div> |
| 241 | |
| 242 | <hr> |
| 243 | EOF |
| 244 | |
| 245 | # Liste dossiers datés (ignore last) |
| 246 | find "$base_dir" -mindepth 1 -maxdepth 1 -type d ! -name "last" 2>/dev/null \ |
| 247 | | sort -r \ |
| 248 | | while read -r d; do |
| 249 | |
| 250 | name="$(basename "$d")" |
| 251 | tarfile="$d/docker_backup.tar.gz" |
| 252 | report="$d/report.html" |
| 253 | |
| 254 | mtime="$(date -r "$d" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo 'N/A')" |
| 255 | |
| 256 | if [ -f "$tarfile" ]; then |
| 257 | tarsize="$(du -sh "$tarfile" 2>/dev/null | awk '{print $1}')" |
| 258 | else |
| 259 | tarsize="N/A" |
| 260 | fi |
| 261 | |
| 262 | if [ -f "$report" ]; then |
| 263 | status="OK" |
| 264 | badge="badge-ok" |
| 265 | else |
| 266 | status="missing report" |
| 267 | badge="badge-warn" |
| 268 | fi |
| 269 | |
| 270 | cat >> "$output_file" <<EOF |
| 271 | <div class="item"> |
| 272 | <div class="row"> |
| 273 | <div class="left"> |
| 274 | <span>📄</span> |
| 275 | <span><b>$name</b></span> |
| 276 | </div> |
| 277 | <span class="badge $badge">$status</span> |
| 278 | </div> |
| 279 | <div class="meta">🕒 $mtime — 🗜️ docker_backup.tar.gz : <b>$tarsize</b></div> |
| 280 | </div> |
| 281 | EOF |
| 282 | done |
| 283 | |
| 284 | cat >> "$output_file" <<EOF |
| 285 | </div> |
| 286 | </body> |
| 287 | </html> |
| 288 | EOF |
| 289 | } |
| 290 | |
| 291 | ############################# |
| 292 | # DÉBUT SCRIPT |
| 293 | ############################# |
| 294 | |
| 295 | SECONDS=0 |
| 296 | |
| 297 | check_pigz |
| 298 | |
| 299 | mkdir -p "$RUN_DIR" |
| 300 | mkdir -p "$BACKUP_DIR" |
| 301 | touch "$LOG_FILE" |
| 302 | |
| 303 | echo -e "${CYAN}=== Backup Docker + Configs (Portainer / CasaOS) ===${NC}" |
| 304 | log_message "Début sauvegarde..." |
| 305 | log_message "Dossier backup (run) : $RUN_DIR" |
| 306 | log_message "Dossier backup (last): $LAST_DIR" |
| 307 | log_message "CasaOS détecté : $CASAOS_PRESENT" |
| 308 | |
| 309 | ############################################## |
| 310 | # 1) Archive Docker |
| 311 | ############################################## |
| 312 | |
| 313 | log_message "Création archive Docker : $BACKUP_FILE" |
| 314 | |
| 315 | EXCLUDE_ARGS=( |
| 316 | "--exclude=*.log" |
| 317 | "--exclude=.composer" |
| 318 | "--exclude=.aptitude" |
| 319 | "--exclude=.cache" |
| 320 | "--exclude=.cmake" |
| 321 | "--exclude=.yarn" |
| 322 | "--exclude=.w3m" |
| 323 | "--exclude=.pip" |
| 324 | "--exclude=.pm2" |
| 325 | "--exclude=.pm" |
| 326 | "--exclude=.bundle" |
| 327 | "--exclude=.gem" |
| 328 | "--exclude=.cpan" |
| 329 | "--exclude=.cpanm" |
| 330 | "--exclude=.git" |
| 331 | "--exclude=.local" |
| 332 | "--exclude=.npm" |
| 333 | "--exclude=.nvm" |
| 334 | "--exclude=.rvm" |
| 335 | "--exclude=node_modules" |
| 336 | "--exclude=lost+found" |
| 337 | ) |
| 338 | |
| 339 | for d in "${EXCLUDE_DIRS[@]}"; do |
| 340 | EXCLUDE_ARGS+=( "--exclude=$d" ) |
| 341 | done |
| 342 | |
| 343 | tar -cf "$BACKUP_FILE" -I pigz \ |
| 344 | --directory="$SOURCE_DIR" \ |
| 345 | "${EXCLUDE_ARGS[@]}" \ |
| 346 | . 2>> "$LOG_FILE" |
| 347 | |
| 348 | if [ $? -ne 0 ]; then |
| 349 | log_message "ERREUR lors de la création du tar Docker." |
| 350 | echo -e "${RED}Erreur lors de la création de l’archive Docker.${NC}" |
| 351 | exit 1 |
| 352 | fi |
| 353 | |
| 354 | ############################################## |
| 355 | # 2) Extraction docker-compose CasaOS (si présent) |
| 356 | ############################################## |
| 357 | |
| 358 | if [ "$CASAOS_PRESENT" = "1" ]; then |
| 359 | log_message "Extraction des docker-compose CasaOS..." |
| 360 | if [ -d "$SOURCE_DIR/casaos_data" ]; then |
| 361 | find "$SOURCE_DIR/casaos_data" -maxdepth 3 -type f -name "docker-compose.y*ml" | while read -r file; do |
| 362 | container_name=$(basename "$(dirname "$file")") |
| 363 | dest_dir="$CONFIG_DIR/casaos/$container_name" |
| 364 | mkdir -p "$dest_dir" |
| 365 | cp "$file" "$dest_dir/" |
| 366 | log_message " [CasaOS] compose : $file" |
| 367 | done |
| 368 | fi |
| 369 | else |
| 370 | log_message "CasaOS non détecté → ignoré." |
| 371 | fi |
| 372 | |
| 373 | ############################################## |
| 374 | # 3) Extraction docker-compose Docker (hors CasaOS) |
| 375 | ############################################## |
| 376 | |
| 377 | log_message "Extraction des docker-compose Docker..." |
| 378 | find "$SOURCE_DIR" -mindepth 2 -maxdepth 2 -type f -name "docker-compose.y*ml" \ |
| 379 | ! -path "$SOURCE_DIR/casaos_data/*" \ |
| 380 | ! -path "$SOURCE_DIR/docker_var_lib/*" \ |
| 381 | 2>/dev/null | while read -r file; do |
| 382 | container_name=$(basename "$(dirname "$file")") |
| 383 | dest_dir="$CONFIG_DIR/Compose/$container_name" |
| 384 | mkdir -p "$dest_dir" |
| 385 | cp "$file" "$dest_dir/" |
| 386 | log_message " [Docker] compose : $file" |
| 387 | done |
| 388 | |
| 389 | ############################################## |
| 390 | # 4) Backup config Portainer (si volume présent) |
| 391 | ############################################## |
| 392 | |
| 393 | PORTAINER_CONFIG_DIR="$CONFIG_DIR/portainer" |
| 394 | PORTAINER_FILES=0 |
| 395 | |
| 396 | if docker volume inspect portainer_data >/dev/null 2>&1; then |
| 397 | log_message "Sauvegarde config Portainer..." |
| 398 | mkdir -p "$PORTAINER_CONFIG_DIR" |
| 399 | cp -r /var/lib/docker/volumes/portainer_data/_data/* "$PORTAINER_CONFIG_DIR/" 2>>"$LOG_FILE" |
| 400 | PORTAINER_FILES=$(find "$PORTAINER_CONFIG_DIR" -type f 2>/dev/null | wc -l) |
| 401 | else |
| 402 | PORTAINER_CONFIG_DIR="" |
| 403 | fi |
| 404 | |
| 405 | ############################################## |
| 406 | # 5) Backup CasaOS (config + FULL TAR) si présent |
| 407 | ############################################## |
| 408 | |
| 409 | CASAOS_CONFIG_DIR="$CONFIG_DIR/casaos/full_config" |
| 410 | |
| 411 | if [ "$CASAOS_PRESENT" = "1" ]; then |
| 412 | if [ -d "/var/lib/casaos" ]; then |
| 413 | log_message "Sauvegarde config CasaOS full..." |
| 414 | mkdir -p "$CASAOS_CONFIG_DIR" |
| 415 | cp -r /var/lib/casaos/* "$CASAOS_CONFIG_DIR/" 2>>"$LOG_FILE" |
| 416 | fi |
| 417 | |
| 418 | CASAOS_TAR_SOURCES=() |
| 419 | [ -d "/var/lib/casaos" ] && CASAOS_TAR_SOURCES+=( "/var/lib/casaos" ) |
| 420 | [ -d "$SOURCE_DIR/casaos_data" ] && CASAOS_TAR_SOURCES+=( "$SOURCE_DIR/casaos_data" ) |
| 421 | |
| 422 | if [ "${#CASAOS_TAR_SOURCES[@]}" -gt 0 ]; then |
| 423 | log_message "Création archive CasaOS FULL : $CASAOS_FULL_TAR" |
| 424 | tar -cf "$CASAOS_FULL_TAR" -I pigz "${CASAOS_TAR_SOURCES[@]}" 2>>"$LOG_FILE" |
| 425 | fi |
| 426 | fi |
| 427 | |
| 428 | ############################################## |
| 429 | # 6) Stats + rotation |
| 430 | ############################################## |
| 431 | |
| 432 | CASAOS_COMPOSE_BACKUP=0 |
| 433 | if [ "$CASAOS_PRESENT" = "1" ]; then |
| 434 | CASAOS_COMPOSE_BACKUP=$(find "$CONFIG_DIR/casaos" -maxdepth 3 -type f -name "docker-compose.y*ml" 2>/dev/null | wc -l) |
| 435 | fi |
| 436 | |
| 437 | DOCKER_COMPOSE_BACKUP=$(find "$CONFIG_DIR/Compose" -type f -name "docker-compose.y*ml" 2>/dev/null | wc -l) |
| 438 | |
| 439 | SOURCE_COMPOSE_COUNT=$(find "$SOURCE_DIR" -mindepth 2 -maxdepth 2 -type f -name "docker-compose.y*ml" \ |
| 440 | ! -path "$SOURCE_DIR/casaos_data/*" \ |
| 441 | ! -path "$SOURCE_DIR/docker_var_lib/*" \ |
| 442 | 2>/dev/null | wc -l) |
| 443 | |
| 444 | CONTAINERS_DOCKER=$(find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -type d \ |
| 445 | ! -path "$SOURCE_DIR/casaos_data" \ |
| 446 | ! -path "$SOURCE_DIR/docker_var_lib" \ |
| 447 | ! -path "$SOURCE_DIR/portainer" 2>/dev/null | wc -l) |
| 448 | |
| 449 | rotate_backups |
| 450 | |
| 451 | TOTAL_BACKUP_SETS=$(find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d ! -name "last" 2>/dev/null | wc -l) |
| 452 | |
| 453 | mapfile -t CURRENT_SETS < <(find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d ! -name "last" 2>/dev/null | sort) |
| 454 | OLDEST_DIR="${CURRENT_SETS[0]}" |
| 455 | NEWEST_DIR="${CURRENT_SETS[${#CURRENT_SETS[@]}-1]}" |
| 456 | |
| 457 | DURATION="$(format_time $SECONDS)" |
| 458 | |
| 459 | TAR_SIZE="$(get_size "$BACKUP_FILE")" |
| 460 | |
| 461 | CASAOS_FULL_TAR_SIZE="" |
| 462 | [ -f "$CASAOS_FULL_TAR" ] && CASAOS_FULL_TAR_SIZE="$(get_size "$CASAOS_FULL_TAR")" |
| 463 | |
| 464 | PORTAINER_CONFIG_SIZE="" |
| 465 | [ -n "$PORTAINER_CONFIG_DIR" ] && [ -d "$PORTAINER_CONFIG_DIR" ] && PORTAINER_CONFIG_SIZE="$(get_size "$PORTAINER_CONFIG_DIR")" |
| 466 | |
| 467 | |
| 468 | ############################################## |
| 469 | # 7) Génération rapport HTML (run daté) |
| 470 | ############################################## |
| 471 | |
| 472 | cat > "$HTML_REPORT" <<EOF |
| 473 | <!doctype html> |
| 474 | <html lang="fr"> |
| 475 | <head> |
| 476 | <meta charset="utf-8"> |
| 477 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 478 | <title>Backup Docker Report - $DATE_DIR</title> |
| 479 | <style> |
| 480 | :root{ |
| 481 | --bg:#0b1220; |
| 482 | --card:#111827; |
| 483 | --border:#1f2937; |
| 484 | --muted:#94a3b8; |
| 485 | --text:#e5e7eb; |
| 486 | --blue:#93c5fd; |
| 487 | --ok:#22c55e; |
| 488 | --warn:#f59e0b; |
| 489 | --bad:#ef4444; |
| 490 | --row:#0f172a; |
| 491 | --row2:#0c1428; |
| 492 | } |
| 493 | *{box-sizing:border-box} |
| 494 | body{font-family:Arial,system-ui,sans-serif;background:var(--bg);color:var(--text);margin:0;padding:22px} |
| 495 | .card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;max-width:1100px;margin:auto} |
| 496 | h1{margin:0 0 8px;font-size:22px} |
| 497 | small{color:var(--muted)} |
| 498 | hr{border:0;border-top:1px solid var(--border);margin:14px 0} |
| 499 | h2{margin:16px 0 8px;font-size:15px;color:var(--blue)} |
| 500 | |
| 501 | .row{display:flex;gap:12px;padding:10px;border-radius:12px;align-items:center} |
| 502 | .row:nth-child(odd){background:var(--row)} |
| 503 | .row:nth-child(even){background:var(--row2)} |
| 504 | .label{flex:1;color:#cbd5e1} |
| 505 | .value{font-weight:bold;color:#f8fafc} |
| 506 | .emoji{width:26px;display:inline-block} |
| 507 | |
| 508 | .ok{border-left:4px solid var(--ok)} |
| 509 | .warn{border-left:4px solid var(--warn)} |
| 510 | .bad{border-left:4px solid var(--bad)} |
| 511 | .info{border-left:4px solid #3b82f6} |
| 512 | |
| 513 | .grid{display:grid;grid-template-columns:1fr 1fr;gap:10px} |
| 514 | @media (max-width:900px){.grid{grid-template-columns:1fr}} |
| 515 | |
| 516 | .tblwrap{overflow:auto;border:1px solid var(--border);border-radius:14px} |
| 517 | .tbl{width:100%;border-collapse:collapse;min-width:720px} |
| 518 | .tbl th,.tbl td{padding:10px;border-bottom:1px solid var(--border);text-align:left;vertical-align:top} |
| 519 | .tbl th{color:#cbd5e1;background:#0f172a;position:sticky;top:0} |
| 520 | .tbl tr:hover td{background:#0a1020} |
| 521 | code{color:#cbd5e1} |
| 522 | .empty{padding:10px;color:var(--muted)} |
| 523 | .footer{margin-top:14px;color:var(--muted);font-size:12px} |
| 524 | .badge{display:inline-block;padding:3px 8px;border-radius:999px;border:1px solid var(--border);font-size:12px;margin-left:8px} |
| 525 | .badge-ok{background:#052e16;color:#86efac;border-color:#14532d} |
| 526 | .badge-warn{background:#451a03;color:#fcd34d;border-color:#92400e} |
| 527 | </style> |
| 528 | </head> |
| 529 | <body> |
| 530 | <div class="card"> |
| 531 | <h1>📦 Backup Docker / CasaOS / Portainer</h1> |
| 532 | <small>📅 $DATE_DIR — 🕒 $(date +'%Y-%m-%d %H:%M:%S')</small> |
| 533 | |
| 534 | <hr> |
| 535 | |
| 536 | <h2>🔧 Global</h2> |
| 537 | EOF |
| 538 | |
| 539 | # lignes global |
| 540 | html_line "Dossier backup courant" "$(echo "$RUN_DIR" | html_escape)" "📁" "info" |
| 541 | html_line "Durée totale" "$DURATION" "⏱️" "info" |
| 542 | html_line "Nombre de sets de backup" "$TOTAL_BACKUP_SETS" "🗂️" "info" |
| 543 | |
| 544 | echo "<h2>🐳 Docker</h2>" >> "$HTML_REPORT" |
| 545 | html_line "Containers détectés" "$CONTAINERS_DOCKER" "🧱" "ok" |
| 546 | html_line "Compose source" "$SOURCE_COMPOSE_COUNT" "📄" "info" |
| 547 | html_line "Compose backup" "$DOCKER_COMPOSE_BACKUP" "📦" "info" |
| 548 | html_line "Archive Docker (.tar.gz)" "$(echo "$BACKUP_FILE ($TAR_SIZE)" | html_escape)" "🗜️" "ok" |
| 549 | |
| 550 | echo "<h2>🏠 CasaOS</h2>" >> "$HTML_REPORT" |
| 551 | if [ "$CASAOS_PRESENT" = "1" ]; then |
| 552 | html_line "CasaOS détecté" "OUI" "✅" "ok" |
| 553 | html_line "Compose CasaOS backup" "$CASAOS_COMPOSE_BACKUP" "📄" "info" |
| 554 | if [ -f "$CASAOS_FULL_TAR" ]; then |
| 555 | html_line "Archive CasaOS FULL (.tar.gz)" "$(echo "$CASAOS_FULL_TAR ($CASAOS_FULL_TAR_SIZE)" | html_escape)" "🗜️" "ok" |
| 556 | else |
| 557 | html_line "Archive CasaOS FULL (.tar.gz)" "Non générée" "⚠️" "warn" |
| 558 | fi |
| 559 | else |
| 560 | html_line "CasaOS détecté" "NON (ignoré)" "🚫" "warn" |
| 561 | fi |
| 562 | |
| 563 | echo "<h2>🧭 Portainer</h2>" >> "$HTML_REPORT" |
| 564 | html_line "Fichiers config Portainer" "$PORTAINER_FILES" "📁" "info" |
| 565 | [ -n "$PORTAINER_CONFIG_SIZE" ] && html_line "Taille config Portainer" "$PORTAINER_CONFIG_SIZE" "📏" "info" |
| 566 | |
| 567 | echo "<h2>📊 Containers / Tailles (dossiers dans $SOURCE_DIR)</h2>" >> "$HTML_REPORT" |
| 568 | html_container_table |
| 569 | |
| 570 | cat >> "$HTML_REPORT" <<EOF |
| 571 | <div class="footer"> |
| 572 | ✅ Rapport généré automatiquement par le script de backup.<br> |
| 573 | ⚠️ Ne partage pas ce rapport publiquement : il révèle des chemins et des infos de structure. |
| 574 | </div> |
| 575 | </div> |
| 576 | </body> |
| 577 | </html> |
| 578 | EOF |
| 579 | |
| 580 | log_message "Rapport HTML généré : $HTML_REPORT" |
| 581 | |
| 582 | ################## |
| 583 | |
| 584 | |
| 585 | ############################################## |
| 586 | # 8) MAJ last/ (SANS copier les archives .gz) |
| 587 | ############################################## |
| 588 | |
| 589 | log_message "Mise à jour du dossier LAST : $LAST_DIR" |
| 590 | |
| 591 | rm -rf "$LAST_DIR" |
| 592 | mkdir -p "$LAST_DIR" |
| 593 | |
| 594 | # HTML |
| 595 | cp -f "$HTML_REPORT" "$LAST_DIR/report.html" |
| 596 | |
| 597 | # Compose / configs (pas d'archives) |
| 598 | [ -d "$CONFIG_DIR/Compose" ] && cp -a "$CONFIG_DIR/Compose" "$LAST_DIR/" |
| 599 | [ -d "$CONFIG_DIR/portainer" ] && cp -a "$CONFIG_DIR/portainer" "$LAST_DIR/" |
| 600 | [ "$CASAOS_PRESENT" = "1" ] && [ -d "$CONFIG_DIR/casaos" ] && cp -a "$CONFIG_DIR/casaos" "$LAST_DIR/" |
| 601 | |
| 602 | # Index HTML last/ |
| 603 | generate_index_html "$BACKUP_DIR" "$HTML_INDEX_LAST" |
| 604 | |
| 605 | log_message "LAST mis à jour ✅" |
| 606 | log_message "Dernier rapport HTML : $HTML_REPORT_LAST" |
| 607 | log_message "Index HTML last : $HTML_INDEX_LAST" |
| 608 | |
| 609 | ############################################## |
| 610 | # 9) Publication WEB (HTML UNIQUEMENT) |
| 611 | ############################################## |
| 612 | |
| 613 | log_message "Publication web (HTML only) vers : $WEB_PUBLIC_DIR" |
| 614 | |
| 615 | rm -rf "$WEB_PUBLIC_DIR" |
| 616 | mkdir -p "$WEB_PUBLIC_DIR" |
| 617 | |
| 618 | |
| 619 | cp -f "$LAST_DIR/report.html" "$WEB_PUBLIC_DIR/report.html" |
| 620 | cp -f "$LAST_DIR/index.html" "$WEB_PUBLIC_DIR/" 2>/dev/null || true |
| 621 | |
| 622 | log_message "Publication web OK ✅" |
| 623 | |
| 624 | ############################################## |
| 625 | # 10) Rapport CONSOLE |
| 626 | ############################################## |
| 627 | |
| 628 | echo -e "${GREEN}===== BACKUP TERMINÉ ✅ =====${NC}" |
| 629 | |
| 630 | ############################################## |
| 631 | # LISTE DES DOSSIERS DOCKER PAR TAILLE (ASCII) |
| 632 | ############################################## |
| 633 | |
| 634 | echo "" |
| 635 | echo -e "${CYAN}----- LISTE/TAILLE DES RÉPERTOIRES DOCKER -----${NC}" |
| 636 | |
| 637 | DOCKER_DIRS=$(find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -type d \ |
| 638 | ! -path "$SOURCE_DIR/casaos_data" \ |
| 639 | ! -path "$SOURCE_DIR/docker_var_lib" \ |
| 640 | ! -path "$SOURCE_DIR/portainer" \ |
| 641 | 2>/dev/null) |
| 642 | |
| 643 | if [ -z "$DOCKER_DIRS" ]; then |
| 644 | echo -e "${YELLOW}(Aucun dossier Docker trouvé)${NC}" |
| 645 | else |
| 646 | echo -e "${CYAN}┌──────────────────────────┬─────────┬────────────────────────────────────────┐${NC}" |
| 647 | printf "${CYAN}│ %-24s │ %-7s │ %-38s │${NC}\n" "Container" "Taille" "Chemin" |
| 648 | echo -e "${CYAN}├──────────────────────────┼─────────┼────────────────────────────────────────┤${NC}" |
| 649 | |
| 650 | # du + tri par taille desc |
| 651 | du -sh $DOCKER_DIRS 2>/dev/null | sort -hr | while read -r size path; do |
| 652 | name=$(basename "$path") |
| 653 | printf "│ %-24s │ %-7s │ %-38s │\n" "$name" "$size" "$path" |
| 654 | done |
| 655 | |
| 656 | echo -e "${CYAN}└──────────────────────────┴─────────┴────────────────────────────────────────┘${NC}" |
| 657 | fi |
| 658 | |
| 659 | ############################################## |
| 660 | |
| 661 | echo -e "${CYAN}Dossier backup courant : ${YELLOW}$RUN_DIR${NC}" |
| 662 | echo "" |
| 663 | |
| 664 | echo -e "${CYAN}----- DOCKER -----${NC}" |
| 665 | echo "Containers Docker détectés : $CONTAINERS_DOCKER" |
| 666 | echo "Compose source (Docker) : $SOURCE_COMPOSE_COUNT" |
| 667 | echo "Compose backup (Docker) : $DOCKER_COMPOSE_BACKUP" |
| 668 | echo "Archive Docker (run) : $BACKUP_FILE ($TAR_SIZE)" |
| 669 | echo "" |
| 670 | |
| 671 | echo -e "${CYAN}----- CASAOS -----${NC}" |
| 672 | if [ "$CASAOS_PRESENT" = "1" ]; then |
| 673 | echo "CasaOS détecté : OUI" |
| 674 | echo "Compose CasaOS backup : $CASAOS_COMPOSE_BACKUP" |
| 675 | [ -f "$CASAOS_FULL_TAR" ] && echo "Archive CasaOS FULL (run) : $CASAOS_FULL_TAR ($CASAOS_FULL_TAR_SIZE)" |
| 676 | else |
| 677 | echo "CasaOS détecté : NON (ignoré)" |
| 678 | fi |
| 679 | echo "" |
| 680 | |
| 681 | echo -e "${CYAN}----- PORTAINER -----${NC}" |
| 682 | echo "Fichiers config Portainer : $PORTAINER_FILES" |
| 683 | [ -n "$PORTAINER_CONFIG_SIZE" ] && echo "Taille Portainer (run) : $PORTAINER_CONFIG_SIZE" |
| 684 | echo "" |
| 685 | |
| 686 | echo -e "${CYAN}----- GLOBAL -----${NC}" |
| 687 | echo "Sets de backup : $TOTAL_BACKUP_SETS" |
| 688 | echo "Oldest backup : ${OLDEST_DIR:-N/A}" |
| 689 | echo "Newest backup : ${NEWEST_DIR:-N/A}" |
| 690 | echo "Durée totale : $DURATION" |
| 691 | echo "" |
| 692 | echo "HTML (run) : $HTML_REPORT" |
| 693 | echo "HTML (last) : $HTML_REPORT_LAST" |
| 694 | echo "WEB index : $WEB_PUBLIC_DIR/index.html" |
| 695 | echo "WEB report : $WEB_PUBLIC_DIR/report.html" |
| 696 | |
| 697 | ############################################## |
| 698 | # JSON optionnel |
| 699 | ############################################## |
| 700 | |
| 701 | if [ "$1" = "--json" ]; then |
| 702 | echo "" |
| 703 | echo "{" |
| 704 | echo " \"date_raw\": \"$DATE_RAW\"," |
| 705 | echo " \"date_dir\": \"$DATE_DIR\"," |
| 706 | echo " \"run_dir\": \"$RUN_DIR\"," |
| 707 | echo " \"last_dir\": \"$LAST_DIR\"," |
| 708 | echo " \"docker_backup_file\": \"$BACKUP_FILE\"," |
| 709 | echo " \"docker_backup_size\": \"$TAR_SIZE\"," |
| 710 | echo " \"casaos_present\": $CASAOS_PRESENT," |
| 711 | echo " \"casaos_full_backup_file\": \"${CASAOS_FULL_TAR:-}\"," |
| 712 | echo " \"casaos_full_backup_size\": \"${CASAOS_FULL_TAR_SIZE:-}\"," |
| 713 | echo " \"portainer_files\": $PORTAINER_FILES," |
| 714 | echo " \"backup_sets\": $TOTAL_BACKUP_SETS," |
| 715 | echo " \"duration\": \"$DURATION\"," |
| 716 | echo " \"html_report_run\": \"$HTML_REPORT\"," |
| 717 | echo " \"html_report_last\": \"$HTML_REPORT_LAST\"," |
| 718 | echo " \"web_index\": \"$WEB_PUBLIC_DIR/index.html\"," |
| 719 | echo " \"web_report\": \"$WEB_PUBLIC_DIR/report.html\"" |
| 720 | echo "}" |
| 721 | fi |
| 722 |