Compare commits

..

17 Commits

Author SHA1 Message Date
4da2ed9f3d added message checking file 2025-05-11 12:24:08 +02:00
101d128a42 release 0.7 2025-05-11 03:08:31 +02:00
82160b12e1 implemented delteingnonexistentFiles from DB 2025-05-11 03:06:23 +02:00
e0ed86899c release 0.6 2025-05-11 01:01:44 +02:00
d6480c8e0e implemented sanitize command 2025-05-11 01:01:09 +02:00
3834c2fba0 release 0.5 2025-05-11 00:20:45 +02:00
f8d8c4461f Delete command imlpemented
Cancellation
Formatting
2025-05-11 00:19:58 +02:00
30738eef24 Implemented DeleteCommand 2025-05-10 22:21:11 +02:00
f82beb62ed released 0.4 2025-05-10 21:15:04 +02:00
0bb5fbdf87 implemented analyze command 2025-05-10 21:13:09 +02:00
45796d9bb2 release 3.0 2025-05-10 19:20:57 +02:00
1bb7d461e0 reload implemented 2025-05-10 19:17:29 +02:00
63c1c88393 save redundancies on exit 2025-05-10 18:57:49 +02:00
5800f2229f Added Timestamp 2025-05-10 18:43:43 +02:00
b66e7dd4ad Released 0.2 2025-05-10 17:08:36 +02:00
a84450cd5e always get absolute outputpath 2025-05-10 15:44:12 +02:00
e6e259ed47 remove tasks
fixed ignoring hidden folders
2025-05-10 14:04:08 +02:00
17 changed files with 595 additions and 139 deletions

BIN
0.2.zip Normal file

Binary file not shown.

BIN
0.3.zip Normal file

Binary file not shown.

BIN
0.4.zip Normal file

Binary file not shown.

BIN
0.5.zip Normal file

Binary file not shown.

BIN
0.6.zip Normal file

Binary file not shown.

BIN
0.7.zip Normal file

Binary file not shown.

View File

@ -4,7 +4,7 @@ namespace RedundancyFinder
{ {
public class Finder public class Finder
{ {
List<Task> tasks = new List<Task>(); public CancellationToken cancellation;
Dictionary<string, Redundancy> redundancies = new Dictionary<string, Redundancy>(); Dictionary<string, Redundancy> redundancies = new Dictionary<string, Redundancy>();
@ -12,6 +12,8 @@ namespace RedundancyFinder
string[] extensions; string[] extensions;
List<string> ignorePaths = new List<string>();
public event EventHandler<DirectoryErrorEventArgs>? DirectoryError; public event EventHandler<DirectoryErrorEventArgs>? DirectoryError;
public event EventHandler<FileErrorEventArgs>? FileError; public event EventHandler<FileErrorEventArgs>? FileError;
public event EventHandler<FileFoundEventArgs>? FileFound; public event EventHandler<FileFoundEventArgs>? FileFound;
@ -19,9 +21,14 @@ namespace RedundancyFinder
public event EventHandler<ProcessingFileEventArgs>? ProcessingFile; public event EventHandler<ProcessingFileEventArgs>? ProcessingFile;
public void FindRedundancies(string[] paths, string[] extensions) public void FindRedundancies(string[] paths, string[] extensions)
{ {
Redundancies?.Values.SelectMany(x => x.Paths).ToList().ForEach(x => ignorePaths.Add(x));
this.extensions = extensions; this.extensions = extensions;
foreach (var path in paths) foreach (var path in paths)
{ {
if (cancellation.IsCancellationRequested)
{
return;
}
if (Directory.Exists(path)) if (Directory.Exists(path))
{ {
ProcessDirectory(path); ProcessDirectory(path);
@ -31,31 +38,40 @@ namespace RedundancyFinder
ProcessFile(path); ProcessFile(path);
} }
} }
// Wait for all tasks to complete
Task.WaitAll(tasks.ToArray());
} }
private void ProcessDirectory(string directoryPath) private void ProcessDirectory(string directoryPath)
{ {
if (cancellation.IsCancellationRequested)
{
return;
}
try try
{ {
// Check if the directory is hidden and skip it if true
var attributes = File.GetAttributes(directoryPath);
if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
{
return;
}
// Process files in the current directory // Process files in the current directory
foreach (var file in Directory.GetFiles(directoryPath)) foreach (var file in Directory.GetFiles(directoryPath))
{ {
if (cancellation.IsCancellationRequested)
{
return;
}
ProcessFile(file); ProcessFile(file);
} }
// Recursively process subdirectories // Recursively process subdirectories
foreach (var subDirectory in Directory.GetDirectories(directoryPath)) foreach (var subDirectory in Directory.GetDirectories(directoryPath))
{ {
if (cancellation.IsCancellationRequested)
{
return;
}
// Check if the directory is hidden and skip it if true
var attributes = File.GetAttributes(subDirectory);
if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
{
continue;
}
try try
{ {
ProcessDirectory(subDirectory); ProcessDirectory(subDirectory);
@ -75,48 +91,48 @@ namespace RedundancyFinder
private void ProcessFile(string filePath) private void ProcessFile(string filePath)
{ {
if (cancellation.IsCancellationRequested)
{
return;
}
if (ignorePaths.Contains(filePath))
{
return;
}
if (!extensions.Contains(Path.GetExtension(filePath))) if (!extensions.Contains(Path.GetExtension(filePath)))
{ {
return; return;
} }
Task task = new(() => //TaskStarted?.Invoke(this, filePath);
{ //ProcessingFile?.Invoke(this, new ProcessingFileEventArgs() { Path = filePath });
ProcessingFile?.Invoke(this, new ProcessingFileEventArgs() { Path = filePath });
try try
{ {
var fileHash = ComputeFileHash(filePath); var fileHash = ComputeFileHash(filePath);
if (fileHash == null) if (fileHash == null)
{ {
return; return;
} }
long fileSize = new FileInfo(filePath).Length; long fileSize = new FileInfo(filePath).Length;
lock (redundancies) lock (Redundancies)
{ {
if (redundancies.ContainsKey(fileHash)) if (!Redundancies.ContainsKey(fileHash))
{ {
return; var redundancy = new Redundancy() { Hash = fileHash, FileSize = fileSize };
} Redundancies.Add(fileHash, redundancy);
}
Redundancies[fileHash].Paths.Add(filePath);
}
FileFound?.Invoke(this, new FileFoundEventArgs(filePath, fileHash, fileSize));
}
catch (Exception ex)
{
FileError?.Invoke(this, new FileErrorEventArgs() { Exception = ex, Path = filePath });
}
if (!redundancies.ContainsKey(fileHash))
{
var redundancy = new Redundancy() { Hash = fileHash, FileSize = fileSize };
redundancies.Add(fileHash, redundancy);
}
redundancies[fileHash].Paths.Add(filePath);
}
FileFound?.Invoke(this, new FileFoundEventArgs(filePath, fileHash, fileSize));
}
catch (Exception ex)
{
FileError?.Invoke(this, new FileErrorEventArgs() { Exception = ex, Path = filePath });
}
});
task.Start();
TaskStarted?.Invoke(this, filePath);
tasks.Add(task);
} }
private static string ComputeFileHash(string filePath) private static string ComputeFileHash(string filePath)

View File

@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Configurations>Debug;Release;Analyze</Configurations>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -0,0 +1,102 @@
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 AnalyzeCommand : Command<AnalyzeCommand.Settings>
{
public sealed class Settings : CommandSettings
{
[Description("Path to analyze.")]
[DefaultValue("redundancies.json")]
[CommandArgument(0, "[path]")]
public string? Path { 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; }
}
public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings)
{
Dictionary<string, Redundancy> redundancies = null;
AnsiConsole.Status()
.Start($"[yellow]Analyzing [/]'{settings.Path}'", ctx =>
{
ctx.Spinner(Spinner.Known.Clock);
ctx.SpinnerStyle = Style.Parse("yellow bold");
Thread.Sleep(1000);
redundancies = JsonConvert.DeserializeObject<Dictionary<string, Redundancy>>(File.ReadAllText(settings.Path));
});
if (redundancies == null)
{
}
var groups = redundancies
.GroupBy(x => Path.GetExtension(x.Value.Paths[0]), x => x.Value)
.ToDictionary(x => x.Key, x => new
{
FileSize = x.Sum(y => y.FileSize),
RedundancySize = x.Sum(y => y.FileSize * (y.Paths.Count - 1)),
RedundancyCount = x.Sum(y => y.Paths.Count) - 1,
RedundantFiles = x.Count(y => y.Paths.Count > 1)
});
// x => new { FileSize = x, RedundancySize = x.Sum()*(x.Count()-1)}
var extensions = settings.Extensions.Split(",", StringSplitOptions.RemoveEmptyEntries);
var table = new Table();
table.AddColumn("Extension");
table.AddColumn(new TableColumn("Files").RightAligned());
table.AddColumn(new TableColumn("Size").RightAligned());
table.AddColumn(new TableColumn("Redundancies").RightAligned());
table.AddColumn(new TableColumn("Redundancies size").RightAligned());
foreach (var extension in extensions.OrderBy(x => x))
{
if (groups.ContainsKey(extension))
{
var size = groups[extension].FileSize;
var sizeFormat = Global.GetSizeFormat((ulong)size);
var redundancySize = groups[extension].RedundancySize;
var redundancySizeFormat = Global.GetSizeFormat((ulong)redundancySize);
table.AddRow(
new Text(extension),
new Markup($"[darkgreen]{groups[extension].RedundantFiles}[/]"),
new Markup($"[green]{sizeFormat}[/]"),
new Markup($"[cyan]{groups[extension].RedundancyCount}[/]"),
new Markup($"[yellow]{redundancySizeFormat}[/]"));
}
}
AnsiConsole.Write(table);
return 0;
}
}
}

View File

@ -0,0 +1,115 @@
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 DeleteCommand : Command<DeleteCommand.Settings>
{
public sealed class Settings : CommandSettings
{
[Description("Paths to Keep.")]
[DefaultValue(new string[] { "H:\\" })]
[CommandArgument(1, "[path]")]
public string[]? FoldersToKeep { get; init; }
[Description("Path to analyze.")]
[DefaultValue("redundancies.json")]
[CommandArgument(0, "[path]")]
public string? Path { 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; }
}
public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings)
{
Global.WriteLine($"[yellow]Analyzing {settings.Path}[/]");
var redundancies = JsonConvert.DeserializeObject<Dictionary<string, Redundancy>>(File.ReadAllText(settings.Path));
var pathsToDelete = new List<string>();
foreach (var redundancy in redundancies.Values)
{
var pathToKeep = redundancy.Paths.FirstOrDefault(x => settings.FoldersToKeep.Any(y => x.StartsWith(y)));
if (pathToKeep != default)
{
foreach (var path in redundancy.Paths)
{
if (path != pathToKeep)
{
pathsToDelete.Add(path);
}
}
}
else if(settings.Verbose)
{
if (redundancy.Paths.Count > 0)
{
Global.WriteLine($"[blue]Skipping [/]'{redundancy.Paths.FirstOrDefault()}'[blue]. No paths to keep![/]");
}
else
{
Global.WriteLine($"[yellow]Skipping [/]'{redundancy.Hash}'[/].[blue] No paths![/]");
}
}
}
foreach (var path in pathsToDelete)
{
AnsiConsole.WriteLine(path);
}
if (pathsToDelete.Count == 0)
{
Global.WriteLine("[yellow]Nothing to delete![/]");
return 0;
}
var confirmation = AnsiConsole.Prompt(
new TextPrompt<bool>("Delete all of the above?")
.AddChoice(true)
.AddChoice(false)
.DefaultValue(true)
.WithConverter(choice => choice ? "y" : "n"));
if (confirmation)
{
foreach (var path in pathsToDelete)
{
try
{
File.Delete(path);
}
catch (Exception e)
{
Global.WriteLine($"[red]Error deleting file: [/]'{path}'\nMessage:\n{e.Message}");
}
Global.WriteLine($"[yellow]Deleted file: [/]'{path}'");
}
}
return 0;
}
}
}

View File

@ -34,23 +34,65 @@ namespace RedundancyFinderCLI
[Description("Output path.")] [Description("Output path.")]
[CommandOption("-o|--output")] [CommandOption("-o|--output")]
[DefaultValue("redundancies.json")] [DefaultValue("redundancies.json")]
public string? OutputPath {get; init;} 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) public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings)
{ {
finder.cancellation = cancellation.Token;
this.settings = settings;
var extensions = settings.Extensions.Split(",", StringSplitOptions.RemoveEmptyEntries); var extensions = settings.Extensions.Split(",", StringSplitOptions.RemoveEmptyEntries);
Finder finder = new Finder();
int hashingTasks = 0; int hashingTasks = 0;
int hashingTasksFinished = 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<Dictionary<string, Redundancy>>(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) => finder.FileError += (sender, e) =>
{ {
@ -58,14 +100,14 @@ namespace RedundancyFinderCLI
{ {
if (settings.Verbose) if (settings.Verbose)
{ {
AnsiConsole.MarkupLine($"[red]Access denied to file: [/]{e.Path}. Skipping. Error: [red]{e.Exception.Message}[/]"); Global.WriteLine($"[red]Access denied to file: [/]{e.Path}. Skipping. Error: [red]{Markup.Escape(e.Exception.Message)}[/]");
} }
} }
else else
{ {
if (settings.Verbose) if (settings.Verbose)
{ {
AnsiConsole.MarkupLine($"[red] Error processing file:\n[/]Path:{e.Path}\n[red]{e.Exception.Message}[/]"); Global.WriteLine($"[red] Error processing file:\n[/]Path:{e.Path}\n[red]{Markup.Escape(e.Exception.Message)}[/]");
} }
} }
}; };
@ -76,14 +118,14 @@ namespace RedundancyFinderCLI
{ {
if (settings.Verbose) if (settings.Verbose)
{ {
AnsiConsole.MarkupLine($"[red]Access denied to directory: [/]{e.Path}. Skipping. Error: [red]{e.Exception.Message}[/]"); Global.WriteLine($"[red]Access denied to directory: [/]{e.Path}. Skipping. Error: [red]{Markup.Escape(e.Exception.Message)}[/]");
} }
} }
else else
{ {
if (settings.Verbose) if (settings.Verbose)
{ {
AnsiConsole.MarkupLine($"[red] Error processing directory:\n[/]Path:{e.Path}\n[red]{e.Exception.Message}[/]"); Global.WriteLine($"[red] Error processing directory:\n[/]Path:{e.Path}\n[red]{Markup.Escape(e.Exception.Message)}[/]");
} }
} }
}; };
@ -92,104 +134,102 @@ namespace RedundancyFinderCLI
{ {
if (settings.Verbose) if (settings.Verbose)
{ {
AnsiConsole.MarkupLine($"[green]Processing file: [/]{e.Path}"); Global.WriteLine($"[green]Processing file: [/]{e.Path}");
} }
}; };
try try
{ {
var p = AnsiConsole.Progress();
p finder.TaskStarted += (sender, e) =>
.Start(ctx =>
{ {
// Define tasks hashingTasks++;
//var hashing = ctx.AddTask("[green]Hashing Files[/]",autoStart:false,); if (settings.Verbose)
finder.TaskStarted += (sender, e) =>
{ {
hashingTasks++; Global.WriteLine($"[green]Task started: [/]{e}");
if (settings.Verbose)
{
AnsiConsole.MarkupLine($"[green]Task started: [/]{e}");
}
//hashing.MaxValue = hashingTasks;
};
finder.FileFound += (sender, e) =>
{
if (settings.Verbose)
{
AnsiConsole.MarkupLine($"[green]File found: [/]{e.FilePath} {GetSizeFormat((ulong)e.Size)} ");
}
hashingTasksFinished++;
//hashing.Value = hashingTasksFinished;
};
//hashing.Value = hashing.MaxValue;
finder.FindRedundancies(settings.SearchPaths, extensions);
//hashing.StopTask();
});
var json = JsonConvert.SerializeObject(finder.Redundancies, Formatting.Indented);
File.WriteAllText(settings.OutputPath, json);
AnsiConsole.MarkupLine($"[yellow]Wrote [/]{finder.Redundancies.Count}[yellow] redundancies to [/]'{settings.OutputPath}'");
ulong totalSize = finder.Redundancies.Select(x => (ulong)x.Value.FileSize).Aggregate((a, b) => a + b);
string sizeFormat = GetSizeFormat(totalSize);
AnsiConsole.MarkupLine($"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();
} }
};
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) catch (Exception e)
{ {
AnsiConsole.MarkupLine($"[red] Error:\n[/]{e.Message}"); Global.WriteLine($"[red] Error:\n[/]{e.Message}");
} }
return 0; return 0;
} }
private static string GetSizeFormat(ulong totalSize) private void End()
{ {
string sizeUnit = "B"; SaveRedundancies(finder, settings.OutputPath);
while (totalSize > 1024) ulong totalSize = finder.Redundancies.Select(x => (ulong)x.Value.FileSize).Aggregate((a, b) => a + b);
{ string sizeFormat = Global.GetSizeFormat(totalSize);
totalSize /= 1024; Global.WriteLine($"Total Size: [green]{sizeFormat}[/]");
sizeUnit = sizeUnit switch Environment.Exit(0); // Exit the application gracefully
{
"B" => "KB",
"KB" => "MB",
"MB" => "GB",
"GB" => "TB",
_ => sizeUnit
};
}
string sizeFormat = $"{totalSize:.##} {sizeUnit}";
return sizeFormat;
} }
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}[/]");
}
}
} }
} }

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spectre.Console;
namespace RedundancyFinderCLI
{
public static class Global
{
public static void WriteLine(string v)
{
AnsiConsole.MarkupLine(Format(v));
}
public static string Format(string v)
{
string now = Markup.Escape($"[{DateTime.Now.ToString("HH:mm:ss")}]");
return $"[gray]{now}[/] {v}";
}
public static string GetSizeFormat(ulong totalSize)
{
string sizeUnit = "B";
double size = totalSize;
while (size > 1024)
{
size /= 1024d;
sizeUnit = sizeUnit switch
{
"B" => "KB",
"KB" => "MB",
"MB" => "GB",
"GB" => "TB",
_ => sizeUnit
};
}
string sizeFormat = $"{size:.00} {sizeUnit}";
return sizeFormat;
}
}
}

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Text;
using System.Text.Json; using System.Text.Json;
using RedundancyFinder; using RedundancyFinder;
using RedundancyFinderCLI; using RedundancyFinderCLI;
@ -9,10 +10,15 @@ internal class Program
{ {
private static void Main(string[] args) private static void Main(string[] args)
{ {
Console.OutputEncoding = Encoding.UTF8;
var app = new CommandApp<FinderCommand>(); var app = new CommandApp<FinderCommand>();
app.Configure(config => app.Configure(config =>
{ {
config.AddCommand<AnalyzeCommand>("analyze");
config.AddCommand<DeleteCommand>("delete");
config.AddCommand<SanitizeCommand>("sanitize");
#if DEBUG #if DEBUG
config.PropagateExceptions(); config.PropagateExceptions();
config.ValidateExamples(); config.ValidateExamples();
#endif #endif

View File

@ -2,7 +2,19 @@
"profiles": { "profiles": {
"RedundancyFinderCLI": { "RedundancyFinderCLI": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "C:\\ -v" "commandLineArgs": "C:\\Users\\daskn\\Pictures\\ -v"
},
"Analyze": {
"commandName": "Project",
"commandLineArgs": "analyze"
},
"Delete": {
"commandName": "Project",
"commandLineArgs": "delete redundancies.json C:\\Users\\daskn\\Pictures\\ -v"
},
"Sanitize": {
"commandName": "Project",
"commandLineArgs": "sanitize C:\\Users\\daskn\\Pictures\\ -v -d redundancies.json"
} }
} }
} }

View File

@ -5,6 +5,7 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Configurations>Debug;Release;Analyze</Configurations>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
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 SanitizeCommand : Command<SanitizeCommand.Settings>
{
public sealed class Settings : CommandSettings
{
[Description("Path to sanitize.")]
[CommandArgument(0, "[path]")]
public string? Path { get; init; }
[Description("Show all information.")]
[CommandOption("-v|--verbose")]
[DefaultValue(false)]
public bool Verbose { get; init; }
[Description("Show all information.")]
[CommandOption("-d|--deleteNonExistent <pathToSource>")]
[DefaultValue("redundancies.json")]
public string? PathToSource { get; init; }
}
Settings settings = null;
public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings)
{
this.settings = settings;
Global.WriteLine($"[white]Sanitizing {settings.Path}[/]");
Global.WriteLine($"[yellow]Deleting empty folders[/]");
int count = DeleteEmptyFolders(settings.Path);
if (File.Exists(settings.PathToSource))
{
DeleteNonExistentPaths();
}
Global.WriteLine($"[green]Deleted [/]{count}[green] empty folders[/]");
return 0;
}
public void DeleteNonExistentPaths()
{
Dictionary<string, Redundancy> redundancies = null;
AnsiConsole.Status()
.Start($"[yellow]Loading [/]'{settings.PathToSource}'", ctx =>
{
ctx.Spinner(Spinner.Known.Clock);
ctx.SpinnerStyle = Style.Parse("yellow bold");
Thread.Sleep(1000);
redundancies = JsonConvert.DeserializeObject<Dictionary<string, Redundancy>>(File.ReadAllText(settings.PathToSource));
});
foreach (var redundancy in redundancies)
{
var paths = redundancy.Value.Paths;
foreach (var path in paths.ToList())
{
Global.WriteLine($"[green]Checking file: {path}[/]");
if (!File.Exists(path))
{
Global.WriteLine($"[red]Deleting non existent file: {path}[/]");
redundancy.Value.Paths.Remove(path);
}
}
}
var json = JsonConvert.SerializeObject(redundancies, Formatting.Indented);
File.WriteAllText(settings.PathToSource, json);
}
public static int DeleteEmptyFolders(string directoryPath, int count = 0)
{
try
{
// Get all subdirectories
foreach (var subDirectory in Directory.GetDirectories(directoryPath))
{
// Recursively delete empty folders in subdirectories
count = DeleteEmptyFolders(subDirectory, count);
}
// Check if the current directory is empty
if (Directory.GetFiles(directoryPath).Length == 0 && Directory.GetDirectories(directoryPath).Length == 0)
{
Directory.Delete(directoryPath);
count++;
Global.WriteLine($"Deleted empty folder: {directoryPath}");
}
}
catch (Exception ex)
{
Global.WriteLine($"[red]Error deleting folder [/]'{directoryPath}':\n [red]{ex.Message}[/]");
}
return count;
}
}
}

View File

@ -1,11 +1,11 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.13.35818.85 d17.13 VisualStudioVersion = 17.13.35818.85
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedundancyFinder", "RedundancyFinder\RedundancyFinder.csproj", "{925C533F-2205-4848-B742-CB013F81DF91}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedundancyFinder", "RedundancyFinder\RedundancyFinder.csproj", "{925C533F-2205-4848-B742-CB013F81DF91}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedundancyFinderCLI", "RedundancyFinderCLI\RedundancyFinderCLI.csproj", "{7187EE24-4F0D-48F3-B76C-DAECD4A96F76}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedundancyFinderCLI", "RedundancyFinderCLI\RedundancyFinderCLI.csproj", "{7187EE24-4F0D-48F3-B76C-DAECD4A96F76}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution