1
0
postauto/update.sh
2025-08-13 08:29:39 +02:00

313 lines
12 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -Eeuo pipefail
# ────────── Helpers & couleurs ──────────
if [ -t 1 ]; then
ROUGE='\e[31m'; VERT='\e[32m'; JAUNE='\e[33m'; BLEU='\e[34m'; NORMAL='\e[0m'
else
ROUGE=''; VERT=''; JAUNE=''; BLEU=''; NORMAL=''
fi
log() { printf "${BLEU}%s${NORMAL}\n" "$*"; }
ok() { printf "${VERT}%s${NORMAL}\n" "$*"; }
warn() { printf "${JAUNE}%s${NORMAL}\n" "$*"; }
err() { printf "${ROUGE}%s${NORMAL}\n" "$*"; }
die() { err "$*"; exit 1; }
install_bin(){ install -m 755 "$1" "$2"; }
# ────────── Paths ──────────
BIN_DIR="$HOME/bin"
AUTOPOST_DIR="$HOME/autopost"
BASHRC_FILE="$HOME/.bashrc"
BASH_COMPLETION_DIR="$HOME/.bash_completion.d"
CONF_SH="$AUTOPOST_DIR/conf.sh"
CFG_JS="$AUTOPOST_DIR/config.js"
mkdir -p "$BIN_DIR" "$AUTOPOST_DIR" "$BASH_COMPLETION_DIR"
TMP_DIR="$(mktemp -d)"
cleanup(){ rm -rf "$TMP_DIR"; }
trap cleanup EXIT
updated=0
errors=0
# ────────── MAJ fichiers distants ──────────
declare -A FILES
FILES["$AUTOPOST_DIR/analyzer.sh"]="https://tig.unfr.pw/UNFR/postauto/raw/branch/main/autopost/analyzer.sh"
FILES["$AUTOPOST_DIR/posteur.sh"]="https://tig.unfr.pw/UNFR/postauto/raw/branch/main/autopost/posteur.sh"
FILES["$AUTOPOST_DIR/common.sh"]="https://tig.unfr.pw/UNFR/postauto/raw/branch/main/autopost/common.sh"
FILES["$BIN_DIR/postauto"]="https://tig.unfr.pw/UNFR/postauto/raw/branch/main/bin/postauto"
FILES["$AUTOPOST_DIR/server.js"]="https://tig.unfr.pw/UNFR/postauto/raw/branch/main/autopost/server.js"
log "Vérification/MAJ des fichiers…"
for LOCAL in "${!FILES[@]}"; do
URL="${FILES[$LOCAL]}"
TMP="$TMP_DIR/$(basename "$LOCAL").dl"
curl -fsSL "$URL" -o "$TMP" || die "Téléchargement échoué: $URL"
if [ ! -f "$LOCAL" ] || ! cmp -s "$LOCAL" "$TMP"; then
cp -f "$LOCAL" "$LOCAL.bak" 2>/dev/null || true
case "$LOCAL" in
*postauto|*.sh) install_bin "$TMP" "$LOCAL" ;;
*) install -m 644 "$TMP" "$LOCAL" ;;
esac
ok "Mise à jour: $LOCAL"
updated=1
fi
done
# ────────── Déplacer la complétion vers ~/.bash_completion.d & supprimer l'ancien bloc ──────────
DEBUT_MARKER="# DEBUT COMPLETION POSTAUTO"
FIN_MARKER="# FIN COMPLETION POSTAUTO"
if [ -f "$BASHRC_FILE" ] && grep -q "$DEBUT_MARKER" "$BASHRC_FILE"; then
log "Suppression de l'ancien bloc de complétion dans $BASHRC_FILE"
cp "$BASHRC_FILE" "${BASHRC_FILE}.bak"
sed -i "/$DEBUT_MARKER/,/$FIN_MARKER/d" "$BASHRC_FILE"
ok "Ancien bloc supprimé."
updated=1
fi
COMP_FILE="$BASH_COMPLETION_DIR/postauto"
read -r -d '' COMPLETION_CODE <<'EOF'
# completion postauto
_autopost_completion() {
local cur prev opts
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="start stop restart show status createdb add log check update"
if [ $COMP_CWORD -eq 1 ]; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ); return 0
fi
if [ $COMP_CWORD -eq 2 ] && [ "${COMP_WORDS[1]}" = "add" ]; then
COMPREPLY=( $(compgen -f -- "${cur}") ); return 0
fi
}
complete -F _autopost_completion postauto
EOF
if [ ! -s "$COMP_FILE" ] || ! cmp -s <(printf "%s" "$COMPLETION_CODE") "$COMP_FILE"; then
printf "%s" "$COMPLETION_CODE" > "$COMP_FILE"
ok "Completion installée: $COMP_FILE"
# hook .bashrc si pas déjà présent
grep -q '\.bash_completion.d/postauto' "$BASHRC_FILE" 2>/dev/null || \
echo '[ -f "$HOME/.bash_completion.d/postauto" ] && . "$HOME/.bash_completion.d/postauto"' >> "$BASHRC_FILE"
updated=1
fi
# ────────── Outils externes (optionnel: installe si manquants) ──────────
ensure_cmd(){ command -v "$1" >/dev/null 2>&1; }
if ! ensure_cmd 7z; then
log "Installation 7z…"
pushd "$TMP_DIR" >/dev/null
wget -q -o /dev/null -O 7z.tar.xz "https://7-zip.org/a/7z2409-linux-x64.tar.xz"
tar -xJf 7z.tar.xz
ZBIN="$(find . -maxdepth 1 -type f -name '7zz*' -perm -u+x | head -n1)"
[ -n "$ZBIN" ] || die "Binaire 7z introuvable"
install_bin "$ZBIN" "$BIN_DIR/7z"
popd >/dev/null
fi
if ! ensure_cmd BDInfo; then
log "Installation BDInfo…"
pushd "$TMP_DIR" >/dev/null
curl -fsSL -o bdinfo.zip "https://github.com/dotnetcorecorner/BDInfo/releases/download/linux-2.0.6/bdinfo_linux_v2.0.6.zip"
unzip -q bdinfo.zip
BDBIN="$(find . -type f -name BDInfo -perm -u+x | head -n1)"
[ -n "$BDBIN" ] || die "BDInfo introuvable"
install_bin "$BDBIN" "$BIN_DIR/BDInfo"
popd >/dev/null
fi
if ! ensure_cmd BDInfoDataSubstractor; then
log "Installation BDInfoDataSubstractor…"
pushd "$TMP_DIR" >/dev/null
curl -fsSL -o substractor.zip "https://github.com/dotnetcorecorner/BDInfo/releases/download/linux-2.0.6/bdinfodatasubstractor_linux_v2.0.6.zip"
unzip -q substractor.zip
SBBIN="$(find . -type f -name BDInfoDataSubstractor -perm -u+x | head -n1)"
[ -n "$SBBIN" ] || die "BDInfoDataSubstractor introuvable"
install_bin "$SBBIN" "$BIN_DIR/BDInfoDataSubstractor"
popd >/dev/null
fi
# ────────── VALIDATION conf.sh (sans exécuter) ──────────
placeholder_re='^(|A[[:space:]]*NOUS|A[[:space:]]*RETROUVER|CHANGE|CHANGEME|TODO|example|your|/path/to|Nom)$'
check_conf() {
local file="$1"
[[ -f "$file" ]] || { err "Manquant: $file"; errors=$((errors+1)); return; }
log "Validation de $file"
# parse simple: NAME=VALUE (ignore commentaires)
declare -A V=()
while IFS= read -r line; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ "$line" =~ ^[[:space:]]*$ ]] && continue
line="${line#export }"
if [[ "$line" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=(.*)$ ]]; then
name="${BASH_REMATCH[1]}"
val="${BASH_REMATCH[2]}"
val="${val%%#*}"; val="${val%%;*}"
val="$(echo -n "$val" | sed -E "s/^[[:space:]]*//; s/[[:space:]]*$//")"
val="$(echo -n "$val" | sed -E "s/^['\"]//; s/['\"]$//")"
V["$name"]="$val"
fi
done < "$file"
# requis absolus
req=(URL_API APIKEY DOSSIER_GLOBAL DOSSIER_NFO DOSSIER_LOGS DOSSIER_NZB_ATTENTE DOSSIER_NZB_FINAL MOVE_CMD MYSQL_TABLE dbtype)
for k in "${req[@]}"; do
v="${V[$k]:-}"
if [[ "$v" =~ $placeholder_re ]]; then
err "conf.sh: '$k' non renseigné (valeur='$v')"; errors=$((errors+1))
fi
done
# MOVE_CMD autorisé
case "${V[MOVE_CMD]:-}" in
"cp -rl"|"cp -rs"|"ln -s"|"mv"|"cp") : ;;
*) err "conf.sh: MOVE_CMD invalide ('${V[MOVE_CMD]:-}'). Choix: cp -rl | cp -rs | ln -s | mv | cp"; errors=$((errors+1));;
esac
# répertoires existants (on ne les crée pas ici, on alerte)
dirs=(DOSSIER_GLOBAL DOSSIER_NFO DOSSIER_LOGS DOSSIER_NZB_ATTENTE DOSSIER_NZB_FINAL)
for k in "${dirs[@]}"; do
p="${V[$k]:-}"
if [[ -z "$p" || ! -d "$p" ]]; then
err "conf.sh: dossier '$k' introuvable: $p"; errors=$((errors+1))
fi
done
# bloc provider Usenet requis
for k in NG_HOST NG_PORT NG_USER NG_PASS NG_NBR_CONN; do
v="${V[$k]:-}"
if [[ "$v" =~ $placeholder_re ]]; then
err "conf.sh: '$k' non renseigné"; errors=$((errors+1))
fi
done
# types numériques
[[ "${V[NG_PORT]:-}" =~ ^[0-9]+$ ]] || { err "conf.sh: NG_PORT doit être numérique"; errors=$((errors+1)); }
[[ "${V[NG_NBR_CONN]:-}" =~ ^[0-9]+$ ]] || { err "conf.sh: NG_NBR_CONN doit être numérique"; errors=$((errors+1)); }
# DB: règles conditionnelles
case "${V[dbtype]:-}" in
sqlite)
# DB_FILE requis, MySQL* facultatifs
if [[ -z "${V[DB_FILE]:-}" || "${V[DB_FILE]}" =~ $placeholder_re ]]; then
err "conf.sh: DB_FILE requis en mode sqlite"; errors=$((errors+1))
else
dbdir="$(dirname -- "${V[DB_FILE]}")"
[[ -d "$dbdir" ]] || { err "conf.sh: dossier DB_FILE inexistant: $dbdir"; errors=$((errors+1)); }
fi
;;
mysql)
# champs MySQL requis, DB_FILE facultatif
for k in MYSQL_HOST MYSQL_PORT MYSQL_USER MYSQL_PASS MYSQL_DB; do
v="${V[$k]:-}"
if [[ "$v" =~ $placeholder_re ]]; then
err "conf.sh: '$k' requis en mode mysql"; errors=$((errors+1))
fi
done
[[ "${V[MYSQL_PORT]:-}" =~ ^[0-9]+$ ]] || { err "conf.sh: MYSQL_PORT doit être numérique"; errors=$((errors+1)); }
;;
*)
err "conf.sh: dbtype doit être 'sqlite' ou 'mysql' (actuel='${V[dbtype]:-}')"; errors=$((errors+1))
;;
esac
}
check_conf "$CONF_SH"
# ────────── VALIDATION config.js (avec Node) ──────────
validate_config_js() {
[[ -f "$CFG_JS" ]] || { err "Manquant: $CFG_JS"; errors=$((errors+1)); return; }
log "Validation de $CFG_JS"
local CHECK="$TMP_DIR/check_config.js"
cat > "$CHECK" <<'JS'
const fs = require('fs');
const path = process.argv[2];
function isEmptyOrPlaceholder(v){
if (v == null) return true;
if (typeof v === 'string') {
const s = v.trim();
if (!s) return true;
const P = [/^voir/i, /^change/i, /^changeme/i, /^todo/i, /^example/i, /^your/i, /^\/path\/to/i];
if (P.some(rx => rx.test(s))) return true;
}
return false;
}
try {
const cfg = require(path);
let errs = [];
// requis généraux
if (!Number.isInteger(cfg.port) || cfg.port < 1 || cfg.port > 65535)
errs.push("config.js: 'port' doit être un entier 1-65535");
if (isEmptyOrPlaceholder(cfg.name)) errs.push("config.js: 'name' non renseigné");
if (isEmptyOrPlaceholder(cfg.sessionSecret)) errs.push("config.js: 'sessionSecret' non renseigné");
if (typeof cfg.trustProxy === 'undefined') errs.push("config.js: 'trustProxy' manquant");
if (typeof cfg.cookieSecure === 'undefined') errs.push("config.js: 'cookieSecure' manquant");
if (isEmptyOrPlaceholder(cfg.DB_TABLE)) errs.push("config.js: 'DB_TABLE' non renseigné");
// chemins
const pathKeys = ['finishdirectory', 'logdirectory', 'infodirectory', 'sessionStorePath'];
for (const k of pathKeys) {
if (typeof cfg[k] === 'string' && cfg[k].trim()) {
try { if (!fs.existsSync(cfg[k])) errs.push(`config.js: chemin inexistant pour '${k}': ${cfg[k]}`); }
catch {}
} else if (k !== 'sessionStorePath') {
errs.push(`config.js: '${k}' non renseigné`);
}
}
// règles conditionnelles dbtype
if (!cfg.dbtype || !['sqlite','mysql'].includes(cfg.dbtype)) {
errs.push("config.js: 'dbtype' doit être 'sqlite' ou 'mysql'");
} else if (cfg.dbtype === 'sqlite') {
if (isEmptyOrPlaceholder(cfg.dbFile)) {
errs.push("config.js: 'dbFile' requis en mode sqlite");
} else {
const dir = require('path').dirname(cfg.dbFile);
if (!fs.existsSync(dir)) errs.push(`config.js: dossier de 'dbFile' inexistant: ${dir}`);
}
// champs MySQL facultatifs → ne rien exiger
} else if (cfg.dbtype === 'mysql') {
// dbFile facultatif → ne rien exiger
if (isEmptyOrPlaceholder(cfg.DB_HOST)) errs.push("config.js: 'DB_HOST' requis en mode mysql");
if (!Number.isInteger(cfg.DB_PORT)) errs.push("config.js: 'DB_PORT' entier requis en mode mysql");
if (isEmptyOrPlaceholder(cfg.DB_USER)) errs.push("config.js: 'DB_USER' requis en mode mysql");
if (isEmptyOrPlaceholder(cfg.DB_PASSWORD)) errs.push("config.js: 'DB_PASSWORD' requis en mode mysql");
if (isEmptyOrPlaceholder(cfg.DB_DATABASE)) errs.push("config.js: 'DB_DATABASE' requis en mode mysql");
}
if (errs.length) { console.error(errs.join('\n')); process.exit(2); }
} catch (e) {
console.error(`Impossible de charger ${path}: ${e.message}`);
process.exit(2);
}
JS
if ! node "$CHECK" "$CFG_JS"; then
errors=$((errors+1))
fi
}
validate_config_js
# ────────── Résumé & exit codes ──────────
if [ "$updated" -eq 1 ]; then
warn "Mises à jour appliquées — relance: 'postauto restart'"
fi
if [ "$errors" -gt 0 ]; then
err "Des problèmes de configuration ont été détectés (${errors}). Corrige-les puis relance lupdate."
exit 2
else
ok "Configuration OK."
fi
# (optionnel) auto-suppression
rm -- "$0" 2>/dev/null || true