Cómo hacer una barra de progreso gráfica para las transferencias con rsync

Publicado el 29 enero 2016 por Gaspar Fernández Moreno @gaspar_fm

Rsync es una gran utilidad, de esas en las que al principio da miedo meterse, pero, cuando un día la descubres no quieres utilizar otra cosa y esperas el momento en que vuelves a utilizarla y encima se lo dices a tus amigos. Que luego te miran con cara de friki, y te dejarán de hablar, pero bueno... esperas pacientemente el momento en que ellos descubran Rsync.

Rsync, es una utilidad para copiar archivos. Tenemos, cp, incluso cp -r, pero rsync es capaz de mostrar el progreso (hace mucho propusieron cp -r, pero lo rechazaron por no cumplir la filosofía) y copiar tanto en la misma máquina como en un servidor externo; tenemos scp, pero rsync es capaz de hacer la copia transmitiendo la menor cantidad de datos posible, es decir además de comprimir la transferencia su copia es incremental, vamos, obtiene sólo los bytes modificados de un archivo en el origen y los copia en el destino, puedes excluir ciertos archivos de la copia, y muchas cosas más.

Eso sí, la indicación de progreso de rsync es sólo para visualizar en terminal, y no es inmediato pasarla a través de una pipe a otro lado (los linuxeros siempre queremos hacer algo con la información que tenemos en pantalla), además de incluir caracteres especiales para posicionarse en la terminal.

En estos ejemplos vamos a utilizar zenity como gestor de la salida en formato gráfico, tal y como indica este ejemplo:

Esta ventana del ejemplo, la podemos obtener fácilmente si escribimos en consola:

y luego, mientras está ejecutándose la aplicación escribimos un número entre el 0 y el 100 veremos que la barra de progreso se mueve, si ponemos cualquier otra cosa, la barra de progreso no se mueve, pero si empezamos una línea con una almohadilla (#) conseguimos que ese texto se escriba en la ventana de progreso también.

Un gran GUI conlleva una garn pérdida de CPU

Siempre será mucho más rápida una copia muda que una que nos esté diciendo todo el rato lo que queda. Aún más si la copia indica su progreso en formato gráfico. Imagina que no sólo tenemos que estar copiando, sino que tenemos que actualizar un dibujo, un texto, e incluso estar calculando todo el rato lo que queda. Es cierto que ahora tenemos todos un montón de cores en nuestro ordenador y no pasa nada, pero bueno, tengo que avisar que no es lo más rápido.

Por otro lado, somos humanos, y si no nos dicen todo el tiempo lo que queda, nos desesperamos, y el tiempo parecerá mucho más lento, como decía Einstein: " Pon tu mano sobre una estufa caliente durante un minuto y te parecerá una hora. Siéntate junto a una chica bonita durante una hora y te parece un minuto. ESO es la relatividad ".

Y bueno, y un poco de sobrecarga también se produce porque tenemos que encadenar varios programas haciendo que la salida de uno vaya a la entrada de otro y la salida de éste a la entrada de otro... usaremos muchas pipes. Pero estará todo contado con detalle.

Dos tipos de salida

Aún así, rsync optimiza mucho la salida, en este caso, los volcados del buffer de salida. Es decir, siempre que escribe en pantalla no realiza un volcado por lo que si queremos extraer los mensajes producidos desde otro programa, éste último no los va a ver. (Ya veremos cómo hace esto).

El tema es que rsync sí que produce un volcado de información de pantalla cuando acaba de copiar un fichero y se dispone a copiar otro nuevo, así que podríamos aprovechar esos volcados para extraer y representar el progreso:

En este ejemplo estamos encadenando tres cosas:

  • rsync:
    • -a : modo de archivos (es recursivo, incluye enlaces, conserva permisos, conserva marcas temporales, grupos, dueños y algunas cosas más).
    • -v : incrementa los detalles de salida. Nos dice más datos sobre la copia.
    • -h : salida en formato humano (los tamaños de archivo, en lugar de decirlos en bytes los transforma a una unidad más legible, como por ejemplo Kb, Mb...). Esta opción no es tan necesaria para el ejemplo, porque sólo necesitamos los porcentajes, pero bueno, si algún día queremos aumentar la salida, aquí la tenemos.
    • -info=progress2 : Indica el progreso en porcentaje y tamaño. Hay dos tipos de progreso, progress a secas es el progreso de cada archivo, es decir, cada archivo irá del 0 al 100, en cambio progress2 es el progreso global. Sólo irá del 0 al 100 una vez.
    • ORIGEN: indicará el archivo o los archivos a copiar, puedes utilizar asteriscos, o llaves para seleccionar los archivos a copiar.
    • usuario@servidor:ruta/en/servidor : en caso de querer copiar a un servidor, podemos utilizar este formato para un servidor a través de SSH (el servidor de destino deberá tener instalado el paquete rsync), esto también puede ser una carpeta en nuestro ordenador. O incluso el servidor puede ser el origen y la carpeta el destino.
  • awk '{print $3; fflush(); }' : para cada línea que me devuelva rsync ejecutaré un script sobre ella. awk es un lenguaje de tratamiento de cadenas de texto, en este caso, estamos escribiendo la tercera columna (las columnas por defecto vienen separadas por espacios). Tras imprimir la tercera columna, hacemos un volcado forzado del buffer de salida para que éste podamos pasarlo al siguiente comando.
  • zenity : ¡ mi preferido ! Hecho en GTK+ nos permite crear diálogos que podemos automatizar.
    • -progress : Crea un dialogo de progreso, como este que queremos. Si curioseamos el programa, podemos ver que permite crear una gran variedad de diálogos diferentes.
    • -title "Título de la ventana" : con esto lo digo todo.
    • -text "Texto sobre la barra" : también se explica solo.
    • -width=300 : la ventana tendrá un ancho de 300 pixels.

Archivos muy grandes

El problema del ejemplo está cuando queremos copiar archivos muy grandes, y por lo tanto tardarán mucho, la barra de progreso no se actualizará hasta que no se transfiera el archivo por completo. Imaginad la copia de un archivo de 1Gb y otro de 1Mb, uno tardará muchísimo menos que el otro, y la barra no se moverá durante varios minutos.

Para solucionar esto, tenemos que forzar el volcado de buffers de rsync, además, por la forma de la salida de rsync, tenemos que transformar los caracteres \r por \n y así poder interpretarlos como líneas.
Para ello, encadenaremos más comandos en el asunto, aunque antes de representar el progreso gráficamente, vamos a extraer las líneas por separado (por si queremos hacer algo con ellas: escribir un log, mandar un e-mail con el progreso, o lo que se os ocurra):

En este comando podemos ver cosas nuevas:

  • rsync:
      -out-format="FILE %f" : Cada vez que se copie un archivo se escribirá el nombre del archivo precedido de la palabra FILE.
  • stdbuf: Cambia el modo de volcado de buffers del comando que ejecutaremos (en este caso, tr)
    • -oL : Los buffers serán de línea, por lo que los datos se volcarán cada línea
    • tr "\r" "\n" : transforma los \r en \n como dijimos antes.
  • while read -r logline; do echo "RSYNC: $logline"; done : Mientras se puedan leer líneas de la salida del comando anterior, dichas líneas las almacenaré en la variable logline e imprimiré en pantalla "RSYNC: logline"

Lo de usar el while está muy bien en muchos casos, es más, ahora debemos analizar línea a línea la salida generada para extraer el progreso, pero lo haremos directamente con awk, como antes:

Ahora, hemos juntado el ejemplo de antes con awk y zenity para escribir el progreso en la barra. Ahora, al ser -P y no -info=progress2 escribirá el progreso de cada archivo por separado. Aunque para los archivos pequeños, la barra no se mueve, ya que no pasará por el 0 para reiniciar el progreso. Vamos a hacer una modificación más.

Hemos complicado un poco el script de awk. Ahora estamos diciéndole que si la primera palabra de la línea es FILE (recordemos que el out-format empezaba por FILE y luego el nombre de archvio), sustituimos FILE por almohadilla (#) y escribimos la línea completa (la # la utilizaba zenity para cambiar el texto sobre la barra). Seguidamente escribimos un INTRO (\n) y un 0 y con esto le decimos a zenity que reinicie el progreso de la barra.

Indicador de progreso global

Lo que podemos hacer con rsync para tener una visión global del progreso, y reaccionar frente a archivos grandes, sería:

  1. Contar el número de archivos que tenemos que copiar
  2. Sabiendo el número, podemos calcular en qué parte del progreso global nos encontramos, y sumarle el progreso de cada archivo por separado.

Aquí tendríamos un problema, si tenemos muchos muchos archivos nos darían igual los archivos grandes, ya que si el progreso de un archivo grande no llega al 1% daría igual ver su progreso por separado o no, de todas formas, vamos a poner el progreso del archivo entre paréntesis.

El script awk es más grande, y al cambiar la comilla simple por doble (porque como voy a meter variables de bash ($NUMBEROFFILES) me viene algo mejor, aunque las variables de awk tengo que escaparlas (por ejemplo \$0, \$1...).

Lo que hago al principio es contar cuántos archivos van a ser transferidos con -list-only. Como este parámetro escribe los archivos a transferir uno por línea, con wc -l cuento las líneas de salida y obtengo el número de archivos total.

Dentro de awk, mi script lo divido en BEGIN {...} y {...}, la primera parte la ejecutaremos antes de la entrada de datos, por lo que inicializaremos variables como el número de archivo que estamos analizando y el nombre del archivo actual.

Ahora, para conocer el progreso hasta el momento utilizo \$2/$NUMBEROFFILES+100*(CURRENT-1)/$NUMBEROFFILES, es decir, el porcentaje total del archivo actual entre el número total de archivos (con lo que obtenemos el porcentaje de progreso global de este archivo) y a eso le sumamos (el número de archivo actual-1)/número de archivos) que será el progreso global hasta el archivo actual.

Para hacer pruebas...

Para hacer pruebas con todo esto podemos utilizar el modificador -bwlimit de rsync con el que podemos limitar por ejemplo a 100K/s (-bwlimit=100K) la transferencia de ficheros, o incluso a menos, el objetivo es que se transmita lento para ver cómo se comporta el script.

Foto principal: Brandon Redfern