alpinux.presentations/slides/sauvegardes/script/sauvegarde-cle-usb
Cédrix a04a72054a
Ajouter la sauvegarde automatique sur clé USB
Dès qu'on branche la clé déclarée (reconnue par son UUID), udev demande
à systemd de lancer une sauvegarde rsync des dossiers choisis. Ce qui a
disparu de l'ordinateur est retiré de la clé, mais conservé 30 jours dans
une corbeille datée sur la clé. Notifications de bureau, journal systemd,
garde-fous contre une destination erronée. Installateur, tests sans sudo
et mode d'emploi fournis.

Refs #1
2026-11-05 17:44:54 +01:00

475 lines
20 KiB
Bash
Executable file

#!/usr/bin/env bash
# =============================================================================
# sauvegarde-cle-usb — sauvegarde automatique vers UNE clé USB précise
# -----------------------------------------------------------------------------
# Lancé par systemd (sauvegarde-cle-usb.service) quand udev voit apparaître
# la clé déclarée dans /etc/sauvegarde-cle-usb.conf. Peut aussi être lancé
# à la main pour tester :
#
# sudo systemctl start sauvegarde-cle-usb (recommandé : journal systemd)
# sudo sauvegarde-cle-usb --simulation (montre ce qui serait fait)
# sudo sauvegarde-cle-usb (sauvegarde réelle)
#
# Rappel : une sauvegarde « miroir » recopie aussi les suppressions.
# C'est pourquoi, par défaut, tout ce qui disparaît ou change sur la clé
# part d'abord dans <clé>/.corbeille/AAAA-MM-JJ_HHMMSS (gardée N jours).
#
# Mode test (sans sudo, sans clé, sans udev) :
# SAUVEGARDE_TEST_DESTINATION=/tmp/fausse-cle \
# SAUVEGARDE_CONF=./ma-conf-de-test.conf ./sauvegarde-cle-usb
# Le dossier indiqué joue le rôle de la racine de la clé : pas de montage,
# pas de vérification d'UUID, notifications affichées dans le terminal.
# SAUVEGARDE_TEST_FSTYPE=vfat permet de simuler une clé FAT.
# =============================================================================
set -euo pipefail
export LC_ALL=C.UTF-8 # messages et formats stables (dates, df)
umask 022
NOM="sauvegarde-cle-usb"
CONF="${SAUVEGARDE_CONF:-/etc/sauvegarde-cle-usb.conf}"
POINT_MONTAGE_DEDIE="/run/sauvegarde-cle-usb"
TEST_DESTINATION="${SAUVEGARDE_TEST_DESTINATION:-}"
SIMULATION=0
# Variables d'état (utilisées par le nettoyage final)
MONTE_PAR_NOUS=0
RACINE_CLE=""
FICHIER_SORTIE=""
RESULTAT_ECRIT=0
CLE_VERIFIEE=0 # passe à 1 quand on est sûr d'être sur la bonne clé
UTILISATEUR_UID=""
# -----------------------------------------------------------------------------
# Petites fonctions utilitaires
# -----------------------------------------------------------------------------
journal() { printf '%s\n' "$*"; } # stdout -> journal systemd
erreur() { printf 'ERREUR : %s\n' "$*" >&2; }
aide() {
cat <<'FIN'
Utilisation : sauvegarde-cle-usb [--simulation] [--aide]
--simulation montre ce qui serait copié/supprimé, sans rien modifier
--aide affiche ce message
Configuration : /etc/sauvegarde-cle-usb.conf
Journal : journalctl -u sauvegarde-cle-usb
FIN
}
# Notification de bureau dans la session graphique de l'utilisateur.
# Ne fait jamais échouer le script (une notification ratée n'est pas grave).
notifier() {
local urgence="$1" titre="$2" message="$3"
journal "[notification] $titre — $message"
if [[ -n "$TEST_DESTINATION" ]]; then
return 0 # mode test : terminal seulement
fi
[[ -n "$UTILISATEUR_UID" ]] || return 0
local bus="/run/user/$UTILISATEUR_UID/bus"
[[ -S "$bus" ]] || return 0 # personne n'est connecté
command -v notify-send >/dev/null 2>&1 || return 0
timeout 10 runuser -u "$UTILISATEUR" -- env \
DBUS_SESSION_BUS_ADDRESS="unix:path=$bus" \
notify-send --app-name="Sauvegarde" --icon=drive-removable-media \
--urgency="$urgence" "$titre" "$message" >/dev/null 2>&1 || true
}
# Écrit le petit compte rendu lisible sur la clé.
ecrire_resultat() {
local resultat="$1"
[[ "$CLE_VERIFIEE" -eq 1 && -d "$RACINE_CLE" ]] || return 0
[[ "$SIMULATION" -eq 0 ]] || return 0
local f="$RACINE_CLE/derniere-sauvegarde.txt"
{
echo "Dernière sauvegarde : $(date '+%d/%m/%Y à %H:%M:%S')"
echo "Ordinateur : $(hostname)"
echo "Résultat : $resultat"
echo "Dossiers sauvegardés :"
local s
for s in "${SOURCES[@]}"; do echo " - $s"; done
echo "Copie sur la clé : $DOSSIER_DESTINATION/"
if [[ "${CORBEILLE_JOURS:-0}" -gt 0 ]]; then
echo "Fichiers supprimés ou remplacés : .corbeille/ (gardés $CORBEILLE_JOURS jours)"
fi
if [[ -n "$FICHIER_SORTIE" && -s "$FICHIER_SORTIE" ]]; then
echo
echo "--- Résumé rsync ---"
grep -E '^(Number of|Total file size|Total transferred|Literal data|sent |total size)' \
"$FICHIER_SORTIE" || true
fi
} > "$f" 2>/dev/null || true
proprietaire_utilisateur "$f"
RESULTAT_ECRIT=1
}
# Sur une clé ext4, rend à l'utilisateur ce que root a créé (il pourra le
# gérer depuis Nemo). Sur FAT/exFAT/NTFS, c'est l'option de montage uid= qui
# s'en charge.
proprietaire_utilisateur() {
[[ -z "$TEST_DESTINATION" ]] || return 0
[[ "${TYPE_FS:-}" == "ext"* ]] || return 0
[[ "$(id -u)" -eq 0 ]] || return 0
chown "$UTILISATEUR:" "$@" 2>/dev/null || true
}
nettoyage() {
local code=$?
set +e
if [[ "$code" -ne 0 && "$RESULTAT_ECRIT" -eq 0 ]]; then
ecrire_resultat "ÉCHEC (code $code) — voir : journalctl -u $NOM"
fi
[[ -n "$FICHIER_SORTIE" ]] && rm -f "$FICHIER_SORTIE"
if [[ -n "$RACINE_CLE" ]]; then
sync -f "$RACINE_CLE/." 2>/dev/null || sync
fi
if [[ "$MONTE_PAR_NOUS" -eq 1 ]]; then
if umount "$POINT_MONTAGE_DEDIE" 2>/dev/null; then
journal "Clé démontée de $POINT_MONTAGE_DEDIE."
else
# Clé arrachée ou encore occupée : démontage « paresseux ».
umount -l "$POINT_MONTAGE_DEDIE" 2>/dev/null
journal "Démontage différé de $POINT_MONTAGE_DEDIE."
fi
rmdir "$POINT_MONTAGE_DEDIE" 2>/dev/null
fi
exit "$code"
}
# Arrête tout, avec notification et message clair.
abandon() {
erreur "$1"
notifier critical "Sauvegarde impossible" "$1"
exit "${2:-1}"
}
# -----------------------------------------------------------------------------
# Lecture des options et de la configuration
# -----------------------------------------------------------------------------
for arg in "$@"; do
case "$arg" in
--simulation|-n) SIMULATION=1 ;;
--aide|--help|-h) aide; exit 0 ;;
*) erreur "option inconnue : $arg"; aide >&2; exit 2 ;;
esac
done
trap nettoyage EXIT
if [[ -z "$TEST_DESTINATION" && "$(id -u)" -ne 0 ]]; then
erreur "ce script doit être lancé par root (sudo $NOM, ou sudo systemctl start $NOM)."
exit 1
fi
[[ -r "$CONF" ]] || { erreur "configuration introuvable : $CONF (lancez installer.sh)"; exit 1; }
# La configuration est exécutée par root : elle doit appartenir à root et ne
# pas être modifiable par d'autres (sinon n'importe qui pourrait y glisser
# une commande).
if [[ -z "$TEST_DESTINATION" ]]; then
if [[ "$(stat -c '%u' "$CONF")" != 0 ]] || [[ -n "$(find "$CONF" -perm /022)" ]]; then
erreur "$CONF doit appartenir à root et n'être modifiable que par lui (chmod 644)."
exit 1
fi
fi
# Valeurs par défaut, puis lecture du fichier
UUID=""
UTILISATEUR=""
SOURCES=()
DOSSIER_DESTINATION="Sauvegarde"
CORBEILLE_JOURS=30
EXCLUSIONS=()
VERIFIER_ESPACE="oui"
# shellcheck source=sauvegarde-cle-usb.conf
. "$CONF"
# --- Contrôles de la configuration -------------------------------------------
[[ -n "$UTILISATEUR" ]] || { erreur "UTILISATEUR est vide dans $CONF"; exit 1; }
id -u "$UTILISATEUR" >/dev/null 2>&1 || { erreur "utilisateur inconnu : $UTILISATEUR"; exit 1; }
UTILISATEUR_UID="$(id -u "$UTILISATEUR")"
UTILISATEUR_GID="$(id -g "$UTILISATEUR")"
[[ "$CORBEILLE_JOURS" =~ ^[0-9]+$ ]] || abandon "CORBEILLE_JOURS doit être un nombre (0 = pas de corbeille)."
if [[ "${#SOURCES[@]}" -eq 0 ]]; then
abandon "aucun dossier à sauvegarder (SOURCES est vide dans $CONF)."
fi
# Chaque source doit exister, être un dossier, et ne pas être vide : un
# dossier personnel vide (disque non monté, dossier chiffré verrouillé...)
# combiné à --delete viderait la clé !
declare -A NOMS_VUS=()
SOURCES_PROPRES=()
for s in "${SOURCES[@]}"; do
[[ -n "$s" ]] || abandon "une entrée de SOURCES est vide."
[[ -d "$s" ]] || abandon "le dossier à sauvegarder n'existe pas : $s"
s="$(realpath -e -- "$s")"
[[ "$s" != "/" ]] || abandon "sauvegarder / entier n'est pas prévu par cet outil."
if [[ -z "$(find "$s" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]]; then
abandon "le dossier $s est vide : sauvegarde annulée par prudence."
fi
base="$(basename -- "$s")"
if [[ -n "${NOMS_VUS[$base]:-}" ]]; then
abandon "deux dossiers de SOURCES portent le même nom « $base » : ils s'écraseraient sur la clé."
fi
NOMS_VUS[$base]=1
SOURCES_PROPRES+=("$s")
done
SOURCES=("${SOURCES_PROPRES[@]}")
# DOSSIER_DESTINATION : un nom de dossier simple, relatif à la racine de la clé.
case "$DOSSIER_DESTINATION" in
""|"/"|"."|".."|/*|*/../*|../*|*/..|.corbeille|.corbeille/*)
abandon "DOSSIER_DESTINATION invalide : « $DOSSIER_DESTINATION » (exemple : Sauvegarde)." ;;
esac
# -----------------------------------------------------------------------------
# Verrou : jamais deux sauvegardes en même temps
# -----------------------------------------------------------------------------
if [[ -n "$TEST_DESTINATION" ]]; then
VERROU="${TMPDIR:-/tmp}/$NOM-test.lock"
else
VERROU="/run/lock/$NOM.lock"
fi
exec 9>"$VERROU"
if ! flock -n 9; then
erreur "une sauvegarde est déjà en cours."
exit 0
fi
# -----------------------------------------------------------------------------
# Trouver (ou monter) la clé
# -----------------------------------------------------------------------------
if [[ -n "$TEST_DESTINATION" ]]; then
journal "*** MODE TEST : la clé est simulée par $TEST_DESTINATION ***"
[[ -d "$TEST_DESTINATION" ]] || abandon "destination de test introuvable : $TEST_DESTINATION"
RACINE_CLE="$(realpath -e -- "$TEST_DESTINATION")"
[[ "$RACINE_CLE" != "/" ]] || abandon "destination de test « / » refusée."
TYPE_FS="${SAUVEGARDE_TEST_FSTYPE:-$(findmnt -n -o FSTYPE --target "$RACINE_CLE" | head -n1)}"
else
[[ "$UUID" =~ ^[A-Za-z0-9-]+$ ]] || abandon "UUID absent ou invalide dans $CONF."
PERIPH_LIEN="/dev/disk/by-uuid/$UUID"
# udev peut nous lancer une fraction de seconde avant que le lien existe.
for _ in $(seq 1 20); do
[[ -b "$PERIPH_LIEN" ]] && break
sleep 0.5
done
[[ -b "$PERIPH_LIEN" ]] || abandon "la clé de sauvegarde (UUID $UUID) n'est pas branchée."
PERIPH="$(readlink -f "$PERIPH_LIEN")"
TYPE_FS="$(blkid -o value -s TYPE "$PERIPH" 2>/dev/null || true)"
[[ -n "$TYPE_FS" ]] || abandon "système de fichiers illisible sur $PERIPH."
notifier normal "Sauvegarde en cours…" "Clé détectée. Ne la retirez pas avant le message « Sauvegarde terminée »."
# Choix : on monte la clé NOUS-MÊMES sur un point dédié, même si
# Nemo/udisks l'a déjà montée dans /media/<utilisateur>/. Linux accepte
# plusieurs montages du même système de fichiers ; ainsi le chemin est
# connu, les options maîtrisées, et un « Éjecter » dans Nemo ne coupe
# pas la sauvegarde en plein milieu (udisks voit la clé encore utilisée).
mkdir -p "$POINT_MONTAGE_DEDIE"
chmod 755 "$POINT_MONTAGE_DEDIE"
if mountpoint -q "$POINT_MONTAGE_DEDIE"; then
# Reste d'une exécution interrompue : on vérifie que c'est la bonne clé.
if [[ "$(findmnt -n -o UUID --mountpoint "$POINT_MONTAGE_DEDIE")" != "$UUID" ]]; then
abandon "$POINT_MONTAGE_DEDIE est occupé par un autre disque."
fi
MONTE_PAR_NOUS=1
else
case "$TYPE_FS" in
vfat|exfat|ntfs|ntfs3)
OPTS="rw,nosuid,nodev,noatime,uid=$UTILISATEUR_UID,gid=$UTILISATEUR_GID,umask=022" ;;
*)
OPTS="rw,nosuid,nodev,noatime" ;;
esac
if mount -o "$OPTS" "$PERIPH" "$POINT_MONTAGE_DEDIE"; then
MONTE_PAR_NOUS=1
RACINE_CLE="$POINT_MONTAGE_DEDIE"
else
# Plan B : réutiliser un montage existant (udisks) en écriture.
rmdir "$POINT_MONTAGE_DEDIE" 2>/dev/null || true
EXISTANT="$(findmnt -n -o TARGET,OPTIONS --source "$PERIPH" | awk '$2 ~ /(^|,)rw(,|$)/ {print $1; exit}')"
[[ -n "$EXISTANT" ]] || abandon "impossible de monter la clé ($PERIPH, $TYPE_FS)."
journal "Montage dédié impossible, réutilisation du montage existant : $EXISTANT"
RACINE_CLE="$EXISTANT"
fi
fi
RACINE_CLE="${RACINE_CLE:-$POINT_MONTAGE_DEDIE}"
fi
# -----------------------------------------------------------------------------
# GARDE-FOU : la destination doit être SUR la clé, et nulle part ailleurs
# -----------------------------------------------------------------------------
# Un rsync --delete vers un mauvais dossier effacerait des données : on
# vérifie tout, et on refuse au moindre doute.
DESTINATION="$RACINE_CLE/$DOSSIER_DESTINATION"
if [[ -z "$TEST_DESTINATION" ]]; then
mountpoint -q "$RACINE_CLE" || abandon "la clé n'est pas montée sur $RACINE_CLE."
UUID_MONTE="$(findmnt -n -o UUID --mountpoint "$RACINE_CLE" | head -n1)"
[[ "$UUID_MONTE" == "$UUID" ]] || abandon "$RACINE_CLE n'est pas la clé attendue (UUID $UUID_MONTE au lieu de $UUID)."
fi
if [[ "$SIMULATION" -eq 0 ]]; then
mkdir -p -- "$DESTINATION" || abandon "impossible de créer $DESTINATION (clé protégée en écriture ?)"
proprietaire_utilisateur "$DESTINATION"
fi
if [[ -e "$DESTINATION" ]]; then
DEST_REELLE="$(realpath -e -- "$DESTINATION")"
else
DEST_REELLE="$(realpath -m -- "$DESTINATION")"
fi
case "$DEST_REELLE/" in
"$RACINE_CLE"/?*/) ;; # bien un sous-dossier de la racine de la clé
*) abandon "destination hors de la clé : $DEST_REELLE (refusé)." ;;
esac
if [[ -z "$TEST_DESTINATION" && -e "$DEST_REELLE" ]]; then
# Même un lien symbolique piégé ne doit pas nous faire sortir de la clé.
[[ "$(findmnt -n -o UUID --target "$DEST_REELLE" | head -n1)" == "$UUID" ]] \
|| abandon "la destination $DEST_REELLE n'est pas sur la clé (refusé)."
fi
for s in "${SOURCES[@]}"; do
case "$DEST_REELLE/" in "$s"/*) abandon "la destination est à l'intérieur de $s (refusé)." ;; esac
case "$s/" in "$DEST_REELLE"/*) abandon "$s est à l'intérieur de la destination (refusé)." ;; esac
done
DESTINATION="$DEST_REELLE"
CLE_VERIFIEE=1
journal "Clé : $RACINE_CLE ($TYPE_FS) — copie dans : $DESTINATION"
# -----------------------------------------------------------------------------
# Options rsync
# -----------------------------------------------------------------------------
HORODATAGE="$(date '+%Y-%m-%d_%H%M%S')"
CORBEILLE_RACINE="$RACINE_CLE/.corbeille"
CORBEILLE="$CORBEILLE_RACINE/$HORODATAGE"
OPTS_RSYNC=(--delete --one-file-system --human-readable --stats --info=del1 --info=name0)
case "$TYPE_FS" in
ext2|ext3|ext4|btrfs|xfs|f2fs)
# Système de fichiers Linux : on garde tout (droits, propriétaires,
# dates, liens symboliques) ; -H : liens physiques.
OPTS_RSYNC+=(-a -H) ;;
vfat|exfat|ntfs|ntfs3|fuseblk)
# FAT/exFAT/NTFS : ni droits ni propriétaires ; dates à 2 s près.
# Les liens symboliques sont ignorés (rsync affiche « skipping
# non-regular file ») : ce n'est pas une erreur.
OPTS_RSYNC+=(-rt --modify-window=2 --no-perms --no-owner --no-group)
if [[ "$TYPE_FS" == "vfat" ]]; then
# FAT32 : 4 Go maximum par fichier.
OPTS_RSYNC+=(--max-size=4095M)
fi ;;
*)
journal "Système de fichiers « $TYPE_FS » non prévu : copie prudente (-rt)."
OPTS_RSYNC+=(-rt --modify-window=2) ;;
esac
if [[ "$CORBEILLE_JOURS" -gt 0 ]]; then
# Rien n'est perdu d'un coup : fichiers supprimés ou remplacés -> .corbeille/
OPTS_RSYNC+=(--backup "--backup-dir=$CORBEILLE")
fi
# Exclusions : ce qui ne sert à rien de sauvegarder
for e in "${EXCLUSIONS[@]}"; do
[[ -n "$e" ]] && OPTS_RSYNC+=("--exclude=$e")
done
if [[ "$SIMULATION" -eq 1 ]]; then
OPTS_RSYNC+=(--dry-run --itemize-changes --info=name1)
journal "*** SIMULATION : rien ne sera modifié sur la clé ***"
fi
# -----------------------------------------------------------------------------
# Vérification de l'espace libre (estimation par un passage à blanc)
# -----------------------------------------------------------------------------
# Approximation : avec la corbeille, les fichiers remplacés ne libèrent pas
# de place ; on compare donc la quantité à copier à l'espace libre.
if [[ "$VERIFIER_ESPACE" == "oui" && "$SIMULATION" -eq 0 ]]; then
A_COPIER="$(rsync "${OPTS_RSYNC[@]}" --dry-run --info=del0 --no-human-readable "${SOURCES[@]}" "$DESTINATION/" 2>/dev/null \
| sed -n 's/^Total transferred file size: \([0-9,]*\).*/\1/p' | tr -d ',' || true)"
LIBRE="$(df -B1 --output=avail "$RACINE_CLE" | tail -n1 | tr -d ' ')"
if [[ "$A_COPIER" =~ ^[0-9]+$ && "$LIBRE" =~ ^[0-9]+$ ]]; then
journal "À copier : $(numfmt --to=iec "$A_COPIER") — libre sur la clé : $(numfmt --to=iec "$LIBRE")"
if (( A_COPIER > LIBRE )); then
abandon "clé trop petite ou pleine : il faut $(numfmt --to=iec "$A_COPIER"), il reste $(numfmt --to=iec "$LIBRE")." 3
fi
fi
fi
# Avertissement FAT32 : fichiers de plus de 4 Go non copiés
if [[ "$TYPE_FS" == "vfat" ]]; then
GROS="$(find "${SOURCES[@]}" -xdev -type f -size +4095M 2>/dev/null | head -n 5 || true)"
if [[ -n "$GROS" ]]; then
journal "Fichiers de plus de 4 Go NON copiés (limite FAT32) :"
journal "$GROS"
notifier normal "Sauvegarde : fichiers trop gros" "Certains fichiers dépassent 4 Go et ne tiennent pas sur cette clé (FAT32). Voir le journal."
fi
fi
# -----------------------------------------------------------------------------
# La sauvegarde
# -----------------------------------------------------------------------------
DEBUT="$(date +%s)"
FICHIER_SORTIE="$(mktemp)"
journal "Sauvegarde de : ${SOURCES[*]}"
set +e
rsync "${OPTS_RSYNC[@]}" "${SOURCES[@]}" "$DESTINATION/" 2>&1 | tee "$FICHIER_SORTIE"
CODE_RSYNC="${PIPESTATUS[0]}"
set -e
DUREE=$(( $(date +%s) - DEBUT ))
if [[ "$SIMULATION" -eq 1 ]]; then
journal "Simulation terminée (code rsync $CODE_RSYNC). Rien n'a été modifié."
exit 0
fi
# Rend à l'utilisateur la corbeille créée par root (clé ext4).
if [[ -d "$CORBEILLE" ]]; then
proprietaire_utilisateur "$CORBEILLE_RACINE" "$CORBEILLE"
fi
# -----------------------------------------------------------------------------
# Purge des vieilles corbeilles
# -----------------------------------------------------------------------------
if [[ "$CORBEILLE_JOURS" -gt 0 && -d "$CORBEILLE_RACINE" ]]; then
# Uniquement les dossiers au format de date attendu, directement dans .corbeille
while IFS= read -r -d '' vieux; do
journal "Purge de l'ancienne corbeille : $(basename "$vieux")"
rm -rf -- "$vieux"
done < <(find "$CORBEILLE_RACINE" -mindepth 1 -maxdepth 1 -type d \
-name '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_[0-9][0-9][0-9][0-9][0-9][0-9]' \
-mtime +"$CORBEILLE_JOURS" -print0)
fi
sync -f "$DESTINATION/." 2>/dev/null || sync
# -----------------------------------------------------------------------------
# Bilan
# -----------------------------------------------------------------------------
case "$CODE_RSYNC" in
0|24)
# 24 = fichiers disparus pendant la copie (normal si on travaille)
ecrire_resultat "OK ($DUREE s)"
journal "Sauvegarde terminée en $DUREE s."
notifier normal "Sauvegarde terminée" "Vous pouvez retirer la clé."
;;
23)
# Copie partielle : souvent des noms interdits sur FAT (: ? * " < > | \)
# ou des fichiers illisibles. Les suppressions n'ont PAS été faites.
ecrire_resultat "PARTIELLE ($DUREE s) — certains fichiers n'ont pas pu être copiés"
journal "Sauvegarde PARTIELLE : certains fichiers n'ont pas été copiés (voir ci-dessus)."
notifier normal "Sauvegarde terminée avec des avertissements" \
"Quelques fichiers n'ont pas pu être copiés (noms interdits sur la clé ?). Détails : journalctl -u $NOM. Vous pouvez retirer la clé."
exit 23
;;
*)
if grep -q 'No space left on device' "$FICHIER_SORTIE"; then
ecrire_resultat "ÉCHEC : clé pleine"
abandon "la clé est pleine. Libérez de la place (ou videz .corbeille) puis rebranchez-la." 3
fi
ecrire_resultat "ÉCHEC (code rsync $CODE_RSYNC)"
abandon "la sauvegarde a échoué (code rsync $CODE_RSYNC). Détails : journalctl -u $NOM" "$CODE_RSYNC"
;;
esac