¿Cómo eliminar un archivo al mismo tiempo que se agrega a un archivo?
Dado el contexto, interpretaré esta pregunta como:
Cómo eliminar datos del disco inmediatamente después de leerlo, antes de que se haya leído el archivo completo, de modo que haya suficiente espacio para el archivo transformado.
La transformación puede ser cualquier cosa que desee hacer con los datos: compresión, cifrado, etc.
La respuesta es esta:
<$file gzip | dd bs=$buffer iflag=fullblock of=$file conv=notrunc
En resumen: lea los datos, tírelos a gzip (o lo que quiera hacer con ellos), guarde el resultado en el búfer para asegurarnos de leer más de lo que escribimos y volver a escribirlo en el archivo. Esta es una versión más bonita y muestra resultados mientras se ejecuta:
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
Lo revisaré, línea por línea:
cat "$file"
lee el archivo que quieres comprimir. Es un uso inútil de cat (UUOC) ya que la siguiente parte, pv, también puede leer el archivo, pero creo que es más bonito.
Lo canaliza en el pv
que muestra información de progreso ( -cN
le dice 'usa algún tipo de [c] ursor' y dale un [N] ame).
Esa tubería en la gzip
que obviamente hace la compresión (lectura de stdin, salida a stdout).
Eso se canaliza a otro pv
(vista de tubería).
Eso entra dd bs=$buffer iflag=fullblock
. La $buffer
variable es un número, algo así como 50 megabytes. Sin embargo, es la cantidad de RAM que desea dedicar al manejo seguro de su archivo (como punto de datos, el búfer de 50 MB para un archivo de 2 GB estaba bien). Le iflag=fullblock
dice dd
que lea hasta $buffer
bytes antes de pasarlo. Al principio, gzip escribirá un encabezado, por lo que la salida de gzip aterrizará en esta dd
línea. Luego dd
esperará hasta que tenga suficientes datos antes de pasarlo, y así la entrada puede leer más. Además, si tiene partes no comprimibles, el archivo de salida puede ser más grande que el archivo de entrada. Este búfer se asegura de que, hasta $buffer
bytes, esto no sea un problema.
Luego vamos a otra línea de vista de tubería y finalmente a nuestra dd
línea de salida . Esta línea tiene of
(archivo de salida) y conv=notrunc
especificada, donde notrunc
le dice a dd
no truncar (eliminar) el archivo de salida antes de escribir. Entonces, si tiene 500 bytes A
y escribe 3 bytes B
, el archivo será BBBAAAAA...
(en lugar de ser reemplazado por BBB
).
No cubrí las 2>/dev/null
partes, y son innecesarias. Simplemente arreglan un poco la salida suprimiendo dd
el mensaje "Terminé y escribí estos bytes". Las barras invertidas al final de cada línea ( \
) hacen que bash trate todo como un gran comando que se conecta entre sí.
Aquí hay un script completo para un uso más fácil. Como anécdota, lo puse en una carpeta llamada 'gz-in-place'. Entonces me di cuenta del acrónimo que hice: GZIP: gnu zip in place. Por la presente les presento, 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!";
Tengo ganas de agregar otra línea de almacenamiento en búfer antes de gzip, para evitar que se escriba demasiado lejos cuando la dd
línea de almacenamiento en búfer pasa, pero con solo 50 MiB de búfer y 1900 MB de /dev/urandom
datos, parece funcionar de todos modos (los md5sums coincidieron después de descomprimir). Buena relación para mí.
Otra mejora sería la detección de la escritura demasiado lejos, pero no veo cómo hacerlo sin eliminar la belleza de la cosa y crear mucha complejidad. En ese momento, también podría convertirlo en un programa de Python completo que lo haga todo correctamente (con seguridad para evitar la destrucción de datos).