using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using Newtonsoft.Json; using RedundancyFinder; using Spectre.Console; using Spectre.Console.Cli; namespace RedundancyFinderCLI { internal sealed class FinderCommand : Command { public sealed class Settings : CommandSettings { [Description("Paths to search.")] [CommandArgument(0, "")] public string[] SearchPaths { get; init; } [Description("File extensions to search for. Comma separated.")] [CommandOption("-e|--extensions")] [DefaultValue(".jpg,.webp,.raw,.pdf,.xsl,.xslx,.doc,.docx,.txt,.jpeg,.mov,.mp4,.mp3,.wav,.bmp,.gif,.png,.cu,.mid,.msb ,.mov,.avi,.wmv,.flv,.m4v,.bak ,.cpr ,.xml,.psd")] public string? Extensions { get; init; } [Description("Show all information.")] [CommandOption("-v|--verbose")] [DefaultValue(false)] public bool Verbose { get; init; } [Description("Output path.")] [CommandOption("-o|--output")] [DefaultValue("redundancies.json")] public string? OutputPath {get; init;} } public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) { var extensions = settings.Extensions.Split(",", StringSplitOptions.RemoveEmptyEntries); Finder finder = new Finder(); int hashingTasks = 0; int hashingTasksFinished = 0; // Register the ProcessExit event to save redundancies on exit AppDomain.CurrentDomain.ProcessExit += (sender, e) => { SaveRedundancies(finder, settings.OutputPath); }; // Load existing redundancies if the output file exists if (File.Exists(settings.OutputPath)) { try { var existingData = File.ReadAllText(settings.OutputPath); var existingRedundancies = JsonConvert.DeserializeObject>(existingData); if (existingRedundancies != null) { foreach (var entry in existingRedundancies) { finder.Redundancies[entry.Key] = entry.Value; } WriteLine($"[yellow]Resumed from existing output file: [/]'{settings.OutputPath}'"); WriteLine($"[yellow]Loaded {finder.Redundancies.Count} redundancies from the file.[/]"); } } catch (Exception ex) { WriteLine($"[red]Failed to load existing output file: {ex.Message}[/]"); } } finder.FileError += (sender, e) => { if (e.Exception is UnauthorizedAccessException) { if (settings.Verbose) { WriteLine($"[red]Access denied to file: [/]{e.Path}. Skipping. Error: [red]{e.Exception.Message}[/]"); } } else { if (settings.Verbose) { WriteLine($"[red] Error processing file:\n[/]Path:{e.Path}\n[red]{e.Exception.Message}[/]"); } } }; finder.DirectoryError += (sender, e) => { if (e.Exception is UnauthorizedAccessException) { if (settings.Verbose) { WriteLine($"[red]Access denied to directory: [/]{e.Path}. Skipping. Error: [red]{e.Exception.Message}[/]"); } } else { if (settings.Verbose) { WriteLine($"[red] Error processing directory:\n[/]Path:{e.Path}\n[red]{e.Exception.Message}[/]"); } } }; finder.ProcessingFile += (sender, e) => { if (settings.Verbose) { WriteLine($"[green]Processing file: [/]{e.Path}"); } }; try { var p = AnsiConsole.Progress(); p.Start(ctx => { finder.TaskStarted += (sender, e) => { hashingTasks++; if (settings.Verbose) { WriteLine($"[green]Task started: [/]{e}"); } }; finder.FileFound += (sender, e) => { if (settings.Verbose) { WriteLine($"[green]File found: [/]{e.FilePath} {GetSizeFormat((ulong)e.Size)} "); } hashingTasksFinished++; }; finder.FindRedundancies(settings.SearchPaths, extensions); }); SaveRedundancies(finder, settings.OutputPath); ulong totalSize = finder.Redundancies.Select(x => (ulong)x.Value.FileSize).Aggregate((a, b) => a + b); string sizeFormat = GetSizeFormat(totalSize); WriteLine($"Total Size: [green]{sizeFormat}[/]"); if (settings.Verbose) { foreach (var redundancy in finder.Redundancies.Values) { AnsiConsole.WriteLine($"Hash: {redundancy.Hash}"); AnsiConsole.WriteLine("Paths:"); foreach (var path in redundancy.Paths) { AnsiConsole.WriteLine(path); } AnsiConsole.WriteLine(); } } } catch (Exception e) { WriteLine($"[red] Error:\n[/]{e.Message}"); } return 0; } private void SaveRedundancies(Finder finder, string outputPath) { try { var json = JsonConvert.SerializeObject(finder.Redundancies, Formatting.Indented); // Check if path is relative or absolute if (!Path.IsPathRooted(outputPath)) { outputPath = Path.Combine(Directory.GetCurrentDirectory(), outputPath); } File.WriteAllText(outputPath, json); WriteLine($"[yellow]Wrote [/]{finder.Redundancies.Count}[yellow] redundancies to [/]'{outputPath}'"); } catch (Exception ex) { WriteLine($"[red]Failed to save redundancies: {ex.Message}[/]"); } } private void WriteLine(string v) { string now = Markup.Escape($"[{DateTime.Now.ToString("HH:mm:ss")}]"); AnsiConsole.MarkupLine($"[gray]{now}[/] {v}"); } private static string GetSizeFormat(ulong totalSize) { string sizeUnit = "B"; while (totalSize > 1024) { totalSize /= 1024; sizeUnit = sizeUnit switch { "B" => "KB", "KB" => "MB", "MB" => "GB", "GB" => "TB", _ => sizeUnit }; } string sizeFormat = $"{totalSize:.##} {sizeUnit}"; return sizeFormat; } } }