Usando a substituição de processos para streaming sem disco
Use a substituição de processos do Bash quando um comando precisa de nomes de arquivo para streams, e um pipeline quando um comando pode ler a saída padrão de outro. Este passo a passo mostra como detectar falhas nos dois casos e, em seguida, fazer upload de um arquivo tar compactado para um receptor HTTP local e comparar os arquivos recebidos.
Use Linux com Bash 5.2.21 ou mais recente, GNU sort, diff, cmp, GNU tar 1.35, gzip 1.12 ou mais recente,
cURL 8.5.0 ou mais recente e Node.js 24.15.0 ou mais recente. Os exemplos também foram testados com
Bash 5.3.15, gzip 1.14, cURL 8.22.0 e Node.js 26.8.1. Execute os scripts de shell salvos com bash, e não com sh;
o receptor usa o suporte nativo a TypeScript do Node.js
e uma extensão .mts explícita. Nenhum pacote precisa ser instalado para o receptor.
Primeiro, cole isto no Bash para criar um pequeno conjunto de dados. Os parênteses mantêm seu
diretório atual inalterado, e && interrompe a configuração se stream-demo já existir ou se a navegação falhar.
(
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
)
Abra dois terminais em stream-demo. Salve ali os scripts a seguir e execute todos os comandos restantes
a partir desse diretório.
Entendendo a substituição de processos
Com <(command), o Bash inicia um produtor de forma assíncrona e passa um nome de arquivo que se refere
à saída dele. No Linux, isso costuma aparecer como /dev/fd/63. Trata-se de um stream, então os
consumidores não podem presumir que conseguem reposicionar a leitura nele como em um arquivo comum. A
forma inversa, >(command), fornece um nome de arquivo no qual você pode escrever como entrada desse
comando. Consulte o
manual de substituição de processos do Bash.
O tentador comando de uma linha diff <(sort file1.txt) <(sort file2.txt) tem uma armadilha: se os dois arquivos
não existirem, ambos os produtores podem falhar enquanto diff compara dois streams vazios e retorna
zero. pipefail não coleta os status desses produtores em segundo plano.
Salve isto como compare.sh. Ele mantém os streams abertos, registra imediatamente o PID de cada
produtor e espera os dois produtores antes de informar o status da comparação:
#!/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"
Execute bash compare.sh input/left.txt input/right.txt: nenhuma saída e status zero significam que os conteúdos
ordenados são iguais. Conteúdos diferentes retornam um; um produtor com falha retorna dois. O script
coleta os status deliberadamente sem set -e, para que um comando com falha não possa pular as chamadas
wait restantes. O Bash documenta a espera por substituições de processos na sua
referência de wait.
Benefícios do streaming sem disco
O upload abaixo evita criar um arquivo tar intermediário no remetente. Ele ainda lê os arquivos de
origem, e o receptor grava o arquivo tar final. Portanto, “sem disco” descreve a transferência
intermediária, não o fluxo de trabalho inteiro. Até mesmo sort
pode despejar entradas grandes em arquivos temporários.
Evitar um arquivo tar preparado previamente economiza o espaço em disco desse arquivo e o ciclo de gravação e leitura. Isso não garante ganho de velocidade: compressão, armazenamento e rede podem, cada um, limitar a vazão. Você também abre mão de uma cópia com leitura reposicionável que o cURL poderia reler após uma falha.
Streaming de dados entre comandos
Para tar e o cURL, um pipe comum é suficiente: tar grava um arquivo tar na stdout e o cURL lê
a stdin. Comece com um receptor que realmente aceite a requisição. Salve isto 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()
})
})
Acrescente o código de inicialização a seguir ao mesmo arquivo. A porta zero pede ao sistema operacional uma porta disponível; um argumento numérico opcional seleciona uma porta específica.
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`)
})
No primeiro terminal, execute node receiver.mts e deixe-o em execução. Copie a URL exibida. O
receptor escuta apenas na interface de loopback, aceita uploads HTTP/1.1 chunked e grava os bytes via
stream em received.tar.gz usando o
pipeline do Node.
A flag wx recusa um arquivo existente. Um upload com falha pode deixar um arquivo parcial, e um
erro de armazenamento encerra a conexão. Este pequeno receptor local não tem autenticação, política
de tamanho de upload nem validação do arquivo tar; mantenha-o privado e faça um upload por vez.
Em seguida, salve esta verificação de argumentos como o início 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
Acrescente o 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
No segundo terminal, execute bash upload.sh input 'COPIED_URL', substituindo COPIED_URL pela
URL completa do receptor. Mantenha o diretório de origem inalterado durante a transferência. O
arquivo tar do receptor fica fora de input, então tar não pode incluir acidentalmente a própria saída.
Aqui, tar -czf - produz um arquivo tar compactado com gzip, então o tipo de conteúdo é
application/gzip.
O --upload-file - do cURL lê a stdin e envia um HTTP PUT.
--disable impede que um .curlrc pessoal altere o comportamento do exemplo. Não há
redirecionamentos nem novas tentativas automáticas. O script exige HTTP 201 deste receptor, além de
um pipeline bem-sucedido: pipefail
torna visível uma falha de tar mesmo quando o cURL envia com sucesso os bytes que recebeu.
Após a mensagem de sucesso, cole isto no segundo terminal para extrair em um novo diretório e comparar o conjunto de dados byte a 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'
)
Isso verifica texto comum, bytes binários que não são texto e um arquivo vazio. Um diretório unpacked
existente interrompe a extração em vez de sobrescrever resultados anteriores. Extraia apenas arquivos
tar em que você confia. Pare o receptor com Ctrl+C ao terminar.
Solução de problemas e boas práticas
Se tar não conseguir ler sua entrada, o script retorna um status diferente de zero mesmo que o
receptor retorne 201. Essa resposta HTTP significa que ele armazenou um corpo de requisição completo;
ela não indica se o produtor falhou nem se o arquivo tar contém os arquivos pretendidos. Trate o
destino como suspeito até verificá-lo.
Uma rejeição HTTP, uma desconexão ou um timeout do cURL também fazem o upload falhar. Uma desconexão após o armazenamento, mas antes de a resposta chegar ao cURL, deixa o resultado incerto: o arquivo pode já estar completo. Se o receptor não conseguir iniciar, verifique o erro dele antes de fazer o upload; uma porta ocupada não prova que seu receptor está em execução.
Não adicione --retry a um processo do cURL que lê de um pipe esperando que ele gere o arquivo tar
novamente. Os bytes consumidos não podem ser rebobinados, e o cURL não reinicia tar. A
documentação de novas tentativas dele também alerta sobre
entrada redirecionada. Para repetir esta demonstração, pare o receptor, inspecione e mova para outro
lugar ou remova o received.tar.gz dele, depois inicie o receptor novamente e execute bash upload.sh outra vez com a
URL recém-exibida. Cada invocação inicia um novo produtor e envia o arquivo tar inteiro. Use um novo
diretório de extração para a verificação; este fluxo de trabalho não tem suporte a retomada.
Opções avançadas de compressão
Mantenha o gzip neste passo a passo para que o produtor, o nome do arquivo, o tipo de conteúdo e o comando de extração sejam coerentes entre si. Trocar de compressor altera os quatro. O ajuste da compressão é uma decisão separada do streaming: meça seus próprios dados antes de escolher um formato diferente.
Aplicabilidade no mundo real
Um destino HTTP real precisa aceitar PUT, o enquadramento de streaming da requisição e o status de sucesso que você espera. Isso não é um upload de formulário multipart nem um comando de extração via SSH. Use HTTPS e a autenticação documentada do destino para transferências remotas; não coloque senhas em uma linha de comando. O script permite HTTP simples apenas para a demonstração em loopback e não segue redirecionamentos.
Se você precisa de bytes que possam ser reenviados, de um tamanho de conteúdo conhecido ou de verificação antes da publicação, preparar um arquivo tar previamente pode ser a melhor escolha. Um receptor de produção também precisa de sua própria política de validação e publicação antes de expor um objeto enviado por upload a outros leitores.
Escolha quando usar streaming
Use a substituição de processos para ferramentas que exigem nomes de arquivo de streams, e um pipeline para um único produtor e consumidor. Em ambos os casos, considere o status de saída de cada produtor antes de declarar a operação bem-sucedida. Para explorar a compressão gerenciada em vez de manter esse caminho de transferência, veja nosso serviço de compressão de arquivos.
