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
475 lines
20 KiB
Bash
Executable file
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
|