Copier un dossier ailleurs avec cp -r, ce n’est pas une sauvegarde. C’est une copie, écrasée à chaque fois, qui reproduit aussi bien tes données que tes erreurs : si tu supprimes un fichier par accident, la copie suivante le supprime aussi.
Ce qu’on veut, c’est une sauvegarde datée : un dossier par jour, chacun montrant l’état des fichiers à ce moment-là. Et sans que chaque jour occupe la taille totale des données. C’est exactement ce que permet rsync avec l’option --link-dest.
Le principe : des liens physiques
Sous Linux, un fichier peut avoir plusieurs « noms » qui pointent vers les mêmes données sur le disque : ce sont les liens physiques (hard links). Le contenu n’est stocké qu’une fois.
Avec --link-dest, rsync compare la source à la sauvegarde précédente :
- un fichier inchangé n’est pas recopié : il est simplement lié à celui de la veille (pas d’espace supplémentaire) ;
- un fichier modifié ou nouveau est copié.
Chaque dossier daté ressemble donc à une copie complète, que tu peux parcourir normalement, mais il ne pèse que ce qui a changé.
/mnt/sauvegardes/data/
├── 2026-10-01_0330/ ← copie complète (la première fois)
├── 2026-10-02_0330/ ← n'a pris de place que pour les changements
├── 2026-10-03_0330/
└── latest -> 2026-10-03_0330
Le script
#!/usr/bin/env bash
set -euo pipefail
SOURCE="/srv/data/"
DEST="/mnt/sauvegardes/data"
GARDER=14
NTFY_URL="${NTFY_URL:-}"
HORODATAGE="$(date +%F_%H%M)"
CIBLE="$DEST/$HORODATAGE"
LATEST="$DEST/latest"
notifier() { # $1 = titre, $2 = message, $3 = priorité (facultatif)
[[ -n "$NTFY_URL" ]] || return 0
curl -fsS -H "Title: $1" -H "Priority: ${3:-default}" -d "$2" "$NTFY_URL" >/dev/null || true
}
echec() {
echo "ERREUR : $1" >&2
notifier "Sauvegarde ECHOUEE" "$(hostname) : $1" high
exit 1
}
trap 'echec "erreur à la ligne $LINENO"' ERR
[[ -d "$SOURCE" ]] || echec "source introuvable : $SOURCE"
mkdir -p "$DEST"
# 1. Copie vers un dossier « partiel », puis renommage : jamais de sauvegarde à moitié finie
rsync -aH --delete --link-dest="$LATEST" "$SOURCE" "$CIBLE.partiel/"
mv "$CIBLE.partiel" "$CIBLE"
ln -sfn "$CIBLE" "$LATEST"
# 2. Rotation : on garde les $GARDER sauvegardes les plus récentes
find "$DEST" -mindepth 1 -maxdepth 1 -type d -name '20*' ! -name '*.partiel'
| sort -r | tail -n +"$((GARDER + 1))"
| while IFS= read -r ancienne; do
rm -rf -- "$ancienne"
done
notifier "Sauvegarde OK" "$(hostname) : $HORODATAGE"
echo "Sauvegarde terminée : $CIBLE"
Ce que fait chaque partie
rsync -aH --delete --link-dest=…
-a(archive) : conserve les permissions, les dates, les propriétaires, les liens symboliques. Presque toujours ce qu’on veut.-H: conserve les liens physiques qui existent déjà dans la source.--delete: supprime dans la destination ce qui n’existe plus dans la source, pour que le snapshot reflète fidèlement l’état du jour.--link-dest="$LATEST": le dossier de référence pour les fichiers inchangés.
À la toute première exécution, latest n’existe pas : rsync affiche un avertissement sur ce link_dest, puis fait une copie complète. C’est normal.
Le slash final de SOURCE="/srv/data/" compte : avec lui, rsync copie le contenu du dossier ; sans lui, il copierait le dossier data lui-même dans la destination.
Le dossier .partiel et le renommage
On copie dans 2026-10-03_0330.partiel, puis on renomme en 2026-10-03_0330 seulement quand tout s’est bien passé. Si la machine s’éteint au milieu, il reste un dossier .partiel (reconnaissable, et ignoré par la rotation grâce au ! -name '*.partiel'), jamais une fausse sauvegarde qui a l’air complète.
trap … ERR
Grâce à set -e, toute commande qui échoue déclenche le piège ERR, qui appelle la fonction echec : un message d’erreur, une notification urgente, et l’arrêt. $LINENO donne la ligne en cause, pratique pour le dépannage.
La rotation
find … | sort -r liste les dossiers datés du plus récent au plus ancien, et tail -n +15 ignore les 14 premières lignes pour ne garder que tous les suivants : ce sont ceux qu’on supprime. La date au format AAAA-MM-JJ_HHMM est triable alphabétiquement, ce qui rend le tri trivial. Le ! -name '*.partiel' exclut les copies inachevées du décompte, et cette formulation fonctionne aussi bien sous Debian que sous macOS.
Supprimer un ancien snapshot ne casse pas les suivants : grâce aux liens physiques, les données restent tant qu’au moins un nom pointe dessus.
La notification
Un message sur le téléphone à la fin, de succès comme d’échec. J’ai détaillé la mise en place de ntfy dans Être alerté sur son téléphone quand ça plante. Le script ne l’impose pas : si NTFY_URL est vide, notifier ne fait rien.
Tester sans risque
Avant la première vraie exécution :
# 1. Simulation : rsync affiche ce qu'il ferait, sans rien copier
rsync -aH --delete --dry-run --itemize-changes /srv/data/ /tmp/test-sauvegarde/
# 2. Syntaxe et pièges classiques
shellcheck sauvegarde.sh
Puis une exécution réelle sur un petit dossier de test, en modifiant SOURCE et DEST. Regarde ensuite l’occupation avec du -sh /tmp/test/* : le deuxième snapshot doit être minuscule.
Sauvegarder vers une autre machine
La destination peut être distante, via SSH :
rsync -aH --delete -e ssh --link-dest="../latest" /srv/data/ utilisateur@nas:/volume1/sauvegardes/data/2026-10-03/
Avec une destination distante, --link-dest désigne un chemin sur la machine de destination, et un chemin relatif est interprété à partir du dossier cible. Pour une configuration sans mot de passe, utilise une clé SSH dédiée, limitée à cette tâche.
La seule vérification qui compte
Une sauvegarde qui n’a jamais été restaurée est une hypothèse. Une fois par mois, restaure un dossier de test :
rsync -a /mnt/sauvegardes/data/latest/documents/ /tmp/restauration-test/
diff -r /srv/data/documents /tmp/restauration-test && echo "identique"
À retenir
--link-destdonne des snapshots complets pour le prix des seuls changements.- Copier dans un dossier
.partielpuis renommer évite les fausses sauvegardes. - Une rotation simple (garder les N dernières) suffit pour un homelab.
- Un message de succès et d’échec : une sauvegarde silencieuse est une sauvegarde dont on ne sait pas si elle marche.
- Teste la restauration, pas seulement la copie.
Il reste à lancer ce script chaque nuit, sans qu’il s’exécute en double : c’est le sujet de l’article suivant.