224 lines
7.9 KiB
C#
224 lines
7.9 KiB
C#
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<FinderCommand.Settings>
|
|
{
|
|
public sealed class Settings : CommandSettings
|
|
{
|
|
[Description("Paths to search.")]
|
|
[CommandArgument(0, "<searchPaths>")]
|
|
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<Dictionary<string, Redundancy>>(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;
|
|
}
|
|
}
|
|
}
|