Sustitución de procesos para transmitir sin disco
Usa la sustitución de procesos de Bash cuando un comando necesite nombres de archivo para flujos, y un pipeline cuando un comando pueda leer la salida estándar de otro. Este tutorial muestra cómo detectar fallos en ambos casos, subir un archivo tar comprimido a un receptor HTTP local y comparar los archivos recibidos.
Usa Linux con Bash 5.2.21 o posterior, GNU sort, diff, cmp, GNU tar 1.35, gzip 1.12 o posterior,
cURL 8.5.0 o posterior y Node.js 24.15.0 o posterior. Los ejemplos también se probaron con Bash 5.3.15,
gzip 1.14, cURL 8.22.0 y Node.js 26.8.1. Ejecuta los scripts de shell guardados con
bash, no con sh;
el receptor usa el soporte nativo de TypeScript de Node.js
y una extensión .mts explícita. No es necesario instalar paquetes para el receptor.
Primero, pega esto en Bash para crear un conjunto de datos pequeño. Los paréntesis mantienen tu
directorio actual sin cambios, y && detiene la preparación si
stream-demo ya existe o si falla el cambio de directorio.
(
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
)
Abre dos terminales en stream-demo. Guarda allí los siguientes scripts y ejecuta
todos los comandos restantes desde ese directorio.
Comprende la sustitución de procesos
Con <(command), Bash inicia un productor de forma asíncrona y pasa un nombre de
archivo que hace referencia a su salida. En Linux suele tener un aspecto como
/dev/fd/63. Es un flujo, por lo que los consumidores no pueden dar por hecho que
pueden desplazarse por él como por un archivo normal. La forma inversa,
>(command), proporciona un nombre de archivo en el que puedes escribir para
suministrar la entrada de ese comando. Consulta el
manual de sustitución de procesos de Bash.
El tentador comando de una línea diff <(sort file1.txt) <(sort file2.txt) tiene una trampa: si faltan ambos
archivos, ambos productores pueden fallar mientras diff compara dos flujos
vacíos y devuelve cero. pipefail no recoge los estados de esos productores en
segundo plano.
Guarda esto como compare.sh. Mantiene los flujos abiertos, registra inmediatamente
el PID de cada productor y espera a ambos productores antes de informar del estado de la comparación:
#!/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"
Ejecuta bash compare.sh input/left.txt input/right.txt: la ausencia de salida y el estado cero indican que los
contenidos ordenados coinciden. Si los contenidos difieren, devuelve uno; si falla un productor,
devuelve dos. El script recoge los estados deliberadamente sin set -e, para
que el fallo de un comando no omita el wait restante. Bash documenta la
espera de sustituciones de procesos en su
referencia de wait.
Ventajas de la transmisión sin disco
La subida que se muestra a continuación evita crear un archivo tar intermedio en el emisor. Aun así,
lee los archivos de origen, y el receptor escribe el archivo tar final. Por tanto, «sin disco»
describe la transferencia intermedia, no todo el flujo de trabajo. Incluso
sort
puede volcar entradas grandes a archivos temporales.
Evitar un archivo tar intermedio ahorra el espacio que ocuparía en disco y su ciclo de escritura y lectura. No garantiza mayor velocidad: la compresión, el almacenamiento y la red pueden limitar el rendimiento. También renuncias a una copia con acceso a posiciones arbitrarias que cURL podría volver a leer tras un fallo.
Transmite datos entre comandos
Para tar y cURL, basta con una tubería normal:
tar escribe un archivo tar en stdout y cURL lee stdin. Empieza con un
receptor que realmente acepte la solicitud. Guarda esto como 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()
})
})
Añade el siguiente código de inicio al mismo archivo. El puerto cero solicita un puerto disponible al sistema operativo; un argumento numérico opcional selecciona un puerto específico.
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`)
})
En la primera terminal, ejecuta node receiver.mts y déjalo en ejecución. Copia la URL que
imprime. El receptor se vincula solo a la interfaz de bucle local, acepta subidas fragmentadas de
HTTP/1.1 y transmite los bytes a received.tar.gz mediante
pipeline de Node.
La opción wx rechaza un archivo existente. Una subida fallida puede dejar
un archivo parcial, y un error de almacenamiento cierra la conexión. Este pequeño receptor local no
tiene autenticación, política de tamaño de subida ni validación de archivos tar; mantenlo privado y
realiza una sola subida a la vez.
A continuación, guarda esta comprobación de argumentos al principio 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
Añade el pipeline a 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
En la segunda terminal, ejecuta bash upload.sh input 'COPIED_URL' y reemplaza
COPIED_URL por la URL completa del receptor. Mantén el directorio de origen sin
cambios durante la transferencia. El archivo tar del receptor se encuentra fuera de
input, por lo que tar no puede incluir su propia
salida por accidente.
Aquí, tar -czf - produce un archivo tar comprimido con gzip, por lo que el tipo de
contenido es application/gzip.
La opción --upload-file - de cURL lee stdin y envía una
solicitud HTTP PUT. --disable impide que un
.curlrc personal cambie el comportamiento del ejemplo. No hay redirecciones ni
reintentos automáticos. El script exige que este receptor devuelva HTTP 201 y que el pipeline se
complete correctamente: pipefail
hace visible un fallo de tar incluso cuando cURL envía correctamente los
bytes que recibió.
Después del mensaje de éxito, pega esto en la segunda terminal para extraer el contenido en un directorio nuevo y comparar el conjunto de datos byte por byte:
(
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'
)
Esto comprueba texto normal, bytes binarios no textuales y un archivo vacío. Si el directorio
unpacked ya existe, la extracción se detiene en lugar de sobrescribir los
resultados anteriores. Extrae únicamente archivos tar en los que confíes.
Detén el receptor con Ctrl+C cuando termines.
Solución de problemas y buenas prácticas
Si tar no puede leer su entrada, el script devuelve un valor distinto de
cero aunque el receptor devuelva 201. Esa respuesta HTTP significa que almacenó un cuerpo de
solicitud completo; no puede determinar si el productor falló o si el archivo tar contiene los
archivos previstos. Considera el destino sospechoso hasta verificarlo.
Un rechazo HTTP, una desconexión o un tiempo de espera agotado de cURL también provocan el fallo de la subida. Una desconexión después del almacenamiento, pero antes de que la respuesta llegue a cURL, deja el resultado incierto: el archivo podría estar ya completo. Si el receptor no puede iniciarse, revisa su error antes de subir datos; un puerto ocupado no demuestra que tu receptor esté en ejecución.
No añadas --retry a un proceso de cURL que lea de una tubería esperando que
regenere el archivo tar. No se puede retroceder para volver a leer los bytes consumidos, y cURL no
reinicia tar. Su
documentación sobre reintentos también advierte sobre la entrada
redirigida. Para reintentar este ejemplo, detén el receptor, inspecciona su
received.tar.gz y muévelo a otro lugar o elimínalo; luego inicia de nuevo el receptor y
vuelve a ejecutar bash upload.sh con la URL recién impresa. Cada invocación inicia un
productor nuevo y envía el archivo tar completo. Usa un directorio de extracción nuevo para la
verificación; este flujo de trabajo no permite reanudar la transferencia.
Opciones avanzadas de compresión
Mantén gzip en este tutorial para que el productor, el nombre de archivo, el tipo de contenido y el comando de extracción coincidan. Cambiar de compresor modifica los cuatro. Ajustar la compresión es una decisión independiente de la transmisión: realiza mediciones con tus propios datos antes de elegir otro formato.
Aplicación en entornos reales
Un destino HTTP real debe aceptar PUT, el enmarcado de transmisión de la solicitud y el estado de éxito que esperas. Esto no es una subida de formulario multiparte ni un comando de extracción por SSH. Usa HTTPS y la autenticación documentada del destino para las transferencias remotas; no pongas contraseñas en una línea de comandos. El script permite HTTP sin cifrar solo para la demostración en bucle local y no sigue redirecciones.
Si necesitas bytes que puedan reenviarse, una longitud de contenido conocida o verificación antes de la publicación, crear un archivo tar intermedio puede ser la mejor opción. Un receptor de producción también necesita su propia política de validación y publicación antes de exponer un objeto subido a otros lectores.
Elige cuándo transmitir
Usa la sustitución de procesos para herramientas que requieran nombres de archivo para flujos, y un pipeline para un único productor y consumidor. En ambos casos, ten en cuenta el estado de salida de cada productor antes de considerar que la operación se completó correctamente. Para explorar la compresión gestionada en lugar de mantener este mecanismo de transferencia, consulta nuestro servicio de compresión de archivos.
