Compare commits
No commits in common. "4da2ed9f3dc9047c6ca93ee44cd2f1d1e27dc3e5" and "775e915cfa5a58a89eb8dd19cc5285a3d1791a9f" have entirely different histories.
4da2ed9f3d
...
775e915cfa
@ -4,7 +4,7 @@ namespace RedundancyFinder
|
|||||||
{
|
{
|
||||||
public class Finder
|
public class Finder
|
||||||
{
|
{
|
||||||
public CancellationToken cancellation;
|
List<Task> tasks = new List<Task>();
|
||||||
|
|
||||||
Dictionary<string, Redundancy> redundancies = new Dictionary<string, Redundancy>();
|
Dictionary<string, Redundancy> redundancies = new Dictionary<string, Redundancy>();
|
||||||
|
|
||||||
@ -12,8 +12,6 @@ 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;
|
||||||
@ -21,14 +19,9 @@ 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);
|
||||||
@ -38,40 +31,31 @@ 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)
|
try
|
||||||
|
{
|
||||||
|
// Check if the directory is hidden and skip it if true
|
||||||
|
var attributes = File.GetAttributes(directoryPath);
|
||||||
|
if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try
|
|
||||||
{
|
|
||||||
// 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);
|
||||||
@ -91,22 +75,14 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TaskStarted?.Invoke(this, filePath);
|
Task task = new(() =>
|
||||||
//ProcessingFile?.Invoke(this, new ProcessingFileEventArgs() { Path = filePath });
|
{
|
||||||
|
ProcessingFile?.Invoke(this, new ProcessingFileEventArgs() { Path = filePath });
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -117,14 +93,19 @@ namespace RedundancyFinder
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!redundancies.ContainsKey(fileHash))
|
||||||
{
|
{
|
||||||
var redundancy = new Redundancy() { Hash = fileHash, FileSize = fileSize };
|
var redundancy = new Redundancy() { Hash = fileHash, FileSize = fileSize };
|
||||||
Redundancies.Add(fileHash, redundancy);
|
redundancies.Add(fileHash, redundancy);
|
||||||
}
|
}
|
||||||
Redundancies[fileHash].Paths.Add(filePath);
|
redundancies[fileHash].Paths.Add(filePath);
|
||||||
}
|
}
|
||||||
FileFound?.Invoke(this, new FileFoundEventArgs(filePath, fileHash, fileSize));
|
FileFound?.Invoke(this, new FileFoundEventArgs(filePath, fileHash, fileSize));
|
||||||
}
|
}
|
||||||
@ -132,7 +113,10 @@ namespace RedundancyFinder
|
|||||||
{
|
{
|
||||||
FileError?.Invoke(this, new FileErrorEventArgs() { Exception = ex, Path = filePath });
|
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)
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
<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>
|
||||||
|
|||||||
@ -1,102 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
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,65 +34,23 @@ 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) =>
|
||||||
{
|
{
|
||||||
@ -100,14 +58,14 @@ namespace RedundancyFinderCLI
|
|||||||
{
|
{
|
||||||
if (settings.Verbose)
|
if (settings.Verbose)
|
||||||
{
|
{
|
||||||
Global.WriteLine($"[red]Access denied to file: [/]{e.Path}. Skipping. Error: [red]{Markup.Escape(e.Exception.Message)}[/]");
|
AnsiConsole.MarkupLine($"[red]Access denied to file: [/]{e.Path}. Skipping. Error: [red]{e.Exception.Message}[/]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (settings.Verbose)
|
if (settings.Verbose)
|
||||||
{
|
{
|
||||||
Global.WriteLine($"[red] Error processing file:\n[/]Path:{e.Path}\n[red]{Markup.Escape(e.Exception.Message)}[/]");
|
AnsiConsole.MarkupLine($"[red] Error processing file:\n[/]Path:{e.Path}\n[red]{e.Exception.Message}[/]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -118,14 +76,14 @@ namespace RedundancyFinderCLI
|
|||||||
{
|
{
|
||||||
if (settings.Verbose)
|
if (settings.Verbose)
|
||||||
{
|
{
|
||||||
Global.WriteLine($"[red]Access denied to directory: [/]{e.Path}. Skipping. Error: [red]{Markup.Escape(e.Exception.Message)}[/]");
|
AnsiConsole.MarkupLine($"[red]Access denied to directory: [/]{e.Path}. Skipping. Error: [red]{e.Exception.Message}[/]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (settings.Verbose)
|
if (settings.Verbose)
|
||||||
{
|
{
|
||||||
Global.WriteLine($"[red] Error processing directory:\n[/]Path:{e.Path}\n[red]{Markup.Escape(e.Exception.Message)}[/]");
|
AnsiConsole.MarkupLine($"[red] Error processing directory:\n[/]Path:{e.Path}\n[red]{e.Exception.Message}[/]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -134,102 +92,104 @@ namespace RedundancyFinderCLI
|
|||||||
{
|
{
|
||||||
if (settings.Verbose)
|
if (settings.Verbose)
|
||||||
{
|
{
|
||||||
Global.WriteLine($"[green]Processing file: [/]{e.Path}");
|
AnsiConsole.MarkupLine($"[green]Processing file: [/]{e.Path}");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var p = AnsiConsole.Progress();
|
||||||
|
p
|
||||||
|
.Start(ctx =>
|
||||||
|
{
|
||||||
|
// Define tasks
|
||||||
|
//var hashing = ctx.AddTask("[green]Hashing Files[/]",autoStart:false,);
|
||||||
|
|
||||||
finder.TaskStarted += (sender, e) =>
|
finder.TaskStarted += (sender, e) =>
|
||||||
{
|
{
|
||||||
hashingTasks++;
|
hashingTasks++;
|
||||||
if (settings.Verbose)
|
if (settings.Verbose)
|
||||||
{
|
{
|
||||||
Global.WriteLine($"[green]Task started: [/]{e}");
|
AnsiConsole.MarkupLine($"[green]Task started: [/]{e}");
|
||||||
}
|
}
|
||||||
|
//hashing.MaxValue = hashingTasks;
|
||||||
};
|
};
|
||||||
|
|
||||||
finder.FileFound += (sender, e) =>
|
finder.FileFound += (sender, e) =>
|
||||||
{
|
{
|
||||||
if (settings.Verbose)
|
if (settings.Verbose)
|
||||||
{
|
{
|
||||||
Global.WriteLine($"[green]File found: [/]{e.FilePath} [darkgreen]{Global.GetSizeFormat((ulong)e.Size)}[/]");
|
AnsiConsole.MarkupLine($"[green]File found: [/]{e.FilePath} {GetSizeFormat((ulong)e.Size)} ");
|
||||||
}
|
}
|
||||||
hashingTasksFinished++;
|
hashingTasksFinished++;
|
||||||
|
//hashing.Value = hashingTasksFinished;
|
||||||
};
|
};
|
||||||
|
//hashing.Value = hashing.MaxValue;
|
||||||
task = Task.Run(() =>
|
|
||||||
{
|
|
||||||
finder.FindRedundancies(settings.SearchPaths, extensions);
|
finder.FindRedundancies(settings.SearchPaths, extensions);
|
||||||
}, cancellation.Token);
|
//hashing.StopTask();
|
||||||
try
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
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)
|
||||||
task.Wait(cancellation.Token);
|
{
|
||||||
|
AnsiConsole.WriteLine($"Hash: {redundancy.Hash}");
|
||||||
|
AnsiConsole.WriteLine("Paths:");
|
||||||
|
foreach (var path in redundancy.Paths)
|
||||||
|
{
|
||||||
|
AnsiConsole.WriteLine(path);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
AnsiConsole.WriteLine();
|
||||||
{
|
|
||||||
|
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
|
|
||||||
End();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Global.WriteLine($"[red] Error:\n[/]{e.Message}");
|
AnsiConsole.MarkupLine($"[red] Error:\n[/]{e.Message}");
|
||||||
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void End()
|
private static string GetSizeFormat(ulong totalSize)
|
||||||
{
|
{
|
||||||
SaveRedundancies(finder, settings.OutputPath);
|
string sizeUnit = "B";
|
||||||
ulong totalSize = finder.Redundancies.Select(x => (ulong)x.Value.FileSize).Aggregate((a, b) => a + b);
|
while (totalSize > 1024)
|
||||||
string sizeFormat = Global.GetSizeFormat(totalSize);
|
{
|
||||||
Global.WriteLine($"Total Size: [green]{sizeFormat}[/]");
|
totalSize /= 1024;
|
||||||
Environment.Exit(0); // Exit the application gracefully
|
sizeUnit = sizeUnit switch
|
||||||
|
{
|
||||||
|
"B" => "KB",
|
||||||
|
"KB" => "MB",
|
||||||
|
"MB" => "GB",
|
||||||
|
"GB" => "TB",
|
||||||
|
_ => sizeUnit
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
string sizeFormat = $"{totalSize:.##} {sizeUnit}";
|
||||||
private void SaveRedundancies(Finder finder, string outputPath)
|
return sizeFormat;
|
||||||
{
|
|
||||||
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}[/]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,45 +0,0 @@
|
|||||||
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,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using RedundancyFinder;
|
using RedundancyFinder;
|
||||||
using RedundancyFinderCLI;
|
using RedundancyFinderCLI;
|
||||||
@ -10,15 +9,10 @@ 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
|
||||||
|
|||||||
@ -2,19 +2,7 @@
|
|||||||
"profiles": {
|
"profiles": {
|
||||||
"RedundancyFinderCLI": {
|
"RedundancyFinderCLI": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"commandLineArgs": "C:\\Users\\daskn\\Pictures\\ -v"
|
"commandLineArgs": "C:\\ -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,7 +5,6 @@
|
|||||||
<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>
|
||||||
|
|||||||
@ -1,118 +0,0 @@
|
|||||||
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
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.13.35818.85
|
VisualStudioVersion = 17.13.35818.85 d17.13
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedundancyFinder", "RedundancyFinder\RedundancyFinder.csproj", "{925C533F-2205-4848-B742-CB013F81DF91}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedundancyFinder", "RedundancyFinder\RedundancyFinder.csproj", "{925C533F-2205-4848-B742-CB013F81DF91}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedundancyFinderCLI", "RedundancyFinderCLI\RedundancyFinderCLI.csproj", "{7187EE24-4F0D-48F3-B76C-DAECD4A96F76}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedundancyFinderCLI", "RedundancyFinderCLI\RedundancyFinderCLI.csproj", "{7187EE24-4F0D-48F3-B76C-DAECD4A96F76}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user