From 4cbd86373c01e00011de3f61c73a6082685cbe33 Mon Sep 17 00:00:00 2001 From: IT Hit Date: Fri, 29 Mar 2024 00:23:27 +0200 Subject: [PATCH] v8.1.26727.0-Beta2 --- Common/Common.csproj | 4 +- Windows/Common/Core/Commands.cs | 20 +- .../Common/Core/Common.Windows.Core.csproj | 6 +- Windows/Common/Core/Registrar.cs | 46 +-- .../Common.Windows.VirtualDrive.csproj | 6 +- .../VirtualDrive/CustomDataExtensions.cs | 2 +- .../Common/VirtualDrive/VirtualEngineBase.cs | 2 + .../VirtualDrive.ShellExtension.csproj | 4 +- .../VirtualDrive/VirtualDrive.csproj | 4 +- .../VirtualFileSystem.csproj | 4 +- .../WebDAVDrive.ShellExtension.csproj | 4 +- .../WebDAVDrive.UI/ChallengeLogin.xaml | 94 ----- .../WebDAVDrive.UI/ChallengeLogin.xaml.cs | 127 ------- .../WebDAVDrive.UI/CredentialManager.cs | 8 +- .../WebDAVDrive/WebDAVDrive.UI/Styles.xaml | 61 +-- .../ViewModels/ChallengeLoginViewModel.cs | 66 ---- .../WebDAVDrive.UI/WebDAVDrive.UI.csproj | 2 +- .../WebDAVDrive/WebDAVDrive/AppSettings.cs | 9 +- Windows/WebDAVDrive/WebDAVDrive/Program.cs | 6 +- .../RemoteStorageMonitorBase.cs | 149 +++++--- .../RemoteStorageMonitorCRUDE.cs | 2 +- .../RemoteStorageMonitorSyncId.cs | 2 +- .../WebDAVDrive/WebDAVDrive/VirtualEngine.cs | 75 ++-- .../WebDAVDrive/WebDAVDrive/VirtualFolder.cs | 18 +- .../WebDAVDrive/WebDAVDrive.csproj | 4 +- .../WebDAVDrive/WebDAVDrive/appsettings.json | 7 + macOS/Common/Core/Common.Core.csproj | 2 +- macOS/Common/Core/SecureStorageBase.cs | 18 +- .../FileProviderExtension.csproj | 2 +- .../FileProviderExtension/VirtualEngine.cs | 22 +- macOS/WebDAVDrive/README.md | 8 + macOS/WebDAVDrive/WebDAVCommon/AppSettings.cs | 9 +- .../WebDAVCommon/DomainSettings.cs | 17 - .../WebDAVDrive/WebDAVCommon/SecureStorage.cs | 3 - .../WebDAVCommon/WebDavSessionUtils.cs | 11 + .../VirtualEngine.cs | 29 +- .../VirtualFileSystemItem.cs | 37 +- .../WebDAVFileProviderExtension.csproj | 2 +- .../Entitlements.plist | 2 + .../FPUIActionExtension.cs | 18 +- .../ViewControllers/AuthViewController.cs | 4 +- .../CookiesAuthViewController.cs | 154 ++++++++ .../WebDAVFileProviderUIExtension.csproj | 2 +- macOS/WebDAVDrive/WebDAVMacApp/AppDelegate.cs | 358 ++++++++---------- macOS/WebDAVDrive/WebDAVMacApp/Info.plist | 13 + .../WebDAVMacApp/RemoteStorageMonitor.cs | 17 +- .../WebDAVMacApp/Resources/appsettings.json | 22 +- .../WebDAVMacApp/WebDAVMacApp.csproj | 2 +- 48 files changed, 679 insertions(+), 805 deletions(-) delete mode 100644 Windows/WebDAVDrive/WebDAVDrive.UI/ChallengeLogin.xaml delete mode 100644 Windows/WebDAVDrive/WebDAVDrive.UI/ChallengeLogin.xaml.cs delete mode 100644 Windows/WebDAVDrive/WebDAVDrive.UI/ViewModels/ChallengeLoginViewModel.cs delete mode 100644 macOS/WebDAVDrive/WebDAVCommon/DomainSettings.cs create mode 100644 macOS/WebDAVDrive/WebDAVFileProviderUIExtension/ViewControllers/CookiesAuthViewController.cs diff --git a/Common/Common.csproj b/Common/Common.csproj index bb2ea3b..9dd9901 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -10,7 +10,7 @@ AnyCPU;x64 - - + + \ No newline at end of file diff --git a/Windows/Common/Core/Commands.cs b/Windows/Common/Core/Commands.cs index 05d1111..0cabf55 100644 --- a/Windows/Common/Core/Commands.cs +++ b/Windows/Common/Core/Commands.cs @@ -174,22 +174,28 @@ public async Task StopEngineAsync() /// Opens Windows File Manager with both remote storage and user file system for testing. /// /// True if the Remote Storage must be opened. False - otherwise. + /// Index used to position Windows Explorer window to show this user file system. + /// Total number of Engined that will be mounted by this app. /// This method is provided solely for the development and testing convenience. - public void ShowTestEnvironment(string userFileSystemWindowName, bool openRemoteStorage = true, CancellationToken cancellationToken = default) + public void ShowTestEnvironment(string userFileSystemWindowName, bool openRemoteStorage = true, CancellationToken cancellationToken = default, int engineIndex = 0, int totalEngines = 1) { - // Open Windows File Manager with user file system. - Commands.Open(Engine.Path); - IntPtr hWndUserFileSystem = WindowManager.FindWindow(userFileSystemWindowName, cancellationToken); - WindowManager.PositionFileSystemWindow(hWndUserFileSystem, 1, 2); + int numWindowsPerEngine = 2; //openRemoteStorage ? 2 : 1; // Each engine shows 2 windows - remote storage and UFS. + int horizintalIndex = engineIndex * numWindowsPerEngine; + int totalWindows = totalEngines * numWindowsPerEngine; + // Open remote storage. if (openRemoteStorage) { - // Open remote storage. Commands.Open(RemoteStorageRootPath); string rsWindowName = Path.GetFileName(RemoteStorageRootPath.TrimEnd('\\')); IntPtr hWndRemoteStorage = WindowManager.FindWindow(rsWindowName, cancellationToken); - WindowManager.PositionFileSystemWindow(hWndRemoteStorage, 0, 2); + WindowManager.PositionFileSystemWindow(hWndRemoteStorage, horizintalIndex, totalWindows); } + + // Open Windows File Manager with user file system. + Commands.Open(Engine.Path); + IntPtr hWndUserFileSystem = WindowManager.FindWindow(userFileSystemWindowName, cancellationToken); + WindowManager.PositionFileSystemWindow(hWndUserFileSystem, horizintalIndex + 1, totalWindows); } #endif diff --git a/Windows/Common/Core/Common.Windows.Core.csproj b/Windows/Common/Core/Common.Windows.Core.csproj index 0460621..bc3a7c4 100644 --- a/Windows/Common/Core/Common.Windows.Core.csproj +++ b/Windows/Common/Core/Common.Windows.Core.csproj @@ -1,6 +1,6 @@ - net7.0-windows10.0.19041.0;net48 + net8.0-windows10.0.19041.0;net48 True Contains functionality common for all Windows Virtual Drive samples. IT Hit LTD. @@ -21,8 +21,8 @@ - - + + \ No newline at end of file diff --git a/Windows/Common/Core/Registrar.cs b/Windows/Common/Core/Registrar.cs index ed12987..567bb85 100644 --- a/Windows/Common/Core/Registrar.cs +++ b/Windows/Common/Core/Registrar.cs @@ -119,7 +119,7 @@ public static async Task UnregisterSyncRootAsync(string syncRootPath, stri if (res && !string.IsNullOrWhiteSpace(dataPath)) { - log.Debug($"Deleteing data folder {syncRootPath} {dataPath}"); + log.Debug($"Deleteing data folder for {syncRootPath}"); try { Directory.Delete(dataPath, true); @@ -172,6 +172,20 @@ private static async Task UnregisterSyncRootAsync(string syncRootPath, ILo } } + // Remore the read-only arrtibute. Otherwise delete fails. + var allItems = Directory.EnumerateFileSystemEntries(syncRootPath, "*", SearchOption.AllDirectories); + foreach(var path in allItems) + { + try + { + new FileInfo(path).IsReadOnly = false; + } + catch (Exception ex) + { + logger.Error($"Failed to remove read-only attribute for {path}", ex); + } + } + // Delete sync root folder. try { @@ -186,36 +200,6 @@ private static async Task UnregisterSyncRootAsync(string syncRootPath, ILo return res; } - /* - public async Task CleanupAppFoldersAsync(EngineWindows engine) - { - Log.Info("\n\nDeleting all file and folder placeholders."); - try - { - if (Directory.Exists(UserFileSystemRootPath)) - { - Directory.Delete(UserFileSystemRootPath, true); - } - } - catch (Exception ex) - { - Log.Error("Failed to delete placeholders.", ex); - } - - try - { - if (engine != null && !string.IsNullOrWhiteSpace(engine.DataPath)) - { - Directory.Delete(engine.DataPath, true); - } - } - catch (Exception ex) - { - Log.Error($"\n{ex}"); - } - } - */ - /// /// Unregisters all sync roots that has a provider ID and removes all components. /// diff --git a/Windows/Common/VirtualDrive/Common.Windows.VirtualDrive.csproj b/Windows/Common/VirtualDrive/Common.Windows.VirtualDrive.csproj index 3e954d4..f2073d7 100644 --- a/Windows/Common/VirtualDrive/Common.Windows.VirtualDrive.csproj +++ b/Windows/Common/VirtualDrive/Common.Windows.VirtualDrive.csproj @@ -1,6 +1,6 @@ - net7.0-windows10.0.19041.0 + net8.0-windows10.0.19041.0 Contains functionality common for all Windows Virtual Drive samples. IT Hit LTD. IT Hit User File System @@ -13,8 +13,8 @@ - - + + diff --git a/Windows/Common/VirtualDrive/CustomDataExtensions.cs b/Windows/Common/VirtualDrive/CustomDataExtensions.cs index d606171..8e3f617 100644 --- a/Windows/Common/VirtualDrive/CustomDataExtensions.cs +++ b/Windows/Common/VirtualDrive/CustomDataExtensions.cs @@ -14,7 +14,7 @@ namespace ITHit.FileSystem.Samples.Common.Windows public static class CustomDataExtensions { /// - /// Saves all custom metadata properties (eTag, locks, etc) to storage associated with an item. + /// Saves all custom metadata properties, such as locks, to storage associated with an item. /// This data that is displayed in custom columns in file manager. /// /// Custom data attached to the item. diff --git a/Windows/Common/VirtualDrive/VirtualEngineBase.cs b/Windows/Common/VirtualDrive/VirtualEngineBase.cs index f634208..12adbb6 100644 --- a/Windows/Common/VirtualDrive/VirtualEngineBase.cs +++ b/Windows/Common/VirtualDrive/VirtualEngineBase.cs @@ -84,6 +84,7 @@ public VirtualEngineBase( Debug += logFormatter.LogDebug; } + /// /// Fired for each file or folder change. /// @@ -160,6 +161,7 @@ private void LogItemChange(ItemsChangeEventArgs e, ChangeEventItem item) break; } } + /// public override async Task FilterAsync(SyncDirection direction, OperationType operationType, string path, FileSystemItemType itemType, string newPath, IOperationContext operationContext) diff --git a/Windows/VirtualDrive/VirtualDrive.ShellExtension/VirtualDrive.ShellExtension.csproj b/Windows/VirtualDrive/VirtualDrive.ShellExtension/VirtualDrive.ShellExtension.csproj index 0619f87..fae31cb 100644 --- a/Windows/VirtualDrive/VirtualDrive.ShellExtension/VirtualDrive.ShellExtension.csproj +++ b/Windows/VirtualDrive/VirtualDrive.ShellExtension/VirtualDrive.ShellExtension.csproj @@ -1,6 +1,6 @@ - net7.0-windows10.0.19041.0 + net8.0-windows10.0.19041.0 True x64 @@ -19,7 +19,7 @@ - + diff --git a/Windows/VirtualDrive/VirtualDrive/VirtualDrive.csproj b/Windows/VirtualDrive/VirtualDrive/VirtualDrive.csproj index d568490..d9b0ed1 100644 --- a/Windows/VirtualDrive/VirtualDrive/VirtualDrive.csproj +++ b/Windows/VirtualDrive/VirtualDrive/VirtualDrive.csproj @@ -1,7 +1,7 @@ Exe - net7.0-windows10.0.19041.0 + net8.0-windows10.0.19041.0 IT Hit LTD. IT Hit LTD. Virtual Drive @@ -40,7 +40,7 @@ This is an advanced project with ETags support, Microsoft Office documents editi - + diff --git a/Windows/VirtualFileSystem/VirtualFileSystem.csproj b/Windows/VirtualFileSystem/VirtualFileSystem.csproj index 8fb19dd..c31df4e 100644 --- a/Windows/VirtualFileSystem/VirtualFileSystem.csproj +++ b/Windows/VirtualFileSystem/VirtualFileSystem.csproj @@ -1,7 +1,7 @@ Exe - net7.0-windows10.0.19041.0 + net8.0-windows10.0.19041.0 IT Hit LTD. IT Hit LTD. Virtual File System @@ -35,7 +35,7 @@ This project does not support ETags, locking, Microsoft Office documents editing - + diff --git a/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/WebDAVDrive.ShellExtension.csproj b/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/WebDAVDrive.ShellExtension.csproj index a8c2633..f7b430a 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/WebDAVDrive.ShellExtension.csproj +++ b/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/WebDAVDrive.ShellExtension.csproj @@ -1,6 +1,6 @@ - net7.0-windows10.0.19041.0 + net8.0-windows10.0.19041.0 True x64 @@ -12,7 +12,7 @@ IT HIT LTD. - + diff --git a/Windows/WebDAVDrive/WebDAVDrive.UI/ChallengeLogin.xaml b/Windows/WebDAVDrive/WebDAVDrive.UI/ChallengeLogin.xaml deleted file mode 100644 index bbae9fe..0000000 --- a/Windows/WebDAVDrive/WebDAVDrive.UI/ChallengeLogin.xaml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Windows/WebDAVDrive/WebDAVDrive.UI/ChallengeLogin.xaml.cs b/Windows/WebDAVDrive/WebDAVDrive.UI/ChallengeLogin.xaml.cs deleted file mode 100644 index 455a178..0000000 --- a/Windows/WebDAVDrive/WebDAVDrive.UI/ChallengeLogin.xaml.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; -using WebDAVDrive.UI.ViewModels; -using System.Security; -using System.Runtime.InteropServices; - -namespace WebDAVDrive.UI -{ - /// - /// Interaction logic for ChallengeLogin.xaml - /// - public partial class ChallengeLogin : Window - { - public ChallengeLogin() - { - InitializeComponent(); - - this.DataContext = new ChallengeLoginViewModel(); - - //Setting backgorund color from windows settings - Brush brush = SystemParameters.WindowGlassBrush.Clone(); - Color backColor = ((SolidColorBrush)brush).Color; - - //Calculating text color from backgorund - //Using of luma, for more see https://en.wikipedia.org/wiki/Luma_%28video%29 - var l = 0.2126 * backColor.ScR + 0.7152 * backColor.ScG + 0.0722 * backColor.ScB; - Brush textColor = l < 0.5 ? Brushes.White : Brushes.Black; - - //set form background and foreground color - this.Resources["FormBackground"] = brush; - this.Resources["FormForeground"] = textColor; - } - - /// - /// This method updates Password in ViewModel (we cannot bind text directly from PasswordBox as with TexBox in security reasons) - /// - private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e) - { - if (this.DataContext != null) - { ((dynamic)this.DataContext).Password = ((PasswordBox)sender).SecurePassword; } - } - - private void Button_Ok_Click(object sender, RoutedEventArgs e) - { - Window.GetWindow(this).DialogResult = true; - Close(); - } - - private void Button_Cancel_Click(object sender, RoutedEventArgs e) - { - Window.GetWindow(this).DialogResult = false; - Close(); - } - - private void OnCloseButtonClick(object sender, RoutedEventArgs e) - { - this.Close(); - } - } - - /// - /// Class PasswordBoxMonitor used to monitor state of PasswordBox and if length is 0 placeholder will be shown. - /// WPF does not contain defalut mechanism for placehloders(watermarks) like in WinForms, so we need this solution - /// - public class PasswordBoxMonitor : DependencyObject - { - public static bool GetIsMonitoring(DependencyObject obj) - { - return (bool)obj.GetValue(IsMonitoringProperty); - } - - public static void SetIsMonitoring(DependencyObject obj, bool value) - { - obj.SetValue(IsMonitoringProperty, value); - } - - public static readonly DependencyProperty IsMonitoringProperty = DependencyProperty.RegisterAttached("IsMonitoring", typeof(bool), typeof(PasswordBoxMonitor), new UIPropertyMetadata(false, OnIsMonitoringChanged)); - - public static int GetPasswordLength(DependencyObject obj) - { - return (int)obj.GetValue(PasswordLengthProperty); - } - - public static void SetPasswordLength(DependencyObject obj, int value) - { - obj.SetValue(PasswordLengthProperty, value); - } - - public static readonly DependencyProperty PasswordLengthProperty = DependencyProperty.RegisterAttached("PasswordLength", typeof(int), typeof(PasswordBoxMonitor), new UIPropertyMetadata(0)); - - private static void OnIsMonitoringChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var pb = d as PasswordBox; - if (pb == null) - { - return; - } - if ((bool)e.NewValue) - { - pb.PasswordChanged += PasswordChanged; - } - else - { - pb.PasswordChanged -= PasswordChanged; - } - } - - static void PasswordChanged(object sender, RoutedEventArgs e) - { - var pb = sender as PasswordBox; - if (pb == null) - { - return; - } - SetPasswordLength(pb, pb.Password.Length); - } - } -} diff --git a/Windows/WebDAVDrive/WebDAVDrive.UI/CredentialManager.cs b/Windows/WebDAVDrive/WebDAVDrive.UI/CredentialManager.cs index 421f6ec..373a218 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.UI/CredentialManager.cs +++ b/Windows/WebDAVDrive/WebDAVDrive.UI/CredentialManager.cs @@ -20,13 +20,13 @@ public class CredentialManager /// /// Resource name under which credentials will be saved. /// User name to be saved. - /// Password to be saved. - public static void SaveCredentials(string resource, string login, SecureString securePassword) + /// Password to be saved. + public static void SaveCredentials(string resource, string login, string password) { PasswordVault vault = new PasswordVault(); //retrive string password form SecureString (password can not be retrived directly from Security string) - string password = new System.Net.NetworkCredential(string.Empty, securePassword).Password; + //string password = new System.Net.NetworkCredential(string.Empty, securePassword).Password; PasswordCredential credential = new PasswordCredential() { @@ -35,7 +35,7 @@ public static void SaveCredentials(string resource, string login, SecureString s Resource = resource }; - //save credential in Credential Manager + // Save credentials in vault. vault.Add(credential); } diff --git a/Windows/WebDAVDrive/WebDAVDrive.UI/Styles.xaml b/Windows/WebDAVDrive/WebDAVDrive.UI/Styles.xaml index 749535a..040665f 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.UI/Styles.xaml +++ b/Windows/WebDAVDrive/WebDAVDrive.UI/Styles.xaml @@ -48,31 +48,6 @@ - - - - - - + diff --git a/Windows/WebDAVDrive/WebDAVDrive.UI/ViewModels/ChallengeLoginViewModel.cs b/Windows/WebDAVDrive/WebDAVDrive.UI/ViewModels/ChallengeLoginViewModel.cs deleted file mode 100644 index f78be1f..0000000 --- a/Windows/WebDAVDrive/WebDAVDrive.UI/ViewModels/ChallengeLoginViewModel.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Security; - - -namespace WebDAVDrive.UI.ViewModels -{ - /// - /// ViewModel for login window - /// - public class ChallengeLoginViewModel : BaseViewModel - { - private string login; - public string Login - { - get=>login; - set - { - login = value; - OnPropertyChanged(nameof(Login)); - } - } - private SecureString password; - public SecureString Password - { - get=>password; - set - { - password = value; - OnPropertyChanged(nameof(Password)); - } - } - - private string url; - - /// - /// Url of WebDAV server - /// - public string Url - { - get => url; - set - { - url = value; - OnPropertyChanged(nameof(Url)); - } - } - - private bool keepLogedIn; - /// - /// State of KeepLogedIn checkbox - /// - public bool KeepLogedIn - { - get => keepLogedIn; - set - { - keepLogedIn = value; - OnPropertyChanged(nameof(KeepLogedIn)); - } - } - - /// - /// Titile, displayd on head of login window - /// - public string WindowTitle { get; set; } - } -} diff --git a/Windows/WebDAVDrive/WebDAVDrive.UI/WebDAVDrive.UI.csproj b/Windows/WebDAVDrive/WebDAVDrive.UI/WebDAVDrive.UI.csproj index c458e01..a7f2c0e 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.UI/WebDAVDrive.UI.csproj +++ b/Windows/WebDAVDrive/WebDAVDrive.UI/WebDAVDrive.UI.csproj @@ -1,7 +1,7 @@  - net7.0-windows10.0.19041.0 + net8.0-windows10.0.19041.0 true True IT Hit LTD. diff --git a/Windows/WebDAVDrive/WebDAVDrive/AppSettings.cs b/Windows/WebDAVDrive/WebDAVDrive/AppSettings.cs index 0bd5ee9..b3e7462 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/AppSettings.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/AppSettings.cs @@ -6,6 +6,7 @@ using ITHit.FileSystem.Samples.Common; using WebDAVDrive.UI; +using ITHit.FileSystem.Synchronization; namespace WebDAVDrive @@ -76,11 +77,15 @@ public class AppSettings : Settings public bool ShellExtensionsComServerRpcEnabled { get; set; } /// - // Mark documents locked by other users as read-only for this user and vice versa. - // A read-only MS Office document opens in a view-only mode preventing document collisions. + /// Mark documents locked by other users as read-only for this user and vice versa. + /// A read-only MS Office document opens in a view-only mode preventing document collisions. /// public bool SetLockReadOnly { get; set; } + /// + /// Preferred incoming synchronization mode. + /// + public IncomingSyncMode PreferredIncomingSyncMode { get; set; } } /// diff --git a/Windows/WebDAVDrive/WebDAVDrive/Program.cs b/Windows/WebDAVDrive/WebDAVDrive/Program.cs index 8cabc47..4cf7861 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/Program.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/Program.cs @@ -261,7 +261,7 @@ private static async Task TryCreateEngineAsync(string webDAVServerUrl, str consoleProcessor.Commands.TryAdd(engine.InstanceId, engine.Commands); engine.SyncService.SyncIntervalMs = Settings.SyncIntervalMs; - engine.SyncService.IncomingSyncMode = ITHit.FileSystem.Synchronization.IncomingSyncMode.SyncId; + engine.SyncService.IncomingSyncMode = Settings.PreferredIncomingSyncMode; engine.AutoLock = Settings.AutoLock; engine.MaxTransferConcurrentRequests = Settings.MaxTransferConcurrentRequests.Value; engine.MaxOperationsConcurrentRequests = Settings.MaxOperationsConcurrentRequests.Value; @@ -277,7 +277,7 @@ private static async Task TryCreateEngineAsync(string webDAVServerUrl, str await engine.StartAsync(); #if DEBUG // Start Windows File Manager with user file system folder. - engine.Commands.ShowTestEnvironment(GetDisplayName(webDAVServerUrl), false); + engine.Commands.ShowTestEnvironment(GetDisplayName(webDAVServerUrl), false, default, Engines.Count-1, Settings.WebDAVServerURLs.Count()); #endif return true; } @@ -337,7 +337,7 @@ public static async Task RemoveEngineAsync(VirtualEngine engine, bool unregister } engine.Dispose(); - // Refresh Windows Explorer. + // Refresh Windows Explorer to remove the root node. PlaceholderItem.UpdateUI(Path.GetDirectoryName(engine.Path)); // If no Engines are running exit the app. diff --git a/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorBase.cs b/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorBase.cs index 557b0ef..3680330 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorBase.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorBase.cs @@ -112,7 +112,7 @@ internal RemoteStorageMonitorBase(string webSocketServerUrl, int maxQueueLength, { this.WebSocketServerUrl = webSocketServerUrl; this.MaxQueueLength = maxQueueLength; - this.Logger = logger.CreateLogger($"RS Monitor {SyncMode}"); + this.Logger = logger; } /// @@ -156,76 +156,99 @@ private async Task RunWebSocketsAsync(CancellationToken cancellationToken) /// /// Starts monitoring changes in the remote storage. /// - /// Cookies to add to web sockets requests. /// The token to monitor for cancellation requests. public async Task StartAsync(CancellationToken cancellationToken = default) { + // Start sockets after first successeful WebDAV PROPFIND. Logger.LogDebug("Starting", WebSocketServerUrl); + // Configure web sockets and connect to the server. + // If connection fails this method throws exception. + // This will signal to the caller that web sockets are not supported. + clientWebSocket = await ConnectWebSocketsAsync(cancellationToken); + await Task.Factory.StartNew( - async () => - { - using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) - { - // Create task for processing websocket events. - handlerChangesTask = CreateHandlerChangesTask(cancellationTokenSource.Token); - - bool repeat = false; - do - { - using (clientWebSocket = new ClientWebSocket()) - { - try - { - repeat = false; - - // Configure web sockets and connect to the server. - clientWebSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(10); - if (Credentials != null) - { - clientWebSocket.Options.Credentials = Credentials; - } - if (Cookies != null) - { - clientWebSocket.Options.Cookies = new CookieContainer(); - clientWebSocket.Options.Cookies.Add(Cookies); - } - if (InstanceId != Guid.Empty) - { - clientWebSocket.Options.SetRequestHeader("InstanceId", InstanceId.ToString()); - } - await clientWebSocket.ConnectAsync(new Uri(WebSocketServerUrl), cancellationToken); - Logger.LogMessage("Connected", WebSocketServerUrl); - - // After esteblishing connection with a server, in case of Sync ID algorithm, - // we must get all changes from the remote storage. - // This is required on Engine start, server recovery, network recovery, etc. - Logger.LogDebug("Getting all changes from server", WebSocketServerUrl); - await ProcessAsync(null); - - Logger.LogMessage("Started", WebSocketServerUrl); - - await RunWebSocketsAsync(cancellationTokenSource.Token); - } - catch (Exception e) when (e is WebSocketException || e is AggregateException) - { - // Start socket after first successeful WebDAV PROPFIND. Restart socket if disconnected. - if (clientWebSocket != null && clientWebSocket?.State != WebSocketState.Closed) - { - Logger.LogError(e.Message, WebSocketServerUrl, null, e); - } - - // Here we delay WebSocket connection to avoid overload on - // network disconnections or server failure. - await Task.Delay(TimeSpan.FromSeconds(2), cancellationTokenSource.Token); - repeat = true; - }; - } - } while (repeat && !cancellationToken.IsCancellationRequested); - } + async () => + { + using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) + { + // Create task for processing websocket events. + handlerChangesTask = CreateHandlerChangesTask(cancellationTokenSource.Token); + + // Restart socket if disconnected. + bool repeat = false; + do + { + using (clientWebSocket ??= await ConnectWebSocketsAsync(cancellationToken)) + { + try + { + repeat = false; + + // After esteblishing connection with a server, + // we must get all changes from the remote storage. + // This is required on Engine start, server recovery, network recovery, etc. + Logger.LogDebug("Getting changes from server", WebSocketServerUrl); + await ProcessAsync(null); + + Logger.LogMessage("Started", WebSocketServerUrl); + + await RunWebSocketsAsync(cancellationTokenSource.Token); + } + catch (Exception e) when (e is WebSocketException || e is AggregateException) + { + if (clientWebSocket != null && clientWebSocket?.State != WebSocketState.Closed) + { + Logger.LogError(e.Message, WebSocketServerUrl, null, e); + } + else + { + Logger.LogDebug(e.Message, WebSocketServerUrl); + } + + // Here we delay WebSocket connection to avoid overload on + // network disconnections or server failure. + await Task.Delay(TimeSpan.FromSeconds(2), cancellationTokenSource.Token); + repeat = true; + }; + } + clientWebSocket = null; + } while (repeat && !cancellationToken.IsCancellationRequested); + } }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); } + /// + /// Configures web sockets and connects to the server. + /// + /// The token to monitor for cancellation requests. + private async Task ConnectWebSocketsAsync(CancellationToken cancellationToken) + { + clientWebSocket = new ClientWebSocket(); + + // Configure web sockets. + clientWebSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(10); + if (Credentials != null) + { + clientWebSocket.Options.Credentials = Credentials; + } + if (Cookies != null) + { + clientWebSocket.Options.Cookies = new CookieContainer(); + clientWebSocket.Options.Cookies.Add(Cookies); + } + if (InstanceId != Guid.Empty) + { + clientWebSocket.Options.SetRequestHeader("InstanceId", InstanceId.ToString()); + } + + // Connect to the server. + await clientWebSocket.ConnectAsync(new Uri(WebSocketServerUrl), cancellationToken); + Logger.LogMessage("Connected", WebSocketServerUrl); + + return clientWebSocket; + } + /// /// Stops monitoring changes in the remote storage. /// @@ -239,7 +262,7 @@ public async Task StopAsync() if (clientWebSocket != null && (clientWebSocket?.State == WebSocketState.Open || clientWebSocket?.State == WebSocketState.CloseSent || clientWebSocket?.State == WebSocketState.CloseReceived)) { - await clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + await clientWebSocket?.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); } } catch (WebSocketException ex) diff --git a/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorCRUDE.cs b/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorCRUDE.cs index 90fa319..9244eaf 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorCRUDE.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorCRUDE.cs @@ -39,7 +39,7 @@ internal class RemoteStorageMonitorCRUDE : RemoteStorageMonitorBase private readonly string webDAVServerUrl; internal RemoteStorageMonitorCRUDE(string webSocketServerUrl, string webDAVServerUrl, VirtualEngine engine) - : base(webSocketServerUrl, int.MinValue, engine.Logger) + : base(webSocketServerUrl, int.MinValue, engine.Logger.CreateLogger($"Remote Storage Monitor: CRUDE")) { this.webDAVServerUrl = webDAVServerUrl; this.engine = engine; diff --git a/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorSyncId.cs b/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorSyncId.cs index c3028d0..89324df 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorSyncId.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorSyncId.cs @@ -36,7 +36,7 @@ internal class RemoteStorageMonitorSyncId : RemoteStorageMonitorBase private readonly string webDAVServerUrl; internal RemoteStorageMonitorSyncId(string webSocketServerUrl, string webDAVServerUrl, VirtualEngine engine) - : base(webSocketServerUrl, 1, engine.Logger) + : base(webSocketServerUrl, 1, engine.Logger.CreateLogger($"Remote Storage Monitor: SyncID")) { this.webDAVServerUrl = webDAVServerUrl; this.engine = engine; diff --git a/Windows/WebDAVDrive/WebDAVDrive/VirtualEngine.cs b/Windows/WebDAVDrive/WebDAVDrive/VirtualEngine.cs index 4da781a..bd0da5f 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/VirtualEngine.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/VirtualEngine.cs @@ -2,19 +2,21 @@ using System.Net; using System.Threading.Tasks; using System.Threading; +using System.Security; +using Windows.Security.Credentials.UI; using ITHit.FileSystem; using ITHit.FileSystem.Windows; using ITHit.FileSystem.Samples.Common.Windows; using ITHit.WebDAV.Client; using ITHit.WebDAV.Client.Exceptions; -using Microsoft.VisualBasic.Logging; -using System.Security; -using WebDAVDrive.UI.ViewModels; -using NotImplementedException = System.NotImplementedException; -using WebDAVDrive.UI; using ITHit.FileSystem.Synchronization; +using WebDAVDrive.UI; +using System.Runtime.InteropServices.WindowsRuntime; +using System.IO; +using System.Windows; +using System.Text; namespace WebDAVDrive { @@ -213,7 +215,7 @@ public override async Task GetMenuCommandAsync(Guid menuGuid, IOpe } Logger.LogError($"Menu not found", Path, menuGuid.ToString()); - throw new NotImplementedException(); + throw new System.NotImplementedException(); } /// @@ -289,7 +291,10 @@ private async Task TryCreateRemoteStorageMonitorAsync( public override async Task StopAsync() { await base.StopAsync(); - await RemoteStorageMonitor?.StopAsync(); + if (RemoteStorageMonitor != null) + { + await RemoteStorageMonitor?.StopAsync(); + } } /// @@ -301,15 +306,15 @@ private async Task GetRootRemoteStorageItemId(string webDAVServerUrl, Ca var response = await DavClient.GetItemAsync(new Uri(webDAVServerUrl), Mapping.GetDavProperties(), null, cancellationToken); IHierarchyItem rootFolder = response.WebDavResponse; - byte[] remoteStorageItemId = Mapping.GetUserFileSystemItemMetadata(rootFolder).RemoteStorageItemId; + IFileSystemItemMetadata metadata = Mapping.GetUserFileSystemItemMetadata(rootFolder); - // This sample requires synchronization support, verifying that the ID was returned. - if (remoteStorageItemId == null) + // If remote storage ID is not returned, the SyncID collection synchronization is not supported. + if (metadata.RemoteStorageItemId == null) { - throw new WebDavException("remote-id or parent-resource-id is not found. Your WebDAV server does not support collection synchronization. Upgrade your .NET WebDAV server to v13.2 or Java WebDAV server to v6.2 or later version."); + Logger.LogMessage("Root resource-id is null.", Path); } - return remoteStorageItemId; + return metadata.RemoteStorageItemId; } /// @@ -433,37 +438,31 @@ private WebDavErrorEventResult ChallengeLoginLogin(Uri failedUri) } else { - string login = null; - SecureString password = null; - bool dialogResult = false; - bool keepLogedin = false; - - // Show login dialog - WebDAVDrive.UI.ChallengeLogin loginForm = null; - Thread thread = new Thread(() => - { - loginForm = new WebDAVDrive.UI.ChallengeLogin(); - ((ChallengeLoginViewModel)loginForm.DataContext).Url = failedUri.OriginalString; - ((ChallengeLoginViewModel)loginForm.DataContext).WindowTitle = Title; - loginForm.ShowDialog(); - - login = ((ChallengeLoginViewModel)loginForm.DataContext).Login; - password = ((ChallengeLoginViewModel)loginForm.DataContext).Password; - keepLogedin = ((ChallengeLoginViewModel)loginForm.DataContext).KeepLogedIn; - dialogResult = (bool)loginForm.DialogResult; - }); - thread.SetApartmentState(ApartmentState.STA); - thread.Start(); - thread.Join(); + CredentialPickerResults res; + CredentialPickerOptions options = new CredentialPickerOptions(); + options.Caption = productName; + options.CredentialSaveOption = CredentialSaveOption.Unselected; + options.AuthenticationProtocol = AuthenticationProtocol.Basic; + options.TargetName = failedUri.OriginalString; + options.Message = failedUri.OriginalString; + + res = CredentialPicker.PickAsync(options).GetAwaiter().GetResult(); loginRetriesCurrent++; - if (dialogResult) + if (res.ErrorCode == 0) { - if (keepLogedin) + if (res.CredentialSaveOption == CredentialSaveOption.Selected) { - CredentialManager.SaveCredentials(CredentialsStorageKey, login, password); + //using (var dataReader = Windows.Storage.Streams.DataReader.FromBuffer(res.Credential)) + //{ + // dataReader.UnicodeEncoding = Windows.Storage.Streams.UnicodeEncoding.Utf16LE; + // string creds = dataReader.ReadString(res.Credential.Length); + //} + + CredentialManager.SaveCredentials(CredentialsStorageKey, res.CredentialUserName, res.CredentialPassword); } - NetworkCredential newNetworkCredential = new NetworkCredential(login, password); + + NetworkCredential newNetworkCredential = new NetworkCredential(res.CredentialUserName, res.CredentialPassword); DavClient.Credentials = newNetworkCredential; this.Credentials = newNetworkCredential; this.CurrentUserPrincipal = newNetworkCredential.UserName; diff --git a/Windows/WebDAVDrive/WebDAVDrive/VirtualFolder.cs b/Windows/WebDAVDrive/WebDAVDrive/VirtualFolder.cs index 8ffb55c..8a6ba94 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/VirtualFolder.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/VirtualFolder.cs @@ -84,10 +84,15 @@ public async Task CreateFileAsync(IFileMetadata fileMetadata, Str // - Content eTag. The Engine will store it to determine if the file content should be updated. // - Medatdata eTag. The Engine will store it to determine if the item metadata should be updated. - string remoteStorageId = response.Headers.GetValues("resource-id").FirstOrDefault(); + byte[] remoteStorageItemId = null; + if (response.Headers.Contains("resource-id")) + { + string remoteStorageId = response.Headers.GetValues("resource-id").FirstOrDefault(); + remoteStorageItemId = Encoding.UTF8.GetBytes(remoteStorageId); + } return new FileMetadataExt() { - RemoteStorageItemId = Encoding.UTF8.GetBytes(remoteStorageId), + RemoteStorageItemId = remoteStorageItemId, ContentETag = response.WebDavResponse // MetadataETag = }; @@ -107,10 +112,15 @@ public async Task CreateFolderAsync(IFolderMetadata folderMetad // - Remote storage item ID. It will be passed to GetFileSystemItem() during next calls. // - Medatdata eTag. The Engine will store it to determine if the item metadata should be updated. - string remoteStorageId = response.Headers.GetValues("resource-id").FirstOrDefault(); + byte[] remoteStorageItemId = null; + if (response.Headers.Contains("resource-id")) + { + string remoteStorageId = response.Headers.GetValues("resource-id").FirstOrDefault(); + remoteStorageItemId = Encoding.UTF8.GetBytes(remoteStorageId); + } return new FolderMetadataExt() { - RemoteStorageItemId = Encoding.UTF8.GetBytes(remoteStorageId) + RemoteStorageItemId = remoteStorageItemId // MetadataETag = }; } diff --git a/Windows/WebDAVDrive/WebDAVDrive/WebDAVDrive.csproj b/Windows/WebDAVDrive/WebDAVDrive/WebDAVDrive.csproj index e68920f..b5fc9a0 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/WebDAVDrive.csproj +++ b/Windows/WebDAVDrive/WebDAVDrive/WebDAVDrive.csproj @@ -1,7 +1,7 @@ Exe - net7.0-windows10.0.19041.0 + net8.0-windows10.0.19041.0 IT Hit LTD. IT Hit LTD. WebDAV Drive @@ -39,7 +39,7 @@ - + diff --git a/Windows/WebDAVDrive/WebDAVDrive/appsettings.json b/Windows/WebDAVDrive/WebDAVDrive/appsettings.json index f99fc29..c4bbf71 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/appsettings.json +++ b/Windows/WebDAVDrive/WebDAVDrive/appsettings.json @@ -60,6 +60,13 @@ "SyncIntervalMs": 10000, + // Preferred incoming synchronization mode. Allowed values are: SyncId, Disabled, TimerPooling. + // - SyncId - Use Sync ID algorithm. If not available, falls back to CRUDE+pooling (Disabled mode). + // - Disabled - This sample will try to receive CRUDE updates via web sockets + pooling on web sockets connection. If web sockets are not available, falls back to TimerPooling. + // - TimerPooling - The Engine will traverse folders loaded on the client, list each folder remote storage content and compare with client content. + "PreferredIncomingSyncMode": "SyncId", + + // Maximum number of create/update/read concurrent requests to remote storage. If null then 6 value is used. "MaxTransferConcurrentRequests": 6, diff --git a/macOS/Common/Core/Common.Core.csproj b/macOS/Common/Core/Common.Core.csproj index e04afad..eedcc80 100644 --- a/macOS/Common/Core/Common.Core.csproj +++ b/macOS/Common/Core/Common.Core.csproj @@ -19,6 +19,6 @@ None - + diff --git a/macOS/Common/Core/SecureStorageBase.cs b/macOS/Common/Core/SecureStorageBase.cs index 6e4bfab..89ffe6a 100644 --- a/macOS/Common/Core/SecureStorageBase.cs +++ b/macOS/Common/Core/SecureStorageBase.cs @@ -6,7 +6,7 @@ namespace Common.Core public class SecureStorageBase { private string appGroupId; - private const string InternalSettingFile = "data.out"; + private const string InternalSettingFile = "userdata.out"; public SecureStorageBase(string appGroupId) { @@ -103,11 +103,21 @@ public string GetSharedContainerPath() } /// - /// Triggers log-in button in file manager. + /// Triggers log-in button in file manager for basic and digest. /// - public async Task RequireAuthenticationAsync() + public async Task RequirePasswordAuthenticationAsync() { - await SetAsync("LoginType", "RequireAuthentication"); + await SetAsync("LoginType", "UserNamePassword"); + await SetAsync("RequireAuthentication", "true"); + } + + /// + /// Triggers log-in button in file manager for cookies. + /// + public async Task RequireCookiesAuthenticationAsync() + { + await SetAsync("LoginType", "Cookies"); + await SetAsync("RequireAuthentication", "true"); } } } diff --git a/macOS/VirtualFileSystem/FileProviderExtension/FileProviderExtension.csproj b/macOS/VirtualFileSystem/FileProviderExtension/FileProviderExtension.csproj index 09eb0f6..8cf30dd 100644 --- a/macOS/VirtualFileSystem/FileProviderExtension/FileProviderExtension.csproj +++ b/macOS/VirtualFileSystem/FileProviderExtension/FileProviderExtension.csproj @@ -76,7 +76,7 @@ - + diff --git a/macOS/VirtualFileSystem/FileProviderExtension/VirtualEngine.cs b/macOS/VirtualFileSystem/FileProviderExtension/VirtualEngine.cs index c1815e0..777f828 100644 --- a/macOS/VirtualFileSystem/FileProviderExtension/VirtualEngine.cs +++ b/macOS/VirtualFileSystem/FileProviderExtension/VirtualEngine.cs @@ -17,6 +17,11 @@ namespace FileProviderExtension [Register(nameof(VirtualEngine))] public class VirtualEngine : EngineMac { + /// + /// Secure Storage. + /// + public SecureStorage SecureStorage; + [Export("initWithDomain:")] public VirtualEngine(NSFileProviderDomain domain) : base(domain) @@ -28,10 +33,18 @@ public VirtualEngine(NSFileProviderDomain domain) Debug += consolelogger.LogDebug; AutoLock = AppGroupSettings.Settings.Value.AutoLock; + SecureStorage = new SecureStorage(); + + // set remote root storage item id. + SetRemoteStorageRootItemId(GetRootStorageItemIdAsync().Result); - SecureStorage secureStorage = new SecureStorage(); + Logger.LogMessage($"Engine started."); + } - DomainSettings domainSettings = secureStorage.GetAsync(domain.Identifier).Result; + /// + public override async Task GetRootStorageItemIdAsync() + { + DomainSettings domainSettings = await SecureStorage.GetAsync(domain.Identifier); string remoteStorageRootPath = AppGroupSettings.Settings.Value.RemoteStorageRootPath; if (domainSettings != null && !string.IsNullOrEmpty(domainSettings.RemoteStorageRootPath)) @@ -39,10 +52,7 @@ public VirtualEngine(NSFileProviderDomain domain) remoteStorageRootPath = domainSettings.RemoteStorageRootPath; } - // set remote root storage item id. - SetRemoteStorageRootItemId(Mapping.EncodePath(remoteStorageRootPath)); - - Logger.LogMessage($"Engine started."); + return Mapping.EncodePath(remoteStorageRootPath); } /// diff --git a/macOS/WebDAVDrive/README.md b/macOS/WebDAVDrive/README.md index 85f63a2..e4cd792 100644 --- a/macOS/WebDAVDrive/README.md +++ b/macOS/WebDAVDrive/README.md @@ -49,6 +49,14 @@

Now you can manage documents using Finder, command prompt or by any other means. You can find the new file system in the 'Locations' sections in Finder. 

For the development and testing convenience, when installing the extension, it will automatically open an instance of Finder with a mounted file system as well as will launch a default web browser navigating to the WebDAV server URL specified in your appsettings.json:

WebDAV Drive for macOS sample

+ +

To create installer for testing purposes and install the sample to /Application folder follow this steps:

+
    +
  1. You need Mac Developer certificate to sign app and 3rd Party Mac Developer Installer certificate to sign pkg. To get them use this guide.
  2. +
  3. Start Release build.
  4. +
  5. Then open Release output folder and find WebDAV Drive signed.pkg and use this pkg to install the Sample on the same host.
  6. +
+

For production environment you need to create Group ID, App Identifies and Provisioning Profiles configuration as described in this article.

See also:

  • macOS File Provider Extension Troubleshooting
  • diff --git a/macOS/WebDAVDrive/WebDAVCommon/AppSettings.cs b/macOS/WebDAVDrive/WebDAVCommon/AppSettings.cs index 3cd8ec9..a191816 100644 --- a/macOS/WebDAVDrive/WebDAVCommon/AppSettings.cs +++ b/macOS/WebDAVDrive/WebDAVCommon/AppSettings.cs @@ -14,14 +14,9 @@ public class AppSettings : Settings public string WebDAVClientLicense { get; set; } /// - /// WebDAV server URL. + /// WebDAV server URLs. /// - public string WebDAVServerUrl { get; set; } - - /// - /// WebSocket server URL. - /// - public string WebSocketServerUrl { get; set; } + public List WebDAVServerURLs { get; set; } = new(); /// /// Automatic lock timout in milliseconds. diff --git a/macOS/WebDAVDrive/WebDAVCommon/DomainSettings.cs b/macOS/WebDAVDrive/WebDAVCommon/DomainSettings.cs deleted file mode 100644 index b1f9f36..0000000 --- a/macOS/WebDAVDrive/WebDAVCommon/DomainSettings.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -namespace WebDAVCommon -{ - public class DomainSettings - { - /// - /// WebDAV server URL. - /// - public string WebDAVServerUrl { get; set; } = string.Empty; - - /// - /// WebSocket server URL. - /// - public string WebSocketServerUrl { get; set; } = string.Empty; - } -} - diff --git a/macOS/WebDAVDrive/WebDAVCommon/SecureStorage.cs b/macOS/WebDAVDrive/WebDAVCommon/SecureStorage.cs index e0141e7..6ceab02 100644 --- a/macOS/WebDAVDrive/WebDAVCommon/SecureStorage.cs +++ b/macOS/WebDAVDrive/WebDAVCommon/SecureStorage.cs @@ -9,9 +9,6 @@ namespace WebDAVCommon { public class SecureStorage: SecureStorageBase { - public const string ExtensionIdentifier = "com.webdav.vfs.app"; - public const string ExtensionDisplayName = "IT Hit WebDAV Drive"; - public SecureStorage(): base("65S3A9JQ35.group.com.webdav.vfs") { diff --git a/macOS/WebDAVDrive/WebDAVCommon/WebDavSessionUtils.cs b/macOS/WebDAVDrive/WebDAVCommon/WebDavSessionUtils.cs index a94429b..6a93b0b 100644 --- a/macOS/WebDAVDrive/WebDAVCommon/WebDavSessionUtils.cs +++ b/macOS/WebDAVDrive/WebDAVCommon/WebDavSessionUtils.cs @@ -20,6 +20,17 @@ public static async Task GetWebDavSessionAsync() { webDavSession.Credentials = new NetworkCredential(await secureStorage.GetAsync("UserName"), await secureStorage.GetAsync("Password")); } + else if (!string.IsNullOrEmpty(loginType) && loginType.Equals("Cookies")) + { + List cookies = await secureStorage.GetAsync>("Cookies"); + if (cookies != null) + { + foreach (Cookie cookie in cookies) + { + webDavSession.CookieContainer.Add(cookie); + } + } + } return webDavSession; } diff --git a/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualEngine.cs b/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualEngine.cs index a70b695..c80a717 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualEngine.cs +++ b/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualEngine.cs @@ -68,12 +68,8 @@ public VirtualEngine(NSFileProviderDomain domain) SecureStorage = new SecureStorage(); - WebDAVServerUrl = AppGroupSettings.Settings.Value.WebDAVServerUrl; - DomainSettings domainSettings = SecureStorage.GetAsync(domain.Identifier).Result; - if (domainSettings != null && !string.IsNullOrEmpty(domainSettings.WebDAVServerUrl)) - { - WebDAVServerUrl = domainSettings.WebDAVServerUrl; - } + // Get WebDAV url from user settings. + WebDAVServerUrl = SecureStorage.GetAsync(domain.Identifier).Result; InitWebDavSession(); @@ -133,11 +129,8 @@ public override async Task GetMenuCommandAsync(Guid menuGuid, IOpe throw new NotImplementedException(); } - /// - /// Returns remote storage item id. - /// - /// - public async Task GetRootStorageItemIdAsync() + /// + public override async Task GetRootStorageItemIdAsync() { Logger.LogMessage($"{nameof(VirtualEngine)}.{nameof(GetRootStorageItemIdAsync)}()"); try @@ -152,7 +145,14 @@ public async Task GetRootStorageItemIdAsync() // Challenge-responce auth: Basic, Digest, NTLM or Kerberos case 401: // Set login type to display sing in button in Finder. - await SecureStorage.RequireAuthenticationAsync(); + await SecureStorage.RequirePasswordAuthenticationAsync(); + return null; + // 302 redirect to login page. + case 302: + Uri failedUri = httpException.Uri; + await SecureStorage.SetAsync("CookiesFailedUrl", failedUri.AbsoluteUri); + // Set login type to display sing in button in Finder. + await SecureStorage.RequireCookiesAuthenticationAsync(); return null; } return null; @@ -180,8 +180,9 @@ protected override void Stop() public override async Task IsAuthenticatedAsync() { Logger.LogMessage($"{nameof(IEngine)}.{nameof(IsAuthenticatedAsync)}()"); - string loginType = await SecureStorage.GetAsync("LoginType"); - return string.IsNullOrEmpty(loginType) || loginType != "RequireAuthentication"; + string requireAuthentication = await SecureStorage.GetAsync("RequireAuthentication"); + + return string.IsNullOrEmpty(requireAuthentication); } } } diff --git a/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFileSystemItem.cs b/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFileSystemItem.cs index 0156532..0c65186 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFileSystemItem.cs +++ b/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFileSystemItem.cs @@ -172,26 +172,55 @@ public Task> GetPropertiesAsync(IOperati protected async void HandleWebExceptions(Client.Exceptions.WebDavHttpException webDavHttpException, IResultContext resultContext) { + Logger.LogError("HandleWebExceptions", ex: webDavHttpException); + switch (webDavHttpException.Status.Code) { // Challenge-responce auth: Basic, Digest, NTLM or Kerberos case 401: - if (Engine.WebDavSession.Credentials == null || !(Engine.WebDavSession.Credentials is NetworkCredential) || - (Engine.WebDavSession.Credentials as NetworkCredential).UserName != await Engine.SecureStorage.GetAsync("UserName")) + if ((Engine.WebDavSession.Credentials == null && !string.IsNullOrEmpty(await Engine.SecureStorage.GetAsync("UserName"))) || + (Engine.WebDavSession.Credentials is NetworkCredential && + (Engine.WebDavSession.Credentials as NetworkCredential).UserName != await Engine.SecureStorage.GetAsync("UserName"))) + { + Engine.Logger.LogDebug("Reset WebDav session - password auth"); + + // Reset WebDavSession. + Engine.InitWebDavSession(); + } + else { // Set login type to display sing in button in Finder. - await Engine.SecureStorage.RequireAuthenticationAsync(); + await Engine.SecureStorage.RequirePasswordAuthenticationAsync(); if (resultContext != null) { resultContext.ReportStatus(CloudFileStatus.STATUS_CLOUD_FILE_AUTHENTICATION_FAILED); } } - else + break; + + // 302 redirect to login page. + case 302: + Uri failedUri = webDavHttpException.Uri; + await Engine.SecureStorage.SetAsync("CookiesFailedUrl", failedUri.AbsoluteUri); + + + if (Engine.WebDavSession.CookieContainer.Count == 0) { + Engine.Logger.LogDebug($"Reset WebDav session - cookies auth: {Engine.WebDavSession.CookieContainer.Count}"); + // Reset WebDavSession. Engine.InitWebDavSession(); } + else + { + // Set login type to display sing in button in Finder. + await Engine.SecureStorage.RequireCookiesAuthenticationAsync(); + if (resultContext != null) + { + resultContext.ReportStatus(CloudFileStatus.STATUS_CLOUD_FILE_AUTHENTICATION_FAILED); + } + } break; } } diff --git a/macOS/WebDAVDrive/WebDAVFileProviderExtension/WebDAVFileProviderExtension.csproj b/macOS/WebDAVDrive/WebDAVFileProviderExtension/WebDAVFileProviderExtension.csproj index 8d98543..9dd067a 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderExtension/WebDAVFileProviderExtension.csproj +++ b/macOS/WebDAVDrive/WebDAVFileProviderExtension/WebDAVFileProviderExtension.csproj @@ -76,7 +76,7 @@ - + diff --git a/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/Entitlements.plist b/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/Entitlements.plist index a62467a..aa0dfbf 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/Entitlements.plist +++ b/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/Entitlements.plist @@ -8,5 +8,7 @@ 65S3A9JQ35.group.com.webdav.vfs + com.apple.security.network.client + diff --git a/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/FPUIActionExtension.cs b/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/FPUIActionExtension.cs index 87200a1..c479b6c 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/FPUIActionExtension.cs +++ b/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/FPUIActionExtension.cs @@ -9,8 +9,7 @@ namespace WebDAVFileProviderUIExtension; [Register("FPUIActionExtension")] public class FPUIActionExtension : FPUIActionExtensionViewControllerMac { - public FPUIActionExtension() : base(NSFileProviderManager.FromDomain(new NSFileProviderDomain(SecureStorage.ExtensionIdentifier, SecureStorage.ExtensionDisplayName)), - new ConsoleLogger(typeof(FPUIActionExtension).Name)) + public FPUIActionExtension() : base(new ConsoleLogger(typeof(FPUIActionExtension).Name)) { } /// @@ -24,8 +23,19 @@ public override async Task GetMenuCommandAsync(Guid menuGuid, /// public override async Task RequireAuthenticationAsync(IMacFPUIActionExtensionContext context) { - Logger.LogMessage($"{nameof(FPUIActionExtension)}.{nameof(RequireAuthenticationAsync)}()"); + SecureStorage secureStorage = new SecureStorage(); + Logger.LogMessage($"{nameof(FPUIActionExtension)}.{nameof(RequireAuthenticationAsync)}()"); - return new AuthViewController(context); + if (await secureStorage.GetAsync("LoginType") == "UserNamePassword") + { + return new AuthViewController(context); + } + else + { + Logger.LogMessage($"Return CookiesAuthViewController."); + string url = await secureStorage.GetAsync("CookiesFailedUrl"); + Logger.LogMessage($"Return CookiesAuthViewController read url {url}."); + return new CookiesAuthViewController(context, url); + } } } diff --git a/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/ViewControllers/AuthViewController.cs b/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/ViewControllers/AuthViewController.cs index 445a7a2..7ce295d 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/ViewControllers/AuthViewController.cs +++ b/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/ViewControllers/AuthViewController.cs @@ -120,9 +120,9 @@ private void OnAuthenticationButtonActivated(object? sender, EventArgs e) { logger.LogDebug($"OnAuthenticationButtonActivated: send signal to file provider."); - secureStorage.SetAsync("LoginType", "UserNamePassword").Wait(); secureStorage.SetAsync("UserName", loginTextField.StringValue).Wait(); - secureStorage.SetAsync("Password", passwordTextField.StringValue).Wait(); + secureStorage.SetAsync("Password", passwordTextField.StringValue).Wait(); + secureStorage.SetAsync("RequireAuthentication", string.Empty).Wait(); extensionContext.CompleteRequest(); } catch(Exception ex) diff --git a/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/ViewControllers/CookiesAuthViewController.cs b/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/ViewControllers/CookiesAuthViewController.cs new file mode 100644 index 0000000..1e2aed6 --- /dev/null +++ b/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/ViewControllers/CookiesAuthViewController.cs @@ -0,0 +1,154 @@ +using System.Net; +using Common.Core; +using ITHit.FileSystem.Mac; +using ObjCRuntime; +using WebDAVCommon; +using WebKit; + +namespace WebDAVFileProviderUIExtension.ViewControllers +{ + public class CookiesAuthViewController : NSViewController, IWKNavigationDelegate + { + private readonly IMacFPUIActionExtensionContext extensionContext; + private readonly ConsoleLogger consoleLogger = new(nameof(CookiesAuthViewController)); + private readonly SecureStorage secureStorage = new(); + private readonly string failedUrl; + private WKWebViewConfiguration webViewConfiguration; + private NSProgressIndicator progressIndicator = new() + { + TranslatesAutoresizingMaskIntoConstraints = false, + ControlSize = NSControlSize.Large, + ControlTint = NSControlTint.Blue + }; + private WKWebView webView; + + public CookiesAuthViewController(IMacFPUIActionExtensionContext extensionContext, string failedUrl) + : base(nameof(CookiesAuthViewController), null) + { + consoleLogger.LogDebug("CookiesAuthViewController constructor"); + this.extensionContext = extensionContext; + this.failedUrl = failedUrl; + consoleLogger.LogDebug("CookiesAuthViewController init all parameters."); + } + + protected CookiesAuthViewController(NativeHandle handle) : base(handle) + { + // This constructor is required if the view controller is loaded from a xib or a storyboard. + // Do not put any initialization here, use ViewDidLoad instead. + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (webView != null) + { + webView.Dispose(); + webView = null; + } + if (webViewConfiguration != null) + { + webViewConfiguration.Dispose(); + webViewConfiguration = null; + } + if (progressIndicator != null) + { + progressIndicator.Dispose(); + progressIndicator = null; + } + } + base.Dispose(disposing); + } + + public override void LoadView() + { + consoleLogger.LogDebug("CookiesAuthViewController LoadView"); + NSView view = new NSView() + { + TranslatesAutoresizingMaskIntoConstraints = false + }; + webViewConfiguration = new WKWebViewConfiguration(); + webView = new WKWebView(new CGRect(0, 0, 250, 250), webViewConfiguration) + { + TranslatesAutoresizingMaskIntoConstraints = false, + NavigationDelegate = this + }; + view.AddSubview(webView); + view.AddSubview(progressIndicator); + View = view; + NSLayoutConstraint.ActivateConstraints(new[] + { + webView.TopAnchor.ConstraintEqualTo(view.TopAnchor), + webView.LeadingAnchor.ConstraintEqualTo(view.LeadingAnchor), + webView.TrailingAnchor.ConstraintEqualTo(view.TrailingAnchor), + webView.BottomAnchor.ConstraintEqualTo(view.BottomAnchor), + progressIndicator.CenterXAnchor.ConstraintEqualTo(view.CenterXAnchor), + progressIndicator.CenterYAnchor.ConstraintEqualTo(view.CenterYAnchor) + }); + + consoleLogger.LogDebug("CookiesAuthViewController finish LoadView"); + } + + public override void ViewDidLoad() + { + consoleLogger.LogDebug($"CookiesAuthViewController ViewDidLoad {failedUrl}"); + base.ViewDidLoad(); + var url = new NSUrl(failedUrl); + var request = new NSUrlRequest(url); + webView.LoadRequest(request); + progressIndicator.StartAnimation(this); + } + + [Export("webView:decidePolicyForNavigationResponse:decisionHandler:")] + public void DecidePolicy( + WKWebView webView, + WKNavigationResponse navigationResponse, + Action decisionHandler) + { + var response = navigationResponse.Response; + consoleLogger.LogDebug($"{nameof(DecidePolicy)} url: {response.Url}"); + if (response is not NSHttpUrlResponse httpUrlResponse || + !CheckIsSuccessStatusCode(httpUrlResponse.StatusCode) || + !httpUrlResponse.Url.ToString().Equals(failedUrl, StringComparison.InvariantCultureIgnoreCase)) + { + consoleLogger.LogDebug($"CookiesAuthViewController WKNavigationResponsePolicy.Allow"); + decisionHandler?.Invoke(WKNavigationResponsePolicy.Allow); + return; + } + webViewConfiguration.WebsiteDataStore.HttpCookieStore.GetAllCookies(cookies => + { + consoleLogger.LogDebug($"CookiesAuthViewController set cookies"); + secureStorage.SetAsync("RequireAuthentication", "").Wait(); + secureStorage.SetAsync("Cookies", cookies.Select(c => new Cookie(c.Name, c.Value, c.Path, c.Domain)).ToList()).Wait(); + extensionContext.CompleteRequest(); + }); + decisionHandler?.Invoke(WKNavigationResponsePolicy.Allow); + + bool CheckIsSuccessStatusCode(nint statusCode) + { + return statusCode >= 200 && statusCode <= 299; + } + } + + [Export("webView:didStartProvisionalNavigation:")] + public void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation) + { + consoleLogger.LogDebug($"{nameof(DidStartProvisionalNavigation)}"); + } + + [Export("webView:didFinishNavigation:")] + public void DidFinishNavigation(WKWebView webView, WKNavigation navigation) + { + consoleLogger.LogDebug($"{nameof(DidFinishNavigation)}"); + progressIndicator.StopAnimation(this); + } + + [Export("webView:didFailNavigation:withError:")] + public void DidFailNavigation(WKWebView webView, WKNavigation navigation, NSError error) + { + consoleLogger.LogDebug($"{nameof(DidFailNavigation)}"); + progressIndicator.StopAnimation(this); + } + } +} + diff --git a/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/WebDAVFileProviderUIExtension.csproj b/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/WebDAVFileProviderUIExtension.csproj index 7fef514..e3540cb 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/WebDAVFileProviderUIExtension.csproj +++ b/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/WebDAVFileProviderUIExtension.csproj @@ -93,6 +93,6 @@ - + diff --git a/macOS/WebDAVDrive/WebDAVMacApp/AppDelegate.cs b/macOS/WebDAVDrive/WebDAVMacApp/AppDelegate.cs index 2fa928a..4ec4800 100644 --- a/macOS/WebDAVDrive/WebDAVMacApp/AppDelegate.cs +++ b/macOS/WebDAVDrive/WebDAVMacApp/AppDelegate.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Text.Json; using System.Threading; +using System.Web; using AppKit; using Common.Core; using FileProvider; @@ -14,284 +15,243 @@ namespace WebDAVMacApp [Register("AppDelegate")] public class AppDelegate : NSApplicationDelegate { + private const string ProtocolPrefix = "fuse:"; private ILogger Logger = new ConsoleLogger("WebDavFileProviderHostApp"); - private RemoteStorageMonitor? RemoteStorageMonitor = null; - private NSMenuItem InstallMenuItem = new NSMenuItem("Install WebDAV FS Extension"); - private NSMenuItem UninstallMenuItem = new NSMenuItem("Uninstall WebDAV FS Extension"); - private NSStatusItem StatusItem; private SecureStorage SecureStorage = new SecureStorage(); + private NSStatusItem StatusItem; + private Dictionary DomainsMenuItems = new Dictionary(); + private Dictionary DomainsRemoteStorageMonitor = new Dictionary(); + public AppDelegate() { } public override void DidFinishLaunching(NSNotification notification) { - NSMenu menu = new NSMenu(); + Logger.LogMessage("Starting DidFinishLaunching"); - string domainIdentifier = Task.Run(async () => await SecureStorage.GetAsync("CurrentDomainIdentifier")).Result; - if (string.IsNullOrEmpty(domainIdentifier)) - { - domainIdentifier = SecureStorage.ExtensionIdentifier; - } + List webDAVServerURLs = Task.Run>(async () => await SecureStorage.GetAsync>("WebDAVServerURLs")).Result; - Task taskIsExtensionRegistered = Task.Run(async () => await Common.Core.Registrar.IsRegisteredAsync(domainIdentifier)); - bool isExtensionRegistered = taskIsExtensionRegistered.Result; - if (isExtensionRegistered) + if (webDAVServerURLs == null) { - UninstallMenuItem.Activated += Uninstall; - Task.Run(async () => - { - // Get WebDAVServerUrl and WebSocketServerUrl - string webDAVServerUrl = AppGroupSettings.Settings.Value.WebDAVServerUrl; - string webSocketServerUrl = AppGroupSettings.Settings.Value.WebSocketServerUrl; - DomainSettings domainSettings = await SecureStorage.GetAsync(domainIdentifier); - if (domainSettings != null && !string.IsNullOrEmpty(domainSettings.WebDAVServerUrl)) - { - webDAVServerUrl = domainSettings.WebDAVServerUrl; - webSocketServerUrl = domainSettings.WebSocketServerUrl; - } - - NSFileProviderManager fileProviderManager = NSFileProviderManager.FromDomain(new NSFileProviderDomain(domainIdentifier, SecureStorage.ExtensionDisplayName)); - RemoteStorageMonitor = new RemoteStorageMonitor(webDAVServerUrl, webSocketServerUrl, fileProviderManager, new ConsoleLogger(typeof(RemoteStorageMonitor).Name)); - RemoteStorageMonitor.ServerNotifications = new ServerNotifications(fileProviderManager, RemoteStorageMonitor.Logger); - await RemoteStorageMonitor.StartAsync(); + webDAVServerURLs = AppGroupSettings.Settings.Value.WebDAVServerURLs; - if (NSProcessInfo.ProcessInfo.Arguments.Length > 1) - { - // Update the domain identifier if the current identifier differs from the arguments. - Uri webDavRootUrl = new Uri(NSProcessInfo.ProcessInfo.Arguments[1]); - domainIdentifier = webDavRootUrl.Host; - - await CheckDomainAndOpenItemAsync(domainIdentifier, NSProcessInfo.ProcessInfo.Arguments[1], NSProcessInfo.ProcessInfo.Arguments[2]); - } - }).Wait(); + // Save urls to user's settings. + Task.Run(async () => await SecureStorage.SetAsync("WebDAVServerURLs", webDAVServerURLs)).Wait(); } - else if (NSProcessInfo.ProcessInfo.Arguments.Length > 1) - { - Logger.LogDebug($"Arguments: {string.Join(",", NSProcessInfo.ProcessInfo.Arguments.Skip(1))}"); - Uri webDavRootUrl = new Uri(NSProcessInfo.ProcessInfo.Arguments[1]); - domainIdentifier = webDavRootUrl.Host; - Task.Run(async () => - { - await SecureStorage.SetAsync(domainIdentifier, - new DomainSettings - { - WebDAVServerUrl = NSProcessInfo.ProcessInfo.Arguments[1], - WebSocketServerUrl = $"wss://{webDavRootUrl.Host}/" - }); - await CreateDomainAsync(domainIdentifier, SecureStorage.ExtensionDisplayName, false); - await OpenItemAsync(domainIdentifier, NSProcessInfo.ProcessInfo.Arguments[1], NSProcessInfo.ProcessInfo.Arguments[2]); - }).Wait(); - - UninstallMenuItem.Activated += Uninstall; - } - else + StatusItem = NSStatusBar.SystemStatusBar.CreateStatusItem(30); + StatusItem.Menu = new NSMenu(); + StatusItem.Image = NSImage.ImageNamed("TrayIcon.png"); + StatusItem.HighlightMode = true; + + foreach (string serverUrl in webDAVServerURLs) { - InstallMenuItem.Activated += Install; + AddDomainMenu(serverUrl); } NSMenuItem exitMenuItem = new NSMenuItem("Quit", (a, b) => { - Task.Run(async () => - { - if (RemoteStorageMonitor != null && RemoteStorageMonitor.SyncState == SynchronizationState.Enabled) - { - string domainIdentifier = await SecureStorage.GetAsync("CurrentDomainIdentifier"); - if (string.IsNullOrEmpty(domainIdentifier)) - { - domainIdentifier = SecureStorage.ExtensionIdentifier; - } - await RemoteStorageMonitor?.StopAsync(); - } - }).Wait(); NSApplication.SharedApplication.Terminate(this); }); + StatusItem.Menu.AddItem(exitMenuItem); - menu.AddItem(InstallMenuItem); - menu.AddItem(UninstallMenuItem); - menu.AddItem(exitMenuItem); - StatusItem = NSStatusBar.SystemStatusBar.CreateStatusItem(30); - StatusItem.Menu = menu; - StatusItem.Image = NSImage.ImageNamed("TrayIcon.png"); - StatusItem.HighlightMode = true; - - // Subscribe to Notification from protocol handler. - NSDistributedNotificationCenter.DefaultCenter.AddObserver(new NSString("ITHitWebDavOpenItem"), OpenItemNotificationHandlerAsync); - NSDistributedNotificationCenter.DefaultCenter.AddObserver(new NSString("ITHitWebDavMountNewDomainAndOpenItem"), MountNewDomainAndOpenItemNotificationHandlerAsync); + Logger.LogMessage("Finished DidFinishLaunching"); } - private void Install(object? sender, EventArgs e) + + public override async void OpenUrls(NSApplication application, NSUrl[] urls) { + Logger.LogMessage($"OpenUrls - {string.Join(",", urls.Select(p => p.AbsoluteUrl))}"); - if (Task.Run(async () => + foreach (NSUrl url in urls) { - string domainIdentifier = await SecureStorage.GetAsync("CurrentDomainIdentifier"); - if (string.IsNullOrEmpty(domainIdentifier)) - { - domainIdentifier = SecureStorage.ExtensionIdentifier; - } + Dictionary parameters = url.AbsoluteUrl.ToString().Replace(ProtocolPrefix, string.Empty).Split(';').ToDictionary(p => p.Split('=')[0], p => p.Split('=')[1]); - if (await SecureStorage.GetAsync(domainIdentifier) == null) - { - await SecureStorage.SetAsync(domainIdentifier, - new DomainSettings - { - WebDAVServerUrl = AppGroupSettings.Settings.Value.WebDAVServerUrl, - WebSocketServerUrl = AppGroupSettings.Settings.Value.WebSocketServerUrl - }); - } - return await CreateDomainAsync(domainIdentifier, SecureStorage.ExtensionDisplayName, true); - }).Result) - { - InstallMenuItem.Activated -= Install; - InstallMenuItem.Action = null; - UninstallMenuItem.Activated += Uninstall; - } - } + Uri itemUrl = new Uri(HttpUtility.UrlDecode(parameters["ItemUrl"])); + Uri webDAVServerUrl = new Uri(HttpUtility.UrlDecode(parameters["MountUrl"])); + string domainIdentifier = webDAVServerUrl.Host; - private void Uninstall(object? sender, EventArgs e) - { - Task.Run(async () => - { - string domainIdentifier = await SecureStorage.GetAsync("CurrentDomainIdentifier"); - if (string.IsNullOrEmpty(domainIdentifier)) + List webDAVServerURLs = await SecureStorage.GetAsync>("WebDAVServerURLs"); + Task taskIsExtensionRegistered = Task.Run(async () => await Common.Core.Registrar.IsRegisteredAsync(domainIdentifier)); + bool isExtensionRegistered = taskIsExtensionRegistered.Result; + if (!isExtensionRegistered) { - domainIdentifier = SecureStorage.ExtensionIdentifier; - } + if (!DomainsMenuItems.ContainsKey(domainIdentifier)) + { + // Add new url. + webDAVServerURLs.Add(webDAVServerUrl.AbsoluteUri); - await RemoteStorageMonitor?.StopAsync(); - await Common.Core.Registrar.UnregisterAsync(domainIdentifier, Logger); - }).Wait(); - InstallMenuItem.Activated += Install; - UninstallMenuItem.Activated -= Uninstall; - UninstallMenuItem.Action = null; - } + // Add menu item with domain name. + AddDomainMenu(webDAVServerUrl.AbsoluteUri); + } + else + { + await SecureStorage.SetAsync(domainIdentifier, webDAVServerUrl.AbsoluteUri); - public override void WillTerminate(NSNotification notification) - { - RemoteStorageMonitor?.Dispose(); - RemoteStorageMonitor = null; - } + // Update existing.url. + int index = webDAVServerURLs.FindIndex(p => p.Contains(domainIdentifier)); + if (index != -1) + { + webDAVServerURLs[index] = webDAVServerUrl.AbsoluteUri; + } + } - private async void OpenItemNotificationHandlerAsync(NSNotification n) - { - NotificationItemSettings notificationItemSettings = JsonSerializer.Deserialize(n.Object.ToString()); - Uri webDavRootUrl = new Uri(notificationItemSettings.MountUrl); - string domainIdentifier = webDavRootUrl.Host; + // Save webdav server url to user's settings. + await SecureStorage.SetAsync("WebDAVServerURLs", webDAVServerURLs); - Task taskIsExtensionRegistered = Task.Run(async () => await Common.Core.Registrar.IsRegisteredAsync(await SecureStorage.GetAsync("CurrentDomainIdentifier"))); - if (taskIsExtensionRegistered.Result) - { - await CheckDomainAndOpenItemAsync(domainIdentifier, notificationItemSettings.MountUrl, notificationItemSettings.DocumentUrl); - } - else - { - // Create domain if not register. - Task.Run(async () => + // Register domain. + Install(DomainsMenuItems[domainIdentifier].installMenu, null, false); + } + else { - await SecureStorage.SetAsync(domainIdentifier, - new DomainSettings + if (await SecureStorage.GetAsync(domainIdentifier) != webDAVServerUrl.AbsoluteUri) + { + Uninstall(DomainsMenuItems[domainIdentifier].uninstallMenu, null); + + // update WebDAV server url. + await SecureStorage.SetAsync(domainIdentifier, webDAVServerUrl.AbsoluteUri); + + // Update existing.url. + int index = webDAVServerURLs.FindIndex(p => p.Contains(domainIdentifier)); + if(index != -1) { - WebDAVServerUrl = notificationItemSettings.MountUrl, - WebSocketServerUrl = $"wss://{webDavRootUrl.Host}/" - }); - await CreateDomainAsync(domainIdentifier, SecureStorage.ExtensionDisplayName, false); + webDAVServerURLs[index] = webDAVServerUrl.AbsoluteUri; + } - InstallMenuItem.Activated -= Install; - InstallMenuItem.Action = null; - UninstallMenuItem.Activated += Uninstall; + await SecureStorage.SetAsync("WebDAVServerURLs", webDAVServerURLs); - await OpenItemAsync(domainIdentifier, notificationItemSettings.MountUrl, notificationItemSettings.DocumentUrl); - }).Wait(); + Install(DomainsMenuItems[domainIdentifier].installMenu, null, false); + } + } + + // Open item url. + NSFileProviderManager fileProviderManager = NSFileProviderManager.FromDomain(await Common.Core.Registrar.GetDomainAsync(domainIdentifier)); + string itemPath = (await fileProviderManager.GetUserVisibleUrlAsync(NSFileProviderItemIdentifier.RootContainer)).ToString() + "/" + + itemUrl.AbsoluteUri.Substring(webDAVServerUrl.AbsoluteUri.Length); + Process.Start("open", itemPath); } } - private async void MountNewDomainAndOpenItemNotificationHandlerAsync(NSNotification n) + private void AddDomainMenu(string serverUrl) { - NotificationItemSettings notificationItemSettings = JsonSerializer.Deserialize(n.Object.ToString()); - - Uninstall(null, null); - Uri webDavRootUri = new Uri(notificationItemSettings.MountUrl); + Uri webDavRootUri = new Uri(serverUrl); + NSMenuItem domainMenu = new NSMenuItem(webDavRootUri.Host); + NSMenuItem installMenuItem = new NSMenuItem("Install"); + NSMenuItem uninstallMenuItem = new NSMenuItem("Uninstall"); string domainIdentifier = webDavRootUri.Host; - await SecureStorage.SetAsync(domainIdentifier, - new DomainSettings - { - WebDAVServerUrl = notificationItemSettings.MountUrl, - WebSocketServerUrl = $"wss://{webDavRootUri.Host}/" - }); - await CreateDomainAsync(domainIdentifier, SecureStorage.ExtensionDisplayName, false); + // Save domain's WebDav server url. + Task.Run(async () => + { + await SecureStorage.SetAsync(domainIdentifier, serverUrl); + }).Wait(); - InstallMenuItem.Activated -= Install; - InstallMenuItem.Action = null; - UninstallMenuItem.Activated += Uninstall; + Task taskIsExtensionRegistered = Task.Run(async () => await Common.Core.Registrar.IsRegisteredAsync(domainIdentifier)); + bool isExtensionRegistered = taskIsExtensionRegistered.Result; + if (isExtensionRegistered) + { + Task.Run(async () => await StartRemoteStorageMonitorAsync(await Common.Core.Registrar.GetDomainAsync(domainIdentifier), serverUrl)).Wait(); + uninstallMenuItem.Activated += Uninstall; + } + else + { + installMenuItem.Activated += Install; + } + installMenuItem.Identifier = uninstallMenuItem.Identifier = webDavRootUri.Host; - await OpenItemAsync(domainIdentifier, notificationItemSettings.MountUrl, notificationItemSettings.DocumentUrl); + domainMenu.Submenu = new NSMenu(); + domainMenu.Submenu.AddItem(installMenuItem); + domainMenu.Submenu.AddItem(uninstallMenuItem); + StatusItem.Menu.InsertItem(domainMenu, 0); + DomainsMenuItems.Add(webDavRootUri.Host, new(installMenuItem, uninstallMenuItem)); } - private async Task OpenItemAsync(string domainIdentifier, string webDavRootUrl, string webDavItemUrl) + private void Install(object? sender, EventArgs e) { - NSFileProviderManager fileProviderManager = NSFileProviderManager.FromDomain(await Common.Core.Registrar.GetDomainAsync(domainIdentifier)); - string itemPath = (await fileProviderManager.GetUserVisibleUrlAsync(NSFileProviderItemIdentifier.RootContainer)).ToString() + "/" + - webDavItemUrl.Substring(webDavRootUrl.Length); - Logger.LogDebug($"Root domain path {await fileProviderManager.GetUserVisibleUrlAsync(NSFileProviderItemIdentifier.RootContainer)}"); - Logger.LogDebug($"File path {itemPath}"); + Install(sender, e, true); + } - Process.Start("open", itemPath); + private void Install(object? sender, EventArgs e, bool openWebDavUrl) + { + string domainIdentifier = (sender as NSMenuItem).Identifier; + if (Task.Run(async () => + { + return await RegisterDomainAsync(domainIdentifier, openWebDavUrl); + }).Result) + { + DomainsMenuItems[domainIdentifier].installMenu.Activated -= Install; + DomainsMenuItems[domainIdentifier].installMenu.Action = null; + DomainsMenuItems[domainIdentifier].uninstallMenu.Activated += Uninstall; + } } - private async Task CheckDomainAndOpenItemAsync(string domainIdentifier, string webDavRootUrl, string webDavItemUrl) + private void Uninstall(object? sender, EventArgs e) { - string currentDomainIdentifier = await SecureStorage.GetAsync("CurrentDomainIdentifier"); + string domainIdentifier = (sender as NSMenuItem).Identifier; - if (currentDomainIdentifier != domainIdentifier) + Task.Run(async () => { - NSDistributedNotificationCenter.DefaultCenter.PostNotificationName("AnotherDomainIsRegistered", - JsonSerializer.Serialize(new NotificationAnotherDomainIsRegistered() { - DocumentUrl = webDavItemUrl, - MountUrl = webDavRootUrl, - CurrentDomain = currentDomainIdentifier - })); - } - else + await DomainsRemoteStorageMonitor[domainIdentifier]?.StopAsync(); + await Common.Core.Registrar.UnregisterAsync(domainIdentifier, Logger); + }).Wait(); + + DomainsMenuItems[domainIdentifier].installMenu.Activated += Install; + DomainsMenuItems[domainIdentifier].uninstallMenu.Activated -= Uninstall; + DomainsMenuItems[domainIdentifier].uninstallMenu.Action = null; + } + + public override void WillTerminate(NSNotification notification) + { + foreach (RemoteStorageMonitor remoteStorageMonitor in DomainsRemoteStorageMonitor.Values) { - await OpenItemAsync(domainIdentifier, webDavRootUrl, webDavItemUrl); + remoteStorageMonitor?.StopAsync(); + remoteStorageMonitor?.Dispose(); } } - - private async Task CreateDomainAsync(string domainIdentifier, string domainDisplayName, bool openWebDavUrl) + private async Task RegisterDomainAsync(string domainIdentifier, bool openWebDavUrl = false) { bool success = false; - // Get domain settings. - DomainSettings domainSettings = await SecureStorage.GetAsync(domainIdentifier); + string webDAVServerUrl = await SecureStorage.GetAsync(domainIdentifier); // Open WebDav url in browser. if (openWebDavUrl) { - Process.Start("open", domainSettings.WebDAVServerUrl); + Process.Start("open", webDAVServerUrl); } // Register domain. - NSFileProviderDomain? domain = await Common.Core.Registrar.RegisterAsync(domainIdentifier, domainDisplayName, Logger); + NSFileProviderDomain? domain = await Common.Core.Registrar.RegisterAsync(domainIdentifier, domainIdentifier, Logger); if (domain != null) { - // Save domain identifier. - await SecureStorage.SetAsync("CurrentDomainIdentifier", domainIdentifier); - RemoteStorageMonitor = new RemoteStorageMonitor(domainSettings.WebDAVServerUrl, domainSettings.WebSocketServerUrl, NSFileProviderManager.FromDomain(domain), - new ConsoleLogger(typeof(RemoteStorageMonitor).Name)); - RemoteStorageMonitor.ServerNotifications = new ServerNotifications(NSFileProviderManager.FromDomain(domain), RemoteStorageMonitor.Logger); - await RemoteStorageMonitor.StartAsync(); + await StartRemoteStorageMonitorAsync(domain, webDAVServerUrl); success = true; } return success; } + + private async Task StartRemoteStorageMonitorAsync(NSFileProviderDomain domain, string webDAVServerUrl) + { + if (DomainsRemoteStorageMonitor.ContainsKey(domain.Identifier)) + { + DomainsRemoteStorageMonitor[domain.Identifier].StartAsync(); + } + else + { + RemoteStorageMonitor remoteStorageMonitor = new RemoteStorageMonitor(webDAVServerUrl, + $"ws{(webDAVServerUrl.StartsWith("https:") ? "s" : string.Empty)}://{domain.Identifier}", + NSFileProviderManager.FromDomain(domain), new ConsoleLogger(typeof(RemoteStorageMonitor).Name)); + remoteStorageMonitor.ServerNotifications = new ServerNotifications(NSFileProviderManager.FromDomain(domain), remoteStorageMonitor.Logger); + await remoteStorageMonitor.StartAsync(); + DomainsRemoteStorageMonitor.Add(domain.Identifier, remoteStorageMonitor); + } + } } } diff --git a/macOS/WebDAVDrive/WebDAVMacApp/Info.plist b/macOS/WebDAVDrive/WebDAVMacApp/Info.plist index c6e9ea2..60af572 100644 --- a/macOS/WebDAVDrive/WebDAVMacApp/Info.plist +++ b/macOS/WebDAVDrive/WebDAVMacApp/Info.plist @@ -32,5 +32,18 @@ WebDAVMacApp LSUIElement + com.apple.developer.web-browser + + CFBundleURLTypes + + + CFBundleURLName + com.webdav.vfs.app.fuse + CFBundleURLSchemes + + fuse + + + diff --git a/macOS/WebDAVDrive/WebDAVMacApp/RemoteStorageMonitor.cs b/macOS/WebDAVDrive/WebDAVMacApp/RemoteStorageMonitor.cs index 1185812..d6a0090 100644 --- a/macOS/WebDAVDrive/WebDAVMacApp/RemoteStorageMonitor.cs +++ b/macOS/WebDAVDrive/WebDAVMacApp/RemoteStorageMonitor.cs @@ -46,13 +46,20 @@ public override bool Filter(WebSocketMessage webSocketMessage) /// True if the WebDav server supports Collection Synchronization. False otherwise. public override async Task IsSyncCollectionSupportedAsync() { - using (WebDavSession webDavSession = await WebDavSessionUtils.GetWebDavSessionAsync()) + try { - Client.PropertyName[] propNames = new Client.PropertyName[1]; - propNames[0] = new Client.PropertyName("supported-report-set", "DAV:"); - Client.IHierarchyItem rootFolder = (await webDavSession.GetItemAsync(WebDAVServerUrl, propNames)).WebDavResponse; + using (WebDavSession webDavSession = await WebDavSessionUtils.GetWebDavSessionAsync()) + { + Client.PropertyName[] propNames = new Client.PropertyName[1]; + propNames[0] = new Client.PropertyName("supported-report-set", "DAV:"); + Client.IHierarchyItem rootFolder = (await webDavSession.GetItemAsync(WebDAVServerUrl, propNames)).WebDavResponse; - return rootFolder.Properties.Any(p => p.Name.Name == "supported-report-set" && p.StringValue.Contains("sync-collection")); + return rootFolder.Properties.Any(p => p.Name.Name == "supported-report-set" && p.StringValue.Contains("sync-collection")); + } + } + catch(ITHit.WebDAV.Client.Exceptions.WebDavHttpException) + { + return false; } } } diff --git a/macOS/WebDAVDrive/WebDAVMacApp/Resources/appsettings.json b/macOS/WebDAVDrive/WebDAVMacApp/Resources/appsettings.json index 808afa0..f63502d 100644 --- a/macOS/WebDAVDrive/WebDAVMacApp/Resources/appsettings.json +++ b/macOS/WebDAVDrive/WebDAVMacApp/Resources/appsettings.json @@ -17,20 +17,14 @@ // To enable a 1-month trial period, download a trial license here: https://www.webdavsystem.com/client/download/ "WebDAVClientLicense": "", - // Your WebDAV server URL. - // In case this parameter is empty, the dialog to specify the server URL will be displayed during first start. - // In this case, the URL is saved in the registry under the HKEY_CURRENT_USER\SOFTWARE\ key. - - // For testing and demo purposes you can use IT Hit demo servers. Navigate to https://webdavserver.net or - // https://webdavserver.com in a web browser. Copy the URL or your test folder, that looks like - // https://webdavserver.net/User123456/ and specify it below. - "WebDAVServerUrl": "https://server/", - - // Your WebSocket server URL. - // In case of IT Hit demo servers specify server root: wss://webdavserver.net or wss://webdavserver.com - "WebSocketServerUrl": "wss://server/", - - // URL to get a thumbnail for OS file manager thumbnails mode. + // This sample will try to connect to sockets server located on the same port as your WebDAV server, on server root. + // In case of HTTPS the WSS protocol will be used. In case of HTTP - WS. + // For exmple for https://serv:2345/Path/ it will use the wss://serv:2345/ URI. + "WebDAVServerURLs": [ + "https://webdavserver.net/User1383834/" + ], + + // URL to get a thumbnail for Windows Explorer thumbnails mode. // Your server must return 404 Not Found if the thumbnail can not be generated. // If incorrect size is returned, the image will be resized by the platform automatically. "ThumbnailGeneratorUrl": "{path to file}?width={thumbnail width}&height={thumbnail height}", diff --git a/macOS/WebDAVDrive/WebDAVMacApp/WebDAVMacApp.csproj b/macOS/WebDAVDrive/WebDAVMacApp/WebDAVMacApp.csproj index 1f19bcc..8c6354c 100644 --- a/macOS/WebDAVDrive/WebDAVMacApp/WebDAVMacApp.csproj +++ b/macOS/WebDAVDrive/WebDAVMacApp/WebDAVMacApp.csproj @@ -145,7 +145,7 @@ - +