diff --git a/global.json b/global.json
index f42869719..667d3dc68 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,8 @@
{
"msbuild-sdks": {
- "Uno.Sdk": "5.4.10"
+ "Uno.Sdk": "5.6.20"
+ },
+ "sdk": {
+ "allowPrerelease": true
}
}
diff --git a/lib/Tmds.Fuse b/lib/Tmds.Fuse
index 73663e78b..e4bf64788 160000
--- a/lib/Tmds.Fuse
+++ b/lib/Tmds.Fuse
@@ -1 +1 @@
-Subproject commit 73663e78ba98e20850ecad10489028f2afedd60f
+Subproject commit e4bf6478812ba61773f0a7e6f42710a2b4a70bdd
diff --git a/lib/nwebdav b/lib/nwebdav
index 17fcebe4d..04c03b7d4 160000
--- a/lib/nwebdav
+++ b/lib/nwebdav
@@ -1 +1 @@
-Subproject commit 17fcebe4d06338f94122400c2c9cfdbb5ea8c77e
+Subproject commit 04c03b7d47ec54046919d33d41d6761d4ccc5318
diff --git a/src/Platforms/Directory.Packages.props b/src/Platforms/Directory.Packages.props
index 6cbaa6ad1..ccf56d713 100644
--- a/src/Platforms/Directory.Packages.props
+++ b/src/Platforms/Directory.Packages.props
@@ -2,31 +2,29 @@
-
-
-
+
+
+
-
+
-
-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
+
@@ -35,6 +33,7 @@
-
+
+
-
+
\ No newline at end of file
diff --git a/src/Platforms/SecureFolderFS.Maui/App.xaml b/src/Platforms/SecureFolderFS.Maui/App.xaml
index 11e594fae..cd4219349 100644
--- a/src/Platforms/SecureFolderFS.Maui/App.xaml
+++ b/src/Platforms/SecureFolderFS.Maui/App.xaml
@@ -1,17 +1,14 @@
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
-
+
+
-
-
-
diff --git a/src/Platforms/SecureFolderFS.Maui/App.xaml.cs b/src/Platforms/SecureFolderFS.Maui/App.xaml.cs
index 4fa5aaaa5..fc0380789 100644
--- a/src/Platforms/SecureFolderFS.Maui/App.xaml.cs
+++ b/src/Platforms/SecureFolderFS.Maui/App.xaml.cs
@@ -1,4 +1,6 @@
-using CommunityToolkit.Mvvm.DependencyInjection;
+using APES.UI.XF;
+using SecureFolderFS.Maui.Extensions.Mappers;
+using SecureFolderFS.Maui.Helpers;
using SecureFolderFS.Shared;
using SecureFolderFS.UI.Helpers;
@@ -6,8 +8,8 @@ namespace SecureFolderFS.Maui
{
public partial class App : Application
{
- public static App Instance => (App)Application.Current!;
-
+ public static App Instance => (App)Current!;
+
public IServiceProvider? ServiceProvider { get; private set; }
public BaseLifecycleHelper ApplicationLifecycle { get; } =
@@ -18,7 +20,7 @@ public partial class App : Application
#else
null;
#endif
-
+
public event EventHandler? AppResumed;
public event EventHandler? AppPutToForeground;
@@ -26,6 +28,11 @@ public App()
{
InitializeComponent();
+ // Configure mappers
+ CustomMappers.AddEntryMappers();
+ CustomMappers.AddLabelMappers();
+ CustomMappers.AddPickerMappers();
+
// Configure exception handlers
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
@@ -33,8 +40,8 @@ public App()
protected override Window CreateWindow(IActivationState? activationState)
{
- APES.UI.XF.ContextMenuContainer.Init();
-
+ ContextMenuContainer.Init();
+
var appShell = Task.Run(GetAppShellAsync).ConfigureAwait(false).GetAwaiter().GetResult();
return new Window(appShell);
}
@@ -49,10 +56,13 @@ private async Task GetAppShellAsync()
// Register IoC
DI.Default.SetServiceProvider(ServiceProvider);
-
+
// Create and initialize AppShell
var appShell = new AppShell();
- await appShell.MainViewModel.InitAsync();
+ await appShell.MainViewModel.InitAsync().ConfigureAwait(false);
+
+ // Initialize ThemeHelper
+ await MauiThemeHelper.Instance.InitAsync().ConfigureAwait(false);
return appShell;
}
diff --git a/src/Platforms/SecureFolderFS.Maui/AppModels/ImageStream.cs b/src/Platforms/SecureFolderFS.Maui/AppModels/ImageStream.cs
index d80b3989a..a89532d9f 100644
--- a/src/Platforms/SecureFolderFS.Maui/AppModels/ImageStream.cs
+++ b/src/Platforms/SecureFolderFS.Maui/AppModels/ImageStream.cs
@@ -1,25 +1,37 @@
+using SecureFolderFS.Shared.ComponentModel;
using IImage = SecureFolderFS.Shared.ComponentModel.IImage;
namespace SecureFolderFS.Maui.AppModels
{
///
- internal sealed class ImageStream : IImage
+ internal sealed class ImageStream : IImageStream
{
- private readonly Stream _stream;
-
+ public Stream Stream { get; }
+
public StreamImageSource Source { get; }
public ImageStream(Stream stream)
{
- _stream = stream;
+ Stream = stream;
Source = new();
- Source.Stream = (cancellationToken) => Task.FromResult(stream);
+ Source.Stream = (ct) => Task.FromResult(stream);
+ }
+
+ ///
+ public async Task CopyToAsync(Stream destination, CancellationToken cancellationToken = default)
+ {
+ var savedPosition = Stream.Position;
+ await Stream.CopyToAsync(destination, cancellationToken);
+ Stream.Position = savedPosition;
}
///
public void Dispose()
{
- _stream.Dispose();
+ if (Stream is OnDemandDisposableStream onDemandDisposableStream)
+ onDemandDisposableStream.ForceClose();
+ else
+ Stream.Dispose();
}
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/AppModels/OnDemandDisposableStream.cs b/src/Platforms/SecureFolderFS.Maui/AppModels/OnDemandDisposableStream.cs
new file mode 100644
index 000000000..858bd0397
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/AppModels/OnDemandDisposableStream.cs
@@ -0,0 +1,16 @@
+namespace SecureFolderFS.Maui.AppModels
+{
+ internal sealed class OnDemandDisposableStream : MemoryStream
+ {
+ public void ForceClose()
+ {
+ base.Dispose(true);
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ _ = disposing;
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/AppModels/VideoStreamServer.cs b/src/Platforms/SecureFolderFS.Maui/AppModels/VideoStreamServer.cs
index 794d66bdc..de9d53483 100644
--- a/src/Platforms/SecureFolderFS.Maui/AppModels/VideoStreamServer.cs
+++ b/src/Platforms/SecureFolderFS.Maui/AppModels/VideoStreamServer.cs
@@ -32,7 +32,7 @@ public Task InitAsync(CancellationToken cancellationToken = default)
{
_httpListener.Start();
_ = BeginListeningAsync();
-
+
return Task.CompletedTask;
async Task BeginListeningAsync()
@@ -41,24 +41,29 @@ async Task BeginListeningAsync()
{
try
{
+ var response = context.Response;
if (context.Request.RawUrl != "/video")
{
- context.Response.StatusCode = (int)HttpStatusCode.NotFound;
- context.Response.Close();
+ response.StatusCode = (int)HttpStatusCode.NotFound;
+ response.Close();
continue;
}
-
- context.Response.StatusCode = (int)HttpStatusCode.OK;
- context.Response.ContentType = _mimeType;
- context.Response.ContentLength64 = _videoStream.Length;
- context.Response.Headers["Accept-Ranges"] = "bytes";
- await _videoStream.CopyToAsync(context.Response.OutputStream, 64 * 1024, cancellationToken);
+ response.ContentType = _mimeType;
+ response.ContentLength64 = _videoStream.Length;
+ response.Headers["Accept-Ranges"] = "bytes";
+
+ await _videoStream.CopyToAsync(response.OutputStream, 64 * 1024, cancellationToken);
+ await _videoStream.FlushAsync(cancellationToken);
_videoStream.Position = 0L;
- break;
+
+ response.StatusCode = (int)HttpStatusCode.OK;
+ response.StatusDescription = "OK";
+ response.Close();
}
- catch (Exception)
+ catch (Exception ex)
{
+ _ = ex;
break;
}
}
@@ -72,7 +77,7 @@ public void Dispose()
_videoStream.Dispose();
_httpListener.Abort();
}
-
+
private static int GetAvailablePort()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
diff --git a/src/Platforms/SecureFolderFS.Maui/AppShell.xaml.cs b/src/Platforms/SecureFolderFS.Maui/AppShell.xaml.cs
index 76aa8d21c..61d1f48d7 100644
--- a/src/Platforms/SecureFolderFS.Maui/AppShell.xaml.cs
+++ b/src/Platforms/SecureFolderFS.Maui/AppShell.xaml.cs
@@ -15,6 +15,7 @@ public AppShell()
Routing.RegisterRoute("LoginPage", typeof(LoginPage));
Routing.RegisterRoute("OverviewPage", typeof(OverviewPage));
Routing.RegisterRoute("BrowserPage", typeof(BrowserPage));
+ Routing.RegisterRoute("HealthPage", typeof(HealthPage));
}
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/Extensions/IocExtensions.cs b/src/Platforms/SecureFolderFS.Maui/Extensions/IocExtensions.cs
index e156409ce..ff5e0b3db 100644
--- a/src/Platforms/SecureFolderFS.Maui/Extensions/IocExtensions.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Extensions/IocExtensions.cs
@@ -1,6 +1,9 @@
using OwlCore.Storage;
+using Plugin.Maui.BottomSheet.Hosting;
+using Plugin.Maui.BottomSheet.Navigation;
using SecureFolderFS.Maui.ServiceImplementation;
using SecureFolderFS.Maui.ServiceImplementation.Settings;
+using SecureFolderFS.Maui.Sheets;
using SecureFolderFS.Sdk.Services;
using SecureFolderFS.UI.ServiceImplementation;
using SecureFolderFS.UI.ServiceImplementation.Settings;
@@ -14,10 +17,12 @@ public static IServiceCollection WithMauiServices(this IServiceCollection servic
return serviceCollection
.AddSingleton(_ => new(new MauiAppSettings(settingsFolder), new UserSettings(settingsFolder)))
.AddSingleton()
+ .AddSingleton()
.AddSingleton()
- .AddTransient()
.AddSingleton()
- //.AddSingleton()
+ .AddSingleton()
+ .AddTransient()
+ .AddBottomSheet(nameof(ViewOptionsSheet))
;
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Entry.cs b/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Entry.cs
new file mode 100644
index 000000000..297266790
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Entry.cs
@@ -0,0 +1,38 @@
+using Microsoft.Maui.Handlers;
+using Microsoft.Maui.Platform;
+using SecureFolderFS.Maui.UserControls.Common;
+
+#if ANDROID
+using Android.Graphics.Drawables.Shapes;
+using Paint = Android.Graphics.Paint;
+using ShapeDrawable = Android.Graphics.Drawables.ShapeDrawable;
+#endif
+
+namespace SecureFolderFS.Maui.Extensions.Mappers
+{
+ public static partial class CustomMappers
+ {
+ public static void AddEntryMappers()
+ {
+ EntryHandler.Mapper.AppendToMapping($"{nameof(CustomMappers)}.{nameof(Entry)}", (handler, view) =>
+ {
+ if (view is not ModernEntry)
+ return;
+#if ANDROID
+ var outerRadii = Enumerable.Range(1, 8).Select(_ => 24f).ToArray();
+ var roundRectShape = new RoundRectShape(outerRadii, null, null);
+ var shape = new ShapeDrawable(roundRectShape);
+
+ shape.Paint!.Color = (App.Instance.Resources["BorderLightColor"] as Color)!.ToPlatform();
+ shape.Paint.StrokeWidth = 4;
+ shape.Paint.SetStyle(Paint.Style.Stroke);
+ handler.PlatformView.Background = shape;
+ handler.PlatformView.SetPadding(40, 32,40, 32);
+#elif IOS
+ handler.PlatformView.Layer.BorderColor = (App.Current.Resources["BorderLightColor"] as Color).ToPlatform().CGColor;
+ handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.RoundedRect;
+#endif
+ });
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Label.cs b/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Label.cs
new file mode 100644
index 000000000..6a996d42f
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Label.cs
@@ -0,0 +1,21 @@
+using Microsoft.Maui.Handlers;
+using SecureFolderFS.Maui.UserControls.Common;
+
+namespace SecureFolderFS.Maui.Extensions.Mappers
+{
+ public static partial class CustomMappers
+ {
+ public static void AddLabelMappers()
+ {
+ LabelHandler.Mapper.AppendToMapping($"{nameof(CustomMappers)}.{nameof(Label)}", (handler, view) =>
+ {
+ if (view is not SelectableLabel)
+ return;
+
+#if ANDROID
+ handler.PlatformView.SetTextIsSelectable(true);
+#endif
+ });
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Picker.cs b/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Picker.cs
new file mode 100644
index 000000000..647ffe472
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Extensions/Mappers/CustomMappers.Picker.cs
@@ -0,0 +1,38 @@
+using Microsoft.Maui.Handlers;
+using Microsoft.Maui.Platform;
+using SecureFolderFS.Maui.UserControls.Common;
+#if ANDROID
+using Android.Graphics.Drawables.Shapes;
+using Paint = Android.Graphics.Paint;
+using ShapeDrawable = Android.Graphics.Drawables.ShapeDrawable;
+#endif
+
+namespace SecureFolderFS.Maui.Extensions.Mappers
+{
+ public static partial class CustomMappers
+ {
+ public static void AddPickerMappers()
+ {
+ PickerHandler.Mapper.AppendToMapping($"{nameof(CustomMappers)}.{nameof(Picker)}", (handler, view) =>
+ {
+ if (view is not ModernPicker modernPicker)
+ return;
+
+#if ANDROID
+ var outerRadii = Enumerable.Range(1, 8).Select(_ => 24f).ToArray();
+ var roundRectShape = new RoundRectShape(outerRadii, null, null);
+ var shape = new ShapeDrawable(roundRectShape);
+
+ shape.Paint!.Color = modernPicker.IsTransparent
+ ? Colors.Transparent.ToPlatform()
+ : (App.Instance.Resources["ThemeSecondaryColorBrush"] as SolidColorBrush)!.Color.ToPlatform();
+ shape.Paint.StrokeWidth = 0;
+ shape.Paint.SetStyle(Paint.Style.FillAndStroke);
+ handler.PlatformView.SetTextColor((App.Instance.Resources["QuarternaryLightColor"] as Color)!.ToPlatform());
+ handler.PlatformView.Background = shape;
+ handler.PlatformView.SetPadding(32, 24, 32, 24);
+#endif
+ });
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Helpers/DeferredInitialization.cs b/src/Platforms/SecureFolderFS.Maui/Helpers/DeferredInitialization.cs
new file mode 100644
index 000000000..0caf1a4df
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Helpers/DeferredInitialization.cs
@@ -0,0 +1,81 @@
+using SecureFolderFS.Shared.ComponentModel;
+
+namespace SecureFolderFS.Maui.Helpers
+{
+ internal sealed class DeferredInitialization : IDisposable
+ where T : notnull
+ {
+ private T? _context;
+ private bool _isProcessing;
+ private readonly List _initializations = new();
+ private readonly SemaphoreSlim _semaphore = new(1, 1);
+
+ public void SetContext(T context)
+ {
+ if (context.Equals(_context))
+ return;
+
+ _context = context;
+ lock (_initializations)
+ _initializations.Clear();
+ }
+
+ public void Enqueue(IAsyncInitialize asyncInitialize)
+ {
+ if (_context is null)
+ return;
+
+ lock (_initializations)
+ _initializations.Add(asyncInitialize);
+
+ _ = StartProcessingAsync();
+ }
+
+ private async Task StartProcessingAsync()
+ {
+ await _semaphore.WaitAsync();
+
+ try
+ {
+ if (_isProcessing)
+ return;
+
+ _isProcessing = true;
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+
+ try
+ {
+ while (true)
+ {
+ IAsyncInitialize[] batch;
+ lock (_initializations)
+ {
+ if (_initializations.Count == 0)
+ break;
+
+ batch = _initializations.Take(UI.Constants.Browser.THUMBNAIL_MAX_PARALLELISATION).ToArray();
+ _initializations.RemoveRange(0, batch.Length);
+ }
+
+ var tasks = batch.Select(init => init.InitAsync());
+ await Task.Run(async () => await Task.WhenAll(tasks));
+ }
+ }
+ finally
+ {
+ _isProcessing = false;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ _initializations.Clear();
+ _semaphore.Dispose();
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Helpers/MauiThemeHelper.cs b/src/Platforms/SecureFolderFS.Maui/Helpers/MauiThemeHelper.cs
new file mode 100644
index 000000000..8d35e86be
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Helpers/MauiThemeHelper.cs
@@ -0,0 +1,19 @@
+using SecureFolderFS.UI.Helpers;
+
+namespace SecureFolderFS.Maui.Helpers
+{
+ ///
+ internal sealed class MauiThemeHelper : ThemeHelper
+ {
+ ///
+ /// Gets the singleton instance of .
+ ///
+ public static MauiThemeHelper Instance { get; } = new();
+
+ ///
+ protected override void UpdateTheme()
+ {
+ App.Instance.UserAppTheme = (AppTheme)(int)CurrentTheme;
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Localization/ResourceString.cs b/src/Platforms/SecureFolderFS.Maui/Localization/ResourceString.cs
index b96c46b33..c8b755050 100644
--- a/src/Platforms/SecureFolderFS.Maui/Localization/ResourceString.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Localization/ResourceString.cs
@@ -5,6 +5,7 @@
namespace SecureFolderFS.Maui.Localization
{
[EditorBrowsable(EditorBrowsableState.Never)]
+ [AcceptEmptyServiceProvider]
internal sealed class ResourceString : IMarkupExtension
{
private static ILocalizationService? LocalizationService { get; set; }
diff --git a/src/Platforms/SecureFolderFS.Maui/MauiProgram.cs b/src/Platforms/SecureFolderFS.Maui/MauiProgram.cs
index 93b27c8a9..1bbebb36f 100644
--- a/src/Platforms/SecureFolderFS.Maui/MauiProgram.cs
+++ b/src/Platforms/SecureFolderFS.Maui/MauiProgram.cs
@@ -1,10 +1,7 @@
using APES.UI.XF;
using CommunityToolkit.Maui;
-using Microsoft.Extensions.Logging;
-using The49.Maui.BottomSheet;
-
+using Plugin.Maui.BottomSheet.Hosting;
#if ANDROID
-using Material.Components.Maui.Extensions;
using MauiIcons.Material;
#elif IOS
using MauiIcons.Cupertino;
@@ -28,16 +25,15 @@ public static MauiApp CreateMauiApp()
// Plugins
.UseMauiCommunityToolkitMediaElement() // https://github.com/CommunityToolkit/Maui
.UseMauiCommunityToolkit() // https://github.com/CommunityToolkit/Maui
- .UseBottomSheet() // https://github.com/the49ltd/The49.Maui.BottomSheet
+ .UseBottomSheet() // https://github.com/lucacivale/Maui.BottomSheet
.ConfigureContextMenuContainer() // https://github.com/anpin/ContextMenuContainer
#if ANDROID
.UseMaterialMauiIcons() // https://github.com/AathifMahir/MauiIcons
- .UseMaterialComponents() // https://github.com/mdc-maui/mdc-maui
#elif IOS
.UseCupertinoMauiIcons()
#endif
-
+
// Handlers
.ConfigureMauiHandlers(handlers =>
{
@@ -45,7 +41,7 @@ public static MauiApp CreateMauiApp()
handlers.AddHandler();
#endif
})
-
+
; // Finish initialization
return builder.Build();
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AndroidManifest.xml b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AndroidManifest.xml
index 19750b79f..5dd70dba8 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AndroidManifest.xml
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AndroidManifest.xml
@@ -16,5 +16,6 @@
+
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/BiometricPromptCallback.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/BiometricPromptCallback.cs
new file mode 100644
index 000000000..a43df0c62
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/BiometricPromptCallback.cs
@@ -0,0 +1,41 @@
+using Android.Hardware.Biometrics;
+using Java.Lang;
+
+namespace SecureFolderFS.Maui.AppModels
+{
+ ///
+ internal sealed class BiometricPromptCallback : BiometricPrompt.AuthenticationCallback
+ {
+ private readonly Action? _onSuccess;
+ private readonly Action? _onError;
+ private readonly Action? _onFailure;
+
+ public BiometricPromptCallback(
+ Action? onSuccess,
+ Action? onError,
+ Action? onFailure)
+ {
+ _onSuccess = onSuccess;
+ _onError = onError;
+ _onFailure = onFailure;
+ }
+
+ ///
+ public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult? result)
+ {
+ _onSuccess?.Invoke(result);
+ }
+
+ ///
+ public override void OnAuthenticationError(BiometricErrorCode errorCode, ICharSequence? errString)
+ {
+ _onError?.Invoke(errorCode, errString?.ToString());
+ }
+
+ ///
+ public override void OnAuthenticationFailed()
+ {
+ _onFailure?.Invoke();
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/DialogOnClickListener.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/DialogOnClickListener.cs
new file mode 100644
index 000000000..7876743dc
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/DialogOnClickListener.cs
@@ -0,0 +1,21 @@
+using Android.Content;
+
+namespace SecureFolderFS.Maui.AppModels
+{
+ ///
+ internal sealed class DialogOnClickListener : Java.Lang.Object, IDialogInterfaceOnClickListener
+ {
+ private readonly Action _onClick;
+
+ public DialogOnClickListener(Action onClick)
+ {
+ _onClick = onClick;
+ }
+
+ ///
+ public void OnClick(IDialogInterface? dialog, int which)
+ {
+ _onClick.Invoke(dialog, which);
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/StreamedMediaSource.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/StreamedMediaSource.cs
new file mode 100644
index 000000000..bff7afcbf
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/AppModels/StreamedMediaSource.cs
@@ -0,0 +1,35 @@
+using Android.Media;
+using Stream = System.IO.Stream;
+
+namespace SecureFolderFS.Maui.AppModels
+{
+ ///
+ internal sealed class StreamedMediaSource : MediaDataSource
+ {
+ private readonly Stream _sourceStream;
+
+ ///
+ public override long Size => _sourceStream.Length;
+
+ public StreamedMediaSource(Stream sourceStream)
+ {
+ _sourceStream = sourceStream;
+ }
+
+ ///
+ public override int ReadAt(long position, byte[]? buffer, int offset, int size)
+ {
+ if (buffer is null)
+ return 0;
+
+ _sourceStream.Position = position;
+ return _sourceStream.Read(buffer, offset, size);
+ }
+
+ ///
+ public override void Close()
+ {
+ _sourceStream.Dispose();
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidBiometricHelpers.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidBiometricHelpers.cs
new file mode 100644
index 000000000..c10efefef
--- /dev/null
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidBiometricHelpers.cs
@@ -0,0 +1,32 @@
+using Android.Security.Keystore;
+using Java.Security;
+
+namespace SecureFolderFS.Maui.Platforms.Android.Helpers
+{
+ internal static class AndroidBiometricHelpers
+ {
+ public static KeyPairGenerator? GetKeyPairGenerator(string keyName, string keyStoreProvider)
+ {
+ var keyPairGenerator = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, keyStoreProvider);
+ if (keyPairGenerator is null)
+ return null;
+
+ var builder = new KeyGenParameterSpec.Builder(
+ keyName,
+ KeyStorePurpose.Sign)
+ .SetDigests(KeyProperties.DigestSha256)
+ .SetSignaturePaddings(KeyProperties.SignaturePaddingRsaPkcs1)
+ .SetUserAuthenticationRequired(true)
+ .SetInvalidatedByBiometricEnrollment(false);
+
+ keyPairGenerator.Initialize(builder.Build());
+ return keyPairGenerator;
+ }
+
+ public static byte[]? SignData(Signature signature, byte[] data)
+ {
+ signature.Update(data);
+ return signature.Sign();
+ }
+ }
+}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidLifecycleHelper.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidLifecycleHelper.cs
index 6d111f2fa..2f9d82222 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidLifecycleHelper.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Helpers/AndroidLifecycleHelper.cs
@@ -48,7 +48,7 @@ protected override IServiceCollection ConfigureServices(IModifiableFolder settin
.AddSingleton()
.AddSingleton()
.AddSingleton()
-
+
.WithMauiServices(settingsFolder)
;
}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/MainActivity.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/MainActivity.cs
index f55b81860..c2a527e42 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/MainActivity.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/MainActivity.cs
@@ -1,6 +1,9 @@
using Android.App;
using Android.Content;
using Android.Content.PM;
+using Android.OS;
+using AndroidX.Activity;
+using Microsoft.Maui.Platform;
namespace SecureFolderFS.Maui
{
@@ -22,6 +25,18 @@ public MainActivity()
Instance ??= this;
}
+ ///
+ protected override void OnCreate(Bundle? savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+
+ // Enable edge to edge
+ EdgeToEdge.Enable(this);
+#pragma warning disable CA1422
+ Window?.SetStatusBarColor((App.Instance.Resources["PrimaryLightColor"] as Color)!.ToPlatform());
+#pragma warning restore CA1422
+ }
+
///
protected override void OnActivityResult(int requestCode, Result resultCode, Intent? data)
{
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/colors.xml b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/colors.xml
index 491dfad4d..ba44c1ca5 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/colors.xml
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/colors.xml
@@ -1,6 +1,6 @@
- #32608d
- #1791ff
+ #1791FF
+ #106bbf
#106bbf
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/styles.xml b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/styles.xml
deleted file mode 100644
index 49937c5ed..000000000
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Resources/values/styles.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidApplicationService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidApplicationService.cs
index dada41b18..a9cb589d8 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidApplicationService.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidApplicationService.cs
@@ -13,7 +13,7 @@ internal sealed class AndroidApplicationService : BaseApplicationService
///
public override string Platform { get; } = "Android - MAUI";
-
+
///
public override Task OpenUriAsync(Uri uri)
{
@@ -30,11 +30,11 @@ public override Task OpenUriAsync(Uri uri)
return Task.CompletedTask;
}
-
+
///
public override string GetSystemVersion()
{
- return DeviceInfo.VersionString;
+ return $"Android {DeviceInfo.VersionString}";
}
///
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidFileExplorerService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidFileExplorerService.cs
index 00506ff75..63cc85c5b 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidFileExplorerService.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidFileExplorerService.cs
@@ -1,12 +1,13 @@
using System.Web;
-using CommunityToolkit.Maui.Core.Extensions;
-using CommunityToolkit.Maui.Storage;
-using OwlCore.Storage;
-using SecureFolderFS.Sdk.Services;
using Android.App;
using Android.Content;
using Android.Provider;
+using CommunityToolkit.Maui.Core.Extensions;
+using CommunityToolkit.Maui.Storage;
+using OwlCore.Storage;
using SecureFolderFS.Maui.Platforms.Android.Storage;
+using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Storage.Pickers;
using AndroidUri = Android.Net.Uri;
using AOSEnvironment = Android.OS.Environment;
@@ -16,23 +17,7 @@ namespace SecureFolderFS.Maui.Platforms.Android.ServiceImplementation
internal sealed class AndroidFileExplorerService : IFileExplorerService
{
///
- public Task TryOpenInFileExplorerAsync(IFolder folder, CancellationToken cancellationToken = default)
- {
- // TODO: Try to implement opening in android file explorer
- return Task.CompletedTask;
- }
-
- ///
- public async Task SaveFileAsync(string suggestedName, Stream dataStream, IDictionary? filter, CancellationToken cancellationToken = default)
- {
- var fileSaver = FileSaver.Default;
- var result = await fileSaver.SaveAsync(suggestedName, dataStream, cancellationToken);
-
- return result.IsSuccessful;
- }
-
- ///
- public async Task PickFileAsync(IEnumerable? filter, bool persist = true, CancellationToken cancellationToken = default)
+ public async Task PickFileAsync(PickerOptions? options, bool offerPersistence = true, CancellationToken cancellationToken = default)
{
var intent = new Intent(Intent.ActionOpenDocument)
.AddCategory(Intent.CategoryOpenable)
@@ -41,20 +26,18 @@ public async Task SaveFileAsync(string suggestedName, Stream dataStream, I
var pickerIntent = Intent.CreateChooser(intent, "Select file");
+ // TODO: Determine if GrantReadUriPermission and GrantWriteUriPermission are needed for access persistence
+
// FilePicker 0x2AF9
var result = await StartActivityAsync(pickerIntent, 0x2AF9);
if (result is null || MainActivity.Instance is null)
return null;
- var file = new AndroidFile(result, MainActivity.Instance);
- if (persist)
- await file.AddBookmarkAsync(cancellationToken);
-
- return file;
+ return new AndroidFile(result, MainActivity.Instance);
}
///
- public async Task PickFolderAsync(bool persist = true, CancellationToken cancellationToken = default)
+ public async Task PickFolderAsync(PickerOptions? options, bool offerPersistence = true, CancellationToken cancellationToken = default)
{
var initialPath = AndroidPathExtensions.GetExternalDirectory();
if (AOSEnvironment.ExternalStorageDirectory is not null)
@@ -64,7 +47,7 @@ public async Task SaveFileAsync(string suggestedName, Stream dataStream, I
var intent = new Intent(Intent.ActionOpenDocumentTree);
intent.PutExtra(DocumentsContract.ExtraInitialUri, initialFolderUri);
- if (persist)
+ if (offerPersistence)
{
intent.AddFlags(ActivityFlags.GrantReadUriPermission);
intent.AddFlags(ActivityFlags.GrantWriteUriPermission);
@@ -75,11 +58,40 @@ public async Task SaveFileAsync(string suggestedName, Stream dataStream, I
if (result is null || MainActivity.Instance is null)
return null;
- var folder = new AndroidFolder(result, MainActivity.Instance);
- if (persist)
- await folder.AddBookmarkAsync(cancellationToken);
+ return new AndroidFolder(result, MainActivity.Instance);
+ }
- return folder;
+ ///
+ public Task TryOpenInFileExplorerAsync(IFolder folder, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var context = MainActivity.Instance;
+ if (context is null)
+ return Task.CompletedTask;
+
+ var intent = new Intent(Intent.ActionView);
+ intent.SetType("*/*");
+ intent.AddCategory(Intent.CategoryDefault);
+ intent.AddFlags(ActivityFlags.NewTask);
+ context.StartActivity(intent);
+
+ return Task.CompletedTask;
+ }
+ catch (Exception ex)
+ {
+ _ = ex;
+ return Task.CompletedTask;
+ }
+ }
+
+ ///
+ public async Task SaveFileAsync(string suggestedName, Stream dataStream, IDictionary? filter, CancellationToken cancellationToken = default)
+ {
+ var fileSaver = FileSaver.Default;
+ var result = await fileSaver.SaveAsync(suggestedName, dataStream, cancellationToken);
+
+ return result.IsSuccessful;
}
private async Task StartActivityAsync(Intent? pickerIntent, int requestCode)
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidSystemService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidSystemService.cs
index 0b71db01b..472891748 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidSystemService.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidSystemService.cs
@@ -1,3 +1,5 @@
+using OwlCore.Storage;
+using SecureFolderFS.Maui.Platforms.Android.Storage;
using SecureFolderFS.Sdk.Services;
namespace SecureFolderFS.Maui.Platforms.Android.ServiceImplementation
@@ -6,8 +8,47 @@ namespace SecureFolderFS.Maui.Platforms.Android.ServiceImplementation
internal sealed class AndroidSystemService : ISystemService
{
// TODO: Use BroadcastReceiver - ActionScreenOff, ActionUserPresent
-
+
+ ///
+ public event EventHandler? DeviceLocked;
+
///
- public event EventHandler? DesktopLocked;
+ public Task GetAvailableFreeSpaceAsync(IFolder storageRoot, CancellationToken cancellationToken = default)
+ {
+#if ANDROID
+ if (storageRoot is not AndroidFolder androidFolder)
+ return Task.FromException(new ArgumentNullException(nameof(storageRoot)));
+
+ // fallback: if we can't resolve, assume /storage/emulated/0
+ var realPath = TryGetRealPath(androidFolder) ?? "/storage/emulated/0";
+ var stat = new global::Android.OS.StatFs(realPath);
+
+ long availableBytes;
+#if ANDROID33_0_OR_GREATER
+ availableBytes = stat.AvailableBytes;
+#else
+ availableBytes = (long)stat.BlockSizeLong * (long)stat.AvailableBlocksLong;
+#endif
+
+ return Task.FromResult(availableBytes);
+
+ static string? TryGetRealPath(AndroidFolder androidFolder)
+ {
+ if (androidFolder.Inner is not { Scheme: "content", Authority: "com.android.externalstorage.documents" })
+ return null; // Couldn't resolve
+
+ var split = androidFolder.Inner.Path?.Split(':');
+ var relativePath = string.Join('/', split?.Skip(1).Take(Range.All) ?? []);
+ var type = split?.FirstOrDefault();
+ if (type?.Contains("primary", StringComparison.OrdinalIgnoreCase) ?? true)
+ return $"/storage/emulated/0/{relativePath}";
+
+ return $"/storage/{type}/{relativePath}";
+ }
+#else
+ throw new PlatformNotSupportedException("Only implemented on Android.");
+#endif
+ }
+
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidVaultCredentialsService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidVaultCredentialsService.cs
index 89360c239..3318e4ef5 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidVaultCredentialsService.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/AndroidVaultCredentialsService.cs
@@ -2,6 +2,7 @@
using OwlCore.Storage;
using SecureFolderFS.Core.Cryptography;
using SecureFolderFS.Core.VaultAccess;
+using SecureFolderFS.Maui.Platforms.Android.ViewModels;
using SecureFolderFS.Sdk.Services;
using SecureFolderFS.Sdk.ViewModels.Controls.Authentication;
using SecureFolderFS.Shared.Models;
@@ -37,6 +38,7 @@ public override async IAsyncEnumerable GetLoginAsync(IF
{
Core.Constants.Vault.Authentication.AUTH_PASSWORD => new PasswordLoginViewModel(),
Core.Constants.Vault.Authentication.AUTH_KEYFILE => new KeyFileLoginViewModel(vaultFolder),
+ Core.Constants.Vault.Authentication.AUTH_ANDROID_BIOMETRIC => new AndroidBiometricLoginViewModel(vaultFolder, config.Uid),
_ => throw new NotSupportedException($"The authentication method '{item}' is not supported by the platform.")
};
}
@@ -48,6 +50,9 @@ public override async IAsyncEnumerable GetCreationAsync
{
// Password
yield return new PasswordCreationViewModel();
+
+ // Android Biometric
+ yield return new AndroidBiometricCreationViewModel(vaultFolder, vaultId);
// Key File
yield return new KeyFileCreationViewModel(vaultId);
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFile.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFile.cs
index 2d06088bf..b828edc90 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFile.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFile.cs
@@ -40,27 +40,24 @@ public Task OpenStreamAsync(FileAccess accessMode, CancellationToken can
return Task.FromResult(stream);
}
+ if (activity.ContentResolver?.OpenInputStream(Inner) is not InputStreamInvoker inputStream)
+ return Task.FromException(new UnauthorizedAccessException($"Could not open a stream to: '{Id}'."));
+
if (accessMode == FileAccess.Read)
{
- stream = activity.ContentResolver?.OpenInputStream(Inner);
+ stream = inputStream;
}
else
{
- var inputStream = (activity.ContentResolver?.OpenInputStream(Inner) as InputStreamInvoker)?.BaseInputStream;
- var outputStream = (activity.ContentResolver?.OpenOutputStream(Inner) as OutputStreamInvoker)?.BaseOutputStream;
- if (inputStream is null || outputStream is null)
+ if (activity.ContentResolver?.OpenOutputStream(Inner) is not OutputStreamInvoker outputStream)
return Task.FromException(new UnauthorizedAccessException($"Could not open a stream to: '{Id}'."));
- var combinedInputStream = new InputOutputStream(inputStream, outputStream, GetFileSize(activity.ContentResolver, Inner));
- stream = combinedInputStream;
+ stream = new InputOutputStream(inputStream.BaseInputStream, outputStream.BaseOutputStream, GetFileSize(activity.ContentResolver, Inner));
}
-
- if (stream is null)
- return Task.FromException(new UnauthorizedAccessException($"Could not open a stream to: '{Id}'."));
return Task.FromResult(stream);
}
-
+
///
public override Task GetPropertiesAsync()
{
@@ -105,12 +102,12 @@ private static bool IsVirtualFile(Context context, AndroidUri uri)
return null;
}
-
+
private static long GetFileSize(ContentResolver? contentResolver, AndroidUri uri)
{
if (contentResolver is null)
return 0L;
-
+
try
{
// Try to get file size using content resolver
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFolder.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFolder.cs
index eb88cd176..a2f757599 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFolder.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidFolder.cs
@@ -2,7 +2,6 @@
using Android.Provider;
using Android.Webkit;
using AndroidX.DocumentFile.Provider;
-using AndroidX.Navigation;
using OwlCore.Storage;
using SecureFolderFS.Maui.Platforms.Android.Storage.StorageProperties;
using SecureFolderFS.Storage.Renamable;
@@ -16,10 +15,10 @@ namespace SecureFolderFS.Maui.Platforms.Android.Storage
internal sealed class AndroidFolder : AndroidStorable, IModifiableFolder, IChildFolder, IGetFirstByName, IRenamableFolder // TODO: Implement: IGetFirstByName, IGetItem
{
private static Exception RenameException { get; } = new IOException("Could not rename the item.");
-
+
///
public override string Name { get; }
-
+
///
public override DocumentFile? Document { get; }
@@ -154,6 +153,8 @@ public Task CreateFileAsync(string name, bool overwrite = false, Can
if (overwrite && existingFile is not null)
existingFile.Delete();
+ else if (existingFile is not null)
+ return Task.FromResult(new AndroidFile(existingFile.Uri, activity, this, permissionRoot));
var newFile = Document?.CreateFile(mimeType, name);
if (newFile is null)
@@ -186,14 +187,14 @@ public async Task GetFirstByNameAsync(string name, CancellationT
return target;
}
-
+
///
public override Task GetPropertiesAsync()
{
if (Document is null)
return Task.FromException(new ArgumentNullException(nameof(Document)));
- properties ??= new AndroidFileProperties(Document);
+ properties ??= new AndroidFolderProperties(Document);
return Task.FromResult(properties);
}
}
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidStorable.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidStorable.cs
index 3e37cad66..ddb5874d1 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidStorable.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/AndroidStorable.cs
@@ -43,9 +43,9 @@ protected AndroidStorable(AndroidUri uri, Activity activity, AndroidFolder? pare
this.permissionRoot = permissionRoot ?? uri;
Inner = uri;
- BookmarkId = bookmarkId;
- Id = Inner.ToString() ?? string.Empty;
+ Id = uri.ToString() ?? string.Empty;
Name = GetFileName(uri);
+ BookmarkId = bookmarkId;
}
///
@@ -89,7 +89,7 @@ public Task RemoveBookmarkAsync(CancellationToken cancellationToken = default)
return Task.CompletedTask;
}
-
+
///
public abstract Task GetPropertiesAsync();
diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/StorageProperties/AndroidFileProperties.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/StorageProperties/AndroidFileProperties.cs
index 4f145d407..f911eba97 100644
--- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/StorageProperties/AndroidFileProperties.cs
+++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/Storage/StorageProperties/AndroidFileProperties.cs
@@ -6,7 +6,7 @@
namespace SecureFolderFS.Maui.Platforms.Android.Storage.StorageProperties
{
///
- internal sealed class AndroidFileProperties : ISizeProperties, IBasicProperties
+ internal sealed class AndroidFileProperties : ISizeProperties, IDateProperties, IBasicProperties
{
private readonly DocumentFile _document;
@@ -19,13 +19,32 @@ public AndroidFileProperties(DocumentFile document)
public Task?> GetSizeAsync(CancellationToken cancellationToken = default)
{
var sizeProperty = new GenericProperty(_document.Length());
- return Task.FromResult>(sizeProperty);
+ return Task.FromResult?>(sizeProperty);
+ }
+
+ ///
+ public Task> GetDateCreatedAsync(CancellationToken cancellationToken = default)
+ {
+ // Created date is not available on Android
+ return GetDateModifiedAsync(cancellationToken);
+ }
+
+ ///
+ public Task> GetDateModifiedAsync(CancellationToken cancellationToken = default)
+ {
+ var timestamp = _document.LastModified();
+ var dateModified = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).UtcDateTime;
+ var dateProperty = new GenericProperty(dateModified);
+
+ return Task.FromResult>(dateProperty);
}
///
public async IAsyncEnumerable> GetPropertiesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
yield return await GetSizeAsync(cancellationToken) as IStorageProperty
+
+ Designer
+
$(DefaultXamlRuntime)
Designer
diff --git a/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoDialogService.cs b/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoDialogService.cs
index caa62688e..baaea20cc 100644
--- a/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoDialogService.cs
+++ b/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoDialogService.cs
@@ -33,6 +33,7 @@ protected override IOverlayControl GetOverlay(IViewable viewable)
PreviewRecoveryOverlayViewModel => new PreviewRecoveryDialog(),
RecoveryOverlayViewModel => new RecoveryDialog(),
MigrationOverlayViewModel => new MigrationDialog(),
+ RecycleBinOverlayViewModel => new RecycleBinDialog(),
// Unused
PaymentOverlayViewModel => new PaymentDialog(),
diff --git a/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoFileExplorerService.cs b/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoFileExplorerService.cs
index 5aba92599..07e332240 100644
--- a/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoFileExplorerService.cs
+++ b/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoFileExplorerService.cs
@@ -6,6 +6,7 @@
using System.Threading.Tasks;
using OwlCore.Storage;
using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Storage.Pickers;
using SecureFolderFS.Storage.SystemStorageEx;
using Windows.Storage;
using Windows.Storage.Pickers;
@@ -15,6 +16,56 @@ namespace SecureFolderFS.Uno.ServiceImplementation
///
internal sealed class UnoFileExplorerService : IFileExplorerService
{
+ ///
+ public async Task PickFileAsync(PickerOptions? options, bool offerPersistence = true, CancellationToken cancellationToken = default)
+ {
+ var filePicker = new FileOpenPicker();
+ WinRT_InitializeObject(filePicker);
+
+ if (options is NameFilter nameFilter)
+ {
+ foreach (var item in nameFilter.Names)
+ filePicker.FileTypeFilter.Add(item);
+ }
+ else
+ filePicker.FileTypeFilter.Add("*");
+
+ var file = await filePicker.PickSingleFileAsync().AsTask(cancellationToken);
+ if (file is null)
+ return null;
+
+ //return new WindowsStorageFile(file);
+ return new SystemFileEx(file.Path);
+ }
+
+ ///
+ public async Task PickFolderAsync(PickerOptions? options, bool offerPersistence = true, CancellationToken cancellationToken = default)
+ {
+ var folderPicker = new FolderPicker();
+ WinRT_InitializeObject(folderPicker);
+
+ if (options is NameFilter nameFilter)
+ {
+ foreach (var item in nameFilter.Names)
+ folderPicker.FileTypeFilter.Add(item);
+ }
+ else
+ folderPicker.FileTypeFilter.Add("*");
+
+ if (options is StartingFolderOptions startingFolderOptions)
+ {
+ if (Enum.TryParse(startingFolderOptions.Location, true, out PickerLocationId pickerLocationId))
+ folderPicker.SuggestedStartLocation = pickerLocationId;
+ }
+
+ var folder = await folderPicker.PickSingleFolderAsync().AsTask(cancellationToken);
+ if (folder is null)
+ return null;
+
+ //return new WindowsStorageFolder(folder);
+ return new SystemFolderEx(folder.Path);
+ }
+
///
public Task TryOpenInFileExplorerAsync(IFolder folder, CancellationToken cancellationToken = default)
{
@@ -23,9 +74,9 @@ public Task TryOpenInFileExplorerAsync(IFolder folder, CancellationToken cancell
Process.Start("xdg-open", folder.Id);
return Task.CompletedTask;
}
-
+
#if __MACOS__ || __MACCATALYST__
- Process.Start("sh", ["-c", $"open {folder.Id}"]);
+ Process.Start("sh", [ "-c", $"open {folder.Id}" ]);
return Task.CompletedTask;
#elif WINDOWS
return global::Windows.System.Launcher.LaunchFolderPathAsync(folder.Id).AsTask(cancellationToken);
@@ -44,10 +95,10 @@ public async Task SaveFileAsync(string suggestedName, Stream dataStream, I
if (filter is not null)
{
foreach (var item in filter)
- filePicker.FileTypeChoices.Add(item.Key, new[] { item.Value == "*" ? "." : item.Value });
+ filePicker.FileTypeChoices.Add(item.Key, [ item.Value == "*" ? "." : item.Value ]);
}
else
- filePicker.FileTypeChoices.Add("All Files", new[] { "." });
+ filePicker.FileTypeChoices.Add("All Files", [ "." ]);
var file = await filePicker.PickSaveFileAsync().AsTask(cancellationToken);
if (file is null)
@@ -60,49 +111,13 @@ public async Task SaveFileAsync(string suggestedName, Stream dataStream, I
return true;
}
- ///
- public async Task PickFileAsync(IEnumerable? filter, bool persist = true, CancellationToken cancellationToken = default)
- {
- var filePicker = new FileOpenPicker();
- WinRT_InitializeObject(filePicker);
-
- if (filter is not null)
- {
- foreach (var item in filter)
- filePicker.FileTypeFilter.Add(item);
- }
- else
- filePicker.FileTypeFilter.Add("*");
-
- var file = await filePicker.PickSingleFileAsync().AsTask(cancellationToken);
- if (file is null)
- return null;
-
- //return new WindowsStorageFile(file);
- return new SystemFileEx(file.Path);
- }
-
- ///
- public async Task PickFolderAsync(bool persist = true, CancellationToken cancellationToken = default)
- {
- var folderPicker = new FolderPicker();
- WinRT_InitializeObject(folderPicker);
-
- folderPicker.FileTypeFilter.Add("*");
- var folder = await folderPicker.PickSingleFolderAsync().AsTask(cancellationToken);
- if (folder is null)
- return null;
-
- //return new WindowsStorageFolder(folder);
- return new SystemFolderEx(folder.Path);
- }
-
private static void WinRT_InitializeObject(object obj)
{
- _ = obj;
#if WINDOWS
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.Instance?.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(obj, hwnd);
+#else
+ _ = obj;
#endif
}
}
diff --git a/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoMediaService.cs b/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoMediaService.cs
index 373a43bb9..23d81fd9a 100644
--- a/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoMediaService.cs
+++ b/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoMediaService.cs
@@ -1,12 +1,21 @@
using System;
+using System.Drawing;
+using System.Drawing.Imaging;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Media.Imaging;
using OwlCore.Storage;
+using SecureFolderFS.Sdk.Helpers;
using SecureFolderFS.Sdk.Services;
using SecureFolderFS.Shared.ComponentModel;
using SecureFolderFS.Uno.AppModels;
+using SecureFolderFS.Uno.Helpers;
+
+#if WINDOWS
+using System.Runtime.InteropServices;
+using Vanara.PInvoke;
+#endif
namespace SecureFolderFS.Uno.ServiceImplementation
{
@@ -24,8 +33,15 @@ public async Task ReadImageFileAsync(IFile file, CancellationToken cance
return new ImageBitmap(bitmapImage, null);
- //var mimeType = MimeTypeMap.GetMimeType(file.Id);
- //return await ImagingHelpers.GetBitmapFromStreamAsync(winrtStream, mimeType, cancellationToken);
+ // TODO: Check if it works
+ var classification = FileTypeHelper.GetClassification(file);
+ return await ImagingHelpers.GetBitmapFromStreamAsync(winrtStream, classification.MimeType, cancellationToken);
+ }
+
+ ///
+ public Task GenerateThumbnailAsync(IFile file, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(null);
}
///
@@ -33,5 +49,84 @@ public Task StreamVideoAsync(IFile file, CancellationToken cancella
{
return Task.FromException(new NotSupportedException());
}
+
+ ///
+ public async Task TrySetFolderIconAsync(IModifiableFolder folder, Stream imageStream, CancellationToken cancellationToken = default)
+ {
+#if WINDOWS
+ try
+ {
+ // Create icon file
+ var iconFile = await folder.CreateFileAsync(Sdk.Constants.Vault.VAULT_ICON_FILENAME_ICO, true, cancellationToken);
+ await using var iconStream = await iconFile.OpenReadWriteAsync(cancellationToken);
+ await ImageToIconAsync(imageStream, iconStream, 100, true, cancellationToken);
+
+ // Set desktop.ini data
+ var desktopIniFile = await folder.CreateFileAsync("desktop.ini", true, cancellationToken);
+ var text = string.Format(UI.Constants.FileData.DESKTOP_INI_ICON_CONFIGURATION, Sdk.Constants.Vault.VAULT_ICON_FILENAME_ICO, "Encrypted vault data folder");
+ await desktopIniFile.WriteTextAsync(text, cancellationToken);
+
+ // Notify Shell of the update
+ File.SetAttributes(desktopIniFile.Id, FileAttributes.Hidden | FileAttributes.System);
+ var folderSettings = new Shell32.SHFOLDERCUSTOMSETTINGS()
+ {
+ cchIconFile = 0,
+ pszIconFile = iconFile.Name,
+ dwMask = Shell32.FOLDERCUSTOMSETTINGSMASK.FCSM_ICONFILE,
+ dwSize = (uint)Marshal.SizeOf()
+ };
+ Shell32.SHGetSetFolderCustomSettings(ref folderSettings, folder.Id, Shell32.FCS.FCS_FORCEWRITE);
+ Shell32.SHChangeNotify(Shell32.SHCNE.SHCNE_UPDATEITEM, Shell32.SHCNF.SHCNF_PATHW, folder.Id);
+
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+#else
+ await Task.CompletedTask;
+ return false;
+#endif
+ }
+
+ private async Task ImageToIconAsync(Stream imageStream, Stream destinationStream, int size, bool preserveAspectRatio, CancellationToken cancellationToken)
+ {
+ var inputBitmap = Image.FromStream(imageStream);
+ int width;
+ int height;
+
+ if (preserveAspectRatio)
+ {
+ width = size;
+ height = inputBitmap.Height / inputBitmap.Width * size;
+ }
+ else
+ width = height = size;
+
+ var newBitmap = new Bitmap(inputBitmap, new(width, height));
+ await using var resizedBitmapStream = new MemoryStream();
+ newBitmap.Save(resizedBitmapStream, ImageFormat.Png);
+
+ await using var writer = new BinaryWriter(destinationStream);
+
+ // Header
+ writer.Write((short)0); // 0 : reserved
+ writer.Write((short)1); // 2 : 1=ico, 2=cur
+ writer.Write((short)1); // 4 : number of images
+
+ // Image Entry
+ writer.Write((byte)Math.Min(width, byte.MaxValue)); // 0 : width
+ writer.Write((byte)Math.Min(height, byte.MaxValue)); // 1 : height
+ writer.Write((byte)0); // 2 : number of colors
+ writer.Write((byte)0); // 3 : reserved
+ writer.Write((short)0); // 4 : number of color planes
+ writer.Write((short)32); // 6 : bits per pixel
+ writer.Write((int)resizedBitmapStream.Length); // 8 : size of image data
+ writer.Write((int)(6 + 16)); // 12 : offset of image data
+
+ writer.Write(resizedBitmapStream.ToArray());
+ writer.Flush();
+ }
}
}
diff --git a/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoNavigationService.cs b/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoNavigationService.cs
index caf87e8b0..0d8f87be5 100644
--- a/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoNavigationService.cs
+++ b/src/Platforms/SecureFolderFS.Uno/ServiceImplementation/UnoNavigationService.cs
@@ -25,11 +25,11 @@ protected override async Task BeginNavigationAsync(IViewDesignation? view,
{
if (Navigator is not FrameNavigationControl frameNavigation)
return false;
-
+
// Navigate back
if (!await Navigator.GoBackAsync())
return false;
-
+
var contentType = frameNavigation.Content?.GetType();
if (contentType is null)
return false;
diff --git a/src/Platforms/SecureFolderFS.Uno/TemplateSelectors/HealthIssueTemplateSelector.cs b/src/Platforms/SecureFolderFS.Uno/TemplateSelectors/HealthIssueTemplateSelector.cs
index 9aa40f86f..2e6f5baa7 100644
--- a/src/Platforms/SecureFolderFS.Uno/TemplateSelectors/HealthIssueTemplateSelector.cs
+++ b/src/Platforms/SecureFolderFS.Uno/TemplateSelectors/HealthIssueTemplateSelector.cs
@@ -22,7 +22,7 @@ internal sealed class HealthIssueTemplateSelector : BaseTemplateSelector NameIssueTemplate,
HealthFileDataIssueViewModel => FileDataIssueTemplate,
HealthDirectoryIssueViewModel => DirectoryIssueTemplate,
- { } => IssueTemplate,
+ not null => IssueTemplate,
_ => base.SelectTemplateCore(item, container)
};
}
diff --git a/src/Platforms/SecureFolderFS.Uno/UserControls/BackButtonTitleControl.xaml.cs b/src/Platforms/SecureFolderFS.Uno/UserControls/BackButtonTitleControl.xaml.cs
index 2f462076b..97031b443 100644
--- a/src/Platforms/SecureFolderFS.Uno/UserControls/BackButtonTitleControl.xaml.cs
+++ b/src/Platforms/SecureFolderFS.Uno/UserControls/BackButtonTitleControl.xaml.cs
@@ -1,7 +1,7 @@
using System.Threading.Tasks;
-using CommunityToolkit.WinUI.UI.Animations;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
+using SecureFolderFS.Uno.Extensions;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -12,7 +12,7 @@ namespace SecureFolderFS.Uno.UserControls
public sealed partial class BackButtonTitleControl : UserControl
{
private bool _isBackShown;
-
+
public event RoutedEventHandler Click;
public BackButtonTitleControl()
diff --git a/src/Platforms/SecureFolderFS.Uno/UserControls/GraphControl.xaml.cs b/src/Platforms/SecureFolderFS.Uno/UserControls/GraphControl.xaml.cs
index 2402e90cc..dcc002a8d 100644
--- a/src/Platforms/SecureFolderFS.Uno/UserControls/GraphControl.xaml.cs
+++ b/src/Platforms/SecureFolderFS.Uno/UserControls/GraphControl.xaml.cs
@@ -52,7 +52,7 @@ private async void Chart_Loaded(object sender, RoutedEventArgs e)
[
new LineSeries()
{
- Values = Data,
+ Values = (IReadOnlyCollection?)Data,
Fill = new LinearGradientPaint([
new(ChartPrimaryColor.R, ChartPrimaryColor.G, ChartPrimaryColor.B, ChartPrimaryColor.A),
new(ChartSecondaryColor.R, ChartSecondaryColor.G, ChartSecondaryColor.B, ChartSecondaryColor.A)
diff --git a/src/Platforms/SecureFolderFS.Uno/UserControls/InterfaceHost/MainAppHostControl.xaml b/src/Platforms/SecureFolderFS.Uno/UserControls/InterfaceHost/MainAppHostControl.xaml
index b062b7c57..ea3fdf40e 100644
--- a/src/Platforms/SecureFolderFS.Uno/UserControls/InterfaceHost/MainAppHostControl.xaml
+++ b/src/Platforms/SecureFolderFS.Uno/UserControls/InterfaceHost/MainAppHostControl.xaml
@@ -2,7 +2,7 @@
x:Class="SecureFolderFS.Uno.UserControls.InterfaceHost.MainAppHostControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:anim="using:CommunityToolkit.WinUI.UI.Animations"
+ xmlns:anim="using:CommunityToolkit.WinUI.Animations"
xmlns:animvis="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:l="using:SecureFolderFS.Uno.Localization"
@@ -15,6 +15,7 @@
mc:Ignorable="d">
+
@@ -57,8 +58,7 @@
CornerRadius="0 6 6 0"
Visibility="Collapsed">
-
-
+
@@ -121,6 +121,7 @@
diff --git a/src/Platforms/SecureFolderFS.Uno/Views/VaultWizard/CredentialsWizardPage.xaml b/src/Platforms/SecureFolderFS.Uno/Views/VaultWizard/CredentialsWizardPage.xaml
index 8694fb494..18781b5a7 100644
--- a/src/Platforms/SecureFolderFS.Uno/Views/VaultWizard/CredentialsWizardPage.xaml
+++ b/src/Platforms/SecureFolderFS.Uno/Views/VaultWizard/CredentialsWizardPage.xaml
@@ -7,7 +7,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="using:SecureFolderFS.Uno.UserControls"
xmlns:ucab="using:SecureFolderFS.Uno.UserControls.ActionBlocks"
- xmlns:vm="using:SecureFolderFS.Sdk.ViewModels"
+ xmlns:vm="using:SecureFolderFS.Sdk.ViewModels.Controls"
xmlns:vm2="using:SecureFolderFS.Sdk.ViewModels.Controls.Authentication"
mc:Ignorable="d">
@@ -75,7 +75,7 @@
SelectedIndex="0"
SelectedItem="{x:Bind ViewModel.ContentCipher, Mode=TwoWay}">
-
+
@@ -85,7 +85,7 @@
+ Text="{l:ResourceString Rid=NameEncryption}" />
-
+
@@ -111,11 +111,12 @@
Grid.Column="1"
HorizontalAlignment="Right"
AutomationProperties.Name="{x:Bind ViewModel.EncodingOption, Mode=OneWay}"
+ IsEnabled="False"
ItemsSource="{x:Bind ViewModel.EncodingOptions, Mode=OneWay}"
SelectedIndex="0"
SelectedItem="{x:Bind ViewModel.EncodingOption, Mode=TwoWay}">
-
+
diff --git a/src/SecureFolderFS.Core.Cryptography/Cipher/Argon2id.cs b/src/SecureFolderFS.Core.Cryptography/Cipher/Argon2id.cs
index 1080202fe..3f0857228 100644
--- a/src/SecureFolderFS.Core.Cryptography/Cipher/Argon2id.cs
+++ b/src/SecureFolderFS.Core.Cryptography/Cipher/Argon2id.cs
@@ -5,13 +5,24 @@ namespace SecureFolderFS.Core.Cryptography.Cipher
/// TODO: Needs docs
public static class Argon2id
{
- public static void DeriveKey(ReadOnlySpan password, ReadOnlySpan salt, Span result)
+ public static void Old_DeriveKey(ReadOnlySpan password, ReadOnlySpan salt, Span result)
+ {
+ using var argon2id = new Konscious.Security.Cryptography.Argon2id(password.ToArray());
+ argon2id.Salt = salt.ToArray();
+ argon2id.DegreeOfParallelism = 8;
+ argon2id.Iterations = 8;
+ argon2id.MemorySize = 102400;
+
+ argon2id.GetBytes(Constants.KeyTraits.ARGON2_KEK_LENGTH).CopyTo(result);
+ }
+
+ public static void V3_DeriveKey(ReadOnlySpan password, ReadOnlySpan salt, Span result)
{
using var argon2id = new Konscious.Security.Cryptography.Argon2id(password.ToArray());
argon2id.Salt = salt.ToArray();
argon2id.DegreeOfParallelism = Constants.Crypto.Argon2.DEGREE_OF_PARALLELISM;
argon2id.Iterations = Constants.Crypto.Argon2.ITERATIONS;
- argon2id.MemorySize = Constants.Crypto.Argon2.MEMORY_SIZE;
+ argon2id.MemorySize = Constants.Crypto.Argon2.MEMORY_SIZE_KIBIBYTES;
argon2id.GetBytes(Constants.KeyTraits.ARGON2_KEK_LENGTH).CopyTo(result);
}
diff --git a/src/SecureFolderFS.Core.Cryptography/Constants.cs b/src/SecureFolderFS.Core.Cryptography/Constants.cs
index 5c608e996..2a74e029c 100644
--- a/src/SecureFolderFS.Core.Cryptography/Constants.cs
+++ b/src/SecureFolderFS.Core.Cryptography/Constants.cs
@@ -4,10 +4,12 @@ public static class Constants
{
public static class KeyTraits
{
- public const int ARGON2_KEK_LENGTH = 32;
+ public const string KEY_TEXT_SEPARATOR = "@@@";
+ public const int SALT_LENGTH = 16;
public const int ENCKEY_LENGTH = 32;
public const int MACKEY_LENGTH = 32;
- public const int SALT_LENGTH = 16;
+ public const int ARGON2_KEK_LENGTH = 32;
+ public const int CHALLENGE_KEY_PART_LENGTH = 128;
}
public static class CipherId
@@ -80,9 +82,9 @@ internal static class AesCtrHmac
internal static class Argon2
{
- public const int DEGREE_OF_PARALLELISM = 8;
- public const int ITERATIONS = 8;
- public const int MEMORY_SIZE = 102400;
+ public const int MEMORY_SIZE_KIBIBYTES = 32768; // 32MiB
+ public const int DEGREE_OF_PARALLELISM = 2;
+ public const int ITERATIONS = 2;
}
}
}
diff --git a/src/SecureFolderFS.Core.Cryptography/Extensions/SecretKeyExtensions.cs b/src/SecureFolderFS.Core.Cryptography/Extensions/SecretKeyExtensions.cs
new file mode 100644
index 000000000..ec0a3a8b2
--- /dev/null
+++ b/src/SecureFolderFS.Core.Cryptography/Extensions/SecretKeyExtensions.cs
@@ -0,0 +1,23 @@
+using SecureFolderFS.Core.Cryptography.SecureStore;
+
+namespace SecureFolderFS.Core.Cryptography.Extensions
+{
+ ///
+ /// Provides extension methods for the class.
+ ///
+ public static class SecretKeyExtensions
+ {
+ ///
+ /// Creates a unique copy of the specified and disposes the original key.
+ ///
+ /// The original to copy.
+ /// A new copy of the .
+ public static SecretKey CreateUniqueCopy(this SecretKey originalKey)
+ {
+ var copiedKey = originalKey.CreateCopy();
+ originalKey.Dispose();
+
+ return copiedKey;
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Core.Cryptography/HeaderCrypt/AesCtrHmacHeaderCrypt.cs b/src/SecureFolderFS.Core.Cryptography/HeaderCrypt/AesCtrHmacHeaderCrypt.cs
index 1185feb24..aa2944242 100644
--- a/src/SecureFolderFS.Core.Cryptography/HeaderCrypt/AesCtrHmacHeaderCrypt.cs
+++ b/src/SecureFolderFS.Core.Cryptography/HeaderCrypt/AesCtrHmacHeaderCrypt.cs
@@ -17,8 +17,8 @@ internal sealed class AesCtrHmacHeaderCrypt : BaseHeaderCrypt
///
public override int HeaderPlaintextSize { get; } = HEADER_NONCE_SIZE + HEADER_CONTENTKEY_SIZE;
- public AesCtrHmacHeaderCrypt(SecretKey encKey, SecretKey macKey)
- : base(encKey, macKey)
+ public AesCtrHmacHeaderCrypt(KeyPair keyPair)
+ : base(keyPair)
{
}
@@ -41,7 +41,7 @@ public override void EncryptHeader(ReadOnlySpan plaintextHeader, Span ciphertextHeader, Span ciphertextHeader, Span headerNonce, ReadOnlySpan ciphertextPayload, Span headerMac)
{
// Initialize HMAC
- using var hmacSha256 = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA256, macKey);
+ using var hmacSha256 = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA256, MacKey);
hmacSha256.AppendData(headerNonce); // headerNonce
hmacSha256.AppendData(ciphertextPayload); // ciphertextPayload
diff --git a/src/SecureFolderFS.Core.Cryptography/HeaderCrypt/AesGcmHeaderCrypt.cs b/src/SecureFolderFS.Core.Cryptography/HeaderCrypt/AesGcmHeaderCrypt.cs
index 6377e5e32..a8d0040d4 100644
--- a/src/SecureFolderFS.Core.Cryptography/HeaderCrypt/AesGcmHeaderCrypt.cs
+++ b/src/SecureFolderFS.Core.Cryptography/HeaderCrypt/AesGcmHeaderCrypt.cs
@@ -15,8 +15,8 @@ internal sealed class AesGcmHeaderCrypt : BaseHeaderCrypt
///
public override int HeaderPlaintextSize { get; } = HEADER_NONCE_SIZE + HEADER_CONTENTKEY_SIZE;
- public AesGcmHeaderCrypt(SecretKey encKey, SecretKey macKey)
- : base(encKey, macKey)
+ public AesGcmHeaderCrypt(KeyPair keyPair)
+ : base(keyPair)
{
}
@@ -39,7 +39,7 @@ public override void EncryptHeader(ReadOnlySpan plaintextHeader, Span ciphertextHeader, Span
internal abstract class BaseHeaderCrypt : IHeaderCrypt
{
- protected readonly SecretKey encKey;
- protected readonly SecretKey macKey;
+ protected readonly KeyPair keyPair;
protected readonly RandomNumberGenerator secureRandom;
+ protected SecretKey DekKey => keyPair.DekKey;
+
+ protected SecretKey MacKey => keyPair.MacKey;
+
///
public abstract int HeaderCiphertextSize { get; }
///
public abstract int HeaderPlaintextSize { get; }
- protected BaseHeaderCrypt(SecretKey encKey, SecretKey macKey)
+ protected BaseHeaderCrypt(KeyPair keyPair)
{
- this.encKey = encKey;
- this.macKey = macKey;
+ this.keyPair = keyPair;
this.secureRandom = RandomNumberGenerator.Create();
}
diff --git a/src/SecureFolderFS.Core.Cryptography/HeaderCrypt/XChaChaHeaderCrypt.cs b/src/SecureFolderFS.Core.Cryptography/HeaderCrypt/XChaChaHeaderCrypt.cs
index fa07284c7..bf0614f7a 100644
--- a/src/SecureFolderFS.Core.Cryptography/HeaderCrypt/XChaChaHeaderCrypt.cs
+++ b/src/SecureFolderFS.Core.Cryptography/HeaderCrypt/XChaChaHeaderCrypt.cs
@@ -15,8 +15,8 @@ internal sealed class XChaChaHeaderCrypt : BaseHeaderCrypt
///
public override int HeaderPlaintextSize { get; } = HEADER_NONCE_SIZE + HEADER_CONTENTKEY_SIZE;
- public XChaChaHeaderCrypt(SecretKey encKey, SecretKey macKey)
- : base(encKey, macKey)
+ public XChaChaHeaderCrypt(KeyPair keyPair)
+ : base(keyPair)
{
}
@@ -39,7 +39,7 @@ public override void EncryptHeader(ReadOnlySpan plaintextHeader, Span ciphertextHeader, Span
internal sealed class AesSivNameCrypt : BaseNameCrypt
{
- public AesSivNameCrypt(SecretKey encKey, SecretKey macKey, string fileNameEncodingId)
- : base(encKey, macKey, fileNameEncodingId)
+ public AesSivNameCrypt(KeyPair keyPair, string fileNameEncodingId)
+ : base(keyPair, fileNameEncodingId)
{
}
diff --git a/src/SecureFolderFS.Core.Cryptography/NameCrypt/BaseNameCrypt.cs b/src/SecureFolderFS.Core.Cryptography/NameCrypt/BaseNameCrypt.cs
index 4822e8c82..17456b7be 100644
--- a/src/SecureFolderFS.Core.Cryptography/NameCrypt/BaseNameCrypt.cs
+++ b/src/SecureFolderFS.Core.Cryptography/NameCrypt/BaseNameCrypt.cs
@@ -1,10 +1,10 @@
-using Lex4K;
-using SecureFolderFS.Core.Cryptography.Cipher;
-using SecureFolderFS.Core.Cryptography.SecureStore;
-using System;
+using System;
using System.Buffers.Text;
using System.Runtime.CompilerServices;
using System.Text;
+using Lex4K;
+using SecureFolderFS.Core.Cryptography.Cipher;
+using SecureFolderFS.Core.Cryptography.SecureStore;
namespace SecureFolderFS.Core.Cryptography.NameCrypt
{
@@ -14,9 +14,9 @@ internal abstract class BaseNameCrypt : INameCrypt
protected readonly AesSiv128 aesSiv128;
protected readonly string fileNameEncodingId;
- protected BaseNameCrypt(SecretKey encKey, SecretKey macKey, string fileNameEncodingId)
+ protected BaseNameCrypt(KeyPair keyPair, string fileNameEncodingId)
{
- this.aesSiv128 = AesSiv128.CreateInstance(encKey, macKey);
+ this.aesSiv128 = AesSiv128.CreateInstance(keyPair.DekKey, keyPair.MacKey);
this.fileNameEncodingId = fileNameEncodingId;
}
@@ -35,14 +35,24 @@ public virtual string EncryptName(ReadOnlySpan plaintextName, ReadOnlySpan
var ciphertextNameBuffer = EncryptFileName(bytes.Slice(0, count), directoryId);
// Encode string
- return Encode(ciphertextNameBuffer);
+ return fileNameEncodingId switch
+ {
+ Constants.CipherId.ENCODING_BASE64URL => Base64Url.EncodeToString(ciphertextNameBuffer),
+ Constants.CipherId.ENCODING_BASE4K => Base4K.EncodeChainToString(ciphertextNameBuffer),
+ _ => throw new ArgumentOutOfRangeException(nameof(fileNameEncodingId))
+ };
}
///
public virtual string? DecryptName(ReadOnlySpan ciphertextName, ReadOnlySpan directoryId)
{
// Decode buffer
- var ciphertextNameBuffer = Decode(ciphertextName);
+ var ciphertextNameBuffer = fileNameEncodingId switch
+ {
+ Constants.CipherId.ENCODING_BASE64URL => Base64Url.DecodeFromChars(ciphertextName),
+ Constants.CipherId.ENCODING_BASE4K => Base4K.DecodeChainToNewBuffer(ciphertextName),
+ _ => throw new ArgumentOutOfRangeException(nameof(fileNameEncodingId))
+ };
// Decrypt
var plaintextNameBuffer = DecryptFileName(ciphertextNameBuffer, directoryId);
@@ -57,28 +67,6 @@ public virtual string EncryptName(ReadOnlySpan plaintextName, ReadOnlySpan
protected abstract byte[]? DecryptFileName(ReadOnlySpan ciphertextFileNameBuffer, ReadOnlySpan directoryId);
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private string Encode(ReadOnlySpan bytes)
- {
- return fileNameEncodingId switch
- {
- Constants.CipherId.ENCODING_BASE64URL => Base64Url.EncodeToString(bytes),
- Constants.CipherId.ENCODING_BASE4K => Base4K.EncodeChainToString(bytes),
- _ => throw new ArgumentOutOfRangeException(nameof(fileNameEncodingId))
- };
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private ReadOnlySpan Decode(ReadOnlySpan encoded)
- {
- return fileNameEncodingId switch
- {
- Constants.CipherId.ENCODING_BASE64URL => Base64Url.DecodeFromChars(encoded),
- Constants.CipherId.ENCODING_BASE4K => Base4K.DecodeChainToNewBuffer(encoded),
- _ => throw new ArgumentOutOfRangeException(nameof(fileNameEncodingId))
- };
- }
-
///
public virtual void Dispose()
{
diff --git a/src/SecureFolderFS.Core.Cryptography/SecureFolderFS.Core.Cryptography.csproj b/src/SecureFolderFS.Core.Cryptography/SecureFolderFS.Core.Cryptography.csproj
index bc85742ee..5180a0917 100644
--- a/src/SecureFolderFS.Core.Cryptography/SecureFolderFS.Core.Cryptography.csproj
+++ b/src/SecureFolderFS.Core.Cryptography/SecureFolderFS.Core.Cryptography.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/SecureFolderFS.Core.Cryptography/SecureStore/KeyPair.cs b/src/SecureFolderFS.Core.Cryptography/SecureStore/KeyPair.cs
new file mode 100644
index 000000000..4621ab875
--- /dev/null
+++ b/src/SecureFolderFS.Core.Cryptography/SecureStore/KeyPair.cs
@@ -0,0 +1,56 @@
+using SecureFolderFS.Core.Cryptography.Extensions;
+using System;
+
+namespace SecureFolderFS.Core.Cryptography.SecureStore
+{
+ ///
+ /// Represents a chain of secret keys used for encryption and message authentication.
+ ///
+ public sealed class KeyPair : IDisposable
+ {
+ ///
+ /// Gets the Data Encryption Key (DEK).
+ ///
+ public SecretKey DekKey { get; }
+
+ ///
+ /// Gets the Message Authentication Code (MAC) key.
+ ///
+ public SecretKey MacKey { get; }
+
+ private KeyPair(SecretKey dekKey, SecretKey macKey)
+ {
+ DekKey = dekKey;
+ MacKey = macKey;
+ }
+
+ ///
+ /// Imports the specified DEK and MAC keys, creating unique copies of them and disposing the original instances.
+ ///
+ /// The DEK to import.
+ /// The MAC key to import.
+ ///
+ /// This method copies the imported keys and disposed of the original instances.
+ /// Make sure no other classes access the passed keys after they are imported.
+ /// Instead, use and instances.
+ ///
+ /// A new instance of the class with the imported keys.
+ public static KeyPair ImportKeys(SecretKey dekKeyToDestroy, SecretKey macKeyToDestroy)
+ {
+ return new(dekKeyToDestroy.CreateUniqueCopy(), macKeyToDestroy.CreateUniqueCopy());
+ }
+
+ ///
+ public override string ToString()
+ {
+ return $"{Convert.ToBase64String(DekKey)}{Constants.KeyTraits.KEY_TEXT_SEPARATOR}{Convert.ToBase64String(MacKey)}";
+ }
+
+ ///
+ public void Dispose()
+ {
+ DekKey.Dispose();
+ MacKey.Dispose();
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Core.Cryptography/SecureStore/SecureKey.cs b/src/SecureFolderFS.Core.Cryptography/SecureStore/SecureKey.cs
index a7cae47f2..0b007e0e7 100644
--- a/src/SecureFolderFS.Core.Cryptography/SecureStore/SecureKey.cs
+++ b/src/SecureFolderFS.Core.Cryptography/SecureStore/SecureKey.cs
@@ -13,6 +13,11 @@ public SecureKey(int size)
Key = new byte[size];
}
+ private SecureKey(byte[] key)
+ {
+ Key = key;
+ }
+
///
public override SecretKey CreateCopy()
{
@@ -27,5 +32,14 @@ public override void Dispose()
{
Array.Clear(Key);
}
+
+ ///
+ /// Takes the ownership of the provided key and manages its lifetime.
+ ///
+ /// The key to import.
+ public static SecretKey TakeOwnership(byte[] key)
+ {
+ return new SecureKey(key);
+ }
}
}
diff --git a/src/SecureFolderFS.Core.Cryptography/Security.cs b/src/SecureFolderFS.Core.Cryptography/Security.cs
index eca8b3a26..3405a0a0b 100644
--- a/src/SecureFolderFS.Core.Cryptography/Security.cs
+++ b/src/SecureFolderFS.Core.Cryptography/Security.cs
@@ -12,66 +12,65 @@ namespace SecureFolderFS.Core.Cryptography
///
public sealed class Security : IDisposable
{
- private readonly SecretKey _encKey;
- private readonly SecretKey _macKey;
+ ///
+ /// Gets the key pair used for storing the DEK and MAC keys.
+ ///
+ public KeyPair KeyPair { get; }
- // TODO: Needs docs
+ ///
+ /// Gets or sets the header cryptography implementation.
+ ///
public required IHeaderCrypt HeaderCrypt { get; init; }
+ ///
+ /// Gets or sets the content cryptography implementation.
+ ///
public required IContentCrypt ContentCrypt { get; init; }
+ ///
+ /// Gets or sets the name cryptography implementation.
+ ///
public required INameCrypt? NameCrypt { get; init; }
- private Security(SecretKey encKey, SecretKey macKey)
+ private Security(KeyPair keyPair)
{
- _encKey = encKey;
- _macKey = macKey;
- }
-
- public SecretKey CopyEncryptionKey()
- {
- return _encKey.CreateCopy();
- }
-
- public SecretKey CopyMacKey()
- {
- return _macKey.CreateCopy();
+ KeyPair = keyPair;
}
///
/// Creates a new instance of object that provides content encryption and cipher access.
///
- /// The DEK key that this class takes ownership of.
- /// The MAC key that this class takes ownership of.
+ /// The key pair that this class takes ownership of.
/// The content cipher ID.
/// The file name cipher ID.
/// The file name encoding ID.
/// A new object.
- public static Security CreateNew(SecretKey encKey, SecretKey macKey, string contentCipherId, string fileNameCipherId, string fileNameEncodingId)
+ /// Thrown when an invalid cipher ID is provided.
+ public static Security CreateNew(KeyPair keyPair, string contentCipherId, string fileNameCipherId, string fileNameEncodingId)
{
// Initialize crypt implementation
IHeaderCrypt headerCrypt = contentCipherId switch
{
- CipherId.AES_CTR_HMAC => new AesCtrHmacHeaderCrypt(encKey, macKey),
- CipherId.AES_GCM => new AesGcmHeaderCrypt(encKey, macKey),
- CipherId.XCHACHA20_POLY1305 => new XChaChaHeaderCrypt(encKey, macKey),
+ CipherId.AES_CTR_HMAC => new AesCtrHmacHeaderCrypt(keyPair),
+ CipherId.AES_GCM => new AesGcmHeaderCrypt(keyPair),
+ CipherId.XCHACHA20_POLY1305 => new XChaChaHeaderCrypt(keyPair),
_ => throw new ArgumentOutOfRangeException(nameof(contentCipherId))
};
IContentCrypt contentCrypt = contentCipherId switch
{
- CipherId.AES_CTR_HMAC => new AesCtrHmacContentCrypt(macKey),
+ CipherId.AES_CTR_HMAC => new AesCtrHmacContentCrypt(keyPair.MacKey),
CipherId.AES_GCM => new AesGcmContentCrypt(),
CipherId.XCHACHA20_POLY1305 => new XChaChaContentCrypt(),
_ => throw new ArgumentOutOfRangeException(nameof(contentCipherId))
};
INameCrypt? nameCrypt = fileNameCipherId switch
{
- CipherId.AES_SIV => new AesSivNameCrypt(encKey, macKey, fileNameEncodingId),
+ CipherId.AES_SIV => new AesSivNameCrypt(keyPair, fileNameEncodingId),
CipherId.NONE => null,
_ => throw new ArgumentOutOfRangeException(nameof(fileNameCipherId))
};
- return new(encKey, macKey)
+ return new(keyPair)
{
ContentCrypt = contentCrypt,
HeaderCrypt = headerCrypt,
@@ -82,9 +81,7 @@ public static Security CreateNew(SecretKey encKey, SecretKey macKey, string cont
///
public void Dispose()
{
- _encKey.Dispose();
- _macKey.Dispose();
-
+ KeyPair.Dispose();
ContentCrypt.Dispose();
HeaderCrypt.Dispose();
NameCrypt?.Dispose();
diff --git a/src/SecureFolderFS.Core.Dokany/AppModels/DokanyOptions.cs b/src/SecureFolderFS.Core.Dokany/AppModels/DokanyOptions.cs
index d5b401663..fac7e6c20 100644
--- a/src/SecureFolderFS.Core.Dokany/AppModels/DokanyOptions.cs
+++ b/src/SecureFolderFS.Core.Dokany/AppModels/DokanyOptions.cs
@@ -16,7 +16,7 @@ public sealed class DokanyOptions : FileSystemOptions
///
public string? MountPoint { get => _mountPoint; init => _mountPoint = value; }
- internal void SetMountPointInternal(string? value) => _mountPoint = value;
+ internal void DangerousSetMountPoint(string? value) => _mountPoint = value;
public static DokanyOptions ToOptions(IDictionary options)
{
@@ -28,6 +28,8 @@ public static DokanyOptions ToOptions(IDictionary options)
IsReadOnly = (bool?)options.Get(nameof(IsReadOnly)) ?? false,
IsCachingChunks = (bool?)options.Get(nameof(IsCachingChunks)) ?? true,
IsCachingFileNames = (bool?)options.Get(nameof(IsCachingFileNames)) ?? true,
+ IsCachingDirectoryIds = (bool?)options.Get(nameof(IsCachingDirectoryIds)) ?? true,
+ RecycleBinSize = (long?)options.Get(nameof(RecycleBinSize)) ?? 0L,
// Dokany specific
MountPoint = (string?)options.Get(nameof(MountPoint))
diff --git a/src/SecureFolderFS.Core.Dokany/Callbacks/OnDeviceDokany.cs b/src/SecureFolderFS.Core.Dokany/Callbacks/OnDeviceDokany.cs
index 3d0211eb1..9e6d392b2 100644
--- a/src/SecureFolderFS.Core.Dokany/Callbacks/OnDeviceDokany.cs
+++ b/src/SecureFolderFS.Core.Dokany/Callbacks/OnDeviceDokany.cs
@@ -4,12 +4,12 @@
using SecureFolderFS.Core.Dokany.OpenHandles;
using SecureFolderFS.Core.Dokany.UnsafeNative;
using SecureFolderFS.Core.FileSystem;
-using SecureFolderFS.Core.FileSystem.Exceptions;
using SecureFolderFS.Core.FileSystem.Extensions;
using SecureFolderFS.Core.FileSystem.Helpers.Paths;
using SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract;
using SecureFolderFS.Core.FileSystem.Helpers.Paths.Native;
using SecureFolderFS.Core.FileSystem.OpenHandles;
+using SecureFolderFS.Storage.VirtualFileSystem;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -19,6 +19,8 @@
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Cryptography;
+using OwlCore.Storage;
+using SecureFolderFS.Core.FileSystem.Helpers.RecycleBin.Native;
using FileAccess = DokanNet.FileAccess;
namespace SecureFolderFS.Core.Dokany.Callbacks
@@ -243,11 +245,11 @@ public override void Cleanup(string fileName, IDokanFileInfo info)
{
var directoryIdPath = Path.Combine(ciphertextPath, FileSystem.Constants.Names.DIRECTORY_ID_FILENAME);
Specifics.DirectoryIdCache.CacheRemove(directoryIdPath);
- Directory.Delete(ciphertextPath, true);
+ NativeRecycleBinHelpers.DeleteOrRecycle(ciphertextPath, Specifics, StorableType.Folder);
}
else
{
- File.Delete(ciphertextPath);
+ NativeRecycleBinHelpers.DeleteOrRecycle(ciphertextPath, Specifics, StorableType.File);
}
}
catch (UnauthorizedAccessException)
@@ -271,7 +273,7 @@ public override NtStatus GetFileInformation(string fileName, out FileInformation
FileSystemInfo fsInfo = new FileInfo(ciphertextPath);
if (!fsInfo.Exists)
fsInfo = new DirectoryInfo(ciphertextPath);
-
+
fileInfo = new FileInformation()
{
FileName = fsInfo.Name,
@@ -580,7 +582,7 @@ public override NtStatus MoveFile(string oldName, string newName, bool replace,
// File
File.Delete(newCiphertextPath);
File.Move(oldCiphertextPath, newCiphertextPath);
-
+
return Trace(DokanResult.Success, fileNameCombined, info);
}
else
@@ -646,7 +648,7 @@ public override NtStatus LockFile(string fileName, long offset, long length, IDo
fileHandle.Lock(offset, length);
return Trace(DokanResult.Success, fileName, info);
}
-
+
return Trace(DokanResult.InvalidHandle, fileName, info);
}
catch (IOException)
diff --git a/src/SecureFolderFS.Core.Dokany/DokanyFileSystem.cs b/src/SecureFolderFS.Core.Dokany/DokanyFileSystem.cs
index 7c871a6a6..9684822a7 100644
--- a/src/SecureFolderFS.Core.Dokany/DokanyFileSystem.cs
+++ b/src/SecureFolderFS.Core.Dokany/DokanyFileSystem.cs
@@ -54,8 +54,8 @@ public async Task MountAsync(IFolder folder, IDisposable unlockContrac
};
if (dokanyOptions.MountPoint is null)
- dokanyOptions.SetMountPointInternal(PathHelpers.GetFreeMountPath(dokanyOptions.VolumeName));
-
+ dokanyOptions.DangerousSetMountPoint(PathHelpers.GetFreeMountPath(dokanyOptions.VolumeName));
+
if (dokanyOptions.MountPoint is null)
throw new DirectoryNotFoundException("No available free mount points for vault file system.");
diff --git a/src/SecureFolderFS.Core.Dokany/SecureFolderFS.Core.Dokany.csproj b/src/SecureFolderFS.Core.Dokany/SecureFolderFS.Core.Dokany.csproj
index 0e6508d4f..da476b1f1 100644
--- a/src/SecureFolderFS.Core.Dokany/SecureFolderFS.Core.Dokany.csproj
+++ b/src/SecureFolderFS.Core.Dokany/SecureFolderFS.Core.Dokany.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/src/SecureFolderFS.Core.FUSE/AppModels/FuseOptions.cs b/src/SecureFolderFS.Core.FUSE/AppModels/FuseOptions.cs
index 3cb4b4850..f30ad7d4d 100644
--- a/src/SecureFolderFS.Core.FUSE/AppModels/FuseOptions.cs
+++ b/src/SecureFolderFS.Core.FUSE/AppModels/FuseOptions.cs
@@ -1,4 +1,3 @@
-using OwlCore.Storage;
using SecureFolderFS.Core.FileSystem.AppModels;
using SecureFolderFS.Shared.Extensions;
using SecureFolderFS.Storage.VirtualFileSystem;
@@ -54,6 +53,8 @@ public static FuseOptions ToOptions(IDictionary options)
IsReadOnly = (bool?)options.Get(nameof(IsReadOnly)) ?? false,
IsCachingChunks = (bool?)options.Get(nameof(IsCachingChunks)) ?? true,
IsCachingFileNames = (bool?)options.Get(nameof(IsCachingFileNames)) ?? true,
+ IsCachingDirectoryIds = (bool?)options.Get(nameof(IsCachingDirectoryIds)) ?? true,
+ RecycleBinSize = (long?)options.Get(nameof(RecycleBinSize)) ?? 0L,
// FUSE specific
MountPoint = (string?)options.Get(nameof(MountPoint)),
diff --git a/src/SecureFolderFS.Core.FileSystem/Chunks/ChunkAccess.cs b/src/SecureFolderFS.Core.FileSystem/Chunks/ChunkAccess.cs
index c35e7a893..da7caeaa5 100644
--- a/src/SecureFolderFS.Core.FileSystem/Chunks/ChunkAccess.cs
+++ b/src/SecureFolderFS.Core.FileSystem/Chunks/ChunkAccess.cs
@@ -155,7 +155,7 @@ public virtual void SetChunkLength(long chunkNumber, int length, bool includeCur
}
else
return; // Ignore resizing the same length
-
+
// Save newly modified chunk
chunkWriter.WriteChunk(chunkNumber, newPlaintextChunk);
}
diff --git a/src/SecureFolderFS.Core.FileSystem/Chunks/ChunkWriter.cs b/src/SecureFolderFS.Core.FileSystem/Chunks/ChunkWriter.cs
index d2230bd05..bea01c599 100644
--- a/src/SecureFolderFS.Core.FileSystem/Chunks/ChunkWriter.cs
+++ b/src/SecureFolderFS.Core.FileSystem/Chunks/ChunkWriter.cs
@@ -59,7 +59,7 @@ public void WriteChunk(long chunkNumber, ReadOnlySpan plaintextChunk)
// Check position bounds
if (streamPosition > ciphertextStream.Length)
return;
-
+
// Write to stream at correct chunk
ciphertextStream.Position = streamPosition;
ciphertextStream.Write(realCiphertextChunk);
diff --git a/src/SecureFolderFS.Core.FileSystem/Constants.cs b/src/SecureFolderFS.Core.FileSystem/Constants.cs
index bbc38f46e..7942cdbb4 100644
--- a/src/SecureFolderFS.Core.FileSystem/Constants.cs
+++ b/src/SecureFolderFS.Core.FileSystem/Constants.cs
@@ -20,7 +20,8 @@ public static class Names
{
public const string ENCRYPTED_FILE_EXTENSION = ".sffs";
public const string DIRECTORY_ID_FILENAME = "dirid.iv";
- public const string RECYCLE_BIN_NAME = "recycle_bin.vi";
+ public const string RECYCLE_BIN_NAME = "recycle_bin";
+ public const string RECYCLE_BIN_CONFIGURATION_FILENAME = "recycle_bin.cfg";
}
public static class Caching
@@ -28,7 +29,7 @@ public static class Caching
public const int RECOMMENDED_SIZE_CHUNK = 6;
public const int RECOMMENDED_SIZE_DIRECTORY_ID = 1000;
public const int RECOMMENDED_SIZE_CIPHERTEXT_FILENAMES = 2000;
- public const int RECOMMENDED_SIZE_Plaintext_FILENAMES = 2000;
+ public const int RECOMMENDED_SIZE_PLAINTEXT_FILENAMES = 2000;
}
}
}
diff --git a/src/SecureFolderFS.Core.FileSystem/CryptFiles/OpenCryptFile.cs b/src/SecureFolderFS.Core.FileSystem/CryptFiles/OpenCryptFile.cs
index ba4bef331..0b68c52ce 100644
--- a/src/SecureFolderFS.Core.FileSystem/CryptFiles/OpenCryptFile.cs
+++ b/src/SecureFolderFS.Core.FileSystem/CryptFiles/OpenCryptFile.cs
@@ -58,7 +58,7 @@ public PlaintextStream OpenStream(Stream ciphertextStream)
// Make sure to also add it to streams manager
_streamsManager.AddStream(ciphertextStream);
-
+
// Open the plaintext stream
return new PlaintextStream(ciphertextStream, _security, _chunkAccess, _headerBuffer, NotifyClosed);
}
diff --git a/src/SecureFolderFS.Core.FileSystem/DataModels/RecycleBinItemDataModel.cs b/src/SecureFolderFS.Core.FileSystem/DataModels/RecycleBinItemDataModel.cs
index 177443441..17a36f9e2 100644
--- a/src/SecureFolderFS.Core.FileSystem/DataModels/RecycleBinItemDataModel.cs
+++ b/src/SecureFolderFS.Core.FileSystem/DataModels/RecycleBinItemDataModel.cs
@@ -4,18 +4,38 @@
namespace SecureFolderFS.Core.FileSystem.DataModels
{
[Serializable]
- public sealed class RecycleBinItemDataModel
+ public sealed record class RecycleBinItemDataModel
{
///
- /// Gets the original (relative) ciphertext path of the item before it was deleted.
+ /// Gets the original ciphertext name of the item before it was deleted.
///
- [JsonPropertyName("originalPath")]
- public required string? OriginalPath { get; init; }
-
+ [JsonPropertyName("originalName")]
+ public required string? OriginalName { get; init; }
+
+ ///
+ /// Gets the original (relative) ciphertext path of the folder where the item resided before it was deleted.
+ ///
+ [JsonPropertyName("parentPath")]
+ public required string? ParentPath { get; init; }
+
+ ///
+ /// Gets the Directory ID of the directory where this item originally belonged to.
+ ///
+ [JsonPropertyName("directoryId")]
+ public required byte[]? DirectoryId { get; init; }
+
///
/// Gets the timestamp of the deletion.
///
[JsonPropertyName("deletionTimestamp")]
public required DateTime? DeletionTimestamp { get; init; }
+
+ ///
+ /// Gets the size in bytes of the item. The value might be less than zero indicating that the size was not calculated.
+ ///
+ [JsonPropertyName("size")]
+ public required long? Size { get; init; }
+
+ // TODO: Add MAC key signing for tamper proofing
}
}
diff --git a/src/SecureFolderFS.Core.FileSystem/Directories/DirectoryIdCache.cs b/src/SecureFolderFS.Core.FileSystem/Directories/DirectoryIdCache.cs
deleted file mode 100644
index 4f64dcd5c..000000000
--- a/src/SecureFolderFS.Core.FileSystem/Directories/DirectoryIdCache.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using SecureFolderFS.Shared.Enums;
-using SecureFolderFS.Shared.Models;
-using SecureFolderFS.Storage.VirtualFileSystem;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace SecureFolderFS.Core.FileSystem.Directories
-{
- ///
- /// Provides a cache for DirectoryIDs found on the encrypting file system.
- ///
- public sealed class DirectoryIdCache
- {
- private readonly object _lock = new();
- private readonly Dictionary _cache;
- private readonly IFileSystemStatistics _statistics;
-
- public DirectoryIdCache(IFileSystemStatistics statistics)
- {
- _statistics = statistics;
- _cache = new(Constants.Caching.RECOMMENDED_SIZE_DIRECTORY_ID);
- }
-
- ///
- /// Gets the DirectoryID of provided DirectoryID .
- ///
- /// The ciphertext path to the DirectoryID file.
- /// The to fill the DirectoryID into.
- /// If the was retrieved successfully; returns true; otherwise false.
- public bool GetDirectoryId(string ciphertextPath, Span directoryId)
- {
- // Check if directoryId is of correct length
- if (directoryId.Length != Constants.DIRECTORY_ID_SIZE)
- throw new ArgumentException($"The size of {nameof(directoryId)} was too small.");
-
- // Check if the ciphertext path is empty
- if (string.IsNullOrEmpty(ciphertextPath))
- throw new ArgumentException($"The {nameof(ciphertextPath)} was empty.");
-
- lock (_lock)
- {
- if (!_cache.TryGetValue(ciphertextPath, out var directoryIdBuffer))
- {
- // Cache miss, update stats
- _statistics.DirectoryIdCache?.Report(CacheAccessType.CacheMiss);
-
- return false;
- }
-
- // Cache hit, update stats
- _statistics.FileNameCache?.Report(CacheAccessType.CacheAccess);
- _statistics.DirectoryIdCache?.Report(CacheAccessType.CacheHit);
-
- directoryIdBuffer.Buffer.CopyTo(directoryId);
-
- return true;
- }
- }
-
- ///
- /// Sets the DirectoryID of provided DirectoryID file path.
- ///
- /// The ciphertext path to the DirectoryID file.
- /// The ID to set for the directory.
- public void SetDirectoryId(string ciphertextPath, ReadOnlySpan directoryId)
- {
- lock (_lock)
- {
- // Remove first item from cache if exceeds size
- if (_cache.Count >= FileSystem.Constants.Caching.RECOMMENDED_SIZE_DIRECTORY_ID)
- _cache.Remove(_cache.Keys.First());
-
- // Copy directoryId to cache
- _cache[ciphertextPath] = new(directoryId.ToArray());
- }
- }
-
- ///
- /// Removes associated DirectoryID from the list of known IDs.
- ///
- /// The path associated with the DirectoryID.
- public void RemoveDirectoryId(string ciphertextPath)
- {
- lock (_lock)
- {
- _ = _cache.Remove(ciphertextPath);
- }
- }
- }
-}
diff --git a/src/SecureFolderFS.Core.FileSystem/Exceptions/FileSystemExceptions.cs b/src/SecureFolderFS.Core.FileSystem/Exceptions/FileSystemExceptions.cs
deleted file mode 100644
index 96f351287..000000000
--- a/src/SecureFolderFS.Core.FileSystem/Exceptions/FileSystemExceptions.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System;
-
-namespace SecureFolderFS.Core.FileSystem.Exceptions
-{
- public static class FileSystemExceptions
- {
- public static Exception FileSystemReadOnly { get; } = new UnauthorizedAccessException("The file system is read-only.");
-
- public static Exception StreamReadOnly { get; } = new NotSupportedException("The Stream instance is read-only.");
-
- public static Exception StreamNotSeekable { get; } = new NotSupportedException("Seek is not supported for this stream.");
- }
-}
diff --git a/src/SecureFolderFS.Core.FileSystem/FileNames/CachingFileNameAccess.cs b/src/SecureFolderFS.Core.FileSystem/FileNames/CachingFileNameAccess.cs
index 320333bc2..2eadca51f 100644
--- a/src/SecureFolderFS.Core.FileSystem/FileNames/CachingFileNameAccess.cs
+++ b/src/SecureFolderFS.Core.FileSystem/FileNames/CachingFileNameAccess.cs
@@ -10,13 +10,13 @@ namespace SecureFolderFS.Core.FileSystem.FileNames
///
internal sealed class CachingFileNameAccess : FileNameAccess
{
- private readonly Dictionary _PlaintextNames;
+ private readonly Dictionary _plaintextNames;
private readonly Dictionary _ciphertextNames;
public CachingFileNameAccess(Security security, IFileSystemStatistics fileSystemStatistics)
: base(security, fileSystemStatistics)
{
- _PlaintextNames = new(FileSystem.Constants.Caching.RECOMMENDED_SIZE_Plaintext_FILENAMES);
+ _plaintextNames = new(FileSystem.Constants.Caching.RECOMMENDED_SIZE_PLAINTEXT_FILENAMES);
_ciphertextNames = new(FileSystem.Constants.Caching.RECOMMENDED_SIZE_CIPHERTEXT_FILENAMES);
}
@@ -26,9 +26,9 @@ public override string GetPlaintextName(ReadOnlySpan ciphertextName, ReadO
string plaintextName;
string stringCiphertext = ciphertextName.ToString();
- lock (_PlaintextNames)
+ lock (_plaintextNames)
{
- if (!_PlaintextNames.TryGetValue(new(directoryId.ToArray(), stringCiphertext), out plaintextName!))
+ if (!_plaintextNames.TryGetValue(new(directoryId.ToArray(), stringCiphertext), out plaintextName!))
{
// Not found in cache
statistics.FileNameCache?.Report(CacheAccessType.CacheMiss);
@@ -86,10 +86,10 @@ public override string GetCiphertextName(ReadOnlySpan plaintextName, ReadO
private void SetPlaintextName(string plaintextName, string ciphertextName, ReadOnlySpan directoryId)
{
- if (_PlaintextNames.Count >= FileSystem.Constants.Caching.RECOMMENDED_SIZE_Plaintext_FILENAMES)
- _PlaintextNames.Remove(_PlaintextNames.Keys.First(), out _);
+ if (_plaintextNames.Count >= FileSystem.Constants.Caching.RECOMMENDED_SIZE_PLAINTEXT_FILENAMES)
+ _plaintextNames.Remove(_plaintextNames.Keys.First(), out _);
- _PlaintextNames[new(directoryId.ToArray(), ciphertextName)] = plaintextName;
+ _plaintextNames[new(directoryId.ToArray(), ciphertextName)] = plaintextName;
}
private void SetCiphertextName(string ciphertextName, string plaintextName, ReadOnlySpan directoryId)
diff --git a/src/SecureFolderFS.Core.FileSystem/FileSystemSpecifics.cs b/src/SecureFolderFS.Core.FileSystem/FileSystemSpecifics.cs
index 9e1f5fd0e..5a75bcdfb 100644
--- a/src/SecureFolderFS.Core.FileSystem/FileSystemSpecifics.cs
+++ b/src/SecureFolderFS.Core.FileSystem/FileSystemSpecifics.cs
@@ -8,6 +8,9 @@
namespace SecureFolderFS.Core.FileSystem
{
+ ///
+ /// Represents the specifics of the file system, including different classes to manipulate the file system components.
+ ///
public sealed class FileSystemSpecifics : IDisposable
{
///
@@ -15,16 +18,34 @@ public sealed class FileSystemSpecifics : IDisposable
///
public required IFolder ContentFolder { get; init; }
+ ///
+ /// Gets the security object used for encrypting and decrypting data.
+ ///
public required Security Security { get; init; }
+ ///
+ /// Gets the streams access object used for managing file streams.
+ ///
public required StreamsAccess StreamsAccess { get; init; }
+ ///
+ /// Gets the file system options.
+ ///
public required FileSystemOptions Options { get; init; }
+ ///
+ /// Gets the cache for Directory IDs.
+ ///
public required UniversalCache DirectoryIdCache { get; init; }
-
+
+ ///
+ /// Gets the cache for ciphertext file names.
+ ///
public required UniversalCache CiphertextFileNameCache { get; init; }
+ ///
+ /// Gets the cache for plaintext file names.
+ ///
public required UniversalCache PlaintextFileNameCache { get; init; }
private FileSystemSpecifics()
@@ -41,18 +62,27 @@ public void Dispose()
Security.Dispose();
}
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The security object used for encrypting and decrypting data.
+ /// The root content folder that holds encrypted files.
+ /// The file system options.
+ /// A new instance of .
public static FileSystemSpecifics CreateNew(Security security, IFolder contentFolder, FileSystemOptions options)
{
return new()
{
ContentFolder = contentFolder,
PlaintextFileNameCache = options.IsCachingFileNames
- ? new(FileSystem.Constants.Caching.RECOMMENDED_SIZE_Plaintext_FILENAMES, options.FileSystemStatistics.FileNameCache)
+ ? new(FileSystem.Constants.Caching.RECOMMENDED_SIZE_PLAINTEXT_FILENAMES, options.FileSystemStatistics.FileNameCache)
: new(false, options.FileSystemStatistics.FileNameCache),
CiphertextFileNameCache = options.IsCachingFileNames
? new(FileSystem.Constants.Caching.RECOMMENDED_SIZE_CIPHERTEXT_FILENAMES, options.FileSystemStatistics.FileNameCache)
: new(false, options.FileSystemStatistics.FileNameCache),
- DirectoryIdCache = new(true, options.FileSystemStatistics.DirectoryIdCache),
+ DirectoryIdCache = options.IsCachingDirectoryIds
+ ? new(FileSystem.Constants.Caching.RECOMMENDED_SIZE_DIRECTORY_ID, options.FileSystemStatistics.DirectoryIdCache)
+ : new(false, options.FileSystemStatistics.DirectoryIdCache),
Options = options,
StreamsAccess = StreamsAccess.CreateNew(security, options.IsCachingChunks, options.FileSystemStatistics),
Security = security
diff --git a/src/SecureFolderFS.Core.FileSystem/Helpers/Paths/Abstract/AbstractPathHelpers.Cipher.cs b/src/SecureFolderFS.Core.FileSystem/Helpers/Paths/Abstract/AbstractPathHelpers.Cipher.cs
index eeb305a3f..acdcc02d4 100644
--- a/src/SecureFolderFS.Core.FileSystem/Helpers/Paths/Abstract/AbstractPathHelpers.Cipher.cs
+++ b/src/SecureFolderFS.Core.FileSystem/Helpers/Paths/Abstract/AbstractPathHelpers.Cipher.cs
@@ -1,8 +1,8 @@
using OwlCore.Storage;
using SecureFolderFS.Shared.ComponentModel;
-using SecureFolderFS.Shared.Models;
using SecureFolderFS.Storage.Extensions;
using System;
+using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -14,14 +14,15 @@ namespace SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract
///
public static partial class AbstractPathHelpers
{
+ // TODO: Add a comment that this method assumes that items have a backing store
public static async Task GetCiphertextPathAsync(IStorableChild plaintextStorable, FileSystemSpecifics specifics, CancellationToken cancellationToken)
{
if (specifics.Security.NameCrypt is null)
return plaintextStorable.Id;
+ var finalPath = string.Empty;
var currentStorable = plaintextStorable;
var expendableDirectoryId = new byte[Constants.DIRECTORY_ID_SIZE];
- var finalPath = string.Empty;
while (await currentStorable.GetParentAsync(cancellationToken).ConfigureAwait(false) is IChildFolder currentParent)
{
@@ -38,6 +39,41 @@ public static partial class AbstractPathHelpers
return Path.Combine(specifics.ContentFolder.Id, finalPath);
}
+ public static async Task GetCiphertextItemAsync(IStorableChild plaintextStorable, FileSystemSpecifics specifics, CancellationToken cancellationToken)
+ {
+ if (specifics.Security.NameCrypt is null)
+ return plaintextStorable;
+
+ var folderChain = new List();
+ var currentStorable = plaintextStorable;
+
+ while (await currentStorable.GetParentAsync(cancellationToken).ConfigureAwait(false) is IChildFolder currentParent)
+ {
+ folderChain.Insert(0, currentParent);
+ currentStorable = currentParent;
+ }
+
+ // Remove the first item (root)
+ folderChain.RemoveAt(0);
+
+ var finalFolder = specifics.ContentFolder;
+ var expendableDirectoryId = new byte[Constants.DIRECTORY_ID_SIZE];
+ foreach (var item in folderChain)
+ {
+ // Walk through plaintext folder chain and retrieve ciphertext folders
+ var subResult = await GetDirectoryIdAsync(finalFolder, specifics, expendableDirectoryId, cancellationToken).ConfigureAwait(false);
+ var subCiphertextName = specifics.Security.NameCrypt.EncryptName(item.Name, subResult ? expendableDirectoryId : ReadOnlySpan.Empty);
+
+ finalFolder = await finalFolder.GetFolderByNameAsync($"{subCiphertextName}{Constants.Names.ENCRYPTED_FILE_EXTENSION}", cancellationToken);
+ }
+
+ // Encrypt and retrieve the final item
+ var result = await GetDirectoryIdAsync(finalFolder, specifics, expendableDirectoryId, cancellationToken).ConfigureAwait(false);
+ var ciphertextName = specifics.Security.NameCrypt.EncryptName(plaintextStorable.Name, result ? expendableDirectoryId : ReadOnlySpan.Empty);
+
+ return await finalFolder.GetFirstByNameAsync($"{ciphertextName}{Constants.Names.ENCRYPTED_FILE_EXTENSION}", cancellationToken);
+ }
+
public static async Task GetPlaintextPathAsync(IStorableChild ciphertextStorable, FileSystemSpecifics specifics, CancellationToken cancellationToken)
{
if (specifics.Security.NameCrypt is null)
@@ -64,40 +100,49 @@ public static partial class AbstractPathHelpers
return finalPath;
}
- public static async Task GetDirectoryIdAsync(IFolder folderOfDirectoryId, FileSystemSpecifics specifics, Memory directoryId, CancellationToken cancellationToken)
+ ///
+ /// Encrypts the provided .
+ ///
+ /// The name to encrypt.
+ /// The ciphertext parent folder.
+ /// The instance associated with the item.
+ /// A that cancels this action.
+ /// A that represents the asynchronous operation. Value is an encrypted name.
+ public static async Task EncryptNameAsync(string plaintextName, IFolder parentFolder, FileSystemSpecifics specifics, CancellationToken cancellationToken = default)
{
- if (folderOfDirectoryId.Id == specifics.ContentFolder.Id)
- return false;
+ if (specifics.Security.NameCrypt is null)
+ return plaintextName;
- BufferHolder? cachedId;
- if (specifics.DirectoryIdCache.IsAvailable)
+ var directoryId = AllocateDirectoryId(specifics.Security, plaintextName);
+ var result = await GetDirectoryIdAsync(parentFolder, specifics, directoryId, cancellationToken);
+
+ return specifics.Security.NameCrypt.EncryptName(plaintextName, result ? directoryId : ReadOnlySpan.Empty) + FileSystem.Constants.Names.ENCRYPTED_FILE_EXTENSION;
+ }
+
+ ///
+ /// Decrypts the provided .
+ ///
+ /// The name to decrypt.
+ /// The ciphertext parent folder.
+ /// The instance associated with the item.
+ /// A that cancels this action.
+ /// A that represents the asynchronous operation. Value is a decrypted name.
+ public static async Task DecryptNameAsync(string ciphertextName, IFolder parentFolder, FileSystemSpecifics specifics, CancellationToken cancellationToken = default)
+ {
+ try
{
- cachedId = specifics.DirectoryIdCache.CacheGet(folderOfDirectoryId.Id);
- if (cachedId is not null)
- {
- cachedId.Buffer.CopyTo(directoryId);
- return true;
- }
- }
+ if (specifics.Security.NameCrypt is null)
+ return ciphertextName;
+
+ var directoryId = AllocateDirectoryId(specifics.Security, ciphertextName);
+ var result = await GetDirectoryIdAsync(parentFolder, specifics, directoryId, cancellationToken);
- var directoryIdFile = await folderOfDirectoryId.GetFileByNameAsync(Constants.Names.DIRECTORY_ID_FILENAME, cancellationToken).ConfigureAwait(false);
- await using var directoryIdStream = await directoryIdFile.OpenStreamAsync(FileAccess.Read, FileShare.Read, cancellationToken).ConfigureAwait(false);
-
- int read;
- if (specifics.DirectoryIdCache.IsAvailable)
+ return specifics.Security.NameCrypt.DecryptName(Path.GetFileNameWithoutExtension(ciphertextName), result ? directoryId : ReadOnlySpan.Empty);
+ }
+ catch (Exception)
{
- cachedId = new(Constants.DIRECTORY_ID_SIZE);
- read = await directoryIdStream.ReadAsync(cachedId.Buffer, cancellationToken).ConfigureAwait(false);
- specifics.DirectoryIdCache.CacheSet(folderOfDirectoryId.Id, cachedId);
+ return null;
}
- else
- read = await directoryIdStream.ReadAsync(directoryId, cancellationToken).ConfigureAwait(false);
-
- if (read < Constants.DIRECTORY_ID_SIZE)
- throw new IOException($"The data inside Directory ID file is of incorrect size: {read}.");
-
- // The Directory ID is not empty - return true
- return true;
}
}
}
diff --git a/src/SecureFolderFS.Core.FileSystem/Helpers/Paths/Abstract/AbstractPathHelpers.Directory.cs b/src/SecureFolderFS.Core.FileSystem/Helpers/Paths/Abstract/AbstractPathHelpers.Directory.cs
new file mode 100644
index 000000000..11e995719
--- /dev/null
+++ b/src/SecureFolderFS.Core.FileSystem/Helpers/Paths/Abstract/AbstractPathHelpers.Directory.cs
@@ -0,0 +1,52 @@
+using OwlCore.Storage;
+using SecureFolderFS.Shared.Models;
+using SecureFolderFS.Storage.Extensions;
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract
+{
+ public static partial class AbstractPathHelpers
+ {
+ public static async Task GetDirectoryIdAsync(IFolder folderOfDirectoryId, FileSystemSpecifics specifics,
+ Memory directoryId, CancellationToken cancellationToken)
+ {
+ if (folderOfDirectoryId.Id == specifics.ContentFolder.Id)
+ return false;
+
+ BufferHolder? cachedId;
+ if (specifics.DirectoryIdCache.IsAvailable)
+ {
+ cachedId = specifics.DirectoryIdCache.CacheGet(Path.Combine(folderOfDirectoryId.Id, Constants.Names.DIRECTORY_ID_FILENAME));
+ if (cachedId is not null)
+ {
+ cachedId.Buffer.CopyTo(directoryId);
+ return true;
+ }
+ }
+
+ var directoryIdFile = await folderOfDirectoryId.GetFileByNameAsync(Constants.Names.DIRECTORY_ID_FILENAME, cancellationToken).ConfigureAwait(false);
+ await using var directoryIdStream = await directoryIdFile.OpenStreamAsync(FileAccess.Read, FileShare.Read, cancellationToken).ConfigureAwait(false);
+
+ int read;
+ if (specifics.DirectoryIdCache.IsAvailable)
+ {
+ cachedId = new(Constants.DIRECTORY_ID_SIZE);
+ read = await directoryIdStream.ReadAsync(cachedId.Buffer, cancellationToken).ConfigureAwait(false);
+ specifics.DirectoryIdCache.CacheSet(Path.Combine(folderOfDirectoryId.Id, Constants.Names.DIRECTORY_ID_FILENAME), cachedId);
+
+ cachedId.Buffer.CopyTo(directoryId);
+ }
+ else
+ read = await directoryIdStream.ReadAsync(directoryId, cancellationToken).ConfigureAwait(false);
+
+ if (read < Constants.DIRECTORY_ID_SIZE)
+ throw new IOException($"The data inside Directory ID file is of incorrect size: {read}.");
+
+ // The Directory ID is not empty - return true
+ return true;
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Core.FileSystem/Helpers/Paths/PathHelpers.cs b/src/SecureFolderFS.Core.FileSystem/Helpers/Paths/PathHelpers.cs
index eb40dcca5..d5c5bcf23 100644
--- a/src/SecureFolderFS.Core.FileSystem/Helpers/Paths/PathHelpers.cs
+++ b/src/SecureFolderFS.Core.FileSystem/Helpers/Paths/PathHelpers.cs
@@ -10,9 +10,10 @@ public static class PathHelpers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsCoreName(string itemName)
{
- return
+ return
itemName.Contains(Constants.Names.DIRECTORY_ID_FILENAME, StringComparison.OrdinalIgnoreCase) ||
- itemName.Contains(Constants.Names.RECYCLE_BIN_NAME, StringComparison.OrdinalIgnoreCase);
+ itemName.Contains(Constants.Names.RECYCLE_BIN_NAME, StringComparison.OrdinalIgnoreCase) ||
+ itemName.Contains(Constants.Names.RECYCLE_BIN_CONFIGURATION_FILENAME, StringComparison.OrdinalIgnoreCase);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -33,7 +34,7 @@ public static string EnsureNoLeadingPathSeparator(string path)
}
else if (OperatingSystem.IsMacCatalyst())
{
- return $@"{Path.DirectorySeparatorChar}{Path.Combine("Volumes", nameHint)}{Path.DirectorySeparatorChar}";
+ return $"{Path.DirectorySeparatorChar}{Path.Combine("Volumes", nameHint)}{Path.DirectorySeparatorChar}";
}
return null;
diff --git a/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Operational.cs b/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Operational.cs
new file mode 100644
index 000000000..eca807176
--- /dev/null
+++ b/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Operational.cs
@@ -0,0 +1,189 @@
+using OwlCore.Storage;
+using SecureFolderFS.Core.FileSystem.DataModels;
+using SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract;
+using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Shared.Extensions;
+using SecureFolderFS.Storage.Extensions;
+using SecureFolderFS.Storage.Renamable;
+using SecureFolderFS.Storage.VirtualFileSystem;
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SecureFolderFS.Core.FileSystem.Helpers.RecycleBin.Abstract
+{
+ public static partial class AbstractRecycleBinHelpers
+ {
+ public static async Task GetDestinationFolderAsync(IStorableChild item, FileSystemSpecifics specifics, IAsyncSerializer streamSerializer, CancellationToken cancellationToken = default)
+ {
+ // Get recycle bin
+ var recycleBin = await TryGetRecycleBinAsync(specifics, cancellationToken);
+ if (recycleBin is null)
+ throw new DirectoryNotFoundException("Could not find recycle bin folder.");
+
+ // Deserialize configuration
+ var deserialized = await GetItemDataModelAsync(item, recycleBin, streamSerializer, cancellationToken);
+ if (deserialized is not { ParentPath: not null, OriginalName: not null })
+ throw new FormatException("Could not deserialize recycle bin configuration file.");
+
+ // Check if destination item exists
+ var parentId = deserialized.ParentPath.Replace('/', Path.DirectorySeparatorChar);
+ var itemId = Path.Combine(parentId, deserialized.OriginalName);
+ try
+ {
+ _ = await specifics.ContentFolder.GetItemByRelativePathOrSelfAsync(itemId, cancellationToken);
+
+ // Destination item already exists, user must choose a new location
+ return null;
+ }
+ catch (Exception) { }
+
+ // Check if destination folder exists
+ try
+ {
+ var parentItem = await specifics.ContentFolder.GetItemByRelativePathOrSelfAsync(parentId, cancellationToken);
+
+ // Assume the parent is a folder and return it
+ return parentItem as IModifiableFolder;
+ }
+ catch (Exception) { }
+
+ // No destination folder has been found, user must choose a new location
+ return null;
+ }
+
+ public static async Task RestoreAsync(IStorableChild item, IModifiableFolder destinationFolder, FileSystemSpecifics specifics, IAsyncSerializer streamSerializer, CancellationToken cancellationToken = default)
+ {
+ if (specifics.Options.IsReadOnly)
+ throw FileSystemExceptions.FileSystemReadOnly;
+
+ // Get recycle bin
+ var recycleBin = await TryGetRecycleBinAsync(specifics, cancellationToken);
+ if (recycleBin is not IRenamableFolder renamableRecycleBin)
+ throw new UnauthorizedAccessException("The recycle bin is not renamable.");
+
+ // Deserialize configuration
+ var deserialized = await GetItemDataModelAsync(item, recycleBin, streamSerializer, cancellationToken);
+ _ = deserialized.OriginalName ?? throw new IOException("Could not get file name.");
+ _ = deserialized.ParentPath ?? throw new IOException("Could not get parent path.");
+
+ if (!destinationFolder.Id.EndsWith(deserialized.ParentPath))
+ {
+ // Destination folder is different from the original destination
+ // A new item name should be chosen fit for the new folder (so that Directory ID match)
+
+ var plaintextName = specifics.Security.NameCrypt?.DecryptName(Path.GetFileNameWithoutExtension(deserialized.OriginalName), deserialized.DirectoryId) ?? deserialized.OriginalName;
+ var ciphertextName = await AbstractPathHelpers.EncryptNameAsync(plaintextName, destinationFolder, specifics, cancellationToken);
+
+ // Rename the item to correct name
+ var renamedItem = await renamableRecycleBin.RenameAsync(item, ciphertextName, cancellationToken);
+
+ // Move item to destination
+ _ = await destinationFolder.MoveStorableFromAsync(renamedItem, renamableRecycleBin, false, cancellationToken);
+ }
+ else
+ {
+ // Destination folder is the same as the original destination
+ // The same name could be used since the Directory IDs match
+ // TODO: Check if the Directory IDs actually match and fallback to above method if not
+
+ // Rename the item to correct name
+ var renamedItem = await renamableRecycleBin.RenameAsync(item, deserialized.OriginalName, cancellationToken);
+
+ // Move item to destination
+ _ = await destinationFolder.MoveStorableFromAsync(renamedItem, renamableRecycleBin, false, cancellationToken);
+ }
+
+ // Delete old configuration file
+ var configurationFile = await recycleBin.GetFileByNameAsync($"{item.Name}.json", cancellationToken);
+ await renamableRecycleBin.DeleteAsync(configurationFile, cancellationToken);
+
+ // Check if the item had any size
+ if (deserialized.Size is not ({ } size and > 0L))
+ return;
+
+ // Update occupied size
+ var occupiedSize = await GetOccupiedSizeAsync(renamableRecycleBin, cancellationToken);
+ var newSize = occupiedSize - size;
+ await SetOccupiedSizeAsync(renamableRecycleBin, newSize, cancellationToken);
+ }
+
+ public static async Task DeleteOrRecycleAsync(
+ IModifiableFolder sourceFolder,
+ IStorableChild item,
+ FileSystemSpecifics specifics,
+ IAsyncSerializer streamSerializer,
+ long sizeHint = -1L,
+ CancellationToken cancellationToken = default)
+ {
+ if (specifics.Options.IsReadOnly)
+ throw FileSystemExceptions.FileSystemReadOnly;
+
+ if (!specifics.Options.IsRecycleBinEnabled())
+ {
+ await sourceFolder.DeleteAsync(item, cancellationToken);
+ return;
+ }
+
+ // Get recycle bin
+ var recycleBin = await GetOrCreateRecycleBinAsync(specifics, cancellationToken);
+ if (recycleBin is not IRenamableFolder renamableRecycleBin)
+ throw new UnauthorizedAccessException("The recycle bin is not renamable.");
+
+ if (sizeHint < 0L && specifics.Options.RecycleBinSize > 0L)
+ {
+ sizeHint = item switch
+ {
+ IFile file => await file.GetSizeAsync(cancellationToken),
+ IFolder folder => await folder.GetSizeAsync(cancellationToken),
+ _ => 0L
+ };
+
+ var occupiedSize = await GetOccupiedSizeAsync(renamableRecycleBin, cancellationToken);
+ var availableSize = specifics.Options.RecycleBinSize - occupiedSize;
+ if (availableSize < sizeHint)
+ {
+ await sourceFolder.DeleteAsync(item, cancellationToken);
+ return;
+ }
+ }
+
+ // Get source Directory ID
+ var directoryId = AbstractPathHelpers.AllocateDirectoryId(specifics.Security, sourceFolder.Id);
+ var directoryIdResult = await AbstractPathHelpers.GetDirectoryIdAsync(sourceFolder, specifics, directoryId, cancellationToken);
+
+ // Move and rename item
+ var guid = Guid.NewGuid().ToString();
+ var movedItem = await renamableRecycleBin.MoveStorableFromAsync(item, sourceFolder, false, cancellationToken);
+ _ = await renamableRecycleBin.RenameAsync(movedItem, guid, cancellationToken);
+
+ // Create configuration file
+ var configurationFile = await renamableRecycleBin.CreateFileAsync($"{guid}.json", false, cancellationToken);
+ await using var configurationStream = await configurationFile.OpenReadWriteAsync(cancellationToken);
+
+ // Serialize configuration data model
+ await using var serializedStream = await streamSerializer.SerializeAsync(
+ new RecycleBinItemDataModel()
+ {
+ OriginalName = item.Name,
+ ParentPath = sourceFolder.Id.Replace(specifics.ContentFolder.Id, string.Empty).Replace(Path.DirectorySeparatorChar, '/'),
+ DirectoryId = directoryIdResult ? directoryId : [],
+ DeletionTimestamp = DateTime.Now,
+ Size = sizeHint
+ }, cancellationToken);
+
+ // Write to destination stream
+ await serializedStream.CopyToAsync(configurationStream, cancellationToken);
+ await configurationStream.FlushAsync(cancellationToken);
+
+ // Update occupied size
+ if (specifics.Options.IsRecycleBinEnabled())
+ {
+ var occupiedSize = await GetOccupiedSizeAsync(renamableRecycleBin, cancellationToken);
+ var newSize = occupiedSize + sizeHint;
+ await SetOccupiedSizeAsync(renamableRecycleBin, newSize, cancellationToken);
+ }
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Shared.cs b/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Shared.cs
new file mode 100644
index 000000000..21a3f473c
--- /dev/null
+++ b/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.Shared.cs
@@ -0,0 +1,74 @@
+using OwlCore.Storage;
+using SecureFolderFS.Core.FileSystem.DataModels;
+using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Shared.Extensions;
+using SecureFolderFS.Storage.Extensions;
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using SecureFolderFS.Storage.VirtualFileSystem;
+
+namespace SecureFolderFS.Core.FileSystem.Helpers.RecycleBin.Abstract
+{
+ public static partial class AbstractRecycleBinHelpers
+ {
+ public static async Task GetOccupiedSizeAsync(IModifiableFolder recycleBin, CancellationToken cancellationToken = default)
+ {
+ var recycleBinConfig = await recycleBin.CreateFileAsync(Constants.Names.RECYCLE_BIN_CONFIGURATION_FILENAME, false, cancellationToken);
+ var text = await recycleBinConfig.ReadAllTextAsync(null, cancellationToken);
+ if (!long.TryParse(text, out var value))
+ return 0L;
+
+ return Math.Max(0L, value);
+ }
+
+ public static async Task SetOccupiedSizeAsync(IModifiableFolder recycleBin, long value, CancellationToken cancellationToken = default)
+ {
+ var recycleBinConfig = await recycleBin.CreateFileAsync(Constants.Names.RECYCLE_BIN_CONFIGURATION_FILENAME, false, cancellationToken);
+ await recycleBinConfig.WriteAllTextAsync(Math.Max(0L, value).ToString(), null, cancellationToken);
+ }
+
+ public static async Task GetItemDataModelAsync(IStorableChild item, IFolder recycleBin, IAsyncSerializer streamSerializer, CancellationToken cancellationToken = default)
+ {
+ // Get the configuration file
+ var configurationFile = !item.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase)
+ ? await recycleBin.GetFileByNameAsync($"{item.Name}.json", cancellationToken)
+ : (IFile)item;
+
+ // Read configuration file
+ await using var configurationStream = await configurationFile.OpenReadAsync(cancellationToken);
+
+ // Deserialize configuration
+ var deserialized = await streamSerializer.DeserializeAsync(configurationStream, cancellationToken);
+ if (deserialized is not { ParentPath: not null })
+ throw new FormatException("Could not deserialize recycle bin configuration file.");
+
+ return deserialized;
+ }
+
+ public static async Task TryGetRecycleBinAsync(FileSystemSpecifics specifics, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ return await specifics.ContentFolder.GetFolderByNameAsync(Constants.Names.RECYCLE_BIN_NAME, cancellationToken);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ public static async Task GetOrCreateRecycleBinAsync(FileSystemSpecifics specifics, CancellationToken cancellationToken = default)
+ {
+ var recycleBin = await TryGetRecycleBinAsync(specifics, cancellationToken);
+ if (recycleBin is not null)
+ return recycleBin;
+
+ if (specifics.ContentFolder is not IModifiableFolder modifiableFolder || specifics.Options.IsReadOnly)
+ throw FileSystemExceptions.FileSystemReadOnly;
+
+ return await modifiableFolder.CreateFolderAsync(Constants.Names.RECYCLE_BIN_NAME, false, cancellationToken);
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.cs b/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.cs
deleted file mode 100644
index 5ee52ea6a..000000000
--- a/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Abstract/AbstractRecycleBinHelpers.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using OwlCore.Storage;
-using SecureFolderFS.Core.FileSystem.DataModels;
-using SecureFolderFS.Shared.ComponentModel;
-using SecureFolderFS.Shared.Extensions;
-using SecureFolderFS.Storage.Extensions;
-using SecureFolderFS.Storage.Renamable;
-
-namespace SecureFolderFS.Core.FileSystem.Helpers.RecycleBin.Abstract
-{
- public static class AbstractRecycleBinHelpers
- {
- public static async Task RestoreAsync(IStorableChild item, FileSystemSpecifics specifics, IAsyncSerializer streamSerializer, CancellationToken cancellationToken = default)
- {
- if (specifics.Options.IsReadOnly)
- return;
-
- // Get recycle bin
- var recycleBin = await GetOrCreateRecycleBinAsync(specifics, cancellationToken);
-
- // Read configuration file
- var configurationFile = await recycleBin.GetFileByNameAsync(Constants.Names.RECYCLE_BIN_NAME, cancellationToken);
- await using var configurationStream = await configurationFile.OpenReadAsync(cancellationToken);
-
- // Deserialize configuration
- var deserialized = await streamSerializer.DeserializeAsync(configurationStream, cancellationToken);
- if (deserialized is not { OriginalPath: { }})
- throw new FormatException("Could not deserialize recycle bin configuration file.");
-
- // Get parent destination folder
- var id = Path.GetDirectoryName(deserialized.OriginalPath.Replace('/', Path.DirectorySeparatorChar)) ?? string.Empty;
- var parentFolder = await specifics.ContentFolder.GetItemRecursiveAsync(id, cancellationToken);
- if (parentFolder is not IModifiableFolder modifiableParent)
- throw new UnauthorizedAccessException("The parent folder is not modifiable.");
-
- await RestoreAsync(item, modifiableParent, specifics, cancellationToken);
- }
-
- public static async Task RestoreAsync(IStorableChild item, IModifiableFolder destinationFolder, FileSystemSpecifics specifics, CancellationToken cancellationToken = default)
- {
- // Get recycle bin
- var recycleBin = await GetOrCreateRecycleBinAsync(specifics, cancellationToken);
- if (recycleBin is not IRenamableFolder renamableRecycleBin)
- throw new UnauthorizedAccessException("The recycle bin is not renamable.");
- }
-
- public static async Task DeleteOrTrashAsync(IModifiableFolder sourceFolder, IStorableChild item, FileSystemSpecifics specifics, IAsyncSerializer streamSerializer, CancellationToken cancellationToken = default)
- {
- if (specifics.Options.IsReadOnly)
- return;
-
- if (!specifics.Options.IsRecycleBinEnabled)
- {
- await sourceFolder.DeleteAsync(item, cancellationToken);
- return;
- }
-
- // Get recycle bin
- var recycleBin = await GetOrCreateRecycleBinAsync(specifics, cancellationToken);
- if (recycleBin is not IRenamableFolder renamableRecycleBin)
- throw new UnauthorizedAccessException("The recycle bin is not renamable.");
-
- // Move and rename item
- var guid = Guid.NewGuid().ToString();
- var movedItem = await renamableRecycleBin.MoveStorableFromAsync(item, sourceFolder, false, cancellationToken);
- _ = await renamableRecycleBin.RenameAsync(movedItem, guid, cancellationToken);
-
- // Create configuration file
- var configurationFile = await renamableRecycleBin.CreateFileAsync($"{guid}.json", false, cancellationToken);
- await using var configurationStream = await configurationFile.OpenReadWriteAsync(cancellationToken);
-
- // Serialize configuration data model
- await using var serializedStream = await streamSerializer.SerializeAsync(
- new RecycleBinItemDataModel()
- {
- OriginalPath = item.Id.Replace(Path.DirectorySeparatorChar, '/'),
- DeletionTimestamp = DateTime.Now
- }, cancellationToken);
-
- // Write to destination stream
- await serializedStream.CopyToAsync(configurationStream, cancellationToken);
- }
-
- public static async Task GetOrCreateRecycleBinAsync(FileSystemSpecifics specifics, CancellationToken cancellationToken = default)
- {
- try
- {
- return await specifics.ContentFolder.GetFolderByNameAsync(Constants.Names.RECYCLE_BIN_NAME, cancellationToken);
- }
- catch (Exception ex)
- {
- _ = ex;
- }
-
- if (specifics.ContentFolder is not IModifiableFolder modifiableFolder)
- throw new UnauthorizedAccessException("The content folder is not modifiable.");
-
- return await modifiableFolder.CreateFolderAsync(Constants.Names.RECYCLE_BIN_NAME, false, cancellationToken);
- }
- }
-}
diff --git a/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Native/NativeRecycleBinHelpers.Operational.cs b/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Native/NativeRecycleBinHelpers.Operational.cs
new file mode 100644
index 000000000..223bdc5bb
--- /dev/null
+++ b/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Native/NativeRecycleBinHelpers.Operational.cs
@@ -0,0 +1,115 @@
+using OwlCore.Storage;
+using SecureFolderFS.Core.FileSystem.DataModels;
+using SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract;
+using SecureFolderFS.Core.FileSystem.Helpers.Paths.Native;
+using SecureFolderFS.Shared.Extensions;
+using SecureFolderFS.Shared.Models;
+using SecureFolderFS.Storage.Extensions;
+using SecureFolderFS.Storage.VirtualFileSystem;
+using System;
+using System.IO;
+
+namespace SecureFolderFS.Core.FileSystem.Helpers.RecycleBin.Native
+{
+ public static partial class NativeRecycleBinHelpers
+ {
+ public static void DeleteOrRecycle(string ciphertextPath, FileSystemSpecifics specifics, StorableType storableType, long sizeHint = -1L)
+ {
+ if (specifics.Options.IsReadOnly)
+ throw FileSystemExceptions.FileSystemReadOnly;
+
+ storableType = AlignStorableType(ciphertextPath);
+ if (!specifics.Options.IsRecycleBinEnabled())
+ {
+ DeleteImmediately(ciphertextPath, storableType);
+ return;
+ }
+
+ var recycleBinPath = Path.Combine(specifics.ContentFolder.Id, Constants.Names.RECYCLE_BIN_NAME);
+ _ = Directory.CreateDirectory(recycleBinPath);
+
+ if (sizeHint < 0L && specifics.Options.RecycleBinSize > 0L)
+ {
+ sizeHint = storableType switch
+ {
+ StorableType.File => new FileInfo(ciphertextPath).Length,
+ StorableType.Folder => GetFolderSizeRecursive(ciphertextPath),
+ _ => 0L
+ };
+
+ var occupiedSize = GetOccupiedSize(specifics);
+ var availableSize = specifics.Options.RecycleBinSize - occupiedSize;
+ if (availableSize < sizeHint)
+ {
+ DeleteImmediately(ciphertextPath, storableType);
+ return;
+ }
+ }
+
+ // Get source Directory ID
+ var directoryId = AbstractPathHelpers.AllocateDirectoryId(specifics.Security);
+ var directoryIdResult = NativePathHelpers.GetDirectoryId(ciphertextPath, specifics, directoryId);
+
+ // Move and rename item
+ var guid = Guid.NewGuid().ToString();
+ var destinationPath = Path.Combine(recycleBinPath, guid);
+ Directory.Move(ciphertextPath, destinationPath);
+
+ // Create configuration file
+ using var configurationStream = File.Create($"{destinationPath}.json");
+
+ // Serialize configuration data model
+ using var serializedStream = StreamSerializer.Instance.SerializeAsync(
+ new RecycleBinItemDataModel()
+ {
+ OriginalName = Path.GetFileName(ciphertextPath),
+ ParentPath = Path.GetDirectoryName(ciphertextPath)?.Replace(specifics.ContentFolder.Id, string.Empty).Replace(Path.DirectorySeparatorChar, '/') ?? string.Empty,
+ DirectoryId = directoryIdResult ? directoryId : [],
+ DeletionTimestamp = DateTime.Now,
+ Size = sizeHint
+ }).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Write to destination stream
+ serializedStream.CopyTo(configurationStream);
+ serializedStream.Flush();
+
+ // Update occupied size
+ if (specifics.Options.IsRecycleBinEnabled())
+ {
+ var occupiedSize = GetOccupiedSize(specifics);
+ var newSize = occupiedSize + sizeHint;
+ SetOccupiedSize(specifics, newSize);
+ }
+
+ return;
+
+ StorableType AlignStorableType(string path)
+ {
+ var type = storableType is StorableType.File or StorableType.Folder ? storableType : GetStorableType(path);
+ if (type == StorableType.None)
+ throw new FileNotFoundException("The item could not be determined.");
+
+ return type;
+ }
+
+ static StorableType GetStorableType(string path)
+ {
+ if (File.Exists(path))
+ return StorableType.File;
+
+ if (Directory.Exists(path))
+ return StorableType.Folder;
+
+ return StorableType.None;
+ }
+
+ static void DeleteImmediately(string path, StorableType type)
+ {
+ if (type == StorableType.File)
+ File.Delete(path);
+ else if (type == StorableType.Folder)
+ Directory.Delete(path, true);
+ }
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Native/NativeRecycleBinHelpers.Shared.cs b/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Native/NativeRecycleBinHelpers.Shared.cs
new file mode 100644
index 000000000..d01acf9a2
--- /dev/null
+++ b/src/SecureFolderFS.Core.FileSystem/Helpers/RecycleBin/Native/NativeRecycleBinHelpers.Shared.cs
@@ -0,0 +1,77 @@
+using SecureFolderFS.Storage.VirtualFileSystem;
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SecureFolderFS.Core.FileSystem.Helpers.RecycleBin.Native
+{
+ public static partial class NativeRecycleBinHelpers
+ {
+ public static long GetFolderSizeRecursive(string path)
+ {
+ if (!Directory.Exists(path))
+ throw new DirectoryNotFoundException($"The directory '{path}' does not exist.");
+
+ var totalSize = 0L;
+ try
+ {
+ // Sum file sizes in the current directory
+ var files = Directory.GetFiles(path);
+ Parallel.ForEach(files, () => 0L, (file, _, localTotal) =>
+ {
+ try
+ {
+ var fileInfo = new FileInfo(file);
+ return localTotal + fileInfo.Length;
+ }
+ catch
+ {
+ // Ignore errors (e.g., access denied)
+ return localTotal;
+ }
+ },
+ localTotal => Interlocked.Add(ref totalSize, localTotal));
+
+ // Recurse into subdirectories in parallel
+ var subDirs = Directory.GetDirectories(path);
+ Parallel.ForEach(subDirs, dir =>
+ {
+ var subDirSize = GetFolderSizeRecursive(dir);
+ Interlocked.Add(ref totalSize, subDirSize);
+ });
+ }
+ catch (Exception)
+ {
+ }
+
+ return totalSize;
+ }
+
+ public static long GetOccupiedSize(FileSystemSpecifics specifics)
+ {
+ var configPath = Path.Combine(specifics.ContentFolder.Id, Constants.Names.RECYCLE_BIN_NAME, Constants.Names.RECYCLE_BIN_CONFIGURATION_FILENAME);
+ using var configStream = specifics.Options.IsReadOnly ? File.OpenRead(configPath) : (!File.Exists(configPath) ? File.Create(configPath) : File.OpenRead(configPath));
+ using var streamReader = new StreamReader(configStream);
+
+ var text = streamReader.ReadToEnd();
+ if (!long.TryParse(text, out var value))
+ return 0L;
+
+ return Math.Max(0L, value);
+ }
+
+ public static void SetOccupiedSize(FileSystemSpecifics specifics, long value)
+ {
+ if (specifics.Options.IsReadOnly)
+ throw FileSystemExceptions.FileSystemReadOnly;
+
+ var configPath = Path.Combine(specifics.ContentFolder.Id, Constants.Names.RECYCLE_BIN_NAME, Constants.Names.RECYCLE_BIN_CONFIGURATION_FILENAME);
+ using var configStream = !File.Exists(configPath) ? File.Create(configPath) : File.OpenWrite(configPath);
+ using var streamWriter = new StreamWriter(configStream);
+
+ var text = Math.Max(0L, value).ToString();
+ streamWriter.Write(text);
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Core.FileSystem/OpenHandles/BaseHandlesManager.cs b/src/SecureFolderFS.Core.FileSystem/OpenHandles/BaseHandlesManager.cs
index cd592161b..746e1ba91 100644
--- a/src/SecureFolderFS.Core.FileSystem/OpenHandles/BaseHandlesManager.cs
+++ b/src/SecureFolderFS.Core.FileSystem/OpenHandles/BaseHandlesManager.cs
@@ -105,8 +105,7 @@ protected sealed class HandlesGenerator
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong ThreadSafeIncrement()
{
- Interlocked.Increment(ref _handleCounter);
- return _handleCounter;
+ return Interlocked.Increment(ref _handleCounter);
}
}
}
diff --git a/src/SecureFolderFS.Core.FileSystem/Storage/CryptoFile.cs b/src/SecureFolderFS.Core.FileSystem/Storage/CryptoFile.cs
index b4e10723d..9ba280ffd 100644
--- a/src/SecureFolderFS.Core.FileSystem/Storage/CryptoFile.cs
+++ b/src/SecureFolderFS.Core.FileSystem/Storage/CryptoFile.cs
@@ -1,11 +1,11 @@
-using System;
+using OwlCore.Storage;
+using SecureFolderFS.Core.FileSystem.Storage.StorageProperties;
+using SecureFolderFS.Storage.StorageProperties;
+using SecureFolderFS.Storage.VirtualFileSystem;
+using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
-using OwlCore.Storage;
-using SecureFolderFS.Core.FileSystem.Exceptions;
-using SecureFolderFS.Core.FileSystem.Storage.StorageProperties;
-using SecureFolderFS.Storage.StorageProperties;
namespace SecureFolderFS.Core.FileSystem.Storage
{
@@ -26,7 +26,7 @@ public virtual async Task OpenStreamAsync(FileAccess access, Cancellatio
var stream = await Inner.OpenStreamAsync(access, cancellationToken);
return CreatePlaintextStream(stream);
}
-
+
///
public override async Task GetPropertiesAsync()
{
@@ -35,7 +35,7 @@ public override async Task GetPropertiesAsync()
var innerProperties = await storableProperties.GetPropertiesAsync();
properties ??= new CryptoFileProperties(specifics, innerProperties);
-
+
return properties;
}
diff --git a/src/SecureFolderFS.Core.FileSystem/Storage/CryptoFolder.cs b/src/SecureFolderFS.Core.FileSystem/Storage/CryptoFolder.cs
index 0af6a3a78..73f215cff 100644
--- a/src/SecureFolderFS.Core.FileSystem/Storage/CryptoFolder.cs
+++ b/src/SecureFolderFS.Core.FileSystem/Storage/CryptoFolder.cs
@@ -1,38 +1,42 @@
-using System;
+using OwlCore.Storage;
+using SecureFolderFS.Core.FileSystem.Helpers.Paths;
+using SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract;
+using SecureFolderFS.Core.FileSystem.Helpers.RecycleBin.Abstract;
+using SecureFolderFS.Core.FileSystem.Storage.StorageProperties;
+using SecureFolderFS.Shared.Models;
+using SecureFolderFS.Storage.Recyclable;
+using SecureFolderFS.Storage.Renamable;
+using SecureFolderFS.Storage.StorageProperties;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
-using OwlCore.Storage;
-using SecureFolderFS.Core.FileSystem.Helpers.Paths;
-using SecureFolderFS.Core.FileSystem.Storage.StorageProperties;
-using SecureFolderFS.Storage.Renamable;
-using SecureFolderFS.Storage.StorageProperties;
namespace SecureFolderFS.Core.FileSystem.Storage
{
// TODO(ns): Add move and copy support
///
- public class CryptoFolder : CryptoStorable, IChildFolder, IModifiableFolder, IGetFirstByName, IRenamableFolder
+ public class CryptoFolder : CryptoStorable, IChildFolder, IGetFirstByName, IRenamableFolder, IRecyclableFolder
{
public CryptoFolder(string plaintextId, IFolder inner, FileSystemSpecifics specifics, CryptoFolder? parent = null)
: base(plaintextId, inner, specifics, parent)
{
}
-
+
///
public async Task RenameAsync(IStorableChild storable, string newName, CancellationToken cancellationToken = default)
{
if (Inner is not IRenamableFolder renamableFolder)
throw new NotSupportedException("Renaming folder contents is not supported.");
-
+
// We need to get the equivalent on the disk
- var ciphertextName = await EncryptNameAsync(storable.Name, Inner, cancellationToken);
+ var ciphertextName = await AbstractPathHelpers.EncryptNameAsync(storable.Name, Inner, specifics, cancellationToken);
var ciphertextItem = await Inner.GetFirstByNameAsync(ciphertextName, cancellationToken);
-
+
// Encrypt name
- var newCiphertextName = await EncryptNameAsync(newName, Inner, cancellationToken);
+ var newCiphertextName = await AbstractPathHelpers.EncryptNameAsync(newName, Inner, specifics, cancellationToken);
var renamedCiphertextItem = await renamableFolder.RenameAsync(ciphertextItem, newCiphertextName, cancellationToken);
var plaintextId = Path.Combine(Inner.Id, newName);
@@ -52,7 +56,7 @@ public async IAsyncEnumerable GetItemsAsync(StorableType type =
if (PathHelpers.IsCoreName(item.Name))
continue;
- var plaintextName = await DecryptNameAsync(item.Name, Inner, cancellationToken);
+ var plaintextName = await AbstractPathHelpers.DecryptNameAsync(item.Name, Inner, specifics, cancellationToken);
if (plaintextName is null)
continue;
@@ -68,7 +72,7 @@ public async IAsyncEnumerable GetItemsAsync(StorableType type =
///
public async Task GetFirstByNameAsync(string name, CancellationToken cancellationToken = default)
{
- var ciphertextName = await EncryptNameAsync(name, Inner, cancellationToken);
+ var ciphertextName = await AbstractPathHelpers.EncryptNameAsync(name, Inner, specifics, cancellationToken);
return await Inner.GetFirstByNameAsync(ciphertextName, cancellationToken) switch
{
IChildFile file => (IStorableChild)Wrap(file, name),
@@ -86,19 +90,37 @@ public Task GetFolderWatcherAsync(CancellationToken cancellation
///
public async Task DeleteAsync(IStorableChild item, CancellationToken cancellationToken = default)
+ {
+ await DeleteAsync(item, -1L, false, cancellationToken);
+ }
+
+ ///
+ public async Task DeleteAsync(IStorableChild item, long sizeHint, bool deleteImmediately = false,
+ CancellationToken cancellationToken = default)
{
if (Inner is not IModifiableFolder modifiableFolder)
throw new NotSupportedException("Modifying folder contents is not supported.");
- // TODO: Invalidate cache on success
// TODO: Get by ID instead of name
-
+
// We need to get the equivalent on the disk
- var ciphertextName = await EncryptNameAsync(item.Name, Inner, cancellationToken);
+ var ciphertextName = await AbstractPathHelpers.EncryptNameAsync(item.Name, Inner, specifics, cancellationToken);
var ciphertextItem = await Inner.GetFirstByNameAsync(ciphertextName, cancellationToken);
-
- // Delete the ciphertext item
- await modifiableFolder.DeleteAsync(ciphertextItem, cancellationToken);
+
+ if (deleteImmediately)
+ {
+ // Delete the ciphertext item
+ await modifiableFolder.DeleteAsync(item, cancellationToken);
+ }
+ else
+ {
+ // Delete or recycle the ciphertext item
+ await AbstractRecycleBinHelpers.DeleteOrRecycleAsync(modifiableFolder, ciphertextItem, specifics, StreamSerializer.Instance, sizeHint, cancellationToken);
+ }
+
+ // Remove deleted directory from cache
+ if (ciphertextItem is IFolder)
+ specifics.DirectoryIdCache.CacheRemove(Path.Combine(ciphertextItem.Id, Constants.Names.DIRECTORY_ID_FILENAME));
}
///
@@ -107,7 +129,7 @@ public async Task CreateFolderAsync(string name, bool overwrite =
if (Inner is not IModifiableFolder modifiableFolder)
throw new NotSupportedException("Modifying folder contents is not supported.");
- var encryptedName = await EncryptNameAsync(name, Inner, cancellationToken);
+ var encryptedName = await AbstractPathHelpers.EncryptNameAsync(name, Inner, specifics, cancellationToken);
var folder = await modifiableFolder.CreateFolderAsync(encryptedName, overwrite, cancellationToken);
if (folder is not IModifiableFolder createdModifiableFolder)
throw new ArgumentException("The created folder is not modifiable.");
@@ -132,7 +154,7 @@ public async Task CreateFileAsync(string name, bool overwrite = fals
if (Inner is not IModifiableFolder modifiableFolder)
throw new NotSupportedException("Modifying folder contents is not supported.");
- var encryptedName = await EncryptNameAsync(name, Inner, cancellationToken);
+ var encryptedName = await AbstractPathHelpers.EncryptNameAsync(name, Inner, specifics, cancellationToken);
var file = await modifiableFolder.CreateFileAsync(encryptedName, overwrite, cancellationToken);
return (IChildFile)Wrap(file, name);
@@ -145,8 +167,8 @@ public override async Task GetPropertiesAsync()
throw new NotSupportedException($"Properties on {nameof(CryptoFolder)}.{nameof(Inner)} are not supported.");
var innerProperties = await storableProperties.GetPropertiesAsync();
- properties ??= new CryptoFileProperties(specifics, innerProperties);
-
+ properties ??= new CryptoFolderProperties(innerProperties);
+
return properties;
}
}
diff --git a/src/SecureFolderFS.Core.FileSystem/Storage/CryptoStorable.cs b/src/SecureFolderFS.Core.FileSystem/Storage/CryptoStorable.cs
index 1bea084c8..afe432887 100644
--- a/src/SecureFolderFS.Core.FileSystem/Storage/CryptoStorable.cs
+++ b/src/SecureFolderFS.Core.FileSystem/Storage/CryptoStorable.cs
@@ -1,11 +1,11 @@
-using OwlCore.Storage;
-using SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract;
-using SecureFolderFS.Shared.ComponentModel;
-using SecureFolderFS.Storage.StorageProperties;
-using System;
+using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using OwlCore.Storage;
+using SecureFolderFS.Core.FileSystem.Helpers.Paths.Abstract;
+using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Storage.StorageProperties;
namespace SecureFolderFS.Core.FileSystem.Storage
{
@@ -60,7 +60,7 @@ protected CryptoStorable(string plaintextId, TCapability inner, FileSystemSpecif
// If the parent of parent is null, then we can assume we are at the root level and should use ContentFolder
var ciphertextParentOfParent = await ciphertextParentFolder.GetParentAsync(cancellationToken) ?? specifics.ContentFolder;
- var plaintextName = await DecryptNameAsync(ciphertextParent.Name, ciphertextParentOfParent, cancellationToken);
+ var plaintextName = await AbstractPathHelpers.DecryptNameAsync(ciphertextParent.Name, ciphertextParentOfParent, specifics, cancellationToken);
if (plaintextName is null)
return null;
@@ -87,41 +87,5 @@ protected virtual IWrapper Wrap(IFolder folder, params object[] objects
var plaintextId = Path.Combine(Id, plaintextName);
return new CryptoFolder(plaintextId, folder, specifics, this as CryptoFolder);
}
-
- ///
- /// Encrypts the provided .
- ///
- /// The name to encrypt.
- /// The ciphertext parent folder.
- /// A that cancels this action.
- /// A that represents the asynchronous operation. Value is an encrypted name.
- protected virtual async Task EncryptNameAsync(string plaintextName, IFolder parentFolder, CancellationToken cancellationToken = default)
- {
- if (specifics.Security.NameCrypt is null)
- return plaintextName;
-
- var directoryId = AbstractPathHelpers.AllocateDirectoryId(specifics.Security, plaintextName);
- var result = await AbstractPathHelpers.GetDirectoryIdAsync(parentFolder, specifics, directoryId, cancellationToken);
-
- return specifics.Security.NameCrypt.EncryptName(plaintextName, result ? directoryId : ReadOnlySpan.Empty) + FileSystem.Constants.Names.ENCRYPTED_FILE_EXTENSION;
- }
-
- ///
- /// Decrypts the provided .
- ///
- /// The name to decrypt.
- /// The ciphertext parent folder.
- /// A that cancels this action.
- /// A that represents the asynchronous operation. Value is a decrypted name.
- protected virtual async Task DecryptNameAsync(string ciphertextName, IFolder parentFolder, CancellationToken cancellationToken = default)
- {
- if (specifics.Security.NameCrypt is null)
- return ciphertextName;
-
- var directoryId = AbstractPathHelpers.AllocateDirectoryId(specifics.Security, ciphertextName);
- var result = await AbstractPathHelpers.GetDirectoryIdAsync(parentFolder, specifics, directoryId, cancellationToken);
-
- return specifics.Security.NameCrypt.DecryptName(Path.GetFileNameWithoutExtension(ciphertextName), result ? directoryId : ReadOnlySpan.Empty);
- }
}
}
diff --git a/src/SecureFolderFS.Core.FileSystem/Storage/StorageProperties/CryptoFileProperties.cs b/src/SecureFolderFS.Core.FileSystem/Storage/StorageProperties/CryptoFileProperties.cs
index df8c337e9..c14377abd 100644
--- a/src/SecureFolderFS.Core.FileSystem/Storage/StorageProperties/CryptoFileProperties.cs
+++ b/src/SecureFolderFS.Core.FileSystem/Storage/StorageProperties/CryptoFileProperties.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
@@ -8,7 +9,7 @@
namespace SecureFolderFS.Core.FileSystem.Storage.StorageProperties
{
///
- public class CryptoFileProperties : ISizeProperties, IBasicProperties
+ public class CryptoFileProperties : ISizeProperties, IDateProperties, IBasicProperties
{
private readonly FileSystemSpecifics _specifics;
private readonly IBasicProperties _properties;
@@ -28,15 +29,40 @@ public CryptoFileProperties(FileSystemSpecifics specifics, IBasicProperties prop
var sizeProperty = await sizeProperties.GetSizeAsync(cancellationToken);
if (sizeProperty is null)
return null;
-
+
var plaintextSize = _specifics.Security.ContentCrypt.CalculatePlaintextSize(sizeProperty.Value);
return new GenericProperty(plaintextSize);
}
-
+
+ ///
+ public async Task> GetDateCreatedAsync(CancellationToken cancellationToken = default)
+ {
+ if (_properties is not IDateProperties dateProperties)
+ throw new NotSupportedException($"{nameof(IDateProperties)} is not supported.");
+
+ return await dateProperties.GetDateCreatedAsync(cancellationToken);
+ }
+
+ ///
+ public async Task> GetDateModifiedAsync(CancellationToken cancellationToken = default)
+ {
+ if (_properties is not IDateProperties dateProperties)
+ throw new NotSupportedException($"{nameof(IDateProperties)} is not supported.");
+
+ return await dateProperties.GetDateModifiedAsync(cancellationToken);
+ }
+
///
public async IAsyncEnumerable> GetPropertiesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- yield return await GetSizeAsync(cancellationToken) as IStorageProperty;
+ if (_properties is ISizeProperties)
+ yield return await GetSizeAsync(cancellationToken) as IStorageProperty;
+
+ if (_properties is IDateProperties)
+ {
+ yield return await GetDateCreatedAsync(cancellationToken) as IStorageProperty;
+ yield return await GetDateModifiedAsync(cancellationToken) as IStorageProperty;
+ }
}
}
}
diff --git a/src/SecureFolderFS.Core.FileSystem/Storage/StorageProperties/CryptoFolderProperties.cs b/src/SecureFolderFS.Core.FileSystem/Storage/StorageProperties/CryptoFolderProperties.cs
index 6fddeee1b..871ccaf52 100644
--- a/src/SecureFolderFS.Core.FileSystem/Storage/StorageProperties/CryptoFolderProperties.cs
+++ b/src/SecureFolderFS.Core.FileSystem/Storage/StorageProperties/CryptoFolderProperties.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
@@ -8,35 +9,41 @@
namespace SecureFolderFS.Core.FileSystem.Storage.StorageProperties
{
///
- public class CryptoFolderProperties : ISizeProperties, IBasicProperties
+ public class CryptoFolderProperties : IDateProperties, IBasicProperties
{
- private readonly FileSystemSpecifics _specifics;
private readonly IBasicProperties _properties;
- public CryptoFolderProperties(FileSystemSpecifics specifics, IBasicProperties properties)
+ public CryptoFolderProperties(IBasicProperties properties)
{
- _specifics = specifics;
_properties = properties;
}
///
- public async Task?> GetSizeAsync(CancellationToken cancellationToken = default)
+ public async Task> GetDateCreatedAsync(CancellationToken cancellationToken = default)
{
- if (_properties is not ISizeProperties sizeProperties)
- return null;
+ if (_properties is not IDateProperties dateProperties)
+ throw new NotSupportedException($"{nameof(IDateProperties)} is not supported.");
- var sizeProperty = await sizeProperties.GetSizeAsync(cancellationToken);
- if (sizeProperty is null)
- return null;
-
- var plaintextSize = _specifics.Security.ContentCrypt.CalculatePlaintextSize(sizeProperty.Value);
- return new GenericProperty(plaintextSize);
+ return await dateProperties.GetDateCreatedAsync(cancellationToken);
}
-
+
+ ///
+ public async Task> GetDateModifiedAsync(CancellationToken cancellationToken = default)
+ {
+ if (_properties is not IDateProperties dateProperties)
+ throw new NotSupportedException($"{nameof(IDateProperties)} is not supported.");
+
+ return await dateProperties.GetDateModifiedAsync(cancellationToken);
+ }
+
///
public async IAsyncEnumerable> GetPropertiesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- yield return await GetSizeAsync(cancellationToken) as IStorageProperty;
+ if (_properties is IDateProperties)
+ {
+ yield return await GetDateCreatedAsync(cancellationToken) as IStorageProperty;
+ yield return await GetDateModifiedAsync(cancellationToken) as IStorageProperty;
+ }
}
}
}
diff --git a/src/SecureFolderFS.Core.FileSystem/Streams/PlaintextStream.cs b/src/SecureFolderFS.Core.FileSystem/Streams/PlaintextStream.cs
index a689ffe76..3972dcaa7 100644
--- a/src/SecureFolderFS.Core.FileSystem/Streams/PlaintextStream.cs
+++ b/src/SecureFolderFS.Core.FileSystem/Streams/PlaintextStream.cs
@@ -1,12 +1,13 @@
using SecureFolderFS.Core.Cryptography;
using SecureFolderFS.Core.FileSystem.Buffers;
using SecureFolderFS.Core.FileSystem.Chunks;
-using SecureFolderFS.Core.FileSystem.Exceptions;
using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Storage.VirtualFileSystem;
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
+using System.Threading;
namespace SecureFolderFS.Core.FileSystem.Streams
{
@@ -17,7 +18,7 @@ internal sealed class PlaintextStream : Stream, IWrapper
private readonly ChunkAccess _chunkAccess;
private readonly HeaderBuffer _headerBuffer;
private readonly Action _notifyStreamClosed;
- private readonly object _writeLock = new();
+ private readonly Lock _writeLock = new();
private long _Length;
private long _Position;
@@ -56,7 +57,7 @@ public PlaintextStream(
_chunkAccess = chunkAccess;
_headerBuffer = headerBuffer;
_notifyStreamClosed = notifyStreamClosed;
-
+
if (CanSeek)
_Length = _security.ContentCrypt.CalculatePlaintextSize(Math.Max(0L, ciphertextStream.Length - _security.HeaderCrypt.HeaderCiphertextSize));
}
@@ -215,7 +216,7 @@ public override long Seek(long offset, SeekOrigin origin)
{
if (!CanSeek)
throw FileSystemExceptions.StreamNotSeekable;
-
+
var seekPosition = origin switch
{
SeekOrigin.Begin => offset,
@@ -336,7 +337,7 @@ private bool TryWriteHeader()
// Check if there is data already written only when we can seek
if (CanSeek && Inner.Length > 0L)
return false;
-
+
// Make sure we save the header state
_headerBuffer.IsHeaderReady = true;
diff --git a/src/SecureFolderFS.Core.FileSystem/UniversalCache.cs b/src/SecureFolderFS.Core.FileSystem/UniversalCache.cs
index eb56d442b..0b60b3d43 100644
--- a/src/SecureFolderFS.Core.FileSystem/UniversalCache.cs
+++ b/src/SecureFolderFS.Core.FileSystem/UniversalCache.cs
@@ -25,7 +25,7 @@ public UniversalCache(int capacity, IProgress? cacheStatistics)
{
this.cache = capacity < 0 ? new() : new(capacity);
this.cacheStatistics = cacheStatistics;
- IsAvailable = capacity > 0;
+ IsAvailable = capacity < 0 || capacity > 0;
}
///
diff --git a/src/SecureFolderFS.Core.FileSystem/VFSRoot.cs b/src/SecureFolderFS.Core.FileSystem/VFSRoot.cs
index 6bb1b013f..4df5517f2 100644
--- a/src/SecureFolderFS.Core.FileSystem/VFSRoot.cs
+++ b/src/SecureFolderFS.Core.FileSystem/VFSRoot.cs
@@ -8,14 +8,13 @@ namespace SecureFolderFS.Core.FileSystem
///
public abstract class VFSRoot : IVFSRoot, IWrapper
{
- protected readonly IFolder storageRoot;
protected readonly FileSystemSpecifics specifics;
///
- IFolder IWrapper.Inner => storageRoot;
+ FileSystemSpecifics IWrapper.Inner => specifics;
///
- FileSystemSpecifics IWrapper.Inner => specifics;
+ public IFolder VirtualizedRoot { get; }
///
public abstract string FileSystemName { get; }
@@ -25,8 +24,8 @@ public abstract class VFSRoot : IVFSRoot, IWrapper
protected VFSRoot(IFolder storageRoot, FileSystemSpecifics specifics)
{
- this.storageRoot = storageRoot;
this.specifics = specifics;
+ VirtualizedRoot = storageRoot;
Options = specifics.Options;
// Automatically add created root
diff --git a/src/SecureFolderFS.Core.FileSystem/Validators/StructureValidator.cs b/src/SecureFolderFS.Core.FileSystem/Validators/StructureValidator.cs
index b07e866e5..ec6af19d0 100644
--- a/src/SecureFolderFS.Core.FileSystem/Validators/StructureValidator.cs
+++ b/src/SecureFolderFS.Core.FileSystem/Validators/StructureValidator.cs
@@ -1,14 +1,14 @@
-using OwlCore.Storage;
-using SecureFolderFS.Shared.ComponentModel;
-using SecureFolderFS.Shared.Models;
-using System;
+using System;
using System.Threading;
using System.Threading.Tasks;
+using OwlCore.Storage;
using SecureFolderFS.Core.FileSystem.Helpers.Paths;
+using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Shared.Models;
namespace SecureFolderFS.Core.FileSystem.Validators
{
- ///
+ ///
internal sealed class StructureValidator : BaseFileSystemValidator<(IFolder, IProgress?)>
{
private readonly IAsyncValidator _fileValidator;
diff --git a/src/SecureFolderFS.Core.Migration/AppModels/EncAndMacKey.cs b/src/SecureFolderFS.Core.Migration/AppModels/EncAndMacKey.cs
deleted file mode 100644
index bd235b855..000000000
--- a/src/SecureFolderFS.Core.Migration/AppModels/EncAndMacKey.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using SecureFolderFS.Core.Cryptography.SecureStore;
-using System;
-
-namespace SecureFolderFS.Core.Migration.AppModels
-{
- internal sealed record class EncAndMacKey(SecretKey EncKey, SecretKey MacKey) : IDisposable
- {
- ///
- public void Dispose()
- {
- EncKey.Dispose();
- MacKey.Dispose();
- }
- }
-}
diff --git a/src/SecureFolderFS.Core.Migration/AppModels/MigratorV1_V2.cs b/src/SecureFolderFS.Core.Migration/AppModels/MigratorV1_V2.cs
index 866e3c093..27a60b8e2 100644
--- a/src/SecureFolderFS.Core.Migration/AppModels/MigratorV1_V2.cs
+++ b/src/SecureFolderFS.Core.Migration/AppModels/MigratorV1_V2.cs
@@ -56,7 +56,7 @@ public async Task UnlockAsync(T credentials, CancellationToken c
using var encKey = new SecureKey(Cryptography.Constants.KeyTraits.ENCKEY_LENGTH);
using var macKey = new SecureKey(Cryptography.Constants.KeyTraits.MACKEY_LENGTH);
- Argon2id.DeriveKey(password.ToArray(), _v1KeystoreDataModel.Salt, kek);
+ Argon2id.Old_DeriveKey(password.ToArray(), _v1KeystoreDataModel.Salt, kek);
// Unwrap keys
using var rfc3394 = new Rfc3394KeyWrap();
@@ -79,6 +79,9 @@ public async Task MigrateAsync(IDisposable unlockContract, ProgressModel
+ public void Dispose()
+ {
+ }
}
}
diff --git a/src/SecureFolderFS.Core.Migration/AppModels/MigratorV2_V3.cs b/src/SecureFolderFS.Core.Migration/AppModels/MigratorV2_V3.cs
index 2088cde41..ee97806e0 100644
--- a/src/SecureFolderFS.Core.Migration/AppModels/MigratorV2_V3.cs
+++ b/src/SecureFolderFS.Core.Migration/AppModels/MigratorV2_V3.cs
@@ -25,6 +25,7 @@ internal sealed class MigratorV2_V3 : IVaultMigratorModel
private readonly IAsyncSerializer _streamSerializer;
private V2VaultConfigurationDataModel? _v2ConfigDataModel;
private VaultKeystoreDataModel? _v2KeystoreDataModel;
+ private SecretKey? _secretKeySequence;
///
public IFolder VaultFolder { get; }
@@ -38,8 +39,11 @@ public MigratorV2_V3(IFolder vaultFolder, IAsyncSerializer streamSeriali
///
public async Task UnlockAsync(T credentials, CancellationToken cancellationToken = default)
{
- if (credentials is not KeyChain keyChain)
- throw new ArgumentException($"Argument {credentials} is not of type {typeof(KeyChain)}.");
+ if (credentials is not KeySequence keySequence)
+ throw new ArgumentException($"Argument {credentials} is not of type {typeof(KeySequence)}.");
+
+ _secretKeySequence?.Dispose();
+ _secretKeySequence = SecureKey.TakeOwnership(keySequence.ToArray());
var configFile = await VaultFolder.GetFileByNameAsync(Constants.Vault.Names.VAULT_CONFIGURATION_FILENAME, cancellationToken);
var keystoreFile = await VaultFolder.GetFileByNameAsync(Constants.Vault.Names.VAULT_KEYSTORE_FILENAME, cancellationToken);
@@ -56,7 +60,7 @@ public async Task UnlockAsync(T credentials, CancellationToken c
using var encKey = new SecureKey(Cryptography.Constants.KeyTraits.ENCKEY_LENGTH);
using var macKey = new SecureKey(Cryptography.Constants.KeyTraits.MACKEY_LENGTH);
- Argon2id.DeriveKey(keyChain.ToArray(), _v2KeystoreDataModel.Salt, kek);
+ Argon2id.Old_DeriveKey(_secretKeySequence, _v2KeystoreDataModel.Salt, kek);
// Unwrap keys
using var rfc3394 = new Rfc3394KeyWrap();
@@ -64,37 +68,38 @@ public async Task UnlockAsync(T credentials, CancellationToken c
rfc3394.UnwrapKey(_v2KeystoreDataModel.WrappedMacKey, kek, macKey.Key);
// Create copies of keys for later use
- return new EncAndMacKey(encKey.CreateCopy(), macKey.CreateCopy());
+ return KeyPair.ImportKeys(encKey, macKey);
}
///
public async Task MigrateAsync(IDisposable unlockContract, ProgressModel progress, CancellationToken cancellationToken = default)
{
- if (_v2ConfigDataModel is null)
- throw new InvalidOperationException($"{nameof(_v2ConfigDataModel)} is null.");
+ _ = _v2ConfigDataModel ?? throw new InvalidOperationException($"{nameof(_v2ConfigDataModel)} is null.");
+ _ = _v2KeystoreDataModel ?? throw new InvalidOperationException($"{nameof(_v2KeystoreDataModel)} is null.");
+ _ = _secretKeySequence ?? throw new InvalidOperationException($"{nameof(_secretKeySequence)} is null.");
- if (unlockContract is not EncAndMacKey encAndMacKey)
+ if (unlockContract is not KeyPair keyPair)
throw new ArgumentException($"{nameof(unlockContract)} is not of correct type.");
// Begin progress report
progress.PercentageProgress?.Report(0d);
- var vaultId = Guid.NewGuid().ToString();
+ // Vault Configuration ------------------------------------
+ //
+ var encKey = keyPair.DekKey;
+ var macKey = keyPair.MacKey;
var v3ConfigDataModel = new VaultConfigurationDataModel()
{
AuthenticationMethod = _v2ConfigDataModel.AuthenticationMethod,
ContentCipherId = _v2ConfigDataModel.ContentCipherId,
FileNameCipherId = _v2ConfigDataModel.FileNameCipherId,
+ FileNameEncodingId = Core.Cryptography.Constants.CipherId.ENCODING_BASE64URL,
+ RecycleBinSize = 0L,
PayloadMac = new byte[HMACSHA256.HashSizeInBytes],
Uid = _v2ConfigDataModel.Uid,
Version = Constants.Vault.Versions.V3
};
- // Calculate and update configuration MAC
-
- var encKey = encAndMacKey.EncKey;
- var macKey = encAndMacKey.MacKey;
-
// Initialize HMAC
using var hmacSha256 = new HMACSHA256(macKey.Key);
@@ -102,6 +107,7 @@ public async Task MigrateAsync(IDisposable unlockContract, ProgressModel
+ public void Dispose()
+ {
+ _secretKeySequence?.Dispose();
+ }
}
}
diff --git a/src/SecureFolderFS.Core.Migration/Helpers/BackupHelpers.cs b/src/SecureFolderFS.Core.Migration/Helpers/BackupHelpers.cs
index a6d588c9b..86e2b7744 100644
--- a/src/SecureFolderFS.Core.Migration/Helpers/BackupHelpers.cs
+++ b/src/SecureFolderFS.Core.Migration/Helpers/BackupHelpers.cs
@@ -8,23 +8,23 @@ namespace SecureFolderFS.Core.Migration.Helpers
{
internal static class BackupHelpers
{
- public static async Task CreateConfigBackup(IModifiableFolder vaultFolder, Stream configStream, CancellationToken cancellationToken)
+ public static async Task CreateBackup(IModifiableFolder folder, string originalName, int version, Stream stream, CancellationToken cancellationToken)
{
- var backupConfigName = $"{Constants.Vault.Names.VAULT_CONFIGURATION_FILENAME}.bkup";
- var backupConfigFile = await vaultFolder.CreateFileAsync(backupConfigName, true, cancellationToken);
- await using var backupConfigStream = await backupConfigFile.OpenWriteAsync(cancellationToken);
+ var backupName = $"{originalName}_{version}.bkup";
+ var backupFile = await folder.CreateFileAsync(backupName, true, cancellationToken);
+ await using var backupStream = await backupFile.OpenWriteAsync(cancellationToken);
- await configStream.CopyToAsync(backupConfigStream, cancellationToken);
- configStream.Position = 0L;
+ await stream.CopyToAsync(backupStream, cancellationToken);
+ stream.Position = 0L;
}
- public static async Task CreateKeystoreBackup(IModifiableFolder vaultFolder, CancellationToken cancellationToken)
+ public static async Task CreateBackup(IModifiableFolder folder, string originalName, int version, CancellationToken cancellationToken)
{
- var keystoreFile = await vaultFolder.GetFileByNameAsync(Constants.Vault.Names.VAULT_KEYSTORE_FILENAME, cancellationToken);
- var backupKeystoreName = $"{Constants.Vault.Names.VAULT_KEYSTORE_FILENAME}.bkup";
- var backupKeystoreFile = await vaultFolder.CreateFileAsync(backupKeystoreName, true, cancellationToken);
+ var backupName = $"{originalName}_{version}.bkup";
+ var backupFile = await folder.CreateFileAsync(backupName, true, cancellationToken);
+ var originalFile = await folder.GetFileByNameAsync(originalName, cancellationToken);
- await keystoreFile.CopyContentsToAsync(backupKeystoreFile, cancellationToken);
+ await originalFile.CopyContentsToAsync(backupFile, cancellationToken);
}
}
}
diff --git a/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/FileSystemProvider.Helpers.cs b/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/FileSystemProvider.Helpers.cs
index cbb49ce16..e3c37ee3e 100644
--- a/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/FileSystemProvider.Helpers.cs
+++ b/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/FileSystemProvider.Helpers.cs
@@ -17,7 +17,7 @@ private bool AddRoot(MatrixCursor matrix, SafRoot safRoot, int iconRid)
if (row is null)
return false;
- var rootFolderId = GetDocumentIdForStorable(safRoot.StorageRoot.Inner, safRoot.RootId);
+ var rootFolderId = GetDocumentIdForStorable(safRoot.StorageRoot.VirtualizedRoot, safRoot.RootId);
row.Add(DocumentsContract.Root.ColumnRootId, safRoot.RootId);
row.Add(DocumentsContract.Root.ColumnDocumentId, rootFolderId);
row.Add(DocumentsContract.Root.ColumnTitle, safRoot.StorageRoot.Options.VolumeName);
@@ -26,7 +26,7 @@ private bool AddRoot(MatrixCursor matrix, SafRoot safRoot, int iconRid)
return true;
}
-
+
private async Task AddDocumentAsync(MatrixCursor matrix, IStorable storable, string? documentId)
{
documentId ??= GetDocumentIdForStorable(storable, null);
@@ -37,38 +37,25 @@ private async Task AddDocumentAsync(MatrixCursor matrix, IStorable storabl
var safRoot = _rootCollection?.GetSafRootForStorable(storable);
if (safRoot is null)
return false;
-
+
AddDocumentId();
AddDisplayName();
await AddSizeAsync();
AddMimeType();
AddFlags();
-
+
return true;
async Task AddSizeAsync()
{
- if (storable is not IStorableProperties storableProperties)
+ if (storable is not IFile file)
{
row.Add(Document.ColumnSize, 0);
return;
}
- var basicProperties = await storableProperties.TryGetPropertiesAsync();
- if (basicProperties is not ISizeProperties sizeProperties)
- {
- row.Add(Document.ColumnSize, 0);
- return;
- }
-
- var sizeProperty = await sizeProperties.GetSizeAsync();
- if (sizeProperty is null)
- {
- row.Add(Document.ColumnSize, 0);
- return;
- }
-
- row.Add(Document.ColumnSize, sizeProperty.Value);
+ var size = await file.GetSizeAsync();
+ row.Add(Document.ColumnSize, size);
}
void AddFlags()
{
@@ -81,12 +68,12 @@ void AddFlags()
| DocumentContractFlags.SupportsDelete
| DocumentContractFlags.SupportsRemove;
}
-
+
if (storable is IFile)
{
if (!safRoot.StorageRoot.Options.IsReadOnly)
baseFlags |= DocumentContractFlags.SupportsWrite;
-
+
row.Add(Document.ColumnFlags, (int)baseFlags);
}
else
@@ -94,7 +81,7 @@ void AddFlags()
baseFlags |= DocumentContractFlags.DirPrefersGrid;
if (!safRoot.StorageRoot.Options.IsReadOnly)
baseFlags |= DocumentContractFlags.DirSupportsCreate;
-
+
row.Add(Document.ColumnFlags, (int)baseFlags);
}
}
@@ -121,7 +108,7 @@ void AddDisplayName()
if (safRoot is null)
return null;
- if (storable.Id == safRoot.StorageRoot.Inner.Id)
+ if (storable.Id == safRoot.StorageRoot.VirtualizedRoot.Id)
return $"{safRoot.RootId}:";
return $"{safRoot.RootId}:{storable.Id}";
@@ -142,7 +129,7 @@ void AddDisplayName()
// Extract RootID and Path
var rootId = split[0];
var path = split[1];
-
+
// Get root
var safRoot = _rootCollection.GetSafRootForRootId(rootId);
if (safRoot is null)
@@ -150,9 +137,9 @@ void AddDisplayName()
// Return base folder if the path is empty
if (string.IsNullOrEmpty(path))
- return safRoot.StorageRoot.Inner;
+ return safRoot.StorageRoot.VirtualizedRoot;
- return safRoot.StorageRoot.Inner.GetItemByRelativePathAsync(path).ConfigureAwait(false).GetAwaiter().GetResult();
+ return safRoot.StorageRoot.VirtualizedRoot.GetItemByRelativePathAsync(path).ConfigureAwait(false).GetAwaiter().GetResult();
}
private string GetMimeForStorable(IStorable storable)
diff --git a/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/FileSystemProvider.Main.cs b/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/FileSystemProvider.Main.cs
index fcd8b020b..2b7d70ee3 100644
--- a/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/FileSystemProvider.Main.cs
+++ b/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/FileSystemProvider.Main.cs
@@ -2,7 +2,6 @@
using Android.Content;
using Android.Database;
using Android.OS;
-using Android.OS.Storage;
using Android.Provider;
using OwlCore.Storage;
using SecureFolderFS.Shared.ComponentModel;
@@ -87,7 +86,7 @@ public override bool OnCreate()
var file = GetStorableForDocumentId(documentId);
if (file is not IChildFile childFile)
return null;
-
+
var safRoot = _rootCollection?.GetSafRootForStorable(file);
if (safRoot is null)
return null;
@@ -95,7 +94,7 @@ public override bool OnCreate()
var fileAccess = ToFileAccess(mode);
if (safRoot.StorageRoot.Options.IsReadOnly && fileAccess.HasFlag(FileAccess.Write))
return null;
-
+
var stream = childFile.TryOpenStreamAsync(fileAccess).ConfigureAwait(false).GetAwaiter().GetResult();
if (stream is null)
return null;
@@ -103,9 +102,9 @@ public override bool OnCreate()
var parcelFileMode = ToParcelFileMode(mode);
if (safRoot.StorageRoot.Options.IsReadOnly && parcelFileMode is ParcelFileMode.WriteOnly or ParcelFileMode.ReadWrite)
return null;
-
+
return _storageManager.OpenProxyFileDescriptor(parcelFileMode, new ReadWriteCallbacks(stream), new Handler(Looper.MainLooper));
-
+
// var storageManager = (StorageManager?)this.Context?.GetSystemService(Context.StorageService);
// if (storageManager is null)
// return null;
@@ -120,10 +119,10 @@ static ParcelFileMode ToParcelFileMode(string? fileMode)
"r" => ParcelFileMode.ReadOnly,
"w" => ParcelFileMode.WriteOnly,
"rw" => ParcelFileMode.ReadWrite,
- _ => throw new ArgumentException($"Unsupported mode: {fileMode}")
+ _ => throw new ArgumentException($"Unsupported mode: {fileMode}.")
};
}
-
+
static FileAccess ToFileAccess(string? fileMode)
{
return fileMode switch
@@ -131,7 +130,7 @@ static FileAccess ToFileAccess(string? fileMode)
"r" => FileAccess.Read,
"w" => FileAccess.Write,
"rw" => FileAccess.ReadWrite,
- _ => throw new ArgumentException($"Unsupported mode: {fileMode}")
+ _ => throw new ArgumentException($"Unsupported mode: {fileMode}.")
};
}
}
@@ -185,15 +184,15 @@ public override void DeleteDocument(string? documentId)
documentId = documentId == "null" ? null : documentId;
if (documentId is null)
return;
-
+
var storable = GetStorableForDocumentId(documentId);
if (storable is not IStorableChild storableChild)
return;
-
+
var safRoot = _rootCollection?.GetSafRootForStorable(storable);
if (safRoot is null)
return;
-
+
if (safRoot.StorageRoot.Options.IsReadOnly)
return;
@@ -203,7 +202,7 @@ public override void DeleteDocument(string? documentId)
// Revoke permissions first
RevokeDocumentPermission(documentId);
-
+
// Perform deletion
modifiableFolder.DeleteAsync(storableChild).ConfigureAwait(false).GetAwaiter().GetResult();
}
@@ -220,33 +219,33 @@ public override void DeleteDocument(string? documentId)
var destinationStorable = GetStorableForDocumentId(targetParentDocumentId);
if (destinationStorable is not IModifiableFolder destinationFolder)
return null;
-
+
var safRoot = _rootCollection?.GetSafRootForStorable(destinationStorable);
if (safRoot is null)
return null;
-
+
if (safRoot.StorageRoot.Options.IsReadOnly)
return null;
var sourceParentStorable = GetStorableForDocumentId(sourceParentDocumentId);
if (sourceParentStorable is not IModifiableFolder sourceParentFolder)
return null;
-
+
var sourceStorable = GetStorableForDocumentId(sourceDocumentId);
switch (sourceStorable)
{
case IChildFile file:
{
- var movedFile = destinationFolder.MoveFromAsync(file, sourceParentFolder, false).ConfigureAwait(false).GetAwaiter().GetResult();
+ var movedFile = destinationFolder.MoveFromAsync(file, sourceParentFolder, false, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
return Path.Combine(targetParentDocumentId, movedFile.Name);
}
-
+
case IChildFolder folder:
{
- var movedFolder = destinationFolder.MoveFromAsync(folder, sourceParentFolder, false).ConfigureAwait(false).GetAwaiter().GetResult();
+ var movedFolder = destinationFolder.MoveFromAsync(folder, sourceParentFolder, false, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
return Path.Combine(targetParentDocumentId, movedFolder.Name);
}
-
+
default: return null;
}
}
@@ -256,30 +255,33 @@ public override void DeleteDocument(string? documentId)
{
if (string.IsNullOrWhiteSpace(displayName))
return null;
-
+
documentId = documentId == "null" ? null : documentId;
if (documentId is null)
return null;
-
+
var storable = GetStorableForDocumentId(documentId);
if (storable is not IStorableChild storableChild)
return null;
-
+
var safRoot = _rootCollection?.GetSafRootForStorable(storable);
if (safRoot is null)
return null;
-
+
if (safRoot.StorageRoot.Options.IsReadOnly)
return null;
-
+
var parentFolder = storableChild.GetParentAsync().ConfigureAwait(false).GetAwaiter().GetResult();
if (parentFolder is not IRenamableFolder renamableFolder)
return null;
-
+
var renamedItem = renamableFolder.RenameAsync(storableChild, displayName).ConfigureAwait(false).GetAwaiter().GetResult();
- if (renamedItem is IWrapper { Inner: IWrapper uriWrapper })
- return uriWrapper.Inner.ToString();
-
+ if (renamedItem is IWrapper { Inner: IWrapper fileUriWrapper })
+ return fileUriWrapper.Inner.ToString();
+
+ if (renamedItem is IWrapper { Inner: IWrapper folderUriWrapper })
+ return folderUriWrapper.Inner.ToString();
+
throw new InvalidOperationException($"{nameof(renamedItem)} does not implement {nameof(IWrapper)}.");
}
@@ -294,14 +296,14 @@ public override void DeleteDocument(string? documentId)
var destinationStorable = GetStorableForDocumentId(targetParentDocumentId);
if (destinationStorable is not IModifiableFolder destinationFolder)
return null;
-
+
var safRoot = _rootCollection?.GetSafRootForStorable(destinationStorable);
if (safRoot is null)
return null;
-
+
if (safRoot.StorageRoot.Options.IsReadOnly)
return null;
-
+
var sourceStorable = GetStorableForDocumentId(sourceDocumentId);
switch (sourceStorable)
{
@@ -310,13 +312,13 @@ public override void DeleteDocument(string? documentId)
var copiedFile = destinationFolder.CreateCopyOfAsync(file, false).ConfigureAwait(false).GetAwaiter().GetResult();
return Path.Combine(targetParentDocumentId, copiedFile.Name);
}
-
+
case IFolder folder:
{
var copiedFolder = destinationFolder.CreateCopyOfAsync(folder, false).ConfigureAwait(false).GetAwaiter().GetResult();
return Path.Combine(targetParentDocumentId, copiedFolder.Name);
}
-
+
default: return null;
}
}
diff --git a/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/ReadWriteCallbacks.cs b/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/ReadWriteCallbacks.cs
index d636b6ff4..a720dcd58 100644
--- a/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/ReadWriteCallbacks.cs
+++ b/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/ReadWriteCallbacks.cs
@@ -25,11 +25,11 @@ public override int OnRead(long offset, int size, byte[]? data)
{
if (data is null)
return 0;
-
+
// Seek to the requested offset
if (offset > 0)
_stream.Seek(offset, SeekOrigin.Begin);
-
+
// Read the requested data
return _stream.Read(data.AsSpan(0, size));
}
@@ -40,7 +40,7 @@ public override int OnRead(long offset, int size, byte[]? data)
//throw new ErrnoException(nameof(OnRead), OsConstants.Eio);
}
}
-
+
///
public override int OnWrite(long offset, int size, byte[]? data)
{
@@ -48,11 +48,11 @@ public override int OnWrite(long offset, int size, byte[]? data)
{
if (data is null)
return 0;
-
+
// Seek to the requested offset
if (offset > 0)
_stream.Seek(offset, SeekOrigin.Begin);
-
+
// Write the requested data
_stream.Write(data.AsSpan(0, size));
diff --git a/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/RootCollection.cs b/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/RootCollection.cs
index dfd44001f..33c58435f 100644
--- a/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/RootCollection.cs
+++ b/src/SecureFolderFS.Core.MobileFS/Platforms/Android/FileSystem/RootCollection.cs
@@ -30,7 +30,7 @@ public RootCollection(Context context)
{
foreach (var safRoot in Roots)
{
- if (storable.Id.StartsWith(safRoot.StorageRoot.Inner.Id))
+ if (storable.Id.StartsWith(safRoot.StorageRoot.VirtualizedRoot.Id))
return safRoot;
}
diff --git a/src/SecureFolderFS.Core.MobileFS/Platforms/Android/Streams/InputOutputStream.cs b/src/SecureFolderFS.Core.MobileFS/Platforms/Android/Streams/InputOutputStream.cs
index c619070dc..ded3e86f0 100644
--- a/src/SecureFolderFS.Core.MobileFS/Platforms/Android/Streams/InputOutputStream.cs
+++ b/src/SecureFolderFS.Core.MobileFS/Platforms/Android/Streams/InputOutputStream.cs
@@ -5,20 +5,20 @@ namespace SecureFolderFS.Core.MobileFS.Platforms.Android.Streams
public class InputOutputStream : Stream
{
private readonly InputStream _inputStream;
- private readonly OutputStream _outputStream;
+ private readonly OutputStream? _outputStream;
private bool _disposed;
private long _position;
private long _length;
-
+
///
public override bool CanRead => true;
-
+
///
public override bool CanSeek => true;
-
+
///
- public override bool CanWrite => true;
-
+ public override bool CanWrite => _outputStream is not null;
+
///
public override long Length => _length;
@@ -31,29 +31,29 @@ public override long Position
// if (value < 0 || (value > 0 && _length != -1 && value > _length))
// throw new ArgumentOutOfRangeException(nameof(value));
- if (value < 0)
- throw new ArgumentOutOfRangeException(nameof(value));
-
- if (value != _position)
+ ArgumentOutOfRangeException.ThrowIfNegative(value);
+ if (value == _position)
+ return;
+
+ // Reset input stream and skip to the desired position
+ _inputStream.Reset(); // Resets to the beginning of the stream
+
+ // Skip to the desired position
+ var skipAmount = value;
+ while (skipAmount > 0)
{
- // Reset input stream and skip to the desired position
- _inputStream.Reset(); // Resets to the beginning of the stream
-
- // Skip to the desired position
- var skipAmount = value;
- while (skipAmount > 0)
- {
- var skipped = _inputStream.Skip(skipAmount);
- if (skipped <= 0) break;
- skipAmount -= skipped;
- }
-
- _position = value;
+ var skipped = _inputStream.Skip(skipAmount);
+ if (skipped <= 0)
+ break;
+
+ skipAmount -= skipped;
}
+
+ _position = value;
}
}
- public InputOutputStream(InputStream inputStream, OutputStream outputStream, long length)
+ public InputOutputStream(InputStream inputStream, OutputStream? outputStream, long length)
{
_inputStream = inputStream;
_outputStream = outputStream;
@@ -66,11 +66,10 @@ public override int Read(byte[] buffer, int offset, int count)
{
// Adjust count if it would read past the end of the stream
if (_length != -1)
- {
count = (int)Math.Min(count, _length - _position);
- }
- if (count <= 0) return 0;
+ if (count <= 0)
+ return 0;
int bytesRead;
if (offset != 0)
@@ -79,19 +78,13 @@ public override int Read(byte[] buffer, int offset, int count)
var tempBuffer = new byte[count];
bytesRead = _inputStream.Read(tempBuffer);
if (bytesRead > 0)
- {
Array.Copy(tempBuffer, 0, buffer, offset, bytesRead);
- }
}
else
- {
bytesRead = _inputStream.Read(buffer);
- }
if (bytesRead > 0)
- {
_position += bytesRead;
- }
return bytesRead;
}
@@ -99,6 +92,9 @@ public override int Read(byte[] buffer, int offset, int count)
///
public override void Write(byte[] buffer, int offset, int count)
{
+ if (_outputStream is null || !CanWrite)
+ throw new NotSupportedException("The stream does not support writing.");
+
if (offset != 0)
{
// Create a temporary buffer if offset is not 0
@@ -107,9 +103,7 @@ public override void Write(byte[] buffer, int offset, int count)
_outputStream.Write(tempBuffer, 0, count);
}
else
- {
_outputStream.Write(buffer, 0, count);
- }
// Update position after writing
_position += count;
@@ -124,17 +118,20 @@ public override void Write(byte[] buffer, int offset, int count)
_length = _position;
}
}
-
+
///
public override void Flush()
{
+ if (_outputStream is null || !CanWrite)
+ throw new NotSupportedException("The stream does not support writing.");
+
_outputStream.Flush();
}
-
+
///
public override long Seek(long offset, SeekOrigin origin)
{
- long newPosition = origin switch
+ var newPosition = origin switch
{
SeekOrigin.Begin => offset,
SeekOrigin.Current => _position + offset,
@@ -145,12 +142,12 @@ public override long Seek(long offset, SeekOrigin origin)
Position = newPosition;
return _position;
}
-
+
///
public override void SetLength(long value)
{
- throw new NotSupportedException("Cannot modify file length on this stream");
- }
+ throw new NotSupportedException("Cannot modify length on this stream.");
+ }
///
protected override void Dispose(bool disposing)
@@ -160,7 +157,7 @@ protected override void Dispose(bool disposing)
if (disposing)
{
_inputStream.Close();
- _outputStream.Close();
+ _outputStream?.Close();
}
_disposed = true;
diff --git a/src/SecureFolderFS.Core.MobileFS/Platforms/iOS/IOSFileSystem.cs b/src/SecureFolderFS.Core.MobileFS/Platforms/iOS/IOSFileSystem.cs
index f62ad9dc3..2830935c0 100644
--- a/src/SecureFolderFS.Core.MobileFS/Platforms/iOS/IOSFileSystem.cs
+++ b/src/SecureFolderFS.Core.MobileFS/Platforms/iOS/IOSFileSystem.cs
@@ -1,6 +1,7 @@
using OwlCore.Storage;
using SecureFolderFS.Core.FileSystem;
using SecureFolderFS.Core.FileSystem.AppModels;
+using SecureFolderFS.Core.FileSystem.Extensions;
using SecureFolderFS.Core.FileSystem.Storage;
using SecureFolderFS.Shared.ComponentModel;
using SecureFolderFS.Storage.Enums;
@@ -27,15 +28,16 @@ public Task GetStatusAsync(CancellationToken cancellatio
///
public async Task MountAsync(IFolder folder, IDisposable unlockContract, IDictionary options, CancellationToken cancellationToken = default)
{
+ await Task.CompletedTask;
if (unlockContract is not IWrapper wrapper)
throw new ArgumentException($"The {nameof(unlockContract)} is invalid.");
- var fileSystemOptions = FileSystemOptions.ToOptions(options, () => new HealthStatistics(folder), static () => new FileSystemStatistics());
+ var fileSystemOptions = FileSystemOptions.ToOptions(options, () => new HealthStatistics(), static () => new FileSystemStatistics());
var specifics = FileSystemSpecifics.CreateNew(wrapper.Inner, folder, fileSystemOptions);
- var storageRoot = new CryptoFolder(Path.DirectorySeparatorChar.ToString(), specifics.ContentFolder, specifics);
+ fileSystemOptions.SetupValidators(specifics);
- await Task.CompletedTask;
- return new IOSVFSRoot(storageRoot, fileSystemOptions);
+ var storageRoot = new CryptoFolder(Path.DirectorySeparatorChar.ToString(), specifics.ContentFolder, specifics);
+ return new IOSVFSRoot(storageRoot, specifics);
}
}
}
diff --git a/src/SecureFolderFS.Core.MobileFS/Platforms/iOS/IOSVFSRoot.cs b/src/SecureFolderFS.Core.MobileFS/Platforms/iOS/IOSVFSRoot.cs
index 53dc6d44f..bba4ca9be 100644
--- a/src/SecureFolderFS.Core.MobileFS/Platforms/iOS/IOSVFSRoot.cs
+++ b/src/SecureFolderFS.Core.MobileFS/Platforms/iOS/IOSVFSRoot.cs
@@ -12,8 +12,8 @@ internal sealed class IOSVFSRoot : VFSRoot
///
public override string FileSystemName { get; } = Constants.IOS.FileSystem.FS_NAME;
- public IOSVFSRoot(IFolder storageRoot, FileSystemOptions options)
- : base(storageRoot, options)
+ public IOSVFSRoot(IFolder storageRoot, FileSystemSpecifics specifics)
+ : base(storageRoot, specifics)
{
}
@@ -23,7 +23,7 @@ public override ValueTask DisposeAsync()
if (!_disposed)
{
_disposed = true;
- FileSystemManager.Instance.RemoveRoot(this);
+ FileSystemManager.Instance.FileSystems.Remove(this);
}
return ValueTask.CompletedTask;
diff --git a/src/SecureFolderFS.Core.MobileFS/SecureFolderFS.Core.MobileFS.csproj b/src/SecureFolderFS.Core.MobileFS/SecureFolderFS.Core.MobileFS.csproj
index 56ef54cbd..89c4baf6f 100644
--- a/src/SecureFolderFS.Core.MobileFS/SecureFolderFS.Core.MobileFS.csproj
+++ b/src/SecureFolderFS.Core.MobileFS/SecureFolderFS.Core.MobileFS.csproj
@@ -1,21 +1,21 @@
-
+
net9.0-android
-
+
true
true
enable
enable
- 12.2
- 26.0
+ 15.0
+ 28.0
-
+
diff --git a/src/SecureFolderFS.Core.WebDav/AppModels/WebDavOptions.cs b/src/SecureFolderFS.Core.WebDav/AppModels/WebDavOptions.cs
index 485648406..d29e195b1 100644
--- a/src/SecureFolderFS.Core.WebDav/AppModels/WebDavOptions.cs
+++ b/src/SecureFolderFS.Core.WebDav/AppModels/WebDavOptions.cs
@@ -47,6 +47,8 @@ public static WebDavOptions ToOptions(IDictionary options)
IsReadOnly = (bool?)options.Get(nameof(IsReadOnly)) ?? false,
IsCachingChunks = (bool?)options.Get(nameof(IsCachingChunks)) ?? true,
IsCachingFileNames = (bool?)options.Get(nameof(IsCachingFileNames)) ?? true,
+ IsCachingDirectoryIds = (bool?)options.Get(nameof(IsCachingDirectoryIds)) ?? true,
+ RecycleBinSize = (long?)options.Get(nameof(RecycleBinSize)) ?? 0L,
// WebDav specific
Protocol = (string?)options.Get(nameof(Protocol)) ?? "http",
diff --git a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreFile.cs b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreFile.cs
index 0790d57f1..5a1466ea7 100644
--- a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreFile.cs
+++ b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreFile.cs
@@ -221,7 +221,7 @@ public override bool Equals(object? obj)
{
if (obj is not EncryptingDiskStoreFile storeItem)
return false;
-
+
return storeItem._fileInfo.FullName.Equals(_fileInfo.FullName, StringComparison.CurrentCultureIgnoreCase);
}
diff --git a/src/SecureFolderFS.Core.WebDav/Helpers/PortHelpers.cs b/src/SecureFolderFS.Core.WebDav/Helpers/PortHelpers.cs
index 508174ce7..4654f3639 100644
--- a/src/SecureFolderFS.Core.WebDav/Helpers/PortHelpers.cs
+++ b/src/SecureFolderFS.Core.WebDav/Helpers/PortHelpers.cs
@@ -26,7 +26,7 @@ private static IEnumerable GetUnavailablePorts()
{
if (OperatingSystem.IsAndroid())
return Enumerable.Empty(); // TODO(u android)
-
+
var properties = IPGlobalProperties.GetIPGlobalProperties();
return properties.GetActiveTcpConnections().Select(x => x.LocalEndPoint.Port)
.Concat(properties.GetActiveTcpListeners().Select(x => x.Port))
diff --git a/src/SecureFolderFS.Core/Constants.cs b/src/SecureFolderFS.Core/Constants.cs
index 8c9cfb571..9c7a32a08 100644
--- a/src/SecureFolderFS.Core/Constants.cs
+++ b/src/SecureFolderFS.Core/Constants.cs
@@ -4,8 +4,6 @@ namespace SecureFolderFS.Core
{
public static class Constants
{
- public const string KEY_TEXT_SEPARATOR = "@@@";
-
public static class Vault
{
public static class Names
@@ -26,7 +24,7 @@ public static class Authentication
public const string AUTH_HARDWARE_KEY = "hardware_key";
public const string AUTH_APPLE_FACEID = "apple_faceid";
public const string AUTH_APPLE_TOUCHID = "apple_touchid";
- public const string AUTH_ANDROID_BIOMETRIC = "android_biometric";
+ public const string AUTH_ANDROID_BIOMETRIC = "android_biometrics";
public const string AUTH_DEVICE_PING = "device_ping";
}
@@ -46,6 +44,7 @@ public static class Associations
public const string ASSOC_CONTENT_CIPHER_ID = "contentCipherScheme";
public const string ASSOC_FILENAME_CIPHER_ID = "filenameCipherScheme";
public const string ASSOC_FILENAME_ENCODING_ID = "filenameEncoding";
+ public const string ASSOC_RECYCLE_SIZE = "recycleBinSize";
public const string ASSOC_SPECIALIZATION = "spec";
public const string ASSOC_AUTHENTICATION = "authMode";
public const string ASSOC_VAULT_ID = "vaultId";
@@ -57,7 +56,7 @@ public static class Versions
public const int V1 = 1;
public const int V2 = 2;
public const int V3 = 3;
- public const int LATEST_VERSION = V2; // TODO: (v3) Update version
+ public const int LATEST_VERSION = V3;
}
}
diff --git a/src/SecureFolderFS.Core/Contracts/KeystoreContract.cs b/src/SecureFolderFS.Core/Contracts/KeystoreContract.cs
deleted file mode 100644
index 9a12a79c2..000000000
--- a/src/SecureFolderFS.Core/Contracts/KeystoreContract.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using SecureFolderFS.Core.Cryptography.SecureStore;
-using SecureFolderFS.Core.DataModels;
-using System;
-
-namespace SecureFolderFS.Core.Contracts
-{
- internal class KeystoreContract : IDisposable
- {
- public SecretKey EncKey { get; }
-
- public SecretKey MacKey { get; }
-
- public VaultKeystoreDataModel KeystoreDataModel { get; }
-
- public VaultConfigurationDataModel ConfigurationDataModel { get; }
-
- public KeystoreContract(SecretKey encKey, SecretKey macKey, VaultKeystoreDataModel keystoreDataModel, VaultConfigurationDataModel configurationDataModel)
- {
- EncKey = encKey;
- MacKey = macKey;
- KeystoreDataModel = keystoreDataModel;
- ConfigurationDataModel = configurationDataModel;
- }
-
- ///
- public override string ToString()
- {
- return $"{Convert.ToBase64String(EncKey)}{Constants.KEY_TEXT_SEPARATOR}{Convert.ToBase64String(MacKey)}";
- }
-
- ///
- public virtual void Dispose()
- {
- EncKey.Dispose();
- MacKey.Dispose();
- }
- }
-}
diff --git a/src/SecureFolderFS.Core/Contracts/SecurityContract.cs b/src/SecureFolderFS.Core/Contracts/SecurityContract.cs
deleted file mode 100644
index 3c0dfefe2..000000000
--- a/src/SecureFolderFS.Core/Contracts/SecurityContract.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using SecureFolderFS.Core.Cryptography;
-using SecureFolderFS.Core.Cryptography.SecureStore;
-using SecureFolderFS.Core.DataModels;
-using SecureFolderFS.Shared.ComponentModel;
-
-namespace SecureFolderFS.Core.Contracts
-{
- internal class SecurityContract : KeystoreContract, IWrapper
- {
- private Security? _security;
-
- ///
- public Security Inner => _security ??= Security.CreateNew(
- encKey: EncKey,
- macKey: MacKey,
- contentCipherId: ConfigurationDataModel.ContentCipherId,
- fileNameCipherId: ConfigurationDataModel.FileNameCipherId,
- fileNameEncodingId: ConfigurationDataModel.FileNameEncodingId);
-
- public SecurityContract(SecretKey encKey, SecretKey macKey, VaultKeystoreDataModel keystoreDataModel, VaultConfigurationDataModel configurationDataModel)
- : base(encKey, macKey, keystoreDataModel, configurationDataModel)
- {
- }
-
- ///
- public override void Dispose()
- {
- _security?.Dispose();
- base.Dispose();
- }
- }
-}
diff --git a/src/SecureFolderFS.Core/DataModels/VaultConfigurationDataModel.cs b/src/SecureFolderFS.Core/DataModels/VaultConfigurationDataModel.cs
index c0c0903a0..fd893741d 100644
--- a/src/SecureFolderFS.Core/DataModels/VaultConfigurationDataModel.cs
+++ b/src/SecureFolderFS.Core/DataModels/VaultConfigurationDataModel.cs
@@ -25,11 +25,22 @@ public sealed class VaultConfigurationDataModel : VersionDataModel
///
/// Gets the ID for file name encoding.
///
- //[JsonPropertyName(Associations.ASSOC_FILENAME_ENCODING_ID)]
- //[DefaultValue("")]
- [JsonIgnore]
+ [JsonPropertyName(Associations.ASSOC_FILENAME_ENCODING_ID)]
+ [DefaultValue("")]
public string FileNameEncodingId { get; set; } = Cryptography.Constants.CipherId.ENCODING_BASE64URL;
+ ///
+ /// Gets the size of the recycle bin.
+ ///
+ ///
+ /// If the size is zero, the recycle bin is disabled.
+ /// If the size is any value smaller than zero, the recycle bin has unlimited size capacity.
+ /// Any values above zero indicate the maximum capacity in bytes that is allowed for the recycling operation to proceed.
+ ///
+ [JsonPropertyName(Associations.ASSOC_RECYCLE_SIZE)]
+ [DefaultValue(0L)]
+ public long RecycleBinSize { get; set; } = 0L;
+
/////
///// Gets the specialization of the vault that hints how the user data should be handled.
/////
diff --git a/src/SecureFolderFS.Core/Models/SecurityWrapper.cs b/src/SecureFolderFS.Core/Models/SecurityWrapper.cs
new file mode 100644
index 000000000..11c8f27bf
--- /dev/null
+++ b/src/SecureFolderFS.Core/Models/SecurityWrapper.cs
@@ -0,0 +1,41 @@
+using SecureFolderFS.Core.Cryptography;
+using SecureFolderFS.Core.Cryptography.SecureStore;
+using SecureFolderFS.Core.DataModels;
+using SecureFolderFS.Shared.ComponentModel;
+using System;
+
+namespace SecureFolderFS.Core.Models
+{
+ internal sealed class SecurityWrapper : IWrapper, IDisposable
+ {
+ private readonly KeyPair _keyPair;
+ private readonly VaultConfigurationDataModel _configurationDataModel;
+ private Security? _security;
+
+ ///
+ public Security Inner => _security ??= Security.CreateNew(
+ _keyPair,
+ contentCipherId: _configurationDataModel.ContentCipherId,
+ fileNameCipherId: _configurationDataModel.FileNameCipherId,
+ fileNameEncodingId: _configurationDataModel.FileNameEncodingId);
+
+ public SecurityWrapper(KeyPair keyPair, VaultConfigurationDataModel configurationDataModel)
+ {
+ _keyPair = keyPair;
+ _configurationDataModel = configurationDataModel;
+ }
+
+ ///
+ public override string ToString()
+ {
+ return _keyPair.ToString();
+ }
+
+ ///
+ public void Dispose()
+ {
+ Inner.Dispose();
+ _security?.Dispose();
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Core/Routines/IOptionsRoutine.cs b/src/SecureFolderFS.Core/Routines/IOptionsRoutine.cs
index 828c2d79f..9c968a89c 100644
--- a/src/SecureFolderFS.Core/Routines/IOptionsRoutine.cs
+++ b/src/SecureFolderFS.Core/Routines/IOptionsRoutine.cs
@@ -2,7 +2,7 @@
namespace SecureFolderFS.Core.Routines
{
- public interface IOptionsRoutine
+ public interface IOptionsRoutine : IFinalizationRoutine
{
void SetOptions(IDictionary options);
}
diff --git a/src/SecureFolderFS.Core/Routines/Operational/CreationRoutine.cs b/src/SecureFolderFS.Core/Routines/Operational/CreationRoutine.cs
index b8ec97edf..e0cc027ef 100644
--- a/src/SecureFolderFS.Core/Routines/Operational/CreationRoutine.cs
+++ b/src/SecureFolderFS.Core/Routines/Operational/CreationRoutine.cs
@@ -8,6 +8,7 @@
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
+using SecureFolderFS.Core.Models;
using static SecureFolderFS.Core.Constants.Vault;
using static SecureFolderFS.Core.Cryptography.Constants;
@@ -21,7 +22,7 @@ internal sealed class CreationRoutine : ICreationRoutine
private VaultKeystoreDataModel? _keystoreDataModel;
private VaultConfigurationDataModel? _configDataModel;
private SecretKey? _macKey;
- private SecretKey? _encKey;
+ private SecretKey? _dekKey;
public CreationRoutine(IFolder vaultFolder, VaultWriter vaultWriter)
{
@@ -54,7 +55,7 @@ public void SetCredentials(SecretKey passkey)
// Create key copies for later use
_macKey = macKey.CreateCopy();
- _encKey = encKey.CreateCopy();
+ _dekKey = encKey.CreateCopy();
}
///
@@ -67,6 +68,7 @@ public void SetOptions(IDictionary options)
FileNameCipherId = options.Get(Associations.ASSOC_FILENAME_CIPHER_ID).TryCast() ?? CipherId.AES_SIV,
FileNameEncodingId = options.Get(Associations.ASSOC_FILENAME_ENCODING_ID).TryCast() ?? CipherId.ENCODING_BASE64URL,
AuthenticationMethod = options.Get(Associations.ASSOC_AUTHENTICATION).TryCast() ?? throw new InvalidOperationException($"Cannot create vault without specifying {Associations.ASSOC_AUTHENTICATION}."),
+ RecycleBinSize = options.Get(Associations.ASSOC_RECYCLE_SIZE).TryCast() ?? 0L,
Uid = options.Get(Associations.ASSOC_VAULT_ID).TryCast() ?? Guid.NewGuid().ToString(),
PayloadMac = new byte[HMACSHA256.HashSizeInBytes]
};
@@ -78,7 +80,7 @@ public async Task FinalizeAsync(CancellationToken cancellationToken
ArgumentNullException.ThrowIfNull(_keystoreDataModel);
ArgumentNullException.ThrowIfNull(_configDataModel);
ArgumentNullException.ThrowIfNull(_macKey);
- ArgumentNullException.ThrowIfNull(_encKey);
+ ArgumentNullException.ThrowIfNull(_dekKey);
// First we need to fill in the PayloadMac of the content
VaultParser.CalculateConfigMac(_configDataModel, _macKey, _configDataModel.PayloadMac);
@@ -92,39 +94,14 @@ public async Task FinalizeAsync(CancellationToken cancellationToken
await modifiableFolder.CreateFolderAsync(Names.VAULT_CONTENT_FOLDERNAME, true, cancellationToken);
// Key copies need to be created because the original ones are disposed of here
- return new CreationContract(_encKey.CreateCopy(), _macKey.CreateCopy());
+ return new SecurityWrapper(KeyPair.ImportKeys(_dekKey, _macKey), _configDataModel);
}
///
public void Dispose()
{
- _encKey?.Dispose();
+ _dekKey?.Dispose();
_macKey?.Dispose();
}
}
-
- internal sealed class CreationContract : IDisposable
- {
- private readonly SecretKey _encKey;
- private readonly SecretKey _macKey;
-
- public CreationContract(SecretKey encKey, SecretKey macKey)
- {
- _encKey = encKey;
- _macKey = macKey;
- }
-
- ///
- public override string ToString()
- {
- return $"{Convert.ToBase64String(_encKey)}{Constants.KEY_TEXT_SEPARATOR}{Convert.ToBase64String(_macKey)}";
- }
-
- ///
- public void Dispose()
- {
- _encKey.Dispose();
- _macKey.Dispose();
- }
- }
}
diff --git a/src/SecureFolderFS.Core/Routines/Operational/ModifyCredentialsRoutine.cs b/src/SecureFolderFS.Core/Routines/Operational/ModifyCredentialsRoutine.cs
index 2aa3fb957..41d753f57 100644
--- a/src/SecureFolderFS.Core/Routines/Operational/ModifyCredentialsRoutine.cs
+++ b/src/SecureFolderFS.Core/Routines/Operational/ModifyCredentialsRoutine.cs
@@ -1,5 +1,6 @@
-using SecureFolderFS.Core.Contracts;
+using SecureFolderFS.Core.Cryptography.SecureStore;
using SecureFolderFS.Core.DataModels;
+using SecureFolderFS.Core.Models;
using SecureFolderFS.Core.VaultAccess;
using SecureFolderFS.Shared.Extensions;
using System;
@@ -7,7 +8,8 @@
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
-using SecureFolderFS.Core.Cryptography.SecureStore;
+using SecureFolderFS.Core.Cryptography;
+using SecureFolderFS.Shared.ComponentModel;
using static SecureFolderFS.Core.Constants.Vault;
namespace SecureFolderFS.Core.Routines.Operational
@@ -16,7 +18,7 @@ namespace SecureFolderFS.Core.Routines.Operational
internal sealed class ModifyCredentialsRoutine : IModifyCredentialsRoutine
{
private readonly VaultWriter _vaultWriter;
- private KeystoreContract? _unlockContract;
+ private KeyPair? _keyPair;
private VaultKeystoreDataModel? _keystoreDataModel;
private VaultConfigurationDataModel? _configDataModel;
@@ -34,10 +36,10 @@ public Task InitAsync(CancellationToken cancellationToken = default)
///
public void SetUnlockContract(IDisposable unlockContract)
{
- if (unlockContract is not KeystoreContract contract)
+ if (unlockContract is not IWrapper securityWrapper)
throw new ArgumentException($"The {nameof(unlockContract)} is invalid.");
- _unlockContract = contract;
+ _keyPair = securityWrapper.Inner.KeyPair;
}
///
@@ -50,6 +52,7 @@ public void SetOptions(IDictionary options)
FileNameCipherId = options.Get(Associations.ASSOC_FILENAME_CIPHER_ID).TryCast() ?? throw GetException(nameof(Associations.ASSOC_FILENAME_CIPHER_ID)),
FileNameEncodingId = options.Get(Associations.ASSOC_FILENAME_ENCODING_ID).TryCast() ?? throw GetException(nameof(Associations.ASSOC_FILENAME_ENCODING_ID)),
AuthenticationMethod = options.Get(Associations.ASSOC_AUTHENTICATION).TryCast() ?? throw GetException(nameof(Associations.ASSOC_AUTHENTICATION)),
+ RecycleBinSize = options.Get(Associations.ASSOC_RECYCLE_SIZE).TryCast() ?? throw GetException(nameof(Associations.ASSOC_RECYCLE_SIZE)),
Uid = options.Get(Associations.ASSOC_VAULT_ID).TryCast() ?? throw GetException(nameof(Associations.ASSOC_VAULT_ID)),
PayloadMac = new byte[HMACSHA256.HashSizeInBytes]
};
@@ -64,7 +67,7 @@ static Exception GetException(string argumentName)
///
public void SetCredentials(SecretKey passkey)
{
- ArgumentNullException.ThrowIfNull(_unlockContract);
+ ArgumentNullException.ThrowIfNull(_keyPair);
// Generate new salt
using var secureRandom = RandomNumberGenerator.Create();
@@ -72,29 +75,31 @@ public void SetCredentials(SecretKey passkey)
secureRandom.GetNonZeroBytes(salt);
// Encrypt new keystore
- _keystoreDataModel = VaultParser.EncryptKeystore(passkey, _unlockContract.EncKey, _unlockContract.MacKey, salt);
+ _keystoreDataModel = VaultParser.EncryptKeystore(passkey, _keyPair.DekKey, _keyPair.MacKey, salt);
}
///
public async Task FinalizeAsync(CancellationToken cancellationToken)
{
- ArgumentNullException.ThrowIfNull(_unlockContract);
+ ArgumentNullException.ThrowIfNull(_keyPair);
ArgumentNullException.ThrowIfNull(_configDataModel);
// First we need to fill in the PayloadMac of the content
- VaultParser.CalculateConfigMac(_configDataModel, _unlockContract.MacKey, _configDataModel.PayloadMac);
+ VaultParser.CalculateConfigMac(_configDataModel, _keyPair.MacKey, _configDataModel.PayloadMac);
// Write the whole configuration
await _vaultWriter.WriteKeystoreAsync(_keystoreDataModel, cancellationToken);
await _vaultWriter.WriteConfigurationAsync(_configDataModel, cancellationToken);
- return _unlockContract;
+ // Key copies need to be created because the original ones are disposed of here
+ using (_keyPair)
+ return new SecurityWrapper(KeyPair.ImportKeys(_keyPair.DekKey, _keyPair.MacKey), _configDataModel);
}
///
public void Dispose()
{
- _unlockContract?.Dispose();
+ _keyPair?.Dispose();
}
}
}
diff --git a/src/SecureFolderFS.Core/Routines/Operational/RecoverRoutine.cs b/src/SecureFolderFS.Core/Routines/Operational/RecoverRoutine.cs
index 83f3c71a9..c28630db1 100644
--- a/src/SecureFolderFS.Core/Routines/Operational/RecoverRoutine.cs
+++ b/src/SecureFolderFS.Core/Routines/Operational/RecoverRoutine.cs
@@ -1,6 +1,6 @@
-using SecureFolderFS.Core.Contracts;
-using SecureFolderFS.Core.Cryptography.SecureStore;
+using SecureFolderFS.Core.Cryptography.SecureStore;
using SecureFolderFS.Core.DataModels;
+using SecureFolderFS.Core.Models;
using SecureFolderFS.Core.Validators;
using SecureFolderFS.Core.VaultAccess;
using System;
@@ -12,23 +12,21 @@ namespace SecureFolderFS.Core.Routines.Operational
///
public sealed class RecoverRoutine : ICredentialsRoutine
{
- private readonly SecretKey _encKey;
+ private readonly SecretKey _dekKey;
private readonly SecretKey _macKey;
private readonly VaultReader _vaultReader;
- private VaultKeystoreDataModel? _keystoreDataModel;
private VaultConfigurationDataModel? _configDataModel;
public RecoverRoutine(VaultReader vaultReader)
{
_vaultReader = vaultReader;
- _encKey = new SecureKey(Cryptography.Constants.KeyTraits.ENCKEY_LENGTH);
+ _dekKey = new SecureKey(Cryptography.Constants.KeyTraits.ENCKEY_LENGTH);
_macKey = new SecureKey(Cryptography.Constants.KeyTraits.MACKEY_LENGTH);
}
///
public async Task InitAsync(CancellationToken cancellationToken)
{
- _keystoreDataModel = await _vaultReader.ReadKeystoreAsync(cancellationToken);
_configDataModel = await _vaultReader.ReadConfigurationAsync(cancellationToken);
}
@@ -36,7 +34,7 @@ public async Task InitAsync(CancellationToken cancellationToken)
public void SetCredentials(SecretKey passkey)
{
// Copy the first part (DEK) of the master key
- passkey.Key.AsSpan(0, Cryptography.Constants.KeyTraits.ENCKEY_LENGTH).CopyTo(_encKey.Key);
+ passkey.Key.AsSpan(0, Cryptography.Constants.KeyTraits.ENCKEY_LENGTH).CopyTo(_dekKey.Key);
// Copy the second part (MAC) of the master key
passkey.Key.AsSpan(Cryptography.Constants.KeyTraits.MACKEY_LENGTH).CopyTo(_macKey.Key);
@@ -46,9 +44,8 @@ public void SetCredentials(SecretKey passkey)
public async Task FinalizeAsync(CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(_configDataModel);
- ArgumentNullException.ThrowIfNull(_keystoreDataModel);
- using (_encKey)
+ using (_dekKey)
using (_macKey)
{
// Create MAC key copy for the validator that can be disposed here
@@ -60,14 +57,14 @@ public async Task FinalizeAsync(CancellationToken cancellationToken
// In this case, we rely on the consumer to take ownership of the keys, and thus manage their lifetimes
// Key copies need to be created because the original ones are disposed of here
- return new SecurityContract(_encKey.CreateCopy(), _macKey.CreateCopy(), _keystoreDataModel, _configDataModel);
+ return new SecurityWrapper(KeyPair.ImportKeys(_dekKey, _macKey), _configDataModel);
}
}
///
public void Dispose()
{
- _encKey.Dispose();
+ _dekKey.Dispose();
_macKey.Dispose();
}
}
diff --git a/src/SecureFolderFS.Core/Routines/Operational/UnlockRoutine.cs b/src/SecureFolderFS.Core/Routines/Operational/UnlockRoutine.cs
index dbb45613d..10f274d21 100644
--- a/src/SecureFolderFS.Core/Routines/Operational/UnlockRoutine.cs
+++ b/src/SecureFolderFS.Core/Routines/Operational/UnlockRoutine.cs
@@ -1,6 +1,6 @@
-using SecureFolderFS.Core.Contracts;
-using SecureFolderFS.Core.Cryptography.SecureStore;
+using SecureFolderFS.Core.Cryptography.SecureStore;
using SecureFolderFS.Core.DataModels;
+using SecureFolderFS.Core.Models;
using SecureFolderFS.Core.Validators;
using SecureFolderFS.Core.VaultAccess;
using System;
@@ -15,7 +15,7 @@ internal sealed class UnlockRoutine : ICredentialsRoutine
private readonly VaultReader _vaultReader;
private VaultKeystoreDataModel? _keystoreDataModel;
private VaultConfigurationDataModel? _configDataModel;
- private SecretKey? _encKey;
+ private SecretKey? _dekKey;
private SecretKey? _macKey;
public UnlockRoutine(VaultReader vaultReader)
@@ -38,19 +38,19 @@ public void SetCredentials(SecretKey passkey)
// Derive keystore
var (encKey, macKey) = VaultParser.DeriveKeystore(passkey, _keystoreDataModel);
- _encKey = encKey;
+ _dekKey = encKey;
_macKey = macKey;
}
///
public async Task FinalizeAsync(CancellationToken cancellationToken)
{
- ArgumentNullException.ThrowIfNull(_encKey);
+ ArgumentNullException.ThrowIfNull(_dekKey);
ArgumentNullException.ThrowIfNull(_macKey);
ArgumentNullException.ThrowIfNull(_configDataModel);
ArgumentNullException.ThrowIfNull(_keystoreDataModel);
- using (_encKey)
+ using (_dekKey)
using (_macKey)
{
// Create MAC key copy for the validator that can be disposed here
@@ -62,14 +62,14 @@ public async Task FinalizeAsync(CancellationToken cancellationToken
// In this case, we rely on the consumer to take ownership of the keys, and thus manage their lifetimes
// Key copies need to be created because the original ones are disposed of here
- return new SecurityContract(_encKey.CreateCopy(), _macKey.CreateCopy(), _keystoreDataModel, _configDataModel);
+ return new SecurityWrapper(KeyPair.ImportKeys(_dekKey, _macKey), _configDataModel);
}
}
///
public void Dispose()
{
- _encKey?.Dispose();
+ _dekKey?.Dispose();
_macKey?.Dispose();
}
}
diff --git a/src/SecureFolderFS.Core/VaultAccess/VaultParser.cs b/src/SecureFolderFS.Core/VaultAccess/VaultParser.cs
index 7edbdd322..0dab4c941 100644
--- a/src/SecureFolderFS.Core/VaultAccess/VaultParser.cs
+++ b/src/SecureFolderFS.Core/VaultAccess/VaultParser.cs
@@ -27,7 +27,8 @@ public static void CalculateConfigMac(VaultConfigurationDataModel configDataMode
hmacSha256.AppendData(BitConverter.GetBytes(Constants.Vault.Versions.LATEST_VERSION)); // Version
hmacSha256.AppendData(BitConverter.GetBytes(CryptHelpers.ContentCipherId(configDataModel.ContentCipherId))); // ContentCipherScheme
hmacSha256.AppendData(BitConverter.GetBytes(CryptHelpers.FileNameCipherId(configDataModel.FileNameCipherId))); // FileNameCipherScheme
- //hmacSha256.AppendData(Encoding.UTF8.GetBytes(configDataModel.FileNameEncodingId)); // FileNameEncodingId // TODO: (v3) Hash Encoding ID
+ hmacSha256.AppendData(BitConverter.GetBytes(configDataModel.RecycleBinSize)); // RecycleBinSize
+ hmacSha256.AppendData(Encoding.UTF8.GetBytes(configDataModel.FileNameEncodingId)); // FileNameEncodingId
hmacSha256.AppendData(Encoding.UTF8.GetBytes(configDataModel.Uid)); // Id
hmacSha256.AppendFinalData(Encoding.UTF8.GetBytes(configDataModel.AuthenticationMethod)); // AuthMethod
@@ -49,7 +50,7 @@ public static (SecretKey encKey, SecretKey macKey) DeriveKeystore(SecretKey pass
// Derive KEK
Span kek = stackalloc byte[Cryptography.Constants.KeyTraits.ARGON2_KEK_LENGTH];
- Argon2id.DeriveKey(passkey.Key, keystoreDataModel.Salt, kek);
+ Argon2id.V3_DeriveKey(passkey.Key, keystoreDataModel.Salt, kek);
// Unwrap keys
using var rfc3394 = new Rfc3394KeyWrap();
@@ -76,7 +77,7 @@ public static VaultKeystoreDataModel EncryptKeystore(
{
// Derive KEK
Span kek = stackalloc byte[Cryptography.Constants.KeyTraits.ARGON2_KEK_LENGTH];
- Argon2id.DeriveKey(passkey, salt, kek);
+ Argon2id.V3_DeriveKey(passkey, salt, kek);
// Wrap keys
using var rfc3394 = new Rfc3394KeyWrap();
diff --git a/src/SecureFolderFS.Sdk/AppModels/HealthModel.cs b/src/SecureFolderFS.Sdk/AppModels/HealthModel.cs
index 5defafd92..68d77c15e 100644
--- a/src/SecureFolderFS.Sdk/AppModels/HealthModel.cs
+++ b/src/SecureFolderFS.Sdk/AppModels/HealthModel.cs
@@ -121,6 +121,7 @@ public void Report(IResult value)
Interlocked.Increment(ref _totalFilesScanned);
else if (typeResult.Value == StorableType.Folder)
Interlocked.Increment(ref _totalFoldersScanned);
+
break;
}
diff --git a/src/SecureFolderFS.Sdk/AppModels/SidebarSearchModel.cs b/src/SecureFolderFS.Sdk/AppModels/SidebarSearchModel.cs
index 81cc7cb4a..9999c50b6 100644
--- a/src/SecureFolderFS.Sdk/AppModels/SidebarSearchModel.cs
+++ b/src/SecureFolderFS.Sdk/AppModels/SidebarSearchModel.cs
@@ -1,10 +1,10 @@
-using SecureFolderFS.Sdk.Models;
-using SecureFolderFS.Sdk.ViewModels.Controls.VaultList;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
+using SecureFolderFS.Sdk.Models;
+using SecureFolderFS.Sdk.ViewModels.Controls.VaultList;
namespace SecureFolderFS.Sdk.AppModels
{
@@ -26,7 +26,7 @@ public async IAsyncEnumerable SearchAsync(string query, [EnumeratorCance
var splitQuery = query.ToLowerInvariant().Split(' ');
foreach (var item in _items)
{
- var found = splitQuery.All(item.VaultViewModel.VaultName.Contains);
+ var found = item.VaultViewModel.Title is not null && splitQuery.All(item.VaultViewModel.Title.Contains);
if (found)
yield return item;
}
diff --git a/src/SecureFolderFS.Sdk/AppModels/Sorters/BaseFolderSorter.cs b/src/SecureFolderFS.Sdk/AppModels/Sorters/BaseFolderSorter.cs
new file mode 100644
index 000000000..87bd5366f
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/AppModels/Sorters/BaseFolderSorter.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using SecureFolderFS.Sdk.ViewModels.Controls.Storage.Browser;
+using SecureFolderFS.Shared.ComponentModel;
+
+namespace SecureFolderFS.Sdk.AppModels.Sorters
+{
+ public abstract class BaseFolderSorter : IItemSorter
+ {
+ private readonly Lock _lock = new();
+
+ ///
+ public virtual int GetInsertIndex(BrowserItemViewModel newItem, ICollection collection)
+ {
+ var low = 0;
+ var high = collection.Count;
+
+ while (low < high)
+ {
+ var mid = (low + high) / 2;
+
+ // If newItem should come before the mid-element, search the left half.
+ if (Compare(newItem, collection.ElementAt(mid)) < 0)
+ high = mid;
+ else
+ low = mid + 1;
+ }
+
+ return low;
+ }
+
+ ///
+ public virtual void SortCollection(IEnumerable source, ICollection destination)
+ {
+ var sortedList = new List(source);
+ sortedList.Sort(Compare);
+ destination.Clear();
+ foreach (var item in sortedList)
+ {
+ destination.Add(item);
+ }
+ }
+
+ ///
+ public abstract int Compare(BrowserItemViewModel? x, BrowserItemViewModel? y);
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/AppModels/Sorters/KindSorter.cs b/src/SecureFolderFS.Sdk/AppModels/Sorters/KindSorter.cs
new file mode 100644
index 000000000..5769a2919
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/AppModels/Sorters/KindSorter.cs
@@ -0,0 +1,46 @@
+using System;
+using System.IO;
+using SecureFolderFS.Sdk.ViewModels.Controls.Storage.Browser;
+using SecureFolderFS.Shared.ComponentModel;
+
+namespace SecureFolderFS.Sdk.AppModels.Sorters
+{
+ public sealed class KindSorter : BaseFolderSorter
+ {
+ private readonly bool _isAscending;
+
+ public static IItemSorter Ascending { get; } = new KindSorter(true);
+
+ public static IItemSorter Descending { get; } = new KindSorter(false);
+
+ private KindSorter(bool isAscending)
+ {
+ _isAscending = isAscending;
+ }
+
+ ///
+ public override int Compare(BrowserItemViewModel? x, BrowserItemViewModel? y)
+ {
+ if (x is null || y is null)
+ return 0;
+
+ var result = string.Compare(GetKind(x), GetKind(y), StringComparison.OrdinalIgnoreCase);
+ return _isAscending ? result : -result;
+ }
+
+ private static string GetKind(BrowserItemViewModel item)
+ {
+ switch (item)
+ {
+ case FolderViewModel: return string.Empty;
+ case FileViewModel:
+ {
+ var extension = Path.GetExtension(item.Inner.Name);
+ return string.IsNullOrEmpty(extension) ? "File" : extension;
+ }
+
+ default: return "Unknown";
+ }
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/AppModels/Sorters/NameSorter.cs b/src/SecureFolderFS.Sdk/AppModels/Sorters/NameSorter.cs
new file mode 100644
index 000000000..552d427ae
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/AppModels/Sorters/NameSorter.cs
@@ -0,0 +1,41 @@
+using System;
+using SecureFolderFS.Sdk.ViewModels.Controls.Storage.Browser;
+using SecureFolderFS.Shared.ComponentModel;
+
+namespace SecureFolderFS.Sdk.AppModels.Sorters
+{
+ public sealed class NameSorter : BaseFolderSorter
+ {
+ private readonly bool _isAscending;
+
+ public static IItemSorter Ascending { get; } = new NameSorter(true);
+
+ public static IItemSorter Descending { get; } = new NameSorter(false);
+
+ private NameSorter(bool isAscending)
+ {
+ _isAscending = isAscending;
+ }
+
+ ///
+ public override int Compare(BrowserItemViewModel? x, BrowserItemViewModel? y)
+ {
+ if (x is null || y is null)
+ return 0;
+
+ // Ensure folders come before files
+ var xIsFolder = x is FolderViewModel;
+ var yIsFolder = y is FolderViewModel;
+
+ if (xIsFolder && !yIsFolder)
+ return -1;
+
+ if (!xIsFolder && yIsFolder)
+ return 1;
+
+ // If both are same type, sort by name
+ var result = string.Compare(x.Title, y.Title, StringComparison.OrdinalIgnoreCase);
+ return _isAscending ? result : -result;
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/AppModels/Sorters/SizeSorter.cs b/src/SecureFolderFS.Sdk/AppModels/Sorters/SizeSorter.cs
new file mode 100644
index 000000000..0a40e4fb2
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/AppModels/Sorters/SizeSorter.cs
@@ -0,0 +1,43 @@
+using System.Threading.Tasks;
+using SecureFolderFS.Sdk.ViewModels.Controls.Storage.Browser;
+using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Storage.Extensions;
+using SecureFolderFS.Storage.StorageProperties;
+
+namespace SecureFolderFS.Sdk.AppModels.Sorters
+{
+ public sealed class SizeSorter : BaseFolderSorter
+ {
+ private readonly bool _isAscending;
+
+ public static IItemSorter Ascending { get; } = new SizeSorter(true);
+
+ public static IItemSorter Descending { get; } = new SizeSorter(false);
+
+ private SizeSorter(bool isAscending)
+ {
+ _isAscending = isAscending;
+ }
+
+ ///
+ public override int Compare(BrowserItemViewModel? x, BrowserItemViewModel? y)
+ {
+ if (x is null || y is null)
+ return 0;
+
+ var size1 = GetSizeAsync(x).ConfigureAwait(false).GetAwaiter().GetResult();
+ var size2 = GetSizeAsync(y).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ var result = size1.CompareTo(size2);
+ return _isAscending ? result : -result;
+ }
+
+ private static async Task GetSizeAsync(BrowserItemViewModel browserItemViewModel)
+ {
+ if (browserItemViewModel is not FileViewModel fileViewModel)
+ return -1L;
+
+ return await fileViewModel.File.GetSizeAsync().ConfigureAwait(false);
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/AppModels/SystemMonitorModel.cs b/src/SecureFolderFS.Sdk/AppModels/SystemMonitorModel.cs
index 3475f0ca7..39919d5f9 100644
--- a/src/SecureFolderFS.Sdk/AppModels/SystemMonitorModel.cs
+++ b/src/SecureFolderFS.Sdk/AppModels/SystemMonitorModel.cs
@@ -36,9 +36,9 @@ public Task InitAsync(CancellationToken cancellationToken = default)
private void AttachEvents()
{
if (SettingsService.UserSettings.LockOnSystemLock)
- SystemService.DesktopLocked += SystemService_DesktopLocked;
+ SystemService.DeviceLocked += SystemService_DesktopLocked;
else
- SystemService.DesktopLocked -= SystemService_DesktopLocked;
+ SystemService.DeviceLocked -= SystemService_DesktopLocked;
}
private void SystemService_DesktopLocked(object? sender, EventArgs e)
@@ -61,7 +61,7 @@ private void UserSettings_PropertyChanged(object? sender, PropertyChangedEventAr
///
public void Dispose()
{
- SystemService.DesktopLocked -= SystemService_DesktopLocked;
+ SystemService.DeviceLocked -= SystemService_DesktopLocked;
SettingsService.UserSettings.PropertyChanged -= UserSettings_PropertyChanged;
}
}
diff --git a/src/SecureFolderFS.Sdk/AppModels/TransferFilter.cs b/src/SecureFolderFS.Sdk/AppModels/TransferFilter.cs
new file mode 100644
index 000000000..b48b9fd92
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/AppModels/TransferFilter.cs
@@ -0,0 +1,11 @@
+using SecureFolderFS.Sdk.Enums;
+using SecureFolderFS.Storage.Pickers;
+
+namespace SecureFolderFS.Sdk.AppModels
+{
+ ///
+ /// Represents a transfer type options.
+ ///
+ /// The type of transfer to use.
+ public sealed record TransferOptions(TransferType TransferType) : PickerOptions;
+}
diff --git a/src/SecureFolderFS.Sdk/AppModels/VaultCollectionModel.cs b/src/SecureFolderFS.Sdk/AppModels/VaultCollectionModel.cs
index 8c2fef8dd..a456911c8 100644
--- a/src/SecureFolderFS.Sdk/AppModels/VaultCollectionModel.cs
+++ b/src/SecureFolderFS.Sdk/AppModels/VaultCollectionModel.cs
@@ -14,6 +14,7 @@
using System.Collections.Specialized;
using System.Threading;
using System.Threading.Tasks;
+using SecureFolderFS.Storage;
namespace SecureFolderFS.Sdk.AppModels
{
@@ -114,17 +115,20 @@ protected override void InsertItem(int index, IVaultModel item)
}
///
- protected override void RemoveItem(int index)
+ protected override async void RemoveItem(int index)
{
var removedItem = this[index];
// Remove persisted
- if (VaultConfigurations.SavedVaults is not null)
- VaultConfigurations.SavedVaults.RemoveAt(index);
+ VaultConfigurations.SavedVaults?.RemoveAt(index);
// Remove widget data for that vault
VaultWidgets.SetForVault(removedItem.Folder.Id, null);
+ // Remove bookmark
+ if (removedItem.Folder is IBookmark bookmark)
+ await bookmark.RemoveBookmarkAsync();
+
// Remove from cache
base.RemoveItem(index);
CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Remove, removedItem, index));
diff --git a/src/SecureFolderFS.Sdk/AppModels/WidgetsCollectionModel.cs b/src/SecureFolderFS.Sdk/AppModels/WidgetsCollectionModel.cs
index 71fc67192..e2c1baa5f 100644
--- a/src/SecureFolderFS.Sdk/AppModels/WidgetsCollectionModel.cs
+++ b/src/SecureFolderFS.Sdk/AppModels/WidgetsCollectionModel.cs
@@ -58,7 +58,7 @@ public bool RemoveWidget(string widgetId)
{
// Get widgets
var widgets = VaultWidgets.GetForVault(_vaultFolder.Id);
-
+
var itemToRemove = widgets?.FirstOrDefault(x => x.WidgetId == widgetId);
if (itemToRemove is null)
return false;
diff --git a/src/SecureFolderFS.Sdk/Constants.cs b/src/SecureFolderFS.Sdk/Constants.cs
index 2bb5061bd..78620d370 100644
--- a/src/SecureFolderFS.Sdk/Constants.cs
+++ b/src/SecureFolderFS.Sdk/Constants.cs
@@ -17,10 +17,9 @@ public static class Graphs
public static class Health
{
- public static bool ARE_UPDATES_OPTIMIZED = true;
- public static bool IS_SCANNING_PARALLELIZED = false;
- public static bool IS_DISCOVERY_ASSUMED_FAST = true;
- public static double INTERVAL_MULTIPLIER = 0.2d;
+ public const bool ARE_UPDATES_OPTIMIZED = true;
+ public const bool IS_SCANNING_PARALLELIZED = false;
+ public const double INTERVAL_MULTIPLIER = 0.2d;
}
}
@@ -31,8 +30,9 @@ public static class Dialogs
public static class Vault
{
- public const string VAULT_ICON_FILENAME = "vault.icon";
public const int MAX_FREE_AMOUNT_OF_VAULTS = 2;
+ public const string VAULT_ICON_FILENAME = "vault_icon";
+ public const string VAULT_ICON_FILENAME_ICO = "vault_icon.ico";
public const string VAULT_README_FILENAME = "_readme_before_continuing.txt";
public const string VAULT_README_MESSAGE = """
PLEASE READ BEFORE USING THIS VAULT
@@ -46,6 +46,13 @@ This will open a virtual storage directory where the files you add will be autom
""";
}
+ public static class Sizes
+ {
+ public const long KILOBYTE = 1024;
+ public const long MEGABYTE = KILOBYTE * 1024;
+ public const long GIGABYTE = MEGABYTE * 1024;
+ }
+
public static class IntegrationPermissions
{
public const string ENUMERATE_VAULTS = "enumerate_vaults"; // List all added vaults and get basic info
diff --git a/src/SecureFolderFS.Sdk/Enums/AuthenticationType.cs b/src/SecureFolderFS.Sdk/Enums/AuthenticationStage.cs
similarity index 93%
rename from src/SecureFolderFS.Sdk/Enums/AuthenticationType.cs
rename to src/SecureFolderFS.Sdk/Enums/AuthenticationStage.cs
index afbdaac7d..4f51365c8 100644
--- a/src/SecureFolderFS.Sdk/Enums/AuthenticationType.cs
+++ b/src/SecureFolderFS.Sdk/Enums/AuthenticationStage.cs
@@ -6,7 +6,7 @@ namespace SecureFolderFS.Sdk.Enums
/// Determines which stage a given authentication method can be applied to.
///
[Flags]
- public enum AuthenticationType : uint
+ public enum AuthenticationStage : uint
{
///
/// The authentication method may only be used as the first stage.
diff --git a/src/SecureFolderFS.Sdk/Enums/BrowserViewType.cs b/src/SecureFolderFS.Sdk/Enums/BrowserViewType.cs
new file mode 100644
index 000000000..20cd3de86
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/Enums/BrowserViewType.cs
@@ -0,0 +1,19 @@
+namespace SecureFolderFS.Sdk.Enums
+{
+ public enum BrowserViewType
+ {
+ // List View and Column View
+ ListView,
+ ColumnView,
+
+ // Grid View
+ SmallGridView,
+ MediumGridView,
+ LargeGridView,
+
+ // Gallery View
+ SmallGalleryView,
+ MediumGalleryView,
+ LargeGalleryView,
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/Enums/PasswordStrength.cs b/src/SecureFolderFS.Sdk/Enums/PasswordStrength.cs
new file mode 100644
index 000000000..a582d9765
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/Enums/PasswordStrength.cs
@@ -0,0 +1,12 @@
+namespace SecureFolderFS.Sdk.Enums
+{
+ public enum PasswordStrength : uint
+ {
+ Blank = 0u,
+ VeryWeak = 1u,
+ Weak = 2u,
+ Medium = 3u,
+ Strong = 4u,
+ VeryStrong = 5u
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/Enums/SeverityType.cs b/src/SecureFolderFS.Sdk/Enums/Severity.cs
similarity index 93%
rename from src/SecureFolderFS.Sdk/Enums/SeverityType.cs
rename to src/SecureFolderFS.Sdk/Enums/Severity.cs
index d50071008..b744f5833 100644
--- a/src/SecureFolderFS.Sdk/Enums/SeverityType.cs
+++ b/src/SecureFolderFS.Sdk/Enums/Severity.cs
@@ -1,6 +1,6 @@
namespace SecureFolderFS.Sdk.Enums
{
- public enum SeverityType
+ public enum Severity
{
///
/// Informational status.
diff --git a/src/SecureFolderFS.Sdk/Enums/TransferType.cs b/src/SecureFolderFS.Sdk/Enums/TransferType.cs
index 5e04a63ec..f73d82494 100644
--- a/src/SecureFolderFS.Sdk/Enums/TransferType.cs
+++ b/src/SecureFolderFS.Sdk/Enums/TransferType.cs
@@ -4,6 +4,6 @@ public enum TransferType
{
Copy = 0,
Move = 1,
- Recycle = 2
+ Select = 2
}
}
diff --git a/src/SecureFolderFS.Sdk/EventArguments/ScanningFinishedEventArgs.cs b/src/SecureFolderFS.Sdk/EventArguments/ScanningFinishedEventArgs.cs
new file mode 100644
index 000000000..4368101a7
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/EventArguments/ScanningFinishedEventArgs.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace SecureFolderFS.Sdk.EventArguments
+{
+ ///
+ /// Event arguments for health scanning finished notifications.
+ ///
+ public sealed class ScanningFinishedEventArgs(bool wasCanceled) : EventArgs
+ {
+ ///
+ /// Gets the value that determines whether the scanning was interrupted (canceled) by the user.
+ ///
+ public bool WasCanceled { get; } = wasCanceled;
+ }
+}
\ No newline at end of file
diff --git a/src/SecureFolderFS.Sdk/EventArguments/ScanningStartedEventArgs.cs b/src/SecureFolderFS.Sdk/EventArguments/ScanningStartedEventArgs.cs
new file mode 100644
index 000000000..a0a70b53b
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/EventArguments/ScanningStartedEventArgs.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace SecureFolderFS.Sdk.EventArguments
+{
+ ///
+ /// Event arguments for health scanning started notifications.
+ ///
+ public sealed class ScanningStartedEventArgs : EventArgs;
+}
\ No newline at end of file
diff --git a/src/SecureFolderFS.Sdk/Extensions/FolderViewModelExtensions.cs b/src/SecureFolderFS.Sdk/Extensions/FolderViewModelExtensions.cs
deleted file mode 100644
index 8a5471ed0..000000000
--- a/src/SecureFolderFS.Sdk/Extensions/FolderViewModelExtensions.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System.Collections.Generic;
-using SecureFolderFS.Sdk.ViewModels.Views.Browser;
-
-namespace SecureFolderFS.Sdk.Extensions
-{
- public static class FolderViewModelExtensions
- {
- public static void SelectAll(this FolderViewModel folderViewModel)
- {
- if (!folderViewModel.BrowserViewModel.IsSelecting)
- return;
-
- foreach (var item in folderViewModel.Items)
- {
- item.IsSelected = true;
- }
- }
-
- public static void UnselectAll(this FolderViewModel folderViewModel)
- {
- foreach (var item in folderViewModel.Items)
- {
- item.IsSelected = false;
- }
- }
-
- public static IEnumerable GetSelectedItems(this FolderViewModel folderViewModel)
- {
- if (!folderViewModel.BrowserViewModel.IsSelecting)
- yield break;
-
- foreach (var item in folderViewModel.Items)
- {
- if (item.IsSelected)
- yield return item;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/SecureFolderFS.Sdk/Extensions/RecycleBinServiceExtensions.cs b/src/SecureFolderFS.Sdk/Extensions/RecycleBinServiceExtensions.cs
new file mode 100644
index 000000000..290980292
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/Extensions/RecycleBinServiceExtensions.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Storage.VirtualFileSystem;
+
+namespace SecureFolderFS.Sdk.Extensions
+{
+ public static class RecycleBinServiceExtensions
+ {
+ ///
+ public static async Task TryGetRecycleBinAsync(this IRecycleBinService recycleBinService, IVFSRoot vfsRoot, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ return await recycleBinService.GetRecycleBinAsync(vfsRoot, cancellationToken);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ ///
+ public static async Task TryGetOrCreateRecycleBinAsync(this IRecycleBinService recycleBinService, IVFSRoot vfsRoot, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ return await recycleBinService.GetOrCreateRecycleBinAsync(vfsRoot, cancellationToken);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ ///
+ public static async Task TryRecalculateSizesAsync(this IRecycleBinService recycleBinService, IVFSRoot vfsRoot, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ await recycleBinService.RecalculateSizesAsync(vfsRoot, cancellationToken);
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/Extensions/SelectableExtensions.cs b/src/SecureFolderFS.Sdk/Extensions/SelectableExtensions.cs
new file mode 100644
index 000000000..29644852c
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/Extensions/SelectableExtensions.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using System.Linq;
+using SecureFolderFS.Sdk.ViewModels.Controls;
+
+namespace SecureFolderFS.Sdk.Extensions
+{
+ public static class SelectableExtensions
+ {
+ public static void SelectAll(this ICollection collection)
+ where T : SelectableItemViewModel
+ {
+ foreach (var item in collection)
+ item.IsSelected = true;
+ }
+
+ public static void UnselectAll(this ICollection collection)
+ where T : SelectableItemViewModel
+ {
+ foreach (var item in collection)
+ item.IsSelected = false;
+ }
+
+ public static IEnumerable GetSelectedItems(this ICollection collection)
+ where T : SelectableItemViewModel
+ {
+ return collection.Where(item => item.IsSelected);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs b/src/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs
index 9ffb8d8bd..26731e147 100644
--- a/src/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs
+++ b/src/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs
@@ -4,23 +4,51 @@
using System.Threading;
using System.Threading.Tasks;
using OwlCore.Storage;
+using SecureFolderFS.Sdk.Enums;
using SecureFolderFS.Sdk.ViewModels.Controls.Transfer;
using SecureFolderFS.Shared.Extensions;
+using SecureFolderFS.Storage.Extensions;
namespace SecureFolderFS.Sdk.Extensions
{
public static class TransferExtensions
{
- public static Task TransferAsync(
+ public static bool IsPickingItems(this TransferViewModel transferViewModel)
+ {
+ return transferViewModel is { IsVisible: true, TransferType: TransferType.Select };
+ }
+
+ public static async Task TransferAsync(
this TransferViewModel transferViewModel,
- TStorable item,
- Func callback,
+ IEnumerable items,
+ Func, CancellationToken, Task> callback,
CancellationToken cancellationToken = default)
- where TStorable : IStorable
{
- return TransferAsync(transferViewModel, [ item ], callback, cancellationToken);
+ var collection = items.ToOrAsCollection();
+ transferViewModel.IsProgressing = true;
+ transferViewModel.IsVisible = true;
+ transferViewModel.Report(new(0, 0));
+ var counter = 0;
+ var reporter = new Progress(_ =>
+ {
+ counter++;
+ transferViewModel.Report(new(counter, 0));
+ });
+
+ for (var i = 0; i < collection.Count; i++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var item = collection.ElementAt(i);
+ await callback(item, reporter, cancellationToken);
+ }
+
+ transferViewModel.Title = "TransferDone".ToLocalized();
+ await Task.Delay(1000, CancellationToken.None);
+ transferViewModel.IsProgressing = false;
+ transferViewModel.IsVisible = false;
}
-
+
public static async Task TransferAsync(
this TransferViewModel transferViewModel,
IEnumerable items,
@@ -32,17 +60,17 @@ public static async Task TransferAsync(
transferViewModel.IsProgressing = true;
transferViewModel.IsVisible = true;
transferViewModel.Report(new(0, collection.Count));
-
+
for (var i = 0; i < collection.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
-
+
var item = collection.ElementAt(i);
await callback(item, cancellationToken);
transferViewModel.Report(new((i + 1), collection.Count));
}
- await Task.Delay(1000);
+ await Task.Delay(1000, CancellationToken.None);
transferViewModel.IsProgressing = false;
transferViewModel.IsVisible = false;
}
diff --git a/src/SecureFolderFS.Sdk/Helpers/BrowserHelpers.cs b/src/SecureFolderFS.Sdk/Helpers/BrowserHelpers.cs
new file mode 100644
index 000000000..62fdd3b25
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/Helpers/BrowserHelpers.cs
@@ -0,0 +1,30 @@
+using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Sdk.ViewModels;
+using SecureFolderFS.Sdk.ViewModels.Controls.Storage.Browser;
+using SecureFolderFS.Sdk.ViewModels.Controls.Transfer;
+using SecureFolderFS.Sdk.ViewModels.Views.Vault;
+using SecureFolderFS.Shared;
+using SecureFolderFS.Shared.ComponentModel;
+
+namespace SecureFolderFS.Sdk.Helpers
+{
+ public static class BrowserHelpers
+ {
+ public static BrowserViewModel CreateBrowser(UnlockedVaultViewModel unlockedVaultViewModel, INavigator? innerNavigator = null, INavigator? outerNavigator = null)
+ {
+ innerNavigator ??= DI.Service();
+ var rootFolder = unlockedVaultViewModel.StorageRoot.VirtualizedRoot;
+ var browserViewModel = new BrowserViewModel(rootFolder, innerNavigator, outerNavigator, unlockedVaultViewModel.VaultViewModel)
+ {
+ StorageRoot = unlockedVaultViewModel.StorageRoot
+ };
+ var transferViewModel = new TransferViewModel(browserViewModel);
+ var folderViewModel = new FolderViewModel(rootFolder, browserViewModel, null);
+
+ browserViewModel.TransferViewModel = transferViewModel;
+ browserViewModel.CurrentFolder = folderViewModel;
+
+ return browserViewModel;
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/Helpers/FileTypeHelper.cs b/src/SecureFolderFS.Sdk/Helpers/FileTypeHelper.cs
index 705c54698..09dbf4450 100644
--- a/src/SecureFolderFS.Sdk/Helpers/FileTypeHelper.cs
+++ b/src/SecureFolderFS.Sdk/Helpers/FileTypeHelper.cs
@@ -30,10 +30,9 @@ public static TypeHint GetType(IStorable storable)
public static TypeHint GetTypeFromMime(string mimeType)
{
return Image()
- ?? PlainText()
+ ?? Plaintext()
?? Document()
?? Media()
- // TODO
?? TypeHint.Unclassified;
TypeHint? Media()
@@ -59,7 +58,7 @@ public static TypeHint GetTypeFromMime(string mimeType)
? TypeHint.Document : null;
}
- TypeHint? PlainText()
+ TypeHint? Plaintext()
{
return mimeType.StartsWith("text/")
&& !mimeType.Equals("text/csv")
diff --git a/src/SecureFolderFS.Sdk/Helpers/FormattingHelpers.cs b/src/SecureFolderFS.Sdk/Helpers/FormattingHelpers.cs
index c35639c9a..e5754632f 100644
--- a/src/SecureFolderFS.Sdk/Helpers/FormattingHelpers.cs
+++ b/src/SecureFolderFS.Sdk/Helpers/FormattingHelpers.cs
@@ -5,13 +5,19 @@ namespace SecureFolderFS.Sdk.Helpers
{
public static class FormattingHelpers
{
+ private static char[] UniversalInvalidCharacters =
+ {
+ Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, Path.VolumeSeparatorChar,
+ Path.PathSeparator, ':', '>', '<', '"', '?', '*', '!', '|', (char)0x0
+ };
+
public static string SanitizeItemName(string itemName, string fallback)
{
var invalidChars = Path.GetInvalidFileNameChars();
- var sanitizedItemName = new string(itemName.Where(ch => !invalidChars.Contains(ch)).ToArray());
+ var sanitizedItemName = new string(itemName.Where(c => !invalidChars.Contains(c) && !UniversalInvalidCharacters.Contains(c)).ToArray());
// Ensure the sanitized name is not empty and has a minimum length
- if (string.IsNullOrWhiteSpace(sanitizedItemName) || sanitizedItemName.Length < 3)
+ if (string.IsNullOrWhiteSpace(sanitizedItemName))
sanitizedItemName = SanitizeVolumeName(fallback, null);
return sanitizedItemName;
diff --git a/src/SecureFolderFS.Sdk/Helpers/RecycleBinHelpers.cs b/src/SecureFolderFS.Sdk/Helpers/RecycleBinHelpers.cs
new file mode 100644
index 000000000..9ae2ddc6d
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/Helpers/RecycleBinHelpers.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using SecureFolderFS.Sdk.Extensions;
+using SecureFolderFS.Sdk.ViewModels.Controls;
+using static SecureFolderFS.Sdk.Constants.Sizes;
+
+namespace SecureFolderFS.Sdk.Helpers
+{
+ public static class RecycleBinHelpers
+ {
+ public static IEnumerable GetSizeOptions(long totalFreeSpace)
+ {
+ // Always return "No size limit" option
+ yield return new("-1", "NoSizeLimit".ToLocalized());
+
+ // Return in tranches
+
+ // 1GB
+ if (totalFreeSpace >= GIGABYTE)
+ yield return new PickerOptionViewModel(GIGABYTE.ToString(), "1GB");
+
+ // 2GB
+ if (totalFreeSpace >= GIGABYTE * 2L)
+ yield return new PickerOptionViewModel((GIGABYTE * 2L).ToString(), "2GB");
+
+ // 5GB
+ if (totalFreeSpace >= GIGABYTE * 5L)
+ yield return new PickerOptionViewModel((GIGABYTE * 5L).ToString(), "5GB");
+
+ // 10GB
+ if (totalFreeSpace >= GIGABYTE * 10L)
+ yield return new PickerOptionViewModel((GIGABYTE * 10L).ToString(), "10GB");
+
+ // 20GB
+ if (totalFreeSpace >= GIGABYTE * 20L)
+ yield return new PickerOptionViewModel((GIGABYTE * 20L).ToString(), "20GB");
+
+ // 50GB
+ if (totalFreeSpace >= GIGABYTE * 50L)
+ yield return new PickerOptionViewModel((GIGABYTE * 50L).ToString(), "50GB");
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/Helpers/ValidationHelpers.cs b/src/SecureFolderFS.Sdk/Helpers/ValidationHelpers.cs
index 0b5d13680..d749a6bc7 100644
--- a/src/SecureFolderFS.Sdk/Helpers/ValidationHelpers.cs
+++ b/src/SecureFolderFS.Sdk/Helpers/ValidationHelpers.cs
@@ -7,6 +7,7 @@
using SecureFolderFS.Shared.Extensions;
using SecureFolderFS.Shared.Models;
using System;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -14,7 +15,7 @@ namespace SecureFolderFS.Sdk.Helpers
{
public static class ValidationHelpers
{
- public static async Task> ValidateExistingVault(IFolder vaultFolder, CancellationToken cancellationToken)
+ public static async Task> ValidateExistingVault(IFolder vaultFolder, CancellationToken cancellationToken)
{
var vaultService = DI.Service();
var validationResult = await vaultService.VaultValidator.TryValidateAsync(vaultFolder, cancellationToken);
@@ -24,26 +25,55 @@ public static async Task> ValidateExistingVault
if (validationResult.Exception is NotSupportedException)
{
// Allow unsupported vaults to be migrated
- return new MessageResult(SeverityType.Warning, "SelectedMayNotBeSupported".ToLocalized());
+ return new MessageResult(Severity.Warning, "SelectedMayNotBeSupported".ToLocalized());
}
- return new MessageResult(SeverityType.Critical, "SelectedInvalidVault".ToLocalized(), false);
+ return new MessageResult(Severity.Critical, "SelectedInvalidVault".ToLocalized(), false);
}
- return new MessageResult(SeverityType.Success, "SelectedValidVault".ToLocalized());
+ return new MessageResult(Severity.Success, "SelectedValidVault".ToLocalized());
}
- public static async Task> ValidateNewVault(IFolder vaultFolder, CancellationToken cancellationToken)
+ public static async Task> ValidateNewVault(IFolder vaultFolder, CancellationToken cancellationToken)
{
var vaultService = DI.Service();
var validationResult = await vaultService.VaultValidator.TryValidateAsync(vaultFolder, cancellationToken);
if (validationResult.Successful || validationResult.Exception is NotSupportedException)
{
// Check if a valid (or unsupported) vault exists at a specified path
- return new MessageResult(SeverityType.Warning, "SelectedToBeOverwritten".ToLocalized());
+ return new MessageResult(Severity.Warning, "SelectedToBeOverwritten".ToLocalized());
}
- return new MessageResult(SeverityType.Success, "SelectedWillCreate".ToLocalized());
+ return new MessageResult(Severity.Success, "SelectedWillCreate".ToLocalized());
+ }
+
+ public static PasswordStrength ValidatePassword(string password)
+ {
+ if (string.IsNullOrWhiteSpace(password))
+ return PasswordStrength.Blank;
+
+ var score = (uint)PasswordStrength.VeryWeak; // Start at VeryWeak
+
+ if (password.Length >= 8)
+ score++;
+
+ if (password.Length >= 10)
+ score++;
+
+ var hasDigit = password.Any(char.IsDigit);
+ var hasUpper = password.Any(c => char.IsLetter(c) && char.IsUpper(c));
+ var hasLower = password.Any(c => char.IsLetter(c) && char.IsLower(c));
+ var hasSpecial = password.Any(c => !char.IsLetterOrDigit(c));
+
+ if (hasDigit && hasUpper && hasLower)
+ score++;
+
+ if (hasSpecial)
+ score++;
+
+ // Clamp to max enum value
+ score = Math.Min(score, (int)PasswordStrength.VeryStrong);
+ return (PasswordStrength)score;
}
}
}
diff --git a/src/SecureFolderFS.Sdk/SecureFolderFS.Sdk.csproj b/src/SecureFolderFS.Sdk/SecureFolderFS.Sdk.csproj
index 38fbb7677..12bc3632f 100644
--- a/src/SecureFolderFS.Sdk/SecureFolderFS.Sdk.csproj
+++ b/src/SecureFolderFS.Sdk/SecureFolderFS.Sdk.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/src/SecureFolderFS.Sdk/Services/IApplicationService.cs b/src/SecureFolderFS.Sdk/Services/IApplicationService.cs
index 29e800241..c6021c09c 100644
--- a/src/SecureFolderFS.Sdk/Services/IApplicationService.cs
+++ b/src/SecureFolderFS.Sdk/Services/IApplicationService.cs
@@ -15,7 +15,7 @@ public interface IApplicationService
/// Gets the value that determines whether the app is operating on a desktop platform.
///
bool IsDesktop { get; }
-
+
///
/// Gets the name that uniquely identifies a platform.
///
diff --git a/src/SecureFolderFS.Sdk/Services/IChangelogService.cs b/src/SecureFolderFS.Sdk/Services/IChangelogService.cs
index 16e6714eb..675fecc4a 100644
--- a/src/SecureFolderFS.Sdk/Services/IChangelogService.cs
+++ b/src/SecureFolderFS.Sdk/Services/IChangelogService.cs
@@ -6,11 +6,25 @@
namespace SecureFolderFS.Sdk.Services
{
- // TODO: Needs docs
+ ///
+ /// Provides functionality to retrieve changelog information for the application.
+ ///
public interface IChangelogService
{
- Task GetLatestAsync(Version version, string platform, CancellationToken cancellationToken);
+ ///
+ /// Retrieves the latest changelog entry for the specified version.
+ ///
+ /// The version of the application to retrieve the changelog for.
+ /// A that cancels this action.
+ /// A that represents the asynchronous operation. Value is the latest .
+ Task GetLatestAsync(Version version, CancellationToken cancellationToken = default);
- IAsyncEnumerable GetSinceAsync(Version version, string platform, CancellationToken cancellationToken);
+ ///
+ /// Retrieves all changelog entries since the specified version.
+ ///
+ /// The version of the application to start retrieving changelogs from.
+ /// A that cancels this action.
+ /// Returns an async operation represented by of type of the changelog entries.
+ IAsyncEnumerable GetSinceAsync(Version version, CancellationToken cancellationToken = default);
}
}
diff --git a/src/SecureFolderFS.Sdk/Services/IFileExplorerService.cs b/src/SecureFolderFS.Sdk/Services/IFileExplorerService.cs
index f9ce93bbf..1c206a1ba 100644
--- a/src/SecureFolderFS.Sdk/Services/IFileExplorerService.cs
+++ b/src/SecureFolderFS.Sdk/Services/IFileExplorerService.cs
@@ -1,4 +1,5 @@
using OwlCore.Storage;
+using SecureFolderFS.Storage.Pickers;
using System.Collections.Generic;
using System.IO;
using System.Threading;
@@ -9,7 +10,7 @@ namespace SecureFolderFS.Sdk.Services
///
/// A service that interacts with the system file explorer.
///
- public interface IFileExplorerService
+ public interface IFileExplorerService : IFilePicker, IFolderPicker
{
///
/// Tries to open the provided in platform's default file explorer.
@@ -28,22 +29,5 @@ public interface IFileExplorerService
/// A that cancels this action.
/// A that represents the asynchronous operation. If successful, returns true; otherwise false.
Task SaveFileAsync(string suggestedName, Stream dataStream, IDictionary? filter, CancellationToken cancellationToken = default);
-
- ///
- /// Awaits the user input and picks single file from the file explorer dialog.
- ///
- /// The filter to apply when picking files.
- /// Determines whether to persist access to the picked item or not.
- /// A that cancels this action.
- /// A that represents the asynchronous operation. If successful and a file has been picked, returns ; otherwise null.
- Task PickFileAsync(IEnumerable? filter, bool persist = true, CancellationToken cancellationToken = default);
-
- ///
- /// Awaits the user input and picks single folder from the file explorer dialog.
- ///
- /// Determines whether to persist access to the picked item or not.
- /// A that cancels this action.
- /// A that represents the asynchronous operation. If successful and a folder has been picked, returns ; otherwise null.
- Task PickFolderAsync(bool persist = true, CancellationToken cancellationToken = default);
}
}
diff --git a/src/SecureFolderFS.Sdk/Services/IMediaService.cs b/src/SecureFolderFS.Sdk/Services/IMediaService.cs
index 82cd83a25..cfd382033 100644
--- a/src/SecureFolderFS.Sdk/Services/IMediaService.cs
+++ b/src/SecureFolderFS.Sdk/Services/IMediaService.cs
@@ -1,6 +1,7 @@
-using System;
-using OwlCore.Storage;
+using OwlCore.Storage;
using SecureFolderFS.Shared.ComponentModel;
+using System;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -14,8 +15,12 @@ public interface IMediaService
/// The to read.
/// A that cancels this action.
/// A that represents the asynchronous operation. Value is the image read from file.
- Task ReadImageFileAsync(IFile file, CancellationToken cancellationToken);
-
- Task StreamVideoAsync(IFile file, CancellationToken cancellationToken);
+ Task ReadImageFileAsync(IFile file, CancellationToken cancellationToken = default);
+
+ Task GenerateThumbnailAsync(IFile file, CancellationToken cancellationToken = default);
+
+ Task StreamVideoAsync(IFile file, CancellationToken cancellationToken = default);
+
+ Task TrySetFolderIconAsync(IModifiableFolder folder, Stream imageStream, CancellationToken cancellationToken = default);
}
}
diff --git a/src/SecureFolderFS.Sdk/Services/IRecycleBinService.cs b/src/SecureFolderFS.Sdk/Services/IRecycleBinService.cs
new file mode 100644
index 000000000..866402dd3
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/Services/IRecycleBinService.cs
@@ -0,0 +1,45 @@
+using SecureFolderFS.Storage.VirtualFileSystem;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SecureFolderFS.Sdk.Services
+{
+ ///
+ /// Provides functionality to manage and interact with a recycle bin for a vault.
+ ///
+ public interface IRecycleBinService
+ {
+ ///
+ /// Configures the recycle bin for a specific unlocked vault.
+ ///
+ /// The root of the virtual file system.
+ /// The maximum size of the recycle bin in bytes.
+ /// A that cancels this action.
+ /// A that represents the asynchronous operation.
+ Task ConfigureRecycleBinAsync(IVFSRoot vfsRoot, long maxSize, CancellationToken cancellationToken = default);
+
+ ///
+ /// Recalculates missing sizes and updates the configuration file.
+ ///
+ /// The root of the virtual file system.
+ /// A that cancels this action.
+ /// A that represents the asynchronous operation.
+ Task RecalculateSizesAsync(IVFSRoot vfsRoot, CancellationToken cancellationToken = default);
+
+ ///
+ /// Retrieves the recycle bin folder for a specific unlocked vault.
+ ///
+ /// The root of the virtual file system.
+ /// A that cancels this action.
+ /// A that represents the asynchronous operation. Returns an instance.
+ Task GetRecycleBinAsync(IVFSRoot vfsRoot, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets or creates the recycle bin folder for a specific unlocked vault.
+ ///
+ /// The root of the virtual file system.
+ /// A that cancels this action.
+ /// A that represents the asynchronous operation. Returns an instance.
+ Task GetOrCreateRecycleBinAsync(IVFSRoot vfsRoot, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/Services/ISecureStoreService.cs b/src/SecureFolderFS.Sdk/Services/ISecureStoreService.cs
deleted file mode 100644
index 40299f578..000000000
--- a/src/SecureFolderFS.Sdk/Services/ISecureStoreService.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System;
-
-namespace SecureFolderFS.Sdk.Services
-{
- public interface ISecureStoreService // TODO
- {
- object ProtectContract(IDisposable unlockContract);
-
- IDisposable UnprotectContract(object protectedContract);
- }
-}
diff --git a/src/SecureFolderFS.Sdk/Services/IShareService.cs b/src/SecureFolderFS.Sdk/Services/IShareService.cs
new file mode 100644
index 000000000..f2fd6ac4e
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/Services/IShareService.cs
@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+
+namespace SecureFolderFS.Sdk.Services
+{
+ public interface IShareService
+ {
+ Task ShareTextAsync(string text, string title);
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/Services/IStorageService.cs b/src/SecureFolderFS.Sdk/Services/IStorageService.cs
index 114afb4b0..a7b2aac58 100644
--- a/src/SecureFolderFS.Sdk/Services/IStorageService.cs
+++ b/src/SecureFolderFS.Sdk/Services/IStorageService.cs
@@ -12,8 +12,8 @@ public interface IStorageService
///
/// Gets the application folder.
///
- ///
- ///
+ /// A that cancels this action.
+ /// A that represents the asynchronous operation. Value is application-specific instance.
Task GetAppFolderAsync(CancellationToken cancellationToken = default);
///
diff --git a/src/SecureFolderFS.Sdk/Services/ISystemService.cs b/src/SecureFolderFS.Sdk/Services/ISystemService.cs
index be64a7955..3b258bf92 100644
--- a/src/SecureFolderFS.Sdk/Services/ISystemService.cs
+++ b/src/SecureFolderFS.Sdk/Services/ISystemService.cs
@@ -1,4 +1,7 @@
using System;
+using System.Threading;
+using System.Threading.Tasks;
+using OwlCore.Storage;
namespace SecureFolderFS.Sdk.Services
{
@@ -8,8 +11,16 @@ namespace SecureFolderFS.Sdk.Services
public interface ISystemService
{
///
- /// Occurs when the user locks their desktop.
+ /// Occurs when the user locks their device.
///
- event EventHandler? DesktopLocked;
- }
+ event EventHandler? DeviceLocked;
+
+ ///
+ /// Gets the available remaining free space in bytes on the users' device.
+ ///
+ /// The root folder of the given storage cluster to get the size from.
+ /// A that cancels this action.
+ /// A that represents the asynchronous operation. Value is the amount of usable storage space in bytes.
+ Task GetAvailableFreeSpaceAsync(IFolder storageRoot, CancellationToken cancellationToken = default);
+ }
}
diff --git a/src/SecureFolderFS.Sdk/Services/IVaultFileSystemService.cs b/src/SecureFolderFS.Sdk/Services/IVaultFileSystemService.cs
index 19c1751cd..42fb3dfa7 100644
--- a/src/SecureFolderFS.Sdk/Services/IVaultFileSystemService.cs
+++ b/src/SecureFolderFS.Sdk/Services/IVaultFileSystemService.cs
@@ -1,21 +1,16 @@
-using System;
-using SecureFolderFS.Sdk.ViewModels.Controls.Widgets.Health;
-using SecureFolderFS.Shared.ComponentModel;
-using SecureFolderFS.Storage.VirtualFileSystem;
+using SecureFolderFS.Storage.VirtualFileSystem;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using OwlCore.Storage;
namespace SecureFolderFS.Sdk.Services
{
public interface IVaultFileSystemService
{
- public delegate void IssueDelegate(HealthIssueViewModel issueViewModel, IResult result);
-
///
/// Gets the local representation of a file system.
///
+ /// A that cancels this action.
/// A new local file system instance.
Task GetLocalFileSystemAsync(CancellationToken cancellationToken);
@@ -26,18 +21,8 @@ public interface IVaultFileSystemService
/// Returned file systems that are available, may not be supported on this device.
/// Use to check if a given file system is supported.
///
- /// An of type of available file systems.
- IAsyncEnumerable GetFileSystemsAsync(CancellationToken cancellationToken);
-
- ///
- /// Gets the implementation for associated from item validation.
- ///
- /// The result of validation.
- /// The affected storable.
/// A that cancels this action.
- /// A that represents the asynchronous operation. If available, value is ; otherwise false.
- Task GetIssueViewModelAsync(IResult result, IStorableChild storable, CancellationToken cancellationToken = default);
-
- Task ResolveIssuesAsync(IEnumerable issues, IDisposable contractOrRoot, IssueDelegate? issueDelegate, CancellationToken cancellationToken = default);
+ /// An of type of available file systems.
+ IAsyncEnumerable GetFileSystemsAsync(CancellationToken cancellationToken);
}
}
diff --git a/src/SecureFolderFS.Sdk/Services/IVaultHealthService.cs b/src/SecureFolderFS.Sdk/Services/IVaultHealthService.cs
new file mode 100644
index 000000000..4a698cb85
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/Services/IVaultHealthService.cs
@@ -0,0 +1,42 @@
+using OwlCore.Storage;
+using SecureFolderFS.Sdk.ViewModels.Controls.Widgets.Health;
+using SecureFolderFS.Shared.ComponentModel;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SecureFolderFS.Sdk.Services
+{
+ ///
+ /// Provides methods for managing and resolving health issues in a vault.
+ ///
+ public interface IVaultHealthService
+ {
+ ///
+ /// Represents a delegate for handling health issues.
+ ///
+ /// The health issue view model.
+ /// The result associated with the issue.
+ public delegate void IssueDelegate(HealthIssueViewModel issueViewModel, IResult result);
+
+ ///
+ /// Gets the implementation for the associated from item validation.
+ ///
+ /// The result of validation.
+ /// The affected storable item.
+ /// A that cancels this action.
+ /// A that represents the asynchronous operation. If available, the value is a ; otherwise, null.
+ Task GetIssueViewModelAsync(IResult result, IStorableChild storable, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resolves the specified health issues.
+ ///
+ /// The collection of health issues to resolve.
+ /// The disposable contract or root object associated with the resolution process.
+ /// An optional delegate to handle individual issues during resolution.
+ /// A that cancels this action.
+ /// A that represents the asynchronous operation.
+ Task ResolveIssuesAsync(IEnumerable issues, IDisposable contractOrRoot, IssueDelegate? issueDelegate, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/Services/Settings/IUserSettings.cs b/src/SecureFolderFS.Sdk/Services/Settings/IUserSettings.cs
index a1e7e7112..04cf0c23c 100644
--- a/src/SecureFolderFS.Sdk/Services/Settings/IUserSettings.cs
+++ b/src/SecureFolderFS.Sdk/Services/Settings/IUserSettings.cs
@@ -37,6 +37,25 @@ public interface IUserSettings : IPersistable, INotifyPropertyChanged
#endregion
+ #region File Browser
+
+ ///
+ /// Gets or sets the value that determines whether to enable item thumbnails in the file browser.
+ ///
+ bool AreThumbnailsEnabled { get; set; }
+
+ ///
+ /// Gets or set the value that determines whether to show file extensions in the file browser.
+ ///
+ bool AreFileExtensionsEnabled { get; set; }
+
+ ///
+ /// Gets or sets the value that determines whether to enable the file content cache.
+ ///
+ bool IsContentCacheEnabled { get; set; }
+
+ #endregion
+
#region Privacy
///
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Authentication/AuthenticationViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Authentication/AuthenticationViewModel.cs
index ec416626f..fbefdb8c5 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/Authentication/AuthenticationViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Authentication/AuthenticationViewModel.cs
@@ -25,7 +25,7 @@ public abstract partial class AuthenticationViewModel(string id)
///
/// Gets the stage (step) availability of the given authentication type.
///
- public abstract AuthenticationType Availability { get; }
+ public abstract AuthenticationStage Availability { get; }
///
/// Occurs when credentials have been provided by the user.
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Banners/UpdateBannerViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Banners/UpdateBannerViewModel.cs
index e955fe62a..d4b8ecf9a 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/Banners/UpdateBannerViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Banners/UpdateBannerViewModel.cs
@@ -45,7 +45,7 @@ public async Task InitAsync(CancellationToken cancellationToken = default)
{
InfoBarViewModel.IsOpen = true;
InfoBarViewModel.Message = "Updates are not supported for the sideloaded version.";
- InfoBarViewModel.Severity = SeverityType.Warning;
+ InfoBarViewModel.Severity = Severity.Warning;
}
}
@@ -70,7 +70,7 @@ private void UpdateService_StateChanged(object? sender, EventArgs e)
InfoBarViewModel.Title = "Error".ToLocalized();
InfoBarViewModel.IsCloseable = true;
InfoBarViewModel.Message = GetMessageForUpdateState(args.UpdateState);
- InfoBarViewModel.Severity = SeverityType.Critical;
+ InfoBarViewModel.Severity = Severity.Critical;
}
[RelayCommand]
@@ -93,7 +93,7 @@ private async Task UpdateAppAsync(CancellationToken cancellationToken)
InfoBarViewModel.Title = "Error".ToLocalized();
InfoBarViewModel.IsCloseable = true;
InfoBarViewModel.Message = result.GetMessage();
- InfoBarViewModel.Severity = SeverityType.Critical;
+ InfoBarViewModel.Severity = Severity.Critical;
}
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/InfoBarViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/InfoBarViewModel.cs
index 874a3bfd8..573379711 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/InfoBarViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/InfoBarViewModel.cs
@@ -13,6 +13,6 @@ public partial class InfoBarViewModel : ObservableObject, IViewable
[ObservableProperty] private string? _Title;
[ObservableProperty] private string? _Message;
[ObservableProperty] private bool _IsCloseable;
- [ObservableProperty] private SeverityType _Severity;
+ [ObservableProperty] private Severity _Severity;
}
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/LoginViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/LoginViewModel.cs
index f2f9206cd..1bb420587 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/LoginViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/LoginViewModel.cs
@@ -26,7 +26,7 @@ namespace SecureFolderFS.Sdk.ViewModels.Controls
[Bindable(true)]
public sealed partial class LoginViewModel : ObservableObject, IAsyncInitialize, IDisposable
{
- private readonly KeyChain _keyChain;
+ private readonly KeySequence _keySequence;
private readonly IVaultModel _vaultModel;
private readonly LoginViewType _loginViewMode;
private readonly IVaultWatcherModel _vaultWatcherModel;
@@ -39,12 +39,12 @@ public sealed partial class LoginViewModel : ObservableObject, IAsyncInitialize,
public event EventHandler? VaultUnlocked;
- public LoginViewModel(IVaultModel vaultModel, LoginViewType loginViewMode, KeyChain? keyChain = null)
+ public LoginViewModel(IVaultModel vaultModel, LoginViewType loginViewMode, KeySequence? keySequence = null)
{
ServiceProvider = DI.Default;
_loginViewMode = loginViewMode;
_vaultModel = vaultModel;
- _keyChain = keyChain ?? new();
+ _keySequence = keySequence ?? new();
_vaultWatcherModel = new VaultWatcherModel(vaultModel.Folder);
_vaultWatcherModel.StateChanged += VaultWatcherModel_StateChanged;
}
@@ -63,7 +63,7 @@ public async Task InitAsync(CancellationToken cancellationToken = default)
//
// Dispose previous state, if any
- _keyChain.Dispose();
+ _keySequence.Dispose();
_loginSequence?.Dispose();
var validationResult = await VaultService.VaultValidator.TryValidateAsync(_vaultModel.Folder, cancellationToken);
@@ -130,8 +130,8 @@ private async Task RecoverAccessAsync(string? recoveryKey, CancellationToken can
[RelayCommand]
private void RestartLoginProcess()
{
- // Dispose built keychain
- _keyChain.Dispose();
+ // Dispose built key sequence
+ _keySequence.Dispose();
// Reset login sequence only if chain is longer than one authentication
if (_loginSequence?.Count > 1)
@@ -147,7 +147,7 @@ private async Task TryUnlockAsync(CancellationToken cancellationToken = de
{
try
{
- var unlockContract = await VaultManagerService.UnlockAsync(_vaultModel.Folder, _keyChain, cancellationToken);
+ var unlockContract = await VaultManagerService.UnlockAsync(_vaultModel.Folder, _keySequence, cancellationToken);
VaultUnlocked?.Invoke(this, new(unlockContract, _vaultModel.Folder, false));
return true;
}
@@ -207,7 +207,7 @@ private async void CurrentViewModel_StateChanged(object? sender, EventArgs e)
private async void CurrentViewModel_CredentialsProvided(object? sender, CredentialsProvidedEventArgs e)
{
// Add authentication
- _keyChain.Add(e.Authentication);
+ _keySequence.Add(e.Authentication);
var result = ProceedAuthentication();
if (!result.Successful && CurrentViewModel is not ErrorViewModel)
@@ -256,7 +256,7 @@ public void Dispose()
if (CurrentViewModel is AuthenticationViewModel authenticationViewModel)
authenticationViewModel.CredentialsProvided -= CurrentViewModel_CredentialsProvided;
- _keyChain.Dispose();
+ _keySequence.Dispose();
_loginSequence?.Dispose();
}
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/VaultOptionViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/PickerOptionViewModel.cs
similarity index 67%
rename from src/SecureFolderFS.Sdk/ViewModels/VaultOptionViewModel.cs
rename to src/SecureFolderFS.Sdk/ViewModels/Controls/PickerOptionViewModel.cs
index 496fc2db5..f53fd12fe 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/VaultOptionViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/PickerOptionViewModel.cs
@@ -2,13 +2,13 @@
using SecureFolderFS.Shared.ComponentModel;
using System.ComponentModel;
-namespace SecureFolderFS.Sdk.ViewModels
+namespace SecureFolderFS.Sdk.ViewModels.Controls
{
///
- /// Represents a vault option.
+ /// Represents a picker option with a property and a unique ID.
///
[Bindable(true)]
- public sealed class VaultOptionViewModel(string id, string? title = null) : ObservableObject, IViewable
+ public sealed class PickerOptionViewModel(string id, string? title = null) : ObservableObject, IViewable
{
///
/// Gets the unique ID associated with this option.
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/BasePreviewerViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/BasePreviewerViewModel.cs
index 463979d35..a3b8e5fa6 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/BasePreviewerViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/BasePreviewerViewModel.cs
@@ -14,10 +14,10 @@ public abstract partial class BasePreviewerViewModel : ObservableObject
/// Gets the data source of the previewer.
///
[ObservableProperty] private TSource? _Source;
-
+
///
[ObservableProperty] private string? _Title;
-
+
///
public abstract Task InitAsync(CancellationToken cancellationToken = default);
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/FallbackPreviewerViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/FallbackPreviewerViewModel.cs
new file mode 100644
index 000000000..2275ff4ef
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/FallbackPreviewerViewModel.cs
@@ -0,0 +1,32 @@
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+using OwlCore.Storage;
+
+namespace SecureFolderFS.Sdk.ViewModels.Controls.Previewers
+{
+ [Bindable(true)]
+ public sealed class FallbackPreviewerViewModel : BasePreviewerViewModel
+ {
+ private readonly IFile? _file;
+
+ public FallbackPreviewerViewModel(IFile file)
+ {
+ _file = file;
+ Title = _file.Name;
+ Source = _file.Name;
+ }
+
+ public FallbackPreviewerViewModel(string itemName)
+ {
+ Title = itemName;
+ Source = itemName;
+ }
+
+ ///
+ public override Task InitAsync(CancellationToken cancellationToken = default)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/ImagePreviewerViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/ImagePreviewerViewModel.cs
index 0b1ec628a..5be0e2cfe 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/ImagePreviewerViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/ImagePreviewerViewModel.cs
@@ -15,7 +15,7 @@ namespace SecureFolderFS.Sdk.ViewModels.Controls.Previewers
public sealed partial class ImagePreviewerViewModel : BasePreviewerViewModel, IDisposable
{
private readonly IFile? _file;
-
+
public ImagePreviewerViewModel(IFile file)
{
ServiceProvider = DI.Default;
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/VideoPreviewerViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/VideoPreviewerViewModel.cs
index 7162df733..86bf08910 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/VideoPreviewerViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Previewers/VideoPreviewerViewModel.cs
@@ -15,23 +15,23 @@ namespace SecureFolderFS.Sdk.ViewModels.Controls.Previewers
public sealed partial class VideoPreviewerViewModel : BasePreviewerViewModel, IDisposable
{
private readonly IFile _file;
-
+
public VideoPreviewerViewModel(IFile file)
{
_file = file;
ServiceProvider = DI.Default;
}
-
+
///
public override async Task InitAsync(CancellationToken cancellationToken = default)
{
var streamedVideo = await MediaService.StreamVideoAsync(_file, cancellationToken);
if (streamedVideo is IAsyncInitialize asyncInitialize)
await asyncInitialize.InitAsync(cancellationToken);
-
+
Source = streamedVideo;
}
-
+
///
public void Dispose()
{
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/RecoveryPreviewControlViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/RecoveryPreviewControlViewModel.cs
index 12c40503e..2bb4b69cb 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/RecoveryPreviewControlViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/RecoveryPreviewControlViewModel.cs
@@ -1,21 +1,23 @@
-using CommunityToolkit.Mvvm.ComponentModel;
+using System;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using SecureFolderFS.Sdk.Attributes;
+using SecureFolderFS.Sdk.Extensions;
using SecureFolderFS.Sdk.Services;
using SecureFolderFS.Shared;
-using System;
-using System.ComponentModel;
-using System.Threading;
-using System.Threading.Tasks;
+using SecureFolderFS.Shared.ComponentModel;
namespace SecureFolderFS.Sdk.ViewModels.Controls
{
- [Inject, Inject, Inject]
+ [Inject, Inject, Inject, Inject]
[Bindable(true)]
- public sealed partial class RecoveryPreviewControlViewModel : ObservableObject
+ public sealed partial class RecoveryPreviewControlViewModel : ObservableObject, IViewable
{
+ [ObservableProperty] private string? _Title;
[ObservableProperty] private string? _VaultId;
- [ObservableProperty] private string? _VaultName;
[ObservableProperty] private string? _RecoveryKey;
public RecoveryPreviewControlViewModel()
@@ -26,14 +28,16 @@ public RecoveryPreviewControlViewModel()
[RelayCommand]
private async Task ExportAsync(string? exportOption, CancellationToken cancellationToken)
{
- _ = VaultName ?? throw new ArgumentNullException(nameof(VaultName));
+ _ = Title ?? throw new ArgumentNullException(nameof(Title));
+ _ = RecoveryKey ?? throw new ArgumentNullException(nameof(Title));
+
switch (exportOption?.ToLowerInvariant())
{
case "print":
{
await ThreadingService.ChangeThreadAsync();
if (await PrinterService.IsSupportedAsync())
- await PrinterService.PrintRecoveryKeyAsync(VaultName, VaultId, RecoveryKey);
+ await PrinterService.PrintRecoveryKeyAsync(Title, VaultId, RecoveryKey);
break;
}
@@ -41,13 +45,14 @@ private async Task ExportAsync(string? exportOption, CancellationToken cancellat
case "copy":
{
if (await ClipboardService.IsSupportedAsync())
- await ClipboardService.SetTextAsync(RecoveryKey ?? string.Empty, cancellationToken);
+ await ClipboardService.SetTextAsync(RecoveryKey, cancellationToken);
+
break;
}
case "share":
{
- // TODO: Share
+ await ShareService.ShareTextAsync(RecoveryKey, "RecoveryKey".ToLocalized());
break;
}
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/RegisterViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/RegisterViewModel.cs
index 2e11acb9b..8c8eb6bee 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/RegisterViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/RegisterViewModel.cs
@@ -16,7 +16,7 @@ namespace SecureFolderFS.Sdk.ViewModels.Controls
[Bindable(true)]
public sealed partial class RegisterViewModel : ObservableObject, IDisposable
{
- private readonly AuthenticationType _authenticationStage;
+ private readonly AuthenticationStage _authenticationStage;
private bool _credentialsAdded;
[ObservableProperty] private bool _CanContinue;
@@ -30,9 +30,9 @@ public sealed partial class RegisterViewModel : ObservableObject, IDisposable
///
/// Gets the user credentials.
///
- public KeyChain Credentials { get; }
+ public KeySequence Credentials { get; }
- public RegisterViewModel(AuthenticationType authenticationStage, KeyChain? credentials = null)
+ public RegisterViewModel(AuthenticationStage authenticationStage, KeySequence? credentials = null)
{
_authenticationStage = authenticationStage;
Credentials = credentials ?? new();
@@ -47,9 +47,9 @@ public async Task RevokeCredentialsAsync(CancellationToken cancellationToken)
if (CurrentViewModel is null)
return;
-
+
await CurrentViewModel.RevokeAsync(null, cancellationToken);
- Credentials.RemoveAt(_authenticationStage == AuthenticationType.FirstStageOnly ? 0 : 1);
+ Credentials.RemoveAt(_authenticationStage == AuthenticationStage.FirstStageOnly ? 0 : 1);
CanContinue = false;
_credentialsAdded = false;
}
@@ -65,7 +65,7 @@ private void ConfirmCredentials()
// In case the authentication was not reported, try to extract it manually, if possible
if (!_credentialsAdded && CurrentViewModel is IWrapper keyWrapper)
{
- Credentials.SetOrAdd(_authenticationStage == AuthenticationType.FirstStageOnly ? 0 : 1, keyWrapper.Inner);
+ Credentials.SetOrAdd(_authenticationStage == AuthenticationStage.FirstStageOnly ? 0 : 1, keyWrapper.Inner);
_credentialsAdded = true;
}
@@ -102,7 +102,7 @@ async partial void OnCurrentViewModelChanged(AuthenticationViewModel? oldValue,
private void CurrentViewModel_CredentialsProvided(object? sender, CredentialsProvidedEventArgs e)
{
_credentialsAdded = true;
- Credentials.SetOrAdd(_authenticationStage == AuthenticationType.FirstStageOnly ? 0 : 1, e.Authentication);
+ Credentials.SetOrAdd(_authenticationStage == AuthenticationStage.FirstStageOnly ? 0 : 1, e.Authentication);
CanContinue = true;
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/SelectableItemViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/SelectableItemViewModel.cs
new file mode 100644
index 000000000..8bbb1b259
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/SelectableItemViewModel.cs
@@ -0,0 +1,18 @@
+using System.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using SecureFolderFS.Shared.ComponentModel;
+
+namespace SecureFolderFS.Sdk.ViewModels.Controls
+{
+ [Bindable(true)]
+ public abstract partial class SelectableItemViewModel : ObservableObject, IViewable
+ {
+ ///
+ [ObservableProperty] private string? _Title;
+
+ ///
+ /// Gets or sets the value indicating whether the item is selected in view.
+ ///
+ [ObservableProperty] private bool _IsSelected;
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/BrowserItemViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/BrowserItemViewModel.cs
new file mode 100644
index 000000000..99bf7c082
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/BrowserItemViewModel.cs
@@ -0,0 +1,347 @@
+using CommunityToolkit.Mvvm.Input;
+using OwlCore.Storage;
+using SecureFolderFS.Sdk.AppModels;
+using SecureFolderFS.Sdk.Attributes;
+using SecureFolderFS.Sdk.Enums;
+using SecureFolderFS.Sdk.Extensions;
+using SecureFolderFS.Sdk.Helpers;
+using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Sdk.ViewModels.Views.Overlays;
+using SecureFolderFS.Sdk.ViewModels.Views.Vault;
+using SecureFolderFS.Shared;
+using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Shared.Extensions;
+using SecureFolderFS.Storage.Extensions;
+using SecureFolderFS.Storage.Recyclable;
+using SecureFolderFS.Storage.Renamable;
+using SecureFolderFS.Storage.StorageProperties;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SecureFolderFS.Sdk.ViewModels.Controls.Storage.Browser
+{
+ [Inject, Inject, Inject]
+ [Bindable(true)]
+ public abstract partial class BrowserItemViewModel : StorageItemViewModel, IAsyncInitialize
+ {
+ ///
+ /// Gets the instance, which this item belongs to.
+ ///
+ public BrowserViewModel BrowserViewModel { get; }
+
+ ///
+ /// Gets the parent that this item resides in, if any.
+ ///
+ public FolderViewModel? ParentFolder { get; }
+
+ protected BrowserItemViewModel(BrowserViewModel browserViewModel, FolderViewModel? parentFolder)
+ {
+ ServiceProvider = DI.Default;
+ BrowserViewModel = browserViewModel;
+ ParentFolder = parentFolder;
+ }
+
+ ///
+ public abstract Task InitAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Updates the storable instance of this item.
+ ///
+ /// The new storable object to use.
+ protected abstract void UpdateStorable(IStorable storable);
+
+ [RelayCommand]
+ protected virtual async Task OpenPropertiesAsync(CancellationToken cancellationToken)
+ {
+ if (Inner is not IStorableProperties storableProperties)
+ return;
+
+ var properties = await storableProperties.GetPropertiesAsync();
+ var propertiesOverlay = new PropertiesOverlayViewModel(Inner, properties);
+ _ = propertiesOverlay.InitAsync(cancellationToken);
+
+ await OverlayService.ShowAsync(propertiesOverlay);
+ }
+
+ [RelayCommand]
+ protected virtual async Task MoveAsync(CancellationToken cancellationToken)
+ {
+ if (ParentFolder is null)
+ return;
+
+ if (BrowserViewModel.TransferViewModel is not { IsProgressing: false } transferViewModel || ParentFolder.Folder is not IModifiableFolder modifiableParent)
+ return;
+
+ var items = BrowserViewModel.IsSelecting ? ParentFolder.Items.GetSelectedItems().ToArray() : [];
+ if (items.IsEmpty())
+ items = [ this ];
+
+ try
+ {
+ // Disable selection, if called with selected items
+ BrowserViewModel.IsSelecting = false;
+
+ using var cts = transferViewModel.GetCancellation();
+ var destination = await transferViewModel.PickFolderAsync(new TransferOptions(TransferType.Move), false, cts.Token);
+ if (destination is not IModifiableFolder destinationFolder)
+ return;
+
+ // Workaround for the fact that the returned folder is IFolder and not FolderViewModel
+ // TODO: Check consequences of this where the CurrentFolder might differ from the actual picked folder
+ var destinationViewModel = BrowserViewModel.CurrentFolder;
+ if (destinationViewModel is null)
+ return;
+
+ if (items.Any(item => destination.Id.Contains(item.Inner.Id, StringComparison.InvariantCultureIgnoreCase)))
+ return;
+
+ await transferViewModel.TransferAsync(items.Select(x => (IStorableChild)x.Inner), async (item, reporter, token) =>
+ {
+ // Move
+ var movedItem = await destinationFolder.MoveStorableFromAsync(item, modifiableParent, false, reporter, token);
+
+ // Remove existing from folder
+ ParentFolder.Items.RemoveMatch(x => x.Inner.Id == item.Id)?.Dispose();
+
+ // Add to destination
+ destinationViewModel.Items.Insert(movedItem switch
+ {
+ IFile file => new FileViewModel(file, BrowserViewModel, destinationViewModel),
+ IFolder folder => new FolderViewModel(folder, ParentFolder.BrowserViewModel, destinationViewModel),
+ _ => throw new ArgumentOutOfRangeException(nameof(movedItem))
+ }, BrowserViewModel.ViewOptions.GetSorter());
+ }, cts.Token);
+ }
+ catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
+ {
+ _ = ex;
+ // TODO: Report error
+ }
+ finally
+ {
+ transferViewModel.IsVisible = false;
+ transferViewModel.IsProgressing = false;
+ }
+ }
+
+ [RelayCommand]
+ protected virtual async Task CopyAsync(CancellationToken cancellationToken)
+ {
+ if (ParentFolder is null)
+ return;
+
+ if (BrowserViewModel.TransferViewModel is not { IsProgressing: false } transferViewModel)
+ return;
+
+ var items = BrowserViewModel.IsSelecting ? ParentFolder.Items.GetSelectedItems().ToArray() : [];
+ if (items.IsEmpty())
+ items = [ this ];
+
+ try
+ {
+ // Disable selection, if called with selected items
+ BrowserViewModel.IsSelecting = false;
+
+ using var cts = transferViewModel.GetCancellation();
+ var destination = await transferViewModel.PickFolderAsync(new TransferOptions(TransferType.Copy), false, cts.Token);
+ if (destination is not IModifiableFolder modifiableDestination)
+ return;
+
+ // Workaround for the fact that the returned folder is IFolder and not FolderViewModel
+ // TODO: Check consequences of this where the CurrentFolder might differ from the actual picked folder
+ var destinationViewModel = BrowserViewModel.CurrentFolder;
+ if (destinationViewModel is null)
+ return;
+
+ if (items.Any(item => destination.Id.Contains(item.Inner.Id, StringComparison.InvariantCultureIgnoreCase)))
+ return;
+
+ await transferViewModel.TransferAsync(items.Select(x => x.Inner), async (item, reporter, token) =>
+ {
+ // Copy
+ var copiedItem = await modifiableDestination.CreateCopyOfStorableAsync(item, false, reporter, token);
+
+ // Add to destination
+ destinationViewModel.Items.Insert(copiedItem switch
+ {
+ IFile file => new FileViewModel(file, BrowserViewModel, destinationViewModel),
+ IFolder folder => new FolderViewModel(folder, BrowserViewModel, destinationViewModel),
+ _ => throw new ArgumentOutOfRangeException(nameof(copiedItem))
+ }, BrowserViewModel.ViewOptions.GetSorter());
+ }, cts.Token);
+ }
+ catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
+ {
+ _ = ex;
+ // TODO: Report error
+ }
+ finally
+ {
+ transferViewModel.IsVisible = false;
+ transferViewModel.IsProgressing = false;
+ }
+ }
+
+ [RelayCommand]
+ protected virtual async Task RenameAsync(CancellationToken cancellationToken)
+ {
+ if (ParentFolder?.Folder is not IRenamableFolder renamableFolder)
+ return;
+
+ if (Inner is not IStorableChild innerChild)
+ return;
+
+ try
+ {
+ var viewModel = new RenameOverlayViewModel("Rename".ToLocalized()) { Message = "ChooseNewName".ToLocalized(), NewName = Inner.Name };
+ var result = await OverlayService.ShowAsync(viewModel);
+ if (!result.Positive())
+ return;
+
+ if (string.IsNullOrWhiteSpace(viewModel.NewName))
+ return;
+
+ var formattedName = FormattingHelpers.SanitizeItemName(viewModel.NewName, "item");
+ if (!Path.HasExtension(formattedName))
+ formattedName = $"{formattedName}{Path.GetExtension(innerChild.Name)}";
+
+ var existingItem = await renamableFolder.TryGetFirstByNameAsync(formattedName, cancellationToken);
+ if (existingItem is not null)
+ {
+ // TODO: Report that the item already exists
+ return;
+ }
+
+ var renamedStorable = await renamableFolder.RenameAsync(innerChild, formattedName, cancellationToken);
+ Title = formattedName;
+ UpdateStorable(renamedStorable);
+ _ = InitAsync(cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ // TODO: Report error
+ _ = ex;
+ }
+ }
+
+ [RelayCommand]
+ protected virtual async Task DeleteAsync(CancellationToken cancellationToken)
+ {
+ if (ParentFolder is null)
+ return;
+
+ // TODO: If moving to trash, show TransferViewModel (with try..catch..finally), otherwise don't show anything
+ var items = BrowserViewModel.IsSelecting ? ParentFolder.Items.GetSelectedItems().ToArray() : [];
+ if (items.IsEmpty())
+ items = [ this ];
+
+ // Disable selection, if called with selected items
+ BrowserViewModel.IsSelecting = false;
+
+ if (BrowserViewModel.StorageRoot.Options.IsRecycleBinEnabled())
+ {
+ if (ParentFolder?.Folder is not IRecyclableFolder recyclableFolder)
+ return;
+
+ var recycleBin = await RecycleBinService.TryGetOrCreateRecycleBinAsync(BrowserViewModel.StorageRoot, cancellationToken);
+ if (recycleBin is null)
+ return;
+
+ var sizes = new List();
+ foreach (var item in items)
+ {
+ sizes.Add(item.Inner switch
+ {
+ IFile file => await file.GetSizeAsync(cancellationToken),
+ IFolder folder => await folder.GetSizeAsync(cancellationToken),
+ _ => 0L
+ });
+ }
+
+ if (BrowserViewModel.StorageRoot.Options.IsRecycleBinUnlimited())
+ {
+ for (var i = 0; i < items.Length; i++)
+ {
+ var item = items[i];
+ await recyclableFolder.DeleteAsync((IStorableChild)item.Inner, sizes[i], false, cancellationToken);
+ ParentFolder?.Items.RemoveAndGet(item)?.Dispose();
+ }
+ }
+ else
+ {
+ var occupiedSize = await recycleBin.GetSizeAsync(cancellationToken);
+ var availableSize = BrowserViewModel.StorageRoot.Options.RecycleBinSize - occupiedSize;
+ if (availableSize < sizes.Sum())
+ {
+ // TODO: Show an overlay telling the user there's not enough space and the items will be deleted permanently
+ for (var i = 0; i < items.Length; i++)
+ {
+ var item = items[i];
+ await recyclableFolder.DeleteAsync((IStorableChild)item.Inner, sizes[i], true, cancellationToken);
+ ParentFolder.Items.RemoveAndGet(item)?.Dispose();
+ }
+ }
+ else
+ {
+ for (var i = 0; i < items.Length; i++)
+ {
+ var item = items[i];
+ await recyclableFolder.DeleteAsync((IStorableChild)item.Inner, sizes[i], false, cancellationToken);
+ ParentFolder.Items.RemoveAndGet(item)?.Dispose();
+ }
+ }
+ }
+ }
+ else
+ {
+ if (ParentFolder?.Folder is not IModifiableFolder modifiableFolder)
+ return;
+
+ // TODO: Show an overlay to ask the user **when deleting permanently**
+
+ foreach (var item in items)
+ {
+ await modifiableFolder.DeleteAsync((IStorableChild)item.Inner, cancellationToken);
+ ParentFolder.Items.RemoveAndGet(item)?.Dispose();
+ }
+ }
+ }
+
+ [RelayCommand]
+ protected virtual async Task ExportAsync(CancellationToken cancellationToken)
+ {
+ if (ParentFolder?.Folder is not IModifiableFolder parentModifiableFolder)
+ return;
+
+ if (BrowserViewModel.TransferViewModel is not { IsProgressing: false } transferViewModel)
+ return;
+
+ var items = BrowserViewModel.IsSelecting ? ParentFolder.Items.GetSelectedItems().ToArray() : [];
+ if (items.IsEmpty())
+ items = [ this ];
+
+ var destination = await FileExplorerService.PickFolderAsync(null, false, cancellationToken);
+ if (destination is not IModifiableFolder destinationFolder)
+ return;
+
+ transferViewModel.TransferType = TransferType.Move;
+ using var cts = transferViewModel.GetCancellation();
+ await transferViewModel.TransferAsync(items.Select(x => x.Inner), async (item, reporter, token) =>
+ {
+ // Copy and delete
+ await destinationFolder.CreateCopyOfStorableAsync(item, false, reporter, cancellationToken);
+ await parentModifiableFolder.DeleteAsync((IStorableChild)item, cancellationToken);
+
+ ParentFolder.Items.RemoveMatch(x => x.Inner.Id == item.Id)?.Dispose();
+ }, cts.Token);
+ }
+
+ [RelayCommand]
+ protected abstract Task OpenAsync(CancellationToken cancellationToken);
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Browser/FileViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/FileViewModel.cs
similarity index 53%
rename from src/SecureFolderFS.Sdk/ViewModels/Views/Browser/FileViewModel.cs
rename to src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/FileViewModel.cs
index d7eacc609..b7b8d8a28 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Browser/FileViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/FileViewModel.cs
@@ -1,15 +1,18 @@
-using OwlCore.Storage;
+using System.ComponentModel;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using OwlCore.Storage;
using SecureFolderFS.Sdk.Attributes;
+using SecureFolderFS.Sdk.Extensions;
using SecureFolderFS.Sdk.Services;
using SecureFolderFS.Sdk.ViewModels.Views.Overlays;
+using SecureFolderFS.Sdk.ViewModels.Views.Vault;
using SecureFolderFS.Shared;
-using System.ComponentModel;
-using System.Threading;
-using System.Threading.Tasks;
-namespace SecureFolderFS.Sdk.ViewModels.Views.Browser
+namespace SecureFolderFS.Sdk.ViewModels.Controls.Storage.Browser
{
- [Inject]
+ [Inject, Inject, Inject]
[Bindable(true)]
public partial class FileViewModel : BrowserItemViewModel
{
@@ -21,19 +24,23 @@ public partial class FileViewModel : BrowserItemViewModel
///
public IFile File { get; protected set; }
- public FileViewModel(IFile file, FolderViewModel? parentFolder)
- : base(parentFolder)
+ public FileViewModel(IFile file, BrowserViewModel browserViewModel, FolderViewModel? parentFolder)
+ : base(browserViewModel, parentFolder)
{
ServiceProvider = DI.Default;
File = file;
- Title = file.Name;
+ Title = !SettingsService.UserSettings.AreFileExtensionsEnabled
+ ? Path.GetFileNameWithoutExtension(file.Name)
+ : file.Name;
}
///
- public override Task InitAsync(CancellationToken cancellationToken = default)
+ public override async Task InitAsync(CancellationToken cancellationToken = default)
{
- // TODO: Load thumbnail
- return Task.CompletedTask;
+ Thumbnail?.Dispose();
+
+ if (SettingsService.UserSettings.AreThumbnailsEnabled)
+ Thumbnail = await MediaService.GenerateThumbnailAsync(File, cancellationToken);
}
///
@@ -45,6 +52,9 @@ protected override void UpdateStorable(IStorable storable)
///
protected override async Task OpenAsync(CancellationToken cancellationToken)
{
+ if (BrowserViewModel.TransferViewModel?.IsPickingItems() ?? false)
+ return;
+
using var viewModel = new PreviewerOverlayViewModel();
await viewModel.LoadFromStorableAsync(Inner, cancellationToken);
await OverlayService.ShowAsync(viewModel);
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Browser/FolderViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/FolderViewModel.cs
similarity index 67%
rename from src/SecureFolderFS.Sdk/ViewModels/Views/Browser/FolderViewModel.cs
rename to src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/FolderViewModel.cs
index 5fd684d75..d05c27cd4 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Browser/FolderViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/FolderViewModel.cs
@@ -1,22 +1,19 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OwlCore.Storage;
+using SecureFolderFS.Sdk.ViewModels.Views.Vault;
using SecureFolderFS.Shared.ComponentModel;
using SecureFolderFS.Shared.Extensions;
-namespace SecureFolderFS.Sdk.ViewModels.Views.Browser
+namespace SecureFolderFS.Sdk.ViewModels.Controls.Storage.Browser
{
[Bindable(true)]
public partial class FolderViewModel : BrowserItemViewModel, IViewDesignation
{
- ///
- /// Gets the instance, which this folder belongs to.
- ///
- public BrowserViewModel BrowserViewModel { get; }
-
///
/// Gets the folder associated with this view model.
///
@@ -31,10 +28,9 @@ public partial class FolderViewModel : BrowserItemViewModel, IViewDesignation
public override IStorable Inner => Folder;
public FolderViewModel(IFolder folder, BrowserViewModel browserViewModel, FolderViewModel? parentFolder)
- : base(parentFolder)
+ : base(browserViewModel, parentFolder)
{
Folder = folder;
- BrowserViewModel = browserViewModel;
Title = folder.Name;
Items = new();
}
@@ -48,18 +44,18 @@ public override Task InitAsync(CancellationToken cancellationToken = default)
public async Task ListContentsAsync(CancellationToken cancellationToken = default)
{
+ Items.DisposeElements();
Items.Clear();
- await foreach (var item in Folder.GetItemsAsync(StorableType.All, cancellationToken))
+
+ var items = await Folder.GetItemsAsync(StorableType.All, cancellationToken).ToArrayAsync(cancellationToken: cancellationToken);
+ BrowserViewModel.ViewOptions.GetSorter().SortCollection(items.Select(x => (BrowserItemViewModel)(x switch
{
- Items.Add(item switch
- {
- IFile file => new FileViewModel(file, this).WithInitAsync(),
- IFolder folder => new FolderViewModel(folder, BrowserViewModel, this).WithInitAsync(),
- _ => throw new ArgumentOutOfRangeException(nameof(item))
- });
- }
+ IFile file => new FileViewModel(file, BrowserViewModel, this),
+ IFolder folder => new FolderViewModel(folder, BrowserViewModel, this),
+ _ => throw new ArgumentOutOfRangeException(nameof(x))
+ })), Items);
}
-
+
///
public virtual void OnAppearing()
{
@@ -68,8 +64,9 @@ public virtual void OnAppearing()
///
public virtual void OnDisappearing()
{
+ Items.DisposeElements();
}
-
+
///
protected override void UpdateStorable(IStorable storable)
{
@@ -81,8 +78,8 @@ protected override async Task OpenAsync(CancellationToken cancellationToken)
{
if (Items.IsEmpty())
_ = ListContentsAsync(cancellationToken);
-
- await BrowserViewModel.Navigator.NavigateAsync(this);
+
+ await BrowserViewModel.InnerNavigator.NavigateAsync(this);
}
}
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/RecycleBinItemViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/RecycleBinItemViewModel.cs
new file mode 100644
index 000000000..dfa14745e
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/RecycleBinItemViewModel.cs
@@ -0,0 +1,92 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using OwlCore.Storage;
+using SecureFolderFS.Sdk.Attributes;
+using SecureFolderFS.Sdk.Extensions;
+using SecureFolderFS.Sdk.Helpers;
+using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Sdk.ViewModels.Views.Overlays;
+using SecureFolderFS.Shared;
+using SecureFolderFS.Shared.Extensions;
+using SecureFolderFS.Storage.Extensions;
+using SecureFolderFS.Storage.Pickers;
+using SecureFolderFS.Storage.VirtualFileSystem;
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SecureFolderFS.Sdk.ViewModels.Controls.Storage
+{
+ [Bindable(true)]
+ [Inject, Inject]
+ public sealed partial class RecycleBinItemViewModel : StorageItemViewModel
+ {
+ private readonly IRecycleBinFolder _recycleBin;
+
+ [ObservableProperty] private string? _OriginalPath;
+ [ObservableProperty] private DateTime? _DeletionTimestamp;
+ [ObservableProperty] private RecycleBinOverlayViewModel _OverlayViewModel;
+
+ ///
+ public override IStorable Inner { get; }
+
+ public RecycleBinItemViewModel(RecycleBinOverlayViewModel overlayViewModel, IRecycleBinItem recycleBinItem, IRecycleBinFolder recycleBin)
+ {
+ ServiceProvider = DI.Default;
+ OverlayViewModel = overlayViewModel;
+ Inner = recycleBinItem.Inner;
+ Title = recycleBinItem.Name;
+ OriginalPath = recycleBinItem.Id;
+ DeletionTimestamp = recycleBinItem.DeletionTimestamp;
+ _recycleBin = recycleBin;
+ }
+
+ [RelayCommand]
+ private async Task RestoreAsync(CancellationToken cancellationToken)
+ {
+ var items = OverlayViewModel.IsSelecting ? OverlayViewModel.Items.GetSelectedItems().ToArray() : [];
+ if (items.IsEmpty())
+ items = [ this ];
+
+ IFolderPicker folderPicker = ApplicationService.IsDesktop
+ ? DI.Service()
+ : BrowserHelpers.CreateBrowser(OverlayViewModel.UnlockedVaultViewModel, outerNavigator: OverlayViewModel.OuterNavigator);
+
+ if (await _recycleBin.TryRestoreItemsAsync(items.Select(x => x.Inner as IStorableChild)!, folderPicker, cancellationToken))
+ {
+ foreach (var item in items)
+ OverlayViewModel.Items.Remove(item);
+ }
+
+ OverlayViewModel.ToggleSelectionCommand.Execute(false);
+ }
+
+ [RelayCommand]
+ private async Task DeletePermanentlyAsync(CancellationToken cancellationToken)
+ {
+ var items = OverlayViewModel.IsSelecting ? OverlayViewModel.Items.GetSelectedItems().ToArray() : [];
+ if (items.IsEmpty())
+ items = [ this ];
+
+ foreach (var item in items)
+ {
+ if (item.Inner is not IStorableChild innerChild)
+ continue;
+
+ try
+ {
+ await _recycleBin.DeleteAsync(innerChild, cancellationToken);
+ OverlayViewModel.Items.Remove(item);
+ }
+ catch (Exception ex)
+ {
+ _ = ex;
+ }
+ }
+
+ OverlayViewModel.ToggleSelectionCommand.Execute(false);
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/StorageItemViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/StorageItemViewModel.cs
new file mode 100644
index 000000000..4fccc74ee
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/StorageItemViewModel.cs
@@ -0,0 +1,27 @@
+using System;
+using System.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using OwlCore.Storage;
+using SecureFolderFS.Shared.ComponentModel;
+
+namespace SecureFolderFS.Sdk.ViewModels.Controls.Storage
+{
+ [Bindable(true)]
+ public abstract partial class StorageItemViewModel : SelectableItemViewModel, IWrapper, IDisposable
+ {
+ ///
+ /// Gets or sets the thumbnail image represented by of this storage item, if any.
+ ///
+ [ObservableProperty] private IImage? _Thumbnail;
+
+ ///
+ public abstract IStorable Inner { get; }
+
+ ///
+ public virtual void Dispose()
+ {
+ Thumbnail?.Dispose();
+ Thumbnail = null;
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/ViewOptionsViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/ViewOptionsViewModel.cs
new file mode 100644
index 000000000..4d26c997c
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Storage/ViewOptionsViewModel.cs
@@ -0,0 +1,116 @@
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using SecureFolderFS.Sdk.AppModels.Sorters;
+using SecureFolderFS.Sdk.Enums;
+using SecureFolderFS.Sdk.Extensions;
+using SecureFolderFS.Sdk.ViewModels.Controls.Storage.Browser;
+using SecureFolderFS.Shared.ComponentModel;
+
+namespace SecureFolderFS.Sdk.ViewModels.Controls.Storage
+{
+ [Bindable(true)]
+ public sealed partial class ViewOptionsViewModel : ObservableObject, IViewable
+ {
+ [ObservableProperty] private ObservableCollection _SortOptions;
+ [ObservableProperty] private ObservableCollection _SizeOptions;
+ [ObservableProperty] private ObservableCollection _ViewOptions;
+ [ObservableProperty] private PickerOptionViewModel? _CurrentSortOption;
+ [ObservableProperty] private PickerOptionViewModel? _CurrentSizeOption;
+ [ObservableProperty] private PickerOptionViewModel? _CurrentViewOption;
+ [ObservableProperty] private BrowserViewType _BrowserViewType;
+ [ObservableProperty] private bool _AreSizeOptionsAvailable;
+ [ObservableProperty] private bool _IsAscending;
+ [ObservableProperty] private string? _Title;
+
+ public ViewOptionsViewModel()
+ {
+ SortOptions = new([
+ new(nameof(NameSorter), "Name".ToLocalized()),
+ new(nameof(KindSorter), "Kind".ToLocalized()),
+ new(nameof(SizeSorter), "Size".ToLocalized())
+ ]);
+ SizeOptions = new([
+ new("Small", "SmallSize".ToLocalized()),
+ new("Medium", "MediumSize".ToLocalized()),
+ new("Large", "LargeSize".ToLocalized())
+ ]);
+ ViewOptions = new([
+ new(nameof(BrowserViewType.ListView), "ListViewLayout".ToLocalized()),
+ new(nameof(Enums.BrowserViewType.ColumnView), "ColumnViewLayout".ToLocalized()),
+ new("GridView", "GridViewLayout".ToLocalized()),
+ new("GalleryView", "GalleryViewLayout".ToLocalized())
+ ]);
+
+ IsAscending = true;
+ CurrentSortOption = SortOptions[0];
+ CurrentSizeOption = SizeOptions[1];
+ CurrentViewOption = ViewOptions[0];
+ Title = "ViewOptions".ToLocalized();
+ }
+
+ public IItemSorter GetSorter()
+ {
+ return CurrentSortOption?.Id switch
+ {
+ nameof(NameSorter) => IsAscending ? NameSorter.Ascending : NameSorter.Descending,
+ nameof(KindSorter) => IsAscending ? KindSorter.Ascending : KindSorter.Descending,
+ nameof(SizeSorter) => IsAscending ? SizeSorter.Ascending : SizeSorter.Descending,
+ _ => NameSorter.Descending
+ };
+ }
+
+ partial void OnCurrentViewOptionChanged(PickerOptionViewModel? value)
+ {
+ switch (value?.Id)
+ {
+ case nameof(BrowserViewType.ListView):
+ BrowserViewType = BrowserViewType.ListView;
+ break;
+
+ case nameof(BrowserViewType.ColumnView):
+ BrowserViewType = BrowserViewType.ColumnView;
+ break;
+ }
+
+ SetLayoutSizeOption(CurrentSizeOption);
+ }
+
+ partial void OnCurrentSizeOptionChanged(PickerOptionViewModel? value)
+ {
+ SetLayoutSizeOption(value);
+ }
+
+ private bool SetLayoutSizeOption(PickerOptionViewModel? value)
+ {
+ switch (CurrentViewOption?.Id)
+ {
+ case "GridView":
+ AreSizeOptionsAvailable = true;
+ BrowserViewType = value?.Id switch
+ {
+ "Small" => BrowserViewType.SmallGridView,
+ "Medium" => BrowserViewType.MediumGridView,
+ "Large" => BrowserViewType.LargeGridView,
+ _ => BrowserViewType
+ };
+ return true;
+
+ case "GalleryView":
+ AreSizeOptionsAvailable = true;
+ BrowserViewType = value?.Id switch
+ {
+ "Small" => BrowserViewType.SmallGalleryView,
+ "Medium" => BrowserViewType.MediumGalleryView,
+ "Large" => BrowserViewType.LargeGalleryView,
+ _ => BrowserViewType
+ };
+ return true;
+
+ default:
+ AreSizeOptionsAvailable = false;
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Transfer/TransferViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Transfer/TransferViewModel.cs
index 4fafb900b..eb138ead3 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/Transfer/TransferViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Transfer/TransferViewModel.cs
@@ -1,24 +1,28 @@
+using System;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
+using OwlCore.Storage;
using SecureFolderFS.Sdk.AppModels;
using SecureFolderFS.Sdk.Enums;
-using SecureFolderFS.Sdk.ViewModels.Views.Browser;
+using SecureFolderFS.Sdk.Extensions;
+using SecureFolderFS.Sdk.ViewModels.Views.Vault;
using SecureFolderFS.Shared.ComponentModel;
-using System;
-using System.ComponentModel;
-using System.Threading;
-using System.Threading.Tasks;
+using SecureFolderFS.Storage.Pickers;
namespace SecureFolderFS.Sdk.ViewModels.Controls.Transfer
{
[Bindable(true)]
- public sealed partial class TransferViewModel : ObservableObject, IViewable, IProgress
+ public sealed partial class TransferViewModel : ObservableObject, IViewable, IProgress, IFolderPicker
{
private readonly BrowserViewModel _browserViewModel;
- private TaskCompletionSource? _tcs;
+ private TaskCompletionSource? _tcs;
private CancellationTokenSource? _cts;
-
+
[ObservableProperty] private string? _Title;
+ [ObservableProperty] private bool _CanCancel;
[ObservableProperty] private bool _IsVisible;
[ObservableProperty] private bool _IsProgressing;
[ObservableProperty] private TransferType _TransferType;
@@ -31,12 +35,12 @@ public TransferViewModel(BrowserViewModel browserViewModel)
///
public void Report(TotalProgress value)
{
- if (value.Achieved >= value.Total)
+ if (value.Achieved >= value.Total && value.Total > 0)
{
- Title = "Done";
+ Title = "TransferDone".ToLocalized();
return;
}
-
+
Title = $"{TransferType switch
{
TransferType.Copy => "Copying",
@@ -44,41 +48,49 @@ public void Report(TotalProgress value)
_ => string.Empty,
}} {GetItemsCount(value)} item(s)";
+ return;
+
static string GetItemsCount(TotalProgress totalProgress)
{
return totalProgress switch
{
{ Achieved: < 0 } => totalProgress.Total.ToString(),
- { Total: < 0 } => totalProgress.Achieved.ToString(),
+ { Total: <= 0 } => totalProgress.Achieved.ToString(),
_ => $"{totalProgress.Achieved}/{totalProgress.Total}"
};
}
}
- public async Task SelectFolderAsync(TransferType transferType, CancellationTokenSource? cts)
+ public CancellationTokenSource GetCancellation()
{
- try
- {
- _tcs?.TrySetCanceled();
- _tcs = new();
- _cts = cts;
- TransferType = transferType;
- Title = "Choose destination folder";
- IsVisible = true;
- return await _tcs.Task;
- }
- finally
- {
- _tcs = null;
- _cts = null;
- }
+ _cts?.Dispose();
+ _cts = new CancellationTokenSource();
+
+ CanCancel = true;
+ return _cts;
+ }
+
+ ///
+ public Task PickFolderAsync(PickerOptions? options, bool offerPersistence = true, CancellationToken cancellationToken = default)
+ {
+ _tcs?.TrySetCanceled(CancellationToken.None);
+ _tcs = new TaskCompletionSource();
+
+ if (options is TransferOptions transferOptions)
+ TransferType = transferOptions.TransferType;
+
+ Title = "ChooseDestinationFolder".ToLocalized();
+ IsProgressing = false;
+ IsVisible = true;
+
+ return _tcs.Task;
}
[RelayCommand]
private void Confirm()
{
// Only used for confirming the destination folder
- _tcs?.TrySetResult(_browserViewModel.CurrentFolder);
+ _tcs?.TrySetResult(_browserViewModel.CurrentFolder?.Folder);
}
[RelayCommand]
@@ -86,17 +98,14 @@ private async Task CancelAsync()
{
if (_tcs is not null)
{
- _tcs?.TrySetCanceled();
+ _tcs.TrySetCanceled(CancellationToken.None);
IsVisible = false;
- return;
}
-
- // Cancel here using the provided CancellationTokenSource
- if (_cts is not null)
+ else if (_cts is not null)
{
- Title = "Cancelling";
+ CanCancel = false;
+ Title = "Cancelling".ToLocalized();
await _cts.CancelAsync();
-
IsVisible = false;
}
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultControlsViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultControlsViewModel.cs
index 13aee4f38..6c4fcadfe 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultControlsViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultControlsViewModel.cs
@@ -9,7 +9,6 @@
using SecureFolderFS.Sdk.Extensions;
using SecureFolderFS.Sdk.Messages;
using SecureFolderFS.Sdk.Services;
-using SecureFolderFS.Sdk.ViewModels.Views.Browser;
using SecureFolderFS.Sdk.ViewModels.Views.Vault;
using SecureFolderFS.Shared;
using SecureFolderFS.Shared.ComponentModel;
@@ -55,7 +54,7 @@ public async void Receive(VaultLockRequestedMessage message)
[RelayCommand]
private async Task RevealFolderAsync(CancellationToken cancellationToken)
{
- await FileExplorerService.TryOpenInFileExplorerAsync(_unlockedVaultViewModel.StorageRoot.Inner, cancellationToken);
+ await FileExplorerService.TryOpenInFileExplorerAsync(_unlockedVaultViewModel.StorageRoot.VirtualizedRoot, cancellationToken);
}
[RelayCommand]
@@ -85,7 +84,7 @@ private async Task OpenPropertiesAsync(CancellationToken cancellationToken)
{
if (_propertiesViewModel is null)
{
- _propertiesViewModel = new(_unlockedVaultViewModel);
+ _propertiesViewModel = new(_unlockedVaultViewModel, _dashboardNavigator, _vaultNavigation);
await _propertiesViewModel.InitAsync(cancellationToken);
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultList/VaultListItemViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultList/VaultListItemViewModel.cs
index 8323cbae7..9f7c7c3db 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultList/VaultListItemViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultList/VaultListItemViewModel.cs
@@ -27,17 +27,17 @@ public sealed partial class VaultListItemViewModel : ObservableObject, IAsyncIni
private readonly IVaultCollectionModel _vaultCollectionModel;
[ObservableProperty] private bool _IsRenaming;
- [ObservableProperty] private bool _CanMove;
- [ObservableProperty] private bool _CanMoveUp;
+ [ObservableProperty] private bool _IsUnlocked;
[ObservableProperty] private bool _CanMoveDown;
- [ObservableProperty] private bool _CanRemoveVault;
+ [ObservableProperty] private bool _CanMoveUp;
+ [ObservableProperty] private bool _CanMove;
[ObservableProperty] private IImage? _CustomIcon;
[ObservableProperty] private VaultViewModel _VaultViewModel;
public VaultListItemViewModel(VaultViewModel vaultViewModel, IVaultCollectionModel vaultCollectionModel)
{
ServiceProvider = DI.Default;
- CanRemoveVault = true;
+ IsUnlocked = false;
VaultViewModel = vaultViewModel;
_vaultCollectionModel = vaultCollectionModel;
@@ -58,7 +58,7 @@ public void Receive(VaultUnlockedMessage message)
if (VaultViewModel.VaultModel.Equals(message.VaultModel))
{
// Prevent from removing vault if it is unlocked
- CanRemoveVault = false;
+ IsUnlocked = true;
}
}
@@ -66,7 +66,13 @@ public void Receive(VaultUnlockedMessage message)
public void Receive(VaultLockedMessage message)
{
if (VaultViewModel.VaultModel.Equals(message.VaultModel))
- CanRemoveVault = true;
+ IsUnlocked = false;
+ }
+
+ [RelayCommand]
+ private void RequestLock()
+ {
+ WeakReferenceMessenger.Default.Send(new VaultLockRequestedMessage(VaultViewModel.VaultModel));
}
[RelayCommand]
@@ -101,12 +107,15 @@ private async Task CustomizeAsync(string? option, CancellationToken cancellation
if (sourceIconFile is null)
return;
- // TODO: Resize icon (don't load large icons)
- // TODO: Add .ico file with desktop.ini
+ // Update vault icon
var destinationIconFile = await modifiableFolder.CreateFileAsync(Constants.Vault.VAULT_ICON_FILENAME, true, cancellationToken);
- await sourceIconFile.CopyContentsToAsync(destinationIconFile, cancellationToken);
+ await sourceIconFile.CopyContentsToAsync(destinationIconFile, cancellationToken); // TODO: Resize icon (don't load large icons)
await UpdateIconAsync(cancellationToken);
+ // Update folder icon
+ await using var iconStream = await sourceIconFile.OpenReadAsync(cancellationToken);
+ await MediaService.TrySetFolderIconAsync(modifiableFolder, iconStream, cancellationToken);
+
break;
}
@@ -132,7 +141,7 @@ private async Task RenameAsync(string? newName, CancellationToken cancellationTo
IsRenaming = false;
if (await VaultViewModel.VaultModel.SetVaultNameAsync(newName, cancellationToken))
- VaultViewModel.VaultName = newName;
+ VaultViewModel.Title = newName;
}
[RelayCommand]
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultList/VaultListSearchViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultList/VaultListSearchViewModel.cs
index 266140b7d..abf3f67ad 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultList/VaultListSearchViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultList/VaultListSearchViewModel.cs
@@ -24,7 +24,7 @@ public async Task SubmitQueryAsync(string query, CancellationToken cancellationT
await foreach (var item in SearchModel.SearchAsync(query, cancellationToken))
{
if (item is VaultListItemViewModel sidebarItem)
- SearchItems.Add(sidebarItem.VaultViewModel.VaultName);
+ SearchItems.Add(sidebarItem.VaultViewModel.Title);
}
}
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultList/VaultListViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultList/VaultListViewModel.cs
index 28ce70e1d..b8df3f405 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultList/VaultListViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/VaultList/VaultListViewModel.cs
@@ -81,7 +81,7 @@ public Task InitAsync(CancellationToken cancellationToken = default)
SelectedItem ??= Items.FirstOrDefault();
HasVaults = !Items.IsEmpty();
-
+
return Task.CompletedTask;
}
@@ -139,7 +139,7 @@ private void RemoveVault(IVaultModel vaultModel)
{
Items.Remove(itemToRemove);
}
- catch (NullReferenceException)
+ catch (Exception)
{
// This happens rarely but the vault is actually removed
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/BaseWidgetViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/BaseWidgetViewModel.cs
index bbc489ed6..34c4d0ce4 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/BaseWidgetViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/BaseWidgetViewModel.cs
@@ -1,10 +1,10 @@
-using CommunityToolkit.Mvvm.ComponentModel;
-using SecureFolderFS.Sdk.Models;
-using SecureFolderFS.Shared.ComponentModel;
-using System;
+using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using SecureFolderFS.Sdk.Models;
+using SecureFolderFS.Shared.ComponentModel;
namespace SecureFolderFS.Sdk.ViewModels.Controls.Widgets
{
@@ -14,6 +14,9 @@ namespace SecureFolderFS.Sdk.ViewModels.Controls.Widgets
[Bindable(true)]
public abstract class BaseWidgetViewModel : ObservableObject, IAsyncInitialize, IDisposable
{
+ ///
+ /// Gets the widget model interface used for manipulating widget data.
+ ///
public IWidgetModel WidgetModel { get; }
protected BaseWidgetViewModel(IWidgetModel widgetModel)
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Data/AggregatedDataWidgetViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Data/AggregatedDataWidgetViewModel.cs
index 2e12f7cda..70572cce4 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Data/AggregatedDataWidgetViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Data/AggregatedDataWidgetViewModel.cs
@@ -21,7 +21,7 @@ public sealed partial class AggregatedDataWidgetViewModel : BaseWidgetViewModel
[ObservableProperty] private string? _TotalRead;
[ObservableProperty] private string? _TotalWrite;
-
+
public AggregatedDataWidgetViewModel(UnlockedVaultViewModel unlockedVaultViewModel, IWidgetModel widgetModel)
: base(widgetModel)
{
@@ -36,7 +36,7 @@ public override Task InitAsync(CancellationToken cancellationToken = default)
_bytesWritten = new();
TotalRead = "0B";
TotalWrite = "0B";
-
+
_fileSystemStatistics.BytesRead = new Progress(x =>
{
if (x > 0)
@@ -47,10 +47,10 @@ public override Task InitAsync(CancellationToken cancellationToken = default)
if (x > 0)
_pendingBytesWritten += (ulong)x;
});
-
+
// We don't want to await it, since it's an async based timer
_ = InitializeBlockingTimer(cancellationToken);
-
+
return Task.CompletedTask;
}
@@ -64,7 +64,7 @@ private async Task InitializeBlockingTimer(CancellationToken cancellationToken)
TotalRead = _bytesRead.ToString().Replace(" ", string.Empty);
_pendingBytesRead = 0UL;
}
-
+
if (_pendingBytesWritten > 0UL)
{
_bytesWritten = _bytesWritten.AddBytes(_pendingBytesWritten);
@@ -73,7 +73,7 @@ private async Task InitializeBlockingTimer(CancellationToken cancellationToken)
}
}
}
-
+
///
public override void Dispose()
{
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Data/GraphsWidgetViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Data/GraphsWidgetViewModel.cs
index 148e1154f..9665dd703 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Data/GraphsWidgetViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Data/GraphsWidgetViewModel.cs
@@ -48,7 +48,7 @@ public override async Task InitAsync(CancellationToken cancellationToken = defau
_fileSystemStatistics.BytesRead = new Progress(x => _currentReadAmount += x);
_fileSystemStatistics.BytesWritten = new Progress(x => _currentWriteAmount += x);
-
+
// We don't want to await it, since it's an async based timer
_ = InitializeBlockingTimer(cancellationToken);
}
@@ -66,7 +66,7 @@ private async Task InitializeBlockingTimer(CancellationToken cancellationToken)
CalculateStatistics();
}
}
-
+
private void CalculateStatistics()
{
var read = Convert.ToInt64(ByteSize.FromBytes(_currentReadAmount).MegaBytes);
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Health/HealthIssueViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Health/HealthIssueViewModel.cs
index e738db23c..8247cb527 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Health/HealthIssueViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Health/HealthIssueViewModel.cs
@@ -14,12 +14,12 @@
namespace SecureFolderFS.Sdk.ViewModels.Controls.Widgets.Health
{
- [Inject]
+ [Inject(Visibility = "protected"), Inject(Visibility = "protected")]
[Bindable(true)]
public partial class HealthIssueViewModel : ErrorViewModel, IWrapper
{
[ObservableProperty] private string? _Icon; // TODO: Change to IImage
- [ObservableProperty] private SeverityType _Severity;
+ [ObservableProperty] private Severity _Severity;
///
/// Gets the associated with this view model.
@@ -39,7 +39,7 @@ public HealthIssueViewModel(IStorableChild storable, string title)
: base(title)
{
ServiceProvider = DI.Default;
- Severity = SeverityType.Warning;
+ Severity = Enums.Severity.Warning;
Inner = storable;
Title = title;
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Health/HealthWidgetViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Health/HealthWidgetViewModel.cs
index 2ed9942d9..d8cf4cd7d 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Health/HealthWidgetViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/Health/HealthWidgetViewModel.cs
@@ -1,8 +1,10 @@
-using CommunityToolkit.Mvvm.ComponentModel;
+using System;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
-using SecureFolderFS.Sdk.AppModels;
using SecureFolderFS.Sdk.Attributes;
-using SecureFolderFS.Sdk.Enums;
using SecureFolderFS.Sdk.EventArguments;
using SecureFolderFS.Sdk.Extensions;
using SecureFolderFS.Sdk.Models;
@@ -11,60 +13,37 @@
using SecureFolderFS.Shared;
using SecureFolderFS.Shared.ComponentModel;
using SecureFolderFS.Shared.Extensions;
-using SecureFolderFS.Storage.Scanners;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Threading;
-using System.Threading.Tasks;
namespace SecureFolderFS.Sdk.ViewModels.Controls.Widgets.Health
{
- [Inject, Inject]
+ [Inject, Inject]
[Bindable(true)]
- public sealed partial class HealthWidgetViewModel : BaseWidgetViewModel, IProgress, IProgress, IViewable
+ public sealed partial class HealthWidgetViewModel : BaseWidgetViewModel
{
- private readonly UnlockedVaultViewModel _unlockedVaultViewModel;
- private readonly List _savedState;
private readonly SynchronizationContext? _context;
private readonly INavigator _dashboardNavigator;
- private IHealthModel? _vaultHealthModel;
- private CancellationTokenSource? _cts;
- private string? _lastScanMode;
- [ObservableProperty] private string? _Title;
[ObservableProperty] private string? _LastCheckedText;
+ [ObservableProperty] private VaultHealthViewModel _HealthViewModel;
[ObservableProperty] private VaultHealthReportViewModel _HealthReportViewModel;
public HealthWidgetViewModel(UnlockedVaultViewModel unlockedVaultViewModel, INavigator dashboardNavigator, IWidgetModel widgetModel)
: base(widgetModel)
{
ServiceProvider = DI.Default;
- _cts = new();
- _savedState = new();
_context = SynchronizationContext.Current;
_dashboardNavigator = dashboardNavigator;
- _unlockedVaultViewModel = unlockedVaultViewModel;
- HealthReportViewModel = new(unlockedVaultViewModel, _context) { StartScanningCommand = StartScanningCommand };
+ HealthViewModel = new(unlockedVaultViewModel);
+ HealthReportViewModel = new(unlockedVaultViewModel, HealthViewModel);
LastCheckedText = string.Format("LastChecked".ToLocalized(), "Unspecified");
- Title = "HealthNoProblems".ToLocalized();
+ HealthViewModel.StateChanged += HealthViewModel_StateChanged;
}
///
public override async Task InitAsync(CancellationToken cancellationToken = default)
{
- var vaultModel = _unlockedVaultViewModel.VaultViewModel.VaultModel;
- var contentFolder = await vaultModel.GetContentFolderAsync(cancellationToken);
- var folderScanner = new DeepFolderScanner(contentFolder);
- var structureValidator = _unlockedVaultViewModel.StorageRoot.Options.HealthStatistics.StructureValidator;
-
- if (_vaultHealthModel is not null)
- {
- _vaultHealthModel.IssueFound -= VaultHealthModel_IssueFound;
- _vaultHealthModel.Dispose();
- }
- _vaultHealthModel = new HealthModel(folderScanner, new(this, this), structureValidator);
- _vaultHealthModel.IssueFound += VaultHealthModel_IssueFound;
+ // Initialize HealthViewModel
+ await HealthViewModel.InitAsync(cancellationToken);
// Get persisted last scanned date
var rawLastScanDate = await WidgetModel.GetWidgetDataAsync(cancellationToken);
@@ -78,144 +57,33 @@ public override async Task InitAsync(CancellationToken cancellationToken = defau
LastCheckedText = string.Format("LastChecked".ToLocalized(), localizedDate);
}
- ///
- public void Report(double value)
- {
- if (!HealthReportViewModel.IsProgressing)
- return;
-
- _context?.Post(_ => HealthReportViewModel.CurrentProgress = Math.Round(value), null);
- }
-
- ///
- public void Report(TotalProgress value)
- {
- if (!HealthReportViewModel.IsProgressing)
- return;
-
- _context?.Post(_ =>
- {
- if (value.Total == 0)
- Title = value.Achieved == 0
- ? "Collecting items..."
- : $"Collecting items ({value.Achieved})";
- else
- Title = value.Achieved == value.Total
- ? "Scan completed"
- : $"Scanning items ({value.Achieved} of {value.Total})";
- }, null);
- }
-
[RelayCommand]
private async Task OpenVaultHealthAsync()
{
await _dashboardNavigator.NavigateAsync(HealthReportViewModel);
}
-
- [RelayCommand]
- private async Task StartScanningAsync(string? mode)
- {
- if (_vaultHealthModel is null)
- return;
-
- // Set IsProgressing status
- HealthReportViewModel.IsProgressing = true;
- Title = "Scanning...";
-
- // Save last scan state
- _savedState.AddMultiple(HealthReportViewModel.FoundIssues);
- HealthReportViewModel.FoundIssues.Clear();
-
- // Determine scan mode
- if (mode?.Contains("rescan", StringComparison.OrdinalIgnoreCase) ?? false)
- mode = _lastScanMode;
- else
- _lastScanMode = mode;
-
- var includeFileContents = mode?.Contains("include_file_contents", StringComparison.OrdinalIgnoreCase) ?? false;
-
- // Begin scanning
- await Task.Delay(10);
- _ = Task.Run(() => ScanAsync(includeFileContents, _cts?.Token ?? default));
- await Task.Yield();
- }
-
- [RelayCommand]
- private void CancelScanning()
+
+ private async void HealthViewModel_StateChanged(object? sender, EventArgs e)
{
- // Restore last scan state
- HealthReportViewModel.FoundIssues.AddMultiple(_savedState);
-
- _cts?.CancelAsync();
- _cts?.Dispose();
- _cts = new();
- EndScanning();
- _context?.Post(_ => HealthReportViewModel.CanResolve = false, null);
- }
-
- private async Task ScanAsync(bool includeFileContents, CancellationToken cancellationToken)
- {
- if (_vaultHealthModel is null)
+ if (e is not ScanningFinishedEventArgs)
return;
- // Begin scanning
- _context?.Post(_ => HealthReportViewModel.CanResolve = false, null);
- await _vaultHealthModel.ScanAsync(includeFileContents, cancellationToken).ConfigureAwait(false);
- EndScanning();
-
- // Finish scanning
var scanDate = DateTime.Now;
- _context?.Post(_ =>
+ _context.PostOrExecute(_ =>
{
- HealthReportViewModel.CanResolve = !HealthReportViewModel.FoundIssues.IsEmpty();
var localizedDate = LocalizationService.LocalizeDate(scanDate);
LastCheckedText = string.Format("LastChecked".ToLocalized(), localizedDate);
- }, null);
+ });
// Persist last scanned date
- await WidgetModel.SetWidgetDataAsync(scanDate.ToString("o"), cancellationToken);
- }
-
- private void EndScanning()
- {
- if (!HealthReportViewModel.IsProgressing)
- return;
-
- _context?.Post(_ =>
- {
- // Reset progress
- HealthReportViewModel.IsProgressing = false;
- HealthReportViewModel.CurrentProgress = 0d;
- _savedState.Clear();
-
- // Update status
- Title = HealthReportViewModel.Severity switch
- {
- SeverityType.Warning => "HealthAttention".ToLocalized(),
- SeverityType.Critical => "HealthProblems".ToLocalized(),
- _ => "HealthNoProblems".ToLocalized()
- };
- }, null);
- }
-
- private async void VaultHealthModel_IssueFound(object? sender, HealthIssueEventArgs e)
- {
- if (e.Result.Successful || e.Storable is null)
- return;
-
- var issueViewModel = await VaultFileSystemService.GetIssueViewModelAsync(e.Result, e.Storable);
- _context?.Post(_ => HealthReportViewModel.FoundIssues.Add(issueViewModel ?? new(e.Storable, e.Result)), null);
+ await WidgetModel.SetWidgetDataAsync(scanDate.ToString("o")).ConfigureAwait(false);
}
///
public override void Dispose()
{
- if (_vaultHealthModel is not null)
- {
- _vaultHealthModel.IssueFound -= VaultHealthModel_IssueFound;
- _vaultHealthModel.Dispose();
- }
-
+ HealthViewModel.StateChanged -= HealthViewModel_StateChanged;
+ HealthViewModel.Dispose();
HealthReportViewModel.Dispose();
base.Dispose();
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/WidgetsListViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/WidgetsListViewModel.cs
index 970e36b7f..61a1e12ef 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/WidgetsListViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Controls/Widgets/WidgetsListViewModel.cs
@@ -58,7 +58,7 @@ public async Task InitAsync(CancellationToken cancellationToken = default)
case Constants.Widgets.GRAPHS_WIDGET_ID:
return new GraphsWidgetViewModel(_unlockedVaultViewModel, widgetModel);
-
+
case Constants.Widgets.AGGREGATED_DATA_WIDGET_ID:
return new AggregatedDataWidgetViewModel(_unlockedVaultViewModel, widgetModel);
diff --git a/src/SecureFolderFS.Sdk/ViewModels/VaultViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/VaultViewModel.cs
index 272af4cfd..c10fba670 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/VaultViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/VaultViewModel.cs
@@ -1,4 +1,8 @@
-using CommunityToolkit.Mvvm.ComponentModel;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using OwlCore.Storage;
using SecureFolderFS.Sdk.Attributes;
@@ -8,21 +12,18 @@
using SecureFolderFS.Sdk.Models;
using SecureFolderFS.Sdk.Services;
using SecureFolderFS.Shared;
+using SecureFolderFS.Shared.ComponentModel;
using SecureFolderFS.Shared.Helpers;
using SecureFolderFS.Storage.VirtualFileSystem;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Threading.Tasks;
namespace SecureFolderFS.Sdk.ViewModels
{
[Inject]
[Inject]
[Bindable(true)]
- public sealed partial class VaultViewModel : ObservableObject
+ public sealed partial class VaultViewModel : ObservableObject, IViewable
{
- [ObservableProperty] private string _VaultName;
+ [ObservableProperty] private string? _Title;
[ObservableProperty] private DateTime? _LastAccessDate;
public IVaultModel VaultModel { get; }
@@ -30,8 +31,8 @@ public sealed partial class VaultViewModel : ObservableObject
public VaultViewModel(IVaultModel vaultModel)
{
ServiceProvider = DI.Default;
+ Title = vaultModel.VaultName;
VaultModel = vaultModel;
- VaultName = vaultModel.VaultName;
LastAccessDate = vaultModel.LastAccessDate;
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Browser/BrowserItemViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Browser/BrowserItemViewModel.cs
deleted file mode 100644
index 4cd749aaf..000000000
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Browser/BrowserItemViewModel.cs
+++ /dev/null
@@ -1,236 +0,0 @@
-using System;
-using System.ComponentModel;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using OwlCore.Storage;
-using SecureFolderFS.Sdk.Attributes;
-using SecureFolderFS.Sdk.Enums;
-using SecureFolderFS.Sdk.Extensions;
-using SecureFolderFS.Sdk.Helpers;
-using SecureFolderFS.Sdk.Services;
-using SecureFolderFS.Sdk.ViewModels.Views.Overlays;
-using SecureFolderFS.Shared;
-using SecureFolderFS.Shared.ComponentModel;
-using SecureFolderFS.Shared.Extensions;
-using SecureFolderFS.Storage.Extensions;
-using SecureFolderFS.Storage.Renamable;
-
-namespace SecureFolderFS.Sdk.ViewModels.Views.Browser
-{
- [Inject, Inject]
- [Bindable(true)]
- public abstract partial class BrowserItemViewModel : ObservableObject, IWrapper, IViewable, IAsyncInitialize
- {
- [ObservableProperty] private string? _Title;
- [ObservableProperty] private bool _IsSelected;
- [ObservableProperty] private IImage? _Thumbnail;
-
- ///
- public abstract IStorable Inner { get; }
-
- ///
- /// Gets the parent that this item resides in, if any.
- ///
- public FolderViewModel? ParentFolder { get; }
-
- protected BrowserItemViewModel(FolderViewModel? parentFolder)
- {
- ServiceProvider = DI.Default;
- ParentFolder = parentFolder;
- }
-
- ///
- public abstract Task InitAsync(CancellationToken cancellationToken = default);
-
- protected abstract void UpdateStorable(IStorable storable);
-
- [RelayCommand]
- protected virtual async Task MoveAsync(CancellationToken cancellationToken)
- {
- var items = ParentFolder?.GetSelectedItems().ToArray();
- if (items?.IsEmpty() ?? true)
- items = [ this ];
-
- if (ParentFolder?.BrowserViewModel.TransferViewModel is not { IsProgressing: false } transferViewModel || ParentFolder?.Folder is not IModifiableFolder modifiableParent)
- return;
-
- try
- {
- // Disable selection, if called with selected items
- ParentFolder.BrowserViewModel.IsSelecting = false;
-
- using var cts = new CancellationTokenSource();
- var destination = await transferViewModel.SelectFolderAsync(TransferType.Move, cts);
- if (destination is not { Folder: IModifiableFolder destinationFolder })
- return;
-
- foreach (var item in items)
- {
- if (destination.Folder.Id.Contains(item.Inner.Id, StringComparison.InvariantCultureIgnoreCase))
- return;
- }
-
- await transferViewModel.TransferAsync(items.Select(x => (IStorableChild)x.Inner), async (storable, token) =>
- {
- // Move
- var movedItem = await destinationFolder.MoveStorableFromAsync(storable, modifiableParent, false, token);
-
- // Remove existing from folder
- ParentFolder.Items.RemoveMatch(x => x.Inner.Id == storable.Id);
-
- // Add to destination
- destination.Items.Add(movedItem switch
- {
- IFile file => new FileViewModel(file, destination),
- IFolder folder => new FolderViewModel(folder, ParentFolder.BrowserViewModel, destination),
- _ => throw new ArgumentOutOfRangeException(nameof(movedItem))
- });
- }, cts.Token);
- }
- catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
- {
- _ = ex;
- // TODO: Report error
- }
- finally
- {
- transferViewModel.IsVisible = false;
- transferViewModel.IsProgressing = false;
- }
- }
-
- [RelayCommand]
- protected virtual async Task CopyAsync(CancellationToken cancellationToken)
- {
- var items = ParentFolder?.GetSelectedItems().ToArray();
- if (items?.IsEmpty() ?? true)
- items = [ this ];
-
- if (ParentFolder?.BrowserViewModel.TransferViewModel is not { IsProgressing: false } transferViewModel)
- return;
-
- try
- {
- // Disable selection, if called with selected items
- ParentFolder.BrowserViewModel.IsSelecting = false;
-
- using var cts = new CancellationTokenSource();
- var destination = await transferViewModel.SelectFolderAsync(TransferType.Copy, cts);
- if (destination is not { Folder: IModifiableFolder destinationFolder })
- return;
-
- foreach (var item in items)
- {
- if (destination.Folder.Id.Contains(item.Inner.Id, StringComparison.InvariantCultureIgnoreCase))
- return;
- }
-
- await transferViewModel.TransferAsync(items.Select(x => x.Inner), async (storable, token) =>
- {
- // Copy
- var copiedItem = await destinationFolder.CreateCopyOfStorableAsync(storable, false, token);
-
- // Add to destination
- destination.Items.Add(copiedItem switch
- {
- IFile file => new FileViewModel(file, destination),
- IFolder folder => new FolderViewModel(folder, ParentFolder.BrowserViewModel, destination),
- _ => throw new ArgumentOutOfRangeException(nameof(copiedItem))
- });
- }, cts.Token);
- }
- catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
- {
- _ = ex;
- // TODO: Report error
- }
- finally
- {
- transferViewModel.IsVisible = false;
- transferViewModel.IsProgressing = false;
- }
- }
-
- [RelayCommand]
- protected virtual async Task RenameAsync(CancellationToken cancellationToken)
- {
- if (ParentFolder?.Folder is not IRenamableFolder renamableFolder)
- return;
-
- if (Inner is not IStorableChild innerChild)
- return;
-
- try
- {
- var viewModel = new RenameOverlayViewModel("Rename item") { Message = "Choose a new name" };
- var result = await OverlayService.ShowAsync(viewModel);
- if (!result.Positive())
- return;
-
- if (string.IsNullOrWhiteSpace(viewModel.NewName))
- return;
-
- var formattedName = FormattingHelpers.SanitizeItemName(viewModel.NewName, "item");
- if (!Path.HasExtension(formattedName))
- formattedName = $"{formattedName}{Path.GetExtension(innerChild.Name)}";
-
- var renamedStorable = await renamableFolder.RenameAsync(innerChild, formattedName, cancellationToken);
-
- Title = formattedName;
- UpdateStorable(renamedStorable);
- }
- catch (Exception ex)
- {
- // TODO: Report error
- _ = ex;
- }
- }
-
- [RelayCommand]
- protected virtual async Task DeleteAsync(CancellationToken cancellationToken)
- {
- if (ParentFolder?.Folder is not IModifiableFolder modifiableFolder)
- return;
-
- // TODO: Show an overlay to ask the user **when deleting permanently**
- // TODO: If moving to trash, show TransferViewModel (with try..catch..finally), otherwise don't show anything
-
- var items = ParentFolder.GetSelectedItems().ToArray();
- if (items.IsEmpty())
- items = [this];
-
- // Disable selection, if called with selected items
- ParentFolder.BrowserViewModel.IsSelecting = false;
-
- foreach (var item in items)
- {
- await modifiableFolder.DeleteAsync((IStorableChild)item.Inner, cancellationToken);
- ParentFolder?.Items.Remove(item);
- }
- }
-
- [RelayCommand]
- protected virtual async Task ExportAsync(CancellationToken cancellationToken)
- {
- if (ParentFolder?.Folder is not IModifiableFolder parentModifiableFolder)
- return;
-
- var destination = await FileExplorerService.PickFolderAsync(false, cancellationToken);
- if (destination is not IModifiableFolder destinationFolder)
- return;
-
- // Copy and delete
- await destinationFolder.CreateCopyOfStorableAsync(Inner, false, cancellationToken);
- await parentModifiableFolder.DeleteAsync((IStorableChild)Inner, cancellationToken);
-
- ParentFolder.Items.Remove(this);
- }
-
- [RelayCommand]
- protected abstract Task OpenAsync(CancellationToken cancellationToken);
- }
-}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Browser/BrowserViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Browser/BrowserViewModel.cs
deleted file mode 100644
index e85c54b1e..000000000
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Browser/BrowserViewModel.cs
+++ /dev/null
@@ -1,170 +0,0 @@
-using System.Collections.ObjectModel;
-using System.ComponentModel;
-using System.Threading;
-using System.Threading.Tasks;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using OwlCore.Storage;
-using SecureFolderFS.Sdk.Attributes;
-using SecureFolderFS.Sdk.Extensions;
-using SecureFolderFS.Sdk.Services;
-using SecureFolderFS.Sdk.ViewModels.Controls;
-using SecureFolderFS.Sdk.ViewModels.Controls.Transfer;
-using SecureFolderFS.Sdk.ViewModels.Views.Overlays;
-using SecureFolderFS.Shared;
-using SecureFolderFS.Shared.ComponentModel;
-using SecureFolderFS.Storage.Extensions;
-
-namespace SecureFolderFS.Sdk.ViewModels.Views.Browser
-{
- [Inject, Inject]
- [Bindable(true)]
- public partial class BrowserViewModel : ObservableObject, IViewDesignation
- {
- [ObservableProperty] private string? _Title;
- [ObservableProperty] private bool _IsSelecting;
- [ObservableProperty] private VaultViewModel _VaultViewModel;
- [ObservableProperty] private FolderViewModel? _CurrentFolder;
- [ObservableProperty] private TransferViewModel? _TransferViewModel;
- [ObservableProperty] private ObservableCollection _Breadcrumbs;
-
- public IFolder BaseFolder { get; }
-
- public INavigator Navigator { get; }
-
- public BrowserViewModel(INavigator navigator, IFolder baseFolder, VaultViewModel vaultViewModel)
- {
- ServiceProvider = DI.Default;
- Navigator = navigator;
- BaseFolder = baseFolder;
- VaultViewModel = vaultViewModel;
- Breadcrumbs = new()
- {
- new(vaultViewModel.VaultName, NavigateBreadcrumbCommand)
- };
- }
-
- ///
- public virtual void OnAppearing()
- {
- }
-
- ///
- public virtual void OnDisappearing()
- {
- }
-
- partial void OnCurrentFolderChanged(FolderViewModel? oldValue, FolderViewModel? newValue)
- {
- oldValue?.UnselectAll();
- IsSelecting = false;
- Title = newValue?.Title;
- if (string.IsNullOrEmpty(Title))
- Title = VaultViewModel.VaultName;
- }
-
- [RelayCommand]
- protected virtual async Task NavigateBreadcrumbAsync(BreadcrumbItemViewModel? itemViewModel, CancellationToken cancellationToken)
- {
- if (itemViewModel is null)
- return;
-
- var lastIndex = Breadcrumbs.Count - 1;
- var breadcrumbIndex = Breadcrumbs.IndexOf(itemViewModel);
- var difference = lastIndex - breadcrumbIndex;
- for (var i = 0; i < difference; i++)
- {
- await Navigator.GoBackAsync();
- }
- }
-
- [RelayCommand]
- protected virtual async Task RefreshAsync(CancellationToken cancellationToken)
- {
- if (CurrentFolder is not null)
- await CurrentFolder.ListContentsAsync(cancellationToken);
- }
-
- [RelayCommand]
- protected virtual void ToggleSelection()
- {
- IsSelecting = !IsSelecting;
- CurrentFolder?.UnselectAll();
- }
-
- [RelayCommand]
- protected virtual async Task NewItemAsync(string? itemType, CancellationToken cancellationToken)
- {
- if (itemType is null)
- return;
-
- itemType = itemType.ToLower();
- if (itemType is not ("folder" or "file"))
- return;
-
- if (CurrentFolder?.Folder is not IModifiableFolder modifiableFolder)
- return;
-
- var viewModel = new NewItemOverlayViewModel();
- var result = await OverlayService.ShowAsync(viewModel);
- if (result.Aborted() || viewModel.ItemName is null)
- return;
-
- switch (itemType)
- {
- case "file":
- {
- var file = await modifiableFolder.CreateFileAsync(viewModel.ItemName, false, cancellationToken);
- CurrentFolder.Items.Add(new FileViewModel(file, CurrentFolder));
- break;
- }
-
- case "folder":
- {
- var folder = await modifiableFolder.CreateFolderAsync(viewModel.ItemName, false, cancellationToken);
- CurrentFolder.Items.Add(new FolderViewModel(folder, this, CurrentFolder));
- break;
- }
- }
- }
-
- [RelayCommand]
- protected virtual async Task ImportItemAsync(string? itemType, CancellationToken cancellationToken)
- {
- if (itemType is null)
- return;
-
- itemType = itemType.ToLower();
- if (itemType is not ("folder" or "file"))
- return;
-
- if (CurrentFolder?.Folder is not IModifiableFolder modifiableFolder)
- return;
-
- switch (itemType)
- {
- case "file":
- {
- var file = await FileExplorerService.PickFileAsync(null, false, cancellationToken);
- if (file is null)
- return;
-
- var copiedFile = await modifiableFolder.CreateCopyOfAsync(file, false, cancellationToken);
- CurrentFolder.Items.Add(new FileViewModel(copiedFile, CurrentFolder));
- break;
- }
-
- case "folder":
- {
- var folder = await FileExplorerService.PickFolderAsync(false, cancellationToken);
- if (folder is null)
- return;
-
- var copiedFolder = await modifiableFolder.CreateCopyOfAsync(folder, false, cancellationToken);
- CurrentFolder.Items.Add(new FolderViewModel(copiedFolder, this, CurrentFolder));
- break;
- }
- }
- }
- }
-}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsConfirmationViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsConfirmationViewModel.cs
index 412c044e9..ee3786555 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsConfirmationViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsConfirmationViewModel.cs
@@ -23,7 +23,7 @@ namespace SecureFolderFS.Sdk.ViewModels.Views.Credentials
public sealed partial class CredentialsConfirmationViewModel : ObservableObject, IDisposable
{
private readonly IFolder _vaultFolder;
- private readonly AuthenticationType _authenticationStage;
+ private readonly AuthenticationStage _authenticationStage;
private readonly TaskCompletionSource _credentialsTcs;
[ObservableProperty] private bool _IsRemoving;
@@ -31,10 +31,10 @@ public sealed partial class CredentialsConfirmationViewModel : ObservableObject,
[ObservableProperty] private bool _IsComplementing;
[ObservableProperty] private RegisterViewModel _RegisterViewModel;
[ObservableProperty] private AuthenticationViewModel? _ConfiguredViewModel;
-
+
public required IDisposable UnlockContract { private get; init; }
- public CredentialsConfirmationViewModel(IFolder vaultFolder, RegisterViewModel registerViewModel, AuthenticationType authenticationStage)
+ public CredentialsConfirmationViewModel(IFolder vaultFolder, RegisterViewModel registerViewModel, AuthenticationStage authenticationStage)
{
ServiceProvider = DI.Default;
_vaultFolder = vaultFolder;
@@ -53,7 +53,7 @@ public async Task ConfirmAsync(CancellationToken cancellationToken)
else
await ModifyAsync(cancellationToken);
}
-
+
private async Task ModifyAsync(CancellationToken cancellationToken)
{
RegisterViewModel.ConfirmCredentialsCommand.Execute(null);
@@ -69,8 +69,8 @@ string[] GetAuthenticationMethod()
ArgumentNullException.ThrowIfNull(RegisterViewModel.CurrentViewModel);
return _authenticationStage switch
{
- AuthenticationType.ProceedingStageOnly => [ configuredOptions.AuthenticationMethod[0], RegisterViewModel.CurrentViewModel.Id ],
- AuthenticationType.FirstStageOnly => configuredOptions.AuthenticationMethod.Length > 1
+ AuthenticationStage.ProceedingStageOnly => [ configuredOptions.AuthenticationMethod[0], RegisterViewModel.CurrentViewModel.Id ],
+ AuthenticationStage.FirstStageOnly => configuredOptions.AuthenticationMethod.Length > 1
? [ RegisterViewModel.CurrentViewModel.Id, configuredOptions.AuthenticationMethod[1] ]
: [ RegisterViewModel.CurrentViewModel.Id ],
@@ -81,7 +81,7 @@ string[] GetAuthenticationMethod()
private async Task RemoveAsync(CancellationToken cancellationToken)
{
- if (_authenticationStage != AuthenticationType.ProceedingStageOnly)
+ if (_authenticationStage != AuthenticationStage.ProceedingStageOnly)
return;
var key = RegisterViewModel.Credentials.Keys.First();
@@ -93,19 +93,14 @@ private async Task RemoveAsync(CancellationToken cancellationToken)
private async Task ChangeCredentialsAsync(IKey key, VaultOptions configuredOptions, string[] authenticationMethod, CancellationToken cancellationToken)
{
- var newOptions = new VaultOptions()
+ var newOptions = configuredOptions with
{
- AuthenticationMethod = authenticationMethod,
- ContentCipherId = configuredOptions.ContentCipherId,
- FileNameCipherId = configuredOptions.FileNameCipherId,
- NameEncodingId = configuredOptions.NameEncodingId,
- VaultId = configuredOptions.VaultId,
- Version = configuredOptions.Version
+ AuthenticationMethod = authenticationMethod
};
await VaultManagerService.ModifyAuthenticationAsync(_vaultFolder, UnlockContract, key, newOptions, cancellationToken);
if (ConfiguredViewModel is not null)
- await ConfiguredViewModel.RevokeAsync(null, cancellationToken);
+ await ConfiguredViewModel.RevokeAsync(configuredOptions.VaultId, cancellationToken);
}
private void RegisterViewModel_CredentialsProvided(object? sender, CredentialsProvidedEventArgs e)
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsResetViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsResetViewModel.cs
index 9dac2ec57..1fcadeb3a 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsResetViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsResetViewModel.cs
@@ -62,14 +62,9 @@ public async Task ConfirmAsync(CancellationToken cancellationToken)
var key = await _credentialsTcs.Task;
var configuredOptions = await VaultService.GetVaultOptionsAsync(_vaultFolder, cancellationToken);
- var newOptions = new VaultOptions()
+ var newOptions = configuredOptions with
{
- AuthenticationMethod = [ RegisterViewModel.CurrentViewModel.Id ],
- ContentCipherId = configuredOptions.ContentCipherId,
- FileNameCipherId = configuredOptions.FileNameCipherId,
- NameEncodingId = configuredOptions.NameEncodingId,
- VaultId = configuredOptions.VaultId,
- Version = configuredOptions.Version
+ AuthenticationMethod = [ RegisterViewModel.CurrentViewModel.Id ]
};
await VaultManagerService.ModifyAuthenticationAsync(_vaultFolder, _unlockContract, key, newOptions, cancellationToken);
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsSelectionViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsSelectionViewModel.cs
index c109720dc..b9cb61e0a 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsSelectionViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Credentials/CredentialsSelectionViewModel.cs
@@ -23,23 +23,23 @@ namespace SecureFolderFS.Sdk.ViewModels.Views.Credentials
public sealed partial class CredentialsSelectionViewModel : ObservableObject, IAsyncInitialize, IDisposable
{
private readonly IFolder _vaultFolder;
- private readonly AuthenticationType _authenticationStage;
+ private readonly AuthenticationStage _authenticationStage;
[ObservableProperty] private bool _CanRemoveCredentials;
[ObservableProperty] private RegisterViewModel? _RegisterViewModel;
[ObservableProperty] private AuthenticationViewModel? _ConfiguredViewModel;
[ObservableProperty] private ObservableCollection _AuthenticationOptions;
-
+
public IDisposable? UnlockContract { private get; set; }
public event EventHandler? ConfirmationRequested;
- public CredentialsSelectionViewModel(IFolder vaultFolder, AuthenticationType authenticationStage)
+ public CredentialsSelectionViewModel(IFolder vaultFolder, AuthenticationStage authenticationStage)
{
ServiceProvider = DI.Default;
_vaultFolder = vaultFolder;
_authenticationStage = authenticationStage;
- _CanRemoveCredentials = authenticationStage != AuthenticationType.FirstStageOnly;
+ _CanRemoveCredentials = authenticationStage != AuthenticationStage.FirstStageOnly;
_AuthenticationOptions = new();
}
@@ -87,7 +87,7 @@ private async Task ItemSelected(AuthenticationViewModel? authenticationViewModel
authenticationViewModel ??= await GetExistingCreationForLoginAsync(cancellationToken);
if (authenticationViewModel is null)
return;
-
+
if (UnlockContract is null || RegisterViewModel is null)
return;
@@ -95,7 +95,7 @@ private async Task ItemSelected(AuthenticationViewModel? authenticationViewModel
ConfirmationRequested?.Invoke(this, new(_vaultFolder, RegisterViewModel, _authenticationStage)
{
IsRemoving = false,
- CanComplement = _authenticationStage != AuthenticationType.FirstStageOnly, // TODO: Also add a flag to the AuthenticationViewModel to indicate if it can be complemented
+ CanComplement = _authenticationStage != AuthenticationStage.FirstStageOnly, // TODO: Also add a flag to the AuthenticationViewModel to indicate if it can be complemented
UnlockContract = UnlockContract,
ConfiguredViewModel = ConfiguredViewModel
});
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Host/MainHostViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Host/MainHostViewModel.cs
index 6888e1490..45ec3b339 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Host/MainHostViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Host/MainHostViewModel.cs
@@ -44,7 +44,7 @@ public async Task InitAsync(CancellationToken cancellationToken = default)
await VaultListViewModel.InitAsync(cancellationToken);
await _systemMonitorModel.InitAsync(cancellationToken);
}
-
+
[RelayCommand]
private async Task OpenSettingsAsync()
{
@@ -63,7 +63,7 @@ private void VaultCollectionModel_CollectionChanged(object? sender, NotifyCollec
NavigationService.Views.Remove(viewModel);
(viewModel as IDisposable)?.Dispose();
break;
-
+
case NotifyCollectionChangedAction.Reset:
NavigationService.Views.DisposeElements();
NavigationService.Views.Clear();
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/IOverlayControls.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/IOverlayControls.cs
index db2479830..f3dfaa2e2 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/IOverlayControls.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/IOverlayControls.cs
@@ -6,7 +6,7 @@ namespace SecureFolderFS.Sdk.ViewModels.Views
///
/// Represents basic overlay controls to display in the UI.
///
- public interface IOverlayControls : IViewable, INotifyPropertyChanged
+ public interface IOverlayControls : IViewable
{
///
/// Gets whether the continuation should be possible or not.
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/ChangelogOverlayViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/ChangelogOverlayViewModel.cs
index 819926dd5..36f7c7504 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/ChangelogOverlayViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/ChangelogOverlayViewModel.cs
@@ -39,12 +39,12 @@ public async Task InitAsync(CancellationToken cancellationToken = default)
{
if (loadLatest)
{
- var changelog = await ChangelogService.GetLatestAsync(appVersion, ApplicationService.Platform, cancellationToken);
+ var changelog = await ChangelogService.GetLatestAsync(appVersion, cancellationToken);
BuildChangelog(changelog, changelogBuilder);
}
else
{
- await foreach (var item in ChangelogService.GetSinceAsync(_changelogSince, ApplicationService.Platform, cancellationToken))
+ await foreach (var item in ChangelogService.GetSinceAsync(_changelogSince, cancellationToken))
{
BuildChangelog(item, changelogBuilder);
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/CredentialsOverlayViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/CredentialsOverlayViewModel.cs
index 48c1527b5..96930b2ce 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/CredentialsOverlayViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/CredentialsOverlayViewModel.cs
@@ -23,29 +23,28 @@ namespace SecureFolderFS.Sdk.ViewModels.Views.Overlays
[Inject, Inject]
public sealed partial class CredentialsOverlayViewModel : OverlayViewModel, IAsyncInitialize, IDisposable
{
- private readonly KeyChain _keyChain;
+ private readonly KeySequence _keySequence;
private readonly IVaultModel _vaultModel;
- private readonly AuthenticationType _authenticationStage;
+ private readonly AuthenticationStage _authenticationStage;
- [ObservableProperty] private bool _CanContinue; // TODO: Use OverlayViewModel.IsPrimaryButtonEnabled
[ObservableProperty] private LoginViewModel _LoginViewModel;
[ObservableProperty] private RegisterViewModel _RegisterViewModel;
[ObservableProperty] private CredentialsSelectionViewModel _SelectionViewModel;
[ObservableProperty] private INotifyPropertyChanged? _SelectedViewModel;
- public CredentialsOverlayViewModel(IVaultModel vaultModel, AuthenticationType authenticationStage)
+ public CredentialsOverlayViewModel(IVaultModel vaultModel, AuthenticationStage authenticationStage)
{
ServiceProvider = DI.Default;
- _keyChain = new();
+ _keySequence = new();
_vaultModel = vaultModel;
_authenticationStage = authenticationStage;
- RegisterViewModel = new(authenticationStage, _keyChain);
- LoginViewModel = new(vaultModel, LoginViewType.Basic, _keyChain);
+ RegisterViewModel = new(authenticationStage, _keySequence);
+ LoginViewModel = new(vaultModel, LoginViewType.Basic, _keySequence);
SelectionViewModel = new(vaultModel.Folder, authenticationStage);
SelectedViewModel = LoginViewModel;
Title = "Authenticate".ToLocalized();
- PrimaryButtonText = "Continue".ToLocalized();
+ PrimaryText = "Continue".ToLocalized();
LoginViewModel.VaultUnlocked += LoginViewModel_VaultUnlocked;
RegisterViewModel.PropertyChanged += RegisterViewModel_PropertyChanged;
@@ -58,8 +57,8 @@ public async Task InitAsync(CancellationToken cancellationToken = default)
var loginMethods = VaultCredentialsService.GetLoginAsync(_vaultModel.Folder, cancellationToken);
SelectionViewModel.ConfiguredViewModel = _authenticationStage switch
{
- AuthenticationType.FirstStageOnly => await loginMethods.FirstOrDefaultAsync(cancellationToken),
- AuthenticationType.ProceedingStageOnly => await loginMethods.ElementAtOrDefaultAsync(1, cancellationToken),
+ AuthenticationStage.FirstStageOnly => await loginMethods.FirstOrDefaultAsync(cancellationToken),
+ AuthenticationStage.ProceedingStageOnly => await loginMethods.ElementAtOrDefaultAsync(1, cancellationToken),
_ => throw new ArgumentOutOfRangeException(nameof(_authenticationStage))
};
@@ -73,18 +72,18 @@ private void LoginViewModel_VaultUnlocked(object? sender, VaultUnlockedEventArgs
if (e.IsRecovered)
{
Title = "SetCredentials".ToLocalized();
- PrimaryButtonText = "Confirm".ToLocalized();
+ PrimaryText = "Confirm".ToLocalized();
CanContinue = false;
// Note: We can omit the fact that a flag other than FirstStage is passed to the ResetViewModel (via RegisterViewModel).
- // The flag is manipulating the order at which keys are placed in the keychain, so it shouldn't matter if it's cleared here
- _keyChain.Dispose();
+ // The flag is manipulating the order at which keys are placed in the key sequence, so it shouldn't matter if it's cleared here
+ _keySequence.Dispose();
SelectedViewModel = new CredentialsResetViewModel(_vaultModel.Folder, e.UnlockContract, RegisterViewModel).WithInitAsync();
}
else
{
Title = "SelectAuthentication".ToLocalized();
- PrimaryButtonText = null;
+ PrimaryText = null;
SelectionViewModel.UnlockContract = e.UnlockContract;
SelectionViewModel.RegisterViewModel = RegisterViewModel;
SelectedViewModel = SelectionViewModel;
@@ -95,7 +94,7 @@ private void SelectionViewModel_ConfirmationRequested(object? sender, Credential
{
CanContinue = e.IsRemoving || (SelectionViewModel.RegisterViewModel?.CanContinue ?? false);
Title = e.IsRemoving ? "RemoveAuthentication".ToLocalized() : "Authenticate".ToLocalized();
- PrimaryButtonText = "Confirm".ToLocalized();
+ PrimaryText = "Confirm".ToLocalized();
SelectedViewModel = e;
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/ExplanationOverlayViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/ExplanationOverlayViewModel.cs
index 517d9f5d9..be214f061 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/ExplanationOverlayViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/ExplanationOverlayViewModel.cs
@@ -16,7 +16,7 @@ public sealed class ExplanationOverlayViewModel : OverlayViewModel, IAsyncInitia
public ExplanationOverlayViewModel()
{
_periodicTimer = new(TimeSpan.FromSeconds(1));
- PrimaryButtonText = Constants.Dialogs.EXPLANATION_DIALOG_TIME_TICKS.ToString();
+ PrimaryText = Constants.Dialogs.EXPLANATION_DIALOG_TIME_TICKS.ToString();
}
///
@@ -32,14 +32,14 @@ private async Task InitializeBlockingTimer(CancellationToken cancellationToken)
while (await _periodicTimer.WaitForNextTickAsync(cancellationToken))
{
_elapsedTicks++;
- PrimaryButtonText = $"{Constants.Dialogs.EXPLANATION_DIALOG_TIME_TICKS - _elapsedTicks}";
+ PrimaryText = $"{Constants.Dialogs.EXPLANATION_DIALOG_TIME_TICKS - _elapsedTicks}";
#if !DEBUG // Skip waiting if debugging
if (_elapsedTicks >= Constants.Dialogs.EXPLANATION_DIALOG_TIME_TICKS)
#endif
{
- PrimaryButtonText = "Close".ToLocalized();
- PrimaryButtonEnabled = true;
+ PrimaryText = "Close".ToLocalized();
+ CanContinue = true;
_periodicTimer.Dispose();
break;
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/MigrationOverlayViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/MigrationOverlayViewModel.cs
index 09ddcd7ab..23111887c 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/MigrationOverlayViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/MigrationOverlayViewModel.cs
@@ -35,9 +35,9 @@ public MigrationOverlayViewModel(MigrationViewModel migrationViewModel)
// and appropriate migrators are chosen based solely on vault version
ServiceProvider = DI.Default;
MigrationViewModel = migrationViewModel;
- PrimaryButtonText = "Continue".ToLocalized();
+ PrimaryText = "Continue".ToLocalized();
Title = "Authenticate".ToLocalized();
- SecondaryButtonEnabled = true;
+ CanCancel = true;
}
///
@@ -77,7 +77,7 @@ private async Task AuthenticateMigrationAsync(object? credentials, CancellationT
StateChanged?.Invoke(this, new VaultUnlockedEventArgs(_unlockContract, MigrationViewModel.VaultFolder, false));
Title = "Migrate".ToLocalized();
- PrimaryButtonText = null;
+ PrimaryText = null;
}
catch (Exception ex)
{
@@ -99,7 +99,7 @@ private async Task MigrateAsync(CancellationToken cancellationToken)
// Start operation and report initial progress
Report(0);
IsProgressing = true;
- SecondaryButtonEnabled = false;
+ CanCancel = false;
// Await a short delay for better UX
await Task.Delay(1000);
@@ -116,7 +116,7 @@ private async Task MigrateAsync(CancellationToken cancellationToken)
finally
{
IsProgressing = false;
- SecondaryButtonEnabled = true;
+ CanCancel = true;
}
}
@@ -124,6 +124,7 @@ private async Task MigrateAsync(CancellationToken cancellationToken)
public void Dispose()
{
_unlockContract?.Dispose();
+ _vaultMigrator?.Dispose();
}
}
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/OverlayViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/OverlayViewModel.cs
index 552fe5b67..d420ff1cb 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/OverlayViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/OverlayViewModel.cs
@@ -1,6 +1,6 @@
using System;
-using CommunityToolkit.Mvvm.ComponentModel;
using System.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
namespace SecureFolderFS.Sdk.ViewModels.Views.Overlays
{
@@ -8,32 +8,25 @@ namespace SecureFolderFS.Sdk.ViewModels.Views.Overlays
/// Serves as the base dialog view model containing reusable code for every dialog.
///
[Bindable(true)]
- [Obsolete("Use BaseDesignationViewModel and IOverlayControls.")]
- public abstract partial class OverlayViewModel : BaseDesignationViewModel
+ [Obsolete("Use BaseDesignationViewModel and IOverlayControls.")] // TODO: Does this need to be Obsolete?
+ public abstract partial class OverlayViewModel : BaseDesignationViewModel, IOverlayControls
{
- ///
- /// Gets or sets whether the primary button should be enabled or not.
- ///
- [ObservableProperty] private bool _PrimaryButtonEnabled;
+ ///
+ [ObservableProperty] private bool _CanContinue;
- ///
- /// Gets or sets whether the secondary button should be enabled or not.
- ///
- [ObservableProperty] private bool _SecondaryButtonEnabled;
+ ///
+ [ObservableProperty] private bool _CanCancel;
- ///
- /// Gets or sets the text of primary button. If value is null, the button is hidden.
- ///
- [ObservableProperty] private string? _PrimaryButtonText;
+ ///
+ [ObservableProperty] private string? _PrimaryText;
- ///
- /// Gets or sets the text of secondary button. If value is null, the button is hidden.
- ///
- [ObservableProperty] private string? _SecondaryButtonText;
+ ///
+ [ObservableProperty] private string? _SecondaryText;
///
/// Gets or sets the text of close button. If value is null, the button is hidden.
///
+ [Obsolete("Use SecondaryText property instead.")]
[ObservableProperty] private string? _CloseButtonText;
}
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PaymentOverlayViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PaymentOverlayViewModel.cs
index 01e290559..3a38eca7f 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PaymentOverlayViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PaymentOverlayViewModel.cs
@@ -24,17 +24,17 @@ private PaymentOverlayViewModel()
public async Task InitAsync(CancellationToken cancellationToken = default)
{
// TODO: Localize
- if (PrimaryButtonText is not null && PrimaryButtonText != "Buy")
+ if (PrimaryText is not null && PrimaryText != "Buy")
return;
var price = await IapService.GetPriceAsync(IapProductType.SecureFolderFS_PlusSubscription, cancellationToken);
if (string.IsNullOrEmpty(price))
{
- PrimaryButtonText = "Buy";
- PrimaryButtonEnabled = false;
+ PrimaryText = "Buy";
+ CanContinue = false;
}
else
- PrimaryButtonText = price;
+ PrimaryText = price;
}
}
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PreviewRecoveryOverlayViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PreviewRecoveryOverlayViewModel.cs
index 1be5c4db0..b961f48dc 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PreviewRecoveryOverlayViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PreviewRecoveryOverlayViewModel.cs
@@ -31,10 +31,10 @@ public PreviewRecoveryOverlayViewModel(IVaultModel vaultModel)
_vaultModel = vaultModel;
_loginViewModel = new(_vaultModel, LoginViewType.Basic);
_recoveryViewModel = new();
-
+
CurrentViewModel = _loginViewModel;
Title = "Authenticate".ToLocalized();
- PrimaryButtonText = "Continue".ToLocalized();
+ PrimaryText = "Continue".ToLocalized();
_loginViewModel.VaultUnlocked += LoginViewModel_VaultUnlocked;
}
@@ -52,14 +52,14 @@ private async void LoginViewModel_VaultUnlocked(object? sender, VaultUnlockedEve
{
// Prepare the recovery view
_recoveryViewModel.VaultId = vaultOptions.VaultId;
- _recoveryViewModel.VaultName = _vaultModel.VaultName;
+ _recoveryViewModel.Title = _vaultModel.VaultName;
_recoveryViewModel.RecoveryKey = e.UnlockContract.ToString();
// Change view to recovery
CurrentViewModel = _recoveryViewModel;
// Adjust the overlay
- PrimaryButtonText = null;
+ PrimaryText = null;
Title = "VaultRecovery".ToLocalized();
}
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PreviewerOverlayViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PreviewerOverlayViewModel.cs
index 5278af4ec..74ea044fe 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PreviewerOverlayViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PreviewerOverlayViewModel.cs
@@ -28,9 +28,9 @@ public async Task LoadFromStorableAsync(IStorable storable, CancellationToken ca
TypeHint.Image => new ImagePreviewerViewModel(file),
TypeHint.Media => new VideoPreviewerViewModel(file),
TypeHint.PlainText => new TextPreviewerViewModel(file),
- _ => null
+ _ => new FallbackPreviewerViewModel(file)
});
-
+
if (previewer is null)
return;
@@ -38,7 +38,7 @@ public async Task LoadFromStorableAsync(IStorable storable, CancellationToken ca
(PreviewerViewModel as IDisposable)?.Dispose();
PreviewerViewModel = previewer as IViewable;
}
-
+
///
public void Dispose()
{
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PropertiesOverlayViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PropertiesOverlayViewModel.cs
new file mode 100644
index 000000000..68f9d9e57
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/PropertiesOverlayViewModel.cs
@@ -0,0 +1,56 @@
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+using ByteSizeLib;
+using CommunityToolkit.Mvvm.ComponentModel;
+using OwlCore.Storage;
+using SecureFolderFS.Sdk.Attributes;
+using SecureFolderFS.Sdk.Extensions;
+using SecureFolderFS.Sdk.Helpers;
+using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Shared;
+using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Storage.StorageProperties;
+
+namespace SecureFolderFS.Sdk.ViewModels.Views.Overlays
+{
+ [Bindable(true)]
+ [Inject]
+ public sealed partial class PropertiesOverlayViewModel : OverlayViewModel, IAsyncInitialize
+ {
+ private readonly IStorable _storable;
+ private readonly IBasicProperties _properties;
+
+ [ObservableProperty] private string? _SizeText;
+ [ObservableProperty] private string? _FileTypeText;
+ [ObservableProperty] private string? _DateModifiedText;
+
+ public PropertiesOverlayViewModel(IStorable storable, IBasicProperties properties)
+ {
+ ServiceProvider = DI.Default;
+ _storable = storable;
+ _properties = properties;
+ Title = _storable.Name;
+ }
+
+ ///
+ public async Task InitAsync(CancellationToken cancellationToken = default)
+ {
+ var typeClassification = FileTypeHelper.GetClassification(_storable);
+ FileTypeText = _storable is IFolder ? "inode/directory" : typeClassification.MimeType;
+
+ if (_properties is ISizeProperties sizeProperties)
+ {
+ var sizeProperty = await sizeProperties.GetSizeAsync(cancellationToken);
+ if (sizeProperty is not null)
+ SizeText = ByteSize.FromBytes(sizeProperty.Value).ToString();
+ }
+
+ if (_properties is IDateProperties dateProperties)
+ {
+ var dateModifiedProperty = await dateProperties.GetDateModifiedAsync(cancellationToken);
+ DateModifiedText = LocalizationService.LocalizeDate(dateModifiedProperty.Value);
+ }
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/RecycleBinOverlayViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/RecycleBinOverlayViewModel.cs
new file mode 100644
index 000000000..60b9436db
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/RecycleBinOverlayViewModel.cs
@@ -0,0 +1,165 @@
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using ByteSizeLib;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using OwlCore.Storage;
+using SecureFolderFS.Sdk.Attributes;
+using SecureFolderFS.Sdk.Extensions;
+using SecureFolderFS.Sdk.Helpers;
+using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Sdk.ViewModels.Controls;
+using SecureFolderFS.Sdk.ViewModels.Controls.Storage;
+using SecureFolderFS.Shared;
+using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Shared.Extensions;
+using SecureFolderFS.Storage.Extensions;
+using SecureFolderFS.Storage.VirtualFileSystem;
+
+namespace SecureFolderFS.Sdk.ViewModels.Views.Overlays
+{
+ [Bindable(true)]
+ [Inject, Inject]
+ public sealed partial class RecycleBinOverlayViewModel : BaseDesignationViewModel, IAsyncInitialize
+ {
+ private long _occupiedSize;
+ private IRecycleBinFolder? _recycleBin;
+
+ [ObservableProperty] private bool _IsSelecting;
+ [ObservableProperty] private bool _IsRecycleBinEnabled;
+ [ObservableProperty] private string? _SpaceTakenText;
+ [ObservableProperty] private double _PercentageTaken;
+ [ObservableProperty] private PickerOptionViewModel? _CurrentSizeOption;
+ [ObservableProperty] private UnlockedVaultViewModel _UnlockedVaultViewModel;
+ [ObservableProperty] private ObservableCollection _Items;
+ [ObservableProperty] private ObservableCollection _SizeOptions;
+
+ public INavigator OuterNavigator { get; }
+
+ public RecycleBinOverlayViewModel(UnlockedVaultViewModel unlockedVaultViewModel, INavigator outerNavigator)
+ {
+ ServiceProvider = DI.Default;
+ Items = new();
+ Title = "RecycleBin".ToLocalized();
+ SizeOptions = new();
+ OuterNavigator = outerNavigator;
+ UnlockedVaultViewModel = unlockedVaultViewModel;
+ }
+
+ ///
+ public async Task InitAsync(CancellationToken cancellationToken = default)
+ {
+ // Get storage root folder
+ var rootFolder = UnlockedVaultViewModel.VaultViewModel.VaultModel.Folder;
+ if (rootFolder is IChildFolder childFolder)
+ rootFolder = await childFolder.GetRootAsync(cancellationToken) ?? childFolder;
+
+ // Get and populate available size options
+ var deviceFreeSpace = await SystemService.GetAvailableFreeSpaceAsync(rootFolder, cancellationToken);
+ var sizeOptions = RecycleBinHelpers.GetSizeOptions(deviceFreeSpace);
+ SizeOptions.AddMultiple(sizeOptions);
+
+ // Choose the saved size option
+ CurrentSizeOption = SizeOptions.FirstOrDefault(x => long.Parse(x.Id) == UnlockedVaultViewModel.StorageRoot.Options.RecycleBinSize)
+ ?? SizeOptions.ElementAtOrDefault(1)
+ ?? SizeOptions.FirstOrDefault();
+
+ // TODO: Is the following logic (order) correct?
+ // Try and retrieve recycle bin for later use
+ _recycleBin ??= await RecycleBinService.TryGetRecycleBinAsync(UnlockedVaultViewModel.StorageRoot, cancellationToken);
+ if (_recycleBin is null)
+ return;
+
+ // Get occupied state
+ if (CurrentSizeOption is not null && CurrentSizeOption.Id != "-1")
+ {
+ _occupiedSize = await _recycleBin.GetSizeAsync(cancellationToken);
+ UpdateSizeBar(CurrentSizeOption);
+ }
+
+ // We can only determine that the recycle bin is enabled if it exists
+ IsRecycleBinEnabled = UnlockedVaultViewModel.StorageRoot.Options.IsRecycleBinEnabled();
+ await foreach (var item in _recycleBin.GetItemsAsync(StorableType.All, cancellationToken))
+ {
+ if (item is not IRecycleBinItem recycleBinItem)
+ continue;
+
+ Items.Add(new(this, recycleBinItem, _recycleBin));
+ }
+ }
+
+ ///
+ /// Toggles the recycle bin on or off updating the configuration file.
+ ///
+ /// The value that determines whether to enable or disable the recycle bin.
+ /// A that cancels this action.
+ public async Task ToggleRecycleBinAsync(bool value, CancellationToken cancellationToken = default)
+ {
+ if (value == IsRecycleBinEnabled)
+ return;
+
+ if (!long.TryParse(CurrentSizeOption?.Id, out var size))
+ return;
+
+ await RecycleBinService.ConfigureRecycleBinAsync(
+ UnlockedVaultViewModel.StorageRoot,
+ value ? size : 0L,
+ cancellationToken);
+
+ IsRecycleBinEnabled = value;
+ if (IsRecycleBinEnabled)
+ _recycleBin ??= await RecycleBinService.TryGetOrCreateRecycleBinAsync(UnlockedVaultViewModel.StorageRoot, cancellationToken);
+ }
+
+ ///
+ /// Updates the size bar occupied size and forces a recalculation, if necessary.
+ ///
+ /// Determines whether to recalculate item sizes in the recycle bin.
+ /// A that cancels this action.
+ public async Task UpdateSizesAsync(bool forceRecalculation, CancellationToken cancellationToken = default)
+ {
+ _recycleBin ??= await RecycleBinService.TryGetRecycleBinAsync(UnlockedVaultViewModel.StorageRoot, cancellationToken);
+ if (_recycleBin is null)
+ return;
+
+ if (forceRecalculation)
+ await RecycleBinService.TryRecalculateSizesAsync(UnlockedVaultViewModel.StorageRoot, cancellationToken);
+
+ _occupiedSize = await _recycleBin.GetSizeAsync(cancellationToken);
+ if (CurrentSizeOption is not null)
+ UpdateSizeBar(CurrentSizeOption);
+ }
+
+ partial void OnCurrentSizeOptionChanged(PickerOptionViewModel? value)
+ {
+ if (value is null || value.Id == "-1")
+ SpaceTakenText = null;
+ else
+ UpdateSizeBar(value);
+ }
+
+ [RelayCommand]
+ private void ToggleSelection(bool? value = null)
+ {
+ IsSelecting = value ?? !IsSelecting;
+ Items.UnselectAll();
+ }
+
+ [RelayCommand]
+ private void SelectAll()
+ {
+ IsSelecting = true;
+ Items.SelectAll();
+ }
+
+ private void UpdateSizeBar(PickerOptionViewModel value)
+ {
+ var totalSize = long.Parse(value.Id);
+ PercentageTaken = (double)_occupiedSize / totalSize * 100d;
+ SpaceTakenText = $"Taken {ByteSize.FromBytes(_occupiedSize).ToBinaryString()} out of {ByteSize.FromBytes(totalSize).ToBinaryString()}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/StorableTypeOverlayViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/StorableTypeOverlayViewModel.cs
new file mode 100644
index 000000000..5ddbe41ec
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/StorableTypeOverlayViewModel.cs
@@ -0,0 +1,17 @@
+using System.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using OwlCore.Storage;
+
+namespace SecureFolderFS.Sdk.ViewModels.Views.Overlays
+{
+ [Bindable(true)]
+ public sealed partial class StorableTypeOverlayViewModel : BaseDesignationViewModel
+ {
+ [ObservableProperty] private StorableType _StorableType;
+
+ public StorableTypeOverlayViewModel()
+ {
+ Title = "Choose an item type";
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/WizardOverlayViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/WizardOverlayViewModel.cs
index 41d212b45..77605458d 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/WizardOverlayViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Overlays/WizardOverlayViewModel.cs
@@ -26,8 +26,8 @@ public sealed partial class WizardOverlayViewModel : OverlayViewModel, INavigata
public WizardOverlayViewModel(IVaultCollectionModel vaultCollectionModel)
{
VaultCollectionModel = vaultCollectionModel;
- PrimaryButtonText = "Continue".ToLocalized();
- SecondaryButtonText = "Cancel".ToLocalized();
+ PrimaryText = "Continue".ToLocalized();
+ SecondaryText = "Cancel".ToLocalized();
}
///
@@ -75,10 +75,10 @@ partial void OnCurrentViewModelChanging(BaseWizardViewModel? oldValue, BaseWizar
newValue.PropertyChanged += CurrentViewModel_PropertyChanged;
newValue.OnAppearing();
- PrimaryButtonEnabled = newValue.CanContinue;
- SecondaryButtonEnabled = newValue.CanCancel;
- PrimaryButtonText = newValue.ContinueText;
- SecondaryButtonText = newValue.CancelText;
+ CanContinue = newValue.CanContinue;
+ CanCancel = newValue.CanCancel;
+ PrimaryText = newValue.PrimaryText;
+ SecondaryText = newValue.SecondaryText;
}
Title = newValue?.Title;
@@ -91,23 +91,23 @@ private void CurrentViewModel_PropertyChanged(object? sender, PropertyChangedEve
switch (e.PropertyName)
{
- case nameof(BaseWizardViewModel.CanContinue):
- PrimaryButtonEnabled = CurrentViewModel.CanContinue;
+ case nameof(CanContinue):
+ CanContinue = CurrentViewModel.CanContinue;
break;
- case nameof(BaseWizardViewModel.CanCancel):
- SecondaryButtonEnabled = CurrentViewModel.CanCancel;
+ case nameof(CanCancel):
+ CanCancel = CurrentViewModel.CanCancel;
break;
- case nameof(BaseWizardViewModel.ContinueText):
- PrimaryButtonText = CurrentViewModel.ContinueText;
+ case nameof(PrimaryText):
+ PrimaryText = CurrentViewModel.PrimaryText;
break;
- case nameof(BaseWizardViewModel.CancelText):
- SecondaryButtonText = CurrentViewModel.CancelText;
+ case nameof(SecondaryText):
+ SecondaryText = CurrentViewModel.SecondaryText;
break;
- case nameof(BaseWizardViewModel.Title):
+ case nameof(Title):
Title = CurrentViewModel.Title;
break;
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Settings/AboutSettingsViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Settings/AboutSettingsViewModel.cs
index 2d22b297d..4c83c80e1 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Settings/AboutSettingsViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Settings/AboutSettingsViewModel.cs
@@ -70,6 +70,7 @@ private async Task OpenChangelogAsync()
{
var changelogOverlay = new ChangelogOverlayViewModel(ApplicationService.AppVersion);
_ = changelogOverlay.InitAsync();
+
await OverlayService.ShowAsync(changelogOverlay);
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Settings/GeneralSettingsViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Settings/GeneralSettingsViewModel.cs
index 15c6ccba4..b376aaa8f 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Settings/GeneralSettingsViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Settings/GeneralSettingsViewModel.cs
@@ -34,7 +34,7 @@ public GeneralSettingsViewModel()
Title = "SettingsGeneral".ToLocalized();
BannerViewModel = new();
Languages = new();
-
+
_currentCulture = LocalizationService.CurrentCulture;
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Settings/PreferencesSettingsViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Settings/PreferencesSettingsViewModel.cs
index 15c887754..319fa453f 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Settings/PreferencesSettingsViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Settings/PreferencesSettingsViewModel.cs
@@ -41,6 +41,24 @@ public bool OpenFolderOnUnlock
set => UserSettings.OpenFolderOnUnlock = value;
}
+ public bool AreThumbnailsEnabled
+ {
+ get => UserSettings.AreThumbnailsEnabled;
+ set => UserSettings.AreThumbnailsEnabled = value;
+ }
+
+ public bool AreFileExtensionsEnabled
+ {
+ get => UserSettings.AreFileExtensionsEnabled;
+ set => UserSettings.AreFileExtensionsEnabled = value;
+ }
+
+ public bool IsContentCacheEnabled
+ {
+ get => UserSettings.IsContentCacheEnabled;
+ set => UserSettings.IsContentCacheEnabled = value;
+ }
+
///
public override async Task InitAsync(CancellationToken cancellationToken = default)
{
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/BrowserViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/BrowserViewModel.cs
new file mode 100644
index 000000000..bbc79480d
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/BrowserViewModel.cs
@@ -0,0 +1,248 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using OwlCore.Storage;
+using SecureFolderFS.Sdk.AppModels;
+using SecureFolderFS.Sdk.Attributes;
+using SecureFolderFS.Sdk.Enums;
+using SecureFolderFS.Sdk.Extensions;
+using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Sdk.ViewModels.Controls;
+using SecureFolderFS.Sdk.ViewModels.Controls.Storage;
+using SecureFolderFS.Sdk.ViewModels.Controls.Storage.Browser;
+using SecureFolderFS.Sdk.ViewModels.Controls.Transfer;
+using SecureFolderFS.Sdk.ViewModels.Views.Overlays;
+using SecureFolderFS.Shared;
+using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Shared.Extensions;
+using SecureFolderFS.Storage.Extensions;
+using SecureFolderFS.Storage.Pickers;
+using SecureFolderFS.Storage.VirtualFileSystem;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SecureFolderFS.Sdk.ViewModels.Views.Vault
+{
+ [Inject, Inject]
+ [Bindable(true)]
+ public partial class BrowserViewModel : BaseDesignationViewModel, IFolderPicker
+ {
+ private readonly IViewable? _rootView;
+
+ [ObservableProperty] private bool _IsSelecting;
+ [ObservableProperty] private FolderViewModel? _CurrentFolder;
+ [ObservableProperty] private TransferViewModel? _TransferViewModel;
+ [ObservableProperty] private ObservableCollection _Breadcrumbs;
+
+ public IFolder BaseFolder { get; }
+
+ public INavigator InnerNavigator { get; }
+
+ public INavigator? OuterNavigator { get; }
+
+ public ViewOptionsViewModel ViewOptions { get; }
+
+ public required IVFSRoot StorageRoot { get; init; }
+
+ public BrowserViewModel(IFolder baseFolder, INavigator innerNavigator, INavigator? outerNavigator, IViewable? rootView)
+ {
+ ServiceProvider = DI.Default;
+ _rootView = rootView;
+ ViewOptions = new();
+ InnerNavigator = innerNavigator;
+ OuterNavigator = outerNavigator;
+ BaseFolder = baseFolder;
+ Breadcrumbs = [ new(rootView?.Title, NavigateBreadcrumbCommand) ];
+ }
+
+ ///
+ public override void OnAppearing()
+ {
+ _ = CurrentFolder?.ListContentsAsync();
+ base.OnAppearing();
+ }
+
+ ///
+ public override void OnDisappearing()
+ {
+ if (TransferViewModel is not null)
+ {
+ if (TransferViewModel.IsVisible && !TransferViewModel.IsProgressing)
+ TransferViewModel?.CancelCommand.Execute(null);
+ }
+
+ CurrentFolder?.Dispose();
+ base.OnDisappearing();
+ }
+
+ ///
+ public async Task PickFolderAsync(PickerOptions? options, bool offerPersistence = true, CancellationToken cancellationToken = default)
+ {
+ if (OuterNavigator is null || TransferViewModel is null)
+ return null;
+
+ try
+ {
+ await OuterNavigator.NavigateAsync(this);
+ var cts = TransferViewModel.GetCancellation();
+ return await TransferViewModel.PickFolderAsync(new TransferOptions(TransferType.Select), false, cts.Token);
+ }
+ finally
+ {
+ if (OuterNavigator is INavigationService { CurrentView: BrowserViewModel })
+ await OuterNavigator.GoBackAsync();
+
+ _ = 0;
+ }
+ }
+
+ partial void OnCurrentFolderChanged(FolderViewModel? oldValue, FolderViewModel? newValue)
+ {
+ oldValue?.Items.UnselectAll();
+ IsSelecting = false;
+ Title = newValue?.Title;
+ if (string.IsNullOrEmpty(Title))
+ Title = _rootView?.Title;
+ }
+
+ [RelayCommand]
+ protected virtual async Task NavigateBreadcrumbAsync(BreadcrumbItemViewModel? itemViewModel, CancellationToken cancellationToken)
+ {
+ if (itemViewModel is null)
+ return;
+
+ var lastIndex = Breadcrumbs.Count - 1;
+ var breadcrumbIndex = Breadcrumbs.IndexOf(itemViewModel);
+ var difference = lastIndex - breadcrumbIndex;
+ for (var i = 0; i < difference; i++)
+ {
+ await InnerNavigator.GoBackAsync();
+ }
+ }
+
+ [RelayCommand]
+ protected virtual async Task RefreshAsync(CancellationToken cancellationToken)
+ {
+ if (CurrentFolder is not null)
+ await CurrentFolder.ListContentsAsync(cancellationToken);
+ }
+
+ [RelayCommand]
+ protected virtual void ToggleSelection(bool? value = null)
+ {
+ IsSelecting = value ?? !IsSelecting;
+ CurrentFolder?.Items.UnselectAll();
+ }
+
+ [RelayCommand]
+ protected virtual async Task ChangeViewOptionsAsync(CancellationToken cancellationToken)
+ {
+ if (CurrentFolder is null)
+ return;
+
+ var originalSortOption = ViewOptions.CurrentSortOption;
+ await OverlayService.ShowAsync(ViewOptions);
+
+ if (originalSortOption != ViewOptions.CurrentSortOption)
+ ViewOptions.GetSorter()?.SortCollection(CurrentFolder.Items, CurrentFolder.Items);
+ }
+
+ [RelayCommand]
+ protected virtual async Task NewItemAsync(string? itemType, CancellationToken cancellationToken)
+ {
+ if (CurrentFolder?.Folder is not IModifiableFolder modifiableFolder)
+ return;
+
+ IResult result;
+ if (itemType is not ("Folder" or "File"))
+ {
+ var storableTypeViewModel = new StorableTypeOverlayViewModel();
+ result = await OverlayService.ShowAsync(storableTypeViewModel);
+ if (result.Aborted())
+ return;
+
+ itemType = storableTypeViewModel.StorableType.ToString();
+ }
+
+ var newItemViewModel = new NewItemOverlayViewModel();
+ result = await OverlayService.ShowAsync(newItemViewModel);
+ if (result.Aborted() || newItemViewModel.ItemName is null)
+ return;
+
+ switch (itemType)
+ {
+ case "File":
+ {
+ var file = await modifiableFolder.CreateFileAsync(newItemViewModel.ItemName, false, cancellationToken);
+ CurrentFolder.Items.Insert(new FileViewModel(file, this, CurrentFolder), ViewOptions.GetSorter());
+ break;
+ }
+
+ case "Folder":
+ {
+ var folder = await modifiableFolder.CreateFolderAsync(newItemViewModel.ItemName, false, cancellationToken);
+ CurrentFolder.Items.Insert(new FolderViewModel(folder, this, CurrentFolder), ViewOptions.GetSorter());
+ break;
+ }
+ }
+ }
+
+ [RelayCommand]
+ protected virtual async Task ImportItemAsync(string? itemType, CancellationToken cancellationToken)
+ {
+ if (CurrentFolder?.Folder is not IModifiableFolder modifiableFolder)
+ return;
+
+ if (TransferViewModel is null)
+ return;
+
+ if (itemType is not ("Folder" or "File"))
+ {
+ var storableTypeViewModel = new StorableTypeOverlayViewModel();
+ var result = await OverlayService.ShowAsync(storableTypeViewModel);
+ if (result.Aborted())
+ return;
+
+ itemType = storableTypeViewModel.StorableType.ToString();
+ }
+
+ switch (itemType)
+ {
+ case "File":
+ {
+ var file = await FileExplorerService.PickFileAsync(null, false, cancellationToken);
+ if (file is null)
+ return;
+
+ TransferViewModel.TransferType = TransferType.Copy;
+ using var cts = TransferViewModel.GetCancellation();
+ await TransferViewModel.TransferAsync([ file ], async (item, token) =>
+ {
+ var copiedFile = await modifiableFolder.CreateCopyOfAsync(item, false, token);
+ CurrentFolder.Items.Insert(new FileViewModel(copiedFile, this, CurrentFolder), ViewOptions.GetSorter());
+ }, cts.Token);
+
+ break;
+ }
+
+ case "Folder":
+ {
+ var folder = await FileExplorerService.PickFolderAsync(null, false, cancellationToken);
+ if (folder is null)
+ return;
+
+ TransferViewModel.TransferType = TransferType.Copy;
+ using var cts = TransferViewModel.GetCancellation();
+ await TransferViewModel.TransferAsync([ folder ], async (item, reporter, token) =>
+ {
+ var copiedFolder = await modifiableFolder.CreateCopyOfAsync(item, false, reporter, token);
+ CurrentFolder.Items.Insert(new FolderViewModel(copiedFolder, this, CurrentFolder), ViewOptions.GetSorter());
+ }, cts.Token);
+
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultDashboardViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultDashboardViewModel.cs
index 4b024f5db..22ee187fd 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultDashboardViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultDashboardViewModel.cs
@@ -15,7 +15,7 @@ namespace SecureFolderFS.Sdk.ViewModels.Views.Vault
public sealed partial class VaultDashboardViewModel : BaseDesignationViewModel, IRecipient, IUnlockedViewContext, IDisposable
{
public INavigationService VaultNavigation { get; }
-
+
public INavigationService DashboardNavigation { get; }
///
@@ -48,10 +48,10 @@ private void VaultViewModel_PropertyChanged(object? sender, PropertyChangedEvent
if (DashboardNavigation.CurrentView is not VaultOverviewViewModel)
return;
- if (e.PropertyName != nameof(VaultViewModel.VaultName))
+ if (e.PropertyName != nameof(VaultViewModel.Title))
return;
- Title = VaultViewModel.VaultName;
+ Title = VaultViewModel.Title;
}
[RelayCommand]
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthReportViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthReportViewModel.cs
index 222d14b18..631575d1a 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthReportViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthReportViewModel.cs
@@ -1,122 +1,74 @@
-using CommunityToolkit.Mvvm.ComponentModel;
+using System;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using SecureFolderFS.Sdk.Attributes;
-using SecureFolderFS.Sdk.Contexts;
-using SecureFolderFS.Sdk.Enums;
+using SecureFolderFS.Sdk.EventArguments;
using SecureFolderFS.Sdk.Extensions;
using SecureFolderFS.Sdk.Services;
using SecureFolderFS.Sdk.ViewModels.Controls.Widgets.Health;
using SecureFolderFS.Shared;
using SecureFolderFS.Shared.ComponentModel;
using SecureFolderFS.Shared.Extensions;
-using System;
-using System.Collections;
-using System.Collections.ObjectModel;
-using System.Collections.Specialized;
-using System.ComponentModel;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Windows.Input;
namespace SecureFolderFS.Sdk.ViewModels.Views.Vault
{
[Bindable(true)]
- [Inject]
- public sealed partial class VaultHealthReportViewModel : BaseDesignationViewModel, IUnlockedViewContext, IDisposable
+ [Inject]
+ public sealed partial class VaultHealthReportViewModel : BaseDesignationViewModel, IDisposable
{
- private readonly SynchronizationContext? _context;
-
+ private readonly UnlockedVaultViewModel _unlockedVaultViewModel;
+
[ObservableProperty] private bool _CanResolve;
- [ObservableProperty] private bool _IsProgressing;
- [ObservableProperty] private double _CurrentProgress;
- [ObservableProperty] private SeverityType _Severity;
- [ObservableProperty] private ICommand? _StartScanningCommand;
- [ObservableProperty] private ObservableCollection _FoundIssues;
-
- ///
- public UnlockedVaultViewModel UnlockedVaultViewModel { get; }
-
- ///
- public VaultViewModel VaultViewModel => UnlockedVaultViewModel.VaultViewModel;
-
- public VaultHealthReportViewModel(UnlockedVaultViewModel unlockedVaultViewModel, SynchronizationContext? context)
+ [ObservableProperty] private VaultHealthViewModel _HealthViewModel;
+
+ public VaultHealthReportViewModel(UnlockedVaultViewModel unlockedVaultViewModel, VaultHealthViewModel healthViewModel)
{
ServiceProvider = DI.Default;
- UnlockedVaultViewModel = unlockedVaultViewModel;
+ HealthViewModel = healthViewModel;
Title = "HealthReport".ToLocalized();
- FoundIssues = new();
- FoundIssues.CollectionChanged += FoundIssues_CollectionChanged;
- _context = context;
- }
-
- partial void OnIsProgressingChanged(bool value)
- {
- _ = value;
- UpdateSeverity(FoundIssues);
+ HealthViewModel.StateChanged += HealthViewModel_StateChanged;
+ _unlockedVaultViewModel = unlockedVaultViewModel;
}
[RelayCommand]
private async Task ResolveAsync(CancellationToken cancellationToken)
{
// Get the readonly status and set IsReadOnly to true
- var isReadOnly = UnlockedVaultViewModel.Options.IsReadOnly;
- UnlockedVaultViewModel.Options.DangerousSetReadOnly(true);
+ var isReadOnly = _unlockedVaultViewModel.Options.IsReadOnly;
+ _unlockedVaultViewModel.Options.DangerousSetReadOnly(true);
// Resolve issues and restore readonly status
- await VaultFileSystemService.ResolveIssuesAsync(FoundIssues, UnlockedVaultViewModel.StorageRoot, IssueResolved, cancellationToken);
- UnlockedVaultViewModel.Options.DangerousSetReadOnly(isReadOnly);
+ await VaultHealthService.ResolveIssuesAsync(HealthViewModel.FoundIssues, _unlockedVaultViewModel.StorageRoot, IssueResolved, cancellationToken);
+ _unlockedVaultViewModel.Options.DangerousSetReadOnly(isReadOnly);
+
+ // Disable repair option until the vault is re-scanned
+ CanResolve = false;
}
private void IssueResolved(HealthIssueViewModel issueViewModel, IResult result)
{
if (result.Successful)
- FoundIssues.RemoveMatch(x => x.Inner.Id == issueViewModel.Inner.Id);
+ HealthViewModel.FoundIssues.RemoveMatch(x => x.Inner.Id == issueViewModel.Inner.Id);
}
-
- private void FoundIssues_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+
+ private void HealthViewModel_StateChanged(object? sender, EventArgs e)
{
- if (IsProgressing)
- return;
-
- UpdateSeverity(e is
+ CanResolve = e switch
{
- Action:
- NotifyCollectionChangedAction.Add or
- NotifyCollectionChangedAction.Replace or
- NotifyCollectionChangedAction.Move,
- NewItems: not null
- } ? e.NewItems : FoundIssues);
+ ScanningStartedEventArgs => false,
+ ScanningFinishedEventArgs args => !args.WasCanceled && !HealthViewModel.FoundIssues.IsEmpty(),
+ _ => CanResolve
+ };
}
-
- private void UpdateSeverity(IEnumerable enumerable)
- {
-#pragma warning disable MVVMTK0034
- var severity = Severity;
- if (severity != SeverityType.Success && FoundIssues.IsEmpty())
- {
- _Severity = SeverityType.Success;
- _context?.Post(_ => OnPropertyChanged(nameof(Severity)), null);
- return;
- }
-
- foreach (HealthIssueViewModel item in enumerable)
- {
- if (severity < item.Severity)
- severity = item.Severity;
- }
-
- if (Severity != severity)
- {
- _Severity = severity;
- _context?.Post(_ => OnPropertyChanged(nameof(Severity)), null);
- }
-#pragma warning restore MVVMTK0034
- }
-
+
///
public void Dispose()
{
- FoundIssues.CollectionChanged -= FoundIssues_CollectionChanged;
+ HealthViewModel.StateChanged -= HealthViewModel_StateChanged;
+ HealthViewModel.Dispose();
}
}
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthViewModel.Scanning.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthViewModel.Scanning.cs
new file mode 100644
index 000000000..53153e82d
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthViewModel.Scanning.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.Input;
+using SecureFolderFS.Sdk.Enums;
+using SecureFolderFS.Sdk.EventArguments;
+using SecureFolderFS.Sdk.Extensions;
+using SecureFolderFS.Shared.Extensions;
+
+namespace SecureFolderFS.Sdk.ViewModels.Views.Vault
+{
+ public sealed partial class VaultHealthViewModel
+ {
+ [RelayCommand]
+ private async Task StartScanningAsync(string? mode)
+ {
+ if (_healthModel is null)
+ return;
+
+ // Set IsProgressing status
+ IsProgressing = true;
+ Title = StatusTitle = "Scanning...";
+
+ // Save last scan state
+ _savedState.AddMultiple(FoundIssues);
+ FoundIssues.Clear();
+
+ // Determine scan mode
+ if (mode?.Contains("rescan", StringComparison.OrdinalIgnoreCase) ?? false)
+ mode = _lastScanMode;
+ else
+ _lastScanMode = mode;
+
+ var includeFileContents = mode?.Contains("include_file_contents", StringComparison.OrdinalIgnoreCase) ?? false;
+
+ // Begin scanning
+ await Task.Delay(10);
+ _ = Task.Run(() => ScanAsync(includeFileContents, _cts?.Token ?? default));
+ await Task.Yield();
+ }
+
+ [RelayCommand]
+ private void CancelScanning()
+ {
+ // Restore last scan state
+ FoundIssues.AddMultiple(_savedState);
+
+ _cts?.CancelAsync();
+ _cts?.Dispose();
+ _cts = new();
+ EndScanning();
+ StateChanged?.Invoke(this, new ScanningFinishedEventArgs(true));
+ }
+
+ private async Task ScanAsync(bool includeFileContents, CancellationToken cancellationToken)
+ {
+ if (_healthModel is null)
+ return;
+
+ // Begin scanning
+ StateChanged?.Invoke(this, new ScanningStartedEventArgs());
+ await _healthModel.ScanAsync(includeFileContents, cancellationToken).ConfigureAwait(false);
+
+ // Finish scanning
+ EndScanning();
+ StateChanged?.Invoke(this, new ScanningFinishedEventArgs(false));
+ }
+
+ private void EndScanning()
+ {
+ if (!IsProgressing)
+ return;
+
+ _context.PostOrExecute(_ =>
+ {
+ // Reset progress
+ IsProgressing = false;
+ CurrentProgress = 0d;
+ _savedState.Clear();
+
+ // Update status
+ Title = Severity switch
+ {
+ Enums.Severity.Warning => "HealthAttention".ToLocalized(),
+ Enums.Severity.Critical => "HealthProblems".ToLocalized(),
+ _ => "HealthNoProblems".ToLocalized()
+ };
+ StatusTitle = "Perform integrity check";
+ Subtitle = null;
+ });
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthViewModel.cs
new file mode 100644
index 000000000..bbab109f5
--- /dev/null
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthViewModel.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using SecureFolderFS.Sdk.AppModels;
+using SecureFolderFS.Sdk.Attributes;
+using SecureFolderFS.Sdk.Enums;
+using SecureFolderFS.Sdk.EventArguments;
+using SecureFolderFS.Sdk.Extensions;
+using SecureFolderFS.Sdk.Models;
+using SecureFolderFS.Sdk.Services;
+using SecureFolderFS.Sdk.ViewModels.Controls.Widgets.Health;
+using SecureFolderFS.Shared;
+using SecureFolderFS.Shared.ComponentModel;
+using SecureFolderFS.Shared.Extensions;
+using SecureFolderFS.Storage.Scanners;
+
+namespace SecureFolderFS.Sdk.ViewModels.Views.Vault
+{
+ [Bindable(true)]
+ [Inject, Inject]
+ public sealed partial class VaultHealthViewModel : ObservableObject, IProgress, IProgress, INotifyStateChanged, IViewable, IAsyncInitialize, IDisposable
+ {
+ private CancellationTokenSource? _cts;
+ private IHealthModel? _healthModel;
+ private string? _lastScanMode;
+ private readonly SynchronizationContext? _context;
+ private readonly List _savedState;
+ private readonly UnlockedVaultViewModel _unlockedVaultViewModel;
+
+ [ObservableProperty] private string? _Title;
+ [ObservableProperty] private string? _Subtitle;
+ [ObservableProperty] private string? _StatusTitle;
+ [ObservableProperty] private bool _IsProgressing;
+ [ObservableProperty] private double _CurrentProgress;
+ [ObservableProperty] private Severity _Severity;
+ [ObservableProperty] private ObservableCollection _FoundIssues;
+
+ ///
+ public event EventHandler? StateChanged;
+
+ public VaultHealthViewModel(UnlockedVaultViewModel unlockedVaultViewModel)
+ {
+ ServiceProvider = DI.Default;
+ _cts = new();
+ _context = SynchronizationContext.Current;
+ _savedState = new();
+ _unlockedVaultViewModel = unlockedVaultViewModel;
+ Title = "HealthNoProblems".ToLocalized();
+ Subtitle = null;
+ StatusTitle = "Perform integrity check";
+ FoundIssues = new();
+ FoundIssues.CollectionChanged += FoundIssues_CollectionChanged;
+ }
+
+ ///
+ public async Task InitAsync(CancellationToken cancellationToken = default)
+ {
+ var vaultModel = _unlockedVaultViewModel.VaultViewModel.VaultModel;
+ var contentFolder = await vaultModel.GetContentFolderAsync(cancellationToken);
+ var folderScanner = new DeepFolderScanner(contentFolder, predicate: x => !VaultService.IsNameReserved(x.Name));
+ var structureValidator = _unlockedVaultViewModel.StorageRoot.Options.HealthStatistics.StructureValidator;
+
+ _healthModel = new HealthModel(folderScanner, new(this, this), structureValidator);
+ _healthModel.IssueFound += HealthModel_IssueFound;
+ }
+
+ ///
+ public void Report(double value)
+ {
+ if (!IsProgressing)
+ return;
+
+ _context.PostOrExecute(_ => CurrentProgress = Math.Round(value));
+ }
+
+ ///
+ public void Report(TotalProgress value)
+ {
+ if (!IsProgressing)
+ return;
+
+ _context.PostOrExecute(_ =>
+ {
+ if (value.Total == 0)
+ {
+ Title = value.Achieved == 0
+ ? "Collecting items..."
+ : $"Collecting items ({value.Achieved})";
+
+ StatusTitle = "Collecting items...";
+ Subtitle = value.Achieved == 0
+ ? "Items are being collected"
+ : $"Collected {value.Achieved} items";
+ }
+ else
+ {
+ Title = value.Achieved == value.Total
+ ? "Scan completed"
+ : $"Scanning items ({value.Achieved} of {value.Total})";
+
+ StatusTitle = value.Achieved == value.Total
+ ? "Scan completed"
+ : "Scanning items";
+ Subtitle = $"Scanned {value.Achieved} out of {value.Total}";
+ }
+ });
+ }
+
+ partial void OnIsProgressingChanged(bool value)
+ {
+ _ = value;
+ UpdateSeverity(FoundIssues);
+ }
+
+ private void UpdateSeverity(IEnumerable enumerable)
+ {
+#pragma warning disable MVVMTK0034
+ var severity = Severity;
+ if (severity != Enums.Severity.Success && FoundIssues.IsEmpty())
+ {
+ _Severity = Enums.Severity.Success;
+ _context.PostOrExecute(_ => OnPropertyChanged(nameof(Severity)));
+ return;
+ }
+
+ foreach (HealthIssueViewModel item in enumerable)
+ {
+ if (severity < item.Severity)
+ severity = item.Severity;
+ }
+
+ if (Severity != severity)
+ {
+ _Severity = severity;
+ _context.PostOrExecute(_ => OnPropertyChanged(nameof(Severity)));
+ }
+#pragma warning restore MVVMTK0034
+ }
+
+ private void FoundIssues_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (IsProgressing)
+ return;
+
+ UpdateSeverity(e is
+ {
+ Action:
+ NotifyCollectionChangedAction.Add or
+ NotifyCollectionChangedAction.Replace or
+ NotifyCollectionChangedAction.Move,
+ NewItems: not null
+ } ? e.NewItems : FoundIssues);
+ }
+
+ private async void HealthModel_IssueFound(object? sender, HealthIssueEventArgs e)
+ {
+ if (e.Result.Successful || e.Storable is null)
+ return;
+
+ var issueViewModel = await VaultHealthService.GetIssueViewModelAsync(e.Result, e.Storable).ConfigureAwait(false);
+ _context.PostOrExecute(_ => FoundIssues.Add(issueViewModel ?? new(e.Storable, e.Result)));
+ }
+
+ ///
+ public void Dispose()
+ {
+ _cts?.TryCancel();
+ _cts?.Dispose();
+ FoundIssues.CollectionChanged -= FoundIssues_CollectionChanged;
+ if (_healthModel is not null)
+ {
+ _healthModel.IssueFound -= HealthModel_IssueFound;
+ _healthModel.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultLoginViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultLoginViewModel.cs
index 38ba046cd..dc7e8b204 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultLoginViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultLoginViewModel.cs
@@ -37,7 +37,7 @@ public sealed partial class VaultLoginViewModel : BaseDesignationViewModel, IVau
public VaultLoginViewModel(VaultViewModel vaultViewModel, INavigationService vaultNavigation)
{
ServiceProvider = DI.Default;
- Title = vaultViewModel.VaultName;
+ Title = vaultViewModel.Title;
VaultNavigation = vaultNavigation;
VaultViewModel = vaultViewModel;
_LoginViewModel = new(vaultViewModel.VaultModel, LoginViewType.Full);
@@ -48,6 +48,21 @@ public VaultLoginViewModel(VaultViewModel vaultViewModel, INavigationService vau
public async Task InitAsync(CancellationToken cancellationToken = default)
{
await LoginViewModel.InitAsync(cancellationToken);
+
+ // Test for quick unlock on mobile
+ if (VaultViewModel.Title != "Vault V3")
+ return;
+
+ // var contentFolder = await VaultViewModel.VaultModel.Folder.GetFolderByNameAsync("content");
+ // var item = await contentFolder.GetItemsAsync().FirstOrDefaultAsync();
+ // if (contentFolder is IModifiableFolder modifiableFolder && item is not null)
+ // await modifiableFolder.DeleteAsync(item);
+
+ var recoveryKey = "TjpPwjoNOB7Zx1xhqm9H79M8ngr+ZR31pqSjWOccBrY=@@@cU/ajAu5WmAryrtT6I3ouBcXQE0FmE4hFU7bkYD1EQE=";
+ var unlockContract = await VaultManagerService.RecoverAsync(VaultViewModel.VaultModel.Folder, recoveryKey, cancellationToken);
+
+ await Task.Delay(300);
+ await UnlockAsync(unlockContract);
}
[RelayCommand]
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultOverviewViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultOverviewViewModel.cs
index ff8f06621..0e8eee24f 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultOverviewViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultOverviewViewModel.cs
@@ -13,7 +13,7 @@
namespace SecureFolderFS.Sdk.ViewModels.Views.Vault
{
- [Inject, Inject]
+ [Inject, Inject, Inject]
[Bindable(true)]
public sealed partial class VaultOverviewViewModel : BaseDesignationViewModel, IUnlockedViewContext, IAsyncInitialize, IDisposable
{
@@ -32,28 +32,27 @@ public VaultOverviewViewModel(UnlockedVaultViewModel unlockedVaultViewModel, Vau
UnlockedVaultViewModel = unlockedVaultViewModel;
WidgetsViewModel = widgetsViewModel;
VaultControlsViewModel = vaultControlsViewModel;
- Title = unlockedVaultViewModel.VaultViewModel.VaultName;
+ Title = unlockedVaultViewModel.VaultViewModel.Title;
VaultViewModel.PropertyChanged += VaultViewModel_PropertyChanged;
}
///
public async Task InitAsync(CancellationToken cancellationToken = default)
{
- if (SettingsService.UserSettings.OpenFolderOnUnlock)
- _ = FileExplorerService.TryOpenInFileExplorerAsync(UnlockedVaultViewModel.StorageRoot.Inner, cancellationToken);
+ if (ApplicationService.IsDesktop && SettingsService.UserSettings.OpenFolderOnUnlock)
+ _ = FileExplorerService.TryOpenInFileExplorerAsync(UnlockedVaultViewModel.StorageRoot.VirtualizedRoot, cancellationToken);
await WidgetsViewModel.InitAsync(cancellationToken);
}
private void VaultViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
- if (e.PropertyName != nameof(VaultViewModel.VaultName))
+ if (e.PropertyName != nameof(VaultViewModel.Title))
return;
- Title = VaultViewModel.VaultName;
+ Title = VaultViewModel.Title;
}
-
///
public void Dispose()
{
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultPropertiesViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultPropertiesViewModel.cs
index 9f3d822af..00c06c526 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultPropertiesViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultPropertiesViewModel.cs
@@ -19,6 +19,9 @@ namespace SecureFolderFS.Sdk.ViewModels.Views.Vault
[Bindable(true)]
public sealed partial class VaultPropertiesViewModel : BaseDesignationViewModel, IUnlockedViewContext, IAsyncInitialize
{
+ private readonly INavigator _innerNavigator;
+ private readonly INavigator _outerNavigator;
+
[ObservableProperty] private string? _SecurityText;
[ObservableProperty] private string? _ContentCipherText;
[ObservableProperty] private string? _FileNameCipherText;
@@ -31,11 +34,13 @@ public sealed partial class VaultPropertiesViewModel : BaseDesignationViewModel,
///
public VaultViewModel VaultViewModel => UnlockedVaultViewModel.VaultViewModel;
- public VaultPropertiesViewModel(UnlockedVaultViewModel unlockedVaultViewModel)
+ public VaultPropertiesViewModel(UnlockedVaultViewModel unlockedVaultViewModel, INavigator innerNavigator, INavigator outerNavigator)
{
ServiceProvider = DI.Default;
UnlockedVaultViewModel = unlockedVaultViewModel;
Title = "VaultProperties".ToLocalized();
+ _innerNavigator = innerNavigator;
+ _outerNavigator = outerNavigator;
}
///
@@ -53,7 +58,7 @@ public async Task InitAsync(CancellationToken cancellationToken = default)
[RelayCommand]
private async Task ChangeFirstAuthenticationAsync(CancellationToken cancellationToken)
{
- using var credentialsOverlay = new CredentialsOverlayViewModel(UnlockedVaultViewModel.VaultViewModel.VaultModel, AuthenticationType.FirstStageOnly);
+ using var credentialsOverlay = new CredentialsOverlayViewModel(UnlockedVaultViewModel.VaultViewModel.VaultModel, AuthenticationStage.FirstStageOnly);
await credentialsOverlay.InitAsync(cancellationToken);
await OverlayService.ShowAsync(credentialsOverlay);
await UpdateSecurityTextAsync(cancellationToken);
@@ -62,7 +67,7 @@ private async Task ChangeFirstAuthenticationAsync(CancellationToken cancellation
[RelayCommand]
private async Task ChangeSecondAuthenticationAsync(CancellationToken cancellationToken)
{
- using var credentialsOverlay = new CredentialsOverlayViewModel(UnlockedVaultViewModel.VaultViewModel.VaultModel, AuthenticationType.ProceedingStageOnly);
+ using var credentialsOverlay = new CredentialsOverlayViewModel(UnlockedVaultViewModel.VaultViewModel.VaultModel, AuthenticationStage.ProceedingStageOnly);
await credentialsOverlay.InitAsync(cancellationToken);
await OverlayService.ShowAsync(credentialsOverlay);
await UpdateSecurityTextAsync(cancellationToken);
@@ -72,11 +77,19 @@ private async Task ChangeSecondAuthenticationAsync(CancellationToken cancellatio
private async Task ViewRecoveryAsync(CancellationToken cancellationToken)
{
using var previewRecoveryOverlay = new PreviewRecoveryOverlayViewModel(UnlockedVaultViewModel.VaultViewModel.VaultModel);
-
await previewRecoveryOverlay.InitAsync(cancellationToken);
await OverlayService.ShowAsync(previewRecoveryOverlay);
}
+ [RelayCommand]
+ private async Task ViewRecycleBinAsync(CancellationToken cancellationToken)
+ {
+ var recycleOverlay = new RecycleBinOverlayViewModel(UnlockedVaultViewModel, _outerNavigator);
+ _ = recycleOverlay.InitAsync(cancellationToken);
+
+ await OverlayService.ShowAsync(recycleOverlay);
+ }
+
private async Task UpdateSecurityTextAsync(CancellationToken cancellationToken)
{
var items = await VaultCredentialsService.GetLoginAsync(UnlockedVaultViewModel.VaultViewModel.VaultModel.Folder, cancellationToken).ToArrayAsync(cancellationToken);
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/BaseWizardViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/BaseWizardViewModel.cs
index a38fd27dc..5372717ac 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/BaseWizardViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/BaseWizardViewModel.cs
@@ -1,31 +1,14 @@
-using CommunityToolkit.Mvvm.ComponentModel;
-using SecureFolderFS.Shared.ComponentModel;
-using System.ComponentModel;
+using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
+using SecureFolderFS.Sdk.ViewModels.Views.Overlays;
+using SecureFolderFS.Shared.ComponentModel;
namespace SecureFolderFS.Sdk.ViewModels.Views.Wizard
{
[Bindable(true)]
- public abstract partial class BaseWizardViewModel : ObservableObject, IViewDesignation // TODO: Use IOverlayControls
+ public abstract class BaseWizardViewModel : OverlayViewModel // TODO: Use IOverlayControls
{
- ///
- [ObservableProperty] private string? _Title;
- [ObservableProperty] private string? _CancelText;
- [ObservableProperty] private string? _ContinueText;
- [ObservableProperty] private bool _CanCancel;
- [ObservableProperty] private bool _CanContinue;
-
- ///
- public virtual void OnAppearing()
- {
- }
-
- ///
- public virtual void OnDisappearing()
- {
- }
-
// TODO: Needs docs
public abstract Task TryContinueAsync(CancellationToken cancellationToken);
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/CredentialsWizardViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/CredentialsWizardViewModel.cs
index 7cf29ee20..704e63f4d 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/CredentialsWizardViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/CredentialsWizardViewModel.cs
@@ -29,12 +29,12 @@ public sealed partial class CredentialsWizardViewModel : BaseWizardViewModel
private readonly string _vaultId;
private readonly TaskCompletionSource _credentialsTcs;
- [ObservableProperty] private VaultOptionViewModel? _ContentCipher;
- [ObservableProperty] private VaultOptionViewModel? _FileNameCipher;
- [ObservableProperty] private VaultOptionViewModel? _EncodingOption;
- [ObservableProperty] private ObservableCollection _ContentCiphers = new();
- [ObservableProperty] private ObservableCollection _FileNameCiphers = new();
- [ObservableProperty] private ObservableCollection _EncodingOptions = new();
+ [ObservableProperty] private PickerOptionViewModel? _ContentCipher;
+ [ObservableProperty] private PickerOptionViewModel? _FileNameCipher;
+ [ObservableProperty] private PickerOptionViewModel? _EncodingOption;
+ [ObservableProperty] private ObservableCollection _ContentCiphers = new();
+ [ObservableProperty] private ObservableCollection _FileNameCiphers = new();
+ [ObservableProperty] private ObservableCollection _EncodingOptions = new();
[ObservableProperty] private ObservableCollection _AuthenticationOptions = new();
[ObservableProperty] private RegisterViewModel _RegisterViewModel;
@@ -45,12 +45,12 @@ public CredentialsWizardViewModel(IModifiableFolder folder)
ServiceProvider = DI.Default;
Folder = folder;
_credentialsTcs = new();
- _RegisterViewModel = new(AuthenticationType.FirstStageOnly);
+ _RegisterViewModel = new(AuthenticationStage.FirstStageOnly);
_vaultId = Guid.NewGuid().ToString();
- ContinueText = "Continue".ToLocalized();
Title = "SetCredentials".ToLocalized();
- CancelText = "Cancel".ToLocalized();
+ PrimaryText = "Continue".ToLocalized();
+ SecondaryText = "Cancel".ToLocalized();
CanContinue = false;
CanCancel = true;
@@ -73,12 +73,14 @@ public override async Task TryContinueAsync(CancellationToken cancellat
// Make sure to also dispose the data within the current view model whether the navigation is successful or not
using (RegisterViewModel.CurrentViewModel)
{
+ // We don't need to set the Version property since the creator will always initialize with the latest one
var vaultOptions = new VaultOptions()
{
+ AuthenticationMethod = [ RegisterViewModel.CurrentViewModel.Id ],
ContentCipherId = ContentCipher.Id,
FileNameCipherId = FileNameCipher.Id,
NameEncodingId = EncodingOption.Id,
- AuthenticationMethod = [ RegisterViewModel.CurrentViewModel.Id ],
+ RecycleBinSize = 0L,
VaultId = _vaultId
};
@@ -121,7 +123,7 @@ public override async void OnAppearing()
RegisterViewModel.CurrentViewModel = AuthenticationOptions.FirstOrDefault();
return;
- static void EnumerateOptions(IEnumerable source, ICollection destination)
+ static void EnumerateOptions(IEnumerable source, ICollection destination)
{
destination.Clear();
foreach (var item in source)
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/LocationWizardViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/LocationWizardViewModel.cs
index 64d740a2c..8da04a5c1 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/LocationWizardViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/LocationWizardViewModel.cs
@@ -27,7 +27,7 @@ public sealed partial class LocationWizardViewModel : BaseWizardViewModel
[ObservableProperty] private string? _Message;
[ObservableProperty] private string? _SelectedLocation;
- [ObservableProperty] private SeverityType _Severity;
+ [ObservableProperty] private Severity _Severity;
public NewVaultCreationType CreationType { get; }
@@ -40,18 +40,22 @@ public LocationWizardViewModel(IVaultCollectionModel vaultCollectionModel, NewVa
CreationType = creationType;
CanCancel = true;
CanContinue = false;
- CancelText = "Cancel".ToLocalized();
- ContinueText = "Continue".ToLocalized();
+ PrimaryText = "Continue".ToLocalized();
+ SecondaryText = "Cancel".ToLocalized();
Title = creationType == NewVaultCreationType.AddExisting ? "AddExisting".ToLocalized() : "CreateNew".ToLocalized();
}
///
- public override Task TryContinueAsync(CancellationToken cancellationToken)
+ public override async Task TryContinueAsync(CancellationToken cancellationToken)
{
if (SelectedFolder is null)
- return Task.FromResult(Result.Failure(null));
+ return Result.Failure(null);
- return Task.FromResult(Result.Success);
+ // Confirm bookmark
+ if (SelectedFolder is IBookmark bookmark)
+ await bookmark.AddBookmarkAsync(cancellationToken);
+
+ return Result.Success;
}
///
@@ -69,11 +73,7 @@ public override async void OnAppearing()
[RelayCommand]
private async Task SelectLocationAsync(CancellationToken cancellationToken)
{
- // Remove previous bookmark
- if (SelectedFolder is IBookmark bookmark)
- await bookmark.RemoveBookmarkAsync(cancellationToken);
-
- SelectedFolder = await FileExplorerService.PickFolderAsync(true, cancellationToken);
+ SelectedFolder = await FileExplorerService.PickFolderAsync(null, true, cancellationToken);
CanContinue = await UpdateStatusAsync(cancellationToken);
}
@@ -84,7 +84,7 @@ public async Task UpdateStatusAsync(CancellationToken cancellationToken =
// No folder selected
if (SelectedFolder is null)
{
- Severity = SeverityType.Default;
+ Severity = Severity.Default;
Message = "SelectFolderToContinue".ToLocalized();
return false;
}
@@ -93,7 +93,7 @@ public async Task UpdateStatusAsync(CancellationToken cancellationToken =
var isDuplicate = _vaultCollectionModel.Any(x => x.Folder.Id == SelectedFolder.Id);
if (isDuplicate)
{
- Severity = SeverityType.Warning;
+ Severity = Severity.Warning;
Message = "VaultAlreadyExists".ToLocalized();
return false;
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/MainWizardViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/MainWizardViewModel.cs
index 46abe4b95..7105078cc 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/MainWizardViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/MainWizardViewModel.cs
@@ -23,8 +23,8 @@ public MainWizardViewModel(IVaultCollectionModel vaultCollectionModel)
CanCancel = true;
CanContinue = true;
Title = "AddNewVault".ToLocalized();
- CancelText = "Cancel".ToLocalized();
- ContinueText = "Continue".ToLocalized();
+ PrimaryText = "Continue".ToLocalized();
+ SecondaryText = "Cancel".ToLocalized();
}
///
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/RecoveryWizardViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/RecoveryWizardViewModel.cs
index f65a3d02e..6317ee228 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/RecoveryWizardViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/RecoveryWizardViewModel.cs
@@ -30,8 +30,8 @@ public RecoveryWizardViewModel(IFolder folder, IResult? additionalData)
{
ServiceProvider = DI.Default;
Title = "VaultRecovery".ToLocalized();
- ContinueText = "Continue".ToLocalized();
- CancelText = "Cancel".ToLocalized();
+ PrimaryText = "Continue".ToLocalized();
+ SecondaryText = "Cancel".ToLocalized();
CanContinue = true;
CanCancel = false;
Folder = folder;
@@ -45,7 +45,7 @@ public RecoveryWizardViewModel(IFolder folder, IResult? additionalData)
RecoveryViewModel = new()
{
VaultId = _vaultId,
- VaultName = Folder.Name,
+ Title = Folder.Name,
RecoveryKey = _unlockContract?.ToString()
};
}
diff --git a/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/SummaryWizardViewModel.cs b/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/SummaryWizardViewModel.cs
index 67e4b80b5..621c32e9b 100644
--- a/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/SummaryWizardViewModel.cs
+++ b/src/SecureFolderFS.Sdk/ViewModels/Views/Wizard/SummaryWizardViewModel.cs
@@ -24,8 +24,8 @@ public sealed partial class SummaryWizardViewModel : BaseWizardViewModel
public SummaryWizardViewModel(IFolder folder, IVaultCollectionModel vaultCollectionModel)
{
Title = "Summary".ToLocalized();
- CancelText = "Close".ToLocalized();
- ContinueText = null;
+ SecondaryText = "Close".ToLocalized();
+ PrimaryText = null;
CanContinue = true;
CanCancel = true;
VaultName = folder.Name;
diff --git a/src/SecureFolderFS.Shared/ComponentModel/IAuthenticator.cs b/src/SecureFolderFS.Shared/ComponentModel/IAuthenticator.cs
index 5fda300ec..2cff9a63b 100644
--- a/src/SecureFolderFS.Shared/ComponentModel/IAuthenticator.cs
+++ b/src/SecureFolderFS.Shared/ComponentModel/IAuthenticator.cs
@@ -11,7 +11,7 @@ public interface IAuthenticator
///
/// Removes any associated authentication profiles for the provided .
///
- /// The ID that uniquely identifies each authentication transaction.
+ /// The persistent ID that uniquely identifies each authentication transaction.
/// A that cancels this action.
/// A that represents the asynchronous operation.
Task RevokeAsync(string? id, CancellationToken cancellationToken = default);
@@ -19,7 +19,7 @@ public interface IAuthenticator
///
/// Creates a new authentication for the user.
///
- /// The ID that uniquely identifies each authentication transaction.
+ /// The persistent ID that uniquely identifies each authentication transaction.
/// The optional data to sign.
/// A that cancels this action.
/// A that represents the asynchronous operation. If successful, value is that represents the authentication.
@@ -28,7 +28,7 @@ public interface IAuthenticator
///
/// Authenticates the user asynchronously.
///
- /// The ID that uniquely identifies each authentication transaction.
+ /// The persistent ID that uniquely identifies each authentication transaction.
/// The optional data to sign.
/// A that cancels this action.
/// A that represents the asynchronous operation. If successful, value is that represents the authentication.
diff --git a/src/SecureFolderFS.Shared/ComponentModel/IImageStream.cs b/src/SecureFolderFS.Shared/ComponentModel/IImageStream.cs
new file mode 100644
index 000000000..b50d130b7
--- /dev/null
+++ b/src/SecureFolderFS.Shared/ComponentModel/IImageStream.cs
@@ -0,0 +1,11 @@
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SecureFolderFS.Shared.ComponentModel
+{
+ public interface IImageStream : IImage
+ {
+ Task CopyToAsync(Stream destination, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/SecureFolderFS.Shared/ComponentModel/IItemSorter.cs b/src/SecureFolderFS.Shared/ComponentModel/IItemSorter.cs
new file mode 100644
index 000000000..9c58499a6
--- /dev/null
+++ b/src/SecureFolderFS.Shared/ComponentModel/IItemSorter.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+
+namespace SecureFolderFS.Shared.ComponentModel
+{
+ public interface IItemSorter : IComparer
+ {
+ ///
+ /// Determines the correct index for inserting a new item so that the collection remains sorted.
+ /// Uses a binary search for efficiency.
+ ///
+ /// The new file system item to insert.
+ /// The already sorted collection.
+ /// The index at which to insert the new item.
+ int GetInsertIndex(T newItem, ICollection collection);
+
+ ///
+ /// Sorts the entire collection according to the current sorting rules.
+ ///
+ /// The items source.
+ /// The destination collection to sort.
+ void SortCollection(IEnumerable source, ICollection destination);
+ }
+}
diff --git a/src/SecureFolderFS.Shared/ComponentModel/IPassword.cs b/src/SecureFolderFS.Shared/ComponentModel/IPassword.cs
index 6a0553fa1..a36d7c77a 100644
--- a/src/SecureFolderFS.Shared/ComponentModel/IPassword.cs
+++ b/src/SecureFolderFS.Shared/ComponentModel/IPassword.cs
@@ -14,7 +14,7 @@ public interface IPassword : IKey
///
/// The number of characters may not be equal to the number of bytes in the password.
///
- int Length { get; }
+ int CharacterCount { get; }
///
/// Gets the password as a sequence of characters.
diff --git a/src/SecureFolderFS.Shared/DI.cs b/src/SecureFolderFS.Shared/DI.cs
index 86a151318..b22c1cd82 100644
--- a/src/SecureFolderFS.Shared/DI.cs
+++ b/src/SecureFolderFS.Shared/DI.cs
@@ -49,7 +49,7 @@ public T GetService()
{
if (_serviceProvider is null)
return default;
-
+
return (T?)GetService(typeof(T));
}
diff --git a/src/SecureFolderFS.Shared/Extensions/CollectionExtensions.cs b/src/SecureFolderFS.Shared/Extensions/CollectionExtensions.cs
index 6638bb475..a1302ef0d 100644
--- a/src/SecureFolderFS.Shared/Extensions/CollectionExtensions.cs
+++ b/src/SecureFolderFS.Shared/Extensions/CollectionExtensions.cs
@@ -2,11 +2,18 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
+using SecureFolderFS.Shared.ComponentModel;
namespace SecureFolderFS.Shared.Extensions
{
public static class CollectionExtensions
{
+ public static void Insert(this IList collection, T item, IItemSorter sorter)
+ {
+ var correctIndex = sorter.GetInsertIndex(item, collection);
+ collection.Insert(correctIndex, item);
+ }
+
public static ICollection ToOrAsCollection(this IEnumerable enumerable)
{
if (enumerable is ICollection collection)
@@ -14,20 +21,20 @@ public static ICollection ToOrAsCollection(this IEnumerable enumerable)
return enumerable.ToArray();
}
-
+
public static TDestination? FirstOrDefaultType(this IEnumerable enumerable)
where TDestination : class, TSource
{
return enumerable.FirstOrDefault(x => x is TDestination) as TDestination;
}
- public static TDestination GetOrAdd(this ICollection collection, Func create)
+ public static TDestination GetOrAdd(this ICollection collection, Func