diff --git a/Common/Common.csproj b/Common/Common.csproj index fc7f4c4..bb2ea3b 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -10,7 +10,7 @@ AnyCPU;x64 - - + + \ No newline at end of file diff --git a/Common/CustomColumnIds.cs b/Common/CustomColumnIds.cs index 8195daa..22dbbb6 100644 --- a/Common/CustomColumnIds.cs +++ b/Common/CustomColumnIds.cs @@ -26,8 +26,13 @@ public enum CustomColumnIds LockExpirationDate = 5, /// - /// ETag column ID. + /// Content ETag column ID. /// - ETag = 6, + ContentETag = 6, + + /// + /// Metadata ETag column ID. + /// + MetadataETag = 7, } } diff --git a/Common/FileMetadataExt.cs b/Common/FileMetadataExt.cs index d940ca8..1fa0f29 100644 --- a/Common/FileMetadataExt.cs +++ b/Common/FileMetadataExt.cs @@ -10,5 +10,8 @@ public class FileMetadataExt : FileSystemItemMetadataExt, IFileMetadata { /// public long Length { get; set; } + + /// + public string ContentETag { get; set; } } } diff --git a/Common/FileSystemItemMetadataExt.cs b/Common/FileSystemItemMetadataExt.cs index e9fa652..02e04da 100644 --- a/Common/FileSystemItemMetadataExt.cs +++ b/Common/FileSystemItemMetadataExt.cs @@ -9,49 +9,13 @@ namespace ITHit.FileSystem.Samples.Common { /// /// Represents a basic information about the file or the folder in the user file system. + /// In addition to the properties provided by provides property. /// - public class FileSystemItemMetadataExt : IFileSystemItemMetadata + public class FileSystemItemMetadataExt : FileSystemItemMetadata { - /// - public byte[] RemoteStorageItemId { get; set; } - - /// - public byte[] RemoteStorageParentItemId { get; set; } - - /// - public string Name { get; set; } - - /// - public FileAttributes Attributes { get; set; } - - /// - public DateTimeOffset CreationTime { get; set; } - - /// - public DateTimeOffset LastWriteTime { get; set; } - - /// - public DateTimeOffset LastAccessTime { get; set; } - - /// - public DateTimeOffset ChangeTime { get; set; } - - /// - /// ETag. - /// - public string ETag { get; set; } - /// /// Lock info or null if the item is not locked. /// public ServerLockInfo Lock { get; set; } - - /// - /// Custom columns data to be displayed in the file manager. - /// - //public IEnumerable CustomProperties { get; set; } = new FileSystemItemPropertyData[] { }; - - /// - public ICustomData Properties { get; set; } } } diff --git a/README.md b/README.md index 3153a52..1bc5f96 100644 --- a/README.md +++ b/README.md @@ -83,5 +83,18 @@ Note that these steps are NOT required for development. Sample project provided

+
  • + +

    iOS File Provider Extension Projects Deployment

    +
    + + +

    +To deploy iOS project you will need to create Group ID, App Identifies and Provisioning Profiles.  +The project requires a physical device. You can NOT run the project on iOS simulator. You MUST add a device to a devices list. +Unlike with macOS, ALL b ... +

    +
    +
  • diff --git a/Windows/Common/Core/Commands.cs b/Windows/Common/Core/Commands.cs index 444f029..05d1111 100644 --- a/Windows/Common/Core/Commands.cs +++ b/Windows/Common/Core/Commands.cs @@ -8,19 +8,21 @@ using log4net; using ITHit.FileSystem.Windows; using System.IO; +using System.Collections.Concurrent; +using System.Threading; namespace ITHit.FileSystem.Samples.Common.Windows { /// - /// Application commands. + /// Commands sent from tray app and comnsole. /// public class Commands { /// /// Engine instance. /// - public EngineWindows Engine; + private readonly EngineWindows Engine; /// /// Remote storage monitor. @@ -28,19 +30,20 @@ public class Commands public ISyncService RemoteStorageMonitor; /// - /// Log4Net logger. + /// Remote storaage path. /// - private readonly ILog log; + private readonly string RemoteStorageRootPath; /// - /// Remote storage root path. + /// Log4Net logger. /// - private readonly string remoteStorageRootPath; + private readonly ILog log; - public Commands(ILog log, string remoteStorageRootPath) + public Commands(EngineWindows engine, string remoteStorageRootPath, ILog log) { + this.Engine = engine; + this.RemoteStorageRootPath = remoteStorageRootPath; this.log = log; - this.remoteStorageRootPath = remoteStorageRootPath; } /// @@ -70,7 +73,7 @@ public async Task StartStopSynchronizationAsync() case SynchronizationState.Disabled: if (Engine.State != EngineState.Running) { - Engine.SyncService.Logger.LogError("Failed to start. The Engine must be running."); + Engine.SyncService.Logger.LogError("Failed to start. The Engine must be running.", Engine.Path); return; } await Engine.SyncService.StartAsync(); @@ -84,11 +87,17 @@ public async Task StartStopSynchronizationAsync() public async Task StartStopRemoteStorageMonitorAsync() { + if(RemoteStorageMonitor == null) + { + Engine.Logger.LogError("Remote storage monitor is null.", Engine.Path); + return; + } + if (RemoteStorageMonitor.SyncState == SynchronizationState.Disabled) { if (Engine.State != EngineState.Running) { - log.Error("Failed to start. The Engine must be running."); + Engine.Logger.LogError("Failed to start. The Engine must be running.", Engine.Path); //Engine.RemoteStorageMonitor.Logger.LogError("Failed to start. The Engine must be running."); return; } @@ -115,9 +124,9 @@ public static void Open(string path) } /// - /// Open Windows File Manager with user file system. + /// Open root user file system folder in Windows Explorer. /// - public async Task OpenFolderAsync() + public async Task OpenRootFolderAsync() { Open(Engine.Path); } @@ -127,13 +136,13 @@ public async Task OpenFolderAsync() /// public async Task OpenRemoteStorageAsync() { - Open(remoteStorageRootPath); + Open(RemoteStorageRootPath); } /// /// Opens support portal. /// - public async Task OpenSupportPortalAsync() + public static async Task OpenSupportPortalAsync() { Open("https://www.userfilesystem.com/support/"); } @@ -141,11 +150,12 @@ public async Task OpenSupportPortalAsync() /// /// Called on app exit. /// - public async Task AppExitAsync() + public async Task EngineExitAsync() { await StopEngineAsync(); - log.Info("\n\nAll downloaded file / folder placeholders remain in file system. Restart the application to continue managing files."); - log.Info("\nYou can also edit documents when the app is not running and than start the app to sync all changes to the remote storage.\n"); + log.Info($"\n\n{RemoteStorageRootPath}"); + log.Info("\nAll downloaded file / folder placeholders remain in file system. Restart the application to continue managing files."); + log.Info("\nYou can edit documents when the app is not running and than start the app to sync all changes to the remote storage.\n"); } /// @@ -160,47 +170,49 @@ public async Task StopEngineAsync() } #if DEBUG - - /// - /// Sets console output defaults. - /// - public void ConfigureConsole() - { - // Enable UTF8 for Console Window and set width. - Console.OutputEncoding = System.Text.Encoding.UTF8; - Console.SetWindowSize(Console.LargestWindowWidth, Console.LargestWindowHeight / 3); - //Console.SetBufferSize(Console.LargestWindowWidth * 2, short.MaxValue / 2); - } - /// /// Opens Windows File Manager with both remote storage and user file system for testing. /// /// True if the Remote Storage must be opened. False - otherwise. /// This method is provided solely for the development and testing convenience. - public void ShowTestEnvironment(bool openRemoteStorage = true) + public void ShowTestEnvironment(string userFileSystemWindowName, bool openRemoteStorage = true, CancellationToken cancellationToken = default) { // Open Windows File Manager with user file system. Commands.Open(Engine.Path); + IntPtr hWndUserFileSystem = WindowManager.FindWindow(userFileSystemWindowName, cancellationToken); + WindowManager.PositionFileSystemWindow(hWndUserFileSystem, 1, 2); if (openRemoteStorage) { // Open remote storage. - Commands.Open(remoteStorageRootPath); + Commands.Open(RemoteStorageRootPath); + string rsWindowName = Path.GetFileName(RemoteStorageRootPath.TrimEnd('\\')); + IntPtr hWndRemoteStorage = WindowManager.FindWindow(rsWindowName, cancellationToken); + WindowManager.PositionFileSystemWindow(hWndRemoteStorage, 0, 2); } } + #endif + public void Test() { - string name = "General.docx"; - var n = Engine.ServerNotifications(Path.Combine(Engine.Path, name)); - IFileSystemItemMetadata metadata = new FileMetadataExt(); + string name = "Notes.txt"; + string filePath = Path.Combine(Engine.Path, name); + //FileInfo fi = new FileInfo(filePath); + //fi.IsReadOnly = true; + + var n = Engine.ServerNotifications(filePath); + IFileMetadata metadata = new FileMetadata(); metadata.Attributes = FileAttributes.Normal; metadata.CreationTime = DateTimeOffset.Now; metadata.LastWriteTime = DateTimeOffset.Now; metadata.ChangeTime = DateTimeOffset.Now; metadata.LastAccessTime = DateTimeOffset.Now; metadata.Name = name; + metadata.MetadataETag = DateTimeOffset.Now.Ticks.ToString(); + metadata.ContentETag = null;//"etag1"; n.UpdateAsync(metadata); } + } } diff --git a/Windows/Common/Core/Common.Windows.Core.csproj b/Windows/Common/Core/Common.Windows.Core.csproj index fd48a1a..0460621 100644 --- a/Windows/Common/Core/Common.Windows.Core.csproj +++ b/Windows/Common/Core/Common.Windows.Core.csproj @@ -1,6 +1,7 @@ net7.0-windows10.0.19041.0;net48 + True Contains functionality common for all Windows Virtual Drive samples. IT Hit LTD. IT Hit User File System @@ -20,8 +21,8 @@ - - + + \ No newline at end of file diff --git a/Windows/Common/Core/ConsoleProcessor.cs b/Windows/Common/Core/ConsoleProcessor.cs index eed5382..0e9a991 100644 --- a/Windows/Common/Core/ConsoleProcessor.cs +++ b/Windows/Common/Core/ConsoleProcessor.cs @@ -1,5 +1,6 @@ using ITHit.FileSystem.Windows; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; @@ -12,15 +13,16 @@ namespace ITHit.FileSystem.Samples.Common.Windows /// public class ConsoleProcessor { + public readonly ConcurrentDictionary Commands = new ConcurrentDictionary(); private readonly Registrar registrar; private readonly LogFormatter logFormatter; - private readonly Commands commands; + private readonly string providerId; - public ConsoleProcessor(Registrar registrar, LogFormatter logFormatter, Commands commands) + public ConsoleProcessor(Registrar registrar, LogFormatter logFormatter, string providerId) { this.registrar = registrar; this.logFormatter = logFormatter; - this.commands = commands; + this.providerId = providerId; } /// @@ -61,9 +63,12 @@ public async Task ProcessUserInputAsync() switch (keyInfo.Key) { - case ConsoleKey.X: - commands.Test(); - break; + //case ConsoleKey.X: + // foreach (var keyValCommands in Commands) + // { + // keyValCommands.Value.Test(); + // } + // break; case ConsoleKey.F1: case ConsoleKey.H: @@ -73,12 +78,18 @@ public async Task ProcessUserInputAsync() case ConsoleKey.E: // Start/stop the Engine and all sync services. - await commands.StartStopEngineAsync(); + foreach(var keyValCommands in Commands) + { + await keyValCommands.Value.StartStopEngineAsync(); + } break; case ConsoleKey.S: // Start/stop synchronization. - await commands.StartStopSynchronizationAsync(); + foreach (var keyValCommands in Commands) + { + await keyValCommands.Value.StartStopSynchronizationAsync(); + } break; case ConsoleKey.D: @@ -88,30 +99,40 @@ public async Task ProcessUserInputAsync() case ConsoleKey.M: // Start/stop remote storage monitor. - await commands.StartStopRemoteStorageMonitorAsync(); + foreach (var keyValCommands in Commands) + { + await keyValCommands.Value.StartStopRemoteStorageMonitorAsync(); + } break; case ConsoleKey.L: // Open log file. - Commands.Open(logFormatter.LogFilePath); + Windows.Commands.Open(logFormatter.LogFilePath); break; case ConsoleKey.B: // Submit support tickets, report bugs, suggest features. - await commands.OpenSupportPortalAsync(); + await Windows.Commands.OpenSupportPortalAsync(); break; case ConsoleKey.Escape: // Simulate app uninstall. - await commands.StopEngineAsync(); + foreach (var keyValCommands in Commands) + { + await keyValCommands.Value.StopEngineAsync(); + } + bool removeSparsePackage = FileSystem.Windows.Package.PackageRegistrar.IsRunningWithSparsePackageIdentity() ? keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift) : false; - await registrar.UnregisterAsync(commands.Engine, removeSparsePackage); + await registrar.UnregisterAllSyncRootsAsync(this.providerId, removeSparsePackage); return; case ConsoleKey.Spacebar: // Simulate app restart or machine reboot. - await commands.AppExitAsync(); + foreach (var keyValCommands in Commands) + { + await keyValCommands.Value.EngineExitAsync(); + } return; default: diff --git a/Windows/Common/Core/LogFormatter.cs b/Windows/Common/Core/LogFormatter.cs index 46f9dc5..f044597 100644 --- a/Windows/Common/Core/LogFormatter.cs +++ b/Windows/Common/Core/LogFormatter.cs @@ -36,19 +36,17 @@ public bool DebugLoggingEnabled { debugLoggingEnabled = value; string debugLoggingState = debugLoggingEnabled ? "Enabled" : "Disabled"; - log.Info($"{Environment.NewLine}Debug logging {debugLoggingState}"); + Log.Info($"{Environment.NewLine}Debug logging {debugLoggingState}"); } } - private bool debugLoggingEnabled = false; + public readonly ILog Log; - private readonly ILog log; + private bool debugLoggingEnabled = false; private readonly string appId; - private readonly string remoteStorageRootPath; - - private const int sourcePathWidth = 60; + private const int sourcePathWidth = 45; private const int remoteStorageIdWidth = 20; private const int indent = -45; @@ -57,11 +55,10 @@ public bool DebugLoggingEnabled /// Creates instance of this class. /// /// Log4net logger. - public LogFormatter(ILog log, string appId, string remoteStorageRootPath) + public LogFormatter(ILog log, string appId) { - this.log = log; + this.Log = log; this.appId = appId; - this.remoteStorageRootPath = remoteStorageRootPath; LogFilePath = ConfigureLogger(); } @@ -91,24 +88,24 @@ private string ConfigureLogger() public void PrintEnvironmentDescription() { // Log environment description. - log.Info($"\n{"AppID:",indent} {appId}"); - log.Info($"\n{"Engine version:",indent} {typeof(IEngine).Assembly.GetName().Version}"); - log.Info($"\n{"OS version:",indent} {RuntimeInformation.OSDescription}"); - log.Info($"\n{".NET version:",indent} {RuntimeInformation.FrameworkDescription} {IntPtr.Size * 8}bit."); - log.Info($"\n{"Package or app identity:",indent} {PackageRegistrar.IsRunningWithIdentity()}"); - log.Info($"\n{"Sparse package identity:",indent} {PackageRegistrar.IsRunningWithSparsePackageIdentity()}"); - log.Info($"\n{"Elevated mode:",indent} {new System.Security.Principal.WindowsPrincipal(System.Security.Principal.WindowsIdentity.GetCurrent()).IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator)}"); + Log.Info($"\n{"AppID:",indent} {appId}"); + Log.Info($"\n{"Engine version:",indent} {typeof(IEngine).Assembly.GetName().Version}"); + Log.Info($"\n{"OS version:",indent} {RuntimeInformation.OSDescription}"); + Log.Info($"\n{".NET version:",indent} {RuntimeInformation.FrameworkDescription} {IntPtr.Size * 8}bit."); + Log.Info($"\n{"Package or app identity:",indent} {PackageRegistrar.IsRunningWithIdentity()}"); + Log.Info($"\n{"Sparse package identity:",indent} {PackageRegistrar.IsRunningWithSparsePackageIdentity()}"); + Log.Info($"\n{"Elevated mode:",indent} {new System.Security.Principal.WindowsPrincipal(System.Security.Principal.WindowsIdentity.GetCurrent()).IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator)}"); string sparsePackagePath = PackageRegistrar.GetSparsePackagePath(); if (File.Exists(sparsePackagePath)) { - log.Info($"\n{"Sparse package location:",indent} {sparsePackagePath}"); + Log.Info($"\n{"Sparse package location:",indent} {sparsePackagePath}"); var cert = System.Security.Cryptography.X509Certificates.X509Certificate.CreateFromSignedFile(sparsePackagePath); - log.Info($"\n{"Sparse package cert:",indent} Subject: {cert.Subject}, Issued by: {cert.Issuer}"); + Log.Info($"\n{"Sparse package cert:",indent} Subject: {cert.Subject}, Issued by: {cert.Issuer}"); } else { - log.Info($"\n{"Sparse package:",indent} Not found"); + Log.Info($"\n{"Sparse package:",indent} Not found"); } } @@ -117,25 +114,32 @@ public void PrintEnvironmentDescription() /// /// Engine instance. /// Remote storage root path. - public async Task PrintEngineStartInfoAsync(EngineWindows engine) + public async Task PrintEngineStartInfoAsync(EngineWindows engine, string remoteStorageRootPath) { - await PrintEngineDescriptionAsync(engine); - log.Info("\n"); + await PrintEngineDescriptionAsync(engine, remoteStorageRootPath); + Log.Info("\n"); // Log logging columns headers. PrintHeader(); } - public async Task PrintEngineDescriptionAsync(EngineWindows engine) + /// + /// + /// + /// Engine instance. + /// Remote storage root path. + /// + public async Task PrintEngineDescriptionAsync(EngineWindows engine, string remoteStorageRootPath) { - log.Info($"\n"); - log.Info($"\n{"File system root:",indent} {engine.Path}"); - log.Info($"\n{"Remote storage root:",indent} {remoteStorageRootPath}"); - log.Info($"\n{"AutoLock:",indent} {engine.AutoLock}"); - log.Info($"\n{"Outgoing sync, ms:",indent} {engine.SyncService.SyncIntervalMs}"); - log.Info($"\n{"Shell extensions RPC enabled:",indent} {engine.ShellExtensionsComServerRpcEnabled}"); - log.Info($"\n{"Max create/read/write concurrent requests:",indent} {engine.MaxTransferConcurrentRequests}"); - log.Info($"\n{"Max list/move/delete concurrent requests:",indent} {engine.MaxOperationsConcurrentRequests}"); + Log.Info($"\n"); + Log.Info($"\n{"File system root:",indent} {engine.Path}"); + Log.Info($"\n{"Remote storage root:",indent} {remoteStorageRootPath}"); + Log.Info($"\n{"AutoLock:",indent} {engine.AutoLock}"); + Log.Info($"\n{"Outgoing sync, ms:",indent} {engine.SyncService.SyncIntervalMs}"); + Log.Info($"\n{"Sync mode:",indent} {engine.SyncService.IncomingSyncMode}"); + Log.Info($"\n{"Shell extensions RPC enabled:",indent} {engine.ShellExtensionsComServerRpcEnabled}"); + Log.Info($"\n{"Max create/read/write concurrent requests:",indent} {engine.MaxTransferConcurrentRequests}"); + Log.Info($"\n{"Max list/move/delete concurrent requests:",indent} {engine.MaxOperationsConcurrentRequests}"); // Log indexing state. Sync root must be indexed. await PrintIndexingStateAsync(engine.Path); @@ -149,17 +153,17 @@ private async Task PrintIndexingStateAsync(string path) { StorageFolder userFileSystemRootFolder = await StorageFolder.GetFolderFromPathAsync(path); IndexedState indexedState = await userFileSystemRootFolder.GetIndexedStateAsync(); - log.Info($"\n{"Indexed state:",indent} {indexedState}"); + Log.Info($"\n{"Indexed state:",indent} {indexedState}"); if (indexedState != IndexedState.FullyIndexed) { - log.ErrorFormat($"\nIndexing is disabled. Indexing must be enabled for {path}"); + Log.ErrorFormat($"\nIndexing is disabled. Indexing must be enabled for {path}"); } } public void LogMessage(string message) { - log.Info(message); + Log.Info(message); } public void LogError(IEngine sender, EngineErrorEventArgs e) @@ -222,15 +226,15 @@ private void WriteLog(IEngine sender, EngineMessageEventArgs e, log4net.Core.Lev { message += Environment.NewLine; } - log.Error(message, ex); + Log.Error(message, ex); } else if (level == log4net.Core.Level.Info) { - log.Info(message); + Log.Info(message); } else if (level == log4net.Core.Level.Debug && DebugLoggingEnabled) { - log.Debug(message); + Log.Debug(message); } } @@ -246,9 +250,9 @@ private static string Format(string date, string process, string priorityHint, s /// private void PrintHeader() { - log.Info("\n"); - log.Info(Format("Time", "Process Name", "Prty", "FS ID", "RS ID", "Component", "Line", "Caller Member Name", "Caller File Path", "Message", "Path", "Attributes")); - log.Info(Format("----", "------------", "----", "_____", "_____", "---------", "____", "------------------", "----------------", "-------", "----", "----------")); + Log.Info("\n"); + Log.Info(Format("Time", "Process Name", "Prty", "FS ID", "RS ID", "Component", "Line", "Caller Member Name", "Caller File Path", "Message", "Path", "Attributes")); + Log.Info(Format("----", "------------", "----", "_____", "_____", "---------", "____", "------------------", "----------------", "-------", "----", "----------")); } /// diff --git a/Windows/Common/Core/Registrar.cs b/Windows/Common/Core/Registrar.cs index a0b19ad..ed12987 100644 --- a/Windows/Common/Core/Registrar.cs +++ b/Windows/Common/Core/Registrar.cs @@ -12,6 +12,7 @@ using ITHit.FileSystem.Windows.Package; using ITHit.FileSystem.Windows.ShellExtension; using System.Reflection; +using System.Linq; namespace ITHit.FileSystem.Samples.Common.Windows { @@ -20,8 +21,6 @@ namespace ITHit.FileSystem.Samples.Common.Windows /// public class Registrar { - protected readonly string SyncRootId; - protected readonly string UserFileSystemRootPath; protected readonly ILog Log; private readonly IEnumerable<(string Name, Guid Guid, bool AlwaysRegister)> shellExtensionHandlers; @@ -37,10 +36,8 @@ public class Registrar /// List of shell extension handlers. Use it only for applications without application or package identity. /// For applications with identity this list is ignored. /// - public Registrar(string syncRootId, string userFileSystemRootPath, ILog log, IEnumerable<(string Name, Guid Guid, bool AlwaysRegister)> shellExtensionHandlers = null) + public Registrar(ILog log, IEnumerable<(string Name, Guid Guid, bool AlwaysRegister)> shellExtensionHandlers = null) { - this.SyncRootId = syncRootId; - this.UserFileSystemRootPath = userFileSystemRootPath; this.Log = log; this.shellExtensionHandlers = shellExtensionHandlers; } @@ -55,27 +52,31 @@ public Registrar(string syncRootId, string userFileSystemRootPath, ILog log, IEn /// In the case of a packaged installer (msix) call this method during first program start. /// In the case of a regular installer (msi) call this method during installation. /// - public async Task RegisterSyncRootAsync(string displayName, string iconPath, string shellExtensionsComServerExePath = null) + public async Task RegisterSyncRootAsync(string syncRootId, string userFileSystemRootPath, string remotestorageRootPath, string displayName, string iconPath, string shellExtensionsComServerExePath = null) { - if (!await IsRegisteredAsync(UserFileSystemRootPath)) + StorageProviderSyncRootInfo syncRoot = null; + if (!await IsRegisteredAsync(userFileSystemRootPath)) { Log.Info($"\n\nRegistering sync root."); - Directory.CreateDirectory(UserFileSystemRootPath); + Directory.CreateDirectory(userFileSystemRootPath); - await RegisterAsync(SyncRootId, UserFileSystemRootPath, displayName, iconPath); + syncRoot = await RegisterAsync(syncRootId, userFileSystemRootPath, remotestorageRootPath, displayName, iconPath); } else { - Log.Info($"\n\nSync root already registered: {UserFileSystemRootPath}"); + Log.Info($"\n\nSync root already registered: {userFileSystemRootPath}"); } if (shellExtensionHandlers != null) { // Register thumbnails handler, custom states handler, etc. - RegisterShellExtensions(shellExtensionsComServerExePath); + RegisterShellExtensions(syncRootId, shellExtensionHandlers, Log, shellExtensionsComServerExePath); } + + return syncRoot; } + /* /// /// Unregisters sync root, shell extensions, deletes all synced items. /// @@ -102,7 +103,90 @@ public virtual async Task UnregisterSyncRootAsync(EngineWindows engine) // Delete all files/folders. await CleanupAppFoldersAsync(engine); } + */ + + /// + /// Unmounts sync root, deletes all synced items and any data stored by the Engine. + /// + public static async Task UnregisterSyncRootAsync(string syncRootPath, string dataPath, ILog log) + { + bool res = await UnregisterSyncRootAsync(syncRootPath, log); + + // Delete data folder. + + // IMPORTANT! + // Delete any data only if unregistration was succesefull! + + if (res && !string.IsNullOrWhiteSpace(dataPath)) + { + log.Debug($"Deleteing data folder {syncRootPath} {dataPath}"); + try + { + Directory.Delete(dataPath, true); + } + catch (Exception ex) + { + res = false; + log.Error($"Failed to delete data folder {syncRootPath} {dataPath}", ex); + } + } + + return res; + } + + /// + /// Unmounts sync root, deletes all synced items. + /// + private static async Task UnregisterSyncRootAsync(string syncRootPath, ILog logger) + { + bool res = false; + // Get sync root ID. + StorageFolder storageFolder = await StorageFolder.GetFolderFromPathAsync(syncRootPath); + StorageProviderSyncRootInfo syncRootInfo = null; + try + { + logger.Debug($"Getting sync root info {syncRootPath}"); + syncRootInfo = StorageProviderSyncRootManager.GetSyncRootInformationForFolder(storageFolder); + } + catch (Exception ex) + { + logger.Error($"Sync root is not registered {syncRootPath}", ex); + } + // Unregister sync root. + if (syncRootInfo != null) + { + try + { + logger.Debug($"Unregistering sync root {syncRootPath} {syncRootInfo.Id}"); + StorageProviderSyncRootManager.Unregister(syncRootInfo.Id); + } + catch (Exception ex) + { + logger.Error($"Failed to unregister sync root {syncRootPath} {syncRootInfo.Id}", ex); + // IMPORTANT! + // If Unregister() failed, deleting items on the client may trigger deleting + // items in the remote storage if the Engine did not stop or if started again. + // Do NOT delete sync root folder in this case! + return res; + } + } + + // Delete sync root folder. + try + { + logger.Debug($"Deleteing sync root folder {syncRootPath}"); + Directory.Delete(syncRootPath, true); + res = true; + } + catch (Exception ex) + { + logger.Error($"Failed to delete sync root folder {syncRootPath}", ex); + } + return res; + } + + /* public async Task CleanupAppFoldersAsync(EngineWindows engine) { Log.Info("\n\nDeleting all file and folder placeholders."); @@ -130,18 +214,40 @@ public async Task CleanupAppFoldersAsync(EngineWindows engine) Log.Error($"\n{ex}"); } } + */ /// - /// Unregisters all components. + /// Unregisters all sync roots that has a provider ID and removes all components. /// - /// + /// This method will only unmount sync roots that has this provider ID. + /// /// Pass true in the released application to remove all registered components. /// Pass false in development mode, to keep sparse package, /// development certificate or any other components required for development convenience. /// - public virtual async Task UnregisterAsync(EngineWindows engine, bool fullUnregistration = true) + public virtual async Task UnregisterAllSyncRootsAsync(string providerId, bool fullUnregistration = true) { - await UnregisterSyncRootAsync(engine); + bool res = true; + var syncRoots = StorageProviderSyncRootManager.GetCurrentSyncRoots(); + foreach(var syncRoot in syncRoots) + { + string storedProviderId = syncRoot.Id?.Split('!')?.FirstOrDefault(); + if (storedProviderId.Equals(providerId)) + { + if (!await UnregisterSyncRootAsync(syncRoot.Path.Path, Log)) + { + res = false; + } + } + } + + // Unregister shell extensions. + if (shellExtensionHandlers != null) + { + UnregisterShellExtensions(); + } + + return res; } /// @@ -156,19 +262,19 @@ public virtual async Task UnregisterAsync(EngineWindows engine, bool fullUnregis /// Note that this method can NOT register context menu commands on Windows 11. Windows 11 context menu /// requires application or package identity. /// - private void RegisterShellExtensions(string shellExtensionsComServerExePath = null) + private static void RegisterShellExtensions(string syncRootId, IEnumerable<(string Name, Guid Guid, bool AlwaysRegister)> shellExtensionHandlers, ILog log, string shellExtensionsComServerExePath = null) { bool isRunningWithIdentity = PackageRegistrar.IsRunningWithIdentity(); foreach (var handler in shellExtensionHandlers) { - if (!ShellExtensionRegistrar.IsHandlerRegistered(handler.Guid)) + //if (!ShellExtensionRegistrar.IsHandlerRegistered(handler.Guid)) { // Register handlers only if this app has no identity. Otherwise manifest will do this automatically. // Unlike other handlers, CopyHook requires registration regardless of identity. if (!isRunningWithIdentity || handler.AlwaysRegister) { - Log.Info($"\nRegistering shell extension {handler.Name} with CLSID {handler.Guid:B}...\n"); - ShellExtensionRegistrar.RegisterHandler(SyncRootId, handler.Name, handler.Guid, shellExtensionsComServerExePath); + log.Info($"\nRegistering shell extension {handler.Name} with CLSID {handler.Guid:B}...\n"); + ShellExtensionRegistrar.RegisterHandler(syncRootId, handler.Name, handler.Guid, shellExtensionsComServerExePath); } } } @@ -178,8 +284,6 @@ private void RegisterShellExtensions(string shellExtensionsComServerExePath = nu /// Unregisters shell service providers COM classes. /// Use this method only for applications without application or package identity. /// - /// List of shell extension handlers. - /// log4net Logger. /// /// Note that this method can NOT unregister context menu commands on Windows 11. Windows 11 context menu /// requires application or package identity. @@ -203,13 +307,15 @@ private void UnregisterShellExtensions() /// /// ID of the sync root. /// A root folder of your user file system. Your file system tree will be located under this folder. + /// Remote storage path. It will be stored inide the sync root to distinguish between sync roots when mounting a new remote storage. /// Human readable display name. /// Path to the drive ico file. + /// Provider ID will be stored in sync root to find if this sync root belongs to this application. /// /// In the case of a packaged installer (msix) call this method during first program start. /// In the case of a regular installer (msi) call this method during installation. /// - private static async Task RegisterAsync(string syncRootId, string path, string displayName, string iconPath) + private static async Task RegisterAsync(string syncRootId, string path, string remoteStoragePath, string displayName, string iconPath) { StorageProviderSyncRootInfo storageInfo = new StorageProviderSyncRootInfo(); storageInfo.Path = await StorageFolder.GetFolderFromPathAsync(path); @@ -218,7 +324,8 @@ private static async Task RegisterAsync(string syncRootId, string path, string d storageInfo.IconResource = iconPath; storageInfo.Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); storageInfo.RecycleBinUri = new Uri("https://userfilesystem.com/recyclebin"); - storageInfo.Context = CryptographicBuffer.ConvertStringToBinary(path, BinaryStringEncoding.Utf8); + storageInfo.SetRemoteStoragePath(remoteStoragePath); + //storageInfo.ProviderId = providerID; // Provider ID is not returned by StorageProviderSyncRootManager.GetCurrentSyncRoots() // To open mp4 files using Windows Movies & TV application the Hydration Policy must be set to Full. storageInfo.HydrationPolicy = StorageProviderHydrationPolicy.Full; @@ -229,17 +336,7 @@ private static async Task RegisterAsync(string syncRootId, string path, string d storageInfo.PopulationPolicy = StorageProviderPopulationPolicy.Full; // Set to Full to list folder content immediately on program start. // The read-only attribute is used to indicate that the item is being locked by another user. Do NOT include it into InSyncPolicy. - storageInfo.InSyncPolicy = - //StorageProviderInSyncPolicy.FileCreationTime | StorageProviderInSyncPolicy.DirectoryCreationTime | - //StorageProviderInSyncPolicy.FileLastWriteTime | StorageProviderInSyncPolicy.DirectoryLastWriteTime | - //StorageProviderInSyncPolicy.FileHiddenAttribute | StorageProviderInSyncPolicy.DirectoryHiddenAttribute | - //StorageProviderInSyncPolicy.FileSystemAttribute | StorageProviderInSyncPolicy.DirectorySystemAttribute | - //StorageProviderInSyncPolicy.FileReadOnlyAttribute | StorageProviderInSyncPolicy.DirectoryReadOnlyAttribute | - StorageProviderInSyncPolicy.Default; - - //storageInfo.ShowSiblingsAsGroup = false; - //storageInfo.HardlinkPolicy = StorageProviderHardlinkPolicy.None; - + storageInfo.InSyncPolicy = StorageProviderInSyncPolicy.Default; // Adds columns to Windows File Manager. // Show/hide columns in the "More..." context menu on the columns header in Windows Explorer. @@ -247,12 +344,15 @@ private static async Task RegisterAsync(string syncRootId, string path, string d proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "Lock Owner" , Id = (int)CustomColumnIds.LockOwnerIcon }); proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "Lock Scope" , Id = (int)CustomColumnIds.LockScope }); proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "Lock Expires" , Id = (int)CustomColumnIds.LockExpirationDate }); - proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "ETag" , Id = (int)CustomColumnIds.ETag }); + proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "Content ETag" , Id = (int)CustomColumnIds.ContentETag }); + proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "Metadata ETag", Id = (int)CustomColumnIds.MetadataETag }); ValidateStorageProviderSyncRootInfo(storageInfo); StorageProviderSyncRootManager.Register(storageInfo); + + return storageInfo; } @@ -308,13 +408,13 @@ private static void ValidateStorageProviderSyncRootInfo(StorageProviderSyncRootI /// /// Determines if the syn root is registered for specified folder. /// - /// Sync root path. + /// Sync root path. /// True if the sync root is registered, false otherwise. - private static async Task IsRegisteredAsync(string path) + private static async Task IsRegisteredAsync(string userFileSystemPath) { - if (Directory.Exists(path)) + if (Directory.Exists(userFileSystemPath)) { - StorageFolder storageFolder = await StorageFolder.GetFolderFromPathAsync(path); + StorageFolder storageFolder = await StorageFolder.GetFolderFromPathAsync(userFileSystemPath); try { StorageProviderSyncRootManager.GetSyncRootInformationForFolder(storageFolder); @@ -328,6 +428,60 @@ private static async Task IsRegisteredAsync(string path) return false; } + /// + /// Determines if the syn root is registered for specified URI. + /// + /// Uri. + /// True if the sync root is registered, false otherwise. + public static bool IsRegisteredUri(Uri uri) + { + return GetSyncRootInfo(uri) != null; + } + + /// + /// Determines if the syn root is registered for specified URI. + /// + /// Uri. + /// True if the sync root is registered, false otherwise. + public static bool IsRegisteredUri(string uriString) + { + if (!Uri.IsWellFormedUriString(uriString, UriKind.Absolute)) + throw new ArgumentException("URI not well formed", nameof(uriString)); + + if (Uri.TryCreate(uriString, UriKind.Absolute, out Uri uri)) + { + return IsRegisteredUri(uri); + } + else + { + throw new ArgumentException("Can not create URI", nameof(uriString)); + } + } + + /// + /// Gets sync root info for specified URI or null if sync root is not registered for this URI. + /// + /// Uri. + /// Sync root info or null. + private static StorageProviderSyncRootInfo GetSyncRootInfo(Uri uri) + { + var syncRoots = StorageProviderSyncRootManager.GetCurrentSyncRoots(); + + foreach (var syncRoot in syncRoots) + { + string storedUri = syncRoot.GetRemoteStoragePath(); + if (Uri.TryCreate(storedUri, UriKind.Absolute, out Uri storedParsedUri)) + { + if (storedParsedUri.Equals(uri)) + { + return syncRoot; + } + } + } + + return null; + } + /// /// Unregisters sync root. /// @@ -348,5 +502,43 @@ private static async Task UnregisterAsync(string syncRootId) { StorageProviderSyncRootManager.Unregister(syncRootId); } + + public static async Task> GetMountedSyncRootsAsync(string providerId, ILog log) + { + _ = string.IsNullOrEmpty(providerId) ? throw new ArgumentNullException(nameof(providerId)) : string.Empty; + + IList mountedRoots = new List(); + IReadOnlyList syncRoots = StorageProviderSyncRootManager.GetCurrentSyncRoots(); + foreach (var syncRoot in syncRoots) + { + string storedProviderId = syncRoot.Id?.Split('!')?.FirstOrDefault(); + if (storedProviderId.Equals(providerId)) + { + string storedUri = syncRoot.GetRemoteStoragePath(); + if (!System.Uri.TryCreate(storedUri, UriKind.Absolute, out System.Uri _)) + { + log.Error($"Can not parse URI for {syncRoot.DisplayNameResource}: {storedUri}"); + continue; + } + + mountedRoots.Add(syncRoot); + } + } + + return mountedRoots; + } + } + + public static class StorageProviderSyncRootInfoExtensions + { + public static string GetRemoteStoragePath(this StorageProviderSyncRootInfo rootInfo) + { + return CryptographicBuffer.ConvertBinaryToString(BinaryStringEncoding.Utf16LE, rootInfo.Context); + } + + public static void SetRemoteStoragePath(this StorageProviderSyncRootInfo rootInfo, string remoteStoragePath) + { + rootInfo.Context = CryptographicBuffer.ConvertStringToBinary(remoteStoragePath, BinaryStringEncoding.Utf16LE); + } } } diff --git a/Windows/Common/Core/WindowManager.cs b/Windows/Common/Core/WindowManager.cs new file mode 100644 index 0000000..c9d723b --- /dev/null +++ b/Windows/Common/Core/WindowManager.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ITHit.FileSystem.Samples.Common.Windows +{ + /// + /// Window helper methods. + /// + public static class WindowManager + + { + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + } + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool GetWindowRect(IntPtr hWnd, ref RECT Rect); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int Width, int Height, bool Repaint); + + [DllImport("user32.dll")] + private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport("user32.dll")] + private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + + /// + /// Console visibility. + /// + public static bool ConsoleVisible { get; private set; } +#if !DEBUG + = false; +#else + = true; +#endif + + /// + /// Hides or hides console window. + /// + /// Console visibility. + public static void SetConsoleWindowVisibility(bool setVisible) + { + IntPtr hWnd = FindWindow(null, Console.Title); + if (hWnd != IntPtr.Zero) + { + ShowWindow(hWnd, setVisible ? 1 : 0); + ConsoleVisible = setVisible; + } + } + +#if DEBUG + /// + /// Stratches console window to the width of the screen and positions it at the bottom of the screen. + /// + private static void PositionConsoleWindow() + { + IntPtr hWnd = FindWindow(null, Console.Title); + if (hWnd != IntPtr.Zero) + { + RECT rect = new RECT(); + if (GetWindowRect(hWnd, ref rect)) + { + var screen = System.Windows.Forms.Screen.FromHandle(hWnd); + int height = screen.WorkingArea.Height / 4; + MoveWindow(hWnd, 0, screen.WorkingArea.Height - height, screen.WorkingArea.Width, height, true); + } + } + } + + public static void PositionFileSystemWindow(IntPtr hWnd, int horizontalIndex, int totalWindows) + { + if (hWnd != IntPtr.Zero) + { + RECT rect = new RECT(); + if (GetWindowRect(hWnd, ref rect)) + { + var screen = System.Windows.Forms.Screen.FromHandle(hWnd); + int height = (screen.WorkingArea.Height / 4)*3; + int width = (screen.WorkingArea.Width / totalWindows); + int x = width * horizontalIndex; + MoveWindow(hWnd, x, 0, width, height, true); + } + } + } + + public static IntPtr FindWindow(string name, CancellationToken cancellationToken = default) + { + IntPtr hWnd = IntPtr.Zero; + do + { + hWnd = FindWindow(null, name); + if (hWnd != IntPtr.Zero) + { + return hWnd; + } + Thread.Sleep(100); + } while(hWnd == IntPtr.Zero && !cancellationToken.IsCancellationRequested); + return IntPtr.Zero; + } + + /// + /// Sets console output defaults. + /// + public static void ConfigureConsole() + { + // Enable UTF8 for Console Window and set width. + Console.OutputEncoding = System.Text.Encoding.UTF8; + Console.SetWindowSize(Console.LargestWindowWidth, Console.LargestWindowHeight / 3); + //Console.SetBufferSize(Console.LargestWindowWidth * 2, short.MaxValue / 2); + + PositionConsoleWindow(); + } +#endif + } +} diff --git a/Windows/Common/VirtualDrive/Common.Windows.VirtualDrive.csproj b/Windows/Common/VirtualDrive/Common.Windows.VirtualDrive.csproj index badc41c..3e954d4 100644 --- a/Windows/Common/VirtualDrive/Common.Windows.VirtualDrive.csproj +++ b/Windows/Common/VirtualDrive/Common.Windows.VirtualDrive.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/Windows/Common/VirtualDrive/CustomDataExtensions.cs b/Windows/Common/VirtualDrive/CustomDataExtensions.cs index 85f16e3..d606171 100644 --- a/Windows/Common/VirtualDrive/CustomDataExtensions.cs +++ b/Windows/Common/VirtualDrive/CustomDataExtensions.cs @@ -50,46 +50,6 @@ public static void SaveProperties(this ICustomData properties, FileSystemItemMet } } } - - // Save eTag. This is for the demo purposes only. - // Update eTag only for offline files. - // For hydrated files eTag must be updated in IFile.ReadAsync() only. - if (metadata.ETag != null && System.IO.File.GetAttributes(path).HasFlag(System.IO.FileAttributes.Offline)) - { - properties.SetETag(metadata.ETag); - } - - //foreach (FileSystemItemPropertyData prop in metadata.CustomProperties) - //{ - // string key = ((CustomColumnIds)prop.Id).ToString(); - // properties.AddOrUpdate(key, prop); - //} - } - - /// - /// Tries to get eTag. - /// - /// Custom data attached to the item. - /// eTag. - /// True if method succeeded. False - otherwise. - public static bool TryGetETag(this ICustomData properties, out string eTag) - { - if (properties.TryGetValue("ETag", out IDataItem propETag)) - { - return propETag.TryGetValue(out eTag); - } - eTag = null; - return false; - } - - /// - /// Sets eTag. - /// - /// Custom data attached to the item. - /// eTag. - public static void SetETag(this ICustomData properties, string eTag) - { - properties.AddOrUpdate("ETag", eTag); } /// @@ -155,24 +115,6 @@ public static bool TryDeleteLockInfo(this ICustomData properties) return properties.Remove("LockInfo"); } - /// - /// Returns true if the remote item is modified. False - otherwise. - /// - /// - /// This method compares client and server eTags and returns true if the - /// content in the user file system must be updated with the data from the remote storage. - /// - /// Placeholder item. - /// Remote storage item metadata. - /// - public static async Task IsModifiedAsync(this PlaceholderItem placeholder, FileSystemItemMetadataExt remoteStorageItemMetadata) - { - //return placeholder.Properties.TryGetETag(out string eTag) && !(eTag?.Equals(remoteStorageItemMetadata.ETag) ?? false); - - placeholder.Properties.TryGetETag(out string clientEtag); - return clientEtag != remoteStorageItemMetadata.ETag; - } - /// /// Gets Engine instance. /// diff --git a/Windows/Common/VirtualDrive/MenuCommandLock.cs b/Windows/Common/VirtualDrive/MenuCommandLock.cs index 310663d..3fbbce9 100644 --- a/Windows/Common/VirtualDrive/MenuCommandLock.cs +++ b/Windows/Common/VirtualDrive/MenuCommandLock.cs @@ -66,7 +66,7 @@ public async Task GetStateAsync(IEnumerable filesPath) } /// - public async Task InvokeAsync(IEnumerable filesPath, IEnumerable remoteStorageItemIds = null) + public async Task InvokeAsync(IEnumerable filesPath, IEnumerable remoteStorageItemIds = null, CancellationToken cancellationToken = default) { // If you need a remote storage ID for each item use the following code: //foreach (string userFileSystemPath in filesPath) diff --git a/Windows/Common/VirtualDrive/SparsePackageRegistrar.cs b/Windows/Common/VirtualDrive/SparsePackageRegistrar.cs index 1daa43b..aead63d 100644 --- a/Windows/Common/VirtualDrive/SparsePackageRegistrar.cs +++ b/Windows/Common/VirtualDrive/SparsePackageRegistrar.cs @@ -32,11 +32,9 @@ public class SparsePackageRegistrar : Registrar /// For applications with identity this list is ignored. /// public SparsePackageRegistrar( - string syncRootId, - string userFileSystemRootPath, ILog log, IEnumerable<(string Name, Guid Guid, bool AlwaysRegister)> shellExtensionHandlers = null) - : base(syncRootId, userFileSystemRootPath, log, shellExtensionHandlers) + : base(log, shellExtensionHandlers) { } @@ -63,7 +61,7 @@ public async Task RegisterSparsePackageAsync() #if DEBUG /// Registering sparse package requires a valid certificate. /// In the development mode we use the below call to install the development certificate. - if (!EnsureDevelopmentCertificateInstalled()) + if (!EnsureDevelopmentCertificateInstalled(Log)) { return false; } @@ -87,7 +85,7 @@ public async Task UnregisterSparsePackageAsync() { #if DEBUG // Uninstall developer certificate. - EnsureDevelopmentCertificateUninstalled(); + EnsureDevelopmentCertificateUninstalled(Log); // Uninstall conflicting packages if any await EnsureConflictingPackagesUninstalled(); @@ -99,14 +97,16 @@ public async Task UnregisterSparsePackageAsync() } /// - public override async Task UnregisterAsync(EngineWindows engine, bool fullUnregistration = true) + public override async Task UnregisterAllSyncRootsAsync(string providerId, bool fullUnregistration = true) { - await base.UnregisterAsync(engine, fullUnregistration); + bool res = await base.UnregisterAllSyncRootsAsync(providerId, fullUnregistration); if (fullUnregistration) { await UnregisterSparsePackageAsync(); } + + return res; } #if DEBUG @@ -120,20 +120,20 @@ public override async Task UnregisterAsync(EngineWindows engine, bool fullUnregi /// should be omitted for packaged application. /// /// True if the the certificate is installed, false - if the installation failed. - public bool EnsureDevelopmentCertificateInstalled() + public static bool EnsureDevelopmentCertificateInstalled(ILog log) { string sparsePackagePath = PackageRegistrar.GetSparsePackagePath(); CertificateRegistrar certificateRegistrar = new CertificateRegistrar(sparsePackagePath); if (!certificateRegistrar.IsCertificateInstalled()) { - Log.Info("\n\nInstalling developer certificate..."); + log.Info("\n\nInstalling developer certificate..."); if (certificateRegistrar.TryInstallCertificate(true, out int errorCode)) { - Log.Info("\nDeveloper certificate successfully installed."); + log.Info("\nDeveloper certificate successfully installed."); } else { - Log.Error($"\nFailed to install the developer certificate. Error code: {errorCode}"); + log.Error($"\nFailed to install the developer certificate. Error code: {errorCode}"); return false; } } @@ -145,20 +145,20 @@ public bool EnsureDevelopmentCertificateInstalled() /// Uninstalls a development certificate. /// /// True if the the certificate is uninstalled, false - if the uninstallation failed. - public bool EnsureDevelopmentCertificateUninstalled() + public static bool EnsureDevelopmentCertificateUninstalled(ILog log) { string sparsePackagePath = PackageRegistrar.GetSparsePackagePath(); CertificateRegistrar certRegistrar = new CertificateRegistrar(sparsePackagePath); if (certRegistrar.IsCertificateInstalled()) { - Log.Info("\n\nUninstalling developer certificate..."); + log.Info("\n\nUninstalling developer certificate..."); if (certRegistrar.TryUninstallCertificate(true, out int errorCode)) { - Log.Info("\nDeveloper certificate successfully uninstalled."); + log.Info("\nDeveloper certificate successfully uninstalled."); } else { - Log.Error($"\nFailed to uninstall the developer certificate. Error code: {errorCode}"); + log.Error($"\nFailed to uninstall the developer certificate. Error code: {errorCode}"); return false; } } diff --git a/Windows/Common/VirtualDrive/VirtualEngineBase.cs b/Windows/Common/VirtualDrive/VirtualEngineBase.cs index 72bf8e7..f634208 100644 --- a/Windows/Common/VirtualDrive/VirtualEngineBase.cs +++ b/Windows/Common/VirtualDrive/VirtualEngineBase.cs @@ -1,6 +1,4 @@ using System; -using System.IO; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; @@ -91,19 +89,18 @@ public VirtualEngineBase( /// private void Engine_ItemsChanged(Engine sender, ItemsChangeEventArgs e) { - var logger = Logger.CreateLogger(e.ComponentName); foreach (ChangeEventItem item in e.Items) { - // Save custom properties received from the remote storage - // that will be displayed in Windows Explorer columns. - if (e.Direction == SyncDirection.Incoming && e.Result.Status == OperationStatus.Success) + // Save custom properties received from the remote storage here + // they will be displayed in Windows Explorer columns. + if (e.Direction == SyncDirection.Incoming && e.Result.IsSuccess) { switch (e.OperationType) { case OperationType.Create: //case OperationType.CreateCompletion: case OperationType.Populate: - case OperationType.Update: + case OperationType.UpdateMetadata: if (item.Metadata != null) { item.Properties.SaveProperties(item.Metadata as FileSystemItemMetadataExt); @@ -113,46 +110,54 @@ private void Engine_ItemsChanged(Engine sender, ItemsChangeEventArgs e) } // If incoming update failed becase a file is in use, - // try to show merge dialog (for MS Office, etc.). + // try to show merge dialog (for MS Office Word, PowerPoint etc.). if (e.Direction == SyncDirection.Incoming - && e.OperationType == OperationType.Update) + && e.OperationType == OperationType.UpdateContent) { switch (e.Result.Status) { case OperationStatus.FileInUse: - ITHit.FileSystem.Windows.AppHelper.MergeHelper.TryNotifyUpdateAvailable(item.Path, e.Result.ShadowFilePath); + ITHit.FileSystem.Windows.AppHelper.Utilities.TryNotifyUpdateAvailable(item.Path, e.Result.ShadowFilePath); break; } } - // Log info about the opertion. - switch (e.Result.Status) - { - case OperationStatus.Success: - switch (e.Direction) - { - case SyncDirection.Incoming: - logger.LogMessage($"{e.Direction} {e.OperationType}: {e.Result.Status}", item.Path, item.NewPath, e.OperationContext); - break; - case SyncDirection.Outgoing: - logger.LogDebug($"{e.Direction} {e.OperationType}: {e.Result.Status}", item.Path, item.NewPath, e.OperationContext); - break; - } - break; - case OperationStatus.Conflict: - logger.LogMessage($"{e.Direction} {e.OperationType}: {e.Result.Status}", item.Path, item.NewPath, e.OperationContext); - break; - case OperationStatus.Exception: - logger.LogError($"{e.Direction} {e.OperationType}", item.Path, item.NewPath, e.Result.Exception); - break; - case OperationStatus.Filtered: - logger.LogDebug($"{e.Direction} {e.OperationType}: {e.Result.Status} by {e.Result.FilteredBy.GetType().Name}", item.Path, item.NewPath, e.OperationContext); - break; - default: - logger.LogDebug($"{e.Direction} {e.OperationType}: {e.Result.Status}. {e.Result.Message}", item.Path, item.NewPath, e.OperationContext); - break; - } + LogItemChange(e, item); + } + } + + private void LogItemChange(ItemsChangeEventArgs e, ChangeEventItem item) + { + ILogger logger = Logger.CreateLogger(e.ComponentName); + string msg = $"{e.Direction} {e.OperationType}: {e.Result.Status}"; + switch (e.Result.Status) + { + case OperationStatus.Success: + switch (e.Direction) + { + case SyncDirection.Incoming: + logger.LogMessage(msg, item.Path, item.NewPath, e.OperationContext); + break; + case SyncDirection.Outgoing: + logger.LogDebug(msg, item.Path, item.NewPath, e.OperationContext); + break; + } + break; + case OperationStatus.Conflict: + logger.LogMessage(msg, item.Path, item.NewPath, e.OperationContext); + break; + case OperationStatus.Exception: + logger.LogError(msg, item.Path, item.NewPath, e.Result.Exception); + break; + case OperationStatus.Filtered: + msg = $"{msg} by {e.Result.FilteredBy.GetType().Name}"; + logger.LogDebug(msg, item.Path, item.NewPath, e.OperationContext); + break; + default: + msg = $"{msg}. {e.Result.Message}"; + logger.LogDebug(msg, item.Path, item.NewPath, e.OperationContext); + break; } } @@ -201,7 +206,7 @@ public override async Task StopAsync() /// Contains new and old Engine state. private void Engine_StateChanged(Engine engine, EngineWindows.StateChangeEventArgs e) { - engine.Logger.LogMessage($"{e.NewState}"); + engine.Logger.LogMessage($"{e.NewState}", engine.Path); } /// @@ -213,7 +218,7 @@ private void SyncService_StateChanged(object sender, SynchEventArgs e) { if (e.NewState == SynchronizationState.Enabled || e.NewState == SynchronizationState.Disabled) { - SyncService.Logger.LogMessage($"{e.NewState}"); + SyncService.Logger.LogMessage($"{e.NewState}", this.Path); } } diff --git a/Windows/VirtualDrive/VirtualDrive.ShellExtension/VirtualDrive.ShellExtension.csproj b/Windows/VirtualDrive/VirtualDrive.ShellExtension/VirtualDrive.ShellExtension.csproj index 3667824..0619f87 100644 --- a/Windows/VirtualDrive/VirtualDrive.ShellExtension/VirtualDrive.ShellExtension.csproj +++ b/Windows/VirtualDrive/VirtualDrive.ShellExtension/VirtualDrive.ShellExtension.csproj @@ -19,7 +19,7 @@ - + diff --git a/Windows/VirtualDrive/VirtualDrive/Program.cs b/Windows/VirtualDrive/VirtualDrive/Program.cs index def0457..7220e23 100644 --- a/Windows/VirtualDrive/VirtualDrive/Program.cs +++ b/Windows/VirtualDrive/VirtualDrive/Program.cs @@ -11,6 +11,7 @@ using ITHit.FileSystem.Samples.Common.Windows; using ITHit.FileSystem.Windows.Package; using ITHit.FileSystem; +using Microsoft.VisualBasic.Logging; namespace VirtualDrive { @@ -37,7 +38,7 @@ class Program /// /// Log4Net logger. /// - private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + internal static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); /// /// Processes OS file system calls, @@ -70,28 +71,26 @@ static async Task Main(string[] args) // Load Settings. Settings = new ConfigurationBuilder().AddJsonFile("appsettings.json", false, true).Build().ReadSettings(); - logFormatter = new LogFormatter(log, Settings.AppID, Settings.RemoteStorageRootPath); - commands = new Commands(log, Settings.RemoteStorageRootPath); - commands.ConfigureConsole(); + logFormatter = new LogFormatter(Log, Settings.AppID); + WindowManager.ConfigureConsole(); // Log environment description. logFormatter.PrintEnvironmentDescription(); - registrar = new SparsePackageRegistrar(SyncRootId, Settings.UserFileSystemRootPath, log, ShellExtension.ShellExtensions.Handlers); - - consoleProcessor = new ConsoleProcessor(registrar, logFormatter, commands); + registrar = new SparsePackageRegistrar(Log, ShellExtension.ShellExtensions.Handlers); + consoleProcessor = new ConsoleProcessor(registrar, logFormatter, Settings.AppID); switch (args.FirstOrDefault()) { #if DEBUG case "-InstallDevCert": /// Called by in elevated mode. - registrar.EnsureDevelopmentCertificateInstalled(); + SparsePackageRegistrar.EnsureDevelopmentCertificateInstalled(Log); return; case "-UninstallDevCert": /// Called by in elevated mode. - registrar.EnsureDevelopmentCertificateUninstalled(); + SparsePackageRegistrar.EnsureDevelopmentCertificateUninstalled(Log); return; #endif case "-Embedding": @@ -119,7 +118,7 @@ static async Task Main(string[] args) } catch (Exception ex) { - log.Error($"\n\n Press Shift-Esc to fully uninstall the app. Then start the app again.\n\n", ex); + Log.Error($"\n\n Press Shift-Esc to fully uninstall the app. Then start the app again.\n\n", ex); await consoleProcessor.ProcessUserInputAsync(); } } @@ -139,7 +138,13 @@ private static void ValidatePackagePrerequisites() private static async Task RunEngineAsync() { // Register sync root and create app folders. - await registrar.RegisterSyncRootAsync(Settings.ProductName, Path.Combine(Settings.IconsFolderPath, "Drive.ico"), Settings.ShellExtensionsComServerExePath); + await registrar.RegisterSyncRootAsync( + SyncRootId, + Settings.UserFileSystemRootPath, + Settings.RemoteStorageRootPath, + Settings.ProductName, + Path.Combine(Settings.IconsFolderPath, "Drive.ico"), + Settings.ShellExtensionsComServerExePath); using (Engine = new VirtualEngine( Settings.UserFileSystemLicense, @@ -148,8 +153,12 @@ private static async Task RunEngineAsync() Settings.IconsFolderPath, logFormatter)) { - commands.Engine = Engine; + commands = new Commands(Engine, Settings.RemoteStorageRootPath, Log); commands.RemoteStorageMonitor = Engine.RemoteStorageMonitor; + consoleProcessor.Commands.TryAdd(Guid.Empty, commands); + + // Here we disable incoming sync. To get changes using pooling call IncomingPooling.ProcessAsync() + Engine.SyncService.IncomingSyncMode = ITHit.FileSystem.Synchronization.IncomingSyncMode.Disabled; Engine.SyncService.SyncIntervalMs = Settings.SyncIntervalMs; Engine.AutoLock = Settings.AutoLock; @@ -166,13 +175,16 @@ private static async Task RunEngineAsync() consoleProcessor.PrintHelp(); // Print Engine config, settings, logging headers. - await logFormatter.PrintEngineStartInfoAsync(Engine); + await logFormatter.PrintEngineStartInfoAsync(Engine, Settings.RemoteStorageRootPath); // Start processing OS file system calls. await Engine.StartAsync(); + + // Sync all changes from remote storage one time for demo purposes. + await Engine.SyncService.IncomingPooling.ProcessAsync(); #if DEBUG // Opens Windows File Manager with user file system folder and remote storage folder. - commands.ShowTestEnvironment(); + commands.ShowTestEnvironment(Settings.ProductName); #endif // Keep this application running and reading user input. await consoleProcessor.ProcessUserInputAsync(); diff --git a/Windows/VirtualDrive/VirtualDrive/RemoteStorage/General.docx b/Windows/VirtualDrive/VirtualDrive/RemoteStorage/General.docx index b69569c..f458480 100644 Binary files a/Windows/VirtualDrive/VirtualDrive/RemoteStorage/General.docx and b/Windows/VirtualDrive/VirtualDrive/RemoteStorage/General.docx differ diff --git a/Windows/VirtualDrive/VirtualDrive/RemoteStorageMonitor.cs b/Windows/VirtualDrive/VirtualDrive/RemoteStorageMonitor.cs index 58e9651..85b3ef2 100644 --- a/Windows/VirtualDrive/VirtualDrive/RemoteStorageMonitor.cs +++ b/Windows/VirtualDrive/VirtualDrive/RemoteStorageMonitor.cs @@ -16,9 +16,8 @@ namespace VirtualDrive /// this class triggers an event with information about changes. /// /// - /// Here, for demo purposes we simulate server by monitoring source file path using FileWatchWrapper. - /// In your application, instead of using FileWatchWrapper, you will connect to your remote storage using web sockets - /// or any other technology. + /// Here, for demo purposes we simulate server by monitoring source file path using file system watcher. + /// In your application, you will connect to your remote storage using web sockets or similar technology. /// public class RemoteStorageMonitor : ISyncService, IDisposable { diff --git a/Windows/VirtualDrive/VirtualDrive/VirtualDrive.csproj b/Windows/VirtualDrive/VirtualDrive/VirtualDrive.csproj index b27d2ba..d568490 100644 --- a/Windows/VirtualDrive/VirtualDrive/VirtualDrive.csproj +++ b/Windows/VirtualDrive/VirtualDrive/VirtualDrive.csproj @@ -40,7 +40,7 @@ This is an advanced project with ETags support, Microsoft Office documents editi - + diff --git a/Windows/VirtualDrive/VirtualDrive/VirtualFile.cs b/Windows/VirtualDrive/VirtualDrive/VirtualFile.cs index ed2ee47..44a5f95 100644 --- a/Windows/VirtualDrive/VirtualDrive/VirtualFile.cs +++ b/Windows/VirtualDrive/VirtualDrive/VirtualFile.cs @@ -42,14 +42,14 @@ public async Task CloseCompletionAsync(IOperationContext operationContext, IResu } /// - public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext, CancellationToken cancellationToken) + public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext, CancellationToken cancellationToken) { // On Windows this method has a 60 sec timeout. // To process longer requests and reset the timout timer write to the output stream or call the resultContext.ReportProgress() or resultContext.ReturnData() methods. Logger.LogMessage($"{nameof(IFile)}.{nameof(ReadAsync)}({offset}, {length})", UserFileSystemPath, default, operationContext); - if (!Mapping.TryGetRemoteStoragePathById(RemoteStorageItemId, out string remoteStoragePath)) return; + if (!Mapping.TryGetRemoteStoragePathById(RemoteStorageItemId, out string remoteStoragePath)) return null; cancellationToken.Register(() => { Logger.LogMessage($"{nameof(IFile)}.{nameof(ReadAsync)}({offset}, {length}) cancelled", UserFileSystemPath, default, operationContext); }); @@ -72,9 +72,11 @@ public async Task ReadAsync(Stream output, long offset, long length, ITransferDa } } - // Save ETag received from your remote storage in persistent placeholder properties. - // string eTag = ... - // operationContext.Properties.SetETag(eTag); + // Return an updated item to the Engine. + // In the returned data set the following fields: + // - 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. + return null; } /// diff --git a/Windows/VirtualDrive/VirtualDrive/VirtualFolder.cs b/Windows/VirtualDrive/VirtualDrive/VirtualFolder.cs index 6bdb12b..ffb21f3 100644 --- a/Windows/VirtualDrive/VirtualDrive/VirtualFolder.cs +++ b/Windows/VirtualDrive/VirtualDrive/VirtualFolder.cs @@ -31,7 +31,7 @@ public VirtualFolder(string path, byte[] remoteStorageItemId, VirtualEngine engi } /// - public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) + public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewItemPath = Path.Combine(UserFileSystemPath, fileMetadata.Name); Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFileAsync)}()", userFileSystemNewItemPath); @@ -72,18 +72,23 @@ public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream con remoteStorageNewItem.LastAccessTimeUtc = fileMetadata.LastAccessTime.UtcDateTime; remoteStorageNewItem.Attributes = fileMetadata.Attributes; - // Save Etag received from your remote storage in - // persistent placeholder properties unlil the next update. - // string eTag = ... - // operationContext.Properties.SetETag(eTag); - // Return remote storage item ID. It will be passed later - // into IEngine.GetFileSystemItemAsync() method on every call. - return WindowsFileSystemItem.GetItemIdByPath(remoteStorageNewItem.FullName); + // Return newly created item to the Engine. + // In the returned data set the following fields: + // - Remote storage item ID. It will be passed to GetFileSystemItem() during next calls. + // - 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. + byte[] remoteStorageId = WindowsFileSystemItem.GetItemIdByPath(remoteStorageNewItem.FullName); + return new FileMetadataExt() + { + RemoteStorageItemId = remoteStorageId, + // ContentETag = + // MetadataETag = + }; } /// - public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOperationContext operationContext, IInSyncResultContext inSyncResultContext, CancellationToken cancellationToken = default) + public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOperationContext operationContext, IInSyncResultContext inSyncResultContext, CancellationToken cancellationToken = default) { string userFileSystemNewItemPath = Path.Combine(UserFileSystemPath, folderMetadata.Name); Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFolderAsync)}()", userFileSystemNewItemPath); @@ -106,13 +111,12 @@ public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOpe remoteStorageNewItem.LastAccessTimeUtc = folderMetadata.LastAccessTime.UtcDateTime; remoteStorageNewItem.Attributes = folderMetadata.Attributes; - // Save ETag received from your remote storage in persistent placeholder properties. - // string eTag = ... - // operationContext.Properties.SetETag(eTag); - - // Return remote storage item ID. It will be passed later - // into IEngine.GetFileSystemItemAsync() method on every call. - return WindowsFileSystemItem.GetItemIdByPath(remoteStorageNewItem.FullName); + byte[] remoteStorageId = WindowsFileSystemItem.GetItemIdByPath(remoteStorageNewItem.FullName); + return new FolderMetadataExt() + { + RemoteStorageItemId = remoteStorageId + // MetadataETag = + }; } /// @@ -127,12 +131,21 @@ public async Task GetChildrenAsync(string pattern, IOperationContext operationCo cancellationToken.Register(() => { Logger.LogMessage($"{nameof(IFolder)}.{nameof(GetChildrenAsync)}({pattern}) cancelled", UserFileSystemPath, default, operationContext); }); - var watch = System.Diagnostics.Stopwatch.StartNew(); - IEnumerable remoteStorageChildren = await EnumerateChildrenAsync(pattern, operationContext, cancellationToken); + List children = new List(); + + if (Mapping.TryGetRemoteStoragePathById(RemoteStorageItemId, out string remoteStoragePath)) + { + IEnumerable remoteStorageChildren = new DirectoryInfo(remoteStoragePath).EnumerateFileSystemInfos(pattern); + foreach (FileSystemInfo remoteStorageItem in remoteStorageChildren) + { + IFileSystemItemMetadata itemInfo = Mapping.GetUserFileSysteItemMetadata(remoteStorageItem); + children.Add(itemInfo); + } + } // To signal that the children enumeration is completed // always call ReturnChildren(), even if the folder is empty. - await resultContext.ReturnChildrenAsync(remoteStorageChildren.ToArray(), remoteStorageChildren.Count()); + await resultContext.ReturnChildrenAsync(children.ToArray(), children.Count(), true, cancellationToken); } public async Task> EnumerateChildrenAsync(string pattern, IOperationContext operationContext, CancellationToken cancellationToken) diff --git a/Windows/VirtualDrive/VirtualDrive/appsettings.json b/Windows/VirtualDrive/VirtualDrive/appsettings.json index b9041b0..cad2b68 100644 --- a/Windows/VirtualDrive/VirtualDrive/appsettings.json +++ b/Windows/VirtualDrive/VirtualDrive/appsettings.json @@ -23,7 +23,7 @@ // Your virtual file system will be mounted under this path. // Make sure to delete the all plceholders created by previous version of the software under the sync root. - "UserFileSystemRootPath": "%USERPROFILE%\\VirtualDriveV73\\", + "UserFileSystemRootPath": "%USERPROFILE%\\VirtualDriveV81\\", // Full synchronization interval in milliseconds. diff --git a/Windows/VirtualFileSystem/Program.cs b/Windows/VirtualFileSystem/Program.cs index dd5d8f6..3221965 100644 --- a/Windows/VirtualFileSystem/Program.cs +++ b/Windows/VirtualFileSystem/Program.cs @@ -11,6 +11,7 @@ using ITHit.FileSystem.Windows; using ITHit.FileSystem.Samples.Common; using ITHit.FileSystem.Samples.Common.Windows; +using System.Threading; namespace VirtualFileSystem @@ -59,21 +60,24 @@ public static async Task Main(string[] args) // Load Settings. Settings = new ConfigurationBuilder().AddJsonFile("appsettings.json", false, true).Build().ReadSettings(); - logFormatter = new LogFormatter(log, Settings.AppID, Settings.RemoteStorageRootPath); - commands = new Commands(log, Settings.RemoteStorageRootPath); - commands.ConfigureConsole(); + logFormatter = new LogFormatter(log, Settings.AppID); + WindowManager.ConfigureConsole(); // Log environment description. logFormatter.PrintEnvironmentDescription(); - registrar = new Registrar(SyncRootId, Settings.UserFileSystemRootPath, log); - - consoleProcessor = new ConsoleProcessor(registrar, logFormatter, commands); + registrar = new Registrar(log); + consoleProcessor = new ConsoleProcessor(registrar, logFormatter, Settings.AppID); try { // Register sync root and create app folders. - await registrar.RegisterSyncRootAsync(Settings.ProductName, Path.Combine(Settings.IconsFolderPath, "Drive.ico")); + await registrar.RegisterSyncRootAsync( + SyncRootId, + Settings.UserFileSystemRootPath, + Settings.RemoteStorageRootPath, + Settings.ProductName, + Path.Combine(Settings.IconsFolderPath, "Drive.ico")); using (Engine = new VirtualEngine( Settings.UserFileSystemLicense, @@ -81,8 +85,12 @@ public static async Task Main(string[] args) Settings.RemoteStorageRootPath, logFormatter)) { - commands.Engine = Engine; + commands = new Commands(Engine, Settings.RemoteStorageRootPath, log); commands.RemoteStorageMonitor = Engine.RemoteStorageMonitor; + consoleProcessor.Commands.TryAdd(Guid.Empty, commands); + + // Here we disable incoming sync. To get changes using pooling call IncomingPooling.ProcessAsync() + Engine.SyncService.IncomingSyncMode = ITHit.FileSystem.Synchronization.IncomingSyncMode.Disabled; // Set the remote storage item ID for the root item. It will be passed to the IEngine.GetFileSystemItemAsync() // method as a remoteStorageItemId parameter when a root folder is requested. @@ -93,13 +101,16 @@ public static async Task Main(string[] args) consoleProcessor.PrintHelp(); // Print Engine config, settings, logging headers. - await logFormatter.PrintEngineStartInfoAsync(Engine); + await logFormatter.PrintEngineStartInfoAsync(Engine, Settings.RemoteStorageRootPath); // Start processing OS file system calls. await Engine.StartAsync(); + + // Sync all changes from remote storage one time for demo purposes. + await Engine.SyncService.IncomingPooling.ProcessAsync(); #if DEBUG // Opens Windows File Manager with user file system folder and remote storage folder. - commands.ShowTestEnvironment(); + commands.ShowTestEnvironment(Settings.ProductName); #endif // Keep this application running and reading user input. await consoleProcessor.ProcessUserInputAsync(); diff --git a/Windows/VirtualFileSystem/RemoteStorage/Notes1.txt b/Windows/VirtualFileSystem/RemoteStorage/Notes.txt similarity index 100% rename from Windows/VirtualFileSystem/RemoteStorage/Notes1.txt rename to Windows/VirtualFileSystem/RemoteStorage/Notes.txt diff --git a/Windows/VirtualFileSystem/RemoteStorageMonitor.cs b/Windows/VirtualFileSystem/RemoteStorageMonitor.cs index b28e209..84f25fc 100644 --- a/Windows/VirtualFileSystem/RemoteStorageMonitor.cs +++ b/Windows/VirtualFileSystem/RemoteStorageMonitor.cs @@ -16,9 +16,8 @@ namespace VirtualFileSystem /// this class triggers an event with information about changes. /// /// - /// Here, for demo purposes we simulate server by monitoring source file path using FileWatchWrapper. - /// In your application, instead of using FileWatchWrapper, you will connect to your remote storage using web sockets - /// or any other technology. + /// Here, for demo purposes we simulate server by monitoring source file path using file system watcher. + /// In your application, you will connect to your remote storage using web sockets or similar technology. /// public class RemoteStorageMonitor : ISyncService, IDisposable { diff --git a/Windows/VirtualFileSystem/VirtualEngine.cs b/Windows/VirtualFileSystem/VirtualEngine.cs index f8c4af1..9679248 100644 --- a/Windows/VirtualFileSystem/VirtualEngine.cs +++ b/Windows/VirtualFileSystem/VirtualEngine.cs @@ -65,44 +65,50 @@ private void Engine_ItemsChanged(Engine sender, ItemsChangeEventArgs e) // If incoming update failed becase a file is in use, // try to show merge dialog (for MS Office, etc.). if (e.Direction == SyncDirection.Incoming - && e.OperationType == OperationType.Update) + && e.OperationType == OperationType.UpdateContent) { switch (e.Result.Status) { case OperationStatus.FileInUse: - ITHit.FileSystem.Windows.AppHelper.MergeHelper.TryNotifyUpdateAvailable(item.Path, e.Result.ShadowFilePath); + ITHit.FileSystem.Windows.AppHelper.Utilities.TryNotifyUpdateAvailable(item.Path, e.Result.ShadowFilePath); break; } } - // Log info about the opertion. - switch (e.Result.Status) - { - case OperationStatus.Success: - switch (e.Direction) - { - case SyncDirection.Incoming: - logger.LogMessage($"{e.Direction} {e.OperationType}: {e.Result.Status}", item.Path, item.NewPath, e.OperationContext); - break; - case SyncDirection.Outgoing: - logger.LogDebug($"{e.Direction} {e.OperationType}: {e.Result.Status}", item.Path, item.NewPath, e.OperationContext); - break; - } - break; - case OperationStatus.Conflict: - logger.LogMessage($"{e.Direction} {e.OperationType}: {e.Result.Status}", item.Path, item.NewPath, e.OperationContext); - break; - case OperationStatus.Exception: - logger.LogError($"{e.Direction} {e.OperationType}", item.Path, item.NewPath, e.Result.Exception); - break; - case OperationStatus.Filtered: - logger.LogDebug($"{e.Direction} {e.OperationType}: {e.Result.Status} by {e.Result.FilteredBy.GetType().Name}", item.Path, item.NewPath, e.OperationContext); - break; - default: - logger.LogDebug($"{e.Direction} {e.OperationType}: {e.Result.Status}. {e.Result.Message}", item.Path, item.NewPath, e.OperationContext); - break; - } + LogItemChange(e, item); + } + } + + private void LogItemChange(ItemsChangeEventArgs e, ChangeEventItem item) + { + var logger = Logger.CreateLogger(e.ComponentName); + + switch (e.Result.Status) + { + case OperationStatus.Success: + switch (e.Direction) + { + case SyncDirection.Incoming: + logger.LogMessage($"{e.Direction} {e.OperationType}: {e.Result.Status}", item.Path, item.NewPath, e.OperationContext); + break; + case SyncDirection.Outgoing: + logger.LogDebug($"{e.Direction} {e.OperationType}: {e.Result.Status}", item.Path, item.NewPath, e.OperationContext); + break; + } + break; + case OperationStatus.Conflict: + logger.LogMessage($"{e.Direction} {e.OperationType}: {e.Result.Status}", item.Path, item.NewPath, e.OperationContext); + break; + case OperationStatus.Exception: + logger.LogError($"{e.Direction} {e.OperationType}", item.Path, item.NewPath, e.Result.Exception); + break; + case OperationStatus.Filtered: + logger.LogDebug($"{e.Direction} {e.OperationType}: {e.Result.Status} by {e.Result.FilteredBy.GetType().Name}", item.Path, item.NewPath, e.OperationContext); + break; + default: + logger.LogDebug($"{e.Direction} {e.OperationType}: {e.Result.Status}. {e.Result.Message}", item.Path, item.NewPath, e.OperationContext); + break; } } diff --git a/Windows/VirtualFileSystem/VirtualFile.cs b/Windows/VirtualFileSystem/VirtualFile.cs index 2521bb5..1824d76 100644 --- a/Windows/VirtualFileSystem/VirtualFile.cs +++ b/Windows/VirtualFileSystem/VirtualFile.cs @@ -43,7 +43,7 @@ public async Task CloseCompletionAsync(IOperationContext operationContext, IResu /// - public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext, CancellationToken cancellationToken) + public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext, CancellationToken cancellationToken) { // On Windows this method has a 60 sec timeout. // To process longer requests and reset the timout timer write to the output stream or call the resultContext.ReportProgress() or resultContext.ReturnData() methods. @@ -64,6 +64,12 @@ public async Task ReadAsync(Stream output, long offset, long length, ITransferDa Logger.LogDebug($"{nameof(ReadAsync)}({offset}, {length}) canceled", UserFileSystemPath, default); } } + + // Return an updated item to the Engine. + // In the returned data set the following fields: + // - 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. + return null; } @@ -124,8 +130,10 @@ public async Task WriteAsync(IFileSystemBasicInfo fileBasicInfo, remoteStorageItem.Attributes = fileBasicInfo.Attributes.Value; } - // On macOS you must return updated file info. - // On Windows you can return null. + // Return an updated item to the Engine. + // In the returned data set the following fields: + // - 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. return null; } } diff --git a/Windows/VirtualFileSystem/VirtualFileSystem.csproj b/Windows/VirtualFileSystem/VirtualFileSystem.csproj index f9fdb1b..8fb19dd 100644 --- a/Windows/VirtualFileSystem/VirtualFileSystem.csproj +++ b/Windows/VirtualFileSystem/VirtualFileSystem.csproj @@ -35,7 +35,7 @@ This project does not support ETags, locking, Microsoft Office documents editing - + diff --git a/Windows/VirtualFileSystem/VirtualFolder.cs b/Windows/VirtualFileSystem/VirtualFolder.cs index f83c550..c0eaba3 100644 --- a/Windows/VirtualFileSystem/VirtualFolder.cs +++ b/Windows/VirtualFileSystem/VirtualFolder.cs @@ -29,7 +29,7 @@ public VirtualFolder(IMapping mapping, string path, ILogger logger) : base(mappi } /// - public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) + public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewItemPath = Path.Combine(UserFileSystemPath, fileMetadata.Name); Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFileAsync)}()", userFileSystemNewItemPath); @@ -62,14 +62,14 @@ public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream con remoteStorageNewItem.LastAccessTimeUtc = fileMetadata.LastAccessTime.UtcDateTime; remoteStorageNewItem.Attributes = fileMetadata.Attributes; - // Typically you must return a remote storage item ID. - // It will be passed later into IEngine.GetFileSystemItemAsync() method. + // Typically you must return IFileMetadata with a remote storage item ID, content eTag and metadata eTag. + // The ID will be passed later into IEngine.GetFileSystemItemAsync() method. // However, becuse we can not read the ID for the network path we return null. return null; } /// - public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOperationContext operationContext, IInSyncResultContext inSyncResultContext, CancellationToken cancellationToken = default) + public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOperationContext operationContext, IInSyncResultContext inSyncResultContext, CancellationToken cancellationToken = default) { string userFileSystemNewItemPath = Path.Combine(UserFileSystemPath, folderMetadata.Name); Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFolderAsync)}()", userFileSystemNewItemPath); @@ -84,8 +84,8 @@ public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOpe remoteStorageNewItem.LastAccessTimeUtc = folderMetadata.LastAccessTime.UtcDateTime; remoteStorageNewItem.Attributes = folderMetadata.Attributes; - // Typically you must return a remote storage item ID. - // It will be passed later into IEngine.GetFileSystemItemAsync() method. + // Typically you must return IFileMetadata with a remote storage item ID and metadata eTag. + // The ID will be passed later into IEngine.GetFileSystemItemAsync() method. // However, becuse we can not read the ID for the network path we return null. return null; } @@ -100,18 +100,18 @@ public async Task GetChildrenAsync(string pattern, IOperationContext operationCo Logger.LogMessage($"{nameof(IFolder)}.{nameof(GetChildrenAsync)}({pattern})", UserFileSystemPath, default, operationContext); - List userFileSystemChildren = new List(); + List children = new List(); IEnumerable remoteStorageChildren = new DirectoryInfo(RemoteStoragePath).EnumerateFileSystemInfos(pattern); foreach (FileSystemInfo remoteStorageItem in remoteStorageChildren) { IFileSystemItemMetadata itemInfo = Mapping.GetUserFileSysteItemMetadata(remoteStorageItem); - userFileSystemChildren.Add(itemInfo); + children.Add(itemInfo); } // To signal that the children enumeration is completed // always call ReturnChildren(), even if the folder is empty. - await resultContext.ReturnChildrenAsync(userFileSystemChildren.ToArray(), userFileSystemChildren.Count()); + await resultContext.ReturnChildrenAsync(children.ToArray(), children.Count(), true, cancellationToken); } /// diff --git a/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/WebDAVDrive.ShellExtension.csproj b/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/WebDAVDrive.ShellExtension.csproj index db930eb..a8c2633 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/WebDAVDrive.ShellExtension.csproj +++ b/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/WebDAVDrive.ShellExtension.csproj @@ -12,7 +12,7 @@ IT HIT LTD. - + diff --git a/Windows/WebDAVDrive/WebDAVDrive.UI/ConsoleManager.cs b/Windows/WebDAVDrive/WebDAVDrive.UI/ConsoleManager.cs deleted file mode 100644 index 773513e..0000000 --- a/Windows/WebDAVDrive/WebDAVDrive.UI/ConsoleManager.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; - -namespace WebDAVDrive.UI -{ - /// - /// Console helper methods. - /// - public static class ConsoleManager - { - [DllImport("user32.dll")] - private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); - - [DllImport("user32.dll")] - private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); - - - /// - /// Console visibility. - /// - public static bool ConsoleVisible { get; private set; } -#if !DEBUG - = false; -#else - = true; -#endif - - /// - /// Hides or hides console window. - /// - /// Console visibility. - public static void SetConsoleWindowVisibility(bool setVisible) - { - IntPtr hWnd = FindWindow(null, Console.Title); - if (hWnd != IntPtr.Zero) - { - ShowWindow(hWnd, setVisible ? 1 : 0); - ConsoleVisible = setVisible; - } - } - } -} diff --git a/Windows/WebDAVDrive/WebDAVDrive.UI/Localization/Resources.Designer.cs b/Windows/WebDAVDrive/WebDAVDrive.UI/Localization/Resources.Designer.cs index daf2e64..9f23466 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.UI/Localization/Resources.Designer.cs +++ b/Windows/WebDAVDrive/WebDAVDrive.UI/Localization/Resources.Designer.cs @@ -213,6 +213,15 @@ public static string StopSync { } } + /// + /// Looks up a localized string similar to Unmount. + /// + public static string Unmount { + get { + return ResourceManager.GetString("Unmount", resourceCulture); + } + } + /// /// Looks up a localized string similar to Please enter correct WebDAV server URL. /// diff --git a/Windows/WebDAVDrive/WebDAVDrive.UI/Localization/Resources.resx b/Windows/WebDAVDrive/WebDAVDrive.UI/Localization/Resources.resx index 077105b..8d4b7ef 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.UI/Localization/Resources.resx +++ b/Windows/WebDAVDrive/WebDAVDrive.UI/Localization/Resources.resx @@ -168,6 +168,9 @@ Stop synchronization + + Unmount + Please enter correct WebDAV server URL diff --git a/Windows/WebDAVDrive/WebDAVDrive.UI/TrayUI.cs b/Windows/WebDAVDrive/WebDAVDrive.UI/TrayUI.cs new file mode 100644 index 0000000..119a801 --- /dev/null +++ b/Windows/WebDAVDrive/WebDAVDrive.UI/TrayUI.cs @@ -0,0 +1,130 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +using ITHit.FileSystem; +using ITHit.FileSystem.Windows; +using ITHit.FileSystem.Samples.Common.Windows; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace WebDAVDrive.UI +{ + /// + /// UI thread. + /// + public class TrayUI : IDisposable//, ISyncService + { + private bool disposedValue; + + private static readonly BlockingCollection newTraysData = new BlockingCollection(new ConcurrentQueue()); + + private static ConcurrentDictionary trays = new ConcurrentDictionary(); + + public class TrayData + { + public string ProductName; + public string WebDavServerPath; + public string IconsFolderPath; + public Commands Commands; + public EngineWindows Engine; + public Guid InstanceId; + } + + /// + /// Creates a new tray application instance. + /// s + /// Product name. + /// Path to the icons folder. + /// Engine commands. + /// Engine instance. The tray app will start and stop this instance as well as will display its status. + /// + public static void CreateTray(string productName, string webDavServerPath, string iconsFolderPath, Commands commands, EngineWindows engine, Guid instanceId) + { + TrayData trayData = new TrayData + { + ProductName = productName, + WebDavServerPath = webDavServerPath, + IconsFolderPath = iconsFolderPath, + Commands = commands, + Engine = engine, + InstanceId = instanceId + }; + newTraysData.Add(trayData); + } + + public static void RemoveTray(Guid instanceId) + { + trays.TryRemove(instanceId, out var tray); + tray?.Dispose(); + } + + public async Task StartAsync(CancellationToken cancellationToken = default) + { + await Task.Run(async () => { await StartUIAsync(cancellationToken); }, cancellationToken); + } + + private static async Task StartUIAsync(CancellationToken cancellationToken) + { + // Run task to cteate trays. + Task.Run(async () => { await StartProcessingTraysQueueAsync(cancellationToken); }, cancellationToken); + + Application.Run(); + } + + private static async Task StartProcessingTraysQueueAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + TrayData trayData = newTraysData.Take(cancellationToken); // Blocks if the queue is empty. + + WindowsTrayInterface windowsTrayInterface = new WindowsTrayInterface(trayData.ProductName, trayData.WebDavServerPath, trayData.IconsFolderPath, trayData.Commands); + // Listen to engine notifications to change menu and icon states. + trayData.Engine.StateChanged += windowsTrayInterface.Engine_StateChanged; + trayData.Engine.SyncService.StateChanged += windowsTrayInterface.SyncService_StateChanged; + + trays.TryAdd(trayData.InstanceId, windowsTrayInterface); + } + + // Clear the queue. + while (newTraysData.TryTake(out _)) + { + } + } + + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + newTraysData?.Dispose(); + + foreach (var tray in trays.Values) + { + tray?.Dispose(); + } + } + + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~WindowsTrayInterface() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Windows/WebDAVDrive/WebDAVDrive.UI/WebDAVDrive.UI.csproj b/Windows/WebDAVDrive/WebDAVDrive.UI/WebDAVDrive.UI.csproj index 1b4443a..c458e01 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.UI/WebDAVDrive.UI.csproj +++ b/Windows/WebDAVDrive/WebDAVDrive.UI/WebDAVDrive.UI.csproj @@ -18,7 +18,7 @@ - + diff --git a/Windows/WebDAVDrive/WebDAVDrive.UI/WindowsTrayInterface.cs b/Windows/WebDAVDrive/WebDAVDrive.UI/WindowsTrayInterface.cs index 085a25b..db41b16 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.UI/WindowsTrayInterface.cs +++ b/Windows/WebDAVDrive/WebDAVDrive.UI/WindowsTrayInterface.cs @@ -15,6 +15,16 @@ namespace WebDAVDrive.UI /// public class WindowsTrayInterface : IDisposable { + /// + /// Stop this engine and exit this tray app menu. + /// + public readonly ToolStripItem MenuExit; + + /// + /// Unmount menu. + /// + public readonly ToolStripItem MenuUnmount; + /// /// Icon in the status bar notification area. /// @@ -62,31 +72,31 @@ public class WindowsTrayInterface : IDisposable /// s /// Product name. /// Path to the icons folder. + /// Engine commands. /// Engine instance. The tray app will start and stop this instance as well as will display its status. - /// ManualResetEvent, used to stop the application. /// - public static async Task StartTrayInterfaceAsync(string productName, string iconsFolderPath, Commands commands, EngineWindows engine) - { - await Task.Run(async () => - { - using (WindowsTrayInterface windowsTrayInterface = new WindowsTrayInterface(productName, iconsFolderPath, commands)) - { - // Listen to engine notifications to change menu and icon states. - engine.StateChanged += windowsTrayInterface.Engine_StateChanged; - engine.SyncService.StateChanged += windowsTrayInterface.SyncService_StateChanged; - - Application.Run(); - } - }); - } + //public static async Task StartTrayInterfaceAsync(string productName, string webDavServerPath, string iconsFolderPath, Commands commands, EngineWindows engine) + //{ + // await Task.Run(async () => + // { + // using (WindowsTrayInterface windowsTrayInterface = new WindowsTrayInterface(productName, webDavServerPath, iconsFolderPath, commands)) + // { + // // Listen to engine notifications to change menu and icon states. + // engine.StateChanged += windowsTrayInterface.Engine_StateChanged; + // engine.SyncService.StateChanged += windowsTrayInterface.SyncService_StateChanged; + + // Application.Run(); + // } + // }); + //} /// /// Creates a tray application instance. /// /// Tray application title. /// Path to the icons folder. - /// Engine instance. - public WindowsTrayInterface(string title, string iconsFolderPath, Commands commands) + /// Engine commands. + public WindowsTrayInterface(string title, string webDavServerPath, string iconsFolderPath, Commands commands) { this.title = title; this.iconsFolderPath = iconsFolderPath ?? throw new ArgumentNullException(nameof(iconsFolderPath)); @@ -99,7 +109,7 @@ public WindowsTrayInterface(string title, string iconsFolderPath, Commands comma notifyIcon.Text = title; // Show/Hide console on app start. Required to hide the console in the release mode. - ConsoleManager.SetConsoleWindowVisibility(ConsoleManager.ConsoleVisible); + WindowManager.SetConsoleWindowVisibility(WindowManager.ConsoleVisible); contextMenu = new ContextMenuStrip(); @@ -111,14 +121,14 @@ public WindowsTrayInterface(string title, string iconsFolderPath, Commands comma // Add Show/Hide console menu item. menuConsole = new ToolStripMenuItem(); - menuConsole.Text = ConsoleManager.ConsoleVisible ? Localization.Resources.HideLog : Localization.Resources.ShowLog; + menuConsole.Text = WindowManager.ConsoleVisible ? Localization.Resources.HideLog : Localization.Resources.ShowLog; menuConsole.Click += MenuConsole_Click; contextMenu.Items.Add(menuConsole); // Add Open Folder menu item. ToolStripMenuItem menuOpenFolder = new ToolStripMenuItem(); menuOpenFolder.Text = Localization.Resources.OpenFolder; - menuOpenFolder.Click += async (s, e) => { await commands.OpenFolderAsync(); }; + menuOpenFolder.Click += async (s, e) => { await commands.OpenRootFolderAsync(); }; contextMenu.Items.Add(menuOpenFolder); // Add open web browser menu item. @@ -131,10 +141,19 @@ public WindowsTrayInterface(string title, string iconsFolderPath, Commands comma contextMenu.Items.Add(new ToolStripSeparator()); // Add Exit menu item. - ToolStripItem menuExit = new ToolStripMenuItem(); - menuExit.Text = $"{Localization.Resources.Exit} {title}"; - menuExit.Click += MenuExit_Click; - contextMenu.Items.Add(menuExit); + //MenuExit = new ToolStripMenuItem(); + //MenuExit.Text = $"{Localization.Resources.Exit} {title}"; + //contextMenu.Items.Add(MenuExit); + + //// Add Unmount menu item. + //MenuUnmount = new ToolStripMenuItem(); + //MenuUnmount.Text = $"{Localization.Resources.Unmount} {title}"; + //contextMenu.Items.Add(MenuUnmount); + + // Drive path, to distinguish tray applications for different drives. + //ToolStripItem name = new ToolStripStatusLabel(); + //name.Text = title; + //contextMenu.Items.Add(webDavServerPath); notifyIcon.ContextMenuStrip = contextMenu; } @@ -152,17 +171,8 @@ private async void MenuStartStop_Click(object sender, EventArgs e) /// private void MenuConsole_Click(object sender, EventArgs e) { - ConsoleManager.SetConsoleWindowVisibility(!ConsoleManager.ConsoleVisible); - menuConsole.Text = ConsoleManager.ConsoleVisible ? Localization.Resources.HideLog : Localization.Resources.ShowLog; - } - - /// - /// App exit. - /// - private async void MenuExit_Click(object sender, EventArgs e) - { - await commands.AppExitAsync(); - Application.Exit(); + WindowManager.SetConsoleWindowVisibility(!WindowManager.ConsoleVisible); + menuConsole.Text = WindowManager.ConsoleVisible ? Localization.Resources.HideLog : Localization.Resources.ShowLog; } /// @@ -191,18 +201,21 @@ private void UpdateMenuStartStop(EngineState state) /// /// Engine /// Contains new and old Engine state. - private void Engine_StateChanged(Engine engine, EngineWindows.StateChangeEventArgs e) + public void Engine_StateChanged(Engine engine, EngineWindows.StateChangeEventArgs e) { - if (contextMenu.IsHandleCreated) + if (!disposedValue) { - contextMenu.Invoke(() => + if (contextMenu.IsHandleCreated) + { + contextMenu.Invoke(() => + { + UpdateMenuStartStop(e.NewState); + }); + } + else { UpdateMenuStartStop(e.NewState); - }); - } - else - { - UpdateMenuStartStop(e.NewState); + } } } @@ -211,19 +224,22 @@ private void Engine_StateChanged(Engine engine, EngineWindows.StateChangeEventAr /// /// Sync service. /// Contains new and old sync service state. - private void SyncService_StateChanged(object sender, SynchEventArgs e) + public void SyncService_StateChanged(object sender, SynchEventArgs e) { - switch (e.NewState) + if (!disposedValue) { - case SynchronizationState.Synchronizing: - notifyIcon.Text = $"{title}\n{Localization.Resources.StatusSync}"; - notifyIcon.Icon = new System.Drawing.Icon(Path.Combine(iconsFolderPath, "DriveSync.ico")); - break; - - case SynchronizationState.Idle: - notifyIcon.Text = $"{title}\n{Localization.Resources.Idle}"; - notifyIcon.Icon = new System.Drawing.Icon(Path.Combine(iconsFolderPath, "Drive.ico")); - break; + switch (e.NewState) + { + case SynchronizationState.Synchronizing: + notifyIcon.Text = $"{title}\n{Localization.Resources.StatusSync}"; + notifyIcon.Icon = new System.Drawing.Icon(Path.Combine(iconsFolderPath, "DriveSync.ico")); + break; + + case SynchronizationState.Idle: + notifyIcon.Text = $"{title}\n{Localization.Resources.Idle}"; + notifyIcon.Icon = new System.Drawing.Icon(Path.Combine(iconsFolderPath, "Drive.ico")); + break; + } } } @@ -233,13 +249,9 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - // TODO: dispose managed state (managed objects) - //notifyIcon?.Icon?.Dispose(); notifyIcon?.Dispose(); } - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null disposedValue = true; } } diff --git a/Windows/WebDAVDrive/WebDAVDrive/AppSettings.cs b/Windows/WebDAVDrive/WebDAVDrive/AppSettings.cs index 2aa76e4..0bd5ee9 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/AppSettings.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/AppSettings.cs @@ -7,6 +7,7 @@ using ITHit.FileSystem.Samples.Common; using WebDAVDrive.UI; + namespace WebDAVDrive { /// @@ -20,14 +21,9 @@ public class AppSettings : Settings public string WebDAVClientLicense { get; set; } /// - /// WebDAV server URL. - /// - public string WebDAVServerUrl { get; set; } - - /// - /// WebSocket server URL. + /// WebDAV server URLs. /// - public string WebSocketServerUrl { get; set; } + public string[] WebDAVServerURLs { get; set; } /// /// Automatic lock timout in milliseconds. @@ -108,12 +104,15 @@ public static AppSettings ReadSettings(this IConfiguration configuration) configuration.Bind(settings); - if (string.IsNullOrEmpty(settings.WebDAVServerUrl)) + + if (settings.WebDAVServerURLs == null || settings.WebDAVServerURLs.Length == 0) { - settings.WebDAVServerUrl = RegistryManager.GetURL(settings); + settings.WebDAVServerURLs = new string[1] { RegistryManager.GetURL(settings) }; + } + for (int i=0; i < settings.WebDAVServerURLs.Length; i++) + { + settings.WebDAVServerURLs[i] = $"{settings.WebDAVServerURLs[i].TrimEnd('/')}/"; } - - settings.WebDAVServerUrl = $"{settings.WebDAVServerUrl.TrimEnd('/')}/"; if (string.IsNullOrEmpty(settings.UserFileSystemRootPath)) { diff --git a/Windows/WebDAVDrive/WebDAVDrive/Images/Drive.png b/Windows/WebDAVDrive/WebDAVDrive/Images/Drive.png new file mode 100644 index 0000000..20f12f8 Binary files /dev/null and b/Windows/WebDAVDrive/WebDAVDrive/Images/Drive.png differ diff --git a/Windows/WebDAVDrive/WebDAVDrive/Mapping.cs b/Windows/WebDAVDrive/WebDAVDrive/Mapping.cs index c76b475..7616166 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/Mapping.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/Mapping.cs @@ -120,7 +120,7 @@ public static FileSystemItemMetadataExt GetUserFileSystemItemMetadata(Client.IHi Client.IFile remoteStorageFile = (Client.IFile)remoteStorageItem; userFileSystemItem = new FileMetadataExt(); ((FileMetadataExt)userFileSystemItem).Length = remoteStorageFile.ContentLength; - userFileSystemItem.ETag = remoteStorageFile.Etag; + ((FileMetadataExt)userFileSystemItem).ContentETag = remoteStorageFile.Etag; userFileSystemItem.Attributes = FileAttributes.Normal; } else @@ -129,6 +129,7 @@ public static FileSystemItemMetadataExt GetUserFileSystemItemMetadata(Client.IHi userFileSystemItem.Attributes = FileAttributes.Normal | FileAttributes.Directory; } + //userFileSystemItem.MetadataETag = GetPropertyValue(remoteStorageItem, "metadata-Etag"); userFileSystemItem.Name = remoteStorageItem.DisplayName; // In case the item is deleted, the min value is returned. @@ -174,5 +175,21 @@ private static byte[] GetPropertyValue(Client.IHierarchyItem remoteStorageItem, return resultValue; } + + /// + /// Gets properties to be returned with each item when listing + /// folder content or getting an item from server. + /// + /// List of properties. + public static Client.PropertyName[] GetDavProperties() + { + Client.PropertyName[] propNames = new Client.PropertyName[4]; + propNames[0] = new Client.PropertyName("resource-id", "DAV:"); // Remote storage item ID + propNames[1] = new Client.PropertyName("parent-resource-id", "DAV:"); // Parent remote storage item ID + propNames[2] = new Client.PropertyName("Etag", "DAV:"); // Content eTag. + propNames[3] = new Client.PropertyName("metadata-Etag", "DAV:"); // Metadata eTag. + + return propNames; + } } } diff --git a/Windows/WebDAVDrive/WebDAVDrive/Menu/MenuCommandCompare.cs b/Windows/WebDAVDrive/WebDAVDrive/Menu/MenuCommandCompare.cs index 5ecc4e6..2496f7d 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/Menu/MenuCommandCompare.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/Menu/MenuCommandCompare.cs @@ -14,17 +14,12 @@ namespace WebDAVDrive { /// /// Implements compare menu command displayed in a file manager. - /// The menu is shown only if the item is in conflict state and a single item is selected. - /// Otherwise the menu is hidden. /// public class MenuCommandCompare : IMenuCommandWindows { private readonly VirtualEngineBase engine; private readonly ILogger logger; - //private const string lockCommandIcon = @"Images\Locked.ico"; - //private const string unlockCommandIcon = @"Images\Unlocked.ico"; - /// /// Creates instance of this class. /// @@ -56,30 +51,46 @@ public async Task GetStateAsync(IEnumerable filesPath) if (filesPath.Count() == 1) { string userFileSystemPath = filesPath.First(); + FileAttributes atts = File.GetAttributes(userFileSystemPath); + + // Enable menu for online files. + if (!atts.HasFlag(FileAttributes.Offline) && !atts.HasFlag(FileAttributes.Directory)) + { + return MenuState.Enabled; + } + /* + // The menu is shown only if the item is in conflict state and a single item is selected. + // Otherwise the menu is hidden. if (engine.Placeholders.TryGetItem(userFileSystemPath, out PlaceholderItem placeholder)) { + if (placeholder.TryGetErrorStatus(out bool errorStatus) && errorStatus) { return MenuState.Enabled; } } + */ } return MenuState.Hidden; } /// - public async Task InvokeAsync(IEnumerable filesPath, IEnumerable remoteStorageItemIds = null) + public async Task InvokeAsync(IEnumerable filesPath, IEnumerable remoteStorageItemIds = null, CancellationToken cancellationToken = default) { string userFileSystemPath = filesPath.First(); + if (engine.Placeholders.TryGetItem(userFileSystemPath, out PlaceholderItem placeholder)) + { + OperationResult res = await (placeholder as PlaceholderFile).TryShadowDownloadAsync(default, logger, cancellationToken); - + ITHit.FileSystem.Windows.AppHelper.Utilities.TryCompare(placeholder.Path, res.ShadowFilePath); + } } /// public async Task GetToolTipAsync(IEnumerable filesPath) { - return "Compare client and server files"; + return "Compare local and remote files"; } } } diff --git a/Windows/WebDAVDrive/WebDAVDrive/Menu/MenuCommandUnmount.cs b/Windows/WebDAVDrive/WebDAVDrive/Menu/MenuCommandUnmount.cs new file mode 100644 index 0000000..69b2b2d --- /dev/null +++ b/Windows/WebDAVDrive/WebDAVDrive/Menu/MenuCommandUnmount.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using ITHit.FileSystem; +using ITHit.FileSystem.Windows; +using ITHit.FileSystem.Samples.Common.Windows; + + +namespace WebDAVDrive +{ + /// + /// Implements Unmount menu command displayed in a file manager. + /// The menu is shown on root items. + /// + public class MenuCommandUnmount : IMenuCommandWindows + { + private readonly VirtualEngine engine; + private readonly ILogger logger; + + /// + /// Creates instance of this class. + /// + /// Engine instance. + /// Logger. + public MenuCommandUnmount(VirtualEngine engine, ILogger logger) + { + this.engine = engine; + this.logger = logger.CreateLogger("Unmount Menu Command"); + } + + /// + public async Task GetTitleAsync(IEnumerable filesPath) + { + return "Unmount..."; + } + + /// + public async Task GetIconAsync(IEnumerable filesPath) + { + //string iconPath = Path.Combine(Path.GetDirectoryName(typeof(MenuCommandLock).Assembly.Location), iconName); + return null; + } + + /// + public async Task GetStateAsync(IEnumerable filesPath) + { + // Show menu only if a single item is selected and the item is in conflict state. + if (filesPath.Count() == 1) + { + string userFileSystemPath = filesPath.First().TrimEnd('\\'); + if(engine.Path.TrimEnd('\\').Equals(userFileSystemPath, StringComparison.InvariantCultureIgnoreCase)) + { + return MenuState.Enabled; + } + } + return MenuState.Hidden; + } + + /// + public async Task InvokeAsync(IEnumerable filesPath, IEnumerable remoteStorageItemIds = null, CancellationToken cancellationToken = default) + { + await Program.RemoveEngineAsync(engine, true); + } + + /// + public async Task GetToolTipAsync(IEnumerable filesPath) + { + return "Unmount this drive"; + } + } +} diff --git a/Windows/WebDAVDrive/WebDAVDrive/Program.cs b/Windows/WebDAVDrive/WebDAVDrive/Program.cs index eead910..8cabc47 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/Program.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/Program.cs @@ -6,19 +6,18 @@ using System.Threading; using System.Threading.Tasks; using System.Linq; +using System.Collections.Generic; +using System.Collections.Concurrent; using Microsoft.Extensions.Configuration; using log4net; using ITHit.FileSystem; -using ITHit.FileSystem.Samples.Common.Windows; +using ITHit.FileSystem.Windows; using ITHit.FileSystem.Windows.Package; - -using ITHit.WebDAV.Client; -using ITHit.WebDAV.Client.Exceptions; +using ITHit.FileSystem.Samples.Common.Windows; using WebDAVDrive.UI; -using WebDAVDrive.UI.ViewModels; namespace WebDAVDrive @@ -39,20 +38,22 @@ namespace WebDAVDrive class Program { /// - /// Application settings. + /// Engine instances. + /// Each item contains Engine instance ID and engine itself. + /// Instance ID is used to delete the Engine from this list if file system is unmounted. /// - internal static AppSettings Settings; + + public static ConcurrentDictionary Engines = new ConcurrentDictionary(); /// - /// Log4Net logger. + /// Application settings. /// - private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + internal static AppSettings Settings; /// - /// Processes OS file system calls, - /// synchronizes user file system to remote storage. + /// Log4Net logger. /// - internal static VirtualEngine Engine; + internal static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); /// /// Outputs logging information. @@ -64,59 +65,33 @@ class Program /// private static SparsePackageRegistrar registrar; - /// - /// Application commands. - /// - private static Commands commands; - /// /// Processes console input. /// private static ConsoleProcessor consoleProcessor; - /// - /// WebDAV client for accessing the WebDAV server. - /// - internal static WebDavSession DavClient; - - /// - /// Maximum number of login attempts. - /// - private static uint loginRetriesMax = 3; - - /// - /// Current login attempt. - /// - private static uint loginRetriesCurrent = 0; - - static async Task Main(string[] args) { // Load Settings. Settings = new ConfigurationBuilder().AddJsonFile("appsettings.json", false, true).Build().ReadSettings(); - logFormatter = new LogFormatter(log, Settings.AppID, Settings.WebDAVServerUrl); - commands = new Commands(log, Settings.WebDAVServerUrl); - commands.ConfigureConsole(); + logFormatter = new LogFormatter(Log, Settings.AppID); + WindowManager.ConfigureConsole(); // Log environment description. logFormatter.PrintEnvironmentDescription(); - registrar = new SparsePackageRegistrar(SyncRootId, Settings.UserFileSystemRootPath, log, ShellExtension.ShellExtensions.Handlers); - - consoleProcessor = new ConsoleProcessor(registrar, logFormatter, commands); - switch (args.FirstOrDefault()) { #if DEBUG case "-InstallDevCert": /// Called by in elevated mode. - registrar.EnsureDevelopmentCertificateInstalled(); + SparsePackageRegistrar.EnsureDevelopmentCertificateInstalled(Log); return; case "-UninstallDevCert": /// Called by in elevated mode. - registrar.EnsureDevelopmentCertificateUninstalled(); + SparsePackageRegistrar.EnsureDevelopmentCertificateUninstalled(Log); return; #endif case "-Embedding": @@ -125,6 +100,13 @@ static async Task Main(string[] args) return; } + registrar = new SparsePackageRegistrar(Log, ShellExtension.ShellExtensions.Handlers); + consoleProcessor = new ConsoleProcessor(registrar, logFormatter, Settings.AppID); + + // Print console commands. + consoleProcessor.PrintHelp(); + + TrayUI trayUI = null; try { ValidatePackagePrerequisites(); @@ -135,18 +117,109 @@ static async Task Main(string[] args) return; // Sparse package registered - restart the sample. } + // Start console input processing. + Task taskConsole = StartConsoleReadKeyAsync(); + + // Start tray processing. + trayUI = new TrayUI(); + Task taskTrayUI = trayUI.StartAsync(); + // Register this app to process COM shell extensions calls. using (ShellExtension.ShellExtensions.StartComServer(Settings.ShellExtensionsComServerRpcEnabled)) { - // Run the User File System Engine. - await RunEngineAsync(); + + // Read mounted file systems. + var syncRoots = await Registrar.GetMountedSyncRootsAsync(Settings.AppID, Log); + if (!syncRoots.Any()) + { + // This is first start. Mount file system roots from settings. + await MountNewAsync(Settings.WebDAVServerURLs); + } + else + { + // Roots were lready mountied during previous runs. This is app restart or reboot. + await RunExistingAsync(syncRoots); + } + + // Wait for console or all tray apps exit. + //System.Windows.Forms.Application.Run(); + + Task.WaitAny(taskConsole, taskTrayUI); } } catch (Exception ex) { - log.Error($"\n\n Press Shift-Esc to fully uninstall the app. Then start the app again.\n\n", ex); + Log.Error($"\n\n Press Shift-Esc to fully uninstall the app. Then start the app again.\n\n", ex); await consoleProcessor.ProcessUserInputAsync(); } + finally + { + trayUI?.Dispose(); + + foreach (var keyValue in Engines) + { + keyValue.Value?.Dispose(); + } + } + } + + private static async Task RunExistingAsync(IEnumerable syncRoots) + { + foreach (var syncRoot in syncRoots) + { + string webDAVServerUrl = syncRoot.GetRemoteStoragePath(); + // Run the User File System Engine. + await TryCreateEngineAsync(webDAVServerUrl, syncRoot.Path.Path); + } + } + + private static async Task MountNewAsync(string[] webDAVServerURLs) + { + // Mount new file system for each URL, run Engine and tray app. + foreach (string webDAVServerUrl in webDAVServerURLs) + { + // Register sync root and run User File System Engine. + await TryMountNewAsync(webDAVServerUrl); + } + } + + private static async Task TryMountNewAsync(string webDAVServerUrl) + { + string userFileSystemRootPath = null; + try + { + userFileSystemRootPath = GenerateRootPathForProtocolMounting(); + string displayName = GetDisplayName(webDAVServerUrl); + + // Register sync root and create app folders. + await registrar.RegisterSyncRootAsync( + GetSyncRootId(webDAVServerUrl), + userFileSystemRootPath, + webDAVServerUrl, + displayName, + Path.Combine(Settings.IconsFolderPath, "Drive.ico"), + Settings.ShellExtensionsComServerExePath); + } + catch (Exception ex) + { + Log.Error($"Failed to mount file system {webDAVServerUrl} {userFileSystemRootPath}", ex); + return false; + } + // Run the User File System Engine. + return await TryCreateEngineAsync(webDAVServerUrl, userFileSystemRootPath); + } + + private static string GetDisplayName(string webDAVServerUrl) + { + return webDAVServerUrl.Remove(0, "https://".Length); + } + + private static string GenerateRootPathForProtocolMounting() + { + string userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string randomName = Path.GetRandomFileName(); + string folderName = Path.GetFileNameWithoutExtension(randomName); + return Path.Combine(userProfilePath, "DAV", folderName); } /// @@ -161,85 +234,58 @@ private static void ValidatePackagePrerequisites() } } - private static async Task RunEngineAsync() + private static async Task TryCreateEngineAsync(string webDAVServerUrl, string userFileSystemRootPath) { - // Register sync root and create app folders. - await registrar.RegisterSyncRootAsync(Settings.ProductName, Path.Combine(Settings.IconsFolderPath, "Drive.ico"), Settings.ShellExtensionsComServerExePath); - - using (Engine = new VirtualEngine( - Settings.UserFileSystemLicense, - Settings.UserFileSystemRootPath, - Settings.WebDAVServerUrl, - Settings.WebSocketServerUrl, - Settings.IconsFolderPath, - Settings.AutoLockTimoutMs, - Settings.ManualLockTimoutMs, - Settings.SetLockReadOnly, - logFormatter)) + try { - commands.Engine = Engine; - commands.RemoteStorageMonitor = Engine.RemoteStorageMonitor; - - Engine.SyncService.SyncIntervalMs = Settings.SyncIntervalMs; - Engine.AutoLock = Settings.AutoLock; - Engine.MaxTransferConcurrentRequests = Settings.MaxTransferConcurrentRequests.Value; - Engine.MaxOperationsConcurrentRequests = Settings.MaxOperationsConcurrentRequests.Value; - Engine.ShellExtensionsComServerRpcEnabled = Settings.ShellExtensionsComServerRpcEnabled; // Enable RPC in case RPC shaell extension handlers, hosted in separate process. - - // Print console commands. - consoleProcessor.PrintHelp(); + Uri webDAVServer = new Uri(webDAVServerUrl); + string webSocketsProtocol = webDAVServer.Scheme == "https" ? "wss" : "ws"; + string webSocketServerUrl = $"{webSocketsProtocol}://{webDAVServer.Authority}/"; + + VirtualEngine engine = new VirtualEngine( + Settings.UserFileSystemLicense, + userFileSystemRootPath, + webDAVServerUrl, + webSocketServerUrl, + Settings.IconsFolderPath, + Settings.AutoLockTimoutMs, + Settings.ManualLockTimoutMs, + Settings.SetLockReadOnly, + logFormatter, + Settings.ProductName); + + Engines.TryAdd(engine.InstanceId, engine); + //engine.Tray.MenuExit.Click += async (object sender, EventArgs e) => { await RemoveEngineAsync(engine, false); }; + //engine.Tray.MenuUnmount.Click += async (object sender, EventArgs e) => { await RemoveEngineAsync(engine, true); }; + + consoleProcessor.Commands.TryAdd(engine.InstanceId, engine.Commands); + + engine.SyncService.SyncIntervalMs = Settings.SyncIntervalMs; + engine.SyncService.IncomingSyncMode = ITHit.FileSystem.Synchronization.IncomingSyncMode.SyncId; + engine.AutoLock = Settings.AutoLock; + engine.MaxTransferConcurrentRequests = Settings.MaxTransferConcurrentRequests.Value; + engine.MaxOperationsConcurrentRequests = Settings.MaxOperationsConcurrentRequests.Value; + engine.ShellExtensionsComServerRpcEnabled = Settings.ShellExtensionsComServerRpcEnabled; // Enable RPC in case RPC shaell extension handlers, hosted in separate process. // Print Engine config, settings, logging headers. - await logFormatter.PrintEngineStartInfoAsync(Engine); + await logFormatter.PrintEngineStartInfoAsync(engine, webDAVServerUrl); #if DEBUG - // Open remote storage. - Commands.Open(Settings.WebDAVServerUrl); + Commands.Open(webDAVServerUrl); // Open remote storage. #endif - using (DavClient = CreateWebDavSession(Engine.InstanceId)) - { - // Set the remote storage item ID for the root folder. It will be passed to the IEngine.GetFileSystemItemAsync() - // method as a remoteStorageItemId parameter when a root folder is requested. - byte[] remoteStorageItemId = await GetRootRemoteStorageItemId(); - Engine.SetRemoteStorageRootItemId(remoteStorageItemId); - - // Start processing OS file system calls. - await Engine.StartAsync(); + // Start processing OS file system calls. + await engine.StartAsync(); #if DEBUG - // Opens Windows File Manager with user file system folder and remote storage folder. - commands.ShowTestEnvironment(false); + // Start Windows File Manager with user file system folder. + engine.Commands.ShowTestEnvironment(GetDisplayName(webDAVServerUrl), false); #endif - // Keep this application running and reading user input - // untill the tray app exits or an exit key in the console is selected. - Task console = StartConsoleReadKeyAsync(); - Task tray = WindowsTrayInterface.StartTrayInterfaceAsync(Settings.ProductName, Settings.IconsFolderPath, commands, Engine); - Task.WaitAny(console, tray); - } + return true; } - } - - /// - /// Gets remote storage item ID for the foor folder. - /// - private static async Task GetRootRemoteStorageItemId() - { - // Specifying properties to get from the WebDAV server. - PropertyName[] propNames = new PropertyName[2]; - propNames[0] = new PropertyName("resource-id", "DAV:"); - propNames[1] = new PropertyName("parent-resource-id", "DAV:"); - - // Sending request to the server. - IHierarchyItem rootFolder = (await DavClient.GetItemAsync(new Uri(Settings.WebDAVServerUrl), propNames)).WebDavResponse; - - byte[] remoteStorageItemId = Mapping.GetUserFileSystemItemMetadata(rootFolder).RemoteStorageItemId; - - // This sample requires synchronization support, verifying that the ID was returned. - if (remoteStorageItemId == null) + catch (Exception ex) { - 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."); + Log.Error($"Failed to start Engine {webDAVServerUrl} {userFileSystemRootPath}", ex); + return false; } - - return remoteStorageItemId; } private static async Task StartConsoleReadKeyAsync() @@ -248,181 +294,56 @@ private static async Task StartConsoleReadKeyAsync() } /// - /// Creates and configures WebDAV client to access the remote storage. + /// Gets automatically generated Sync Root ID. /// - /// Engine instance ID to be sent with every request to the remote storage. - private static WebDavSession CreateWebDavSession(Guid engineInstanceId) + /// An identifier in the form: [Storage Provider ID]![Windows SID]![Account ID] + private static string GetSyncRootId(string remoteStoragePathRoot) { - System.Net.Http.HttpClientHandler handler = new System.Net.Http.HttpClientHandler() - { - AllowAutoRedirect = false, - - // To enable pre-authentication (to avoid double requests) uncomment the code below. - // This option improves performance but is less secure. - // PreAuthenticate = true, - }; - WebDavSession davClient = new WebDavSession(Program.Settings.WebDAVClientLicense); - davClient.WebDavError += DavClient_WebDavError; - davClient.WebDavMessage += DavClient_WebDAVMessage; - davClient.CustomHeaders.Add("InstanceId", engineInstanceId.ToString()); - return davClient; + return $"{Settings.AppID}!{System.Security.Principal.WindowsIdentity.GetCurrent().User}!{remoteStoragePathRoot}!User"; } /// - /// Fired on every request to the WebDAV server. + /// Stops the Engine and removes it from the list of running engines, exits tray app, + /// exit app if engine list is empty. Optionally unmount sync root. /// - /// Request to the WebDAV client. - /// WebDAV message details. - private static void DavClient_WebDAVMessage(ISession client, WebDavMessageEventArgs e) + /// Engine instance. + /// Pass true to unmount sync root. + public static async Task RemoveEngineAsync(VirtualEngine engine, bool unregisterSyncRoot) { - string msg = $"\n{e.Message}"; - - if (logFormatter.DebugLoggingEnabled) + Log.Info($"\n\nRemoving {engine.RemoteStorageRootPath}"); + if (!unregisterSyncRoot) { - log.Debug($"{msg}\n"); + Log.Info("\nAll downloaded file / folder placeholders remain in file system. Restart the application to continue managing files."); + Log.Info("\nYou can edit documents when the app is not running and than start the app to sync all changes to the remote storage.\n"); } - } - - /// - /// Event handler to process WebDAV errors. - /// If server returns 401 or 302 response here we show the login dialog. - /// - /// WebDAV session. - /// WebDAV error details. - private static void DavClient_WebDavError(ISession sender, WebDavErrorEventArgs e) - { - WebDavHttpException httpException = e.Exception as WebDavHttpException; - if (httpException != null) + else { - switch (httpException.Status.Code) - { - // 302 redirect to login page. - case 302: - log.Debug($"\n{httpException?.Status.Code} {httpException?.Status.Description} {e.Exception.Message} "); - - // Show login dialog. - - // Azure AD can not navigate directly to login page - failed corelation. - //string loginUrl = ((Redirect302Exception)e.Exception).Location; - //Uri url = new System.Uri(loginUrl, System.UriKind.Absolute); - - Uri failedUri = (e.Exception as WebDavHttpException).Uri; - - WebBrowserLogin(failedUri); - - // Replay the request, so the listing or update can complete succesefully. - // Unless this is LOCK - incorrect lock owner map be passed in this case. - //bool isLock = httpException.HttpMethod.NotEquals("LOCK", StringComparison.InvariantCultureIgnoreCase); - bool isLock = false; - e.Result = isLock ? WebDavErrorEventResult.Fail : WebDavErrorEventResult.Repeat; - - break; - - // Challenge-responce auth: Basic, Digest, NTLM or Kerberos - case 401: - log.Debug($"\n{httpException?.Status.Code} {httpException?.Status.Description} {e.Exception.Message} "); - - if (loginRetriesCurrent < loginRetriesMax) - { - failedUri = (e.Exception as WebDavHttpException).Uri; - e.Result = ChallengeLoginLogin(failedUri); - } - break; - default: - ILogger logger = Engine.Logger.CreateLogger("WebDAV Session"); - logger.LogMessage($"{httpException.Status.Code} {e.Exception.Message}", httpException.Uri.ToString()); - break; - } + Log.Info("\nAll downloaded file / folder are deleted."); } - } - private static void WebBrowserLogin(Uri failedUri) - { - WebDAVDrive.UI.WebBrowserLogin webBrowserLogin = null; - Thread thread = new Thread(() => - { - webBrowserLogin = new WebDAVDrive.UI.WebBrowserLogin(failedUri, log); - webBrowserLogin.Title = Settings.ProductName; - webBrowserLogin.ShowDialog(); - }); - thread.SetApartmentState(ApartmentState.STA); - thread.Start(); - thread.Join(); - - // Request currenly loged-in user name or ID from server here and set it below. - // In case of WebDAV current-user-principal can be used for this purpose. - // For demo purposes we just set "DemoUserX". - Engine.CurrentUserPrincipal = "DemoUserX"; - - // Set cookies collected from the web browser dialog. - DavClient.CookieContainer.Add(webBrowserLogin.Cookies); - Engine.Cookies = webBrowserLogin.Cookies; - } - - private static WebDavErrorEventResult ChallengeLoginLogin(Uri failedUri) - { - Windows.Security.Credentials.PasswordCredential passwordCredential = CredentialManager.GetCredentials(Settings.ProductName, log); - if (passwordCredential != null) + // Stop Engine. + if (engine?.State == EngineState.Running) { - passwordCredential.RetrievePassword(); - NetworkCredential networkCredential = new NetworkCredential(passwordCredential.UserName, passwordCredential.Password); - DavClient.Credentials = networkCredential; - Engine.Credentials = networkCredential; - Engine.CurrentUserPrincipal = networkCredential.UserName; - return WebDavErrorEventResult.Repeat; + await engine.StopAsync(); } - else + + Engines.TryRemove(engine.InstanceId, out _); + consoleProcessor.Commands.TryRemove(engine.InstanceId, out _); + + // Unmount sync root. + if (unregisterSyncRoot) { - 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 = Settings.ProductName; - 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(); - - loginRetriesCurrent++; - if (dialogResult) - { - if (keepLogedin) - { - CredentialManager.SaveCredentials(Settings.ProductName, login, password); - } - NetworkCredential newNetworkCredential = new NetworkCredential(login, password); - DavClient.Credentials = newNetworkCredential; - Engine.Credentials = newNetworkCredential; - Engine.CurrentUserPrincipal = newNetworkCredential.UserName; - return WebDavErrorEventResult.Repeat; - } + await Registrar.UnregisterSyncRootAsync(engine.Path, engine.DataPath, Program.Log); } + engine.Dispose(); - return WebDavErrorEventResult.Fail; - } + // Refresh Windows Explorer. + PlaceholderItem.UpdateUI(Path.GetDirectoryName(engine.Path)); - /// - /// Gets automatically generated Sync Root ID. - /// - /// An identifier in the form: [Storage Provider ID]![Windows SID]![Account ID] - private static string SyncRootId - { - get + // If no Engines are running exit the app. + if (!Engines.Any()) { - return $"{Settings.AppID}!{System.Security.Principal.WindowsIdentity.GetCurrent().User}!User"; + System.Windows.Forms.Application.Exit(); } } } diff --git a/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitorBase.cs b/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorBase.cs similarity index 83% rename from Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitorBase.cs rename to Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorBase.cs index aaff070..557b0ef 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitorBase.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorBase.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Net.WebSockets; +using System.Security.RightsManagement; using System.Text; using System.Text.Json; using System.Threading; @@ -15,8 +16,6 @@ namespace WebDAVDrive { /// /// Monitors changes in the remote storage, notifies the client and updates the user file system. - /// If any file or folder is created, updated, delated, moved, locked or unlocked in the remote storage, - /// calls . /// public abstract class RemoteStorageMonitorBase : ISyncService, IDisposable { @@ -45,6 +44,12 @@ public abstract class RemoteStorageMonitorBase : ISyncService, IDisposable /// public readonly ILogger Logger; + /// + /// Sync mode that corresponds with this remote storage monitor type; + /// + public virtual ITHit.FileSystem.Synchronization.IncomingSyncMode SyncMode { get; } + + /// /// Current synchronization state. /// @@ -59,6 +64,13 @@ public virtual SynchronizationState SyncState } } + /// + /// Maximum number of items allowed in the message queue. + /// If queue cntains more messages, extra messages will be ignored. + /// This property is required for Sync ID mode. + /// + private readonly int MaxQueueLength; + /// /// WebSocket client. /// @@ -67,7 +79,7 @@ public virtual SynchronizationState SyncState /// /// WebSocket server url. /// - private readonly string webSocketServerUrl; + protected readonly string WebSocketServerUrl; /// /// WebSocket cancellation token. @@ -88,17 +100,19 @@ public virtual SynchronizationState SyncState /// arrive from remote storage during execution only the last one will be processed. /// We do not want to execute multiple requests concurrently. /// - private BlockingCollection changeQueue = new BlockingCollection(); + private BlockingCollection changeQueue = new BlockingCollection(); /// /// Creates instance of this class. /// /// WebSocket server url. + /// Maximum number of items allowed in the message queue. /// Logger. - internal RemoteStorageMonitorBase(string webSocketServerUrl, ILogger logger) + internal RemoteStorageMonitorBase(string webSocketServerUrl, int maxQueueLength, ILogger logger) { - this.Logger = logger.CreateLogger("Remote Storage Monitor"); - this.webSocketServerUrl = webSocketServerUrl; + this.WebSocketServerUrl = webSocketServerUrl; + this.MaxQueueLength = maxQueueLength; + this.Logger = logger.CreateLogger($"RS Monitor {SyncMode}"); } /// @@ -106,7 +120,7 @@ internal RemoteStorageMonitorBase(string webSocketServerUrl, ILogger logger) /// in the user file system and should be updated. /// /// Information about change in the remote storage. - /// True if the item exists and should be updated. False otherwise. + /// True if the item does NOT exists in user file system and should NOT be updated. False - otherwise. public abstract bool Filter(WebSocketMessage webSocketMessage); /// @@ -132,9 +146,9 @@ private async Task RunWebSocketsAsync(CancellationToken cancellationToken) // Because of the on-demand loading, item or its parent may not exists or be offline. // We can ignore notifiction in this case and avoid many requests to the remote storage. - if (webSocketMessage != null && !Filter(webSocketMessage) && changeQueue.Count == 0) + if (webSocketMessage != null && !Filter(webSocketMessage) && changeQueue.Count <= (MaxQueueLength-1)) { - changeQueue.Add(webSocketMessage.ItemPath); + changeQueue.Add(webSocketMessage); } } } @@ -146,7 +160,7 @@ private async Task RunWebSocketsAsync(CancellationToken cancellationToken) /// The token to monitor for cancellation requests. public async Task StartAsync(CancellationToken cancellationToken = default) { - Logger.LogDebug("Starting", webSocketServerUrl); + Logger.LogDebug("Starting", WebSocketServerUrl); await Task.Factory.StartNew( async () => @@ -180,15 +194,16 @@ await Task.Factory.StartNew( { clientWebSocket.Options.SetRequestHeader("InstanceId", InstanceId.ToString()); } - await clientWebSocket.ConnectAsync(new Uri(webSocketServerUrl), cancellationToken); - Logger.LogMessage("Connected", webSocketServerUrl); + await clientWebSocket.ConnectAsync(new Uri(WebSocketServerUrl), cancellationToken); + Logger.LogMessage("Connected", WebSocketServerUrl); - // After esteblishing connection with a server we must get all changes from the remote storage. + // 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(); + Logger.LogDebug("Getting all changes from server", WebSocketServerUrl); + await ProcessAsync(null); - Logger.LogMessage("Started", webSocketServerUrl); + Logger.LogMessage("Started", WebSocketServerUrl); await RunWebSocketsAsync(cancellationTokenSource.Token); } @@ -197,7 +212,7 @@ await Task.Factory.StartNew( // 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); + Logger.LogError(e.Message, WebSocketServerUrl, null, e); } // Here we delay WebSocket connection to avoid overload on @@ -229,32 +244,24 @@ public async Task StopAsync() } catch (WebSocketException ex) { - Logger.LogError("Failed to close websocket.", webSocketServerUrl, null, ex); + Logger.LogError("Failed to close websocket.", WebSocketServerUrl, null, ex); }; - Logger.LogMessage("Stoped", webSocketServerUrl); + Logger.LogMessage("Stoped", WebSocketServerUrl); } /// - /// Triggers call to get - /// and process all changes from the remote storage. + /// Processes message recieved from the remote storage. /// + /// Information about changes or null in case of web sockets start/reconnection. /// + /// This method is called on each message being received as well as on web sockets connection and reconnection. + /// /// We do not pass WebSockets cancellation token to this method because stopping /// web sockets should not stop processing changes. /// To stop processing changes that are already received the Engine must be stopped. /// - private async Task ProcessAsync() - { - try - { - await ServerNotifications.ProcessChangesAsync(); - } - catch (Exception ex) - { - Logger.LogError("Failed to process changes", null, null, ex); - } - } + protected abstract Task ProcessAsync(WebSocketMessage message = null); /// /// Starts thread that processes changes queue. @@ -269,9 +276,8 @@ private Task CreateHandlerChangesTask(CancellationToken cancelationToken) { while (!cancelationToken.IsCancellationRequested) { - _ = changeQueue.Take(cancelationToken); - - await ProcessAsync(); + WebSocketMessage message = changeQueue.Take(cancelationToken); + await ProcessAsync(message); } } catch (OperationCanceledException) diff --git a/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorCRUDE.cs b/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorCRUDE.cs new file mode 100644 index 0000000..90fa319 --- /dev/null +++ b/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorCRUDE.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ITHit.FileSystem; +using ITHit.FileSystem.Samples.Common.Windows; +using ITHit.FileSystem.Synchronization; +using ITHit.FileSystem.Windows; +using ITHit.WebDAV.Client; + + +namespace WebDAVDrive +{ + /// + /// Monitors changes in the remote storage, notifies the client and updates the user file system. + /// + /// + /// This monitor receives messages about files being created, updated, deleted and moved. + /// It also performs a complete synchronization via poolin using a + /// call on application start and web sockets reconnection. + /// + /// If any item changed in the remote storage it calls interfce methods. + /// + internal class RemoteStorageMonitorCRUDE : RemoteStorageMonitorBase + { + /// + /// Sync mode that corresponds with this remote storage monitor type; + /// + public override IncomingSyncMode SyncMode { get { return IncomingSyncMode.Disabled; } } + + /// + /// instance. + /// + private readonly VirtualEngine engine; + + private readonly string webDAVServerUrl; + + internal RemoteStorageMonitorCRUDE(string webSocketServerUrl, string webDAVServerUrl, VirtualEngine engine) + : base(webSocketServerUrl, int.MinValue, engine.Logger) + { + this.webDAVServerUrl = webDAVServerUrl; + this.engine = engine; + } + + /// + public override bool Filter(WebSocketMessage webSocketMessage) + { + string remoteStoragePath = engine.Mapping.GetAbsoluteUri(webSocketMessage.ItemPath); + + // Just in case there is more than one WebSockets server/virtual folder that + // is sending notifications (like with webdavserver.net, webdavserver.com), + // here we filter notifications that come from a different server/virtual folder. + if (remoteStoragePath.StartsWith(webDAVServerUrl, StringComparison.InvariantCultureIgnoreCase)) + { + Logger.LogDebug($"EventType: {webSocketMessage.EventType}", webSocketMessage.ItemPath, webSocketMessage.TargetPath); + return false; + } + + return true; + } + + /// + /// Triggers call to get + /// and process all changes from the remote storage. + /// + /// Information about changes or null in case of Engine start or sockets reconnection. + /// + /// We do not pass WebSockets cancellation token to this method because stopping + /// web sockets should not stop processing changes. + /// To stop processing changes that are already received the Engine must be stopped. + /// + protected override async Task ProcessAsync(WebSocketMessage webSocketMessage = null) + { + if(webSocketMessage == null) + { + // This is the Engine start, sockets reconnection or authentication event. + // Performing full sync of loaded items using pooling. + await engine.SyncService.IncomingPooling.ProcessAsync(); + } + else + { + // The item is created, updated, moved or deleted. + // Updating a single item. + await ProcessWebSocketsMessageAsync(webSocketMessage); + } + } + + protected async Task ProcessWebSocketsMessageAsync(WebSocketMessage webSocketMessage) + { + try + { + string remoteStoragePath = engine.Mapping.GetAbsoluteUri(webSocketMessage.ItemPath); + + string userFileSystemPath = engine.Mapping.ReverseMapPath(remoteStoragePath); + var cancellationToken = engine.CancellationTokenSource.Token; + switch (webSocketMessage.EventType) + { + case "created": + await CreateAsync(remoteStoragePath); + break; + + case "updated": + case "locked": + case "unlocked": + var resUpdate = await engine.DavClient.GetItemAsync(new Uri(remoteStoragePath), Mapping.GetDavProperties(), null, cancellationToken); + IHierarchyItem itemUpdate = resUpdate.WebDavResponse; + if (itemUpdate != null) + { + IFileSystemItemMetadata metadataUpdate = Mapping.GetUserFileSystemItemMetadata(itemUpdate); + await engine.ServerNotifications(userFileSystemPath).UpdateAsync(metadataUpdate); + } + break; + + case "deleted": + await engine.ServerNotifications(userFileSystemPath).DeleteAsync(); + break; + + case "moved": + string remoteStorageNewPath = engine.Mapping.GetAbsoluteUri(webSocketMessage.TargetPath); + string userFileSystemNewPath = engine.Mapping.ReverseMapPath(remoteStorageNewPath); + var res = await engine.ServerNotifications(userFileSystemPath).MoveToAsync(userFileSystemNewPath); + switch (res.Status) + { + case OperationStatus.NotFound: + // Source item is not loaded. Creating the item in the target folder. + await CreateAsync(remoteStorageNewPath); + break; + + case OperationStatus.TargetNotFound: + // The target parent folder does not exists or is offline, delete the source item. + await engine.ServerNotifications(userFileSystemPath).DeleteAsync(); + break; + } + + break; + } + } + catch (Exception ex) + { + Logger.LogError($"Failed to process:{webSocketMessage.EventType}", webSocketMessage.ItemPath, webSocketMessage.TargetPath, ex); + } + } + + private async Task CreateAsync(string remoteStoragePath) + { + var resCreate = await engine.DavClient.GetItemAsync(new Uri(remoteStoragePath), Mapping.GetDavProperties(), null, engine.CancellationTokenSource.Token); + IHierarchyItem itemCreate = resCreate.WebDavResponse; + if (itemCreate != null) + { + string userFileSystemPath = engine.Mapping.ReverseMapPath(remoteStoragePath); + string userFileSystemParentPath = Path.GetDirectoryName(userFileSystemPath); + + IFileSystemItemMetadata metadataCreate = Mapping.GetUserFileSystemItemMetadata(itemCreate); + await engine.ServerNotifications(userFileSystemParentPath).CreateAsync(new[] { metadataCreate }); + } + } + } +} diff --git a/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor.cs b/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorSyncId.cs similarity index 62% rename from Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor.cs rename to Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorSyncId.cs index c2ff738..c3028d0 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/RemoteStorageMonitor/RemoteStorageMonitorSyncId.cs @@ -7,29 +7,42 @@ using ITHit.FileSystem; using ITHit.FileSystem.Samples.Common.Windows; +using ITHit.FileSystem.Synchronization; using ITHit.FileSystem.Windows; namespace WebDAVDrive { - internal class RemoteStorageMonitor : RemoteStorageMonitorBase + /// + /// Monitors changes in the remote storage, notifies the client and updates the user file system. + /// + /// + /// If any file or folder is created, updated, delated, moved, locked or unlocked in the remote storage, + /// calls method. + /// + internal class RemoteStorageMonitorSyncId : RemoteStorageMonitorBase { + /// + /// Sync mode that corresponds with this remote storage monitor type; + /// + public override IncomingSyncMode SyncMode { get { return IncomingSyncMode.SyncId; } } + + /// /// instance. /// private readonly VirtualEngine engine; - internal RemoteStorageMonitor(string webSocketServerUrl, VirtualEngine engine) : base(webSocketServerUrl, engine.Logger) + private readonly string webDAVServerUrl; + + internal RemoteStorageMonitorSyncId(string webSocketServerUrl, string webDAVServerUrl, VirtualEngine engine) + : base(webSocketServerUrl, 1, engine.Logger) { + this.webDAVServerUrl = webDAVServerUrl; this.engine = engine; } - /// - /// Verifies that the WebSockets message is for the item that exists - /// in the user file system and should be updated. - /// - /// Information about change in the remote storage. - /// True if the item exists and should be updated. False otherwise. + /// public override bool Filter(WebSocketMessage webSocketMessage) { string remoteStoragePath = engine.Mapping.GetAbsoluteUri(webSocketMessage.ItemPath); @@ -37,7 +50,7 @@ public override bool Filter(WebSocketMessage webSocketMessage) // Just in case there is more than one WebSockets server/virtual folder that // is sending notifications (like with webdavserver.net, webdavserver.com), // here we filter notifications that come from a different server/virtual folder. - if (remoteStoragePath.StartsWith(Program.Settings.WebDAVServerUrl, StringComparison.InvariantCultureIgnoreCase)) + if (remoteStoragePath.StartsWith(webDAVServerUrl, StringComparison.InvariantCultureIgnoreCase)) { Logger.LogDebug($"EventType: {webSocketMessage.EventType}", webSocketMessage.ItemPath, webSocketMessage.TargetPath); @@ -47,7 +60,6 @@ public override bool Filter(WebSocketMessage webSocketMessage) { case "created": // Verify that parent folder exists and is not offline. - return !((Directory.Exists(userFileSystemParentPath) && !new DirectoryInfo(userFileSystemParentPath).Attributes.HasFlag(FileAttributes.Offline)) || engine.Placeholders.IsPinned(userFileSystemParentPath)); case "deleted": @@ -80,5 +92,27 @@ public override bool Filter(WebSocketMessage webSocketMessage) return true; } + + /// + /// Triggers call to get + /// and process all changes from the remote storage. + /// + /// Information about changes or null in case of Engine start or sockets reconnection. + /// + /// We do not pass WebSockets cancellation token to this method because stopping + /// web sockets should not stop processing changes. + /// To stop processing changes that are already received the Engine must be stopped. + /// + protected override async Task ProcessAsync(WebSocketMessage message = null) + { + try + { + await ServerNotifications.ProcessChangesAsync(); + } + catch (Exception ex) + { + Logger.LogError("Failed to process changes", WebSocketServerUrl, null, ex); + } + } } } diff --git a/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ContextMenuVerbIntegratedCompare.cs b/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ContextMenuVerbIntegratedCompare.cs index c8073b5..850b3df 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ContextMenuVerbIntegratedCompare.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ContextMenuVerbIntegratedCompare.cs @@ -13,7 +13,7 @@ namespace WebDAVDrive.ShellExtension [Guid("A54BD1AD-4816-44B0-9247-8F43D8CA7AE7")] public class ContextMenuVerbIntegratedCompare : CloudFilesContextMenuVerbIntegratedBase { - public ContextMenuVerbIntegratedCompare() : base(Program.Engine) + public ContextMenuVerbIntegratedCompare() : base(Program.Engines) { } } diff --git a/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ContextMenuVerbIntegratedLock.cs b/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ContextMenuVerbIntegratedLock.cs index f1c1edb..f2c3227 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ContextMenuVerbIntegratedLock.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ContextMenuVerbIntegratedLock.cs @@ -13,7 +13,7 @@ namespace WebDAVDrive.ShellExtension [Guid("A22EBD03-343E-433C-98DF-372C6B3A1538")] public class ContextMenuVerbIntegratedLock : CloudFilesContextMenuVerbIntegratedBase { - public ContextMenuVerbIntegratedLock() : base(Program.Engine) + public ContextMenuVerbIntegratedLock() : base(Program.Engines) { } } diff --git a/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ContextMenuVerbIntegratedUnmount.cs b/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ContextMenuVerbIntegratedUnmount.cs new file mode 100644 index 0000000..4cbc82a --- /dev/null +++ b/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ContextMenuVerbIntegratedUnmount.cs @@ -0,0 +1,20 @@ +using System.Runtime.InteropServices; + +using ITHit.FileSystem.Windows.ShellExtension; + + +namespace WebDAVDrive.ShellExtension +{ + /// + /// Implements Windows Explorer Unmount context menu, displayed on a root node. + /// + [ComVisible(true)] + [ProgId("WebDAVDrive.ContextMenuVerbUnmount")] + [Guid("FF039488-137F-454D-A546-AA329A1D963F")] + public class ContextMenuVerbIntegratedUnmount : CloudFilesContextMenuVerbIntegratedBase + { + public ContextMenuVerbIntegratedUnmount() : base(Program.Engines) + { + } + } +} diff --git a/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/CustomStateProviderIntegrated.cs b/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/CustomStateProviderIntegrated.cs index 85f84f5..25832e0 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/CustomStateProviderIntegrated.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/CustomStateProviderIntegrated.cs @@ -14,7 +14,7 @@ namespace WebDAVDrive.ShellExtension [Guid("754F334F-095C-46CD-B033-B2C0523D2829")] public class CustomStateProviderIntegrated : CustomStateHandlerIntegratedBase { - public CustomStateProviderIntegrated() : base(Program.Engine) + public CustomStateProviderIntegrated() : base(Program.Engines) { } } diff --git a/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ShellExtensions.cs b/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ShellExtensions.cs index d20df8b..0d96f28 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ShellExtensions.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ShellExtensions.cs @@ -28,6 +28,7 @@ internal static class ShellExtensions ("ThumbnailProvider", typeof(ThumbnailProviderIntegrated).GUID, false), ("MenuVerbHandler_0", typeof(ContextMenuVerbIntegratedLock).GUID, false), ("MenuVerbHandler_1", typeof(ContextMenuVerbIntegratedCompare).GUID, false), + ("MenuVerbHandler_2", typeof(ContextMenuVerbIntegratedUnmount).GUID, false), ("CustomStateHandler", typeof(CustomStateProviderIntegrated).GUID, false), //("UriHandler", typeof(ShellExtension.UriSourceIntegrated).GUID, false) }; @@ -52,6 +53,7 @@ internal static LocalServer StartComServer(bool shellExtensionsComServerRpcEnabl server.RegisterClass(); server.RegisterClass(); server.RegisterClass(); + server.RegisterClass(); server.RegisterWinRTClass(); //server.RegisterWinRTClass(); diff --git a/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ThumbnailProviderIntegrated.cs b/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ThumbnailProviderIntegrated.cs index 5039bd4..e7d65ec 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ThumbnailProviderIntegrated.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/ShellExtension/ThumbnailProviderIntegrated.cs @@ -13,7 +13,7 @@ namespace WebDAVDrive.ShellExtension [Guid("A5B0C82F-50AA-445C-A404-66DEB510E84B")] public class ThumbnailProviderIntegrated : ThumbnailProviderHandlerIntegratedBase { - public ThumbnailProviderIntegrated() : base(Program.Engine) + public ThumbnailProviderIntegrated() : base(Program.Engines) { } } diff --git a/Windows/WebDAVDrive/WebDAVDrive/SparsePackage/appxmanifest.xml b/Windows/WebDAVDrive/WebDAVDrive/SparsePackage/appxmanifest.xml index 6d70e13..a4ff8ef 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/SparsePackage/appxmanifest.xml +++ b/Windows/WebDAVDrive/WebDAVDrive/SparsePackage/appxmanifest.xml @@ -57,6 +57,7 @@ + @@ -75,6 +76,8 @@ + + --> diff --git a/Windows/WebDAVDrive/WebDAVDrive/VirtualEngine.cs b/Windows/WebDAVDrive/WebDAVDrive/VirtualEngine.cs index 0f02072..4da781a 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/VirtualEngine.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/VirtualEngine.cs @@ -6,6 +6,14 @@ 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; namespace WebDAVDrive @@ -13,20 +21,25 @@ namespace WebDAVDrive /// public class VirtualEngine : VirtualEngineBase { + /// + /// WebDAV client for accessing the WebDAV server. + /// + public readonly WebDavSession DavClient; + /// /// Engine instance ID, unique for every Engine instance. /// /// /// Used to prevent circular calls between remote storage and user file system. - /// You can send this ID with every update to the remote storage. Your remote storage - /// will return this ID back to the client. If IDs match you do not update the item. + /// You will send this ID with every update to the remote storage, + /// so you remote storage do not send updates to the client that initiated the change. /// public readonly Guid InstanceId = Guid.NewGuid(); /// /// Monitors changes in the remote storage, notifies the client and updates the user file system. /// - public readonly RemoteStorageMonitorBase RemoteStorageMonitor; + public RemoteStorageMonitorBase RemoteStorageMonitor; /// /// Maps remote storage path to the user file system path and vice versa. @@ -45,6 +58,16 @@ public class VirtualEngine : VirtualEngineBase /// public CookieCollection Cookies { get; set; } = new CookieCollection(); + /// + /// Tray UI. + /// + //public readonly WindowsTrayInterface Tray; + + /// + /// Commands. + /// + public readonly Commands Commands; + /// /// Automatic lock timout in milliseconds. /// @@ -55,6 +78,52 @@ public class VirtualEngine : VirtualEngineBase /// private readonly double manualLockTimoutMs; + /// + /// Maximum number of login attempts. + /// + private readonly uint loginRetriesMax = 3; + + /// + /// Current login attempt. + /// + private uint loginRetriesCurrent = 0; + + /// + /// Logger for UI. + /// + private log4net.ILog log; + + /// + /// Displayed in UI. + /// + private readonly string productName; + + /// + /// Remote storage root path. + /// + internal readonly string RemoteStorageRootPath; + + /// + /// Web sockets server that sends notifications about changes on the server. + /// + private readonly string webSocketServerUrl; + + /// + /// Title to be displayed in UI. + /// + private string Title + { + get { return $"{productName} - {RemoteStorageRootPath}"; } + } + + /// + /// Storage key under which credentials are stored. + /// + private string CredentialsStorageKey + { + get { return $"{productName} - {RemoteStorageRootPath}"; } + } + /// /// Creates a vitual file system Engine. /// @@ -79,15 +148,33 @@ public VirtualEngine( double autoLockTimoutMs, double manualLockTimoutMs, bool setLockReadOnly, - LogFormatter logFormatter) + LogFormatter logFormatter, + string productname) : base(license, userFileSystemRootPath, remoteStorageRootPath, iconsFolderPath, setLockReadOnly, logFormatter) { + this.productName = productname; + this.RemoteStorageRootPath = remoteStorageRootPath; + this.webSocketServerUrl = webSocketServerUrl; + this.log = logFormatter.Log; + Mapping = new Mapping(Path, remoteStorageRootPath); - RemoteStorageMonitor = new RemoteStorageMonitor(webSocketServerUrl, this); + this.autoLockTimoutMs = autoLockTimoutMs; this.manualLockTimoutMs = manualLockTimoutMs; + + DavClient = CreateWebDavSession(InstanceId); + + Commands = new Commands(this, remoteStorageRootPath, logFormatter.Log); + + //// Create tray app. + TrayUI.CreateTray(productName, remoteStorageRootPath, iconsFolderPath, Commands, this, this.InstanceId); + //Tray = new WindowsTrayInterface(productName, remoteStorageRootPath, iconsFolderPath, Commands); + + //// Listen to engine notifications to change menu and icon states. + //this.StateChanged += Tray.Engine_StateChanged; + //this.SyncService.StateChanged += Tray.SyncService_StateChanged; } /// @@ -120,27 +207,271 @@ public override async Task GetMenuCommandAsync(Guid menuGuid, IOpe { return new MenuCommandCompare(this, this.Logger); } + if (menuGuid == typeof(ShellExtension.ContextMenuVerbIntegratedUnmount).GUID) + { + return new MenuCommandUnmount(this, this.Logger); + } - Logger.LogError($"Menu not found", menuGuid.ToString()); + Logger.LogError($"Menu not found", Path, menuGuid.ToString()); throw new NotImplementedException(); } /// public override async Task StartAsync(bool processModified = true, CancellationToken cancellationToken = default) { + // Set the remote storage item ID for the root folder. It will be passed to the IEngine.GetFileSystemItemAsync() + // method as a remoteStorageItemId parameter when a root folder is requested. + byte[] remoteStorageItemId = await GetRootRemoteStorageItemId(this.RemoteStorageRootPath, cancellationToken); + this.SetRemoteStorageRootItemId(remoteStorageItemId); + await base.StartAsync(processModified, cancellationToken); - RemoteStorageMonitor.Credentials = this.Credentials; - RemoteStorageMonitor.Cookies = this.Cookies; - RemoteStorageMonitor.InstanceId = this.InstanceId; - RemoteStorageMonitor.ServerNotifications = this.ServerNotifications(this.Path, RemoteStorageMonitor.Logger); - await RemoteStorageMonitor.StartAsync(); + // Start remote storage monitor. + if (SyncService.IncomingSyncMode != IncomingSyncMode.TimerPooling) + { + if (RemoteStorageMonitor == null) + { + Logger.LogMessage($"Prefered sync mode: {SyncService.IncomingSyncMode}", Path); + + // Create and start monitor, depending on server capabilities and prefered SyncMode. + RemoteStorageMonitor = await TryCreateRemoteStorageMonitorAsync(SyncService.IncomingSyncMode); + + // If Sync ID & Manual pooling modes are not available, set timer pooling mode. + SyncService.IncomingSyncMode = RemoteStorageMonitor?.SyncMode ?? IncomingSyncMode.TimerPooling; + Logger.LogMessage($"Actual sync mode: {SyncService.IncomingSyncMode}", Path); + + Commands.RemoteStorageMonitor = RemoteStorageMonitor; + } + else + { + await RemoteStorageMonitor?.StartAsync(); + } + } + } + + /// + /// Creates and starts remote storage monitor, depending on sync mode. + /// + /// Prefered sync mode. + /// Remote storage monitor or null if sync mode is not supported or sockets failed to connect. + private async Task TryCreateRemoteStorageMonitorAsync(IncomingSyncMode preferedSyncMode) + { + RemoteStorageMonitorBase monitor = null; + try + { + if (preferedSyncMode == IncomingSyncMode.SyncId && SyncService.IsSyncIdSupported) + { + monitor = new RemoteStorageMonitorSyncId(webSocketServerUrl, RemoteStorageRootPath, this); + } + else if (preferedSyncMode != IncomingSyncMode.TimerPooling) + { + monitor = new RemoteStorageMonitorCRUDE(webSocketServerUrl, RemoteStorageRootPath, this); + } + if (monitor != null) + { + monitor.Credentials = this.Credentials; + monitor.Cookies = this.Cookies; + monitor.InstanceId = this.InstanceId; + monitor.ServerNotifications = this.ServerNotifications(this.Path, monitor.Logger); + await monitor.StartAsync(); + } + } + catch (Exception ex) + { + monitor = null; + // Sync ID & manual pooling modes are not available. Use timer pooling. + Logger.LogMessage($"Failed to create remote storage monitor. {ex.Message}", Path); + } + + return monitor; } public override async Task StopAsync() { await base.StopAsync(); - await RemoteStorageMonitor.StopAsync(); + await RemoteStorageMonitor?.StopAsync(); + } + + /// + /// Gets remote storage item ID for the foor folder. + /// + private async Task GetRootRemoteStorageItemId(string webDAVServerUrl, CancellationToken cancellationToken) + { + // Sending request to the server. + var response = await DavClient.GetItemAsync(new Uri(webDAVServerUrl), Mapping.GetDavProperties(), null, cancellationToken); + IHierarchyItem rootFolder = response.WebDavResponse; + + byte[] remoteStorageItemId = Mapping.GetUserFileSystemItemMetadata(rootFolder).RemoteStorageItemId; + + // This sample requires synchronization support, verifying that the ID was returned. + if (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."); + } + + return remoteStorageItemId; + } + + /// + /// Creates and configures WebDAV client to access the remote storage. + /// + /// Engine instance ID to be sent with every request to the remote storage. + private WebDavSession CreateWebDavSession(Guid engineInstanceId) + { + System.Net.Http.HttpClientHandler handler = new System.Net.Http.HttpClientHandler() + { + AllowAutoRedirect = false, + + // To enable pre-authentication (to avoid double requests) uncomment the code below. + // This option improves performance but is less secure. + // PreAuthenticate = true, + }; + WebDavSession davClient = new WebDavSession(Program.Settings.WebDAVClientLicense); + davClient.WebDavError += DavClient_WebDavError; + davClient.WebDavMessage += DavClient_WebDAVMessage; + davClient.CustomHeaders.Add("InstanceId", engineInstanceId.ToString()); + return davClient; + } + + /// + /// Fired on every request to the WebDAV server. + /// + /// Request to the WebDAV client. + /// WebDAV message details. + private void DavClient_WebDAVMessage(ISession client, WebDavMessageEventArgs e) + { + Logger.LogDebug(e.Message); + } + + /// + /// Event handler to process WebDAV errors. + /// If server returns 401 or 302 response here we show the login dialog. + /// + /// WebDAV session. + /// WebDAV error details. + private void DavClient_WebDavError(ISession sender, WebDavErrorEventArgs e) + { + WebDavHttpException httpException = e.Exception as WebDavHttpException; + if (httpException != null) + { + Uri failedUri = httpException.Uri; + + switch (httpException.Status.Code) + { + // 302 redirect to login page. + case 302: + // Show login dialog. + + // Azure AD can not navigate directly to login page - failed corelation. + //string loginUrl = ((Redirect302Exception)e.Exception).Location; + //Uri url = new System.Uri(loginUrl, System.UriKind.Absolute); + + Logger.LogDebug($"{httpException?.Status.Code} {httpException?.Status.Description} {e.Exception.Message}", null, failedUri?.OriginalString); + + WebBrowserLogin(failedUri); + + // Replay the request, so the listing or update can complete succesefully. + // Unless this is LOCK - incorrect lock owner map be passed in this case. + //bool isLock = httpException.HttpMethod.NotEquals("LOCK", StringComparison.InvariantCultureIgnoreCase); + bool isLock = false; + e.Result = isLock ? WebDavErrorEventResult.Fail : WebDavErrorEventResult.Repeat; + + break; + + // Challenge-responce auth: Basic, Digest, NTLM or Kerberos + case 401: + + Logger.LogDebug($"{httpException?.Status.Code} {httpException?.Status.Description} {e.Exception.Message}", null, failedUri?.OriginalString); + + if (loginRetriesCurrent < loginRetriesMax) + { + e.Result = ChallengeLoginLogin(failedUri); + } + break; + default: + ILogger logger = this.Logger.CreateLogger("WebDAV Session"); + logger.LogMessage($"{httpException.Status.Code} {e.Exception.Message}", null, failedUri?.OriginalString); + break; + } + } + } + + private void WebBrowserLogin(Uri failedUri) + { + WebDAVDrive.UI.WebBrowserLogin webBrowserLogin = null; + Thread thread = new Thread(() => + { + webBrowserLogin = new WebDAVDrive.UI.WebBrowserLogin(failedUri, log); + webBrowserLogin.Title = Title; + webBrowserLogin.ShowDialog(); + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + + // Request currenly loged-in user name or ID from server here and set it below. + // In case of WebDAV current-user-principal can be used for this purpose. + // For demo purposes we just set "DemoUserX". + this.CurrentUserPrincipal = "DemoUserX"; + + // Set cookies collected from the web browser dialog. + DavClient.CookieContainer.Add(webBrowserLogin.Cookies); + this.Cookies = webBrowserLogin.Cookies; + } + + private WebDavErrorEventResult ChallengeLoginLogin(Uri failedUri) + { + Windows.Security.Credentials.PasswordCredential passwordCredential = CredentialManager.GetCredentials(CredentialsStorageKey, log); + if (passwordCredential != null) + { + passwordCredential.RetrievePassword(); + NetworkCredential networkCredential = new NetworkCredential(passwordCredential.UserName, passwordCredential.Password); + DavClient.Credentials = networkCredential; + this.Credentials = networkCredential; + this.CurrentUserPrincipal = networkCredential.UserName; + return WebDavErrorEventResult.Repeat; + } + 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(); + + loginRetriesCurrent++; + if (dialogResult) + { + if (keepLogedin) + { + CredentialManager.SaveCredentials(CredentialsStorageKey, login, password); + } + NetworkCredential newNetworkCredential = new NetworkCredential(login, password); + DavClient.Credentials = newNetworkCredential; + this.Credentials = newNetworkCredential; + this.CurrentUserPrincipal = newNetworkCredential.UserName; + return WebDavErrorEventResult.Repeat; + } + } + + return WebDavErrorEventResult.Fail; } private bool disposedValue; @@ -151,11 +482,12 @@ protected override void Dispose(bool disposing) { if (disposing) { - RemoteStorageMonitor.Dispose(); + TrayUI.RemoveTray(this.InstanceId); + //Tray?.Dispose(); + RemoteStorageMonitor?.Dispose(); + DavClient?.Dispose(); } - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null disposedValue = true; } base.Dispose(disposing); diff --git a/Windows/WebDAVDrive/WebDAVDrive/VirtualFile.cs b/Windows/WebDAVDrive/WebDAVDrive/VirtualFile.cs index e9a5124..408f255 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/VirtualFile.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/VirtualFile.cs @@ -46,7 +46,7 @@ public async Task CloseCompletionAsync(IOperationContext operationContext, IResu } /// - public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext, CancellationToken cancellationToken) + public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext, CancellationToken cancellationToken) { // On Windows this method has a 60 sec timeout. // To process longer requests and reset the timout timer write to the output stream or call the resultContext.ReportProgress() or resultContext.ReturnData() methods. @@ -59,11 +59,11 @@ public async Task ReadAsync(Stream output, long offset, long length, ITransferDa offset = -1; } - string eTag = null; + string contentETag = null; // Buffer size must be multiple of 4096 bytes for optimal performance. const int bufferSize = 0x500000; // 5Mb. - using (Client.IDownloadResponse response = await Program.DavClient.DownloadAsync(new Uri(RemoteStoragePath), offset, length, null, cancellationToken)) + using (Client.IDownloadResponse response = await Dav.DownloadAsync(new Uri(RemoteStoragePath), offset, length, null, cancellationToken)) { using (Stream stream = await response.GetResponseStreamAsync()) { @@ -77,11 +77,19 @@ public async Task ReadAsync(Stream output, long offset, long length, ITransferDa Logger.LogMessage($"{nameof(ReadAsync)}({offset}, {length}) canceled", UserFileSystemPath, default); } } - eTag = response.Headers.ETag.Tag; + // Return content eTag to the Engine. + contentETag = response.Headers.ETag.Tag; } - // Store ETag here. - operationContext.Properties.SetETag(eTag); + // Return an updated item to the Engine. + // In the returned data set the following fields: + // - 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. + return new FileMetadataExt() + { + ContentETag = contentETag + //MetadataETag = + }; } /// @@ -101,13 +109,15 @@ public async Task ValidateDataAsync(long offset, long length, IValidateDataOpera /// public async Task WriteAsync(IFileSystemBasicInfo fileBasicInfo, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { - Logger.LogMessage($"{nameof(IFile)}.{nameof(WriteAsync)}()", UserFileSystemPath, default, operationContext); + long contentLength = content != null ? content.Length : 0; + Logger.LogMessage($"{nameof(IFile)}.{nameof(WriteAsync)}({contentLength})", UserFileSystemPath, default, operationContext); + string newContentEtag = null; if (content != null) { // Send the ETag to the server as part of the update to ensure // the file in the remote storge is not modified since last read. - operationContext.Properties.TryGetETag(out string oldEtag); + string oldContentEtag = fileBasicInfo.ContentETag; Client.LockUriTokenPair[] lockTokens = null; // Read the lock-token and send it to the server as part of the update. @@ -120,20 +130,19 @@ public async Task WriteAsync(IFileSystemBasicInfo fileBasicInfo, try { // Update remote storage file content. - Client.IWebDavResponse response = await Program.DavClient.UploadAsync(new Uri(RemoteStoragePath), async (outputStream) => + Client.IWebDavResponse response = await Dav.UploadAsync(new Uri(RemoteStoragePath), async (outputStream) => { content.Position = 0; // Setting position to 0 is required in case of retry. await content.CopyToAsync(outputStream, cancellationToken); - }, null, content.Length, 0, -1, lockTokens, oldEtag, null, cancellationToken); + }, null, content.Length, 0, -1, lockTokens, oldContentEtag, null, cancellationToken); - // Save a new ETag returned by the server, if any. - string newEtag = response.WebDavResponse; + // Return new content eTag back to the Engine. + newContentEtag = response.WebDavResponse; - if (string.IsNullOrEmpty(newEtag)) + if (string.IsNullOrEmpty(newContentEtag)) { Logger.LogError("The server did not return ETag after update.", UserFileSystemPath, null, null, operationContext); } - operationContext.Properties.SetETag(newEtag); } catch (Client.Exceptions.LockedException) { @@ -152,12 +161,20 @@ public async Task WriteAsync(IFileSystemBasicInfo fileBasicInfo, // This item can not be uploaded automatically, conflict needs to be resolved first. Logger.LogMessage($"Conflict. The item is modified", UserFileSystemPath, default, operationContext); - Engine.Placeholders.GetItem(UserFileSystemPath).SetConflictStatus(true); + Engine.Placeholders.GetFile(UserFileSystemPath).SetConflictStatus(true); inSyncResultContext.SetInSync = false; } } - return null; + // Return an updated item to the Engine. + // In the returned data set the following fields: + // - 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. + return new FileMetadataExt() + { + ContentETag = newContentEtag + //MetadataETag = + }; } } } diff --git a/Windows/WebDAVDrive/WebDAVDrive/VirtualFileSystemItem.cs b/Windows/WebDAVDrive/WebDAVDrive/VirtualFileSystemItem.cs index db73c33..2f57832 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/VirtualFileSystemItem.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/VirtualFileSystemItem.cs @@ -44,6 +44,8 @@ public abstract class VirtualFileSystemItem : IFileSystemItemWindows, ILock /// protected readonly VirtualEngine Engine; + protected readonly WebDavSession Dav; + /// /// Automatic lock timout in milliseconds. /// @@ -71,6 +73,7 @@ public VirtualFileSystemItem(byte[] remoteStorageId, string userFileSystemPath, } Logger = logger ?? throw new ArgumentNullException(nameof(logger)); Engine = engine ?? throw new ArgumentNullException(nameof(engine)); + Dav = engine.DavClient; UserFileSystemPath = userFileSystemPath; RemoteStorageItemId = remoteStorageId; @@ -98,7 +101,7 @@ public async Task MoveToCompletionAsync(string targetUserFileSystemPath, byte[] string remoteStorageOldPath = RemoteStoragePath; string remoteStorageNewPath = Engine.Mapping.MapPath(userFileSystemNewPath); - await Program.DavClient.MoveToAsync(new Uri(remoteStorageOldPath), new Uri(remoteStorageNewPath), true, null, null, cancellationToken); + await Dav.MoveToAsync(new Uri(remoteStorageOldPath), new Uri(remoteStorageNewPath), true, null, null, cancellationToken); } /// @@ -127,7 +130,7 @@ public async Task DeleteCompletionAsync(IOperationContext operationContext, IInS try { - await Program.DavClient.DeleteAsync(new Uri(RemoteStoragePath), null, null, cancellationToken); + await Dav.DeleteAsync(new Uri(RemoteStoragePath), null, null, cancellationToken); } catch(NotFoundException ex) { @@ -157,7 +160,7 @@ public async Task GetThumbnailAsync(uint size, IOperationContext operati try { - using (IDownloadResponse response = await Program.DavClient.DownloadAsync(new Uri(filePathRemote))) + using (IDownloadResponse response = await Dav.DownloadAsync(new Uri(filePathRemote))) { using (Stream stream = await response.GetResponseStreamAsync()) { @@ -243,17 +246,29 @@ public async Task> GetPropertiesAsync(IO } - // Read ETag. - if (operationContext.Properties.TryGetETag(out string eTag)) + // Read content ETag. + if (operationContext.Properties.TryGetContentETag(out string contentETag)) + { + FileSystemItemPropertyData propertyContentETag = new FileSystemItemPropertyData() + { + Id = (int)CustomColumnIds.ContentETag, + Value = contentETag, + IconResource = Path.Combine(Engine.IconsFolderPath, "Empty.ico") + }; + props.Add(propertyContentETag); + } + + // Read metadata ETag. + if (operationContext.Properties.TryGetMetadataETag(out string metadataETag)) { - FileSystemItemPropertyData propertyETag = new FileSystemItemPropertyData() + FileSystemItemPropertyData propertyMetadataETag = new FileSystemItemPropertyData() { - Id = (int)CustomColumnIds.ETag, - Value = eTag, + Id = (int)CustomColumnIds.MetadataETag, + Value = metadataETag, IconResource = Path.Combine(Engine.IconsFolderPath, "Empty.ico") }; - props.Add(propertyETag); - } + props.Add(propertyMetadataETag); + } return props; } @@ -273,7 +288,7 @@ public async Task LockAsync(LockMode lockMode, IOperationContext operationContex double timOutMs = lockMode == LockMode.Auto ? autoLockTimoutMs : manualLockTimoutMs; TimeSpan timeOut = timOutMs == -1 ? TimeSpan.MaxValue : TimeSpan.FromMilliseconds(timOutMs); - LockInfo lockInfo = (await Program.DavClient.LockAsync(new Uri(RemoteStoragePath), LockScope.Exclusive, false, lockOwner, timeOut, null, cancellationToken)).WebDavResponse; + LockInfo lockInfo = (await Dav.LockAsync(new Uri(RemoteStoragePath), LockScope.Exclusive, false, lockOwner, timeOut, null, cancellationToken)).WebDavResponse; // Save lock-token and lock-mode. Start the timer to refresh the lock. await SaveLockAsync(lockInfo, lockMode, operationContext, cancellationToken); @@ -346,7 +361,7 @@ private async void LockRefreshAsync(string lockToken, TimeSpan timOut, LockMode { // Extend (refresh) the lock. //Program.DavClient.RefreshLockAsync(new Uri(RemoteStoragePath), lockToken, timout, cancellationToken); - IHierarchyItem item = (await Program.DavClient.GetItemAsync(new Uri(RemoteStoragePath), null, cancellationToken)).WebDavResponse; + IHierarchyItem item = (await Dav.GetItemAsync(new Uri(RemoteStoragePath), null, cancellationToken)).WebDavResponse; LockInfo lockInfo = (await item.RefreshLockAsync(lockToken, timOut, null, cancellationToken)).WebDavResponse; Logger.LogMessage($"Lock extended, new timout: {lockInfo.TimeOut:hh\\:mm\\:ss\\.ff}", UserFileSystemPath); @@ -393,7 +408,7 @@ public async Task UnlockAsync(IOperationContext operationContext, CancellationTo // Unlock the item in the remote storage. try { - await Program.DavClient.UnlockAsync(new Uri(RemoteStoragePath), lockTokens, null, cancellationToken); + await Dav.UnlockAsync(new Uri(RemoteStoragePath), lockTokens, null, cancellationToken); } catch (ITHit.WebDAV.Client.Exceptions.ConflictException) { diff --git a/Windows/WebDAVDrive/WebDAVDrive/VirtualFolder.cs b/Windows/WebDAVDrive/WebDAVDrive/VirtualFolder.cs index 8e73084..8ffb55c 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/VirtualFolder.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/VirtualFolder.cs @@ -35,19 +35,30 @@ public VirtualFolder(byte[] remoteStorageId, string userFileSystemPath, VirtualE } /// - public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) + public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewItemPath = Path.Combine(UserFileSystemPath, fileMetadata.Name); - Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFileAsync)}()", userFileSystemNewItemPath); + string contentParam = content != null ? content.Length.ToString() : "null"; + Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFileAsync)}({contentParam})", userFileSystemNewItemPath); + + // Comment out the code below if you require a 0-lenght file to + // be created in the remote storage as soon as possible. The Engine + // will call IFile.WriteAsync() when the app completes writing to the file. + if (content == null) + { + // If content is nul, we can not obtain a file handle. + // The application is still writing into the file. + inSyncResultContext.SetInSync = false; + return null; + } // Create a new file in the remote storage. Uri newFileUri = new Uri(new Uri(RemoteStoragePath), fileMetadata.Name); - long contentLength = content != null ? content.Length : 0; - // Send content to remote storage. // Get the ETag returned by the server, if any. - Client.IWebDavResponse response = (await Program.DavClient.UploadAsync(newFileUri, async (outputStream) => + long contentLength = content != null ? content.Length : 0; + Client.IWebDavResponse response = (await Dav.UploadAsync(newFileUri, async (outputStream) => { if (content != null) { @@ -66,32 +77,42 @@ public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream con //} - // Store ETag in persistent placeholder properties untill the next update. - operationContext.Properties.SetETag(response.WebDavResponse); - // Return newly created item remote storage item ID, - // it will be passed to GetFileSystemItem() during next calls. + // Return newly created item to the Engine. + // In the returned data set the following fields: + // - Remote storage item ID. It will be passed to GetFileSystemItem() during next calls. + // - 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(); - return Encoding.UTF8.GetBytes(remoteStorageId); + return new FileMetadataExt() + { + RemoteStorageItemId = Encoding.UTF8.GetBytes(remoteStorageId), + ContentETag = response.WebDavResponse + // MetadataETag = + }; } /// - public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOperationContext operationContext, IInSyncResultContext inSyncResultContext, CancellationToken cancellationToken = default) + public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOperationContext operationContext, IInSyncResultContext inSyncResultContext, CancellationToken cancellationToken = default) { string userFileSystemNewItemPath = Path.Combine(UserFileSystemPath, folderMetadata.Name); Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFolderAsync)}()", userFileSystemNewItemPath); Uri newFolderUri = new Uri(new Uri(RemoteStoragePath), folderMetadata.Name); - Client.IResponse response = await Program.DavClient.CreateFolderAsync(newFolderUri, null, null, cancellationToken); + Client.IResponse response = await Dav.CreateFolderAsync(newFolderUri, null, null, cancellationToken); - // Store ETag (if any) unlil the next update here. - // WebDAV server typically does not provide eTags for folders. - // Engine.Placeholders.GetItem(userFileSystemNewItemPath).SetETag(eTag); + // Return newly created item to the Engine. + // In the returned data set the following fields: + // - 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. - // Return newly created item remote storage item ID, - // it will be passed to GetFileSystemItem() during next calls. string remoteStorageId = response.Headers.GetValues("resource-id").FirstOrDefault(); - return Encoding.UTF8.GetBytes(remoteStorageId); + return new FolderMetadataExt() + { + RemoteStorageItemId = Encoding.UTF8.GetBytes(remoteStorageId) + // MetadataETag = + }; } /// @@ -104,37 +125,20 @@ public async Task GetChildrenAsync(string pattern, IOperationContext operationCo Logger.LogMessage($"{nameof(IFolder)}.{nameof(GetChildrenAsync)}({pattern})", UserFileSystemPath, default, operationContext); - IEnumerable remoteStorageChildren = await EnumerateChildrenAsync(pattern); - - List userFileSystemChildren = new List(); - foreach (FileSystemItemMetadataExt itemMetadata in remoteStorageChildren) - { - userFileSystemChildren.Add(itemMetadata); - } - - // To signal that the children enumeration is completed - // always call ReturnChildren(), even if the folder is empty. - await resultContext.ReturnChildrenAsync(userFileSystemChildren.ToArray(), userFileSystemChildren.Count()); - } - - public async Task> EnumerateChildrenAsync(string pattern, CancellationToken cancellationToken = default) - { - Client.PropertyName[] propNames = new Client.PropertyName[2]; - propNames[0] = new Client.PropertyName("resource-id", "DAV:"); - propNames[1] = new Client.PropertyName("parent-resource-id", "DAV:"); - // WebDAV Client lib will retry the request in case authentication is requested by the server. - IList remoteStorageChildren = (await Program.DavClient.GetChildrenAsync(new Uri(RemoteStoragePath), false, propNames, null, cancellationToken)).WebDavResponse; + var response = await Dav.GetChildrenAsync(new Uri(RemoteStoragePath), false, Mapping.GetDavProperties(), null, cancellationToken); - List userFileSystemChildren = new List(); + List children = new List(); - foreach (Client.IHierarchyItem remoteStorageItem in remoteStorageChildren) + foreach (Client.IHierarchyItem remoteStorageItem in response.WebDavResponse) { FileSystemItemMetadataExt itemInfo = Mapping.GetUserFileSystemItemMetadata(remoteStorageItem); - userFileSystemChildren.Add(itemInfo); + children.Add(itemInfo); } - return userFileSystemChildren; + // To signal that the children enumeration is completed + // always call ReturnChildren(), even if the folder is empty. + await resultContext.ReturnChildrenAsync(children.ToArray(), children.Count()); } /// @@ -142,6 +146,11 @@ public async Task WriteAsync(IFileSystemBasicInfo fileBasicInfo { // Typically we can not change any folder metadata on a WebDAV server, just logging the call. Logger.LogMessage($"{nameof(IFolder)}.{nameof(WriteAsync)}()", UserFileSystemPath, default, operationContext); + + // Return an updated item to the Engine. + // In the returned data set the following fields: + // - Medatdata eTag. The Engine will store it to determine if the item metadata should be updated. + return null; } @@ -150,21 +159,16 @@ public async Task GetChangesAsync(string syncToken, bool deep, long? l { Logger.LogMessage($"{nameof(IFolder)}.{nameof(GetChangesAsync)}({syncToken})", UserFileSystemPath); + // In this sample we use sync id algoritm for synchronization. Client.IChanges davChanges = null; Changes changes = new Changes(); changes.NewSyncToken = syncToken; - // In this sample we use sync id algoritm for synchronization. - Client.PropertyName[] propNames = new Client.PropertyName[3]; - propNames[0] = new Client.PropertyName("resource-id", "DAV:"); - propNames[1] = new Client.PropertyName("parent-resource-id", "DAV:"); - propNames[2] = new Client.PropertyName("Etag", "DAV:"); - do { - davChanges = (await Program.DavClient.GetChangesAsync( + davChanges = (await Dav.GetChangesAsync( new Uri(RemoteStoragePath), - propNames, + Mapping.GetDavProperties(), changes.NewSyncToken, deep, limit, @@ -179,21 +183,7 @@ public async Task GetChangesAsync(string syncToken, bool deep, long? l IFileSystemItemMetadata itemInfo = Mapping.GetUserFileSystemItemMetadata(remoteStorageItem); // Changed, created, moved and deleted item. Change changeType = remoteStorageItem.ChangeType == Client.Change.Changed ? Change.Changed : Change.Deleted; - ChangedItem changedItem = new ChangedItem(changeType, itemInfo); - - if (changeType == Change.Changed) - { - // If remote storage Etag does not match client Etag the file content - // is modified in the remote storage and must be downloaded (for hydrated items). - if (Engine.Placeholders.TryFindByRemoteStorageId(itemInfo.RemoteStorageItemId, out PlaceholderItem placeholderItem)) - { - if(await placeholderItem.IsModifiedAsync(itemInfo as FileSystemItemMetadataExt)) - { - changedItem.ChangeType = Change.MetadataAndContent; - } - } - } changes.Add(changedItem); } } diff --git a/Windows/WebDAVDrive/WebDAVDrive/WebDAVDrive.csproj b/Windows/WebDAVDrive/WebDAVDrive/WebDAVDrive.csproj index b22a145..e68920f 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/WebDAVDrive.csproj +++ b/Windows/WebDAVDrive/WebDAVDrive/WebDAVDrive.csproj @@ -39,8 +39,9 @@ - + + diff --git a/Windows/WebDAVDrive/WebDAVDrive/appsettings.json b/Windows/WebDAVDrive/WebDAVDrive/appsettings.json index f0bcb4f..f99fc29 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/appsettings.json +++ b/Windows/WebDAVDrive/WebDAVDrive/appsettings.json @@ -25,19 +25,21 @@ "WebDAVClientLicense": "", - // Your WebDAV server URL. + // Your WebDAV server URLs. This sample will mount each URL under separate sync root. // 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://webdavserver.net/", + // For testing and demo purposes you can IT Hit's 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. + + // 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://server1/", + "https://server2/" + ], //Your virtual file system will be mounted under this path. diff --git a/macOS/Common/Core/Common.Core.csproj b/macOS/Common/Core/Common.Core.csproj index 1b86664..e04afad 100644 --- a/macOS/Common/Core/Common.Core.csproj +++ b/macOS/Common/Core/Common.Core.csproj @@ -19,6 +19,6 @@ None - + diff --git a/macOS/VirtualFileSystem/FileProviderExtension/ActionMenuCommand.cs b/macOS/VirtualFileSystem/FileProviderExtension/ActionMenuCommand.cs index 02ad210..fa8495d 100644 --- a/macOS/VirtualFileSystem/FileProviderExtension/ActionMenuCommand.cs +++ b/macOS/VirtualFileSystem/FileProviderExtension/ActionMenuCommand.cs @@ -24,7 +24,7 @@ public ActionMenuCommand(VirtualEngine engine, ILogger logger) this.logger = logger.CreateLogger("Simple Action Command"); } - public async Task InvokeAsync(IEnumerable filesPath, IEnumerable remoteStorageItemIds) + public async Task InvokeAsync(IEnumerable filesPath, IEnumerable remoteStorageItemIds, CancellationToken cancellationToken = default) { logger.LogMessage($"Action Menu items: {string.Join(",", remoteStorageItemIds.Select(p => Encoding.UTF8.GetString(p)))}"); } diff --git a/macOS/VirtualFileSystem/FileProviderExtension/FileProviderExtension.csproj b/macOS/VirtualFileSystem/FileProviderExtension/FileProviderExtension.csproj index 0b567d3..09eb0f6 100644 --- a/macOS/VirtualFileSystem/FileProviderExtension/FileProviderExtension.csproj +++ b/macOS/VirtualFileSystem/FileProviderExtension/FileProviderExtension.csproj @@ -76,7 +76,7 @@ - + diff --git a/macOS/VirtualFileSystem/FileProviderExtension/VirtualFile.cs b/macOS/VirtualFileSystem/FileProviderExtension/VirtualFile.cs index 4756cd7..3046e6c 100644 --- a/macOS/VirtualFileSystem/FileProviderExtension/VirtualFile.cs +++ b/macOS/VirtualFileSystem/FileProviderExtension/VirtualFile.cs @@ -21,7 +21,7 @@ public VirtualFile(string remoteStoragePath, ILogger logger) : base(remoteStorag } /// - public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext, CancellationToken cancellationToken) + public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext, CancellationToken cancellationToken) { Logger.LogMessage($"{nameof(IFile)}.{nameof(ReadAsync)}({offset}, {length})", RemoteStoragePath); @@ -31,6 +31,8 @@ public async Task ReadAsync(Stream output, long offset, long length, ITransferDa const int bufferSize = 0x500000; // 5Mb. Buffer size must be multiple of 4096 bytes for optimal performance. await stream.CopyToAsync(output, bufferSize, length); } + + return null; } /// diff --git a/macOS/VirtualFileSystem/FileProviderExtension/VirtualFolder.cs b/macOS/VirtualFileSystem/FileProviderExtension/VirtualFolder.cs index 121eaf0..0e7e466 100644 --- a/macOS/VirtualFileSystem/FileProviderExtension/VirtualFolder.cs +++ b/macOS/VirtualFileSystem/FileProviderExtension/VirtualFolder.cs @@ -25,7 +25,7 @@ public VirtualFolder(string remoteStoragePath, ILogger logger) : base(remoteStor } /// - public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) + public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFileAsync)}()", Path.Combine(RemoteStoragePath, fileMetadata.Name)); @@ -48,11 +48,14 @@ public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream con remoteStorageItem.LastAccessTimeUtc = fileMetadata.LastAccessTime.UtcDateTime; remoteStorageItem.LastWriteTimeUtc = fileMetadata.LastWriteTime.UtcDateTime; - return Mapping.EncodePath(remoteStorageItem.FullName); + return new FileMetadataMac + { + RemoteStorageItemId = Mapping.EncodePath(remoteStorageItem.FullName) + }; } /// - public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) + public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFolderAsync)}()", Path.Combine(RemoteStoragePath, folderMetadata.Name)); @@ -66,7 +69,10 @@ public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOpe remoteStorageItem.LastAccessTimeUtc = folderMetadata.LastAccessTime.UtcDateTime; remoteStorageItem.LastWriteTimeUtc = folderMetadata.LastWriteTime.UtcDateTime; - return Mapping.EncodePath(remoteStorageItem.FullName); + return new FolderMetadataMac + { + RemoteStorageItemId = Mapping.EncodePath(remoteStorageItem.FullName) + }; } /// diff --git a/macOS/VirtualFileSystem/VirtualFileSystemMacApp/RemoteStorageMonitor.cs b/macOS/VirtualFileSystem/VirtualFileSystemMacApp/RemoteStorageMonitor.cs index bcb5e2e..fc0cc88 100644 --- a/macOS/VirtualFileSystem/VirtualFileSystemMacApp/RemoteStorageMonitor.cs +++ b/macOS/VirtualFileSystem/VirtualFileSystemMacApp/RemoteStorageMonitor.cs @@ -35,7 +35,7 @@ public class RemoteStorageMonitor: IDisposable /// /// Server notifications will be sent to this object. /// - public IServerCollectionNotifications ServerNotifications; + public IServerNotifications ServerNotifications; public RemoteStorageMonitor(string remoteStorageRootPath, ILogger logger) { diff --git a/macOS/WebDAVDrive/README.md b/macOS/WebDAVDrive/README.md index 1ace409..85f63a2 100644 --- a/macOS/WebDAVDrive/README.md +++ b/macOS/WebDAVDrive/README.md @@ -25,7 +25,7 @@

    Solution Structure

    The macOS sample solution consists of 3 projects: container application, an extension project, and a common code.

    The container application provides a Menu Bar icon to install/uninstall the file system extension. 

    -

    The extension project runs in the background and implements a virtual file system on macOS (File Provider). It processes requests from macOS applications sent via macOS file system API and lists folders content. The macOS extension can be installed only as part of a container application, you can not install the extension application by itself.

    +

    The extension project runs in the background and implements a virtual file system on macOS (File Provider). It processes requests from macOS applications sent via macOS file system API and lists folders content. The macOS extension can be installed only as part of a container application, you can not install the extension application separately.

    Setting License

    Note that to use the sample you need both the IT Hit WebDAV Client Library license and IT Hit User File System license.

    To run the example, you will need both IT Hit WebDAV Client Library for .NET license and IT Hit User File System Engine for .NET License. You can download a WebDAV Client for .NET trial license in the IT Hit WebDAV Client Library product download area and the User File System trial license in the IT Hit User File System product download area. Note that this sample is fully functional with a trial licenses and does not have any limitations. The trial licenses are valid for one month will stop working after this. You can check the expiration date inside the license file. Download the license files and specify license strings in the WebDAVClientLicense and UserFileSystemLicense fields respectively in WebDAVMacApp\Resources\appsettings.json file. Set the license content directly as a value (NOT as a path to the license file). Do not forget to escape quotes: \":

    @@ -43,7 +43,7 @@

    Note that this sample does NOT require Group ID, App Identifies and Provisioning Profiles configuration for development. It is required only required for production deployment.

    To run the sample:

      -
    1. Open the project in Visual Studio and run the project. The application is added the macOS Status Bar.
    2. +
    3. Open the project in Visual Studio and run the project. The application is added to the macOS Status Bar.
    4. Select 'Install Extension' command in the Status Bar. This will mount your WebDAV file system.

    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. 

    @@ -55,5 +55,5 @@
  • macOS File Provider Extension Projects Deployment
  • Next Article:

    -File Provider Extension Troubleshooting on macOS +WebDAV Drive Sample for iOS in .NET, C# diff --git a/macOS/WebDAVDrive/WebDAVCommon/WebDAVCommon.csproj b/macOS/WebDAVDrive/WebDAVCommon/WebDAVCommon.csproj index 41da35e..1983797 100644 --- a/macOS/WebDAVDrive/WebDAVCommon/WebDAVCommon.csproj +++ b/macOS/WebDAVDrive/WebDAVCommon/WebDAVCommon.csproj @@ -46,6 +46,6 @@
    - + diff --git a/macOS/WebDAVDrive/WebDAVCommon/WebDavSessionUtils.cs b/macOS/WebDAVDrive/WebDAVCommon/WebDavSessionUtils.cs new file mode 100644 index 0000000..a94429b --- /dev/null +++ b/macOS/WebDAVDrive/WebDAVCommon/WebDavSessionUtils.cs @@ -0,0 +1,28 @@ +using System; +using System.Net; +using ITHit.WebDAV.Client; + +namespace WebDAVCommon +{ + public class WebDavSessionUtils + { + /// + /// Initializes WebDAV session. + /// + public static async Task GetWebDavSessionAsync() + { + SecureStorage secureStorage = new SecureStorage(); + WebDavSession webDavSession = new WebDavSession(AppGroupSettings.Settings.Value.WebDAVClientLicense); + webDavSession.CustomHeaders.Add("InstanceId", Environment.MachineName); + + string loginType = await secureStorage.GetAsync("LoginType"); + if (!string.IsNullOrEmpty(loginType) && loginType.Equals("UserNamePassword")) + { + webDavSession.Credentials = new NetworkCredential(await secureStorage.GetAsync("UserName"), await secureStorage.GetAsync("Password")); + } + + return webDavSession; + } + } +} + diff --git a/macOS/WebDAVDrive/WebDAVFileProviderExtension/LockMenuCommand.cs b/macOS/WebDAVDrive/WebDAVFileProviderExtension/LockMenuCommand.cs index cfcacb2..22daebd 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderExtension/LockMenuCommand.cs +++ b/macOS/WebDAVDrive/WebDAVFileProviderExtension/LockMenuCommand.cs @@ -28,7 +28,7 @@ public LockMenuCommand(VirtualEngine engine, ILogger logger) this.logger = logger.CreateLogger(nameof(LockMenuCommand)); } - public async Task InvokeAsync(IEnumerable filesPath, IEnumerable remoteStorageItemIds) + public async Task InvokeAsync(IEnumerable filesPath, IEnumerable remoteStorageItemIds, CancellationToken cancellationToken = default) { logger.LogMessage($"{nameof(LockMenuCommand)}.{nameof(InvokeAsync)}()", string.Join(",", remoteStorageItemIds.Select(p => Encoding.UTF8.GetString(p)))); diff --git a/macOS/WebDAVDrive/WebDAVFileProviderExtension/Mapping.cs b/macOS/WebDAVDrive/WebDAVFileProviderExtension/Mapping.cs index 74b6b44..546be26 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderExtension/Mapping.cs +++ b/macOS/WebDAVDrive/WebDAVFileProviderExtension/Mapping.cs @@ -10,6 +10,8 @@ using Common.Core; using ITHit.FileSystem.Mac; using ITHit.WebDAV.Client; +using System.Security.Policy; +using FileProvider; namespace WebDAVFileProviderExtension { @@ -38,7 +40,7 @@ public static Uri GetUriById(byte[] remoteStorageItemId, string webDAVServerRoot /// User file system item info. public static IFileSystemItemMetadata GetUserFileSystemItemMetadata(Client.IHierarchyItem remoteStorageItem) { - IFileSystemItemMetadata userFileSystemItem; + IFileSystemItemMetadataMac userFileSystemItem; if (remoteStorageItem is Client.IFile) { @@ -48,6 +50,7 @@ public static IFileSystemItemMetadata GetUserFileSystemItemMetadata(Client.IHier userFileSystemItem.Attributes = FileAttributes.Normal; // Set etag. + ((FileMetadataMac)userFileSystemItem).ContentETag = remoteStorageFile.Etag; userFileSystemItem.Properties.AddOrUpdate("eTag", remoteStorageFile.Etag); } else @@ -57,11 +60,21 @@ public static IFileSystemItemMetadata GetUserFileSystemItemMetadata(Client.IHier } userFileSystemItem.Name = remoteStorageItem.DisplayName; - userFileSystemItem.RemoteStorageItemId = GetPropertyValue(remoteStorageItem, "resource-id", remoteStorageItem.Href.AbsoluteUri); - userFileSystemItem.RemoteStorageParentItemId = GetPropertyValue(remoteStorageItem, "parent-resource-id", null); + userFileSystemItem.RemoteStorageItemId = Encoding.UTF8.GetBytes(GetPropertyValue(remoteStorageItem, "resource-id", remoteStorageItem.Href.AbsoluteUri)); + userFileSystemItem.RemoteStorageParentItemId = Encoding.UTF8.GetBytes(GetPropertyValue(remoteStorageItem, "parent-resource-id", + remoteStorageItem.Href.AbsoluteUri.Remove(remoteStorageItem.Href.AbsoluteUri.Length - remoteStorageItem.Href.Segments.Last().Length))); + userFileSystemItem.MetadataETag = GetPropertyValue(remoteStorageItem, "metadata-Etag", null); + + // Set item capabilities. + userFileSystemItem.Capabilities = FileSystemItemCapabilityMac.Writing + | FileSystemItemCapabilityMac.Deleting + | FileSystemItemCapabilityMac.Reading + | FileSystemItemCapabilityMac.Renaming + | FileSystemItemCapabilityMac.Reparenting + | FileSystemItemCapabilityMac.ExcludingFromSync; if (DateTime.MinValue != remoteStorageItem.CreationDate) - { + { DateTimeOffset lastModifiedDate = remoteStorageItem.LastModified; userFileSystemItem.CreationTime = remoteStorageItem.CreationDate; userFileSystemItem.LastWriteTime = lastModifiedDate; @@ -87,7 +100,13 @@ public static IFileSystemItemMetadata GetUserFileSystemItemMetadata(Client.IHier // Add Unclock context menu for the item in macOS finder. userFileSystemItemMac.UserInfo.AddOrUpdate("locked", "1"); - } + + if(lockInfo.Owner != Environment.UserName) + { + // Set readOnly attributes when a file is locked by another user. + userFileSystemItem.Attributes |= FileAttributes.ReadOnly; + } + } return userFileSystemItem; } @@ -95,25 +114,18 @@ public static IFileSystemItemMetadata GetUserFileSystemItemMetadata(Client.IHier /// /// Returns property value, if property not exists returns default value. /// - private static byte[] GetPropertyValue(Client.IHierarchyItem remoteStorageItem, string propertyName, string defaultValue) + private static string GetPropertyValue(Client.IHierarchyItem remoteStorageItem, string propertyName, string defaultValue) { - byte[] resultValue = null; - try - { - Client.Property property = remoteStorageItem.Properties.Where(p => p.Name.Name == propertyName).FirstOrDefault(); - if (property != null) - { - resultValue = Encoding.UTF8.GetBytes(property.StringValue); - } - else if(defaultValue != null) - { - resultValue = Encoding.UTF8.GetBytes(defaultValue); - } + string resultValue = null; + Client.Property property = remoteStorageItem.Properties.Where(p => p.Name.Name == propertyName).FirstOrDefault(); + if (property != null) + { + resultValue = property.StringValue; } - catch (Exception ex) + else if (defaultValue != null) { - (new ConsoleLogger("Mapping")).LogError($"Error parsing {remoteStorageItem.Href.AbsoluteUri} property {propertyName}", ex: ex); + resultValue = defaultValue; } return resultValue; diff --git a/macOS/WebDAVDrive/WebDAVFileProviderExtension/UnLockMenuCommand.cs b/macOS/WebDAVDrive/WebDAVFileProviderExtension/UnLockMenuCommand.cs index 4e2d74a..9b69ff1 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderExtension/UnLockMenuCommand.cs +++ b/macOS/WebDAVDrive/WebDAVFileProviderExtension/UnLockMenuCommand.cs @@ -25,7 +25,7 @@ public UnLockMenuCommand(VirtualEngine engine, ILogger logger) this.logger = logger.CreateLogger(nameof(UnLockMenuCommand)); } - public async Task InvokeAsync(IEnumerable filesPath, IEnumerable remoteStorageItemIds) + public async Task InvokeAsync(IEnumerable filesPath, IEnumerable remoteStorageItemIds, CancellationToken cancellationToken = default) { logger.LogMessage($"{nameof(UnLockMenuCommand)}.{nameof(InvokeAsync)}()", string.Join(",", remoteStorageItemIds.Select(p => Encoding.UTF8.GetString(p)))); diff --git a/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualEngine.cs b/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualEngine.cs index 100f4a0..a70b695 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualEngine.cs +++ b/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualEngine.cs @@ -96,14 +96,7 @@ internal void InitWebDavSession() { WebDavSession.Dispose(); } - WebDavSession = new WebDavSession(AppGroupSettings.Settings.Value.WebDAVClientLicense); - WebDavSession.CustomHeaders.Add("InstanceId", Environment.MachineName); - - string loginType = SecureStorage.GetAsync("LoginType").Result; - if (!string.IsNullOrEmpty(loginType) && loginType.Equals("UserNamePassword")) - { - WebDavSession.Credentials = new NetworkCredential(SecureStorage.GetAsync("UserName").Result, SecureStorage.GetAsync("Password").Result); - } + WebDavSession = WebDavSessionUtils.GetWebDavSessionAsync().Result; } /// diff --git a/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFile.cs b/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFile.cs index f9e4cb4..d90f2c8 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFile.cs +++ b/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFile.cs @@ -21,12 +21,13 @@ public VirtualFile(byte[] remoteStorageId, VirtualEngine engine, ILogger logger) } /// - public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext, CancellationToken cancellationToken) + public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext, CancellationToken cancellationToken) { Logger.LogMessage($"{nameof(IFile)}.{nameof(ReadAsync)}({offset}, {length})", RemoteStorageUriById.AbsoluteUri, default, operationContext); // Buffer size must be multiple of 4096 bytes for optimal performance. const int bufferSize = 0x500000; // 5Mb. + string contentETag = null; using (Client.IDownloadResponse response = await Engine.WebDavSession.DownloadAsync(new Uri(RemoteStorageUriById.AbsoluteUri), offset, length, null, cancellationToken)) { using (Stream stream = await response.GetResponseStreamAsync()) @@ -46,7 +47,16 @@ public async Task ReadAsync(Stream output, long offset, long length, ITransferDa HandleWebExceptions(httpException, resultContext); } } + + // Return content eTag to the Engine. + contentETag = response.Headers.ETag.Tag; } + + return new FileMetadataMac() + { + ContentETag = contentETag + //MetadataETag = + }; } diff --git a/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFileSystemItem.cs b/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFileSystemItem.cs index 42b8732..0156532 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFileSystemItem.cs +++ b/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFileSystemItem.cs @@ -80,18 +80,19 @@ public async Task DeleteAsync(IOperationContext operationContext = null, IConfir Logger.LogMessage($"{nameof(IFileSystemItem)}.{nameof(GetMetadataAsync)}()", RemoteStorageUriById.AbsoluteUri); IHierarchyItem? item = null; - Client.PropertyName[] propNames = new Client.PropertyName[2]; + Client.PropertyName[] propNames = new Client.PropertyName[3]; propNames[0] = new Client.PropertyName("resource-id", "DAV:"); propNames[1] = new Client.PropertyName("parent-resource-id", "DAV:"); + propNames[2] = new Client.PropertyName("metadata-Etag", "DAV:"); try { // Return IFileMetadata for a file, IFolderMetadata for a folder. item = (await Engine.WebDavSession.GetItemAsync(RemoteStorageUriById, propNames)).WebDavResponse; } - catch (Client.Exceptions.NotFoundException e) + catch (Client.Exceptions.NotFoundException) { - Logger.LogError($"{nameof(IFileSystemItem)}.{nameof(GetMetadataAsync)}()", RemoteStorageUriById.AbsoluteUri, ex: e); + Logger.LogDebug($"{nameof(IFileSystemItem)}.{nameof(GetMetadataAsync)}() - item not found.", RemoteStorageUriById.AbsoluteUri); item = null; } diff --git a/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFolder.cs b/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFolder.cs index 93d3daf..63383f0 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFolder.cs +++ b/macOS/WebDAVDrive/WebDAVFileProviderExtension/VirtualFolder.cs @@ -11,6 +11,7 @@ using System.Text; using ITHit.WebDAV.Client.Exceptions; using WebDAVCommon; +using ITHit.FileSystem.Exceptions; namespace WebDAVFileProviderExtension { @@ -31,7 +32,7 @@ public VirtualFolder(byte[] remoteStorageId, VirtualEngine engine, ILogger logge } /// - public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream? content = null, IOperationContext operationContext = null, IInSyncResultContext? inSyncResultContext = null, CancellationToken cancellationToken = default) + public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream? content = null, IOperationContext operationContext = null, IInSyncResultContext? inSyncResultContext = null, CancellationToken cancellationToken = default) { Uri newFileUri = new Uri(await GetItemHrefAsync(), fileMetadata.Name); Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFileAsync)}()", newFileUri.AbsoluteUri); @@ -51,11 +52,16 @@ public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream? co // Return new item remove storage id to the Engine. // It will be past to GetFileSystemItemAsync during next call. - return Encoding.UTF8.GetBytes(response.Headers.GetValues("resource-id").FirstOrDefault() ?? string.Empty); + IEnumerable values; + return new FileMetadata + { + RemoteStorageItemId = Encoding.UTF8.GetBytes(response.Headers.TryGetValues("resource-id", out values) ? (values.FirstOrDefault() ?? newFileUri.AbsoluteUri) : newFileUri.AbsoluteUri), + ContentETag = response.WebDavResponse + }; } /// - public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) + public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { Uri newFolderUri = new Uri(await GetItemHrefAsync(), folderMetadata.Name); Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFolderAsync)}()", newFolderUri.AbsoluteUri); @@ -64,7 +70,11 @@ public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IOpe // Return new item remove storage id to the Engine. // It will be past to GetFileSystemItemAsync during next call. - return Encoding.UTF8.GetBytes(response.Headers.GetValues("resource-id").FirstOrDefault()); + IEnumerable values; + return new FolderMetadata() + { + RemoteStorageItemId = Encoding.UTF8.GetBytes(response.Headers.TryGetValues("resource-id", out values) ? (values.FirstOrDefault() ?? newFolderUri.AbsoluteUri) : newFolderUri.AbsoluteUri) + }; } /// @@ -74,9 +84,10 @@ public async Task GetChildrenAsync(string pattern, IOperationContext operationCo { Logger.LogMessage($"{nameof(IFolder)}.{nameof(GetChildrenAsync)}({pattern})", RemoteStorageUriById.AbsoluteUri); - Client.PropertyName[] propNames = new Client.PropertyName[2]; + Client.PropertyName[] propNames = new Client.PropertyName[3]; propNames[0] = new Client.PropertyName("resource-id", "DAV:"); propNames[1] = new Client.PropertyName("parent-resource-id", "DAV:"); + propNames[2] = new Client.PropertyName("metadata-Etag", "DAV:"); IList remoteStorageChildren = (await Engine.WebDavSession.GetChildrenAsync(RemoteStorageUriById, false, propNames, null, cancellationToken)).WebDavResponse; @@ -115,25 +126,35 @@ public async Task GetChangesAsync(string syncToken, bool deep, long? l Changes changes = new Changes(); changes.NewSyncToken = syncToken; - // In this sample we use sync id algoritm for synchronization. - Client.PropertyName[] propNames = new Client.PropertyName[2]; - propNames[0] = new Client.PropertyName("resource-id", "DAV:"); - propNames[1] = new Client.PropertyName("parent-resource-id", "DAV:"); - - do + try { - davChanges = (await Engine.WebDavSession.GetChangesAsync(RemoteStorageUriById, propNames, changes.NewSyncToken, deep, limit, cancellationToken: cancellationToken)).WebDavResponse; - changes.NewSyncToken = davChanges.NewSyncToken; + // In this sample we use sync id algoritm for synchronization. + Client.PropertyName[] propNames = new Client.PropertyName[3]; + propNames[0] = new Client.PropertyName("resource-id", "DAV:"); + propNames[1] = new Client.PropertyName("parent-resource-id", "DAV:"); + propNames[2] = new Client.PropertyName("metadata-Etag", "DAV:"); - foreach (Client.IChangedItem remoteStorageItem in davChanges) + do { - ChangedItem itemInfo = new ChangedItem(remoteStorageItem.ChangeType == Client.Change.Changed ? Change.Changed : Change.Deleted, - Mapping.GetUserFileSystemItemMetadata(remoteStorageItem)); + davChanges = (await Engine.WebDavSession.GetChangesAsync(RemoteStorageUriById, propNames, changes.NewSyncToken, deep, limit, cancellationToken: cancellationToken)).WebDavResponse; + changes.NewSyncToken = davChanges.NewSyncToken; + + foreach (Client.IChangedItem remoteStorageItem in davChanges) + { + ChangedItem itemInfo = new ChangedItem(remoteStorageItem.ChangeType == Client.Change.Changed ? Change.Changed : Change.Deleted, + Mapping.GetUserFileSystemItemMetadata(remoteStorageItem)); - changes.Add(itemInfo); + changes.Add(itemInfo); + } } + while (davChanges.MoreResults); + } + catch (Client.Exceptions.MethodNotAllowedException) + { + Logger.LogDebug($"{nameof(IFolder)}.{nameof(GetChangesAsync)}({syncToken}) - not supported."); + + throw new SyncIdNotSupportedException(); } - while (davChanges.MoreResults); // Returns changes to the Engine. Engine applies changes to the user file system and stores the new sync-token. return changes; diff --git a/macOS/WebDAVDrive/WebDAVFileProviderExtension/WebDAVFileProviderExtension.csproj b/macOS/WebDAVDrive/WebDAVFileProviderExtension/WebDAVFileProviderExtension.csproj index 90de87c..8d98543 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderExtension/WebDAVFileProviderExtension.csproj +++ b/macOS/WebDAVDrive/WebDAVFileProviderExtension/WebDAVFileProviderExtension.csproj @@ -75,8 +75,8 @@
    - - + + diff --git a/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/WebDAVFileProviderUIExtension.csproj b/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/WebDAVFileProviderUIExtension.csproj index 0b89efe..7fef514 100644 --- a/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/WebDAVFileProviderUIExtension.csproj +++ b/macOS/WebDAVDrive/WebDAVFileProviderUIExtension/WebDAVFileProviderUIExtension.csproj @@ -92,4 +92,7 @@ + + + diff --git a/macOS/WebDAVDrive/WebDAVMacApp/AppDelegate.cs b/macOS/WebDAVDrive/WebDAVMacApp/AppDelegate.cs index 04fb971..2fa928a 100644 --- a/macOS/WebDAVDrive/WebDAVMacApp/AppDelegate.cs +++ b/macOS/WebDAVDrive/WebDAVMacApp/AppDelegate.cs @@ -52,10 +52,9 @@ public override void DidFinishLaunching(NSNotification notification) webSocketServerUrl = domainSettings.WebSocketServerUrl; } - RemoteStorageMonitor = new RemoteStorageMonitor(webDAVServerUrl, webSocketServerUrl, new ConsoleLogger(typeof(RemoteStorageMonitor).Name)); - RemoteStorageMonitor.ServerNotifications = new ServerNotifications( - NSFileProviderManager.FromDomain(new NSFileProviderDomain(domainIdentifier, SecureStorage.ExtensionDisplayName)), - RemoteStorageMonitor.Logger); + 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(); if (NSProcessInfo.ProcessInfo.Arguments.Length > 1) @@ -285,7 +284,8 @@ private async Task CreateDomainAsync(string domainIdentifier, string domai { // Save domain identifier. await SecureStorage.SetAsync("CurrentDomainIdentifier", domainIdentifier); - RemoteStorageMonitor = new RemoteStorageMonitor(domainSettings.WebDAVServerUrl, domainSettings.WebSocketServerUrl, new ConsoleLogger(typeof(RemoteStorageMonitor).Name)); + 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(); success = true; diff --git a/macOS/WebDAVDrive/WebDAVMacApp/Mapping.cs b/macOS/WebDAVDrive/WebDAVMacApp/Mapping.cs index 6ce9e87..975af23 100644 --- a/macOS/WebDAVDrive/WebDAVMacApp/Mapping.cs +++ b/macOS/WebDAVDrive/WebDAVMacApp/Mapping.cs @@ -15,7 +15,7 @@ public static string GetAbsoluteUri(string relativePath, string webDAVServerRoot Uri webDavServerUri = new Uri(webDAVServerRootUrl); string host = webDavServerUri.GetLeftPart(UriPartial.Authority); - string path = $"{host}/{relativePath}"; + string path = $"{host}/{relativePath}/"; return path; } } diff --git a/macOS/WebDAVDrive/WebDAVMacApp/RemoteStorageMonitor.cs b/macOS/WebDAVDrive/WebDAVMacApp/RemoteStorageMonitor.cs index a78eeb1..1185812 100644 --- a/macOS/WebDAVDrive/WebDAVMacApp/RemoteStorageMonitor.cs +++ b/macOS/WebDAVDrive/WebDAVMacApp/RemoteStorageMonitor.cs @@ -1,20 +1,20 @@ using Common.Core; using ITHit.FileSystem; +using ITHit.WebDAV.Client; +using Client = ITHit.WebDAV.Client; + using WebDAVCommon; +using FileProvider; namespace WebDAVMacApp { internal class RemoteStorageMonitor : RemoteStorageMonitorBase - { - /// - /// WebDAV server root url. - /// - public readonly string WebDAVServerUrl; + { - internal RemoteStorageMonitor(string webDAVServerUrl, string webSocketServerUrl, ILogger logger) : base(webSocketServerUrl, logger) + internal RemoteStorageMonitor(string webDAVServerUrl, string webSocketServerUrl, NSFileProviderManager fileProviderManager, ILogger logger) : + base(webDAVServerUrl, webSocketServerUrl, fileProviderManager, logger) { - this.InstanceId = Environment.MachineName; - this.WebDAVServerUrl = webDAVServerUrl; + this.InstanceId = Environment.MachineName; } /// @@ -39,5 +39,21 @@ public override bool Filter(WebSocketMessage webSocketMessage) return true; } + + /// + /// Indicates if the root folder supports Collection Synchronization implementation. + /// + /// True if the WebDav server supports Collection Synchronization. False otherwise. + public override async Task IsSyncCollectionSupportedAsync() + { + 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")); + } + } } } diff --git a/macOS/WebDAVDrive/WebDAVMacApp/RemoteStorageMonitorBase.cs b/macOS/WebDAVDrive/WebDAVMacApp/RemoteStorageMonitorBase.cs index d999ee6..4080243 100644 --- a/macOS/WebDAVDrive/WebDAVMacApp/RemoteStorageMonitorBase.cs +++ b/macOS/WebDAVDrive/WebDAVMacApp/RemoteStorageMonitorBase.cs @@ -1,15 +1,12 @@ -using System; using System.Collections.Concurrent; -using System.IO; -using System.Linq; using System.Net; using System.Net.WebSockets; using System.Text; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - +using FileProvider; using ITHit.FileSystem; +using ITHit.FileSystem.Mac; +using ITHit.WebDAV.Client; namespace WebDAVMacApp { @@ -20,6 +17,11 @@ namespace WebDAVMacApp /// public abstract class RemoteStorageMonitorBase : ISyncService, IDisposable { + /// + /// WebDAV server root url. + /// + public readonly string WebDAVServerUrl; + /// /// Credentials to authenticate web sockets. /// @@ -69,6 +71,11 @@ public virtual SynchronizationState SyncState ///
    private readonly string webSocketServerUrl; + /// + /// Indicates if the root folder supports Collection Synchronization implementation. + /// + private bool isSyncCollectionSupported = false; + /// /// WebSocket cancellation token. /// @@ -88,17 +95,25 @@ public virtual SynchronizationState SyncState /// arrive from remote storage during execution only the last one will be processed. /// We do not want to execute multiple requests concurrently. /// - private BlockingCollection changeQueue = new BlockingCollection(); + private BlockingCollection changeQueue = new BlockingCollection(); + + /// + /// File provider manager. + /// + private readonly NSFileProviderManager fileProviderManager; /// /// Creates instance of this class. /// /// WebSocket server url. + /// File provider manager. /// Logger. - internal RemoteStorageMonitorBase(string webSocketServerUrl, ILogger logger) + internal RemoteStorageMonitorBase(string webDAVServerUrl, string webSocketServerUrl, NSFileProviderManager fileProviderManager, ILogger logger) { this.Logger = logger.CreateLogger("Remote Storage Monitor"); + this.fileProviderManager = fileProviderManager; this.webSocketServerUrl = webSocketServerUrl; + this.WebDAVServerUrl = webDAVServerUrl; } /// @@ -109,6 +124,12 @@ internal RemoteStorageMonitorBase(string webSocketServerUrl, ILogger logger) /// True if the item exists and should be updated. False otherwise. public abstract bool Filter(WebSocketMessage webSocketMessage); + /// + /// Indicates if the root folder supports Collection Synchronization implementation. + /// + /// True if the WebDav server supports Collection Synchronization. False otherwise. + public abstract Task IsSyncCollectionSupportedAsync(); + /// /// Monitors and processes WebSockets notifications from the remote storage. /// @@ -132,9 +153,9 @@ private async Task RunWebSocketsAsync(CancellationToken cancellationToken) // Because of the on-demand loading, item or its parent may not exists or be offline. // We can ignore notifiction in this case and avoid many requests to the remote storage. - if (webSocketMessage != null && !Filter(webSocketMessage) && changeQueue.Count == 0) + if (webSocketMessage != null && !Filter(webSocketMessage) && (changeQueue.Count == 0 || !isSyncCollectionSupported)) { - changeQueue.Add(webSocketMessage.ItemPath); + changeQueue.Add(webSocketMessage); } } } @@ -165,6 +186,9 @@ await Task.Factory.StartNew( { repeat = false; + // Check if WebDAV server supports Collection Synchronization. + isSyncCollectionSupported = await IsSyncCollectionSupportedAsync(); + // Configure web sockets and connect to the server. clientWebSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(10); if (Credentials != null) @@ -183,10 +207,17 @@ await Task.Factory.StartNew( await clientWebSocket.ConnectAsync(new Uri(webSocketServerUrl), cancellationToken); Logger.LogMessage("Connected", webSocketServerUrl); - // 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 all changes from server", webSocketServerUrl); - await ProcessAsync(); + if (isSyncCollectionSupported) + { + // 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 all changes from server", webSocketServerUrl); + await ProcessChangesAsync(); + } + else + { + await PoolingAsync(); + } Logger.LogMessage("Started", webSocketServerUrl); @@ -204,7 +235,7 @@ await Task.Factory.StartNew( // network disconnections or server failure. await Task.Delay(TimeSpan.FromSeconds(2), cancellationTokenSource.Token); repeat = true; - }; + } } } while (repeat && !cancellationToken.IsCancellationRequested); } @@ -244,7 +275,7 @@ public async Task StopAsync() /// web sockets should not stop processing changes. /// To stop processing changes that are already received the Engine must be stopped. /// - private async Task ProcessAsync() + private async Task ProcessChangesAsync() { try { @@ -256,6 +287,26 @@ private async Task ProcessAsync() } } + /// + /// Starts pooling synchronization. + /// + /// + /// We do not pass WebSockets cancellation token to this method because stopping + /// web sockets should not stop processing pooling. + /// To stop processing changes that are already received the Engine must be stopped. + /// + private async Task PoolingAsync() + { + try + { + await ServerNotifications.PoolingAsync(); + } + catch (Exception ex) + { + Logger.LogError("Failed to process pooling", null, null, ex); + } + } + /// /// Starts thread that processes changes queue. /// @@ -269,9 +320,16 @@ private Task CreateHandlerChangesTask(CancellationToken cancelationToken) { while (!cancelationToken.IsCancellationRequested) { - _ = changeQueue.Take(cancelationToken); - - await ProcessAsync(); + WebSocketMessage message = changeQueue.Take(cancelationToken); + + if (isSyncCollectionSupported) + { + await ProcessChangesAsync(); + } + else + { + await PoolingAsync(); + } } } catch (OperationCanceledException) diff --git a/macOS/WebDAVDrive/WebDAVMacApp/WebDAVMacApp.csproj b/macOS/WebDAVDrive/WebDAVMacApp/WebDAVMacApp.csproj index 16ddaf6..1f19bcc 100644 --- a/macOS/WebDAVDrive/WebDAVMacApp/WebDAVMacApp.csproj +++ b/macOS/WebDAVDrive/WebDAVMacApp/WebDAVMacApp.csproj @@ -71,6 +71,10 @@ AfterBuild cp -rf "${ProjectDir}/../WebDAVFileProviderExtension/bin/Release/WebDAVFileProviderExtension.appex" "${TargetDir}/WebDAV Drive.app/Contents/PlugIns" + + AfterBuild + cp -rf "${ProjectDir}/../WebDAVFileProviderUIExtension/bin/Release/WebDAVFileProviderUIExtension.appex" "${TargetDir}/WebDAV Drive.app/Contents/PlugIns" + AfterBuild codesign --force --sign "Mac Developer" --entitlements ${ProjectDir}/Entitlements.plist --timestamp --generate-entitlement-der "${TargetDir}/WebDAV Drive.app" @@ -141,7 +145,7 @@ - + diff --git a/macOS/WebDAVDrive/WebDAVMacApp/pkg.sh b/macOS/WebDAVDrive/WebDAVMacApp/pkg.sh index a8548a9..7d16a86 100644 --- a/macOS/WebDAVDrive/WebDAVMacApp/pkg.sh +++ b/macOS/WebDAVDrive/WebDAVMacApp/pkg.sh @@ -1,35 +1 @@ -rm -R "$1/root" -mkdir "$1/root" -cp -R "$1/WebDAV Drive.app" "$1/root" - -cat < $1/root/pkg.plist - - - - - - BundleHasStrictIdentifier - - BundleIsRelocatable - - BundleIsVersionChecked - - BundleOverwriteAction - upgrade - ChildBundles - - - BundleOverwriteAction - - RootRelativeBundlePath - WebDAV Drive.app/Contents/PlugIns/WebDAVFileProviderExtension.appex - - - RootRelativeBundlePath - WebDAV Drive.app - - - -EOF - -pkgbuild --root "$1/root" --version 1 --install-location /Applications --component-plist "$1/root/pkg.plist" "$1/WebDAV Drive-1.0.pkg" +rm -R "$1/root" mkdir "$1/root" cp -R "$1/WebDAV Drive.app" "$1/root" cat < $1/root/pkg.plist BundleHasStrictIdentifier BundleIsRelocatable BundleIsVersionChecked BundleOverwriteAction upgrade ChildBundles BundleOverwriteAction RootRelativeBundlePath WebDAV Drive.app/Contents/PlugIns/WebDAVFileProviderExtension.appex dict> BundleOverwriteAction RootRelativeBundlePath WebDAV Drive.app/Contents/PlugIns/WebDAVFileProviderUIExtension.appex RootRelativeBundlePath WebDAV Drive.app EOF pkgbuild --root "$1/root" --version 1 --install-location /Applications --component-plist "$1/root/pkg.plist" "$1/WebDAV Drive.pkg" productsign --sign "3rd Party Mac Developer Installer" "$1/WebDAV Drive.pkg" "$1/WebDAV Drive signed.pkg" \ No newline at end of file