Un set -e combiné à rsync exit 23 a empêché la rétention de mon backup de tourner pendant des semaines : le 1er juillet 2026, je trouve 37 snapshots du dossier de données Nextcloud sur le NAS, pour une rétention configurée à 3 jours. La cause : rsync sortait régulièrement avec le code 23 parce que des fichiers Nextcloud changeaient pendant la copie, et set -euo pipefail tuait le script avant la purge. Le backup « marchait », la purge jamais.
Le montage : un pull nocturne avec snapshots --link-dest
Le NAS récupère chaque nuit les sauvegardes du serveur srv-a en mode pull, avec une clé SSH restreinte comme décrit dans l’article sur le backup pull et la clé SSH from= + no-pty. Pour les données Nextcloud (environ 100 Go), le script pull-from-srv-a.sh créait un snapshot daté par jour, dédupliqué par --link-dest : --link-dest est l’option de rsync qui crée un hardlink vers le snapshot précédent pour chaque fichier inchangé, au lieu de le recopier.
Version simplifiée de l’ancien script :
set -euo pipefail
RETENTION_DAYS_CLOUD="${RETENTION_DAYS_CLOUD:-3}"
log "[2/2] Rsync cloud-data -> $SNAP_DIR (link-dest=current)"
rsync -az --delete -e "$SSH_CMD" \
--exclude='data/updater-*/' \
"${LINK_DEST_OPT[@]}" \
"$SRC:/srv/nextcloud-data/" "$SNAP_DIR/"
# Mise a jour symlink current
ln -snf "$DATE" "$CURRENT_LINK"
# ---------- Retention ----------
CUTOFF_CLOUD=$(date -d "$RETENTION_DAYS_CLOUD days ago" +%F)
for snap in "$DEST/nextcloud/data"/20[0-9][0-9]-[01][0-9]-[0-3][0-9]; do
[ -d "$snap" ] || continue
name=$(basename "$snap")
if [[ "$name" < "$CUTOFF_CLOUD" ]]; then
log " rm $name"
rm -rf "$snap"
fi
done
Tout est séquentiel : copie, lien current, purge. Si la copie « échoue », le reste n’existe pas.
Pourquoi rsync sort en exit 23 sur un dossier Nextcloud ?
Le code 23 de rsync signifie, d’après sa page de manuel, Partial transfer due to error ; le code 24, Partial transfer due to vanished source files. Ce sont des transferts partiels : presque tout est copié, quelques fichiers posent problème.
Sur les données d’une instance Nextcloud en service, certains fichiers sont réécrits en permanence. Dans mon cas, ceux qui revenaient étaient les appstore/*.json, nextcloud.log et des CACHEDIR.TAG : des fichiers lus par rsync pendant qu’ils étaient modifiés. Rien de grave pour la sauvegarde, mais rsync le signale avec un code non nul.
Et set -e ne fait pas de nuance : pour bash, un code différent de 0 est un échec, et le script s’arrête sur la ligne de rsync. Le lien current n’est pas mis à jour, la boucle de rétention n’est jamais atteinte.
Avec
set -e, un code de sortie « partiellement réussi » vaut un crash : tout ce qui suit la commande, purge comprise, n’est jamais exécuté.
Le correctif : un wrapper rsync_tol() qui tolère 23 et 24
Je n’ai pas retiré set -e, qui reste un bon garde-fou. J’ai encapsulé rsync dans une fonction qui accepte explicitement 0, 23 et 24, les journalise, et renvoie le vrai code pour tout le reste :
# rsync tolerant aux codes 23/24 (fichiers volatils) ; renvoie le vrai code sinon
rsync_tol() {
local rc=0
rsync "$@" || rc=$?
if [ "$rc" -eq 0 ] || [ "$rc" -eq 23 ] || [ "$rc" -eq 24 ]; then
[ "$rc" -ne 0 ] && log " (rsync rc=$rc tolere : fichiers volatils)"
return 0
fi
log " ERREUR rsync rc=$rc"
return "$rc"
}
Le || rc=$? est le détail qui compte : une commande suivie de || ne déclenche pas set -e, ce qui permet de capturer le code au lieu de mourir dessus. Une connexion SSH coupée ou un disque plein sortent avec un autre code et arrêtent toujours le script.
⚠️ Le code 23 couvre aussi de vraies erreurs, comme un fichier illisible pour cause de permissions. Le tolérer sans le journaliser reviendrait à cacher le problème suivant : d’où la ligne de log à chaque occurrence.
Le même jour, j’ai simplifié le modèle : les données Nextcloud passent d’un snapshot daté par jour à un miroir unique (--delete), et seuls les dumps de bases gardent 7 jours glissants. Moins de snapshots, moins de choses à purger.
Ce que je surveillais, et ce que j’aurais dû surveiller
Le script tournait chaque nuit et les données arrivaient : vu de loin, le backup était sain. Ce qui ne marchait pas, c’était l’effet attendu de la fin du script, la purge. 37 dossiers au lieu de 3, c’est visible en une commande, à condition de la lancer.
Une vérification utile compte le résultat plutôt que le code de retour : le nombre de snapshots présents, la date du plus ancien, l’espace occupé. Si la rétention est de 3 jours et qu’il y a plus de 4 dossiers, quelque chose ne tourne pas.
Lessons learned
- Avec
set -e, rsync exit 23 arrête le script comme une vraie panne. Tout ce qui vient après la copie, rétention comprise, est silencieusement sauté. - Les codes 23 et 24 sont normaux sur un dossier vivant. Des fichiers modifiés ou supprimés pendant la copie produisent un transfert partiel, pas un backup raté.
- Tolérer un code, c’est le nommer, pas tout ignorer. Un wrapper qui accepte 23/24, les logue et renvoie les autres codes garde
set -eutile. commande || rc=$?capture le code sans déclencherset -e. C’est le motif de base pour traiter un code de sortie dans un script strict.- Surveillez l’effet d’un script, pas seulement son exécution. Compter les snapshots aurait révélé la rétention cassée dès le quatrième jour.