Compare commits

...

6 Commits

Author SHA1 Message Date
Oliver Booth c65d6ed414
chore: bump to 1.2.0 2024-04-17 14:48:43 +01:00
Oliver Booth f07fd7f641
ci: add automatic tagged release 2024-04-17 14:48:17 +01:00
Oliver Booth 8273a34fef
feat: default to . to reduce need for it as cli arg 2024-04-17 14:35:30 +01:00
Oliver Booth 7da6faff83
fix: use ConcurrentBag for hash cache 2024-04-17 14:35:04 +01:00
Oliver Booth 3396c2bc74
refactor: move stack push to method
reduces method complexity and now Rider isn't crying
2024-04-17 14:24:54 +01:00
Oliver Booth 435318cd20
feat: enable verbose output to list hash of every file 2024-04-17 14:24:18 +01:00
5 changed files with 166 additions and 27 deletions

66
.github/workflows/prerelease.yml vendored Normal file
View File

@ -0,0 +1,66 @@
name: Tagged Pre-Release
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+-*"
jobs:
release:
name: "Tagged Pre-Release"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Get version from tag
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
- name: Build and Publish
run: |
dotnet publish -c Release -p PublishSingleFile=true -p:VersionSuffix='prerelease' -p:BuildNumber=${{ github.run_number }} -o ./publish/win-x64 -r win-x64
dotnet publish -c Release -p PublishSingleFile=true -p:VersionSuffix='prerelease' -p:BuildNumber=${{ github.run_number }} -o ./publish/win-x86 -r win-x86
dotnet publish -c Release -p PublishSingleFile=true -p:VersionSuffix='prerelease' -p:BuildNumber=${{ github.run_number }} -o ./publish/linux-x64 -r linux-x64
dotnet publish -c Release -p PublishSingleFile=true -p:VersionSuffix='prerelease' -p:BuildNumber=${{ github.run_number }} -o ./publish/osx-x64 -r osx-x64
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: publish
path: publish/
- name: Create Release
id: create_release
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false
- name: Upload Release Assets
id: upload-release-assets
uses: actions/upload-release-asset@v1
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: |
./publish/win-x64/fdup.exe
./publish/win-x86/fdup.exe
./publish/linux-x64/fdup
./publish/osx-x64/fdup
asset_name: |
fdup-${{ steps.get_version.outputs.VERSION }}-win-x64.exe
fdup-${{ steps.get_version.outputs.VERSION }}-win-x86.exe
fdup-${{ steps.get_version.outputs.VERSION }}-linux_x64
fdup-${{ steps.get_version.outputs.VERSION }}-macos_x64
asset_content_type: application/octet-stream
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

66
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,66 @@
name: Tagged Release
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
jobs:
release:
name: "Tagged Release"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Get version from tag
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
- name: Build and Publish
run: |
dotnet publish -c Release -p PublishSingleFile=true -o ./publish/win-x64 -r win-x64
dotnet publish -c Release -p PublishSingleFile=true -o ./publish/win-x86 -r win-x86
dotnet publish -c Release -p PublishSingleFile=true -o ./publish/linux-x64 -r linux-x64
dotnet publish -c Release -p PublishSingleFile=true -o ./publish/osx-x64 -r osx-x64
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: publish
path: publish/
- name: Create Release
id: create_release
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false
- name: Upload Release Assets
id: upload-release-assets
uses: actions/upload-release-asset@v1
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: |
./publish/win-x64/fdup.exe
./publish/win-x86/fdup.exe
./publish/linux-x64/fdup
./publish/osx-x64/fdup
asset_name: |
fdup-${{ steps.get_version.outputs.VERSION }}-win-x64.exe
fdup-${{ steps.get_version.outputs.VERSION }}-win-x86.exe
fdup-${{ steps.get_version.outputs.VERSION }}-linux_x64
fdup-${{ steps.get_version.outputs.VERSION }}-macos_x64
asset_content_type: application/octet-stream
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>fdup</AssemblyName>
<VersionPrefix>1.1.0</VersionPrefix>
<VersionPrefix>1.2.0</VersionPrefix>
<Authors>Oliver Booth</Authors>
</PropertyGroup>

View File

@ -1,6 +1,4 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using Spectre.Console;
@ -10,7 +8,7 @@ namespace FindDuplicates;
internal sealed class ListCommand : AsyncCommand<ListSettings>
{
private readonly ConcurrentDictionary<string, List<FileInfo>> _fileHashMap = new();
private readonly ConcurrentDictionary<string, ConcurrentBag<FileInfo>> _fileHashMap = new();
public override async Task<int> ExecuteAsync(CommandContext context, ListSettings settings)
{
@ -31,7 +29,7 @@ internal sealed class ListCommand : AsyncCommand<ListSettings>
AnsiConsole.WriteLine();
int duplicates = 0;
foreach ((string hash, List<FileInfo> files) in _fileHashMap)
foreach ((string hash, ConcurrentBag<FileInfo> files) in _fileHashMap)
{
int fileCount = files.Count;
@ -91,18 +89,7 @@ internal sealed class ListCommand : AsyncCommand<ListSettings>
if (relativePath != ".")
AnsiConsole.MarkupLineInterpolated($"Searching [cyan]{relativePath}[/]");
if (settings.Recursive)
{
try
{
foreach (DirectoryInfo childDirectory in currentDirectory.EnumerateDirectories())
directoryStack.Push(childDirectory);
}
catch (Exception ex)
{
AnsiConsole.MarkupLineInterpolated($"[red]Error:[/] {ex.Message}");
}
}
AddChildDirectories(settings, currentDirectory, directoryStack);
try
{
@ -110,7 +97,7 @@ internal sealed class ListCommand : AsyncCommand<ListSettings>
{
string relativeFilePath = Path.GetRelativePath(inputDirectory.FullName, file.FullName);
AnsiConsole.MarkupLineInterpolated($"Checking hash for [cyan]{relativeFilePath}[/]");
tasks.Add(Task.Run(() => ProcessFile(file)));
tasks.Add(Task.Run(() => ProcessFile(file, settings)));
}
}
catch (Exception ex)
@ -120,7 +107,7 @@ internal sealed class ListCommand : AsyncCommand<ListSettings>
}
}
private void ProcessFile(FileInfo file)
private void ProcessFile(FileInfo file, ListSettings settings)
{
Span<byte> buffer = stackalloc byte[64];
try
@ -129,12 +116,10 @@ internal sealed class ListCommand : AsyncCommand<ListSettings>
using BufferedStream bufferedStream = new BufferedStream(stream, 1048576 /* 1MB */);
SHA512.HashData(bufferedStream, buffer);
string hash = ByteSpanToString(buffer);
Trace.WriteLine($"{file.FullName}: {hash}");
if (settings.Verbose)
AnsiConsole.WriteLine($"{file.FullName} ->\n {hash}");
if (!_fileHashMap.TryGetValue(hash, out List<FileInfo>? cache))
_fileHashMap[hash] = cache = new List<FileInfo>();
lock (cache)
ConcurrentBag<FileInfo> cache = _fileHashMap.GetOrAdd(hash, _ => []);
cache.Add(file);
}
catch (Exception ex)
@ -143,6 +128,22 @@ internal sealed class ListCommand : AsyncCommand<ListSettings>
}
}
private static void AddChildDirectories(ListSettings settings, DirectoryInfo directory, Stack<DirectoryInfo> stack)
{
if (!settings.Recursive)
return;
try
{
foreach (DirectoryInfo childDirectory in directory.EnumerateDirectories())
stack.Push(childDirectory);
}
catch (Exception ex)
{
AnsiConsole.MarkupLineInterpolated($"[red]Error:[/] {ex.Message}");
}
}
private static string ByteSpanToString(ReadOnlySpan<byte> buffer)
{
var builder = new StringBuilder();

View File

@ -5,12 +5,18 @@ namespace FindDuplicates;
internal sealed class ListSettings : CommandSettings
{
[CommandArgument(0, "<path>")]
[Description("The path to search.")]
public string InputPath { get; set; } = string.Empty;
[CommandArgument(0, "[path]")]
[Description("The path to search. Defaults to the current directory.")]
[DefaultValue(".")]
public string InputPath { get; set; } = ".";
[CommandOption("-r|--recursive")]
[Description("When this flag is set, the directory will be scanned recursively. This may take longer.")]
[DefaultValue(false)]
public bool Recursive { get; set; } = false;
[CommandOption("--verbose")]
[Description("Enable verbose output.")]
[DefaultValue(false)]
public bool Verbose { get; set; } = false;
}