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; } } CancellationTokenSource cancellation = new CancellationTokenSource(); Task task = null; Finder finder = new Finder(); Settings settings = null; public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) { finder.cancellation = cancellation.Token; this.settings = settings; var extensions = settings.Extensions.Split(",", StringSplitOptions.RemoveEmptyEntries); int hashingTasks = 0; int hashingTasksFinished = 0; // Register the ProcessExit event to save redundancies on exit AppDomain.CurrentDomain.UnhandledException += (sender, e) => { End(); }; // Register the CancelKeyPress event to handle Ctrl+C Console.CancelKeyPress += (sender, e) => { cancellation.Cancel(); // Cancel the ongoing tasks e.Cancel = true; // Prevent the process from terminating immediately Global.WriteLine("[yellow]Ctrl+C detected. Saving redundancies before exiting...[/]"); End(); }; // 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; } Global.WriteLine($"[yellow]Resumed from existing output file: [/]'{settings.OutputPath}'"); Global.WriteLine($"[yellow]Loaded {finder.Redundancies.Count} redundancies from the file.[/]"); } } catch (Exception ex) { Global.WriteLine($"[red]Failed to load existing output file: {Markup.Escape(ex.Message)}[/]"); } } finder.FileError += (sender, e) => { if (e.Exception is UnauthorizedAccessException) { if (settings.Verbose) { Global.WriteLine($"[red]Access denied to file: [/]{e.Path}. Skipping. Error: [red]{Markup.Escape(e.Exception.Message)}[/]"); } } else { if (settings.Verbose) { Global.WriteLine($"[red] Error processing file:\n[/]Path:{e.Path}\n[red]{Markup.Escape(e.Exception.Message)}[/]"); } } }; finder.DirectoryError += (sender, e) => { if (e.Exception is UnauthorizedAccessException) { if (settings.Verbose) { Global.WriteLine($"[red]Access denied to directory: [/]{e.Path}. Skipping. Error: [red]{Markup.Escape(e.Exception.Message)}[/]"); } } else { if (settings.Verbose) { Global.WriteLine($"[red] Error processing directory:\n[/]Path:{e.Path}\n[red]{Markup.Escape(e.Exception.Message)}[/]"); } } }; finder.ProcessingFile += (sender, e) => { if (settings.Verbose) { Global.WriteLine($"[green]Processing file: [/]{e.Path}"); } }; try { finder.TaskStarted += (sender, e) => { hashingTasks++; if (settings.Verbose) { Global.WriteLine($"[green]Task started: [/]{e}"); } }; finder.FileFound += (sender, e) => { if (settings.Verbose) { Global.WriteLine($"[green]File found: [/]{e.FilePath} [darkgreen]{Global.GetSizeFormat((ulong)e.Size)}[/]"); } hashingTasksFinished++; }; task = Task.Run(() => { finder.FindRedundancies(settings.SearchPaths, extensions); }, cancellation.Token); try { task.Wait(cancellation.Token); } catch (Exception) { } finally { End(); } } catch (Exception e) { Global.WriteLine($"[red] Error:\n[/]{e.Message}"); } return 0; } private void End() { SaveRedundancies(finder, settings.OutputPath); ulong totalSize = finder.Redundancies.Select(x => (ulong)x.Value.FileSize).Aggregate((a, b) => a + b); string sizeFormat = Global.GetSizeFormat(totalSize); Global.WriteLine($"Total Size: [green]{sizeFormat}[/]"); Environment.Exit(0); // Exit the application gracefully } private void SaveRedundancies(Finder finder, string outputPath) { try { // Check if path is relative or absolute if (!Path.IsPathRooted(outputPath)) { outputPath = Path.Combine(Directory.GetCurrentDirectory(), outputPath); } string json = null; // AnsiConsole.Status() //.Start(Global.Format($"[yellow]Writing to [/]'{outputPath}' [yellow] This may take a long time.[/]"), ctx => //{ // ctx.Spinner(Spinner.Known.Clock); // ctx.SpinnerStyle = Style.Parse("yellow bold"); // Thread.Sleep(1000); // lock (finder.Redundancies) // { // } //}); json = JsonConvert.SerializeObject(finder.Redundancies, Formatting.Indented); File.WriteAllText(outputPath, json); Global.WriteLine($"[yellow]Wrote [/]{finder.Redundancies.Count}[yellow] redundancies to [/]'{outputPath}'"); } catch (Exception ex) { Global.WriteLine($"[red]Failed to save redundancies: {ex.Message}[/]"); } } } }