Skip to content

Commit

Permalink
Implement Sync Root pruning
Browse files Browse the repository at this point in the history
  • Loading branch information
AliveDevil committed Jun 13, 2024
1 parent b0cfe7c commit e407f56
Show file tree
Hide file tree
Showing 10 changed files with 437 additions and 9 deletions.
20 changes: 16 additions & 4 deletions CloudFiles.Troubleshooter.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,32 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudFiles.Troubleshooter", "src\CloudFiles.Troubleshooter.csproj", "{DA5D9A91-CFB4-4E7C-BD69-F950FB9E9016}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudFiles.Troubleshooter", "src\CloudFiles.Troubleshooter.csproj", "{DA5D9A91-CFB4-4E7C-BD69-F950FB9E9016}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DF2157F6-7876-4F50-BB7B-B6777C263F3D}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
Directory.Packages.props = Directory.Packages.props
global.json = global.json
LICENSE.md = LICENSE.md
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DA5D9A91-CFB4-4E7C-BD69-F950FB9E9016}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA5D9A91-CFB4-4E7C-BD69-F950FB9E9016}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA5D9A91-CFB4-4E7C-BD69-F950FB9E9016}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA5D9A91-CFB4-4E7C-BD69-F950FB9E9016}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
9 changes: 7 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<Project>
<PropertyGroup>
<UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>


<PropertyGroup Label="Assembly Info">
<Company>iterate GmbH</Company>
<PackageLicenseFile>$(MSBuildThisFileDirectory)LICENSE.md</PackageLicenseFile>
<Version>0.1.0</Version>
</PropertyGroup>
</Project>
</Project>
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageVersion>
<PackageVersion Include="Spectre.Console" Version="0.49.1" />
</ItemGroup>
</Project>
5 changes: 4 additions & 1 deletion src/CloudFiles.Troubleshooter.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<AssemblyName>cloudfiles-troubleshooter</AssemblyName>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

<ItemGroup>
Expand All @@ -15,6 +17,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Spectre.Console" />
</ItemGroup>

</Project>
</Project>
70 changes: 70 additions & 0 deletions src/CommandSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
namespace CloudFiles.Troubleshooter;

public abstract class CommandSettings
{
private readonly OptionParser _optionParser = new();

public bool Confirm { get; private set; } = false;

public bool WhatIf { get; private set; } = false;

public CommandSettings()
{
Register(nameof(Confirm), ParseConfirm);
Register(nameof(WhatIf), ParseWhatIf);
}

public void Parse(ReadOnlySpan<string> parse)
{
var remaining = _optionParser.Parse(parse);
ConsumeRemaining(remaining);
}

protected void Register(string propertyName, OptionHandler handler)
{
ArgumentNullException.ThrowIfNull(handler);
ArgumentException.ThrowIfNullOrWhiteSpace(propertyName);

if (!_optionParser.AddOption(propertyName, handler))
{
throw new ArgumentException("Key already exists", nameof(propertyName));
}
}

protected virtual void ConsumeRemaining(List<string> args)
{ }

private OptionResult ParseConfirm(OptionType optionType, string? value)
{
if (optionType != OptionType.Switch)
{
// OptionType is KeyValue, thus `--Confirm:<Value>`.
ArgumentException.ThrowIfNullOrWhiteSpace(value);
// Let fail parsing of bool, if invalid value provided.
Confirm = bool.Parse(value);
}
else
{
Confirm = true;
}

return OptionResult.Continue;
}

private OptionResult ParseWhatIf(OptionType optionType, string? value)
{
if (optionType != OptionType.Switch)
{
// OptionType is KeyValue, thus `--WhatIf:<Value>`.
ArgumentException.ThrowIfNullOrWhiteSpace(value);
// Let parsing of bool fail if bad value provided.
WhatIf = bool.Parse(value);
}
else
{
WhatIf = true;
}

return OptionResult.Continue;
}
}
25 changes: 25 additions & 0 deletions src/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// <auto-generated />
#pragma warning disable IDE0079 // Dont remove next line
#pragma warning disable IDE0005, CS8019 // Dont remove global usings

global using static CloudFiles.Troubleshooter.Consts;
global using static Spectre.Console.AnsiConsole;
global using static Windows.Win32.PInvoke;

using System.Diagnostics;

using Windows.Win32.Storage.CloudFilters;

namespace CloudFiles.Troubleshooter
{
delegate int CommandHandler(ReadOnlySpan<string> args);

static unsafe class Consts
{
public static CF_CALLBACK_REGISTRATION[] NoneCallbackRegistration = [
new() { Type = CF_CALLBACK_TYPE.CF_CALLBACK_TYPE_NONE, Callback = null }
];

public static string UsageFormat = $"Usage: {Process.GetCurrentProcess().ProcessName} {{0}} {{1}}";
}
}
9 changes: 9 additions & 0 deletions src/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CF_SYNC_ROOT_BASIC_INFO
CF_SYNC_ROOT_PROVIDER_INFO
CF_SYNC_ROOT_STANDARD_INFO
CfConnectSyncRoot
CfDisconnectSyncRoot
CfUnregisterSyncRoot
CfUpdatePlaceholder
CreateFile
FILE_ACCESS_RIGHTS
112 changes: 112 additions & 0 deletions src/OptionParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System.Buffers;

namespace CloudFiles.Troubleshooter
{
internal class OptionParser
{
private static readonly SearchValues<char> ValueSeparators = SearchValues.Create([':', '=']);
private readonly Dictionary<string, OptionHandler> _handler = new(StringComparer.OrdinalIgnoreCase);

public bool AddOption(string name, OptionHandler handler)
{
return _handler.TryAdd(name, handler);
}

public List<string> Parse(ReadOnlySpan<string> args)
{
List<string> remaining = [];

string lastKey = default!;
OptionHandler? optionHandler = default;
var type = OptionType.Argument;
foreach (ref readonly var option in args)
{
if (type == OptionType.Switch)
{
type = OptionType.KeyValue;
if (optionHandler!(OptionType.KeyValue, option) != OptionResult.Continue)
{
throw new ArgumentException($"Bad value \"{option}\" supplied", lastKey);
}
}
else if (option.StartsWith('-'))
{
type = OptionType.Switch;
var optionExpression = option.AsSpan().TrimStart('-');
var valueSeparator = optionExpression.IndexOfAny(ValueSeparators);
string? value = null;
if (valueSeparator != -1)
{
type = OptionType.KeyValue;
value = optionExpression[(valueSeparator + 1)..].ToString();
optionExpression = optionExpression[..valueSeparator];
}

lastKey = optionExpression.ToString();
if ((optionHandler = FindOptionHandler(lastKey)) is null)
{
throw new ArgumentException("Unknown option", lastKey);
}
else if (optionHandler(type, value) != OptionResult.NeedMore)
{
type = OptionType.Argument;
}
}
else
{
remaining.Add(option);
}
}

return remaining;
}

private OptionHandler? FindOptionHandler(string key)
{
return _handler.TryGetValue(key, out var handler)
? handler
: FindSlow(key);

OptionHandler? FindSlow(ReadOnlySpan<char> key)
{
List<KeyValuePair<string, OptionHandler>> keys = [.. _handler];
for (int i = 0; i < key.Length; i++)
{
ref readonly char symbol = ref key[i];
for (int j = keys.Count - 1; j >= 0; j--)
{
var handler = keys[j];
// remove all keys, that are smaller than input key
// and remove all keys that don't match ignore-case
if (handler.Key.Length < key.Length || (char.ToUpperInvariant(symbol) != char.ToUpperInvariant(handler.Key[i])))
{
keys.RemoveAt(j);
}
else if (keys.Count == 1)
{
return handler.Value;
}
}
}

return null;
}
}
}

public delegate OptionResult OptionHandler(OptionType optionType, string? value);

public enum OptionResult
{
Continue,
NeedMore,
Unknown
}

public enum OptionType
{
Argument,
Switch,
KeyValue
}
}
43 changes: 41 additions & 2 deletions src/Program.cs
Original file line number Diff line number Diff line change
@@ -1,2 +1,41 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
using CloudFiles.Troubleshooter;

if (args.Length == 0)
{
goto help;
}

if (FindCommand(args) is { } handler)
{
return handler(args.AsSpan(1));
}

help:
PrintHelp();
return 0;

static void PrintHelp()
{
WriteLine($$"""
cloudfiles-troubleshooter Copyright (C) 2024 iterate GmbH
This program comes with ABSOLUTELY NO WARRANTY
This is free software, and you are welcome to redistribute it
under certain conditions
{{string.Format(UsageFormat, "<Command>", "[Options]")}}
Commands:
prune Nukes a broken sync root from the filesystem.
No, or any other unsupported command will print this Help.
""");
}

static CommandHandler? FindCommand(ReadOnlySpan<string> args)
{
return args[0].ToUpperInvariant() switch
{
"PRUNE" => PruneCommand.Run,
_ => null
};
}
Loading

0 comments on commit e407f56

Please sign in to comment.