Exploiter la substitution de processus pour un flux sans disque
Utilisez la substitution de processus Bash lorsqu’une commande a besoin de noms de fichiers pour des flux, et un pipeline lorsqu’une commande peut lire la sortie standard d’une autre. Ce tutoriel montre comment détecter les échecs dans les deux cas, puis comment envoyer une archive tar compressée vers un récepteur HTTP local et comparer les fichiers reçus.
Utilisez Linux avec Bash 5.2.21 ou plus récent, GNU sort, diff, cmp, GNU tar 1.35, gzip 1.12 ou plus récent,
cURL 8.5.0 ou plus récent et Node.js 24.15.0 ou plus récent. Les exemples ont aussi été testés avec
Bash 5.3.15, gzip 1.14, cURL 8.22.0 et Node.js 26.8.1. Exécutez les scripts shell enregistrés avec bash, et non sh ;
le récepteur utilise la prise en charge native de TypeScript par Node.js
et une extension .mts explicite. Aucun paquet n’est à installer pour le récepteur.
Commencez par coller ceci dans Bash pour créer un petit jeu de données. Les parenthèses laissent
votre répertoire courant inchangé, et && interrompt la préparation si stream-demo existe déjà ou si le
changement de répertoire échoue.
(
mkdir stream-demo &&
cd stream-demo &&
mkdir input &&
printf 'pear\napple\n' >input/left.txt &&
printf 'apple\npear\n' >input/right.txt &&
printf '\000\377\200binary\n' >input/bytes.bin &&
: >input/empty
)
Ouvrez deux terminaux dans stream-demo. Enregistrez-y les scripts suivants et exécutez toutes les commandes
restantes depuis ce répertoire.
Comprendre la substitution de processus
Avec <(command), Bash lance un producteur de manière asynchrone et transmet un nom de fichier qui désigne
sa sortie. Sous Linux, il ressemble souvent à /dev/fd/63. Il s’agit d’un flux : les consommateurs ne
peuvent donc pas supposer pouvoir s’y positionner librement comme dans un fichier ordinaire. La
forme inverse, >(command), fournit un nom de fichier dans lequel vous pouvez écrire pour alimenter
l’entrée de cette commande. Consultez le
manuel Bash sur la substitution de processus.
La commande d’une ligne tentante diff <(sort file1.txt) <(sort file2.txt) cache un piège : si les deux fichiers sont
absents, les deux producteurs peuvent échouer pendant que diff compare deux flux vides et renvoie
zéro. pipefail ne récupère pas les statuts de ces producteurs en arrière-plan.
Enregistrez ceci sous compare.sh. Il garde les flux ouverts, mémorise immédiatement le PID de chaque
producteur et attend la fin des deux producteurs avant de signaler le statut de la comparaison :
#!/usr/bin/env bash
if (( $# != 2 )); then
printf 'Usage: bash compare.sh FILE1 FILE2\n' >&2
exit 2
fi
exec {left}< <(LC_ALL=C sort -- "$1")
left_pid=$!
exec {right}< <(LC_ALL=C sort -- "$2")
right_pid=$!
diff "/dev/fd/$left" "/dev/fd/$right"
comparison=$?
exec {left}<&- {right}<&-
wait "$left_pid"; left_status=$?
wait "$right_pid"; right_status=$?
if (( left_status != 0 || right_status != 0 )); then
printf 'Cannot compare: a sort failed.\n' >&2
exit 2
fi
exit "$comparison"
Exécutez bash compare.sh input/left.txt input/right.txt : l’absence de sortie et un statut zéro signifient que les contenus
triés correspondent. Si les contenus diffèrent, le statut vaut un ; si un producteur échoue, il
vaut deux. Le script collecte délibérément les statuts sans set -e, afin qu’une commande en échec ne
puisse pas contourner les wait restants. Bash documente l’attente des substitutions de processus
dans sa référence de wait.
Avantages du flux sans disque
L’envoi ci-dessous évite de créer une archive intermédiaire côté émetteur. Il lit tout de même les
fichiers source, et le récepteur écrit l’archive finale. « Sans disque » décrit donc le transfert
intermédiaire, et non l’ensemble de la procédure. Même sort
peut déverser de grosses entrées dans des fichiers temporaires.
Éviter une archive intermédiaire économise l’espace disque de cette archive ainsi que son cycle d’écriture et de lecture. Cela ne garantit pas un gain de vitesse : la compression, le stockage et le réseau peuvent chacun limiter le débit. Vous renoncez aussi à une copie à accès aléatoire que cURL pourrait relire après un échec.
Transmettre des données en flux entre commandes
Pour tar et cURL, un tube ordinaire suffit : tar écrit une archive sur stdout et cURL lit
stdin. Commencez par un récepteur qui accepte réellement la requête. Enregistrez ceci sous receiver.mts :
import { createWriteStream } from 'node:fs'
import { createServer } from 'node:http'
import { pipeline } from 'node:stream'
const server = createServer((request, response) => {
if (request.method !== 'PUT' || request.url !== '/archive') {
request.resume()
response.writeHead(404).end()
return
}
pipeline(request, createWriteStream('received.tar.gz', { flags: 'wx', mode: 0o600 }), (error) => {
if (error) {
console.error('Upload failed; inspect received.tar.gz before retrying.')
response.destroy()
return
}
response.writeHead(201).end()
})
})
Ajoutez le code de démarrage suivant à la fin du même fichier. Le port zéro demande au système d’exploitation un port disponible ; un argument numérique facultatif permet de choisir un port précis.
const port = Number(process.argv[2] ?? 0)
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error('Port must be an integer from 0 to 65535')
}
server.on('error', (error) => {
console.error('Receiver could not listen:', error.message)
process.exitCode = 1
})
server.listen(port, '127.0.0.1', () => {
const address = server.address()
if (!address || typeof address === 'string') throw new Error('Missing listening address')
console.log(`http://127.0.0.1:${address.port}/archive`)
})
Dans le premier terminal, exécutez node receiver.mts et laissez-le tourner. Copiez l’URL affichée. Le
récepteur n’écoute que sur l’interface de bouclage, accepte les envois HTTP/1.1 par blocs (chunked)
et écrit les octets en flux dans received.tar.gz à l’aide de
pipeline de Node.
L’indicateur wx refuse un fichier déjà existant. Un envoi échoué peut laisser un fichier partiel,
et une erreur de stockage ferme la connexion. Ce petit récepteur local n’a ni authentification, ni
politique de taille d’envoi, ni validation d’archive ; gardez-le privé et n’effectuez qu’un envoi
à la fois.
Ensuite, enregistrez cette vérification des arguments comme début de upload.sh :
#!/usr/bin/env bash
if (( $# != 2 )); then
printf 'Usage: bash upload.sh DIRECTORY URL\n' >&2
exit 2
fi
if [[ $2 != https://* && ! $2 =~ ^http://127[.]0[.]0[.]1:[0-9]+/ ]]; then
printf 'Use HTTPS, or loopback HTTP for this demo.\n' >&2
exit 2
fi
Ajoutez le pipeline à la fin de upload.sh :
set -o pipefail
if status=$(tar -czf - -C "$1" . |
curl --disable -fsS --http1.1 --noproxy 127.0.0.1 --globoff \
--connect-timeout 5 --max-time 60 --proto '=http,https' \
--header 'Content-Type: application/gzip' --upload-file - \
--output /dev/null --write-out '%{http_code}' --url "$2"
) && [[ $status == 201 ]]; then
printf 'Archive sent; verify the received files.\n'
else
printf 'Upload failed; inspect the receiver before retrying.\n' >&2
exit 1
fi
Dans le second terminal, exécutez bash upload.sh input 'COPIED_URL', en remplaçant COPIED_URL par l’URL complète du
récepteur. Ne modifiez pas le répertoire source pendant le transfert. L’archive du récepteur se
trouve en dehors de input, donc tar ne peut pas inclure accidentellement sa propre sortie.
Ici, tar -czf - produit une archive compressée avec gzip, donc le type de contenu est
application/gzip.
L’option --upload-file - de cURL lit stdin et envoie une requête HTTP PUT.
--disable empêche un .curlrc personnel de modifier le comportement de l’exemple. Il n’y a ni
redirection ni nouvelle tentative automatique. Le script exige une réponse HTTP 201 de ce récepteur
ainsi qu’un pipeline réussi : pipefail
rend visible l’échec de tar même lorsque cURL envoie correctement les octets qu’il a reçus.
Après le message de réussite, collez ceci dans le second terminal pour extraire l’archive dans un nouveau répertoire et comparer le jeu de données octet par octet :
(
mkdir unpacked &&
tar -xzf received.tar.gz -C unpacked &&
cmp input/left.txt unpacked/left.txt &&
cmp input/right.txt unpacked/right.txt &&
cmp input/bytes.bin unpacked/bytes.bin &&
cmp input/empty unpacked/empty &&
printf 'All four files match.\n'
)
Cela vérifie du texte ordinaire, des octets binaires non textuels et un fichier vide. Un répertoire
unpacked existant interrompt l’extraction au lieu d’écraser les résultats précédents. N’extrayez
que des archives auxquelles vous faites confiance. Arrêtez le récepteur avec Ctrl+C une fois
terminé.
Dépannage et bonnes pratiques
Si tar ne peut pas lire son entrée, le script renvoie un statut non nul même si le récepteur
renvoie 201. Cette réponse HTTP signifie que le récepteur a stocké un corps de requête complet ;
elle ne permet pas de savoir si le producteur a échoué ni si l’archive contient les fichiers prévus.
Considérez la destination comme douteuse tant qu’elle n’a pas été vérifiée.
Un rejet HTTP, une déconnexion ou un délai d’expiration de cURL font aussi échouer l’envoi. Une déconnexion survenue après le stockage mais avant que la réponse n’atteigne cURL laisse le résultat incertain : le fichier peut déjà être complet. Si le récepteur ne démarre pas, consultez son erreur avant d’envoyer quoi que ce soit ; un port occupé ne prouve pas que votre récepteur est en cours d’exécution.
N’ajoutez pas --retry à un processus cURL qui lit un tube en espérant qu’il régénère l’archive.
Les octets consommés ne peuvent pas être rembobinés, et cURL ne relance pas tar. Sa
documentation sur les nouvelles tentatives met aussi en garde contre
les entrées redirigées. Pour réessayer cette démonstration, arrêtez le récepteur, examinez son
received.tar.gz puis déplacez-le ou supprimez-le, redémarrez ensuite le récepteur et relancez bash upload.sh avec la
nouvelle URL affichée. Chaque invocation démarre un nouveau producteur et envoie l’archive entière.
Utilisez un nouveau répertoire d’extraction pour la vérification ; cette procédure ne prend pas en
charge la reprise.
Options de compression avancées
Conservez gzip pour ce tutoriel afin que le producteur, le nom de fichier, le type de contenu et la commande d’extraction concordent. Changer de compresseur modifie ces quatre éléments. Le réglage de la compression est une décision distincte du transfert en flux : mesurez vos propres données avant de choisir un autre format.
Application en conditions réelles
Une véritable destination HTTP doit accepter PUT, le format de transmission en flux de la requête et le statut de réussite que vous attendez. Il ne s’agit ni d’un envoi de formulaire multipart ni d’une commande d’extraction SSH. Pour les transferts distants, utilisez HTTPS et l’authentification documentée par la destination ; ne mettez pas de mots de passe dans une ligne de commande. Le script n’autorise HTTP en clair que pour la démonstration en boucle locale et ne suit pas les redirections.
Si vous avez besoin d’octets rejouables, d’une longueur de contenu connue ou d’une vérification avant publication, préparer une archive intermédiaire peut être le meilleur compromis. Un récepteur de production a aussi besoin de ses propres règles de validation et de publication avant d’exposer un objet envoyé à d’autres lecteurs.
Choisir quand utiliser un flux
Utilisez la substitution de processus pour les outils qui exigent des noms de fichiers de flux, et un pipeline pour un seul producteur et un seul consommateur. Dans les deux cas, tenez compte du statut de sortie de chaque producteur avant de déclarer l’opération réussie. Pour découvrir la compression gérée plutôt que de maintenir ce chemin de transfert, consultez notre service de compression de fichiers.
