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
This commit is contained in:
parent
9f65bc807b
commit
a04a72054a
8 changed files with 1164 additions and 0 deletions
|
|
@ -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).
|
et conducteur dans `slides/sauvegardes/` (#1).
|
||||||
- « Ranger et sauvegarder » : une diapo propose une arborescence type pour le dossier
|
- « 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.
|
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.
|
- Ajout de cette page des nouveautés.
|
||||||
|
|
||||||
## 10 octobre 2026
|
## 10 octobre 2026
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
228
slides/sauvegardes/script/LISEZMOI.md
Normal file
228
slides/sauvegardes/script/LISEZMOI.md
Normal file
|
|
@ -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/<votre nom>/…`, 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/<votre nom>/` 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/<utilisateur>/…`, 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/<date>` si `CORBEILLE_JOURS` > 0.
|
||||||
208
slides/sauvegardes/script/installer.sh
Executable file
208
slides/sauvegardes/script/installer.sh
Executable file
|
|
@ -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 <<FIN
|
||||||
|
Clé de sauvegarde : UUID $UUID_CHOISI
|
||||||
|
Utilisateur : $UTILISATEUR_CHOISI
|
||||||
|
|
||||||
|
1. Pour tester sans rien modifier : sudo sauvegarde-cle-usb --simulation
|
||||||
|
2. Débranchez puis rebranchez la clé : la sauvegarde démarre toute seule.
|
||||||
|
Attendez la notification « Sauvegarde terminée » avant de la retirer.
|
||||||
|
3. Journal : journalctl -u sauvegarde-cle-usb
|
||||||
|
|
||||||
|
Réglages (dossiers, exclusions, corbeille) : sudo xed $CONF_CIBLE
|
||||||
|
FIN
|
||||||
475
slides/sauvegardes/script/sauvegarde-cle-usb
Executable file
475
slides/sauvegardes/script/sauvegarde-cle-usb
Executable file
|
|
@ -0,0 +1,475 @@
|
||||||
|
#!/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
|
||||||
60
slides/sauvegardes/script/sauvegarde-cle-usb.conf
Normal file
60
slides/sauvegardes/script/sauvegarde-cle-usb.conf
Normal file
|
|
@ -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"
|
||||||
18
slides/sauvegardes/script/sauvegarde-cle-usb.service
Normal file
18
slides/sauvegardes/script/sauvegarde-cle-usb.service
Normal file
|
|
@ -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
|
||||||
167
slides/sauvegardes/script/tester-sans-sudo.sh
Executable file
167
slides/sauvegardes/script/tester-sans-sudo.sh
Executable file
|
|
@ -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/<date>" 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 ]]
|
||||||
Loading…
Reference in a new issue