Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Platforms/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PackageVersion Include="AathifMahir.Maui.MauiIcons.Cupertino" Version="6.0.0" />
<PackageVersion Include="AathifMahir.Maui.MauiIcons.Material" Version="6.0.0" />
<PackageVersion Include="AcrylicView.Maui" Version="3.0.1" />
<PackageVersion Include="CliFx" Version="2.3.0" />
<PackageVersion Include="CommunityToolkit.Maui" Version="14.0.0" />
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
Expand Down Expand Up @@ -48,4 +49,4 @@
<PackageVersion Include="Xamarin.AndroidX.DocumentFile" Version="1.1.0.3" />
<PackageVersion Include="Yubico.YubiKey" Version="1.15.1" />
</ItemGroup>
</Project>
</Project>
81 changes: 81 additions & 0 deletions src/Platforms/SecureFolderFS.Cli/CliCommandHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System.Runtime.Serialization;
using System.Security.Cryptography;
using CliFx.Infrastructure;
using OwlCore.Storage;
using SecureFolderFS.Core;
using SecureFolderFS.Sdk.Helpers;
using SecureFolderFS.Shared.Models;
using SecureFolderFS.Storage.SystemStorageEx;
using SecureFolderFS.Storage.VirtualFileSystem;

namespace SecureFolderFS.Cli;

internal static class CliCommandHelpers
{
public static IFolder GetVaultFolder(string path)
{
var fullPath = Path.GetFullPath(path);
return new SystemFolderEx(fullPath);
}

public static Dictionary<string, object> BuildMountOptions(string volumeName, bool readOnly, string? mountPoint)
{
var options = new Dictionary<string, object>
{
[nameof(VirtualFileSystemOptions.VolumeName)] = FormattingHelpers.SanitizeVolumeName(volumeName, "Vault"),
[nameof(VirtualFileSystemOptions.IsReadOnly)] = readOnly
};

if (!string.IsNullOrWhiteSpace(mountPoint))
options["MountPoint"] = Path.GetFullPath(mountPoint);

return options;
}

public static VaultOptions BuildVaultOptions(string[] methods, string vaultId, string? contentCipher, string? fileNameCipher)
{
return new VaultOptions
{
UnlockProcedure = new AuthenticationMethod(methods, null),
VaultId = vaultId,
ContentCipherId = string.IsNullOrWhiteSpace(contentCipher)
? Core.Cryptography.Constants.CipherId.AES_GCM
: contentCipher,
FileNameCipherId = string.IsNullOrWhiteSpace(fileNameCipher)
? Core.Cryptography.Constants.CipherId.AES_SIV
: fileNameCipher,
};
}

public static int HandleException(Exception ex, IConsole console, CliGlobalOptions options)
{
switch (ex)
{
case CryptographicException:
case FormatException:
CliOutput.Error(console, options, ex.Message);
return CliExitCodes.AuthenticationFailure;

case FileNotFoundException:
case DirectoryNotFoundException:
case SerializationException:
CliOutput.Error(console, options, ex.Message);
return CliExitCodes.VaultUnreadable;

case NotSupportedException:
CliOutput.Error(console, options, ex.Message);
return CliExitCodes.MountFailure;

case InvalidOperationException:
case ArgumentException:
CliOutput.Error(console, options, ex.Message);
return CliExitCodes.BadArguments;

default:
CliOutput.Error(console, options, ex.ToString());
return CliExitCodes.GeneralError;
}
}
}


13 changes: 13 additions & 0 deletions src/Platforms/SecureFolderFS.Cli/CliExitCodes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace SecureFolderFS.Cli;

internal static class CliExitCodes
{
public const int Success = 0;
public const int GeneralError = 1;
public const int BadArguments = 2;
public const int AuthenticationFailure = 3;
public const int VaultUnreadable = 4;
public const int MountFailure = 5;
public const int MountStateError = 6;
}

17 changes: 17 additions & 0 deletions src/Platforms/SecureFolderFS.Cli/CliGlobalOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using CliFx.Attributes;
using CliFx.Infrastructure;

namespace SecureFolderFS.Cli;

public abstract class CliGlobalOptions : CliFx.ICommand
{
[CommandOption("quiet", 'q', Description = "Suppress decorative/info output. Errors always go to stderr.")]
public bool Quiet { get; init; }

[CommandOption("no-color", Description = "Disable ANSI color output.")]
public bool NoColor { get; init; }

public abstract ValueTask ExecuteAsync(IConsole console);
}


49 changes: 49 additions & 0 deletions src/Platforms/SecureFolderFS.Cli/CliLifecycleHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OwlCore.Storage;
using SecureFolderFS.Sdk.Services;
using SecureFolderFS.Shared.Extensions;
using SecureFolderFS.UI.Helpers;
using SecureFolderFS.UI.ServiceImplementation;
using AddService = Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions;

namespace SecureFolderFS.Cli
{
internal sealed class CliLifecycleHelper : BaseLifecycleHelper
{
/// <inheritdoc/>
public override string AppDirectory { get; } = Directory.GetCurrentDirectory();

/// <inheritdoc/>
public override void LogExceptionToFile(Exception? ex)
{
_ = ex; // No-op
}

/// <inheritdoc/>
protected override IServiceCollection ConfigureServices(IModifiableFolder settingsFolder)
{
return base.ConfigureServices(settingsFolder)
.Override<IVaultFileSystemService, CliVaultFileSystemService>(AddService.AddSingleton)
.Override<IVaultCredentialsService, CliVaultCredentialsService>(AddService.AddSingleton)
.Override<ITelemetryService, DebugTelemetryService>(AddService.AddSingleton)
.Override<CredentialReader, CredentialReader>(AddService.AddSingleton);
}

/// <inheritdoc/>
protected override IServiceCollection WithLogging(IServiceCollection serviceCollection)
{
return serviceCollection
.AddLogging(builder =>
{
builder.ClearProviders();
builder.SetMinimumLevel(LogLevel.Information);
builder.AddSimpleConsole(options =>
{
options.SingleLine = true;
options.TimestampFormat = "HH:mm:ss ";
});
});
}
}
}
53 changes: 53 additions & 0 deletions src/Platforms/SecureFolderFS.Cli/CliOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Text.RegularExpressions;
using CliFx.Infrastructure;

namespace SecureFolderFS.Cli;

internal static partial class CliOutput
{
private const string Reset = "\u001b[0m";
private const string Red = "\u001b[31m";
private const string Yellow = "\u001b[33m";
private const string Green = "\u001b[32m";

public static void Info(IConsole console, CliGlobalOptions options, string message)
{
if (options.Quiet)
return;

console.Output.WriteLine(StripIfNeeded(options, message));
}

public static void Success(IConsole console, CliGlobalOptions options, string message)
{
if (options.Quiet)
return;

var line = options.NoColor ? message : $"{Green}{message}{Reset}";
console.Output.WriteLine(StripIfNeeded(options, line));
}

public static void Warning(IConsole console, CliGlobalOptions options, string message)
{
if (options.Quiet)
return;

var line = options.NoColor ? $"warning: {message}" : $"{Yellow}warning:{Reset} {message}";
console.Output.WriteLine(StripIfNeeded(options, line));
}

public static void Error(IConsole console, CliGlobalOptions options, string message)
{
var line = options.NoColor ? $"error: {message}" : $"{Red}error:{Reset} {message}";
console.Error.WriteLine(StripIfNeeded(options, line));
}

public static string StripIfNeeded(CliGlobalOptions options, string value)
{
return options.NoColor ? AnsiRegex().Replace(value, string.Empty) : value;
}

[GeneratedRegex("\\x1B\\[[0-9;]*[A-Za-z]")]
private static partial Regex AnsiRegex();
}

13 changes: 13 additions & 0 deletions src/Platforms/SecureFolderFS.Cli/CliTypeActivator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.Extensions.DependencyInjection;

namespace SecureFolderFS.Cli;

internal static class CliTypeActivator
{
public static object CreateInstance(IServiceProvider serviceProvider, Type type)
{
return ActivatorUtilities.CreateInstance(serviceProvider, type);
}
}


27 changes: 27 additions & 0 deletions src/Platforms/SecureFolderFS.Cli/CliVaultCredentialsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Threading;
using OwlCore.Storage;
using SecureFolderFS.Sdk.ViewModels.Controls.Authentication;
using SecureFolderFS.UI.ServiceImplementation;

namespace SecureFolderFS.Cli;

internal sealed class CliVaultCredentialsService : BaseVaultCredentialsService
{
public override async IAsyncEnumerable<AuthenticationViewModel> GetLoginAsync(IFolder vaultFolder, CancellationToken cancellationToken = default)
{
_ = vaultFolder;
await Task.CompletedTask;
yield break;
}

public override async IAsyncEnumerable<AuthenticationViewModel> GetCreationAsync(IFolder vaultFolder, string vaultId,
CancellationToken cancellationToken = default)
{
_ = vaultFolder;
_ = vaultId;
await Task.CompletedTask;
yield break;
}
}

40 changes: 40 additions & 0 deletions src/Platforms/SecureFolderFS.Cli/CliVaultFileSystemService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using SecureFolderFS.Core.FUSE;
using SecureFolderFS.Core.WebDav;
using SecureFolderFS.Sdk.Enums;
using SecureFolderFS.Sdk.Models;
using SecureFolderFS.Sdk.ViewModels.Views.Wizard.DataSources;
using SecureFolderFS.Storage.VirtualFileSystem;
using SecureFolderFS.UI.ServiceImplementation;

namespace SecureFolderFS.Cli;

internal sealed class CliVaultFileSystemService : BaseVaultFileSystemService
{
public override async IAsyncEnumerable<IFileSystemInfo> GetFileSystemsAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.CompletedTask;

// Keep ordering aligned with desktop targets: WebDAV first, then native adapters.
yield return new CliWebDavFileSystem();
yield return new FuseFileSystem();

#if SFFS_WINDOWS_FS
yield return new SecureFolderFS.Core.WinFsp.WinFspFileSystem();
yield return new SecureFolderFS.Core.Dokany.DokanyFileSystem();
#endif
}

public override async IAsyncEnumerable<BaseDataSourceWizardViewModel> GetSourcesAsync(IVaultCollectionModel vaultCollectionModel, NewVaultMode mode,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
_ = vaultCollectionModel;
_ = mode;
await Task.CompletedTask;
yield break;
}
}

31 changes: 31 additions & 0 deletions src/Platforms/SecureFolderFS.Cli/CliWebDavFileSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using NWebDav.Server.Dispatching;
using OwlCore.Storage.Memory;
using SecureFolderFS.Core.FileSystem;
using SecureFolderFS.Core.FileSystem.Storage;
using SecureFolderFS.Core.WebDav;
using SecureFolderFS.Core.WebDav.AppModels;
using SecureFolderFS.Storage.VirtualFileSystem;

namespace SecureFolderFS.Cli;

internal sealed class CliWebDavFileSystem : WebDavFileSystem
{
protected override async Task<IVfsRoot> MountAsync(FileSystemSpecifics specifics, HttpListener listener, WebDavOptions options,
IRequestDispatcher requestDispatcher, CancellationToken cancellationToken)
{
await Task.CompletedTask;

var remotePath = $"{options.Protocol}://{options.Domain}:{options.Port}/";
var webDavWrapper = new WebDavWrapper(listener, requestDispatcher, remotePath);
webDavWrapper.StartFileSystem();

var virtualizedRoot = new MemoryFolder(remotePath, options.VolumeName);
var plaintextRoot = new CryptoFolder(Path.DirectorySeparatorChar.ToString(), specifics.ContentFolder, specifics);
return new WebDavVfsRoot(webDavWrapper, virtualizedRoot, plaintextRoot, specifics);
}
}

Loading
Loading