Como excluir um arquivo ao mesmo tempo em que é adicionado a um arquivo morto?
Dado o contexto, interpretarei esta pergunta como:
Como remover dados do disco imediatamente após a leitura, antes da leitura do arquivo completo, para que haja espaço suficiente para o arquivo transformado.
A transformação pode ser qualquer coisa que você queira fazer com os dados: compactar, criptografar etc.
A resposta é esta:
<$file gzip | dd bs=$buffer iflag=fullblock of=$file conv=notrunc
Resumindo: leia dados, jogue-os no gzip (ou o que você quiser fazer com eles), armazene em buffer a saída, para que possamos ler mais do que escrevemos e escrevê-los novamente no arquivo. Esta é uma versão mais bonita e mostra a saída durante a execução:
cat "$file" \
| pv -cN 'bytes read from file' \
| gzip \
| pv -cN 'bytes received from compressor' \
| dd bs=$buffer iflag=fullblock 2>/dev/null \
| pv -cN 'bytes written back to file' \
| dd of="$file" conv=notrunc 2>/dev/null
Vou passar por isso, linha por linha:
cat "$file"
lê o arquivo que você deseja compactar. É um uso inútil de gato (UUOC), já que a próxima parte, pv, também pode ler o arquivo, mas acho isso mais bonito.
Ele o canaliza para pv
mostrar informações de progresso ( -cN
diz 'use algum tipo de [c] ursor' e dê a [N] ame).
Aqueles tubos nos gzip
quais obviamente faz a compactação (leitura de stdin, saída para stdout).
Isso entra em outro pv
(exibição de canal).
Isso entra dd bs=$buffer iflag=fullblock
. A $buffer
variável é um número, algo como 50 megabytes. É a quantidade de RAM que você deseja dedicar ao manuseio seguro do seu arquivo (como ponto de dados, o buffer de 50 MB para um arquivo de 2 GB foi bom). O iflag=fullblock
instrui dd
a ler até $buffer
bytes antes de transmiti-lo. No início, o gzip escreverá um cabeçalho, portanto a saída do gzip será inserida nessa dd
linha. Em seguida dd
, espere até que haja dados suficientes antes de transmiti-los, para que a entrada possa ler mais. Além disso, se você tiver peças descompactáveis, o arquivo de saída poderá ser maior que o arquivo de entrada. Esse buffer garante que, até $buffer
bytes, isso não seja um problema.
Em seguida, entramos em outra linha de visualização de tubos e, finalmente, em nossa dd
linha de saída . Esta linha tem of
(arquivo de saída) e conv=notrunc
especificada, onde notrunc
indica dd
para não truncar (excluir) o arquivo de saída antes de gravar. Portanto, se você possui 500 bytes A
e escreve 3 bytes B
, o arquivo será BBBAAAAA...
(em vez de substituído por BBB
).
Não cobri as 2>/dev/null
peças e elas são desnecessárias. Eles apenas arrumam um pouco a saída suprimindo dd
a mensagem "Eu terminei e escrevi tantos bytes". As barras invertidas no final de cada linha ( \
) fazem com que o bash trate a coisa toda como um grande comando que se encaixa.
Aqui está um script completo para facilitar o uso. Curiosamente, eu o coloquei em uma pasta chamada 'gz-in-place'. Percebi então o acrônimo que fiz: GZIP: gnu zip no local. Então, apresento GZIP.sh:
#!/usr/bin/env bash
### Settings
# Buffer is how many bytes to buffer before writing back to the original file.
# It is meant to prevent the gzip header from overwriting data, and in case
# there are parts that are uncompressible where the compressor might exceed
# the original filesize. In these cases, the buffer will help prevent damage.
buffer=$((1024*1024*50)) # 50 MiB
# You will need something that can work in stream mode from stdin to stdout.
compressor="gzip"
# For gzip, you might want to pass -9 for better compression. The default is
# (typically?) 6.
compressorargs=""
### End of settings
# FYI I'm aware of the UUOC but it's prettier this way
if [ $# -ne 1 ] || [ "x$1" == "x-h" ] || [ "x$1" == "x--help" ]; then
cat << EOF
Usage: $0 filename
Where 'filename' is the file to compress in-place.
NO GUARANTEES ARE GIVEN THAT THIS WILL WORK!
Only operate on data that you have backups of.
(But you always back up important data anyway, right?)
See the source for more settings, such as buffer size (more is safer) and
compression level.
The only non-standard dependency is pv, though you could take it out
with no adverse effects, other than having no info about progress.
EOF
exit 1;
fi;
b=$(($buffer/1024/1024));
echo "Progressing '$1' with ${b}MiB buffer...";
echo "Note: I have no means of detecting this, but if you see the 'bytes read from";
echo "file' exceed 'bytes written back to file', your file is now garbage.";
echo "";
cat "$1" \
| pv -cN 'bytes read from file' \
| $compressor $compressorargs \
| pv -cN 'bytes received from compressor' \
| dd bs=$buffer iflag=fullblock 2>/dev/null \
| pv -cN 'bytes written back to file' \
| dd of="$1" conv=notrunc 2>/dev/null
echo "Done!";
Eu sinto vontade de adicionar outra linha de buffer antes do gzip, para impedir que ele seja dd
gravado muito longe quando a linha de buffer flui, mas com apenas 50MiB de buffer e 1900MB de /dev/urandom
dados, parece que já funciona de qualquer maneira (o md5sums correspondia após a descompactação). Relação boa o suficiente para mim.
Outra melhoria seria a detecção de escrita muito longe, mas não vejo como fazer isso sem remover a beleza da coisa e criar muita complexidade. Nesse ponto, você também pode torná-lo um programa python completo que faz tudo corretamente (com falhas de segurança para impedir a destruição de dados).