2024-04-16 20:33:16 +01:00
|
|
|
using System.Collections.Concurrent;
|
2024-04-16 20:15:07 +01:00
|
|
|
using System.Text;
|
2024-04-17 16:11:01 +01:00
|
|
|
using Humanizer;
|
2024-04-16 20:15:07 +01:00
|
|
|
using Spectre.Console;
|
|
|
|
using Spectre.Console.Cli;
|
|
|
|
|
|
|
|
namespace FindDuplicates;
|
|
|
|
|
|
|
|
internal sealed class ListCommand : AsyncCommand<ListSettings>
|
|
|
|
{
|
2024-04-17 14:35:04 +01:00
|
|
|
private readonly ConcurrentDictionary<string, ConcurrentBag<FileInfo>> _fileHashMap = new();
|
2024-04-16 20:15:07 +01:00
|
|
|
|
|
|
|
public override async Task<int> ExecuteAsync(CommandContext context, ListSettings settings)
|
|
|
|
{
|
|
|
|
var inputDirectory = new DirectoryInfo(settings.InputPath);
|
|
|
|
if (!inputDirectory.Exists)
|
|
|
|
{
|
|
|
|
AnsiConsole.MarkupLine($"[red]{inputDirectory} does not exist![/]");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
AnsiConsole.MarkupLineInterpolated($"Searching [cyan]{inputDirectory.FullName}[/]");
|
|
|
|
AnsiConsole.MarkupLine($"Recursive mode is {(settings.Recursive ? "[green]ON" : "[red]OFF")}[/]");
|
2024-04-17 16:11:01 +01:00
|
|
|
AnsiConsole.MarkupLine($"Using hash algorithm [cyan]{settings.Algorithm.Humanize()}[/]");
|
2024-04-16 20:15:07 +01:00
|
|
|
|
2024-04-16 20:59:43 +01:00
|
|
|
await AnsiConsole.Status()
|
|
|
|
.StartAsync("Waiting to hash files...", DoHashWaitAsync)
|
|
|
|
.ConfigureAwait(false);
|
2024-04-16 20:15:07 +01:00
|
|
|
|
|
|
|
AnsiConsole.WriteLine();
|
|
|
|
|
|
|
|
int duplicates = 0;
|
2024-04-17 14:35:04 +01:00
|
|
|
foreach ((string hash, ConcurrentBag<FileInfo> files) in _fileHashMap)
|
2024-04-16 20:15:07 +01:00
|
|
|
{
|
|
|
|
int fileCount = files.Count;
|
|
|
|
|
2024-04-17 16:00:46 +01:00
|
|
|
if (fileCount <= 1)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
duplicates += fileCount;
|
|
|
|
AnsiConsole.MarkupLineInterpolated($"Found [cyan]{fileCount}[/] identical files");
|
|
|
|
AnsiConsole.MarkupLineInterpolated($"{settings.Algorithm.Humanize()} [green]{hash}[/]:");
|
2024-04-16 20:15:07 +01:00
|
|
|
|
2024-04-17 16:00:46 +01:00
|
|
|
foreach (FileInfo file in files)
|
|
|
|
AnsiConsole.MarkupLineInterpolated($"- {file.FullName}");
|
2024-04-16 20:15:07 +01:00
|
|
|
|
2024-04-17 16:00:46 +01:00
|
|
|
AnsiConsole.WriteLine();
|
2024-04-16 20:15:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (duplicates == 0)
|
|
|
|
AnsiConsole.MarkupLine("[green]No duplicates found![/]");
|
|
|
|
else
|
|
|
|
AnsiConsole.MarkupLineInterpolated($"[yellow]Found [cyan]{duplicates}[/] duplicates![/]");
|
|
|
|
|
|
|
|
return 0;
|
2024-04-16 20:59:43 +01:00
|
|
|
|
|
|
|
async Task DoHashWaitAsync(StatusContext ctx)
|
|
|
|
{
|
|
|
|
await WaitForHashCompletionAsync(settings, inputDirectory, ctx);
|
|
|
|
}
|
2024-04-16 20:15:07 +01:00
|
|
|
}
|
|
|
|
|
2024-04-16 20:59:43 +01:00
|
|
|
private async Task WaitForHashCompletionAsync(ListSettings settings,
|
|
|
|
DirectoryInfo inputDirectory,
|
|
|
|
StatusContext ctx)
|
2024-04-16 20:15:07 +01:00
|
|
|
{
|
|
|
|
var tasks = new List<Task>();
|
2024-04-16 20:59:43 +01:00
|
|
|
SearchDuplicates(inputDirectory, settings, tasks);
|
|
|
|
await Task.Run(() =>
|
|
|
|
{
|
|
|
|
int incompleteTasks;
|
|
|
|
do
|
|
|
|
{
|
|
|
|
incompleteTasks = tasks.Count(t => !t.IsCompleted);
|
|
|
|
ctx.Status($"Waiting to hash {incompleteTasks} {(incompleteTasks == 1 ? "file" : "files")}...");
|
|
|
|
ctx.Refresh();
|
|
|
|
} while (tasks.Count > 0 && incompleteTasks > 0);
|
|
|
|
|
|
|
|
ctx.Status("Hash complete");
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void SearchDuplicates(DirectoryInfo inputDirectory, ListSettings settings, ICollection<Task> tasks)
|
|
|
|
{
|
2024-04-16 20:15:07 +01:00
|
|
|
var directoryStack = new Stack<DirectoryInfo>([inputDirectory]);
|
|
|
|
while (directoryStack.Count > 0)
|
|
|
|
{
|
|
|
|
DirectoryInfo currentDirectory = directoryStack.Pop();
|
|
|
|
string relativePath = Path.GetRelativePath(inputDirectory.FullName, currentDirectory.FullName);
|
|
|
|
if (relativePath != ".")
|
|
|
|
AnsiConsole.MarkupLineInterpolated($"Searching [cyan]{relativePath}[/]");
|
|
|
|
|
2024-04-17 14:24:54 +01:00
|
|
|
AddChildDirectories(settings, currentDirectory, directoryStack);
|
2024-04-16 20:15:07 +01:00
|
|
|
|
2024-04-16 21:00:02 +01:00
|
|
|
try
|
2024-04-16 20:15:07 +01:00
|
|
|
{
|
2024-04-16 21:00:02 +01:00
|
|
|
foreach (FileInfo file in currentDirectory.EnumerateFiles())
|
|
|
|
{
|
|
|
|
string relativeFilePath = Path.GetRelativePath(inputDirectory.FullName, file.FullName);
|
|
|
|
AnsiConsole.MarkupLineInterpolated($"Checking hash for [cyan]{relativeFilePath}[/]");
|
2024-04-17 14:17:56 +01:00
|
|
|
tasks.Add(Task.Run(() => ProcessFile(file, settings)));
|
2024-04-16 21:00:02 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (Exception ex)
|
|
|
|
{
|
|
|
|
AnsiConsole.MarkupLineInterpolated($"[red]Error:[/] {ex.Message}");
|
2024-04-16 20:15:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-17 14:17:56 +01:00
|
|
|
private void ProcessFile(FileInfo file, ListSettings settings)
|
2024-04-16 20:15:07 +01:00
|
|
|
{
|
2024-04-17 16:11:01 +01:00
|
|
|
Span<byte> buffer = stackalloc byte[settings.Algorithm.GetByteCount()];
|
2024-04-16 21:00:02 +01:00
|
|
|
try
|
|
|
|
{
|
|
|
|
using FileStream stream = file.OpenRead();
|
|
|
|
using BufferedStream bufferedStream = new BufferedStream(stream, 1048576 /* 1MB */);
|
2024-04-17 16:11:01 +01:00
|
|
|
settings.Algorithm.HashData(bufferedStream, buffer);
|
2024-04-16 21:00:02 +01:00
|
|
|
string hash = ByteSpanToString(buffer);
|
2024-04-17 14:17:56 +01:00
|
|
|
if (settings.Verbose)
|
|
|
|
AnsiConsole.WriteLine($"{file.FullName} ->\n {hash}");
|
2024-04-16 20:15:07 +01:00
|
|
|
|
2024-04-17 14:35:04 +01:00
|
|
|
ConcurrentBag<FileInfo> cache = _fileHashMap.GetOrAdd(hash, _ => []);
|
|
|
|
cache.Add(file);
|
2024-04-16 21:00:02 +01:00
|
|
|
}
|
|
|
|
catch (Exception ex)
|
|
|
|
{
|
|
|
|
AnsiConsole.MarkupLineInterpolated($"[red]Error:[/] {ex.Message}");
|
|
|
|
}
|
2024-04-16 20:15:07 +01:00
|
|
|
}
|
|
|
|
|
2024-04-17 14:24:54 +01:00
|
|
|
private static void AddChildDirectories(ListSettings settings, DirectoryInfo directory, Stack<DirectoryInfo> stack)
|
|
|
|
{
|
|
|
|
if (!settings.Recursive)
|
|
|
|
return;
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
foreach (DirectoryInfo childDirectory in directory.EnumerateDirectories())
|
|
|
|
stack.Push(childDirectory);
|
|
|
|
}
|
|
|
|
catch (Exception ex)
|
|
|
|
{
|
|
|
|
AnsiConsole.MarkupLineInterpolated($"[red]Error:[/] {ex.Message}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-16 20:15:07 +01:00
|
|
|
private static string ByteSpanToString(ReadOnlySpan<byte> buffer)
|
|
|
|
{
|
2024-04-17 16:11:01 +01:00
|
|
|
var builder = new StringBuilder(buffer.Length * 2);
|
2024-04-16 20:15:07 +01:00
|
|
|
|
|
|
|
foreach (byte b in buffer)
|
|
|
|
builder.Append($"{b:X2}");
|
|
|
|
|
|
|
|
return builder.ToString();
|
|
|
|
}
|
|
|
|
}
|