Compare commits
17 Commits
775e915cfa
...
4da2ed9f3d
| Author | SHA1 | Date | |
|---|---|---|---|
| 4da2ed9f3d | |||
| 101d128a42 | |||
| 82160b12e1 | |||
| e0ed86899c | |||
| d6480c8e0e | |||
| 3834c2fba0 | |||
| f8d8c4461f | |||
| 30738eef24 | |||
| f82beb62ed | |||
| 0bb5fbdf87 | |||
| 45796d9bb2 | |||
| 1bb7d461e0 | |||
| 63c1c88393 | |||
| 5800f2229f | |||
| b66e7dd4ad | |||
| a84450cd5e | |||
| e6e259ed47 |
@ -4,7 +4,7 @@ namespace RedundancyFinder
|
||||
{
|
||||
public class Finder
|
||||
{
|
||||
List<Task> tasks = new List<Task>();
|
||||
public CancellationToken cancellation;
|
||||
|
||||
Dictionary<string, Redundancy> redundancies = new Dictionary<string, Redundancy>();
|
||||
|
||||
@ -12,6 +12,8 @@ namespace RedundancyFinder
|
||||
|
||||
string[] extensions;
|
||||
|
||||
List<string> ignorePaths = new List<string>();
|
||||
|
||||
public event EventHandler<DirectoryErrorEventArgs>? DirectoryError;
|
||||
public event EventHandler<FileErrorEventArgs>? FileError;
|
||||
public event EventHandler<FileFoundEventArgs>? FileFound;
|
||||
@ -19,9 +21,14 @@ namespace RedundancyFinder
|
||||
public event EventHandler<ProcessingFileEventArgs>? ProcessingFile;
|
||||
public void FindRedundancies(string[] paths, string[] extensions)
|
||||
{
|
||||
Redundancies?.Values.SelectMany(x => x.Paths).ToList().ForEach(x => ignorePaths.Add(x));
|
||||
this.extensions = extensions;
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (cancellation.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
ProcessDirectory(path);
|
||||
@ -31,31 +38,40 @@ namespace RedundancyFinder
|
||||
ProcessFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete
|
||||
Task.WaitAll(tasks.ToArray());
|
||||
}
|
||||
|
||||
private void ProcessDirectory(string directoryPath)
|
||||
{
|
||||
if (cancellation.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
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
|
||||
foreach (var file in Directory.GetFiles(directoryPath))
|
||||
{
|
||||
if (cancellation.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ProcessFile(file);
|
||||
}
|
||||
|
||||
// Recursively process subdirectories
|
||||
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
|
||||
{
|
||||
ProcessDirectory(subDirectory);
|
||||
@ -75,48 +91,48 @@ namespace RedundancyFinder
|
||||
|
||||
private void ProcessFile(string filePath)
|
||||
{
|
||||
if (cancellation.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (ignorePaths.Contains(filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!extensions.Contains(Path.GetExtension(filePath)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Task task = new(() =>
|
||||
{
|
||||
ProcessingFile?.Invoke(this, new ProcessingFileEventArgs() { Path = filePath });
|
||||
//TaskStarted?.Invoke(this, filePath);
|
||||
//ProcessingFile?.Invoke(this, new ProcessingFileEventArgs() { Path = filePath });
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var fileHash = ComputeFileHash(filePath);
|
||||
if (fileHash == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
long fileSize = new FileInfo(filePath).Length;
|
||||
lock (redundancies)
|
||||
{
|
||||
if (redundancies.ContainsKey(fileHash))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var fileHash = ComputeFileHash(filePath);
|
||||
if (fileHash == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
long fileSize = new FileInfo(filePath).Length;
|
||||
lock (Redundancies)
|
||||
{
|
||||
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 });
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Configurations>Debug;Release;Analyze</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
102
RedundancyFinderCLI/AnalyzeCommand.cs
Normal file
102
RedundancyFinderCLI/AnalyzeCommand.cs
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
115
RedundancyFinderCLI/DeleteCommand.cs
Normal file
115
RedundancyFinderCLI/DeleteCommand.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -34,23 +34,65 @@ namespace RedundancyFinderCLI
|
||||
[Description("Output path.")]
|
||||
[CommandOption("-o|--output")]
|
||||
[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)
|
||||
{
|
||||
|
||||
|
||||
finder.cancellation = cancellation.Token;
|
||||
this.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.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) =>
|
||||
{
|
||||
@ -58,14 +100,14 @@ namespace RedundancyFinderCLI
|
||||
{
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[green]Processing file: [/]{e.Path}");
|
||||
Global.WriteLine($"[green]Processing file: [/]{e.Path}");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var p = AnsiConsole.Progress();
|
||||
p
|
||||
.Start(ctx =>
|
||||
|
||||
finder.TaskStarted += (sender, e) =>
|
||||
{
|
||||
// Define tasks
|
||||
//var hashing = ctx.AddTask("[green]Hashing Files[/]",autoStart:false,);
|
||||
|
||||
finder.TaskStarted += (sender, e) =>
|
||||
hashingTasks++;
|
||||
if (settings.Verbose)
|
||||
{
|
||||
hashingTasks++;
|
||||
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();
|
||||
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)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red] Error:\n[/]{e.Message}");
|
||||
|
||||
Global.WriteLine($"[red] Error:\n[/]{e.Message}");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string GetSizeFormat(ulong totalSize)
|
||||
private void End()
|
||||
{
|
||||
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;
|
||||
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}[/]");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
45
RedundancyFinderCLI/Global.cs
Normal file
45
RedundancyFinderCLI/Global.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using RedundancyFinder;
|
||||
using RedundancyFinderCLI;
|
||||
@ -9,10 +10,15 @@ internal class Program
|
||||
{
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
var app = new CommandApp<FinderCommand>();
|
||||
app.Configure(config =>
|
||||
{
|
||||
config.AddCommand<AnalyzeCommand>("analyze");
|
||||
config.AddCommand<DeleteCommand>("delete");
|
||||
config.AddCommand<SanitizeCommand>("sanitize");
|
||||
#if DEBUG
|
||||
|
||||
config.PropagateExceptions();
|
||||
config.ValidateExamples();
|
||||
#endif
|
||||
|
||||
@ -2,7 +2,19 @@
|
||||
"profiles": {
|
||||
"RedundancyFinderCLI": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Configurations>Debug;Release;Analyze</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
118
RedundancyFinderCLI/SanitizeCommand.cs
Normal file
118
RedundancyFinderCLI/SanitizeCommand.cs
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.13.35818.85 d17.13
|
||||
VisualStudioVersion = 17.13.35818.85
|
||||
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
|
||||
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
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user