Transformer les candidats en groupes de doublons

JavaBeginner
Pratiquer maintenant

Introduction

Une empreinte candidate réduit le coût d'une recherche dans une archive, mais ne prouve pas l'identité du contenu. Vous allez terminer un outil en lecture seule qui confirme les octets réels et conserve chaque chemin relatif dans chaque groupe de doublons.

Ce laboratoire suppose les notions de lecture binaire et de SHA-256 du précédent, ainsi que les classes, listes et exceptions Java. Il démarre indépendamment avec une empreinte déjà implémentée et une nouvelle copie de la galerie. Vous réalisez le regroupement ; les adaptateurs de répertoire, rapport et lancement sont fournis. L'auteur estime 55 minutes.

Confirmer chaque candidat avec les octets réels

Dans cette étape, vous transformerez les faits candidats en groupes de doublons complets. Ouvrez WebIDE, choisissez Terminal > New Terminal et entrez dans l'espace de travail indépendant :

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

Le véritable décodeur PNG montre à nouveau des octets égaux sous des noms différents et des octets différents de même taille. Le lanceur utilise une nouvelle compilation temporaire à chaque exécution ; ses processus enfants compilateur/JVM utilisent C.UTF-8 et des réglages UTF-8 explicites pour le code et le texte. Les paramètres de votre terminal restent inchangés.

Lisez l'adaptateur public et le code initial :

cat src/Digester.java src/Finder.java

Finder.find(Path root, Digester candidates) doit renvoyer une liste de groupes de doublons, chacun contenant tous les chemins de ses membres relatifs à root. root.relativize(path) produit ce chemin relatif ; root.resolve(relative) le localise à nouveau pour la lecture. sameBytes(left, right) compare le contenu, pas les noms. Conservez ces points d'entrée publics ; les auxiliaires et collections internes peuvent différer.

L'implémentation suivante utilise un Map pour associer une clé candidate à ses groupes possibles. La clé combine taille et empreinte. containsKey vérifie l'existence d'une clé, put crée sa valeur initiale et get la récupère. Chaque clé contient une liste de groupes confirmés par les octets, car deux contenus distincts pourraient partager une empreinte candidate. List<List<Path>> désigne une liste dont les éléments sont des listes de chemins.

Pour chaque fichier, comparez-le au chemin représentant chaque groupe existant du compartiment. Une vraie correspondance ajoute le nouveau chemin ; sinon, créez un autre sous-groupe. placed indique si le chemin courant a rejoint un groupe ; break arrête cette recherche interne après une correspondance, afin de n'ajouter chaque chemin qu'une fois. Seuls les groupes d'au moins deux chemins figurent dans le rapport. Ne vous arrêtez pas à deux membres : une troisième copie reste un chemin distinct à examiner.

sameBytes rejette d'abord les longueurs différentes, puis lit les deux fichiers. read() renvoie un octet sous forme d'entier de 0 à 255, ou -1 à la fin. Un octet différent renvoie false ; atteindre la fin ensemble renvoie true. Les deux flux sont fermés avec try-with-resources. Une lecture octet par octet facilite la compréhension de ce petit exemple ; les comparaisons avec tampon ou tableaux complets sont aussi valables, avec leurs compromis de mémoire et de performances.

La table before associe chaque Path à ses métadonnées originales. before.keySet() renvoie ces clés de chemin pour le dernier contrôle de changement ; buckets.values() renvoie les listes enregistrées de sous-groupes candidats pour assembler le résultat. L'adaptateur d'attributs fourni mémorise taille, date de modification et identité avant la lecture, puis refuse les changements détectés. Il ne verrouille ni ne fige l'arbre. Gardez les entrées stables pendant l'analyse ; les écritures concurrentes, liens symboliques et fichiers spéciaux ne sont pas pris en charge. Chaque chemin ordinaire compte, y compris plusieurs chemins vers un même fichier lié physiquement. L'identité sert uniquement à détecter les changements, jamais à écarter un chemin.

Remplacez src/Finder.java. Comme dans le laboratoire précédent, le here-document entre guillemets écrit le texte littéralement ; le marqueur final JAVA reste seul sur sa ligne. > remplace uniquement ce fichier source.

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

Le programme fourni Main appelle ce même find avec new Sha256(). Les tests peuvent fournir un autre Digester qui renvoie une même valeur candidate pour des fichiers différents. C'est un test contrôlé de collision de candidats, pas une véritable collision SHA-256 ; la comparaison finale doit toujours séparer les contenus différents. Écrire simplement une méthode de comparaison jamais appelée ne suffit pas.

Générez le vrai rapport hors de la racine analysée :

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

Le résultat attendu est Scan complete: 3 duplicate groups. Si la compilation échoue, corrigez la ligne signalée dans src/Finder.java, enregistrez et réessayez ; vérifiez accolades et signatures avant de modifier les données.

Lire le groupe entier et relancer après un changement

Dans cette étape, vous lirez le rapport terminé et observerez son évolution avec les octets actuels. Continuez dans le même terminal intégré de WebIDE, dans /home/labex/project/duplicates.

cat reports/groups.tsv

L'en-tête TSV en UTF-8 contient GROUP, puis PATH. Chaque ligne identifie un chemin relatif ; des étiquettes de groupe égales relient des contenus égaux. Les étiquettes ne sont pas des identités de fichier et leur ordre importe peu. Le groupe de l'image rouge doit contenir ses trois chemins, les deux fichiers vides constituent un groupe valide et les deux binaires au tampon final partiel un autre. L'image bleue et les readme de même nom mais de contenu différent ne sont pas des doublons. Vous pouvez retrouver chaque membre sous duplicates/data/gallery dans Explorer ; aucune entrée n'est supprimée.

Testez un changement avec des copies jetables dans work, en laissant les originaux intacts. mkdir -p crée le répertoire d'exercice si nécessaire. cp copie un fichier vers un nouveau chemin.

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

Vous devez obtenir un groupe de doublons. À présent, printf écrit le texte entre guillemets et son retour à la ligne ; > remplace uniquement le deuxième fichier d'exercice. Il ne contient plus de PNG : son extension ne prouve donc pas son contenu.

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

Vous devez obtenir zéro groupe. Le rapport conserve son en-tête :

cat reports/practice-after.tsv

Un résultat vide réussi diffère d'une analyse échouée. La commande suivante demande volontairement un rapport dans l'arbre d'entrée. Attendez-vous à Scan failed, au code de sortie 2 et à l'absence de nouveau fichier d'entrée :

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

Corrigez le chemin de sortie et régénérez le rapport de la galerie :

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

L'adaptateur fourni écrit un nouveau fichier temporaire voisin, puis remplace le chemin de sortie après une analyse réussie. Il n'ouvre pas un lien physique existant en écriture : un chemin de rapport ne peut donc pas écraser les octets originaux par ce lien. Cela ne garantit pas la durabilité après plantage. Si une lecture échoue, la commande échoue et un ancien rapport ne prouve pas l'état courant ; ne sautez jamais silencieusement un fichier en annonçant un succès.

Le contrôle final exécute votre code actuel dans des jeux de test isolés et lit le vrai rapport de la galerie. Il vérifie les groupes complets, le contenu actuel, les erreurs et la conservation des originaux. Un ancien rapport correct ne justifie pas un code ensuite cassé.

Résumé

Vous avez réduit les candidats avec la taille et SHA-256, confirmé les octets réels, conservé chaque chemin en double et régénéré un rapport après une véritable modification. Un succès avec zéro groupe et un échec d'analyse ont désormais des significations distinctes.