2020/06/14

Ayudas para gestionar archivos duplicados.

Una de mis tantas tareas postergadas que #quedateencasa me está permitiendo encarar, es limpiar un poco mi máquina de cosas repetidas, el tiempo pasa y a veces bajo algo otra vez pues no recuerdo haberlo hecho antes, tengo un proyecto al que le hice una copia por backup, tengo un proyecto que ha estado en distintas máquinas y cuando estoy por reformatear una copio a la principal y así. Mi disco tiene una selva parecida a esto:

Documentos
inbox
descargado
la datasheet

 anterior
Documentos
anterior
proyectos
precursor del proyecto

la datasheet

 inbox
clasificar
datashets
la datasheet

proyectos
precursor del proyecto

Desktop
repositorios
assembla
github
cpantel
el proyecto

doc
la datasheet

rescate
pendrive01
Desktop
el proyecto

la datasheet
 

Y lo mismo con instaladores, papers, libros, manuales, código mío y ajeno.

Me imagino que tendrás algo parecido.

Mas por salud mental que por el espacio ocupado, es mejorcito ordenar y limpiar.

Dado que en mi máquina por momentos pueden haber muchos, muchos archivos, no es una tarea sencilla y me he armado una serie de trucos/scripts para facilitarla.

Un millón y medio es un montón:

sudo find /home -type f | wc -l
790790

sudo find / -mount -type f | wc -l
718397

sudo es por que tu usuario no puede ver todo, de ahora en más hay uno implícito.

-type f es por que no queremos contar cosas que no son archivos.

-mount es para que no se escape de la partición y vuelva a contar a /home.

No recuerdo si herramientas forenses como Encase tienen alguna funcionalidad para detectar duplicados, porbablemente si pues una de sus funciones básicas es asociar un hash a cada archivo.

No me estoy metiendo con archivos de un sistema ajeno, son los míos, los conozco, esto es un complemento, no un sistema de gestión de archivos duplicados.

Si te preocupa el espacio, find te ayuda a encontrar los archivos grandes, sea cual sea tu criterio de grande:

find / -type f -size +1G -exec ls {} \; > 1g.txt

-size +1G significa mayores a un gigabyte

-exec significa ejecutá el siguiente comando hasta \; reemplazando {} con cada elemento hallado. Ojo que se va a ejecutar cada vez, si obtuviste mil archivos, van a haber mil ls, te aviso para cuando pongas un comando más pesadito.

Antes de cualquier limpieza está bueno medir:

df  / /home -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 276G 41G 221G 16% /
/dev/sda6 612G 574G 7,4G 99% /home

-h significa mostrame los bytes con K, M, G

1% de disco libre puede parecer poco, pero tené en cuenta que hay varias carpetas por ahí que se llaman "*BORRABLE*", en realidad tengo %10 libre.

Pero había dicho que lo que importa es la salud, no es espacio, vamos a buscar duplicados.

Nombres



Asociar por nombres tiene problemas, hay archivos que los he bajado y les he cambiado el nombre, por lo general papers, que vienen con un código que no me sirve de nada, o las datasheets que bajan todas con el mismo nombre.


A veces me agarra la loca y al nombre de la datasheet le agrego una descripción.

Esta linea sirve para obtener todos los normbres de los archivos, contando las repeticiones:


find / -type f | rev | cut -d "/" -f 1 | rev  | sort | uniq -c | sort -n

rev lo necesito para el próximo paso, invierte el sentido del texto

cut corta en pedacitos separados por el delimitador...

-d "/" barra y me da el pedacito...

-f 1 primero. Esto es para quedarme con el nombre de archivo, lo que era lo último

rev necesito que esté al derecho otra vez

sort ordena alfabéticamente para juntar los iguales

uniq descarta duplicados mientras...

-c  cuenta cuantos duplicados hubo

sort nuevamente ordenamos según el número de repeticiones

-n numéricamente



Me pudiste haber preguntado por que no usar:

find / -type f -exec basename {} \; | sort | uniq -c | sort -nr

Recordá que antes te dije que -exec no es precisamente eficiente, el tiempo de ejecución paso de 11 segundos a... me aburrí de esperar, mientras hago otras cosa dejé escrito despues esto así me entero.

aplay /usr/share/sounds/speech-dispatcher/dummy-message.wav

aplay tira al sistema de audio el archivo de audio que le dés, ojo que no soporta cualquier cosa.


Hashes



Mejor buscar por hash, con md5 estamos bien, no estamos ante un adversario.

Recordemos que es un hash y que ocurre cuando hay un adversario de por medio.

Un hash es una función que se le aplica a un dato tal que "lo comprime" a un tamaño fijo, perdiendo un montón de información, pero con la característica de que distintos datos dan distintos hashes, hasta cierto punto.

La idea es que si dos archivos tienen la misma firma es problable que sean iguales.

Lo que tiene md5 es que el tamaño es un tanto reducido y que además no es seguro criptográficamente hablando, se pueden tomar atajos en las cuentas, no lo uses entonces si hay un adversario de por medio.

Puede haber usado sha1sum, sha2sum, etc, pero para lo que quiero, md5 alcanza y sobra.

find / -type f -iname "*.pdf" -o "*.epub" | while read BOOK; do
  md5sum "$BOOK"
done
-iname es para que busque cosas que terminen en pdf

-o es o lo siguiente

while toma cada linea y se la da a a read

read lee cada linea y la pone en la variable de nombre BOOK

md5sum calcula el hash md5 del archivo apuntado por $BOOK

Casi cuatro horas lleva esto...


Inspección


Como método complementario queda la inspección manual, tiene mucho de recuerdos, asociaciones, ver en que carpeta está cada cosa y qué lo rodea.


Ejemplo concreto


Primero un listado parcial de nombres de archivos con su frecuencia:

 3 Esapi-datasheet.pdf
2 msp430fr569xx_datasheet.pdf
2 BK-913-datasheet.pdf
1 spms376e_Tiva-TM4C123GH6PM_datasheet.pdf
1 sg90_datasheet.pdf
1 pic16f73-4-6-7-datasheet.pdf
1 msp430fr59x_69x_datasheet.pdf
1 e16g301_datasheet.pdf
1 datasheet.pdf
1 atmel-2586-avr-8-bit-microc....iny85_datasheet.pdf
1 atmel-2586-avr-8-bit-microc....iny85_Datasheet.pdf
1 AD9523-1_datasheet.pdf
1 utc uln2003 DARLINGTON SINK DRIVER.pdf
1 utc uln2003.pdf
Ahi hay dos pares de archivos probablemente el mismo con distintos nombres.

Estos son los hashes, todo recortado para que se vea bien:

c23db  /PlanDeEstu...uments/msp430fr569xx_datasheet.pdf
dbad8 /PlanDeEstu...sheets/AD9523/AD9523-1_datasheet.pdf
af564 /PlanDeEstu...2018/parallella/docs/e16g301_datasheet.pdf
b04c2 /INBOX/pasa...dspecs/atmel-2586-avr-8-bit-microco...iny85_datasheet.pdf
7f1c9 /INBOX/ANTE...ATABLE/robot/datasheet.pdf
f6e40 /INBOX/ANTE...ATABLE/robot/utc uln2003.pdf
c23db /REPO/githu...bricante/TI/msp430fr59x_69x_datasheet.pdf
bf45f /REPO/githu...AA_K60/Datasheets/BK-913-datasheet.pdf
bf45f /REPO/githu...EDU-NXP/Datasheets/BK-913-datasheet.pdf
c23db /SORT/free/...atasheet/TI/msp430fr569xx_datasheet.pdf
b04c2 /REPO/githu...h_cooler/doc/Atmel-2586-AVR-8-bit-Microco...iny85_Datasheet.pdf
a08eb /SORT/free/.../sg90_datasheet.pdf
f6e40 /SORT/free/.../components/utc uln2003 DARLINGTON SINK DRIVER.pdf
7a8ac /SORT/Secur...aining April 16th 2010/OWASP ESAPI/Esapi-datasheet.pdf
7a8ac /SORT/Secur...g/OWASP Default Training/OWASP ESAPI/Esapi-datasheet.pdf
7a8ac /SORT/Secur...aining May 28th 2010/OWASP ESAPI/Esapi-datasheet.pdf
fa658 /formacion/...sembly/spms376e_Tiva-TM4C123GH6PM_datasheet.pdf
aa14a /research/m...ntroller/pic/pic16f73-4-6-7-datasheet.pdf
Fijate que coincide la sumatoria del primer listado con la longitud de éste.

Ordenando por hashes, saltan los iguales:


7a8ac  /SORT/Secur...aining April 16th 2010/OWASP ESAPI/Esapi-datasheet.pdf
7a8ac  /SORT/Secur...aining May 28th 2010/OWASP ESAPI/Esapi-datasheet.pdf
7a8ac  /SORT/Secur...g/OWASP Default Training/OWASP ESAPI/Esapi-datasheet.pdf
7f1c9  /INBOX/ANTERIOR/RESCATABLE/robot/datasheet.pdf
a08eb  /SORT/free/.../sg90_datasheet.pdf
aa14a  /research/m...ntroller/pic/pic16f73-4-6-7-datasheet.pdf
af564  /PlanDeEstu...2018/parallella/docs/e16g301_datasheet.pdf
b04c2  /INBOX/pasa...dspecs/atmel-2586-avr-8-bit-microco...iny85_datasheet.pdf
b04c2 /REPO/githu...h_cooler/doc/Atmel-2586-AVR-8-bit-Microco...iny85_Datasheet.pdf
bf45f  /REPO/githu...AA_K60/Datasheets/BK-913-datasheet.pdf
bf45f  /REPO/githu...EDU-NXP/Datasheets/BK-913-datasheet.pdf
c23db  /PlanDeEstu...uments/msp430fr569xx_datasheet.pdf
c23db  /REPO/githu...bricante/TI/msp430fr59x_69x_datasheet.pdf
c23db  /SORT/free/...atasheet/TI/msp430fr569xx_datasheet.pdf
dbad8  /PlanDeEstu...sheets/AD9523/AD9523-1_datasheet.pdf
f6e40 /INBOX/ANTE...ATABLE/robot/utc uln2003.pdf
f6e40 /SORT/free/.../components/utc uln2003 DARLINGTON SINK DRIVER.pdf
fa658  /formacion/...sembly/spms376e_Tiva-TM4C123GH6PM_datasheet.pdf


<interrupción>

Se activó aplay ¡28 minutos y medio! termino el find con -exec, te recuerdo que del otro modo fueron 11 segundos.

</interrupción>


Finalmente, análisis manual:

7f1c9  /INBOX/ANTERIOR/RESCATABLE/robot/datasheet.pdf
es equivalente a

f6e40  /INBOX/ANTE...ATABLE/robot/utc uln2003.pdf
f6e40 /SORT/free/.../components/utc uln2003 DARLINGTON SINK DRIVER.pdf

¡Qué trabajito!, ¿no? Multiplicalo por mil.


Duplicados legítimos


Hay archivos que están duplicados y es legítimo, por ejemplo documentación del mismo componente en distintos repositorios. O esto:

35...c8  Vivado/2015.2/.../mig_7series_v1_9/.../redirect/ug586_7Series_MIS.pdf
35...c8  Vivado/2015.2/.../mig_7series_v1_9/.../ug586_7Series_MIS.pdf
35...c8  Vivado/2015.2/.../mig_7series_v1_8/.../redirect/ug586_7Series_MIS.pdf
35...c8  Vivado/2015.2/.../mig_7series_v1_8/.../ug586_7Series_MIS.pdf
35...c8  Vivado/2015.2/.../mig_7series_v1_7/.../redirect/ug586_7Series_MIS.pdf
35...c8  Vivado/2015.2/.../mig_7series_v1_7/.../ug586_7Series_MIS.pdf
35...c8  Vivado/2018.2/.../mig_7series_v1_9/.../redirect/ug586_7Series_MIS.pdf
35...c8  Vivado/2018.2/.../mig_7series_v1_9/.../ug586_7Series_MIS.pdf
35...c8  Vivado/2018.2/.../mig_7series_v1_8/.../redirect/ug586_7Series_MIS.pdf
35...c8  Vivado/2018.2/.../mig_7series_v1_8/.../ug586_7Series_MIS.pdf
35...c8  Vivado/2018.2/.../mig_7series_v1_7/.../redirect/ug586_7Series_MIS.pdf
35...c8  Vivado/2018.2/.../mig_7series_v1_7/.../ug586_7Series_MIS.pdf
35...c8  Vivado/2015.4/.../mig_7series_v1_9/.../redirect/ug586_7Series_MIS.pdf
35...c8  Vivado/2015.4/.../mig_7series_v1_9/.../ug586_7Series_MIS.pdf
35...c8  Vivado/2015.4/.../mig_7series_v1_8/.../redirect/ug586_7Series_MIS.pdf
35...c8  Vivado/2015.4/.../mig_7series_v1_8/.../ug586_7Series_MIS.pdf
35...c8  Vivado/2015.4/.../mig_7series_v1_7/.../redirect/ug586_7Series_MIS.pdf
35...c8  Vivado/2015.4/.../mig_7series_v1_7/.../ug586_7Series_MIS.pdf

18 archivos iguales, mala suerte, no se pueden borrar.

Me dirías que esos son parte de programas, no es asunto mío, no debería ni buscar por fuera de /home. Pués si lo es, por que hay programas y programas. Lo que están instalados por la distribución ok, es verdad que no importan, pero muchos otros como los de xilinx son independientes de la distribución, o pueden ser resultado de la compilación de un repositorio.

Como sea, nos encontramos con que hay "falsos positivos" como partes de programas, clones de versionamiento, carpetas tipo "BORRAR".

Lo que podría hacer es algo tipo

grep -v -f whitelist.txt

-v es que invierta la selección, esto es que me traiga lo que NO cumple

-f es una lista de rutas

Pero si me interesa cuando la duplicación se produce entre estos falsos positivos y lo que está fuera de esas rutas, puedo tener el archivo ug586_7Series_MIS.pdf
incluido en un curso, lo conservo. O puede estar en ~/Downloads, puedo borrarlo.



Ayuda al análisis manual

Hasta acá estas son mis reglas:

  • Hay paths que corresponden a aplicaciones, no me interesan los hallazgos a menos que coincidan con elementos fuera de esos paths.
  • Si están versionados, no me interesan los hallazgos a menos que coincidan con elementos fuera de esos paths.
  • Mejor ver de conjunto para detectar grupos de duplicación, producto de backups, snapshots, rescates y sincronizaciones.

Para ayudarme en este análisis, me he hecho un script en awk, un lenguaje extremadamente interesante, suele estar asociado a la herramienta sed que viene a ser algo así como automatizar las operaciones del programa vi, que es un editor de texto.

No te puedo contar el tiempo que me llevó escribir el siguiente script pues me dá vergüenza decir que fueron dos horas, es bastante básico pero aunque tengo experiencia con awk, tiene algunas sutilezas que voy a comentar. No soy experto pero lo vengo usándo hace décadas de modo esporádico, cuando hice este programa choqué con varias cosas y eso me indujo a escribir este post.

El programa lo que hace es:
  • Carga una lista de falsos positivos.
  • Recibe linea por linea algo del tipo "hash ruta" ordenado por hash.
  • Descarta ocurrencias simples de hash.
  • Señala los casos correspondientes a la lista de falsos positivos.

Comento el código señalando la resolución a las dificultades que tuve y lo que me llama la atención del lenguaje pensando que venís de C y bash.

awk soporta funciones pero sus returns no devuelven tipos complejos, no arrays, aunque se pueden usar globales mejor por referencia:

function load_whitelist(file, whitelist) {
   while (1) {
      status = getline record < file
      if (status == -1) {
         print "falla " file;
         exit 1;
      }
      if (status == 0) break;
      whitelist[++count] = record
   }
   close(file);
}

Atención, index devuelve la posición, no el desplazamiento...

function check_in_whitelist(path, whitelist) {
  for ( elem in whitelist ) {
    if ( index(path, whitelist[elem]) == 35 ) {
      return 1;
    }
  }
  return 0
}
function print_checked(line, whitelist) {
  if ( check_in_whitelist(line, whitelist) ) {
    print "XXX " line
  } else {
    print "    " line
  }
}

Recordemos que lo que esté en el bloque BEGIN se ejecuta una sola vez al comienzo del programa, viene a ser como el setup() en ArduinoIDE

BEGIN {
  state = "state_first"
  load_whitelist("whitelist.txt", whitelist)
}

He utilizado una FSM como lo relatado en ...

Las instrucciones estas se ejecutan para cada linea leida por STDIN, a la que se le aplica el pattern. Normalmente se hace mucho con esos patterns, pero en este caso que las lineas son todas homogéneas con esto alcanza.

Recordá que cada pattern y su bloque correspondiente se van evaluando y ejecutando en orden, con next podés pasar a procesar la siguiente linea de entrada sin aplicar las reglas restantes.

Recuerdo, estoy seguro, que en los patterns no se pueden usar variables.

/.*/ {
  switch (state) {
    case "state_first":
       linea_anterior = $0
       md5_anterior   = $1
       state = "state_head"
    break;
    case "state_head":
      if ($1 == md5_anterior) {
        print_checked(linea_anterior, whitelist)
        linea_anterior = $0
        state = "state_tail"
      } else {
       linea_anterior = $0
       md5_anterior   = $1
      }
    break;
    case "state_tail":
      if ($1 == md5_anterior) {
        print_checked(linea_anterior, whitelist)
        linea_anterior = $0
      } else {
        print_checked(linea_anterior, whitelist)
        print "======================================"
       linea_anterior = $0
        md5_anterior   = $1
        state = "state_head"
      }
    break;
  }
}

Finalmente, qué sorpresa, está el bloque END, para procesar la última línea:

END {
  if (state == "state_tail" ) {
    print_checked(linea_anterior, whitelist)
 }
}

Aplicándolo al ejemplo que vengo arrastrando:


    7a8ac  /SORT/Secur...aining April 16th 2010/OWASP ESAPI/Esapi-datasheet.pdf
7a8ac  /SORT/Secur...aining May 28th 2010/OWASP ESAPI/Esapi-datasheet.pdf
7a8ac  /SORT/Secur...g/OWASP Default Training/OWASP ESAPI/Esapi-datasheet.pdf
======================================
b04c2  /INBOX/pasa...dspecs/atmel-2586-avr-8-bit-microco...iny85_datasheet.pdf
b04c2 /REPO/githu...h_cooler/doc/Atmel-2586-AVR-8-bit-Microco...iny85_Datasheet.pdf
======================================
 bf45f  /REPO/githu...AA_K60/Datasheets/BK-913-datasheet.pdf
bf45f  /REPO/githu...EDU-NXP/Datasheets/BK-913-datasheet.pdf
======================================
c23db  /PlanDeEstu...uments/msp430fr569xx_datasheet.pdf
c23db  /REPO/githu...bricante/TI/msp430fr59x_69x_datasheet.pdf
c23db  /SORT/free/...atasheet/TI/msp430fr569xx_datasheet.pdf
======================================
f6e40 /INBOX/ANTE...ATABLE/robot/utc uln2003.pdf
f6e40 /SORT/free/.../components/utc uln2003 DARLINGTON SINK DRIVER.pdf

Y si en el archivo whitelist.txt ponemos alguna ruta, por ejemplo la de github:

    7a8ac  /SORT/Secur...aining April 16th 2010/OWASP ESAPI/Esapi-datasheet.pdf
7a8ac  /SORT/Secur...aining May 28th 2010/OWASP ESAPI/Esapi-datasheet.pdf
7a8ac  /SORT/Secur...g/OWASP Default Training/OWASP ESAPI/Esapi-datasheet.pdf
======================================
b04c2  /INBOX/pasa...dspecs/atmel-2586-avr-8-bit-microco...iny85_datasheet.pdf
XXX b04c2 /REPO/githu...h_cooler/doc/Atmel-2586-AVR-8-bit-Microco...iny85_Datasheet.pdf
======================================
XXX bf45f  /REPO/githu...AA_K60/Datasheets/BK-913-datasheet.pdf
XXX bf45f  /REPO/githu...EDU-NXP/Datasheets/BK-913-datasheet.pdf
======================================
c23db  /PlanDeEstu...uments/msp430fr569xx_datasheet.pdf
XXX c23db  /REPO/githu...bricante/TI/msp430fr59x_69x_datasheet.pdf
c23db  /SORT/free/...atasheet/TI/msp430fr569xx_datasheet.pdf
======================================
f6e40 /INBOX/ANTE...ATABLE/robot/utc uln2003.pdf
f6e40 /SORT/free/.../components/utc uln2003 DARLINGTON SINK DRIVER.pdf

con XXX marca cuales he decidido ignorar pero me sirven para saber que hacer, por ejemplo puedo borrar msp430fr569xx_datasheet.pdf si lo deseo, probablemente no, pues me gusta tener una carpeta con datasheets aunque queden duplicadas.


Por último, después de mirar muchos duplicados veremos que hay un patrón, puede ser muy útil aparte de identificar duplicación de archivos la duplicación aunque sea parcial de carpetas.

Cuando encontras varios archivos duplicados dentro de una misma carpeta, podés comparar las carpetas con el comando dirdiff, si no lo teneś:

sudo apt install dirdiff

Este programa te muestra canditatos a ser diferentes por fecha de modificación, pero no contenido, ni siquiera por longitud, no le confíes mucho, pero es indudablemente útil para ordenarte.

El código en github

No hay comentarios:

Publicar un comentario