FindDuplicateFiles/DuplicateFinder.java
2026-08-29 04:31:57 -04:00

322 lines
11 KiB
Java

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
/**
* DuplicateFinder - Command line program to find duplicate files using MD5 hashing.
*
* Uses a Hashtable where MD5 hash (String) -> List of file paths (String).
* Files sharing the same MD5 are duplicates (identical content).
*
* Usage:
* java DuplicateFinder [options] <directory>
* java DuplicateFinder --gui (launches Swing GUI)
*
* Options:
* -r, --recursive Recurse into subdirectories (default: true)
* --no-recursive Do not recurse
* -h, --help Show help
* --gui Launch Swing GUI
*
* Standalone CLI - can be used without GUI.
* GUI wrapper is in DuplicateFinderGUI.java
*/
public class DuplicateFinder {
private static final int BUFFER_SIZE = 8192;
/**
* Compute MD5 hash for a file and return as hex string.
* @param file file to hash
* @return hex MD5 string
* @throws IOException if read fails
*/
public static String computeMD5(File file) throws IOException {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 not available", e);
}
InputStream is = null;
try {
is = new FileInputStream(file);
byte[] buffer = new byte[BUFFER_SIZE];
int read;
while ((read = is.read(buffer)) != -1) {
md.update(buffer, 0, read);
}
} finally {
if (is != null) {
try { is.close(); } catch (IOException ignored) {}
}
}
byte[] digest = md.digest();
StringBuilder sb = new StringBuilder(32);
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
/**
* Collect all files under root.
* @param root starting directory or file
* @param recursive if true, walk subdirectories
* @return list of files (not directories)
*/
public static List<File> collectFiles(File root, boolean recursive) {
List<File> files = new ArrayList<File>();
if (root == null || !root.exists()) {
return files;
}
if (root.isFile()) {
files.add(root);
return files;
}
File[] children = root.listFiles();
if (children == null) {
System.err.println("Warning: cannot list directory: " + root.getAbsolutePath());
return files;
}
for (File child : children) {
if (child.isFile()) {
files.add(child);
} else if (child.isDirectory() && recursive) {
// avoid following symlinks that cause cycles? isFile/isDirectory follows links but
// we simply recurse; deep cycles unlikely on Windows
files.addAll(collectFiles(child, true));
} else if (child.isDirectory() && !recursive) {
// skip
}
}
return files;
}
/**
* Build Hashtable mapping MD5 hash -> list of file paths.
* This is the core hashtable required by the specification.
*
* @param root directory to search
* @param recursive recurse flag
* @return Hashtable where key = md5 hex, value = list of absolute paths
*/
public static Hashtable<String, List<String>> buildHashTable(File root, boolean recursive) {
Hashtable<String, List<String>> table = new Hashtable<String, List<String>>();
List<File> files = collectFiles(root, recursive);
for (File f : files) {
try {
// skip zero-length? No, include - empty files duplicates too
// skip unreadable
if (!f.canRead()) {
System.err.println("Warning: cannot read file: " + f.getAbsolutePath());
continue;
}
String md5 = computeMD5(f);
List<String> list = table.get(md5);
if (list == null) {
list = new ArrayList<String>();
table.put(md5, list);
}
list.add(f.getAbsolutePath());
} catch (IOException ex) {
System.err.println("Warning: failed to hash " + f.getAbsolutePath() + " : " + ex.getMessage());
}
}
return table;
}
/**
* Filter hashtable to only entries with duplicates (more than one file per hash).
* @param table full table
* @return filtered table containing only duplicates
*/
public static Hashtable<String, List<String>> filterDuplicates(Hashtable<String, List<String>> table) {
Hashtable<String, List<String>> dupes = new Hashtable<String, List<String>>();
Enumeration<String> keys = table.keys();
while (keys.hasMoreElements()) {
String hash = keys.nextElement();
List<String> files = table.get(hash);
if (files != null && files.size() > 1) {
dupes.put(hash, files);
}
}
return dupes;
}
/**
* Convenience: find duplicates directly.
*/
public static Hashtable<String, List<String>> findDuplicates(File root, boolean recursive) {
Hashtable<String, List<String>> table = buildHashTable(root, recursive);
return filterDuplicates(table);
}
// Callback interface for GUI progress reporting
public interface ProgressCallback {
void onProgress(int current, int total, String file);
boolean isCancelled();
}
/**
* Build hashtable with progress callback (for GUI).
*/
public static Hashtable<String, List<String>> buildHashTableWithProgress(File root, boolean recursive, ProgressCallback cb) {
Hashtable<String, List<String>> table = new Hashtable<String, List<String>>();
List<File> files = collectFiles(root, recursive);
int total = files.size();
int current = 0;
for (File f : files) {
if (cb != null && cb.isCancelled()) {
break;
}
current++;
if (cb != null) {
cb.onProgress(current, total, f.getAbsolutePath());
}
try {
if (!f.canRead()) {
System.err.println("Warning: cannot read file: " + f.getAbsolutePath());
continue;
}
String md5 = computeMD5(f);
List<String> list = table.get(md5);
if (list == null) {
list = new ArrayList<String>();
table.put(md5, list);
}
list.add(f.getAbsolutePath());
} catch (IOException ex) {
System.err.println("Warning: failed to hash " + f.getAbsolutePath() + " : " + ex.getMessage());
}
}
return table;
}
public static void printDuplicates(Hashtable<String, List<String>> dupes) {
if (dupes.isEmpty()) {
System.out.println("No duplicate files found.");
return;
}
// Sort hashes for deterministic output
List<String> hashes = new ArrayList<String>(dupes.keySet());
Collections.sort(hashes);
int group = 1;
int totalDuplicateFiles = 0;
for (String hash : hashes) {
List<String> files = dupes.get(hash);
Collections.sort(files);
totalDuplicateFiles += files.size();
System.out.println("=== Duplicate Group " + group + " ===");
System.out.println("MD5: " + hash + " (" + files.size() + " files)");
for (String path : files) {
File f = new File(path);
System.out.println(" " + path + " (" + f.length() + " bytes)");
}
System.out.println();
group++;
}
System.out.println("Summary: " + dupes.size() + " duplicate group(s), " + totalDuplicateFiles + " files involved.");
}
public static void printUsage() {
System.out.println("DuplicateFinder - Find duplicate files via MD5 hashtable");
System.out.println();
System.out.println("Usage:");
System.out.println(" java DuplicateFinder [options] <directory>");
System.out.println(" java DuplicateFinder --gui");
System.out.println();
System.out.println("Options:");
System.out.println(" -r, --recursive Recurse into subdirectories (default)");
System.out.println(" --no-recursive Only check top-level directory");
System.out.println(" -h, --help Show this help");
System.out.println(" --gui Launch Swing GUI");
System.out.println();
System.out.println("Examples:");
System.out.println(" java DuplicateFinder C:\\Users\\Me\\Documents");
System.out.println(" java DuplicateFinder --no-recursive C:\\Temp");
System.out.println(" java DuplicateFinder --gui");
System.out.println(" java DuplicateFinderGUI (directly launches GUI)");
}
public static void main(String[] args) {
boolean recursive = true;
String dirArg = null;
boolean showHelp = false;
boolean launchGui = false;
for (String arg : args) {
if (arg.equals("-h") || arg.equals("--help")) {
showHelp = true;
} else if (arg.equals("-r") || arg.equals("--recursive")) {
recursive = true;
} else if (arg.equals("--no-recursive")) {
recursive = false;
} else if (arg.equals("--gui")) {
launchGui = true;
} else if (arg.startsWith("-")) {
System.err.println("Unknown option: " + arg);
printUsage();
System.exit(1);
} else {
if (dirArg == null) {
dirArg = arg;
} else {
System.err.println("Too many positional arguments: " + arg);
printUsage();
System.exit(1);
}
}
}
if (launchGui) {
// Delegate to GUI
DuplicateFinderGUI.main(new String[0]);
return;
}
if (showHelp) {
printUsage();
return;
}
if (dirArg == null) {
System.err.println("Error: no directory specified.");
printUsage();
System.exit(1);
}
File root = new File(dirArg);
if (!root.exists()) {
System.err.println("Error: path does not exist: " + dirArg);
System.exit(1);
}
System.out.println("Scanning: " + root.getAbsolutePath());
System.out.println("Recursive: " + recursive);
System.out.println();
long start = System.currentTimeMillis();
Hashtable<String, List<String>> dupes = findDuplicates(root, recursive);
long elapsed = System.currentTimeMillis() - start;
printDuplicates(dupes);
System.out.println("Scanned in " + elapsed + " ms.");
}
}