---
navigation: true
title: Scripts bash
main:
  fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Scripts bash

Quelques scripts en vracs qui m'ont sauvé la vie.

## Detection de doublons et remplacement par des hardlinks
---

Six mois après avoir téléchargé des térabytes de media, je me suis rendu compte que Sonarr et Radarr les copaient dans ma biblio Plex au lieu de créer des hardlinks. C'est dû à un mécanisme contre intuitif qui est que si vous montez plusieurs dossiers dans Sonarr/Radarr, il les voit comme deux systemes de fichiers différents. Et ne peut donc pas créer de hardlinks. C'est pour cela qu'il ne faut monter qu'un seul dossier parent, qui contient tous les enfants (`downloads`, `movies`, `tvseries` dans le dossier parent `media` par exemple).

J'ai donc restructuré mes dossiers, remis à la main chaque chemin dans Qbittorrent, Plex, et autres. Il restait à trouver un moyen de détecter les doublons existants et d'automatiquement les supprimer et de créer des hardlinks à la place, pour économiser de l'espace.

Mes dossiers :

```console
.
└── media
    ├── seedbox
    ├── radarr
    │   └── tv-radarr
    ├── movies
    └── tvseries
```

Mes dossiers originaux sont dans `seedbox`, et il ne faut surtout pas les modifier pour qu'ils continuent d'etre "seed". Les copies, et donc doublons, sont dans `movies` et `tvseries`. Mais pour complexifier la chose, j'ai aussi des media uniques originaux déposés par ailleurs dans `movies` et `tvseries`, sinon cela serait trop facile. Et dans ces deux dossiers, il peut y avoir des sous dossiers, des sous-sous dossiers, etc. 

L'idée est donc de : 

- lister les originaux dans seedbox
- lister les fichiers dans movies
- comparer les deux listes et isoler les chemins des doublons
- supprimer les doublons
- hardlinker les originaux dans les dossiers des doublons supprimés

Alors oui j'ai demandé à ChatGPT et à Qwen3 (que j'héberge sur une machine dédiée à l'IA). Et evidemment ils m'ont conseillé les rfind, rdfind, dupes, rdupes, rmlint... Mais comparer les hash de 30TB de media, faudrait plusieurs jours, j'ai vite abandonné.

Au final, je n'ai que des `.mkv` à chercher et les doublons ont exactement les mêmes noms que les originaux, ce qui simplifie grandement la tâche. Un simple script bash devait donc être suffisant.

Je vous passe les incessantes questions réponses avec ChatGPT, je suis assez déçu. Qwen3 a été bien plus propre. ChatGPT n'a pas cessé de mettre des solutions type awk, qui pètent la lecture des chemins au moindre espace. En faisant relire à Qwen, et en lui demandant de se passer de awk, le résultat a été immediatement plus qualitatif.

Pour tester, j'ai d'abord demandé un script qui ne fait que lister et comparer :

```bash
#!/bin/bash

# Créer un tableau associatif pour stocker les doublons
declare -A seen

# Trouver tous les fichiers .mkv uniquement (exclure les dossiers)
find /media/seedbox /media/movies /media/tvseries -type f -name "*.mkv" -print0 | \
while IFS= read -r -d '' file; do
    # Obtenir l'inode du fichier et son chemin
    inode=$(stat --format="%i" "$file")
    filename=$(basename "$file")
    
    # Si ce nom de fichier a déjà été vu
    if [[ -n "${seen[$filename]}" ]]; then
        # Vérifier si l'inode est différent du précédent
        if [[ "${seen[$filename]}" != "$inode" ]]; then
            # Ajouter le doublon à la sortie en affichant les chemins complets
            echo "Doublons pour \"$filename\" :"
            echo "${seen["$filename"]} ${seen["$filename:full_path"]}"
            echo "$inode $file"
            echo
        fi
    else
        # Si c'est la première fois qu'on rencontre ce nom de fichier
        seen[$filename]="$inode"
        seen["$filename:full_path"]="$file"
    fi
done
```

J'ai ainsi obtenu ce type de réponse :

```
Doublons pour "episode1.mkv" :
1234567 /media/seedbox/sonarr/Serie 1/Season1/episode1.mkv
2345678 /media/tvseries/Serie 1/Season1/episode1.mkv
```

Avec "awk", il se serait arrêté à  `/media/seedbox/sonarr/Serie`. Je ne suis absolument pas un pro, mais Qwen3 a été plus performant et m'a expliqué de A à Z pourquoi et comment faire.

Une fois que j'ai vu que cela fonctionnait bien, j'ai demandé un script qui fait l'intégralité de la cinématique, de la comparaison aux hardlinks en passant par la suppression des doublons.
Encore une fois ChatGPT a été décevant. Malgré mes demandes, il créait d'abord les hardlinks et ensuite il supprimait les doublons. Ce qui.. suprimme aussi le lien (meme si cela conserve l'originale). Idiot.
Petit détour par Qwen3, et ma RTX 5090 en PLS, et paf un résultat bien plus propre. Bon il a gardé les emoji de ChatGPT qui peut pas s'empecher d'en mettre partout, mais voilà :

```bash
#!/bin/bash

echo "🔍 Étape 1 : Indexation des fichiers originaux dans /media/seedbox..."
declare -A seen

# Indexe tous les .mkv dans seedbox
while IFS= read -r -d '' file; do
    filename=$(basename "$file")
    seen["$filename"]="$file"
done < <(find /media/seedbox -type f -name "*.mkv" -print0)

echo "📦 Étape 2 : Remplacement automatique des doublons..."
total_doublons=0
total_ko_economises=0

while IFS= read -r -d '' file; do
    filename=$(basename "$file")
    original="${seen[$filename]}"

    if [[ -n "$original" && "$original" != "$file" ]]; then
        inode_orig=$(stat -c %i "$original")
        inode_dupe=$(stat -c %i "$file")

        if [[ "$inode_orig" != "$inode_dupe" ]]; then
            size_kb=$(du -k "$file" | cut -f1)
            echo "🔁 Remplacement :"
            echo "    Doublon   : $file"
            echo "    Original  : $original"
            echo "    Taille    : ${size_kb} Ko"

            rm "$file" && ln "$original" "$file" && echo "✅ Hardlink créé."

            total_doublons=$((total_doublons + 1))
            total_ko_economises=$((total_ko_economises + size_kb))
        fi
    fi
done < <(find /media/movies /media/tvseries -type f -name "*.mkv" -print0)

echo ""
echo "🧾 Résumé :"
echo "    🔗 Doublons remplacés par hardlink : $total_doublons"
echo "    💾 Espace disque économisé approximatif : ${total_ko_economises} Ko (~$((total_ko_economises / 1024)) Mo)"
echo "✅ Terminé."
```

Bilan j'ai :
- appris pas mal de subtilité bash
- appris qu'il ne faut jamais copier coller un script généré ChatGPT sans le comprendre et sans le tester en dry-run
- appris que Qwen sur une RTX 5090 est plus cohérent que ChatGPT 4o sur des fermes de serveurs (je vous passe les résultats de la version "normale").
- appris que même quand on a 100TB d'espace, monitorer ce dernier m'aurait permis de voir beaucoup plus tot que j'avais 12TB de doublons qui trainent.

## Backup des headers luks pour disques/volumes chiffrés
---

Je me suis rendu compte il y a peu qu'il ne suffisait pas d'avoir le mot de passe pour deverouiller un volume luks apres une panne ou une corruption. J'ai ainsi appris à dump les headers luks des disques/volumes et à utiliser les numéros de série + noms de partitions pour pouvoir bien identifier quel header correspond à quel disque/partition (j'en ai 10 !).

Après avoir bien galéré à la main, j'avoue avoir demandé à Qwen3 (llm hebergé sur ma RTX 5090) de me faire un script qui automatise le listing et identification des disques, dump les headers et les stock dans une archive chiffrée prete à etre backupée sur mon serveur de sauvegarde.

Ainsi, ce script :
* Liste et identifie les disques avec leur numéro de série
* Liste les partition
* Dump les headers dans un dossier dans `/root` (dossier sécurisé)
* Cree une archive temporaire
* Prompt pour saisir un mot de passe
* Chiffre avec le mot de passe
* Détruit l'archive non chiffrée

```
#!/bin/bash

# Directory where LUKS headers will be backed up
DEST="/root/luks-headers-backup"
mkdir -p "$DEST"

echo "🔍 Searching for LUKS containers on all partitions..."

# Loop through all possible disk partitions (including NVMe and SATA)
for part in /dev/sd? /dev/sd?? /dev/nvme?n?p?; do
    # Skip if the device doesn't exist
    if [ ! -b "$part" ]; then
        continue
    fi

    # Check if the partition is a LUKS encrypted volume
    if cryptsetup isLuks "$part"; then
        # Find the parent disk device (e.g. nvme0n1p4 → nvme0n1)
        disk=$(lsblk -no pkname "$part" | head -n 1)
        full_disk="/dev/$disk"

        # Get the serial number of the parent disk
        SERIAL=$(udevadm info --query=all --name="$full_disk" | grep ID_SERIAL= | cut -d= -f2)
        if [ -z "$SERIAL" ]; then
            SERIAL="unknown"
        fi

        # Extract the partition name (e.g. nvme0n1p4)
        PART_NAME=$(basename "$part")

        # Build the output filename with partition name and disk serial
        OUTPUT="$DEST/luks-header-${PART_NAME}__${SERIAL}.img"

        echo "🔐 Backing up LUKS header of $part (Serial: $SERIAL)..."

        # Backup the LUKS header to the output file
        cryptsetup luksHeaderBackup "$part" --header-backup-file "$OUTPUT"
        if [[ $? -eq 0 ]]; then
            echo "✅ Backup successful → $OUTPUT"
        else
            echo "❌ Backup failed for $part"
        fi
    fi
done

# Create a timestamped compressed tar archive of all header backups
ARCHIVE_NAME="/root/luks-headers-$(date +%Y%m%d_%H%M%S).tar.gz"
echo "📦 Creating archive $ARCHIVE_NAME..."
tar -czf "$ARCHIVE_NAME" -C "$DEST" .

# Encrypt the archive symmetrically using GPG with AES256 cipher
echo "🔐 Encrypting the archive with GPG..."
gpg --symmetric --cipher-algo AES256 "$ARCHIVE_NAME"
if [[ $? -eq 0 ]]; then
    echo "✅ Encrypted archive created: ${ARCHIVE_NAME}.gpg"
    # Remove the unencrypted archive for security
    rm -f "$ARCHIVE_NAME"
else
    echo "❌ Encryption failed"
fi
```

Ne pas oublier de backup `/etc/fstab` et `/etc/crypttab` !