Created
November 15, 2025 17:15
-
-
Save brahimmachkouri/761867dd75bbf52b45e8871efad4fa4f to your computer and use it in GitHub Desktop.
Crée une archive tar.xz compressée de manière sécurisée en utilisant la compression parallèle XZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # cpxs.sh — tar -> xz parallèle, safe by default | |
| # Usage: ./cxps.sh -s /chemin/du/repertoire [-o sortie.tar.xz] [-t threads] [-l level] [-e exclude] [-p] [-v] [-F] [-h] | |
| # Voir exemples en bas. | |
| set -euo pipefail | |
| IFS=$'\n\t' | |
| prog=$(basename "$0") | |
| usage() { | |
| cat <<EOF | |
| $prog — Compression tar.xz parallèle (sécurisée) | |
| Options : | |
| -s DIR Répertoire source à archiver (obligatoire) | |
| -o FILE Archive de sortie (défaut: <basename>_YYYYMMDD.tar.xz) | |
| -t THREADS Threads pour xz. 0 = auto (défaut: 0) | |
| -l LEVEL Niveau xz (1..9 ou e). Défaut: 6 | |
| -e PATTERN Exclure motif (peut être répété) | |
| -p Afficher progression via pv (pv requis) | |
| -v Vérifier l'archive après création | |
| -F Force : autorise l'écrasement de la sortie existante | |
| -h Aide | |
| Exemples : | |
| $prog -s /data/projet -o projet.tar.xz -t 0 -l 7 -p -v | |
| $prog -s . -e '.git' -e 'node_modules' -o backup_$(date +%F).tar.xz | |
| Note : le script NE SUPPRIME PAS le répertoire source. | |
| EOF | |
| exit 1 | |
| } | |
| # Defaults | |
| SRC="" | |
| OUT="" | |
| THREADS=0 | |
| LEVEL=6 | |
| EXCLUDES=() | |
| SHOW_PV=0 | |
| VERIFY=0 | |
| FORCE=0 | |
| while getopts ":s:o:t:l:e:p vFh" opt; do | |
| case "$opt" in | |
| s) SRC=$OPTARG ;; | |
| o) OUT=$OPTARG ;; | |
| t) THREADS=$OPTARG ;; | |
| l) LEVEL=$OPTARG ;; | |
| e) EXCLUDES+=("$OPTARG") ;; | |
| p) SHOW_PV=1 ;; | |
| v) VERIFY=1 ;; | |
| F) FORCE=1 ;; | |
| h) usage ;; | |
| \?) echo "Option invalide: -$OPTARG" >&2; usage ;; | |
| :) echo "Option -$OPTARG requiert un argument." >&2; usage ;; | |
| esac | |
| done | |
| if [ -z "$SRC" ]; then | |
| echo "Erreur: répertoire source requis (-s)." >&2 | |
| usage | |
| fi | |
| if [ ! -e "$SRC" ]; then | |
| echo "Erreur: source introuvable: $SRC" >&2 | |
| exit 2 | |
| fi | |
| # Default output name | |
| if [ -z "$OUT" ]; then | |
| base=$(basename "$SRC") | |
| OUT="${base}_$(date +%Y%m%d).tar.xz" | |
| fi | |
| # Resolve absolute paths (realpath/readlink -f fallback) | |
| realpath_cmd() { | |
| if command -v realpath >/dev/null 2>&1; then | |
| realpath "$1" | |
| else | |
| readlink -f "$1" | |
| fi | |
| } | |
| SRC_ABS=$(realpath_cmd "$SRC") | |
| OUT_DIR=$(realpath_cmd "$(dirname "$OUT")") | |
| OUT_ABS="$OUT_DIR/$(basename "$OUT")" | |
| # Prevent output inside source | |
| case "$OUT_DIR/" in | |
| "$SRC_ABS/"* ) | |
| echo "Erreur : le fichier de sortie se trouve dans le répertoire source. Choisis un emplacement externe." >&2 | |
| exit 3 | |
| ;; | |
| esac | |
| # Prevent overwrite unless forced | |
| if [ -e "$OUT_ABS" ] && [ "$FORCE" -ne 1 ]; then | |
| echo "Erreur : '$OUT_ABS' existe déjà. Supprime-le ou passe -F pour forcer l'écrasement." >&2 | |
| exit 4 | |
| fi | |
| # Tools check | |
| command -v tar >/dev/null 2>&1 || { echo "tar manquant"; exit 5; } | |
| command -v xz >/dev/null 2>&1 || { echo "xz manquant"; exit 6; } | |
| # pv optional only if requested | |
| if [ "$SHOW_PV" -eq 1 ]; then | |
| command -v pv >/dev/null 2>&1 || { echo "pv requis pour -p : installe 'pv' ou relance sans -p." >&2; exit 7; } | |
| fi | |
| XZ_BIN=$(command -v xz) | |
| PXZ_BIN=$(command -v pxz || true) | |
| # detect xz --threads support | |
| XZ_SUPPORTS_THREADS=0 | |
| if "$XZ_BIN" --help 2>&1 | grep -Eq -- '--threads| -T'; then | |
| XZ_SUPPORTS_THREADS=1 | |
| fi | |
| # Estimate size (for pv and disk check) | |
| EST_SIZE="" | |
| if du -sb "$SRC" >/dev/null 2>&1; then | |
| EST_SIZE=$(du -sb "$SRC" | awk '{print $1}') | |
| fi | |
| # Disk space check on output directory: need at least EST_SIZE (or 10% margin) | |
| if [ -n "$EST_SIZE" ]; then | |
| avail_bytes=$(df --output=avail -B1 "$OUT_DIR" | tail -n1) | |
| # conservative margin: need EST_SIZE (uncompressed) + 5% | |
| need=$(( EST_SIZE + EST_SIZE/20 )) | |
| if [ "$avail_bytes" -lt "$need" ]; then | |
| echo "Erreur: espace insuffisant dans $OUT_DIR (disponible: $avail_bytes, requis estimé: $need)." >&2 | |
| exit 8 | |
| fi | |
| fi | |
| # Build tar exclude args | |
| TAR_EXCLUDE_ARGS=() | |
| for p in "${EXCLUDES[@]}"; do | |
| TAR_EXCLUDE_ARGS+=(--exclude="$p") | |
| done | |
| # Choose compressor array (as array to avoid eval) | |
| COMPRESS_CMD=() | |
| if [ "$XZ_SUPPORTS_THREADS" -eq 1 ]; then | |
| if [ "$THREADS" -eq 0 ]; then | |
| THREADS_ARG="--threads=0" | |
| else | |
| THREADS_ARG="--threads=$THREADS" | |
| fi | |
| COMPRESS_CMD=( "$XZ_BIN" "$THREADS_ARG" -"$LEVEL" -c ) | |
| else | |
| if [ -n "$PXZ_BIN" ]; then | |
| COMPRESS_CMD=( "$PXZ_BIN" -"$LEVEL" -c ) | |
| else | |
| COMPRESS_CMD=( "$XZ_BIN" -"${LEVEL}" -c ) | |
| fi | |
| fi | |
| echo "Source : $SRC_ABS" | |
| echo "Sortie : $OUT_ABS" | |
| echo "Threads : $THREADS (0=auto)" | |
| echo "Level : $LEVEL" | |
| [ "${#EXCLUDES[@]}" -gt 0 ] && echo "Exclu : ${EXCLUDES[*]}" | |
| [ "$SHOW_PV" -eq 1 ] && echo "Progression: activée (pv)" | |
| [ "$VERIFY" -eq 1 ] && echo "Vérification: activée" | |
| [ "$FORCE" -eq 1 ] && echo "Force overwrite : activée" | |
| # Prepare tmpfile in same dir for atomic move | |
| tmpdir="$OUT_DIR" | |
| tmpfile="$(mktemp "${tmpdir}/$(basename "$OUT").tmp.XXXXXX")" | |
| trap 'rc=$?; [ -f "$tmpfile" ] && rm -f "$tmpfile"; exit $rc' INT TERM EXIT | |
| # Construct tar command (use -C dirname basename to avoid full paths) | |
| TAR_CMD=(tar -cf - "${TAR_EXCLUDE_ARGS[@]}" -C "$(dirname "$SRC")" "$(basename "$SRC")") | |
| # Run pipeline | |
| if [ "$SHOW_PV" -eq 1 ] && [ -n "$EST_SIZE" ]; then | |
| echo "Lancement : tar -> pv -> xz -> tmpfile" | |
| "${TAR_CMD[@]}" | pv -s "$EST_SIZE" | "${COMPRESS_CMD[@]}" > "$tmpfile" | |
| elif [ "$SHOW_PV" -eq 1 ]; then | |
| echo "Lancement : tar -> pv -> xz -> tmpfile (taille inconnue)" | |
| "${TAR_CMD[@]}" | pv | "${COMPRESS_CMD[@]}" > "$tmpfile" | |
| else | |
| echo "Lancement : tar -> xz -> tmpfile" | |
| "${TAR_CMD[@]}" | "${COMPRESS_CMD[@]}" > "$tmpfile" | |
| fi | |
| # Move atomically to final destination (overwrite if forced) | |
| mv -f "$tmpfile" "$OUT_ABS" | |
| # remove trap cleanup (success) | |
| trap - INT TERM EXIT | |
| echo "Compression terminée : $OUT_ABS" | |
| # SHA256 | |
| if command -v sha256sum >/dev/null 2>&1; then | |
| sha256sum "$OUT_ABS" > "$OUT_ABS.sha256" | |
| elif command -v shasum >/dev/null 2>&1; then | |
| shasum -a 256 "$OUT_ABS" > "$OUT_ABS.sha256" | |
| fi | |
| echo "SHA256 enregistré : $OUT_ABS.sha256" | |
| # Vérification optionnelle | |
| if [ "$VERIFY" -eq 1 ]; then | |
| echo "Test xz..." | |
| if ! "$XZ_BIN" -t "$OUT_ABS"; then | |
| echo "Erreur : test xz a échoué." >&2 | |
| exit 9 | |
| fi | |
| echo "Lecture test tar (vérification des headers)..." | |
| if ! "$XZ_BIN" -dc "$OUT_ABS" | tar -tf - >/dev/null; then | |
| echo "Erreur : lecture tar après décompression a échoué." >&2 | |
| exit 10 | |
| fi | |
| echo "Vérification OK." | |
| fi | |
| echo "Terminé." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment