diff --git a/NOUVEAUTES.md b/NOUVEAUTES.md index 59ce769..ef7d4df 100644 --- a/NOUVEAUTES.md +++ b/NOUVEAUTES.md @@ -10,6 +10,9 @@ Les évolutions du site des présentations, de la plus récente à la plus ancie et conducteur dans `slides/sauvegardes/` (#1). - « Ranger et sauvegarder » : une diapo propose une arborescence type pour le dossier Documents, et une diapo facultative présente la copie automatique sur clé USB. +- Nouveau script `slides/sauvegardes/script/` : sauvegarde rsync automatique dès + qu'on branche une clé USB déclarée, avec corbeille de 30 jours sur la clé + (installateur, tests, mode d'emploi dans `LISEZMOI.md`). - Ajout de cette page des nouveautés. ## 10 octobre 2026 diff --git a/slides/sauvegardes/script/99-sauvegarde-cle-usb.rules.modele b/slides/sauvegardes/script/99-sauvegarde-cle-usb.rules.modele new file mode 100644 index 0000000..0233777 --- /dev/null +++ b/slides/sauvegardes/script/99-sauvegarde-cle-usb.rules.modele @@ -0,0 +1,5 @@ +# /etc/udev/rules.d/99-sauvegarde-cle-usb.rules +# Écrit par installer.sh (@UUID@ est remplacé par l'UUID de la clé). +# Quand la partition portant cet UUID apparaît, udev demande à systemd de +# démarrer le service de sauvegarde. Les autres clés ne déclenchent rien. +ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}=="@UUID@", TAG+="systemd", ENV{SYSTEMD_WANTS}+="sauvegarde-cle-usb.service" diff --git a/slides/sauvegardes/script/LISEZMOI.md b/slides/sauvegardes/script/LISEZMOI.md new file mode 100644 index 0000000..897dac4 --- /dev/null +++ b/slides/sauvegardes/script/LISEZMOI.md @@ -0,0 +1,228 @@ +# Sauvegarde automatique sur clé USB + +Petit outil pour **Linux Mint 22** (Cinnamon) : on branche **sa** clé de +sauvegarde, la copie de ses documents démarre toute seule, une notification +dit quand on peut la retirer. + +Accompagne la séance Alpinux « Ranger et sauvegarder ses documents ». + +## Sommaire + +| Pour commencer | Au quotidien | En cas de besoin | +|---|---|---| +| [Pour qui ?](#pour-qui) | [Tester](#tester) | [Restaurer un fichier](#restaurer-un-fichier) | +| [Ce que ça fait](#ce-que-ça-fait) | [Lire le journal](#lire-le-journal) | [Désinstaller](#désinstaller) | +| [Installer (3 commandes)](#installer-en-3-commandes) | [Régler](#régler) | [Limites](#limites-à-connaître) | +| [Trouver l'UUID](#trouver-luuid-de-sa-clé) | | [Comment ça marche](#comment-ça-marche) | + +## Pour qui ? + +Pour qui veut une copie de ses documents **sans y penser** : on branche la +clé, on attend le message, on la débranche. Il faut savoir ouvrir un +terminal et taper une commande avec `sudo` (une fois, pour l'installation). + +## Ce que ça fait + +- Seule **la clé déclarée** déclenche la sauvegarde (reconnue par son + identifiant UUID). Une autre clé ne fait rien. +- Le dossier personnel (ou une liste de dossiers) est copié dans + `Sauvegarde/` sur la clé. Les fois suivantes, seul ce qui a changé est + recopié : c'est rapide. +- Ce qui a été **supprimé** de l'ordinateur est retiré de la clé, pour que la + copie reste fidèle. + +> **Synchroniser n'est pas sauvegarder.** Une copie « miroir » recopie aussi +> les erreurs : un fichier effacé par mégarde disparaîtrait de la clé au +> branchement suivant. C'est pourquoi, par défaut, **rien n'est effacé d'un +> coup** : ce qui est retiré ou remplacé sur la clé est d'abord mis de côté +> dans `.corbeille/AAAA-MM-JJ_HHMMSS/`, et gardé **30 jours**. + +- Notifications : « Sauvegarde en cours… », puis « Sauvegarde terminée, + vous pouvez retirer la clé », ou un message d'erreur (clé pleine…). +- Un fichier `derniere-sauvegarde.txt` à la racine de la clé indique la date + et le résultat de la dernière sauvegarde. +- Ne sont pas copiés : caches (`.cache`), corbeille de l'ordinateur, + miniatures, fichiers temporaires. `Téléchargements` peut être exclu. + +## Installer en 3 commandes + +Branchez d'abord la clé de sauvegarde, puis dans un terminal : + +```bash +git clone --depth 1 https://gitea.alpinux.org/alpinux.cedrica5l/alpinux.presentations.git +cd alpinux.presentations/slides/sauvegardes/script +sudo ./installer.sh +``` + +(Si `git` manque : `sudo apt install git`.) + +L'installateur affiche les clés USB branchées, demande laquelle utiliser et +pour quel utilisateur, puis met tout en place. On peut le relancer sans +risque (changer de clé, mettre à jour) : vos réglages sont conservés. + +Ensuite : **débranchez et rebranchez la clé**, la première sauvegarde part. +Elle peut être longue ; les suivantes sont rapides. + +Sans questions : `sudo ./installer.sh --uuid E234-70AF --utilisateur marie` + +## Trouver l'UUID de sa clé + +L'installateur le fait pour vous. Pour le voir vous-même : + +```bash +lsblk -o NAME,LABEL,UUID,FSTYPE,SIZE,TRAN +``` + +Repérez votre clé (colonne `TRAN` = `usb`, taille, nom `LABEL`) : l'UUID est +sur la ligne de la **partition** (`sdb1`…), par exemple `E234-70AF` pour une +clé FAT, ou `3d6f976c-1822-…` pour une clé ext4. +Attention : **reformater la clé change son UUID** ; relancez alors +`sudo ./installer.sh`. + +## Tester + +```bash +sudo sauvegarde-cle-usb --simulation # montre ce qui serait copié/supprimé, sans rien faire +sudo systemctl start sauvegarde-cle-usb # lance une vraie sauvegarde maintenant +``` + +Pour vérifier la logique sans clé ni `sudo` (développeurs) : +`./tester-sans-sudo.sh` — un dossier temporaire joue le rôle de la clé. + +## Lire le journal + +```bash +journalctl -u sauvegarde-cle-usb # tout l'historique +journalctl -u sauvegarde-cle-usb -b # depuis le démarrage +journalctl -u sauvegarde-cle-usb -f # en direct, pendant une sauvegarde +``` + +Ou plus simplement, ouvrez `derniere-sauvegarde.txt` sur la clé. + +## Restaurer un fichier + +**Un fichier qui existe encore dans la sauvegarde** : ouvrez la clé dans le +gestionnaire de fichiers, dossier `Sauvegarde//…`, et copiez le +fichier où vous voulez (copier, pas déplacer). + +**Un fichier supprimé ou une ancienne version** : il est dans la corbeille de +la clé, un dossier caché. Dans le gestionnaire de fichiers, appuyez sur +**Ctrl+H** pour voir les dossiers cachés, puis ouvrez `.corbeille/`. Chaque +sous-dossier porte la date de la sauvegarde où le fichier a été retiré +(ex. `2026-11-05_183012`) et reprend l'arborescence de vos dossiers. + +Pour chercher un fichier par son nom : + +```bash +find /media/$USER/*/.corbeille -iname '*lettre*' +``` + +**Tout restaurer** (nouvel ordinateur…) : copiez le contenu de +`Sauvegarde//` dans votre dossier personnel. + +## Régler + +Tout est dans `/etc/sauvegarde-cle-usb.conf`, commenté : +`sudo xed /etc/sauvegarde-cle-usb.conf` + +| Réglage | Rôle | Par défaut | +|---|---|---| +| `SOURCES` | dossiers à sauvegarder | le dossier personnel | +| `DOSSIER_DESTINATION` | dossier sur la clé | `Sauvegarde` | +| `CORBEILLE_JOURS` | durée de conservation de `.corbeille` ; `0` = miroir pur, sans filet (déconseillé) | `30` | +| `EXCLUSIONS` | ce qu'on ne copie pas (enlever le `#` devant `Téléchargements/` pour l'exclure) | caches, corbeille, temporaires | +| `VERIFIER_ESPACE` | vérifier la place libre avant de commencer | `oui` | + +Pris en compte au prochain branchement. + +## Désinstaller + +```bash +sudo ./installer.sh --desinstaller +``` + +Retire le script, le service, la règle et la configuration. Les copies +présentes sur la clé ne sont pas touchées. + +## Limites à connaître + +- **Une clé n'est qu'une copie, et elle reste à la maison.** Un cambriolage + ou un dégât des eaux emporte l'ordinateur *et* la clé. Gardez aussi une + copie **ailleurs** (une 2ᵉ clé chez un proche, au travail, ou en ligne). +- **Attendez la notification** « Sauvegarde terminée » avant de retirer la + clé. Retirée en cours de route, la copie est incomplète (elle se + complétera au branchement suivant). +- **Clé FAT32 / exFAT / NTFS** (les clés vendues en magasin) : les documents + sont bien copiés, mais pas les droits Linux ni les liens symboliques + (ignorés, ce n'est pas une erreur). En **FAT32**, pas de fichier de plus de + **4 Go** (signalé par une notification) et certains caractères sont interdits + dans les noms (`: * ? " < > | \`) : ces fichiers-là ne sont pas copiés, la + sauvegarde est dite « avec avertissements » et, par prudence, rien n'est + supprimé de la clé ce jour-là. Pour une copie fidèle, formatez la clé en + **ext4** (Disques ou Formater dans le menu de la clé) — elle ne sera plus + lisible sous Windows. +- **Papiers sensibles** (impôts, banque, santé, pièces d'identité) : une clé + se perd facilement. **Chiffrez-la** (Disques → Formater → « Protégé par mot + de passe (LUKS) »). Après le déverrouillage, le démarrage automatique n'est + pas garanti : lancez alors `sudo systemctl start sauvegarde-cle-usb`, et + choisissez dans l'installateur la ligne `crypt` (clé déverrouillée). +- La corbeille de la clé prend de la place. Si la clé est pleine, supprimez + les plus vieux dossiers de `.corbeille/`. +- Les fichiers en cours d'écriture pendant la copie (base de données de + Thunderbird ou Firefox ouverts…) peuvent être copiés dans un état + incohérent : fermez ces logiciels pour une copie parfaite. +- Si la clé est branchée au démarrage de l'ordinateur, la sauvegarde part + aussitôt, avant même la connexion (sans notification). + +## Comment ça marche + +``` +clé branchée ─► udev reconnaît l'UUID ─► systemd lance sauvegarde-cle-usb.service + ─► le script monte la clé sur /run/sauvegarde-cle-usb + ─► vérifications ─► rsync ─► purge des vieilles corbeilles + ─► sync, démontage ─► notification « vous pouvez retirer la clé » +``` + +| Fichier (dans ce dossier) | Installé dans | +|---|---| +| `sauvegarde-cle-usb` (le script) | `/usr/local/sbin/` | +| `sauvegarde-cle-usb.conf` (modèle) | `/etc/sauvegarde-cle-usb.conf` | +| `sauvegarde-cle-usb.service` | `/etc/systemd/system/` | +| `99-sauvegarde-cle-usb.rules.modele` | `/etc/udev/rules.d/99-sauvegarde-cle-usb.rules` | +| `installer.sh`, `tester-sans-sudo.sh` | (non installés) | + +**Pourquoi udev + systemd ?** udev voit la clé arriver, mais ne doit pas +lancer lui-même une tâche longue (il la tuerait au bout de quelques +secondes) : il demande donc à systemd de démarrer le service, qui tourne +aussi longtemps que nécessaire et écrit dans le journal. + +**Pourquoi un montage dédié ?** Quand on branche la clé, Cinnamon (Nemo, +udisks) la monte souvent dans `/media//…`, mais pas toujours +(personne de connecté, automontage désactivé) et pas forcément avant le +démarrage de la sauvegarde. Le script monte donc lui-même la clé sur +`/run/sauvegarde-cle-usb`, que Nemo l'ait montée ou non : Linux accepte +plusieurs montages du même système de fichiers. Avantages : le chemin est +toujours le même, les options sont maîtrisées (clé FAT montée au nom de +l'utilisateur), pas de course avec l'automontage, et un « Éjecter » dans Nemo +pendant la copie ne retire que le montage de Nemo, pas celui de la +sauvegarde. Si ce montage échoue, le script réutilise le montage de Nemo +s'il est en écriture. À la fin : `sync` puis démontage de son seul montage +(celui de Nemo reste, la clé reste visible). + +**Garde-fous** (une erreur de chemin avec suppression peut effacer des +données) : + +- la sauvegarde refuse de partir si la destination n'est pas un sous-dossier + de la clé montée **et** portant l'UUID attendu (même à travers un lien + symbolique) ; +- refus si `SOURCES` est vide, si un dossier source est absent ou **vide** + (disque non monté, dossier chiffré verrouillé : sinon tout serait retiré de + la clé), ou si deux sources portent le même nom ; +- jamais deux sauvegardes en même temps (verrou) ; +- vérification de la place libre avant de commencer ; +- si rsync rencontre une erreur de lecture, il ne supprime rien. + +**Options rsync** : clé ext4 → `-aH` (tout est conservé) ; +FAT/exFAT/NTFS → `-rt --modify-window=2` (dates à 2 s près, sans droits) ; +toujours `--delete --one-file-system`, plus +`--backup --backup-dir=.corbeille/` si `CORBEILLE_JOURS` > 0. diff --git a/slides/sauvegardes/script/installer.sh b/slides/sauvegardes/script/installer.sh new file mode 100755 index 0000000..a647b1d --- /dev/null +++ b/slides/sauvegardes/script/installer.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# ============================================================================= +# installer.sh — installe (ou désinstalle) la sauvegarde automatique sur clé +# ----------------------------------------------------------------------------- +# Utilisation, depuis ce dossier : +# sudo ./installer.sh installation (questions posées) +# sudo ./installer.sh --uuid E234-70AF --utilisateur marie +# installation sans questions +# sudo ./installer.sh --desinstaller tout retirer (la clé n'est pas touchée) +# +# On peut le relancer autant de fois que l'on veut (changer de clé, mettre +# à jour le script) : les réglages personnels de la configuration existante +# sont conservés, seuls l'UUID et l'utilisateur sont mis à jour. +# ============================================================================= + +set -euo pipefail +export LC_ALL=C.UTF-8 + +ICI="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +SCRIPT_CIBLE="/usr/local/sbin/sauvegarde-cle-usb" +SERVICE_CIBLE="/etc/systemd/system/sauvegarde-cle-usb.service" +REGLE_CIBLE="/etc/udev/rules.d/99-sauvegarde-cle-usb.rules" +CONF_CIBLE="/etc/sauvegarde-cle-usb.conf" + +UUID_CHOISI="" +UTILISATEUR_CHOISI="" +ACTION="installer" + +erreur() { printf 'ERREUR : %s\n' "$*" >&2; exit 1; } +titre() { printf '\n== %s ==\n' "$*"; } + +aide() { + sed -n '4,13p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --desinstaller|--désinstaller) ACTION="desinstaller" ;; + --uuid) UUID_CHOISI="${2:-}"; shift ;; + --utilisateur) UTILISATEUR_CHOISI="${2:-}"; shift ;; + --aide|--help|-h) aide; exit 0 ;; + *) aide >&2; erreur "option inconnue : $1" ;; + esac + shift +done + +[[ "$(id -u)" -eq 0 ]] || erreur "lancez ce programme avec sudo : sudo $0" + +recharger() { + systemctl daemon-reload + udevadm control --reload-rules +} + +# ----------------------------------------------------------------------------- +# Désinstallation +# ----------------------------------------------------------------------------- +if [[ "$ACTION" == "desinstaller" ]]; then + titre "Désinstallation" + systemctl stop sauvegarde-cle-usb.service 2>/dev/null || true + for f in "$REGLE_CIBLE" "$SERVICE_CIBLE" "$SCRIPT_CIBLE" "$CONF_CIBLE"; do + if [[ -e "$f" ]]; then + rm -f -- "$f" + echo "Supprimé : $f" + fi + done + recharger + echo + echo "La sauvegarde automatique est désinstallée." + echo "Les copies déjà présentes sur la clé n'ont pas été touchées." + exit 0 +fi + +# ----------------------------------------------------------------------------- +# Vérifications préalables +# ----------------------------------------------------------------------------- +for f in sauvegarde-cle-usb sauvegarde-cle-usb.conf sauvegarde-cle-usb.service 99-sauvegarde-cle-usb.rules.modele; do + [[ -f "$ICI/$f" ]] || erreur "fichier manquant à côté de installer.sh : $f" +done +command -v rsync >/dev/null || erreur "rsync n'est pas installé : sudo apt install rsync" +command -v notify-send >/dev/null || echo "Remarque : notify-send absent (sudo apt install libnotify-bin) : pas de notifications." + +# ----------------------------------------------------------------------------- +# Choix de la clé +# ----------------------------------------------------------------------------- +# Une partition est candidate si elle a un UUID et si elle est (directement ou +# non, ex. clé chiffrée déverrouillée) sur un périphérique USB. +lister_candidats() { + local nom type uuid fstype + while read -r nom type uuid fstype; do + [[ -n "$uuid" && -n "$fstype" ]] || continue + case "$type" in part|disk|crypt) ;; *) continue ;; esac + lsblk -nso TRAN "$nom" 2>/dev/null | grep -qx usb || continue + printf '%s %s %s\n' "$nom" "$uuid" "$fstype" + done < <(lsblk -rnpo NAME,TYPE,UUID,FSTYPE) +} + +if [[ -z "$UUID_CHOISI" ]]; then + titre "Choix de la clé de sauvegarde" + echo "Branchez la clé à utiliser (et, si elle est chiffrée, déverrouillez-la" + echo "dans le gestionnaire de fichiers), puis appuyez sur Entrée." + read -r _ + mapfile -t CANDIDATS < <(lister_candidats) + [[ "${#CANDIDATS[@]}" -gt 0 ]] || erreur "aucune clé USB trouvée. Est-elle bien branchée ?" + + echo "Clés USB trouvées :" + echo + i=0 + for c in "${CANDIDATS[@]}"; do + i=$((i + 1)) + read -r nom _ _ <<<"$c" + printf ' [%d] ' "$i" + lsblk -dno NAME,LABEL,UUID,FSTYPE,SIZE "$nom" | head -n1 + done + echo + echo "(Colonnes : nom, étiquette, UUID, système de fichiers, taille)" + echo "Pour comparer : lsblk -o NAME,LABEL,UUID,FSTYPE,SIZE,TRAN" + read -r -p "Numéro de la clé de sauvegarde : " num + [[ "$num" =~ ^[0-9]+$ && "$num" -ge 1 && "$num" -le "${#CANDIDATS[@]}" ]] || erreur "choix invalide." + read -r _ UUID_CHOISI fstype <<<"${CANDIDATS[$((num - 1))]}" +else + periph="$(blkid -U "$UUID_CHOISI" 2>/dev/null || true)" + if [[ -n "$periph" ]]; then + fstype="$(blkid -o value -s TYPE "$periph" 2>/dev/null || true)" + else + fstype="" + echo "Attention : aucune partition d'UUID $UUID_CHOISI n'est branchée actuellement." + fi +fi + +[[ "$UUID_CHOISI" =~ ^[A-Za-z0-9-]+$ ]] || erreur "UUID invalide : $UUID_CHOISI" + +periph="$(blkid -U "$UUID_CHOISI" 2>/dev/null || true)" +if [[ -n "$periph" && "$(lsblk -dno TYPE "$periph" 2>/dev/null)" == "crypt" ]]; then + echo "Clé chiffrée : le démarrage automatique n'est pas garanti après le" + echo "déverrouillage. Lancez alors la sauvegarde avec :" + echo " sudo systemctl start sauvegarde-cle-usb" +fi + +case "${fstype:-}" in + crypto_LUKS) + erreur "c'est l'enveloppe chiffrée de la clé. Déverrouillez-la dans le gestionnaire de fichiers, relancez l'installateur et choisissez la ligne « crypt » qui apparaît." ;; + ext4|ext3|ext2|btrfs|xfs) echo "Clé au format Linux ($fstype) : droits et propriétaires seront conservés." ;; + vfat) echo "Clé au format FAT32 : ni droits ni liens symboliques, fichiers de 4 Go maximum." ;; + exfat|ntfs) echo "Clé au format $fstype : ni droits ni propriétaires (suffisant pour des documents)." ;; + "") ;; + *) echo "Format $fstype : non testé, copie prudente sans droits." ;; +esac + +# ----------------------------------------------------------------------------- +# Choix de l'utilisateur +# ----------------------------------------------------------------------------- +if [[ -z "$UTILISATEUR_CHOISI" ]]; then + defaut="${SUDO_USER:-}" + [[ "$defaut" == "root" ]] && defaut="" + read -r -p "Utilisateur dont on sauvegarde le dossier personnel [${defaut}] : " UTILISATEUR_CHOISI + UTILISATEUR_CHOISI="${UTILISATEUR_CHOISI:-$defaut}" +fi +[[ -n "$UTILISATEUR_CHOISI" ]] || erreur "aucun utilisateur indiqué." +[[ "$UTILISATEUR_CHOISI" =~ ^[a-z_][a-z0-9_.-]*$ ]] || erreur "nom d'utilisateur invalide : $UTILISATEUR_CHOISI" +id -u "$UTILISATEUR_CHOISI" >/dev/null 2>&1 || erreur "utilisateur inconnu : $UTILISATEUR_CHOISI" +DOSSIER_PERSO="$(getent passwd "$UTILISATEUR_CHOISI" | cut -d: -f6)" + +# ----------------------------------------------------------------------------- +# Installation des fichiers +# ----------------------------------------------------------------------------- +titre "Installation" + +install -o root -g root -m 755 "$ICI/sauvegarde-cle-usb" "$SCRIPT_CIBLE" +echo "Script : $SCRIPT_CIBLE" + +install -o root -g root -m 644 "$ICI/sauvegarde-cle-usb.service" "$SERVICE_CIBLE" +echo "Service : $SERVICE_CIBLE" + +if [[ -f "$CONF_CIBLE" ]]; then + # Configuration existante : on garde les réglages, on met à jour la clé. + sed -i -e "s|^UUID=.*|UUID=\"$UUID_CHOISI\"|" \ + -e "s|^UTILISATEUR=.*|UTILISATEUR=\"$UTILISATEUR_CHOISI\"|" "$CONF_CIBLE" + echo "Réglages : $CONF_CIBLE (conservés, clé et utilisateur mis à jour)" + echo " vérifiez la liste SOURCES si l'utilisateur a changé." +else + sed -e "s|^UUID=.*|UUID=\"$UUID_CHOISI\"|" \ + -e "s|^UTILISATEUR=.*|UTILISATEUR=\"$UTILISATEUR_CHOISI\"|" \ + -e "s|/home/marie|$DOSSIER_PERSO|g" \ + "$ICI/sauvegarde-cle-usb.conf" > "$CONF_CIBLE" + echo "Réglages : $CONF_CIBLE" +fi +chown root:root "$CONF_CIBLE" +chmod 644 "$CONF_CIBLE" + +sed "s|@UUID@|$UUID_CHOISI|" "$ICI/99-sauvegarde-cle-usb.rules.modele" > "$REGLE_CIBLE" +chmod 644 "$REGLE_CIBLE" +echo "Règle udev : $REGLE_CIBLE" + +recharger + +titre "C'est prêt" +cat </.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//. 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 diff --git a/slides/sauvegardes/script/sauvegarde-cle-usb.conf b/slides/sauvegardes/script/sauvegarde-cle-usb.conf new file mode 100644 index 0000000..b39a31e --- /dev/null +++ b/slides/sauvegardes/script/sauvegarde-cle-usb.conf @@ -0,0 +1,60 @@ +# shellcheck shell=bash disable=SC2034 +# ============================================================================= +# /etc/sauvegarde-cle-usb.conf — réglages de la sauvegarde automatique +# ----------------------------------------------------------------------------- +# Ce fichier est écrit par installer.sh. Vous pouvez le modifier ensuite : +# sudo xed /etc/sauvegarde-cle-usb.conf +# Les changements sont pris en compte au prochain branchement de la clé. +# Si vous changez l'UUID, relancez plutôt : sudo ./installer.sh +# Syntaxe : celle de bash (NOM="valeur", listes entre parenthèses). +# ============================================================================= + +# Identifiant (UUID) du système de fichiers de LA clé de sauvegarde. +# Une autre clé ne déclenche rien. Pour le trouver : lsblk -o NAME,LABEL,UUID +UUID="XXXX-XXXX" + +# Utilisateur à prévenir par des notifications (et propriétaire des copies). +UTILISATEUR="marie" + +# Dossiers à sauvegarder (un par ligne, entre guillemets). +# Par défaut : tout le dossier personnel. +SOURCES=( + "/home/marie" +) +# Exemple pour ne garder que l'essentiel : +# SOURCES=( +# "/home/marie/Documents" +# "/home/marie/Images" +# "/home/marie/Bureau" +# ) + +# Dossier, sur la clé, qui reçoit la copie (ex. la clé contiendra +# Sauvegarde/marie/Documents/...). +DOSSIER_DESTINATION="Sauvegarde" + +# Ce qui n'existe plus sur l'ordinateur est retiré de la clé. Mais avant, +# ces fichiers (et les anciennes versions des fichiers modifiés) sont mis de +# côté dans .corbeille/AAAA-MM-JJ_HHMMSS sur la clé, pendant ce nombre de +# jours. 0 = miroir pur : ce qui est supprimé sur l'ordinateur est supprimé +# de la clé, sans filet. (Déconseillé.) +CORBEILLE_JOURS=30 + +# Ce qu'on ne sauvegarde pas (motifs rsync, relatifs aux dossiers ci-dessus). +# « dossier/ » = un dossier de ce nom, où qu'il soit +# « *.tmp » = tous les fichiers finissant par .tmp +EXCLUSIONS=( + ".cache/" # caches des logiciels (se recréent tout seuls) + ".local/share/Trash/" # la corbeille de l'ordinateur + ".thumbnails/" # miniatures d'images + ".gvfs/" + "*.tmp" # fichiers temporaires + "*~" # copies de secours des éditeurs + ".~lock.*#" # verrous LibreOffice (document ouvert) + "*.part" # téléchargements inachevés + # "Téléchargements/" # enlevez le # pour ne pas sauvegarder ce dossier + # ".local/share/Steam/" # jeux : très gros, se retéléchargent +) + +# Vérifier avant de commencer qu'il y a assez de place sur la clé ("oui"/"non"). +# Cela demande un premier passage rapide (lecture seule) des dossiers. +VERIFIER_ESPACE="oui" diff --git a/slides/sauvegardes/script/sauvegarde-cle-usb.service b/slides/sauvegardes/script/sauvegarde-cle-usb.service new file mode 100644 index 0000000..b59fd39 --- /dev/null +++ b/slides/sauvegardes/script/sauvegarde-cle-usb.service @@ -0,0 +1,18 @@ +# /etc/systemd/system/sauvegarde-cle-usb.service +# Démarré par la règle udev 99-sauvegarde-cle-usb.rules quand LA clé de +# sauvegarde est branchée (udev ne doit pas lancer lui-même de tâche longue). +# Lancement manuel : sudo systemctl start sauvegarde-cle-usb +# Journal : journalctl -u sauvegarde-cle-usb + +[Unit] +Description=Sauvegarde automatique sur clé USB +After=local-fs.target + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/sauvegarde-cle-usb +# Une première sauvegarde peut être longue : pas de limite de temps. +TimeoutStartSec=infinity +# Rester discret pendant que l'ordinateur sert à autre chose. +Nice=10 +IOSchedulingClass=idle diff --git a/slides/sauvegardes/script/tester-sans-sudo.sh b/slides/sauvegardes/script/tester-sans-sudo.sh new file mode 100755 index 0000000..4701c61 --- /dev/null +++ b/slides/sauvegardes/script/tester-sans-sudo.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# ============================================================================= +# tester-sans-sudo.sh — vérifie la logique de sauvegarde, sans clé ni sudo +# ----------------------------------------------------------------------------- +# Utilise le mode test de sauvegarde-cle-usb (SAUVEGARDE_TEST_DESTINATION) : +# un dossier temporaire joue le rôle de la clé. Rien n'est monté, rien n'est +# écrit dans /etc. Usage : ./tester-sans-sudo.sh [dossier-de-travail] +# ============================================================================= + +# shellcheck disable=SC2016 # les bash -c reçoivent leurs arguments via $1, voulu +set -euo pipefail +ICI="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT="$ICI/sauvegarde-cle-usb" +BASE="${1:-$(mktemp -d)}" +TRAVAIL="$BASE/essai-$$" +SRC="$TRAVAIL/maison/marie" +CLE="$TRAVAIL/cle" +CONF="$TRAVAIL/test.conf" +export TMPDIR="$TRAVAIL" + +REUSSIS=0 +RATES=0 +ok() { echo " OK $*"; REUSSIS=$((REUSSIS + 1)); } +rate() { echo " RATÉ $*"; RATES=$((RATES + 1)); } +verifier() { local desc="$1"; shift; if "$@"; then ok "$desc"; else rate "$desc"; fi; } + +ecrire_conf() { # $1 = CORBEILLE_JOURS, $2 = DOSSIER_DESTINATION, $3.. = sources + local jours="$1" dest="$2"; shift 2 + { + echo "UUID=\"TEST\"" + echo "UTILISATEUR=\"$(id -un)\"" + printf 'SOURCES=('; printf ' "%s"' "$@"; echo ' )' + echo "DOSSIER_DESTINATION=\"$dest\"" + echo "CORBEILLE_JOURS=$jours" + echo 'EXCLUSIONS=( ".cache/" ".local/share/Trash/" "*.tmp" )' + echo 'VERIFIER_ESPACE="oui"' + } > "$CONF" +} + +lancer() { # lance le script en mode test ; sortie dans $TRAVAIL/sortie.txt + sleep 1 # horodatage à la seconde : une corbeille par lancement + set +e + SAUVEGARDE_CONF="$CONF" SAUVEGARDE_TEST_DESTINATION="${DEST_TEST:-$CLE}" \ + "$SCRIPT" "$@" > "$TRAVAIL/sortie.txt" 2>&1 + CODE=$? + set -e +} + +nb_corbeilles() { find "$CLE/.corbeille" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l; } + +mkdir -p "$SRC/Documents" "$SRC/Images" "$SRC/.cache/firefox" "$SRC/.local/share/Trash/files" "$CLE" +echo "lettre v1" > "$SRC/Documents/lettre.txt" +echo "impôts 2026" > "$SRC/Documents/impots.txt" +echo "photo" > "$SRC/Images/vacances.jpg" +echo "cache" > "$SRC/.cache/firefox/gros-cache" +echo "jeté" > "$SRC/.local/share/Trash/files/vieux.txt" +echo "temporaire" > "$SRC/Documents/brouillon.tmp" +ecrire_conf 30 Sauvegarde "$SRC" +COPIE="$CLE/Sauvegarde/marie" + +echo "Dossier de test : $TRAVAIL" + +echo "1. Première sauvegarde" +lancer +verifier "code de sortie 0" test "$CODE" -eq 0 +verifier "lettre.txt copiée" test -f "$COPIE/Documents/lettre.txt" +verifier "vacances.jpg copiée" test -f "$COPIE/Images/vacances.jpg" +verifier ".cache exclu" test ! -e "$COPIE/.cache" +verifier "corbeille locale exclue" test ! -e "$COPIE/.local/share/Trash" +verifier "*.tmp exclu" test ! -e "$COPIE/Documents/brouillon.tmp" +verifier "derniere-sauvegarde.txt = OK" grep -q 'Résultat *: OK' "$CLE/derniere-sauvegarde.txt" + +echo "2. Simulation (--simulation) après suppression de impots.txt" +rm "$SRC/Documents/impots.txt" +lancer --simulation +verifier "code de sortie 0" test "$CODE" -eq 0 +verifier "annonce la suppression" grep -q 'deleting.*impots.txt' "$TRAVAIL/sortie.txt" +verifier "impots.txt toujours sur la clé" test -f "$COPIE/Documents/impots.txt" +verifier "aucune corbeille créée" test "$(nb_corbeilles)" -eq 0 + +echo "3. Suppression côté ordinateur → retiré de la copie, gardé en corbeille" +lancer +verifier "code de sortie 0" test "$CODE" -eq 0 +verifier "impots.txt absent de la copie" test ! -e "$COPIE/Documents/impots.txt" +CORB1="$(find "$CLE/.corbeille" -mindepth 1 -maxdepth 1 -type d | head -n1)" +verifier "nom de corbeille AAAA-MM-JJ_HHMMSS" bash -c '[[ "$(basename "$1")" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}$ ]]' _ "$CORB1" +verifier "impots.txt dans .corbeille/" test -f "$CORB1/marie/Documents/impots.txt" + +echo "4. Modification d'un fichier → nouvelle version copiée, ancienne en corbeille" +echo "lettre v2, bien plus longue" > "$SRC/Documents/lettre.txt" +lancer +verifier "code de sortie 0" test "$CODE" -eq 0 +verifier "copie = v2" grep -q 'v2' "$COPIE/Documents/lettre.txt" +verifier "ancienne v1 en corbeille" bash -c 'grep -rqs "lettre v1" "$1"/.corbeille/*/marie/Documents/lettre.txt' _ "$CLE" +verifier "deux corbeilles" test "$(nb_corbeilles)" -eq 2 + +echo "5. Purge des corbeilles de plus de 30 jours" +mkdir -p "$CLE/.corbeille/2026-09-01_120000/marie" "$CLE/.corbeille/2026-10-30_120000" +echo vieux > "$CLE/.corbeille/2026-09-01_120000/marie/vieux.txt" +touch -d '40 days ago' "$CLE/.corbeille/2026-09-01_120000" +touch -d '6 days ago' "$CLE/.corbeille/2026-10-30_120000" +mkdir -p "$CLE/.corbeille/a-moi"; touch -d '90 days ago' "$CLE/.corbeille/a-moi" +lancer +verifier "code de sortie 0" test "$CODE" -eq 0 +verifier "corbeille de 40 jours purgée" test ! -e "$CLE/.corbeille/2026-09-01_120000" +verifier "corbeille de 6 jours gardée" test -d "$CLE/.corbeille/2026-10-30_120000" +verifier "dossier au nom inattendu épargné" test -d "$CLE/.corbeille/a-moi" +verifier "purge annoncée dans le journal" grep -q 'Purge.*2026-09-01_120000' "$TRAVAIL/sortie.txt" + +echo "6. CORBEILLE_JOURS=0 : miroir pur, sans corbeille" +ecrire_conf 0 Sauvegarde "$SRC" +avant="$(nb_corbeilles)" +rm "$SRC/Images/vacances.jpg" +lancer +verifier "code de sortie 0" test "$CODE" -eq 0 +verifier "vacances.jpg absent de la copie" test ! -e "$COPIE/Images/vacances.jpg" +verifier "aucune nouvelle corbeille" test "$(nb_corbeilles)" -eq "$avant" +verifier "vacances.jpg nulle part sur la clé" test -z "$(find "$CLE" -name vacances.jpg)" +ecrire_conf 30 Sauvegarde "$SRC" + +echo "7. Clé FAT simulée : lien symbolique ignoré sans erreur" +ln -s Documents/lettre.txt "$SRC/raccourci" +SAUVEGARDE_TEST_FSTYPE=vfat lancer +verifier "code de sortie 0" test "$CODE" -eq 0 +verifier "lien non copié" test ! -e "$COPIE/raccourci" -a ! -L "$COPIE/raccourci" +rm "$SRC/raccourci" + +echo "8. Garde-fous (tous doivent REFUSER, sans rien toucher)" +empreinte() { find "$CLE" "$SRC" -printf '%p %s\n' | sort | md5sum; } +E0="$(empreinte)" +DEST_TEST="$TRAVAIL/nexiste-pas" lancer +verifier "destination de test absente → refus" test "$CODE" -ne 0 +DEST_TEST="/" lancer +verifier "destination « / » → refus" test "$CODE" -ne 0 +for d in "../evasion" "a/../../evasion" "/tmp/x" "." ".corbeille" ""; do + ecrire_conf 30 "$d" "$SRC"; lancer + verifier "DOSSIER_DESTINATION=« $d » → refus" test "$CODE" -ne 0 +done +verifier "rien n'a été créé hors de la clé" test ! -e "$TRAVAIL/evasion" -a ! -e "$TRAVAIL/x" +ln -s "$TRAVAIL/maison" "$CLE/piege" +ecrire_conf 30 piege "$SRC"; lancer +verifier "lien sur la clé vers l'extérieur → refus" test "$CODE" -ne 0 +rm "$CLE/piege" +ecrire_conf 30 Sauvegarde; lancer +verifier "SOURCES vide → refus" test "$CODE" -ne 0 +mkdir -p "$TRAVAIL/vide" +ecrire_conf 30 Sauvegarde "$TRAVAIL/vide"; lancer +verifier "source vide → refus" test "$CODE" -ne 0 +ecrire_conf 30 Sauvegarde "$TRAVAIL/absent"; lancer +verifier "source inexistante → refus" test "$CODE" -ne 0 +ecrire_conf 30 Sauvegarde "$CLE"; lancer +verifier "source contenant la clé → refus" test "$CODE" -ne 0 +ecrire_conf 30 Sauvegarde "$SRC/Documents" "$TRAVAIL/maison/marie/../marie/Documents"; lancer +verifier "deux sources au même nom → refus" test "$CODE" -ne 0 +verifier "clé et sources inchangées" test "$(empreinte)" = "$E0" +ecrire_conf 30 Sauvegarde "$SRC" + +echo "9. Verrou : deux sauvegardes simultanées impossibles" +exec 8>"$TRAVAIL/sauvegarde-cle-usb-test.lock"; flock -n 8 +lancer +verifier "2e exécution s'arrête sans rien faire" grep -q 'déjà en cours' "$TRAVAIL/sortie.txt" +exec 8>&- + +echo +echo "Bilan : $REUSSIS réussis, $RATES ratés." +[[ "$RATES" -eq 0 ]] && rm -rf "$TRAVAIL" +[[ "$RATES" -eq 0 ]]