Skip to content

Commit 387dda9

Browse files
authored
Begin work on Cli (#120)
1 parent 4ef5eec commit 387dda9

39 files changed

Lines changed: 2319 additions & 17 deletions

src/Platforms/Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PackageVersion Include="AathifMahir.Maui.MauiIcons.Cupertino" Version="6.0.0" />
44
<PackageVersion Include="AathifMahir.Maui.MauiIcons.Material" Version="6.0.0" />
55
<PackageVersion Include="AcrylicView.Maui" Version="3.0.1" />
6+
<PackageVersion Include="CliFx" Version="2.3.0" />
67
<PackageVersion Include="CommunityToolkit.Maui" Version="14.0.0" />
78
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.251219" />
89
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
@@ -48,4 +49,4 @@
4849
<PackageVersion Include="Xamarin.AndroidX.DocumentFile" Version="1.1.0.3" />
4950
<PackageVersion Include="Yubico.YubiKey" Version="1.15.1" />
5051
</ItemGroup>
51-
</Project>
52+
</Project>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System.Runtime.Serialization;
2+
using System.Security.Cryptography;
3+
using CliFx.Infrastructure;
4+
using OwlCore.Storage;
5+
using SecureFolderFS.Core;
6+
using SecureFolderFS.Sdk.Helpers;
7+
using SecureFolderFS.Shared.Models;
8+
using SecureFolderFS.Storage.SystemStorageEx;
9+
using SecureFolderFS.Storage.VirtualFileSystem;
10+
11+
namespace SecureFolderFS.Cli;
12+
13+
internal static class CliCommandHelpers
14+
{
15+
public static IFolder GetVaultFolder(string path)
16+
{
17+
var fullPath = Path.GetFullPath(path);
18+
return new SystemFolderEx(fullPath);
19+
}
20+
21+
public static Dictionary<string, object> BuildMountOptions(string volumeName, bool readOnly, string? mountPoint)
22+
{
23+
var options = new Dictionary<string, object>
24+
{
25+
[nameof(VirtualFileSystemOptions.VolumeName)] = FormattingHelpers.SanitizeVolumeName(volumeName, "Vault"),
26+
[nameof(VirtualFileSystemOptions.IsReadOnly)] = readOnly
27+
};
28+
29+
if (!string.IsNullOrWhiteSpace(mountPoint))
30+
options["MountPoint"] = Path.GetFullPath(mountPoint);
31+
32+
return options;
33+
}
34+
35+
public static VaultOptions BuildVaultOptions(string[] methods, string vaultId, string? contentCipher, string? fileNameCipher)
36+
{
37+
return new VaultOptions
38+
{
39+
UnlockProcedure = new AuthenticationMethod(methods, null),
40+
VaultId = vaultId,
41+
ContentCipherId = string.IsNullOrWhiteSpace(contentCipher)
42+
? Core.Cryptography.Constants.CipherId.AES_GCM
43+
: contentCipher,
44+
FileNameCipherId = string.IsNullOrWhiteSpace(fileNameCipher)
45+
? Core.Cryptography.Constants.CipherId.AES_SIV
46+
: fileNameCipher,
47+
};
48+
}
49+
50+
public static int HandleException(Exception ex, IConsole console, CliGlobalOptions options)
51+
{
52+
switch (ex)
53+
{
54+
case CryptographicException:
55+
case FormatException:
56+
CliOutput.Error(console, options, ex.Message);
57+
return CliExitCodes.AuthenticationFailure;
58+
59+
case FileNotFoundException:
60+
case DirectoryNotFoundException:
61+
case SerializationException:
62+
CliOutput.Error(console, options, ex.Message);
63+
return CliExitCodes.VaultUnreadable;
64+
65+
case NotSupportedException:
66+
CliOutput.Error(console, options, ex.Message);
67+
return CliExitCodes.MountFailure;
68+
69+
case InvalidOperationException:
70+
case ArgumentException:
71+
CliOutput.Error(console, options, ex.Message);
72+
return CliExitCodes.BadArguments;
73+
74+
default:
75+
CliOutput.Error(console, options, ex.ToString());
76+
return CliExitCodes.GeneralError;
77+
}
78+
}
79+
}
80+
81+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace SecureFolderFS.Cli;
2+
3+
internal static class CliExitCodes
4+
{
5+
public const int Success = 0;
6+
public const int GeneralError = 1;
7+
public const int BadArguments = 2;
8+
public const int AuthenticationFailure = 3;
9+
public const int VaultUnreadable = 4;
10+
public const int MountFailure = 5;
11+
public const int MountStateError = 6;
12+
}
13+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using CliFx.Attributes;
2+
using CliFx.Infrastructure;
3+
4+
namespace SecureFolderFS.Cli;
5+
6+
public abstract class CliGlobalOptions : CliFx.ICommand
7+
{
8+
[CommandOption("quiet", 'q', Description = "Suppress decorative/info output. Errors always go to stderr.")]
9+
public bool Quiet { get; init; }
10+
11+
[CommandOption("no-color", Description = "Disable ANSI color output.")]
12+
public bool NoColor { get; init; }
13+
14+
public abstract ValueTask ExecuteAsync(IConsole console);
15+
}
16+
17+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Logging;
3+
using OwlCore.Storage;
4+
using SecureFolderFS.Sdk.Services;
5+
using SecureFolderFS.Shared.Extensions;
6+
using SecureFolderFS.UI.Helpers;
7+
using SecureFolderFS.UI.ServiceImplementation;
8+
using AddService = Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions;
9+
10+
namespace SecureFolderFS.Cli
11+
{
12+
internal sealed class CliLifecycleHelper : BaseLifecycleHelper
13+
{
14+
/// <inheritdoc/>
15+
public override string AppDirectory { get; } = Directory.GetCurrentDirectory();
16+
17+
/// <inheritdoc/>
18+
public override void LogExceptionToFile(Exception? ex)
19+
{
20+
_ = ex; // No-op
21+
}
22+
23+
/// <inheritdoc/>
24+
protected override IServiceCollection ConfigureServices(IModifiableFolder settingsFolder)
25+
{
26+
return base.ConfigureServices(settingsFolder)
27+
.Override<IVaultFileSystemService, CliVaultFileSystemService>(AddService.AddSingleton)
28+
.Override<IVaultCredentialsService, CliVaultCredentialsService>(AddService.AddSingleton)
29+
.Override<ITelemetryService, DebugTelemetryService>(AddService.AddSingleton)
30+
.Override<CredentialReader, CredentialReader>(AddService.AddSingleton);
31+
}
32+
33+
/// <inheritdoc/>
34+
protected override IServiceCollection WithLogging(IServiceCollection serviceCollection)
35+
{
36+
return serviceCollection
37+
.AddLogging(builder =>
38+
{
39+
builder.ClearProviders();
40+
builder.SetMinimumLevel(LogLevel.Information);
41+
builder.AddSimpleConsole(options =>
42+
{
43+
options.SingleLine = true;
44+
options.TimestampFormat = "HH:mm:ss ";
45+
});
46+
});
47+
}
48+
}
49+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Text.RegularExpressions;
2+
using CliFx.Infrastructure;
3+
4+
namespace SecureFolderFS.Cli;
5+
6+
internal static partial class CliOutput
7+
{
8+
private const string Reset = "\u001b[0m";
9+
private const string Red = "\u001b[31m";
10+
private const string Yellow = "\u001b[33m";
11+
private const string Green = "\u001b[32m";
12+
13+
public static void Info(IConsole console, CliGlobalOptions options, string message)
14+
{
15+
if (options.Quiet)
16+
return;
17+
18+
console.Output.WriteLine(StripIfNeeded(options, message));
19+
}
20+
21+
public static void Success(IConsole console, CliGlobalOptions options, string message)
22+
{
23+
if (options.Quiet)
24+
return;
25+
26+
var line = options.NoColor ? message : $"{Green}{message}{Reset}";
27+
console.Output.WriteLine(StripIfNeeded(options, line));
28+
}
29+
30+
public static void Warning(IConsole console, CliGlobalOptions options, string message)
31+
{
32+
if (options.Quiet)
33+
return;
34+
35+
var line = options.NoColor ? $"warning: {message}" : $"{Yellow}warning:{Reset} {message}";
36+
console.Output.WriteLine(StripIfNeeded(options, line));
37+
}
38+
39+
public static void Error(IConsole console, CliGlobalOptions options, string message)
40+
{
41+
var line = options.NoColor ? $"error: {message}" : $"{Red}error:{Reset} {message}";
42+
console.Error.WriteLine(StripIfNeeded(options, line));
43+
}
44+
45+
public static string StripIfNeeded(CliGlobalOptions options, string value)
46+
{
47+
return options.NoColor ? AnsiRegex().Replace(value, string.Empty) : value;
48+
}
49+
50+
[GeneratedRegex("\\x1B\\[[0-9;]*[A-Za-z]")]
51+
private static partial Regex AnsiRegex();
52+
}
53+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
3+
namespace SecureFolderFS.Cli;
4+
5+
internal static class CliTypeActivator
6+
{
7+
public static object CreateInstance(IServiceProvider serviceProvider, Type type)
8+
{
9+
return ActivatorUtilities.CreateInstance(serviceProvider, type);
10+
}
11+
}
12+
13+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Collections.Generic;
2+
using System.Threading;
3+
using OwlCore.Storage;
4+
using SecureFolderFS.Sdk.ViewModels.Controls.Authentication;
5+
using SecureFolderFS.UI.ServiceImplementation;
6+
7+
namespace SecureFolderFS.Cli;
8+
9+
internal sealed class CliVaultCredentialsService : BaseVaultCredentialsService
10+
{
11+
public override async IAsyncEnumerable<AuthenticationViewModel> GetLoginAsync(IFolder vaultFolder, CancellationToken cancellationToken = default)
12+
{
13+
_ = vaultFolder;
14+
await Task.CompletedTask;
15+
yield break;
16+
}
17+
18+
public override async IAsyncEnumerable<AuthenticationViewModel> GetCreationAsync(IFolder vaultFolder, string vaultId,
19+
CancellationToken cancellationToken = default)
20+
{
21+
_ = vaultFolder;
22+
_ = vaultId;
23+
await Task.CompletedTask;
24+
yield break;
25+
}
26+
}
27+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Collections.Generic;
2+
using System.Runtime.CompilerServices;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using SecureFolderFS.Core.FUSE;
6+
using SecureFolderFS.Core.WebDav;
7+
using SecureFolderFS.Sdk.Enums;
8+
using SecureFolderFS.Sdk.Models;
9+
using SecureFolderFS.Sdk.ViewModels.Views.Wizard.DataSources;
10+
using SecureFolderFS.Storage.VirtualFileSystem;
11+
using SecureFolderFS.UI.ServiceImplementation;
12+
13+
namespace SecureFolderFS.Cli;
14+
15+
internal sealed class CliVaultFileSystemService : BaseVaultFileSystemService
16+
{
17+
public override async IAsyncEnumerable<IFileSystemInfo> GetFileSystemsAsync([EnumeratorCancellation] CancellationToken cancellationToken)
18+
{
19+
await Task.CompletedTask;
20+
21+
// Keep ordering aligned with desktop targets: WebDAV first, then native adapters.
22+
yield return new CliWebDavFileSystem();
23+
yield return new FuseFileSystem();
24+
25+
#if SFFS_WINDOWS_FS
26+
yield return new SecureFolderFS.Core.WinFsp.WinFspFileSystem();
27+
yield return new SecureFolderFS.Core.Dokany.DokanyFileSystem();
28+
#endif
29+
}
30+
31+
public override async IAsyncEnumerable<BaseDataSourceWizardViewModel> GetSourcesAsync(IVaultCollectionModel vaultCollectionModel, NewVaultMode mode,
32+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
33+
{
34+
_ = vaultCollectionModel;
35+
_ = mode;
36+
await Task.CompletedTask;
37+
yield break;
38+
}
39+
}
40+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.IO;
2+
using System.Net;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using NWebDav.Server.Dispatching;
6+
using OwlCore.Storage.Memory;
7+
using SecureFolderFS.Core.FileSystem;
8+
using SecureFolderFS.Core.FileSystem.Storage;
9+
using SecureFolderFS.Core.WebDav;
10+
using SecureFolderFS.Core.WebDav.AppModels;
11+
using SecureFolderFS.Storage.VirtualFileSystem;
12+
13+
namespace SecureFolderFS.Cli;
14+
15+
internal sealed class CliWebDavFileSystem : WebDavFileSystem
16+
{
17+
protected override async Task<IVfsRoot> MountAsync(FileSystemSpecifics specifics, HttpListener listener, WebDavOptions options,
18+
IRequestDispatcher requestDispatcher, CancellationToken cancellationToken)
19+
{
20+
await Task.CompletedTask;
21+
22+
var remotePath = $"{options.Protocol}://{options.Domain}:{options.Port}/";
23+
var webDavWrapper = new WebDavWrapper(listener, requestDispatcher, remotePath);
24+
webDavWrapper.StartFileSystem();
25+
26+
var virtualizedRoot = new MemoryFolder(remotePath, options.VolumeName);
27+
var plaintextRoot = new CryptoFolder(Path.DirectorySeparatorChar.ToString(), specifics.ContentFolder, specifics);
28+
return new WebDavVfsRoot(webDavWrapper, virtualizedRoot, plaintextRoot, specifics);
29+
}
30+
}
31+

0 commit comments

Comments
 (0)