Surveiller des fichiers avec inotifywait

Menu principal

À travers un exemple concret, l’article décrit comment surveiller des fichiers avec l’utilitaire inotifywait sous Linux avec Bash

Installation

inotifywait est un utilitaire en ligne de commande qui se trouve dans le paquet inotify-tools sur Debian et Ubuntu.

Pour l’installer, il suffit de faire :

shell
sudo apt install inotify-tools

Il est également utile d’avoir l’utilitaire lsof :

shell
sudo apt install lsof

Code source

Voici le code source, les explications viennent après.

shell
#!/bin/bash
function step {
	local message="$1"
	local date=$(date "+%Y-%m-%d %T%z")
	shift

	printf '[%s] %s: %s\n' "$date" "$message" "$*"
	"$@"
}

has_extension() {
	local file_extension="${1##*.}"
	shift

	for extension in $*
	do
		[ "$file_extension" == "$extension" ] && return 0
	done

	return 1
}

clean_files() {
	local filename="$1"

	[ -f "$filename.gz" ] && step "REMOVE" rm "$filename.gz"
	[ -f "$filename.br" ] && step "REMOVE" rm "$filename.br"
	[ -f "$filename.webp" ] && step "REMOVE" rm "$filename.webp"
}

jpg_precompress() {
	local filename="$1"

	test "${filename}" -ot "${filename}.webp" && return

	step "JPEG-WEBP" \
	cwebp -quiet \
	      -pass 10 \
	      -af \
	      -sharp_yuv \
	      "${filename}" \
	      -o "${filename}.webp"
}

png_precompress() {
	local filename="$1"

	test "${filename}" -ot "${filename}.webp" && return

	step "PNG-WEBP" \
	cwebp -quiet \
	      -near_lossless 50 \
	      -q 100 \
	      -alpha_q 50 \
	      -mt \
	      "${filename}" \
	      -o "${filename}.webp"
}

text_precompress() {
	local filename="$1"

	test "${filename}" -ot "${filename}.gz" && return

	step "GZIP" zopfli --i127 "$filename"

	step "BROTLI" brotli --best --force --no-copy-stat "$filename"
	step "BROTLI" chmod 644 "$filename.br"
}

cd /var/www/html

inotifywait \
	--quiet \
	--monitor \
	--recursive \
	--event modify,create,delete \
	--exclude '\.(gz|br|webp)$' \
	--format '%w%f' \
	. \
| while read filename
do
	has_extension "$filename" html css js svg xml json xsl xsd txt ps png jpg ico \
	|| continue

	(test -f "$filename" && lsof -- "$filename") && continue

	if [ ! -f "$filename" ]
	then
		clean_files "$filename"
	elif has_extension "$filename" html css js svg xml json xsl xsd txt ps ico
	then
		text_precompress "$filename"
	elif has_extension "$filename" jpeg jpg
	then
		jpg_precompress "$filename"
	elif has_extension "$filename" png
	then
		png_precompress "$filename"
	fi
done

Explications

Fonction step

shell
function step {
	local message="$1"
	local date=$(date "+%Y-%m-%d %T%z")
	shift

	printf '[%s] %s: %s\n' "$date" "$message" "$*"
	"$@"
}

La fonction step permet de garder une trace des actions effectuées par le script.

Elle affiche ces informations sous la forme suivante :

shell
[2020-03-24 21:31:29+0000] GZIP: zopfli --i127 ./shell/surveiller-fichiers-inotifywait/index.xml
[2020-03-24 21:31:30+0000] BROTLI: brotli --best --force --no-copy-stat ./shell/surveiller-fichiers-inotifywait/index.xml
[2020-03-24 21:31:30+0000] BROTLI: chmod 644 ./shell/surveiller-fichiers-inotifywait/index.xml.br

D’abord la date et l’heure, suivies du nom de l’action pour enfin terminer par la commande exécutée.

Notez la subtile différence entre $* et $@ ! "$*" permet de placer tous les paramètres existants dans un seul paramètre. A contrario "$@" est un cas particulier puisqu’il reproduit tous les paramètres à leur exact position.

Fonction has_extension

shell
has_extension() {
	local file_extension="${1##*.}"
	shift

	for extension in $*
	do
		[ "$file_extension" == "$extension" ] && return 0
	done

	return 1
}

La fonction has_extension permet de tester si le nom de fichier passé en premier paramètre se termine par l’une des extensions qui suivent.

Fonction clean_files

shell
clean_files() {
	local filename="$1"

	[ -f "$filename.gz" ] && step "REMOVE" rm "$filename.gz"
	[ -f "$filename.br" ] && step "REMOVE" rm "$filename.br"
	[ -f "$filename.webp" ] && step "REMOVE" rm "$filename.webp"
}

Cette fonction sert simplement à supprimer des fichiers générés par le script au cas ou le fichier d’origine aurait été supprimé.

Fonction jpg_precompress

shell
jpg_precompress() {
	local filename="$1"

	test "${filename}" -ot "${filename}.webp" && return

	step "JPEG-WEBP" \
	cwebp -quiet \
	      -pass 10 \
	      -af \
	      -sharp_yuv \
	      "${filename}" \
	      -o "${filename}.webp"
}

La fonction jpg_precompress se charge de convertir un jpeg en webp.

Elle commence tout d’abord par vérifier s’il est nécessaire de convertir le jpeg en comparant sa date de dernière modification avec celle d’un éventuel webp correspondant. Le test est nécessaire car une opération sur un fichier peut générer plusieurs types d’événements. Il faut éviter de faire plusieurs fois le travail car il s’agit d’un traitement lourd.

Les paramètres sont poussés à l’extrême afin de profiter du meilleur niveau de qualité et de la meilleure compression (l’opération n’est effectuée qu’une seule fois).

La fonction utilise les paramètres de compression destructive de webp.

Fonction png_precompress

shell
png_precompress() {
	local filename="$1"

	test "${filename}" -ot "${filename}.webp" && return

	step "PNG-WEBP" \
	cwebp -quiet \
	      -near_lossless 50 \
	      -q 100 \
	      -alpha_q 50 \
	      -mt \
	      "${filename}" \
	      -o "${filename}.webp"
}

Ce qui a été dit pour jpg_precompress s’applique également à png_precompress.

La différence réside dans les paramètres qui tentent de faire une compression sans perte (ou à la rigueur avec des pertes non visibles).

Fonction text_precompress

shell
text_precompress() {
	local filename="$1"

	test "${filename}" -ot "${filename}.gz" && return

	step "GZIP" zopfli --i127 "$filename"

	step "BROTLI" brotli --best --force --no-copy-stat "$filename"
	step "BROTLI" chmod 644 "$filename.br"
}

La fonction text_precompress s’occupe de précompresser les fichiers texte (HTML, XML, JS, CSS etc.) selon 2 formats.

Pour générer le format gzip (le plus largement supporté par les navigateurs), elle utilise l’utilitaire zopfli. Ce dernier permet des taux de compression plus élevés que gzip tout en générant des fichiers parfaitement compatibles avec lui. L’opération prend en revanche beaucoup plus de temps qu’avec gzip mais cette opération étant faite une fois pour toute, ce n’est pas gênant.

Pour générer le format brotli (supporté par Chrome et Firefox mais uniquement sur des connexions https) offre des taux de compression plus élevés qu’avec zopfli. On l’utilise également avec les paramètres les plus poussés.

Inotifywait en mode monitor

shell
inotifywait \
	--quiet \
	--monitor \
	--recursive \
	--event modify,create,delete \
	--exclude '\.(gz|br|webp)$' \
	--format '%w%f' \
	. \
| while read filename
do
	# ...
done

Le script utilise inotifywait en mode monitor. inotifywait va alors surveiller en permanence les changements survenant dans l’arborescence et les indiquer sur la sortie standard.

Explication des paramètres :

--quiet
Mode silencieux, inotifywait n’affiche aucun message sur son activité
--monitor
Mode monitor
--recursive
Surveille toute une arboresence de répertoires
--event modify,create,delete
Écoute les événement de modification, création et suppression de fichiers
--exclude '\.(gz|br|webp)$'
Ignore les modifications des fichier gzip, brotli et webp pour éviter de générer des événements sur nos propres traitements
--format '%w%f'
Pour chaque événement détecté, affiche le chemin complet vers le fichier

La sortie d’inotifywait est redirigée dans une boucle while read. C’est cette boucle qui va effectuer les traitements pour chaque modification ou suppression d’un fichier.

Boucle principale

shell
	has_extension "$filename" html css js svg xml json xsl xsd txt ps png jpg ico \
	|| continue

La boucle principale commence par vérifier que l’extension du fichier qui a généré l’événement fait bien partie de celles sur lesquelles nous opérons des traitements.

shell
(test -f "$filename" && lsof -- "$filename") && continue

Elle vérifie ensuite, si le fichier existe, qu’il n’est plus ouvert par une autre application. Si tel était le cas, cela voudrait dire que l’application opération des modifications sur le fichier n’a pas encore terminé son travail et qu’il n’est pas encore opportun de traiter ce fichier.

shell
	if [ ! -f "$filename" ]
	then
		clean_files "$filename"

Quand un fichier est supprimé, inotifywait génère un événement. Le script reçoit donc le nom d’un fichier qui n’existe plus. Dans ce cas-là, il faut faire le ménage et retirer les éventuels fichiers gzip, brotli et webp qui ont été créés pour ce fichier.

shell
	elif has_extension "$filename" html css js svg xml json xsl xsd txt ps ico
	then
		text_precompress "$filename"

Teste si le fichier porte l’extension d’un type de fichier texte (ou compressible par gzip et brotli) et effectue le traitement approprié.

Note : oui, les fichiers ico sont compressibles.

shell
	elif has_extension "$filename" jpg
	then
		jpg_precompress "$filename"

Effectue le traitement approprié s’il s’agit d’un jpeg.

shell
	elif has_extension "$filename" png
	then
		png_precompress "$filename"
	fi

Effectue le traitement approprié s’il s’agit d’un png.