Migrate docudjeex to Docus v4 with EN/FR content

This commit is contained in:
Djeex
2026-08-30 16:41:37 +02:00
commit c51fcd5df6
241 changed files with 41143 additions and 0 deletions
@@ -0,0 +1,2 @@
title: Bash
icon: i-lucide-file-terminal
@@ -0,0 +1,140 @@
---
title: Doublons servarr
description: Un script bash pour détecter et corriger les fichiers médias en double dans les bibliothèques Sonarr et Radarr en remplaçant les copies par des hardlinks.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
# 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 :
```sh
.
└── 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 :
```sh
#!/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à :
```sh
#!/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.
@@ -0,0 +1,87 @@
---
title: Luks backup
description: Un script bash pour extraire automatiquement les headers LUKS de tous les disques chiffrés, les identifier par numéro de série et les archiver de façon chiffrée.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
# 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` !
@@ -0,0 +1,95 @@
---
title: Socat Proxy
description: Utiliser socat pour proxifier le socket Docker via Docker Socket Proxy, permettant à Beszel de collecter les stats des conteneurs sans exposer le socket complet.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
# Socat Proxy
---
Ce projet répond à un cas d'usage problématique :
- J'ai [Beszel](https://beszel.dev/), un conteneur de monitoring en mode host, nécessitant d'exposer le socket de Docker afin qu'il récupère les stats des conteneurs
- Afin de ne pas laisser le socket complètement ouvert pour Beszel, j'ai [Docker Socket Proxy](https://github.com/Tecnativa/docker-socket-proxy), un conteneur qui se place entre le scoket de docker et le conteneur qui en a besoin, et qui filtre le requêtes en paramétrant les bonnes permissions pour ne pas tout exposer au conteneur qui l'utilise.
Problème, si __Beszel__ est en mode host, il doit contacter __Docker Socket Proxy__ directement sur un port de l'host, c'est à dire en exposant un port de __Docker Socket Proxy__. Ce qui fait que n'importe quel conteneur/application sur mon host peut l'appeler et utiliser le socket docker.
C'est là qu'intervient [Socat Proxy](https://git.djeex.fr/Djeex/socat-proxy). Ce dernier est un conteneur qui :
- Crée un socket UNIX
- Ecoute ce socket
- Envoie les requetes vers Docker Socket Proxy et vice versa
- Permet de remplacer le vrai socket docker en exposant le socket proxy créé, dans le conteneur final via un bind mount (ici, Beszel)
Ainsi, un filtre comme Docker Socket Proxy dialogue avec Socat Proxy dans leur propre réseau isolé (en mode bridge), et le bind mount du socket UNIX créé est localisé sur l'host dans un dossier avec les permissions nécessaires pour ne pas etre visible des autres conteneurs/applicatifs.
En gros :
![](/img/nonsense/socat-proxy.svg)
Par exemple, pour Beszel cela rendrait comme ceci :
```yaml
services:
socat-proxy:
image: git.djeex.fr/djeex/socat-proxy:latest
container_name: socat-proxy-beszel
environment:
- TARGET_HOST=${TARGET_HOST}
- TARGET_PORT=${TARGET_PORT}
- UNIX_SOCKET_PATH=${UNIX_SOCKET_PATH}
- HOST_SOCKET_PATH=${HOST_SOCKET_PATH}
- UNIX_SOCKET_NAME=${UNIX_SOCKET_NAME}
volumes:
- ${HOST_SOCKET_PATH}:${UNIX_SOCKET_PATH}
restart: unless-stopped
depends_on:
- ${TARGET_HOST}
socket-proxy:
image: lscr.io/linuxserver/socket-proxy:latest
container_name: ${TARGET_HOST}
security_opt:
- no-new-privileges:true
environment:
- CONTAINERS=1
- INFO=1
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
read_only: true
tmpfs:
- /run
beszel-agent:
image: henrygd/beszel-agent:latest
container_name: beszel-agent
restart: unless-stopped
network_mode: host
security_opt:
- no-new-privileges:true
volumes:
- ${HOST_SOCKET_PATH}/${UNIX_SOCKET_NAME}:/var/run/docker.sock:ro
environment:
- #... your Beszel environment var
depends_on:
- socat-proxy
```
Plus d'infos directement sur le repo :
::card{title="🐋 __Socat Proxy__"}
[A lighteweight bind mount socket proxy](https://git.djeex.fr/Djeex/socat-proxy)
::
+43
View File
@@ -0,0 +1,43 @@
---
title: HotDisk
description: Un script bash qui surveille la température des disques durs et éteint automatiquement le serveur lorsqu'ils dépassent un seuil de sécurité trop longtemps.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
# HotDisk
---
Quand on a un NAS avec plusieurs disques dans une buanderie, les températures peuvent vite grimper. Or, un disque dur est très sensible aux températures et peut subir de gros dommages s'il dépasse une température seuil pendant un certain temps. Après un été très chaud qui a fournit son lot de sueur froide en regardant la température de mes disques, j'ai cherché un moyen de pouvoir automatiser l'extinction du serveur en cas de dépassement prolonger de la température maximale supportée par mes disques.
N'ayant rien trouvé de convaincant, je l'ai donc fait moi-même.
- Le script lit la température SMART de tous les disques SATA chaque minute.
- Il compte le nombre de minutes consécutives où la température est au-dessus ou en dessous du seuil.
- Il envoie des notifications Discord si le seuil est dépassé ou si la température redescend.
- Il déclenche larrêt du système si la température dépasse la limite pendant la durée configurée.
- Il enregistre toutes les températures et l’état des compteurs, et effectue automatiquement la rotation des journaux.
Puis tant qu'on y est, j'ai ajouté un script d'installation qui installe le script, le rend éxecutable, crée un service systemd et un timer systemd et l'active. Le script d'installation permet aussi de régler les différentes variables:
| Variable | Description | Valeur par défaut |
|-----------------------|------------------------------------------------------------------------------|-----------------------------------------------|
| `MAX_TEMP` | Température maximale autorisée (°C) avant le début du compte à rebours darrêt | `60` |
| `HOT_DURATION` | Minutes consécutives au-dessus de `MAX_TEMP` avant larrêt du système | `5` |
| `COOL_RESET_DURATION` | Minutes consécutives en dessous de `MAX_TEMP` pour réinitialiser les compteurs | `5` |
| `LOG_FILE` | Chemin du fichier journal principal | `/var/log/hdd_temp_monitor.log` |
| `LOG_ROTATE_COUNT` | Nombre de fichiers journaux à conserver | `7` |
| `LOG_ROTATE_PERIOD` | Période de rotation des journaux (`daily` ou `weekly`) | `daily` |
| `DISCORD_WEBHOOK` | URL du webhook Discord pour les notifications | _Obligatoire_ |
Il execute aussi un autre script qui paramètre le logrotate avec les éléments configurés prédédemment. Et enfin, le script d'installation peut etre executé via un simple curl + execution d'un dernier script pour les plus flemmard.
Il a fallu également gérer le sujet du root sans sudo, du sudo seul, de l'utilisateur sans sudo, les divers cas d'erreur (dépendances manquantes, erreur dans les permissions, créations de fichier, de lecture des données des disques, etc...)
Et l'acces concurrent au fichier de statuts.
Plus d'infos directement sur le repo :
::card{title="📜 __HotDisk__"}
[Gardez vos disques au frais !](https://git.djeex.fr/Djeex/hotdisk)
::
@@ -0,0 +1,118 @@
---
title: Backrest Docker Stop
description: Un script bash qui arrête les conteneurs Docker avant une sauvegarde Backrest et les redémarre après — pour des sauvegardes de bases de données sans dump complexe.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
# Backrest Docker Stop
---
[Backrest](https://github.com/garethgeorge/backrest) est un formidable outil de backup. Dans le cas de [Serveex](https://docu.djeex.fr/fr/serveex/introduction), la majeure partie des données à sauvegarder sont des conteneurs, et souvent ces conteneurs possèdent des bases de données. Le problème ? On ne peut pas sauvegarder proprement une BDD qui est en route. Alors, il existe plein de solutions complexe à base de dump des bases de données, mais souvent le plus simple cela reste de stopper les conteneurs, de sauvegarder, et de redémarrer les conteneurs.
**Backrest** ne propose pas de solutions native, mais il propose d'executer des scripts customisés à déclencher sur des évenements, comme le démarrage et la fin de la sauvegarde par exemple. Notre besoin est donc de stopper les conteneurs dont on veut sauvegarder la BDD, à chaque démarrage du plan de sauvegarde, et de les redémarrer à la fin de l'execution du plan de sauvegarde.
Pour cela nous allons avoir besoin d'un script bash et de connecter Backrest au socker de Docker, afin d'avoir la cinématique suivante :
- Le plan de sauvegarde se met en route
- L'evenement déclenche l'execution d'un script custom
- Le script contacte docker et demande la liste des conteneurs qui comportent le label `backrest.backup.stop=true`
- Il récupère cette liste et leur envoie une commande d'extinction
- Le plan de sauvegarde s'arrête
- L'evenement déclenche l'execution d'un script custom
- Le script contacte docker et demande la liste des conteneurs qui comportent le label `backrest.backup.stop=true`
- Il récupère cette liste et leur envoie une commande de démarrage
## Faire communiquer Backrest et Docker en toute sécurité
Pour faire communiquer **Backrest** et Docker en toute sécurité, nous utiliserons [Docker Socket Proxy](https://github.com/linuxserver/docker-socket-proxy), afin de n'accorder que les droits nécessaires plutot que d'exposer l'intégralité du socket à Docker. Voici donc la stack :
```yaml
---
services:
backrest:
image: garethgeorge/backrest:latest
container_name: backrest
hostname: backrest
security_opt:
- no-new-privileges:true
volumes:
- ... # vos volumes
environment:
- ... # vos variables
- DOCKER_HOST=tcp://socket-proxy-backrest:2375
restart: unless-stopped
ports:
- ... # vos ports
depends_on:
- socket-proxy
socket-proxy:
image: lscr.io/linuxserver/socket-proxy:latest
container_name: socket-proxy-backrest
security_opt:
- no-new-privileges:true
environment:
- CONTAINERS=1
- ALLOW_START=1
- ALLOW_STOP=1
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
read_only: true
tmpfs:
- /run
```
Et voilà, Backrest pourra ainsi communiquer avec Docker en toute sécurité.
## Les scripts
Vous trouverez ci-dessous les scripts à renseigner en action pour les évèvenements de démarrage et d'arrêt de la sauvegarde dans **Backrest**.
::code-group
```sh [Stop]
#!/usr/bin/env bash
BACKUP_LABEL="backrest.backup.stop=true"
BACKUP_CONTAINERS=$(docker ps -aqf "label=$BACKUP_LABEL")
for BC in $BACKUP_CONTAINERS
do
docker stop "$BC"
done
sleep 10
```
```sh [Start]
#!/usr/bin/env bash
BACKUP_LABEL="backrest.backup.stop=true"
BACKUP_CONTAINERS=$(docker ps -aqf "label=$BACKUP_LABEL")
for BC in $BACKUP_CONTAINERS
do
docker start "$BC"
done
sleep 10
```
::
## Le label
Une fois les scripts renseignés et paramétrés pour les bons hooks dans **Backrest**, vous n'avez plus qu'à ajouter le libellé `backrest.backup.stop=true` dans les fichiers `compose.yaml` des conteneurs à éteindre et rallumer durant les sauvegardes :
```yaml
services:
votre_service:
...
labels:
- com.centurylinklabs.watchtower.enable=true
```
Et voilà, à la prochaine sauvegarde, les conteneurs correctement labelisés s'arreteront pendant la sauvegarde et redémarreront tout seuls à la fin.