Transformar candidatos em grupos de duplicatas

JavaBeginner
Pratique Agora

Introdução

Uma impressão digital candidata reduz o custo da busca em uma coleção de arquivos, mas não comprova conteúdo idêntico. Você concluirá um localizador de duplicatas somente de leitura que confirma bytes reais e preserva cada caminho relativo de cada grupo.

Este laboratório pressupõe as ideias de leitura binária e SHA-256 do anterior, além de classes, listas e exceções Java. Ele começa independentemente com o hash concluído e uma nova cópia da galeria. Você implementa o agrupamento; os adaptadores de diretório, relatório e execução estão prontos. A estimativa do autor é de 55 minutos.

Confirmar cada candidato com bytes reais

Nesta etapa, você transformará os fatos candidatos em grupos completos de duplicatas. Abra WebIDE, escolha Terminal > New Terminal e entre no espaço de trabalho independente:

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

O decodificador PNG real mostra novamente bytes iguais com nomes diferentes e bytes diferentes do mesmo tamanho. O inicializador usa uma nova compilação temporária em cada execução; os processos filhos do compilador/JVM usam C.UTF-8 e configurações explícitas de UTF-8 para fonte e texto. As configurações do terminal não mudam.

Leia o adaptador público e a implementação inicial:

cat src/Digester.java src/Finder.java

Finder.find(Path root, Digester candidates) deve retornar uma lista de grupos de duplicatas, cada um contendo todos os caminhos de seus membros relativos a root. root.relativize(path) cria esse caminho relativo; root.resolve(relative) o localiza novamente para leitura. sameBytes(left, right) compara conteúdo, não nomes. Mantenha esses pontos de entrada públicos; auxiliares e coleções internos podem variar.

A implementação abaixo usa um Map para associar uma chave candidata aos seus possíveis grupos. A chave combina tamanho e hash. containsKey pergunta se uma chave existe, put cria seu valor inicial e get o recupera. Cada chave contém uma lista de grupos confirmados por bytes, pois dois conteúdos diferentes poderiam compartilhar a mesma impressão digital candidata. List<List<Path>> significa uma lista cujos elementos são listas de caminhos.

Para cada arquivo, compare-o com o caminho representativo de cada grupo existente naquele bucket. Uma correspondência real adiciona o novo caminho; caso contrário, inicia outro subgrupo. placed registra se o caminho atual já entrou em um grupo; break interrompe a busca interna após uma correspondência, garantindo que cada caminho seja adicionado uma única vez. Apenas grupos com pelo menos dois caminhos entram no relatório. Não pare em dois membros: uma terceira cópia ainda é um caminho separado que alguém precisa revisar.

sameBytes primeiro rejeita comprimentos diferentes e depois lê os dois arquivos. read() retorna um byte como inteiro entre 0 e 255, ou -1 no fim. Um byte diferente retorna false; atingir o fim ao mesmo tempo retorna true. Ambos os fluxos são fechados por try-with-resources. A leitura byte a byte facilita entender este pequeno exemplo; comparações com buffer ou arrays completos são alternativas válidas com diferentes custos de memória e desempenho.

O mapa before associa cada Path aos seus metadados originais. before.keySet() retorna essas chaves de caminho para a verificação final de mudanças; buckets.values() retorna as listas armazenadas de subgrupos candidatos para montar o resultado. O adaptador de atributos preparado registra tamanho, horário de modificação e identidade do arquivo antes da leitura, rejeitando mudanças detectadas. Ele não bloqueia nem congela a árvore. Mantenha as entradas estáveis durante a varredura; gravações concorrentes, links simbólicos e arquivos especiais não são suportados. Cada caminho comum importa, inclusive caminhos diferentes ligados fisicamente ao mesmo arquivo. A identidade do arquivo serve apenas para detectar mudanças, nunca para descartar um caminho.

Substitua src/Finder.java. Como no laboratório anterior, o here-document entre aspas escreve texto literal; o JAVA final deve ocupar sua própria linha. > substitui apenas este arquivo-fonte.

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

O Main preparado chama esse mesmo método find com new Sha256(). Os testes podem fornecer outro Digester que retorna um único valor candidato para arquivos diferentes. É um teste controlado de colisão de candidatos, não uma colisão real de SHA-256; a comparação final de bytes ainda deve separar conteúdos diferentes. Escrever apenas um método de comparação que nunca é chamado não basta.

Gere o relatório real fora da raiz examinada:

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

Espere Scan complete: 3 duplicate groups. Se a compilação falhar, corrija a linha indicada de src/Finder.java, salve e execute novamente; verifique as chaves e as assinaturas dos métodos antes de mudar os dados.

Ler o grupo inteiro e repetir após uma mudança

Nesta etapa, você lerá o relatório concluído e verá como ele acompanha os bytes atuais. Continue no mesmo terminal integrado do WebIDE, em /home/labex/project/duplicates.

cat reports/groups.tsv

O cabeçalho TSV em UTF-8 é GROUP, seguido de PATH. Cada linha identifica um caminho relativo; rótulos de grupo iguais conectam conteúdos iguais. Os rótulos não são identidades de arquivo e sua ordem não importa. O grupo da imagem vermelha deve conter os três caminhos, os dois arquivos vazios formam um grupo válido e os dois binários com buffer final parcial formam outro. A imagem azul e os readme de mesmo nome, mas conteúdo diferente, não são duplicatas. Você pode localizar cada membro em duplicates/data/gallery no Explorer; nenhuma entrada é removida.

Teste uma mudança usando cópias descartáveis em work, mantendo intactos os originais da galeria. mkdir -p cria o diretório de prática se necessário. cp copia um arquivo para um novo caminho.

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

Espere um grupo de duplicatas. Agora printf escreve o texto entre aspas e sua quebra de linha; > substitui apenas o segundo arquivo de prática. Ele não contém mais um PNG, portanto sua extensão não comprova seu conteúdo.

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

Espere zero grupos de duplicatas. O relatório continua com seu cabeçalho:

cat reports/practice-after.tsv

Um resultado vazio bem-sucedido é diferente de uma varredura que falhou. O próximo comando solicita deliberadamente um relatório dentro da árvore de entrada. Espere Scan failed, status de saída 2 e nenhum novo arquivo de entrada:

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

Corrija o caminho de saída e regenere o relatório da galeria:

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

O adaptador de relatórios preparado escreve um novo arquivo temporário vizinho e substitui o caminho de saída depois de concluir a varredura. Ele não abre um link físico existente para gravação, portanto o caminho do relatório não pode sobrescrever bytes originais por esse link. Isso não garante durabilidade após um encerramento anormal. Se uma leitura falhar, o comando falha e um relatório anterior não comprova a execução atual; nunca ignore silenciosamente um arquivo e anuncie sucesso.

A verificação final executa seu código atual em dados de teste isolados e lê o relatório real da galeria. Ela verifica grupos completos, conteúdo atual, erros e preservação dos originais. Um relatório anteriormente correto não justifica um código-fonte danificado depois.

Resumo

Você restringiu candidatos com tamanho e SHA-256, confirmou bytes reais, preservou cada caminho duplicado e regenerou um relatório após uma mudança real. Um sucesso com zero grupos e uma falha de varredura agora têm significados diferentes.