Files
Files/Files/Program.cs
2021-04-07 07:50:03 +03:00

165 lines
7.2 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using Spectre.Console;
using Microsoft.Data.Sqlite;
using Mono.Unix;
using System.Threading;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.Threading.Tasks;
using System.Linq;
using Dapper;
using System.Security.Cryptography;
namespace Files {
class Program {
private static async Task IndexFiles(bool isVerbose, DirectoryInfo startDirectory, CancellationToken ct) {
await AnsiConsole.Status()
.StartAsync("Thinking...", async ctx => {
using var connection = new SqliteConnection("Data Source=db.db");
connection.Open();
await using var transaction = await connection.BeginTransactionAsync();
var cnt = connection.ExecuteScalar<int>("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=@tableName;", new { tableName = "files" });
if (cnt == 0)
{
connection.Execute("CREATE TABLE IF NOT EXISTS files (name TEXT, size INTEGER, inode INTEGER);");
}
Queue<string> directoriesQueue = new Queue<string>();
directoriesQueue.Enqueue(startDirectory?.ToString() ?? ".");
try {
while (directoriesQueue.TryDequeue(out string peekedDir)) {
ctx.Status(peekedDir.Replace("[", "[[").Replace("]", "]]"));
UnixDirectoryInfo dirInfo = new(peekedDir);
if (!dirInfo.CanAccess(Mono.Unix.Native.AccessModes.R_OK)
|| !dirInfo.CanAccess(Mono.Unix.Native.AccessModes.X_OK)) {
AnsiConsole.MarkupLine($"[red]:cross_mark: NO_ACCESS:[/] :file_folder: {dirInfo.ToString().Replace("[", "[[").Replace("]", "]]")}");
return;
}
UnixFileSystemInfo[] entries = dirInfo.GetFileSystemEntries();
foreach (UnixFileSystemInfo entry in entries) {
string relativePath = Path.Combine(peekedDir, entry.Name);
if (!entry.CanAccess(Mono.Unix.Native.AccessModes.R_OK)) {
if (entry.IsDirectory)
AnsiConsole.MarkupLine($"[red]:cross_mark: NO_ACCESS:[/] :file_folder: {relativePath.Replace("[", "[[").Replace("]", "]]")}");
else if (entry.IsRegularFile)
AnsiConsole.MarkupLine($"[red]:cross_mark: NO_ACCESS:[/] :page_facing_up: {relativePath.Replace("[", "[[").Replace("]", "]]")}");
continue;
}
if (entry.IsDirectory) {
directoriesQueue.Enqueue(relativePath);
continue;
}
connection.Execute("INSERT INTO files (name, size, inode) VALUES (@name, @Length, @Inode);", new { name = relativePath, entry.Length, entry.Inode });
if (isVerbose)
AnsiConsole.MarkupLine($"[green]:check_mark: OK:[/] {relativePath.Replace("[", "[[").Replace("]", "]]")}");
if (ct.IsCancellationRequested)
return;
}
}
transaction.Commit();
} catch (Exception exception) {
await transaction.RollbackAsync();
AnsiConsole.WriteException(exception);
return;
}
ctx.Status("Finding duplicates...");
ctx.Spinner(Spinner.Known.Aesthetic);
var potential = connection.Query<(int cnt, long size)>("SELECT COUNT(*) cnt, size FROM files WHERE size != 0 GROUP BY size HAVING cnt > 1 ORDER BY size * cnt DESC;");
foreach (var potentialFile in potential) {
var sameSize = connection.Query<DbRecord>("SELECT name, size, inode FROM files WHERE size = @size",
new { potentialFile.size }).ToList();
var unporocessable = sameSize
.Where(r => !r.Hash.HasValue);
foreach (var dbRecord in unporocessable)
{
AnsiConsole.MarkupLine($"[red]:cross_mark: NO_ACCESS:[/] :page_facing_up: {dbRecord.Name.Replace("[", "[[").Replace("]", "]]")}");
}
var equalGrouped = sameSize
.Where(r => r.Hash.HasValue)
.GroupBy(r=>r.Hash)
.Where(g=>g.Count() > 1)
.ToList();
foreach (var grp in equalGrouped) {
var root = new Tree(":double_exclamation_mark: " + grp.Key);
foreach (var item in grp) {
root.AddNode(item.Name);
}
AnsiConsole.Render(root);
}
}
});
}
private static async Task Main(string[] args) {
var verboseOption = new Option<bool>(new []{"--verbose", "-v"} ,"Verbose");
var directoryArgument = new Argument<DirectoryInfo>(
result => new DirectoryInfo("./"), isDefault: true)
{
Name = "directory",
Description = "Directory to scan.",
Arity = ArgumentArity.ZeroOrOne,
}.ExistingOnly();
var rootCommand = new RootCommand("$ File -v false ./")
{
verboseOption,
directoryArgument,
};
ParseResult result = rootCommand.Parse(args);
ArgumentResult dirResult = result.FindResultFor(directoryArgument);
var dir = new DirectoryInfo(
dirResult.Tokens.FirstOrDefault()?.Value
?? dirResult.Argument.GetDefaultValue()?.ToString());
rootCommand.Handler = CommandHandler.Create<bool, CancellationToken>(
async (verbose, ct) => await IndexFiles(verbose, dir, ct));
await rootCommand.InvokeAsync(args);
}
}
public class DbRecord {
private readonly Lazy<Guid?> _guid;
public DbRecord() {
_guid = new Lazy<Guid?>(GetHash);
}
public string Name { get; set; }
public long Size { get; set; }
public long Inode { get; set; }
public Guid? Hash => _guid.Value;
public Guid? GetHash() {
try {
using FileStream stream = File.OpenRead(Name);
var md5 = MD5.Create();
var bytes = md5.ComputeHash(stream);
return new Guid(bytes);
} catch {
return null;
}
}
}
}