Kandidaten in Duplikatgruppen überführen

JavaBeginner
Jetzt üben

Einführung

Ein Kandidatenfingerabdruck verringert den Aufwand einer Archivsuche, beweist aber keinen identischen Inhalt. Du vervollständigst einen lesenden Dublettenfinder, der echte Bytes bestätigt und jeden relativen Pfad einer Duplikatgruppe behält.

Dieses Lab setzt binäres Lesen und SHA-256 aus dem vorherigen Lab sowie Java-Klassen, Listen und Ausnahmen voraus. Es beginnt unabhängig mit einem fertigen Hash und einer neuen Kopie der Galerie. Du implementierst die Gruppierung; Verzeichnis-, Berichts- und Startadapter sind vorbereitet. Die Autorenschätzung beträgt 55 Minuten.

Jeden Kandidaten mit echten Bytes bestätigen

In diesem Schritt machst du aus Kandidatenfakten vollständige Duplikatgruppen. Öffne WebIDE, wähle Terminal > New Terminal und wechsle in den unabhängigen Arbeitsbereich:

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

Der echte PNG-Dekodierer zeigt erneut gleiche Bytes unter verschiedenen Namen und verschiedene Bytes gleicher Größe. Der Starter verwendet bei jedem Lauf einen neuen temporären Build; Compiler-/JVM-Unterprozesse verwenden C.UTF-8 und explizite UTF-8-Einstellungen für Quelltext und Text. Deine Terminaleinstellungen bleiben unverändert.

Lies den öffentlichen Adapter und die Ausgangsimplementierung:

cat src/Digester.java src/Finder.java

Finder.find(Path root, Digester candidates) muss eine Liste von Duplikatgruppen zurückgeben. Jede enthält alle Mitgliedspfade relativ zu root. root.relativize(path) bildet diesen relativen Pfad; root.resolve(relative) findet ihn zum erneuten Lesen. sameBytes(left, right) vergleicht Inhalte statt Namen. Behalte diese öffentlichen Einstiegspunkte bei; interne Hilfsmethoden und Sammlungen dürfen anders organisiert sein.

Die folgende Implementierung ordnet über eine Map einem Kandidatenschlüssel seine möglichen Gruppen zu. Der Schlüssel kombiniert Größe und Hash. containsKey prüft, ob ein Schlüssel existiert, put legt seinen Anfangswert an und get ruft ihn ab. Jeder Schlüssel enthält eine Liste bytebestätigter Gruppen, weil unterschiedliche Inhalte denselben Kandidatenfingerabdruck besitzen könnten. List<List<Path>> bezeichnet eine Liste, deren Elemente Pfadlisten sind.

Vergleiche jede Datei mit dem repräsentativen Pfad jeder vorhandenen Gruppe in diesem Bucket. Eine echte Übereinstimmung ergänzt den neuen Pfad; andernfalls entsteht eine weitere Untergruppe. placed merkt, ob der aktuelle Pfad einer Gruppe zugeordnet wurde; break beendet danach die innere Suche, damit jeder Pfad nur einmal hinzukommt. Nur Gruppen mit mindestens zwei Pfaden erscheinen im Bericht. Höre nicht bei zwei Mitgliedern auf: Auch eine dritte Kopie ist ein eigener zu prüfender Pfad.

sameBytes lehnt zunächst unterschiedliche Längen ab und liest danach beide Dateien. read() liefert ein Byte als Ganzzahl von 0 bis 255 oder am Ende -1. Ein abweichendes Byte liefert false; ein gemeinsam erreichtes Ende liefert true. Beide Streams werden durch try-with-resources geschlossen. Einzelbyte-Lesezugriffe halten dieses kleine Kursbeispiel verständlich; gepufferte oder vollständige Arrayvergleiche sind gültige Alternativen mit eigenen Speicher- und Leistungsabwägungen.

Die Zuordnung before verbindet jeden Path mit seinen ursprünglichen Metadaten. before.keySet() liefert diese Pfadschlüssel für die abschließende Änderungsprüfung; buckets.values() liefert die gespeicherten Listen von Kandidatenuntergruppen zum Zusammensetzen des Ergebnisses. Der Attributadapter erfasst vor dem Lesen Größe, Änderungszeit und Dateiidentität und lehnt erkannte Änderungen ab. Er sperrt oder friert den Baum nicht ein. Halte Eingaben während des Scans stabil; nebenläufige Schreibzugriffe, symbolische Links und Spezialdateien werden nicht unterstützt. Jeder gewöhnliche Dateipfad zählt, auch verschiedene Hardlink-Pfade zu derselben Datei. Die Dateiidentität dient nur der Änderungserkennung, niemals dem Verwerfen eines Pfades.

Ersetze src/Finder.java. Wie im vorherigen Lab schreibt das Here-Document mit Anführungszeichen den Text unverändert; das abschließende JAVA steht allein in seiner Zeile. > ersetzt nur diese Quelldatei.

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

Das vorbereitete Main ruft dieselbe Methode find mit new Sha256() auf. Tests können einen anderen Digester übergeben, der für unterschiedliche Dateien denselben Kandidatenwert liefert. Das ist ein kontrollierter Kandidatenkollisionstest, keine echte SHA-256-Kollision; der abschließende Bytevergleich muss verschiedene Inhalte weiterhin trennen. Eine nur vorhandene, nie aufgerufene Vergleichsmethode reicht nicht.

Erzeuge den echten Bericht außerhalb der gescannten Wurzel:

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

Erwarte Scan complete: 3 duplicate groups. Schlägt die Kompilierung fehl, korrigiere die genannte Zeile in src/Finder.java, speichere und starte neu; prüfe Klammern und Methodensignaturen, bevor du Daten veränderst.

Die ganze Gruppe lesen und nach Änderungen neu scannen

In diesem Schritt liest du den fertigen Bericht und beobachtest, wie er den aktuellen Bytes folgt. Arbeite im selben integrierten WebIDE-Terminal unter /home/labex/project/duplicates weiter.

cat reports/groups.tsv

Die UTF-8-TSV-Kopfzeile enthält GROUP und danach PATH. Jede Zeile bezeichnet einen relativen Pfad; gleiche Gruppenkennungen verbinden gleiche Inhalte. Die Kennungen sind keine Dateiidentitäten, ihre Reihenfolge ist unwichtig. Die rote Bildgruppe muss alle drei Pfade enthalten, die beiden leeren Dateien bilden eine gültige Gruppe und die zwei Binärdateien mit teilweise gefülltem Endpuffer eine weitere. Das blaue Bild und die gleichnamigen readme-Dateien mit verschiedenen Inhalten sind keine Duplikate. Jedes Mitglied findest du im Explorer unter duplicates/data/gallery; keine Eingabe wird entfernt.

Probiere eine Änderung mit entbehrlichen Kopien in work, ohne die Galerieoriginale anzutasten. mkdir -p erstellt bei Bedarf das Übungsverzeichnis. cp kopiert eine Datei an einen neuen Pfad.

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

Erwarte eine Duplikatgruppe. Jetzt schreibt printf den Text in Anführungszeichen mit seinem Zeilenumbruch; > ersetzt nur die zweite Übungsdatei. Sie enthält kein PNG mehr; ihre Endung beweist daher nicht ihren Inhalt.

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

Erwarte null Duplikatgruppen. Der Bericht besitzt weiterhin seine Kopfzeile:

cat reports/practice-after.tsv

Ein erfolgreiches leeres Ergebnis unterscheidet sich von einem fehlgeschlagenen Scan. Der nächste Befehl verlangt absichtlich einen Bericht innerhalb des Eingabebaums. Erwarte Scan failed, Exitstatus 2 und keine neue Eingabedatei:

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

Korrigiere den Ausgabepfad und erzeuge den Galeriebericht erneut:

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

Der vorbereitete Berichtsadapter schreibt eine neue temporäre Nachbardatei und ersetzt nach erfolgreichem Scan den Ausgabepfad. Er öffnet keinen bestehenden Hardlink zum Schreiben; ein Berichtspfad kann deshalb über diesen Link keine Originalbytes überschreiben. Dies garantiert keine Dauerhaftigkeit bei Abstürzen. Scheitert das Lesen, scheitert der Befehl und ein älterer Bericht belegt nicht den aktuellen Lauf. Überspringe niemals stillschweigend eine Datei und melde Erfolg.

Die Abschlussprüfung führt deinen aktuellen Quelltext auf isolierten Testdaten aus und liest den tatsächlichen Galeriebericht. Sie prüft vollständige Gruppen, aktuellen Inhalt, Fehler und erhaltene Originale. Ein früher korrekter Bericht entschuldigt keinen später beschädigten Quelltext.

Zusammenfassung

Du hast Kandidaten über Größe und SHA-256 eingegrenzt, echte Bytes bestätigt, jeden Duplikatpfad erhalten und den Bericht nach einer tatsächlichen Änderung neu erzeugt. Ein Erfolg mit null Gruppen und ein Scanfehler haben nun unterschiedliche Bedeutungen.