Add project files.
This commit is contained in:
25
Files.sln
Normal file
25
Files.sln
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 16
|
||||||
|
VisualStudioVersion = 16.0.31025.218
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Files", "Files\Files.csproj", "{28005FE3-CBA7-4D00-9272-180C392D35A9}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{28005FE3-CBA7-4D00-9272-180C392D35A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{28005FE3-CBA7-4D00-9272-180C392D35A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{28005FE3-CBA7-4D00-9272-180C392D35A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{28005FE3-CBA7-4D00-9272-180C392D35A9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {FB5DD751-57CA-4F80-923A-1CE1C71654D7}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
27
Files/.vscode/launch.json
vendored
Normal file
27
Files/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
||||||
|
"name": ".NET Core Launch (console)",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
|
"program": "${workspaceFolder}/bin/Debug/net6.0/Files.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
"console": "internalConsole",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach",
|
||||||
|
"processId": "${command:pickProcess}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
42
Files/.vscode/tasks.json
vendored
Normal file
42
Files/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/Files.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/Files.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"${workspaceFolder}/Files.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
Files/Files.csproj
Normal file
16
Files/Files.csproj
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Dapper" Version="2.0.78" />
|
||||||
|
<PackageReference Include="Mono.Posix.NETStandard" Version="5.20.1-preview" />
|
||||||
|
<PackageReference Include="Spectre.Console" Version="0.38.1-preview.0.24" />
|
||||||
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.0-preview.2.21154.2" />
|
||||||
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20574.7" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
156
Files/Program.cs
Normal file
156
Files/Program.cs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Files/Properties/launchSettings.json
Normal file
7
Files/Properties/launchSettings.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"Files": {
|
||||||
|
"commandName": "Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Files/db.db
Normal file
BIN
Files/db.db
Normal file
Binary file not shown.
7
global.json
Normal file
7
global.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"allowPrerelease": true,
|
||||||
|
"rollForward": "latestMinor"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user