Convertir candidatos en grupos duplicados

JavaBeginner
Practicar Ahora

Introducción

Una huella candidata abarata la búsqueda en una colección, pero no demuestra que el contenido sea idéntico. Completarás un buscador de duplicados de solo lectura que confirme los bytes reales y conserve cada ruta relativa de cada grupo duplicado.

Este laboratorio presupone las ideas de lectura binaria y SHA-256 del anterior, además de clases, listas y excepciones de Java. Comienza de forma independiente con el resumen completado y una nueva copia de la galería. Tú implementas la agrupación; los adaptadores de directorio, informe y lanzamiento están preparados. El autor estima 55 minutos.

Confirmar cada candidato con bytes reales

En este paso, convertirás los hechos candidatos en grupos duplicados completos. Abre WebIDE, elige Terminal > New Terminal y entra en el espacio de trabajo independiente:

cd /home/labex/project/duplicates
./run.sh Sample

El decodificador de PNG reales vuelve a mostrar bytes iguales bajo nombres diferentes y bytes distintos con igual tamaño. El lanzador utiliza una nueva compilación temporal en cada ejecución; los procesos hijos del compilador/JVM usan C.UTF-8 y ajustes explícitos de UTF-8 para el código y el texto. La configuración de tu terminal no cambia.

Lee el adaptador público y el código inicial:

cat src/Digester.java src/Finder.java

Finder.find(Path root, Digester candidates) debe devolver una lista de grupos duplicados; cada grupo contiene todas las rutas de sus miembros relativas a root. root.relativize(path) crea esa ruta relativa; root.resolve(relative) permite localizarla de nuevo para leerla. sameBytes(left, right) compara contenido, no nombres. Conserva estos puntos de entrada públicos; los auxiliares y las colecciones internos pueden variar.

La implementación siguiente utiliza un Map para asociar una clave candidata con sus posibles grupos. La clave combina tamaño y resumen. containsKey pregunta si existe una clave, put crea su valor inicial y get lo recupera. Cada clave contiene una lista de grupos confirmados por bytes porque dos contenidos distintos podrían compartir una huella candidata. List<List<Path>> significa una lista cuyos elementos son listas de rutas.

Para cada archivo, prueba la ruta representante de cada grupo existente en ese contenedor. Una coincidencia real añade la nueva ruta; en otro caso, crea otro subgrupo. placed indica si la ruta actual ya se ha unido a un grupo; break detiene la búsqueda interior tras una coincidencia para añadir cada ruta una sola vez. Solo los grupos con al menos dos rutas llegan al informe. No te detengas en dos miembros: una tercera copia sigue siendo una ruta distinta que se debe revisar.

sameBytes primero rechaza longitudes diferentes y después lee ambos archivos. read() devuelve un byte como entero entre 0 y 255, o -1 al final. Un byte diferente devuelve false; llegar al final a la vez devuelve true. Ambos flujos se cierran mediante try-with-resources. La lectura byte a byte facilita entender este pequeño ejemplo; las comparaciones con búfer o con arrays completos son alternativas válidas con distintas implicaciones de memoria y rendimiento.

El mapa before asocia cada Path con sus metadatos originales. before.keySet() devuelve esas claves de ruta para la comprobación final de cambios; buckets.values() devuelve las listas guardadas de subgrupos candidatos para construir los resultados. El adaptador preparado registra tamaño, tiempo de modificación e identidad del archivo antes de leer, y rechaza los cambios detectados. No bloquea ni congela el árbol. Mantén estables las entradas durante el análisis; no se admiten escritores concurrentes, enlaces simbólicos ni archivos especiales. Cada ruta de archivo ordinario importa, incluidas rutas diferentes al mismo archivo mediante enlaces duros. La identidad del archivo solo sirve para detectar cambios, nunca para descartar una ruta.

Sustituye src/Finder.java. Como en el laboratorio anterior, el here-document entre comillas escribe texto literal; el cierre JAVA debe ocupar su propia línea. > reemplaza solo este archivo fuente.

cat > src/Finder.java <<'JAVA'
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;

public class Finder {
    public static boolean sameBytes(Path left, Path right) throws IOException {
        if (Files.size(left) != Files.size(right)) return false;
        try (InputStream a = Files.newInputStream(left);
             InputStream b = Files.newInputStream(right)) {
            int value;
            while ((value = a.read()) != -1) {
                if (value != b.read()) return false;
            }
            return b.read() == -1;
        }
    }
    public static List<List<Path>> find(Path root, Digester candidates) throws IOException {
        Map<String, List<List<Path>>> buckets = new LinkedHashMap<>();
        Map<Path, BasicFileAttributes> before = new LinkedHashMap<>();
        for (Path path : Inputs.files(root)) {
            BasicFileAttributes facts = Inputs.attributes(path);
            before.put(path, facts);
            String key = facts.size() + ":" + candidates.digest(path);
            Inputs.unchanged(path, facts);
            if (!buckets.containsKey(key)) buckets.put(key, new ArrayList<>());
            List<List<Path>> groups = buckets.get(key);
            boolean placed = false;
            for (List<Path> group : groups) {
                if (sameBytes(path, root.resolve(group.get(0)))) {
                    group.add(root.relativize(path));
                    placed = true;
                    break;
                }
            }
            if (!placed) {
                List<Path> group = new ArrayList<>();
                group.add(root.relativize(path));
                groups.add(group);
            }
        }
        for (Path path : before.keySet()) Inputs.unchanged(path, before.get(path));
        List<List<Path>> result = new ArrayList<>();
        for (List<List<Path>> groups : buckets.values()) {
            for (List<Path> group : groups) {
                if (group.size() > 1) result.add(group);
            }
        }
        return result;
    }
}
JAVA

El programa preparado Main llama al mismo método find con new Sha256(). Las pruebas pueden proporcionar otro Digester que devuelva un único valor candidato para archivos diferentes. Es una prueba controlada de colisión de candidatos, no una colisión real de SHA-256; la comparación final de bytes debe seguir separando contenidos distintos. Escribir un método de comparación que nunca se llama no basta.

Genera el informe real fuera de la raíz analizada:

./run.sh Main data/gallery reports/groups.tsv

Espera Scan complete: 3 duplicate groups. Si falla la compilación, corrige la línea indicada de src/Finder.java, guarda y vuelve a ejecutar; comprueba las llaves y las firmas antes de cambiar los datos.

Leer el grupo completo y repetir tras un cambio

En este paso, leerás el informe completado y observarás cómo sigue los bytes actuales. Continúa en el mismo terminal integrado de WebIDE, en /home/labex/project/duplicates.

cat reports/groups.tsv

La cabecera TSV en UTF-8 es GROUP, seguida de PATH. Cada fila identifica una ruta relativa; las etiquetas de grupo iguales conectan contenidos iguales. Las etiquetas no son identidades de archivo y su orden no importa. El grupo de la imagen roja debe contener sus tres rutas, los dos archivos vacíos forman un grupo válido y los dos binarios con búfer final parcial forman otro. La imagen azul y los readme con el mismo nombre pero contenido diferente no son duplicados. Puedes localizar cada miembro en duplicates/data/gallery mediante Explorer; no se elimina ninguna entrada.

Prueba un cambio con copias desechables en work, manteniendo intactos los originales de la galería. mkdir -p crea el directorio de práctica si es necesario. cp copia un archivo a una ruta nueva.

mkdir -p work/mini
cp data/gallery/red.png work/mini/first.png
cp data/gallery/copies/renamed.png work/mini/second.png
./run.sh Main work/mini reports/practice-before.tsv

Espera un grupo duplicado. Ahora printf escribe el texto entre comillas y su salto de línea; > reemplaza únicamente el segundo archivo de práctica. Ya no contiene un PNG, de modo que su extensión no demuestra su contenido.

printf 'changed\n' > work/mini/second.png
./run.sh Main work/mini reports/practice-after.tsv

Espera cero grupos duplicados. El informe conserva su cabecera:

cat reports/practice-after.tsv

Un resultado vacío correcto es distinto de un análisis fallido. El siguiente comando solicita deliberadamente un informe dentro del árbol de entrada. Espera Scan failed, estado de salida 2 y ningún archivo de entrada nuevo:

./run.sh Main data/gallery data/gallery/report.tsv

Corrige la ruta de salida y regenera el informe de la galería:

./run.sh Main data/gallery reports/groups.tsv

El adaptador preparado escribe un nuevo archivo temporal en el mismo directorio y sustituye la ruta de salida cuando el análisis termina correctamente. No abre un enlace duro existente para escribir, por lo que una ruta de informe no puede sobrescribir bytes originales a través de ese enlace. No garantiza durabilidad ante fallos. Si falla una lectura, el comando falla y un informe anterior no demuestra el estado actual; nunca omitas un archivo silenciosamente y anuncies éxito.

La comprobación final ejecuta tu código actual en casos aislados y lee el informe real de la galería. Comprueba grupos completos, contenido actual, errores y conservación de originales. Un informe anteriormente correcto no justifica un código fuente estropeado después.

Resumen

Has reducido candidatos con tamaño y SHA-256, confirmado bytes reales, conservado cada ruta duplicada y regenerado un informe tras un cambio real. Obtener correctamente cero grupos y sufrir un fallo de análisis tienen ahora significados distintos.