diff --git a/Common/Common.csproj b/Common/Common.csproj index 99b6925..cb0a2ab 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -9,6 +9,6 @@ ITHit.FileSystem.Samples.Common - + \ No newline at end of file diff --git a/Common/FileSystemItemMetadataExt.cs b/Common/FileSystemItemMetadataExt.cs index aa88e67..c959b8d 100644 --- a/Common/FileSystemItemMetadataExt.cs +++ b/Common/FileSystemItemMetadataExt.cs @@ -21,9 +21,6 @@ public class FileSystemItemMetadataExt : IFileSystemItemMetadata /// public FileAttributes Attributes { get; set; } - /// - public byte[] CustomData { get; set; } = new byte[] { }; - /// public DateTimeOffset CreationTime { get; set; } diff --git a/Common/Settings.cs b/Common/Settings.cs index 3130a78..e4bff52 100644 --- a/Common/Settings.cs +++ b/Common/Settings.cs @@ -32,12 +32,6 @@ public class Settings /// public string UserFileSystemRootPath { get; set; } - /// - /// Network delay in milliseconds. When this parameter is > 0 the file download will be delayd to demonstrate file transfer progress. - /// Set this parameter to 0 to avoid any network simulation delays. - /// - public int NetworkSimulationDelayMs { get; set; } - /// /// Path to the icons folder. /// diff --git a/Common/StreamCopy.cs b/Common/StreamCopy.cs index cf07e7c..321acb3 100644 --- a/Common/StreamCopy.cs +++ b/Common/StreamCopy.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace ITHit.FileSystem.Samples.Common @@ -19,14 +20,15 @@ public static class StreamCopy /// The stream to which the contents of the current file stream will be copied. /// The size, in bytes, of the buffer. This value must be greater than zero. /// Number of bytes to copy. - public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, long count) + /// The token to monitor for cancellation requests. + public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, long count, CancellationToken cancellationToken = default) { byte[] buffer = new byte[bufferSize]; int read; while (count > 0 - && (read = await source.ReadAsync(buffer, 0, (int)Math.Min(buffer.LongLength, count))) > 0) + && (read = await source.ReadAsync(buffer, 0, (int)Math.Min(buffer.LongLength, count), cancellationToken)) > 0) { - await destination.WriteAsync(buffer, 0, read); + await destination.WriteAsync(buffer, 0, read, cancellationToken); count -= read; } } diff --git a/Windows/Common/Core/Common.Windows.Core.csproj b/Windows/Common/Core/Common.Windows.Core.csproj index 0ec41fc..beea4ae 100644 --- a/Windows/Common/Core/Common.Windows.Core.csproj +++ b/Windows/Common/Core/Common.Windows.Core.csproj @@ -14,7 +14,7 @@ - + \ No newline at end of file diff --git a/Windows/Common/Core/Registrar.cs b/Windows/Common/Core/Registrar.cs index 65a3b1e..4f5b759 100644 --- a/Windows/Common/Core/Registrar.cs +++ b/Windows/Common/Core/Registrar.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Windows.Security.Cryptography; @@ -134,7 +135,7 @@ public static async Task IsRegisteredAsync(string path) try { StorageProviderSyncRootManager.GetSyncRootInformationForFolder(storageFolder); - return true; + return FileSystem.Windows.PlaceholderItem.IsPlaceholder(path); } catch { @@ -164,5 +165,26 @@ public static async Task UnregisterAsync(string syncRootId) { StorageProviderSyncRootManager.Unregister(syncRootId); } + + /// + /// Helper method to determine if the process is running in a packaged context. + /// + /// True if the application is running in a packaged context, false otherwise. + public static bool IsRunningAsUwp() + { + const long APPMODEL_ERROR_NO_PACKAGE = 15700L; + + int length = 0; + return GetCurrentPackageFullName(ref length, null) != APPMODEL_ERROR_NO_PACKAGE; + } + + /// + /// Gets the package full name for the calling process. + /// + /// On input, the size of the packageFullName buffer, in characters. On output, the size of the package full name returned, in characters, including the null terminator. + /// The package full name. + /// If the function succeeds it returns ERROR_SUCCESS. Otherwise, the function returns an error code. + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder packageFullName); } } diff --git a/Windows/Common/Rpc.Proto/ShellExtension.proto b/Windows/Common/Rpc.Proto/ShellExtension.proto index a2f77be..ea80b7a 100644 --- a/Windows/Common/Rpc.Proto/ShellExtension.proto +++ b/Windows/Common/Rpc.Proto/ShellExtension.proto @@ -9,6 +9,9 @@ service ShellExtensionRpc { rpc LogMessage (LogMessageRequest) returns (EmptyMessage) {} rpc LogError (LogErrorRequest) returns (EmptyMessage) {} rpc GetItemProperties (ItemPropertyRequest) returns (ItemsPropertyList) {} + + rpc GetPathForContentUri (UriSourceRequest) returns (GetPathForContentUriResult) {} + rpc GetContentInfoForPath (UriSourceRequest) returns (GetContentInfoForPathResult) {} } message ItemsPathList { @@ -59,3 +62,20 @@ message ItemsPropertyList { message ItemPropertyRequest { string path = 1; } + +message UriSourceRequest { + string pathOrUri = 1; +} + +message GetPathForContentUriResult { + string path = 1; + int32 status = 2; +} + +message GetContentInfoForPathResult { + string contentId = 1; + string contentUri = 2; + int32 status = 3; +} + + diff --git a/Windows/Common/ShellExtension/Common.Windows.ShellExtension.csproj b/Windows/Common/ShellExtension/Common.Windows.ShellExtension.csproj index f5713b2..d1f3e96 100644 --- a/Windows/Common/ShellExtension/Common.Windows.ShellExtension.csproj +++ b/Windows/Common/ShellExtension/Common.Windows.ShellExtension.csproj @@ -20,7 +20,7 @@ - + diff --git a/Windows/Common/ShellExtension/ShellExtensionInstaller.cs b/Windows/Common/ShellExtension/ShellExtensionInstaller.cs deleted file mode 100644 index 6c1c68a..0000000 --- a/Windows/Common/ShellExtension/ShellExtensionInstaller.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text; -using Microsoft.Win32; -using ITHit.FileSystem.Samples.Common.Windows.ShellExtension.Thumbnails; - -namespace ITHit.FileSystem.Samples.Common.Windows.ShellExtension -{ - /// - /// Thumbnail COM registartion helper methods. - /// - /// - /// These methods are NOT required in case of intallation via packaging. - /// You can use them if you need to register/unregister COM thumbnail in your msi installer. - /// - public static class ShellExtensionInstaller - { - private static readonly string LocalServer32Path = @"SOFTWARE\Classes\CLSID\{0:B}\LocalServer32"; - - private static string ExePath => typeof(ShellExtensionInstaller).Assembly.Location; - - /// - /// Register com library for thumbnails - /// - public static void RegisterComServer(params Guid[] clsids) - { - foreach (Guid clsid in clsids) - { - string serverKey = string.Format(LocalServer32Path, clsid); - using RegistryKey regKey = Registry.LocalMachine.CreateSubKey(serverKey); - regKey.SetValue(null, ExePath); - } - } - - /// - /// Unregister com library for thumbnails - /// - public static void UnregisterComServer(params Guid[] clsids) - { - foreach (Guid clsid in clsids) - { - // Unregister local server - string serverKey = string.Format(LocalServer32Path, clsid); - Registry.LocalMachine.DeleteSubKey(serverKey, throwOnMissingSubKey: false); - } - } - - public static bool HandleRegCommand(string regCommand, params Guid[] clsids) - { - if (regCommand.Equals("/regserver", StringComparison.OrdinalIgnoreCase) || regCommand.Equals("-regserver", StringComparison.OrdinalIgnoreCase)) - { - // Register local server and type library - RegisterComServer(clsids); - return true; - } - else if (regCommand.Equals("/unregserver", StringComparison.OrdinalIgnoreCase) || regCommand.Equals("-unregserver", StringComparison.OrdinalIgnoreCase)) - { - // Unregister local server and type library - UnregisterComServer(clsids); - return true; - } - - return false; - } - } -} diff --git a/Windows/Common/ShellExtension/ShellExtensionRegistrar.cs b/Windows/Common/ShellExtension/ShellExtensionRegistrar.cs new file mode 100644 index 0000000..1a61ffd --- /dev/null +++ b/Windows/Common/ShellExtension/ShellExtensionRegistrar.cs @@ -0,0 +1,43 @@ +using Microsoft.Win32; +using System; + +namespace ITHit.FileSystem.Samples.Common.Windows.ShellExtension +{ + public class ShellExtensionRegistrar + { + private static readonly string ClsidKeyPathFormat = @"SOFTWARE\Classes\CLSID\{0:B}"; + private static readonly string LocalServer32PathFormat = @$"{ClsidKeyPathFormat}\LocalServer32"; + private static readonly string SyncRootPathFormat = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager\{0}"; + + /// + /// Register shell service provider COM class and register it for sync root. + /// + /// Sync root identifier + /// Name of shell service provider + /// CLSID of shell service provider + /// Absolute path to COM server executable + public static void Register(string syncRootId, string handlerName, Guid handlerGuid, string comServerPath) + { + string handlerPath = handlerGuid.ToString("B").ToUpper(); + string syncRootPath = string.Format(SyncRootPathFormat, syncRootId); + + using RegistryKey syncRootKey = Registry.LocalMachine.OpenSubKey(syncRootPath, true); + syncRootKey.SetValue(handlerName, handlerPath); + + string localServer32Path = string.Format(LocalServer32PathFormat, handlerPath); + using RegistryKey localServer32Key = Registry.CurrentUser.CreateSubKey(localServer32Path); + localServer32Key.SetValue(null, comServerPath); + } + + /// + /// Unregister shell service provider COM class. + /// + /// + public static void Unregister(Guid handlerClsid) + { + string thumbnailProviderGuid = handlerClsid.ToString("B").ToUpper(); + string clsidKeyPath = string.Format(ClsidKeyPathFormat, thumbnailProviderGuid); + Registry.CurrentUser.DeleteSubKeyTree(clsidKeyPath, false); + } + } +} diff --git a/Windows/Common/VirtualDrive/Common.Windows.VirtualDrive.csproj b/Windows/Common/VirtualDrive/Common.Windows.VirtualDrive.csproj index e608327..27e8eee 100644 --- a/Windows/Common/VirtualDrive/Common.Windows.VirtualDrive.csproj +++ b/Windows/Common/VirtualDrive/Common.Windows.VirtualDrive.csproj @@ -23,7 +23,7 @@ - + diff --git a/Windows/Common/VirtualDrive/FullSync/ClientToServerSync.cs b/Windows/Common/VirtualDrive/FullSync/ClientToServerSync.cs deleted file mode 100644 index fb8edd4..0000000 --- a/Windows/Common/VirtualDrive/FullSync/ClientToServerSync.cs +++ /dev/null @@ -1,93 +0,0 @@ -using log4net; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -using ITHit.FileSystem.Samples.Common.Windows; -using ITHit.FileSystem.Windows; -using ITHit.FileSystem; - -namespace ITHit.FileSystem.Samples.Common.Windows -{ - /// - /// User File System to Remote Storage synchronization. - /// - /// In most cases you can use this class in your project without any changes. - internal class ClientToServerSync : Logger - { - /// - /// Virtual drive. - /// - private readonly VirtualEngineBase engine; - - /// - /// Creates instance of this class. - /// - /// Logger. - internal ClientToServerSync(VirtualEngineBase engine, ILog log) : base("UFS -> RS Sync", log) - { - this.engine = engine; - } - - /// - /// Recursively updates and creates files and folders in the remote storage. - /// - /// Folder path in the user file system. - /// - /// Synchronizes only folders already loaded into the user file system. - /// This method does not sync moved and deleted items. - /// - internal async Task SyncronizeFolderAsync(string userFileSystemFolderPath) - { - // In case of on-demand loading the user file system contains only a subset of the server files and folders. - // Here we sync folder only if its content already loaded into user file system (folder is not offline). - // The folder content is loaded inside IFolder.GetChildrenAsync() method. - if (new DirectoryInfo(userFileSystemFolderPath).Attributes.HasFlag(System.IO.FileAttributes.Offline)) - { -// LogMessage("Folder offline, skipping:", userFileSystemFolderPath); - return; - } - - IEnumerable userFileSystemChildren = Directory.EnumerateFileSystemEntries(userFileSystemFolderPath, "*"); -// LogMessage("Synchronizing:", userFileSystemFolderPath); - - - // Update and create files/folders in remote storage. - foreach (string userFileSystemPath in userFileSystemChildren) - { - try - { - await engine.ClientNotifications(userFileSystemPath, this).CreateOrUpdateAsync(); - } - catch (ClientLockFailedException ex) - { - // Blocked for create/update/lock/unlock operation from another thread. - // Thrown by CreateAsync()/UpdateAsync() call. This is a normal behaviour. - LogMessage(ex.Message, ex.Path); - } - catch (Exception ex) - { - LogError("Creation or update failed", userFileSystemPath, null, ex); - } - - // Synchronize subfolders. - try - { - if (Directory.Exists(userFileSystemPath)) - { - await SyncronizeFolderAsync(userFileSystemPath); - } - } - catch (Exception ex) - { - LogError("Folder sync failed", userFileSystemPath, null, ex); - } - } - } - } -} diff --git a/Windows/Common/VirtualDrive/IVirtualFolder.cs b/Windows/Common/VirtualDrive/IVirtualFolder.cs index e9b0736..d0f55c4 100644 --- a/Windows/Common/VirtualDrive/IVirtualFolder.cs +++ b/Windows/Common/VirtualDrive/IVirtualFolder.cs @@ -2,12 +2,13 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace ITHit.FileSystem.Samples.Common.Windows { public interface IVirtualFolder { - public Task> EnumerateChildrenAsync(string pattern); + public Task> EnumerateChildrenAsync(string pattern, CancellationToken cancellationToken); } } diff --git a/Windows/Common/VirtualDrive/Rpc/GrpcServerServiceImpl.cs b/Windows/Common/VirtualDrive/Rpc/GrpcServerServiceImpl.cs index b2100a5..f32cd26 100644 --- a/Windows/Common/VirtualDrive/Rpc/GrpcServerServiceImpl.cs +++ b/Windows/Common/VirtualDrive/Rpc/GrpcServerServiceImpl.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using ITHit.FileSystem.Windows; using ITHit.FileSystem.Samples.Common.Windows.Rpc.Generated; +using ITHit.FileSystem.Samples.Common.Windows.VirtualDrive.Rpc; using Grpc.Core; using System.Xml.Serialization; using System.IO; @@ -42,9 +43,9 @@ public override async Task SetLockStatus(ItemsStatusList request, IClientNotifications clientNotifications = engine.ClientNotifications(filePath); if (fileStatus) - await clientNotifications.LockAsync(); + await clientNotifications.LockAsync(cancellationToken: context.CancellationToken); else - await clientNotifications.UnlockAsync(); + await clientNotifications.UnlockAsync(cancellationToken: context.CancellationToken); } catch (Exception ex) { @@ -72,7 +73,7 @@ public override async Task GetLockStatus(ItemsPathList request, foreach (string filePath in request.Files) { IClientNotifications clientNotifications = engine.ClientNotifications(filePath); - LockMode lockMode = await clientNotifications.GetLockModeAsync(); + LockMode lockMode = await clientNotifications.GetLockModeAsync(cancellationToken: context.CancellationToken); bool lockStatus = lockMode != LockMode.None; itemsStatusList.FilesStatus.Add(filePath, lockStatus); @@ -174,5 +175,32 @@ public override async Task GetItemProperties(ItemPropertyRequ throw new RpcException(new Status(StatusCode.Internal, ex.Message)); } } + + public override async Task GetPathForContentUri(UriSourceRequest request, ServerCallContext context) + { + logger.LogMessage($"{nameof(GetPathForContentUri)}()", request.PathOrUri); + + GetPathForContentUriResult result = new GetPathForContentUriResult() + { + Path = request.PathOrUri + "\\test", + Status = (int)StorageProviderUriSourceStatus.Success + }; + + return result; + } + + public override async Task GetContentInfoForPath(UriSourceRequest request, ServerCallContext context) + { + logger.LogMessage($"{nameof(GetContentInfoForPath)}()", request.PathOrUri); + + GetContentInfoForPathResult result = new GetContentInfoForPathResult() + { + ContentId = "id. " + request.PathOrUri, + ContentUri = "uri\\" + request.PathOrUri, + Status = (int)StorageProviderUriSourceStatus.Success + }; + + return result; + } } } diff --git a/Windows/Common/VirtualDrive/Rpc/StorageProviderUriSourceStatus.cs b/Windows/Common/VirtualDrive/Rpc/StorageProviderUriSourceStatus.cs new file mode 100644 index 0000000..5a9e608 --- /dev/null +++ b/Windows/Common/VirtualDrive/Rpc/StorageProviderUriSourceStatus.cs @@ -0,0 +1,9 @@ +namespace ITHit.FileSystem.Samples.Common.Windows.VirtualDrive.Rpc +{ + public enum StorageProviderUriSourceStatus : int + { + Success = 0, + NoSyncRoot = 1, + FileNotFound = 2, + } +} diff --git a/Windows/Common/VirtualDrive/VirtualEngineBase.cs b/Windows/Common/VirtualDrive/VirtualEngineBase.cs index 5cc464a..a052e18 100644 --- a/Windows/Common/VirtualDrive/VirtualEngineBase.cs +++ b/Windows/Common/VirtualDrive/VirtualEngineBase.cs @@ -8,6 +8,7 @@ using ITHit.FileSystem.Samples.Common.Windows; using ITHit.FileSystem.Samples.Common.Windows.Rpc; using System.Collections.Generic; +using System.Threading; namespace ITHit.FileSystem.Samples.Common.Windows { @@ -122,9 +123,9 @@ public override async Task FilterAsync(OperationType operationType, string } /// - public override async Task StartAsync(bool processModified = true) + public override async Task StartAsync(bool processModified = true, CancellationToken cancellationToken = default) { - await base.StartAsync(processModified); + await base.StartAsync(processModified, cancellationToken); //RemoteStorageMonitor.Start(); //await SyncService.StartAsync(); grpcServer.Start(); diff --git a/Windows/Common/WinRT.ShellExtension.Rpc/StorageProviderGetContentInfoForPathResult.cs b/Windows/Common/WinRT.ShellExtension.Rpc/StorageProviderGetContentInfoForPathResult.cs new file mode 100644 index 0000000..2136ae2 --- /dev/null +++ b/Windows/Common/WinRT.ShellExtension.Rpc/StorageProviderGetContentInfoForPathResult.cs @@ -0,0 +1,18 @@ +namespace CommonShellExtensionRpc +{ + public sealed class StorageProviderGetContentInfoForPathResult + { + public StorageProviderGetContentInfoForPathResult(string contentId, string contentUri, int status) + { + ContentId = contentId; + ContentUri = contentUri; + Status = status; + } + + public string ContentId { get; set; } + + public string ContentUri { get; set; } + + public int Status { get; set; } + } +} diff --git a/Windows/Common/WinRT.ShellExtension.Rpc/StorageProviderGetPathForContentUriResult.cs b/Windows/Common/WinRT.ShellExtension.Rpc/StorageProviderGetPathForContentUriResult.cs new file mode 100644 index 0000000..c98172c --- /dev/null +++ b/Windows/Common/WinRT.ShellExtension.Rpc/StorageProviderGetPathForContentUriResult.cs @@ -0,0 +1,15 @@ +namespace CommonShellExtensionRpc +{ + public sealed class StorageProviderGetPathForContentUriResult + { + public StorageProviderGetPathForContentUriResult(string path, int status) + { + Path = path; + Status = status; + } + + public string Path { get; set; } + + public int Status { get; set; } + } +} diff --git a/Windows/Common/WinRT.ShellExtension.Rpc/UriSourceProxy.cs b/Windows/Common/WinRT.ShellExtension.Rpc/UriSourceProxy.cs new file mode 100644 index 0000000..804bb8e --- /dev/null +++ b/Windows/Common/WinRT.ShellExtension.Rpc/UriSourceProxy.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using ITHit.FileSystem.Samples.Common.Windows.Rpc; +using ITHit.FileSystem.Samples.Common.Windows.Rpc.Generated; + +namespace CommonShellExtensionRpc +{ + public sealed class UriSourceProxy + { + public StorageProviderGetPathForContentUriResult GetPathForContentUri(string contentUri) + { + string channelName = "VirtualDrive.RPC"; + + GrpcClient grpcClient = new GrpcClient(channelName); + + try + { + UriSourceRequest request = new() + { + PathOrUri = contentUri + }; + + GetPathForContentUriResult result = grpcClient.RpcClient.GetPathForContentUriAsync(request).GetAwaiter().GetResult(); + + return new StorageProviderGetPathForContentUriResult(result.Path, result.Status); + } + catch (Exception ex) + { + LogErrorRequest request = new() + { + Message = ex.Message, + SourcePath = contentUri + }; + + grpcClient.RpcClient.LogError(request); + } + + return new StorageProviderGetPathForContentUriResult("", 0); + } + + public StorageProviderGetContentInfoForPathResult GetContentInfoForPath(string path) + { + string channelName = "VirtualDrive.RPC"; + + GrpcClient grpcClient = new GrpcClient(channelName); + + try + { + UriSourceRequest request = new() + { + PathOrUri = path + }; + + GetContentInfoForPathResult result = grpcClient.RpcClient.GetContentInfoForPathAsync(request).GetAwaiter().GetResult(); + + return new StorageProviderGetContentInfoForPathResult(result.ContentId, result.ContentUri, result.Status); + } + catch (Exception ex) + { + LogErrorRequest request = new() + { + Message = ex.Message, + SourcePath = path + }; + + grpcClient.RpcClient.LogError(request); + } + + return new StorageProviderGetContentInfoForPathResult("", "", 0); + } + } +} diff --git a/Windows/Common/WinRT.ShellExtension/CustomStateProvider.idl b/Windows/Common/WinRT.ShellExtension/CustomStateProvider.idl index 3da783b..d6ee934 100644 --- a/Windows/Common/WinRT.ShellExtension/CustomStateProvider.idl +++ b/Windows/Common/WinRT.ShellExtension/CustomStateProvider.idl @@ -1,9 +1,7 @@ namespace CommonWindowsRtShellExtenstion { - runtimeclass CustomStateProvider : [default] Windows.Storage.Provider.IStorageProviderItemPropertySource { - CustomStateProvider(); } } diff --git a/Windows/UserFileSystemSamples.sln b/Windows/UserFileSystemSamples.sln index 6308b25..494cfd5 100644 --- a/Windows/UserFileSystemSamples.sln +++ b/Windows/UserFileSystemSamples.sln @@ -43,6 +43,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VirtualDrive.WinRT.ShellExt EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebDAVDrive.WinRT.ShellExtension", "WebDAVDrive\WebDAVDrive.WinRT.ShellExtension\WebDAVDrive.WinRT.ShellExtension.vcxproj", "{5730488F-9F58-4951-9502-49A5A9A42B07}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualDrive.Common", "VirtualDrive\VirtualDrive.Common\VirtualDrive.Common.csproj", "{C6806A0E-50E7-401D-B474-CA2C7CEAD78A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -383,6 +385,26 @@ Global {5730488F-9F58-4951-9502-49A5A9A42B07}.Release|x64.Build.0 = Release|x64 {5730488F-9F58-4951-9502-49A5A9A42B07}.Release|x86.ActiveCfg = Release|Win32 {5730488F-9F58-4951-9502-49A5A9A42B07}.Release|x86.Build.0 = Release|Win32 + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Debug|ARM.Build.0 = Debug|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Debug|ARM64.Build.0 = Debug|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Debug|x64.ActiveCfg = Debug|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Debug|x64.Build.0 = Debug|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Debug|x86.Build.0 = Debug|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Release|Any CPU.Build.0 = Release|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Release|ARM.ActiveCfg = Release|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Release|ARM.Build.0 = Release|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Release|ARM64.ActiveCfg = Release|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Release|ARM64.Build.0 = Release|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Release|x64.ActiveCfg = Release|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Release|x64.Build.0 = Release|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Release|x86.ActiveCfg = Release|Any CPU + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -404,6 +426,7 @@ Global {8EA7BABA-FC44-4074-86CB-88B8F42CA055} = {F24CBDD0-7A18-43F0-BCD2-A8FD1A8A7B54} {98E623B9-BCAB-48D2-80A2-1D7AADE897D6} = {CAE8E8A6-2721-4C90-BCD9-F4B03C3D8F13} {5730488F-9F58-4951-9502-49A5A9A42B07} = {264745B0-DF86-41E1-B400-3CAA1B403830} + {C6806A0E-50E7-401D-B474-CA2C7CEAD78A} = {CAE8E8A6-2721-4C90-BCD9-F4B03C3D8F13} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {740A716A-38A7-46BC-A21F-18336D0023B7} diff --git a/Windows/VirtualDrive/README.md b/Windows/VirtualDrive/README.md index f8c45fe..f62c147 100644 --- a/Windows/VirtualDrive/README.md +++ b/Windows/VirtualDrive/README.md @@ -5,7 +5,7 @@

This sample is provided with IT Hit User File System v3 Beta and later versions.

    -
  • .NET Core 5.0 or later.
  • +
  • .NET Core 6.0 or later.
  • Microsoft Windows 10 Creators Update or later version.
  • NTFS file system.
@@ -14,7 +14,8 @@

To specify the folder that will be used for remote storage simulation edit the "RemoteStorageRootPath" parameter in appsettings.json. This could be either an absolute path or a path relative to the application root.

To specify the user file system folder edit the "UserFileSystemRootPath" parameter in appsettings.json.

-

To run the example, you will need a valid IT Hit User File System Engine for .NET License. You can download the license in the product download area. Note that the Engine is fully functional with a trial license and does not have any limitations. The trial license is valid for one month and the engine will stop working after this. You can check the expiration date inside the license file. Download the license file and specify its content in the UserFileSystemLicense field in appsettings.json file.

+

To run the example, you will need a valid IT Hit User File System Engine for .NET License. You can download the license in the product download area. Note that the Engine is fully functional with a trial license and does not have any limitations. The trial license is valid for one month and the engine will stop working after this. You can check the expiration date inside the license file. Download the license file and specify its content in the UserFileSystemLicense field in 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: \":

+
"UserFileSystemLicense": "<?xml version=\"1.0\" encoding=\"utf-8\"?><License…

You can also run the sample without explicitly specifying a license for 5 days. In this case, the Engine will automatically request the trial license from the IT Hit website https://www.userfilesystem.com. Make sure it is accessible via firewalls if any. After 5 days the Engine will stop working. To extend the trial period you will need to download a license in a product download area and specify it in appsettings.json

To run the sample open the project in Visual Studio and run the project in debug mode. When starting in the debug mode, it will automatically create a folder in which the virtual file system will reside, register the virtual drive with the platform and then open two instances of Windows File Manager, one of which will show a virtual drive and another a folder simulating remote storage. 

diff --git a/Windows/VirtualDrive/VirtualDrive/AppSettings.cs b/Windows/VirtualDrive/VirtualDrive.Common/AppSettings.cs similarity index 99% rename from Windows/VirtualDrive/VirtualDrive/AppSettings.cs rename to Windows/VirtualDrive/VirtualDrive.Common/AppSettings.cs index d8b9c8b..d8f15df 100644 --- a/Windows/VirtualDrive/VirtualDrive/AppSettings.cs +++ b/Windows/VirtualDrive/VirtualDrive.Common/AppSettings.cs @@ -1,13 +1,12 @@ using Microsoft.Extensions.Configuration; -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Reflection; using ITHit.FileSystem.Samples.Common; +using System; +using System.IO; -namespace VirtualDrive +namespace VirtualDrive.Common { /// /// Strongly binded project settings. diff --git a/Windows/VirtualDrive/VirtualDrive.Common/VirtualDrive.Common.csproj b/Windows/VirtualDrive/VirtualDrive.Common/VirtualDrive.Common.csproj new file mode 100644 index 0000000..b393e2b --- /dev/null +++ b/Windows/VirtualDrive/VirtualDrive.Common/VirtualDrive.Common.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + + + + + + + + + + + diff --git a/Windows/VirtualDrive/VirtualDrive.Package/Package.appxmanifest b/Windows/VirtualDrive/VirtualDrive.Package/Package.appxmanifest index a6d10dc..a5efaf2 100644 --- a/Windows/VirtualDrive/VirtualDrive.Package/Package.appxmanifest +++ b/Windows/VirtualDrive/VirtualDrive.Package/Package.appxmanifest @@ -5,8 +5,9 @@ xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:desktop3="http://schemas.microsoft.com/appx/manifest/desktop/windows10/3" + xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10" - IgnorableNamespaces="uap rescap"> + IgnorableNamespaces="uap rescap desktop3 desktop4"> - + - + - + + + - + - - + + - - - - + + + + + + + - + diff --git a/Windows/VirtualDrive/VirtualDrive.ShellExtension/ContextMenusProvider.cs b/Windows/VirtualDrive/VirtualDrive.ShellExtension/ContextMenusProvider.cs index 10c1394..036208e 100644 --- a/Windows/VirtualDrive/VirtualDrive.ShellExtension/ContextMenusProvider.cs +++ b/Windows/VirtualDrive/VirtualDrive.ShellExtension/ContextMenusProvider.cs @@ -15,12 +15,10 @@ namespace VirtualDrive.ShellExtension /// Implements Windows Explorer context menu. /// [ComVisible(true)] - [ProgId("VirtualDrive.ContextMenusProvider"), Guid(ContextMenusClass)] + [ProgId("VirtualDrive.ContextMenusProvider")] + [Guid("9C923BF3-3A4B-487B-AB4E-B4CF87FD1C25")] public class ContextMenusProvider : ContextMenusProviderBase { - public const string ContextMenusClass = "9C923BF3-3A4B-487B-AB4E-B4CF87FD1C25"; - public static readonly Guid ContextMenusClassGuid = Guid.Parse(ContextMenusClass); - public const string LockCommandIcon = "Locked.ico"; public const string UnlockCommandIcon = "Unlocked.ico"; diff --git a/Windows/VirtualDrive/VirtualDrive.ShellExtension/Program.cs b/Windows/VirtualDrive/VirtualDrive.ShellExtension/Program.cs index d51efb5..cba360d 100644 --- a/Windows/VirtualDrive/VirtualDrive.ShellExtension/Program.cs +++ b/Windows/VirtualDrive/VirtualDrive.ShellExtension/Program.cs @@ -5,13 +5,7 @@ using ITHit.FileSystem.Samples.Common.Windows.ShellExtension; using ITHit.FileSystem.Samples.Common.Windows.ShellExtension.ComInfrastructure; using ITHit.FileSystem.Samples.Common; -using System.Reflection; -using System.IO; -using System.Linq; -using log4net; -using log4net.Appender; -using log4net.Config; - +using VirtualDrive.Common; namespace VirtualDrive.ShellExtension { @@ -19,14 +13,6 @@ class Program { static async Task Main(string[] args) { - // Typically you will register COM exe server in packaged installer, inside Package.appxmanifest. - // This code is used for testing purposes only, it will register COM exe if you run this program directly. - if (args.Length == 1 && - ShellExtensionInstaller.HandleRegCommand(args[0], ThumbnailProvider.ThumbnailClassGuid, ContextMenusProvider.ContextMenusClassGuid)) - { - return; - } - // Load and initialize settings. IConfiguration configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json", false, true).Build(); Settings settings = configuration.ReadSettings(); @@ -37,8 +23,8 @@ static async Task Main(string[] args) { using (var server = new LocalServer()) { - server.RegisterClass(ThumbnailProvider.ThumbnailClassGuid); - server.RegisterClass(ContextMenusProvider.ContextMenusClassGuid); + server.RegisterClass(typeof(ThumbnailProvider).GUID); + server.RegisterClass(typeof(ContextMenusProvider).GUID); await server.Run(); } diff --git a/Windows/VirtualDrive/VirtualDrive.ShellExtension/ThumbnailProvider.cs b/Windows/VirtualDrive/VirtualDrive.ShellExtension/ThumbnailProvider.cs index ea77f13..f8f3c8c 100644 --- a/Windows/VirtualDrive/VirtualDrive.ShellExtension/ThumbnailProvider.cs +++ b/Windows/VirtualDrive/VirtualDrive.ShellExtension/ThumbnailProvider.cs @@ -1,9 +1,7 @@ using System; -using System.Drawing; using System.Runtime.InteropServices; using System.Threading.Tasks; using ITHit.FileSystem.Samples.Common.Windows.ShellExtension.Thumbnails; -using ITHit.FileSystem.Samples.Common.Windows.ShellExtension.Interop; using ITHit.FileSystem.Samples.Common.Windows.Rpc; using ITHit.FileSystem.Samples.Common.Windows.Rpc.Generated; using ITHit.FileSystem.Samples.Common.Windows.ShellExtension; @@ -14,12 +12,10 @@ namespace VirtualDrive.ShellExtension /// Thumbnails provider Windows Shell Extension. /// [ComVisible(true)] - [ProgId("VirtualDrive.ThumbnailProvider"), Guid(ThumbnailClass)] + [ProgId("VirtualDrive.ThumbnailProvider")] + [Guid("05CF065E-E135-4B2B-9D4D-CFB3FBAC73A4")] public class ThumbnailProvider : ThumbnailProviderBase { - public const string ThumbnailClass = "05CF065E-E135-4B2B-9D4D-CFB3FBAC73A4"; - public static readonly Guid ThumbnailClassGuid = Guid.Parse(ThumbnailClass); - public override async Task GetThumbnailsAsync(string filePath, uint size) { try diff --git a/Windows/VirtualDrive/VirtualDrive.ShellExtension/VirtualDrive.ShellExtension.csproj b/Windows/VirtualDrive/VirtualDrive.ShellExtension/VirtualDrive.ShellExtension.csproj index f8f60c3..c6b8c78 100644 --- a/Windows/VirtualDrive/VirtualDrive.ShellExtension/VirtualDrive.ShellExtension.csproj +++ b/Windows/VirtualDrive/VirtualDrive.ShellExtension/VirtualDrive.ShellExtension.csproj @@ -8,7 +8,7 @@ WinExe - Virtul Drive + Virtual Drive IT Hit LTD. IT Hit LTD. IT Hit LTD. @@ -40,7 +40,7 @@ - + diff --git a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/Common_Windows_WinRT_ShellExtension.exe.manifest b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/Common_Windows_WinRT_ShellExtension.exe.manifest index ed32231..6c00cd1 100644 --- a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/Common_Windows_WinRT_ShellExtension.exe.manifest +++ b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/Common_Windows_WinRT_ShellExtension.exe.manifest @@ -11,6 +11,21 @@ name="CommonShellExtensionRpc.ItemProperty" threadingModel="both" xmlns="urn:schemas-microsoft-com:winrt.v1" /> - + + + + + + + \ No newline at end of file diff --git a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/ShellExtensionModule.cpp b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/ShellExtensionModule.cpp index f2814b0..d0b9eb2 100644 --- a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/ShellExtensionModule.cpp +++ b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/ShellExtensionModule.cpp @@ -20,6 +20,11 @@ void ShellExtensionModule::Start() auto customStateProviderVirtualDrive = winrt::make>(); winrt::check_hresult(CoRegisterClassObject(CLSID_CustomStateProviderVirtualDrive, customStateProviderVirtualDrive.get(), CLSCTX_LOCAL_SERVER, REGCLS_MULTI_SEPARATE, &cookie)); + + cookie = 0; + auto uriSourceVirtualDrive = winrt::make>(); + winrt::check_hresult(CoRegisterClassObject(CLSID_UriSource, uriSourceVirtualDrive.get(), CLSCTX_LOCAL_SERVER, REGCLS_MULTI_SEPARATE, &cookie)); + } void ShellExtensionModule::Stop() diff --git a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/StorageProviderUriSourceStatus.cpp b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/StorageProviderUriSourceStatus.cpp new file mode 100644 index 0000000..a149c37 --- /dev/null +++ b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/StorageProviderUriSourceStatus.cpp @@ -0,0 +1,2 @@ +#include "pch.h" +#include "StorageProviderUriSourceStatus.h" diff --git a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/StorageProviderUriSourceStatus.h b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/StorageProviderUriSourceStatus.h new file mode 100644 index 0000000..ae2cdf1 --- /dev/null +++ b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/StorageProviderUriSourceStatus.h @@ -0,0 +1,5 @@ +#pragma once + +class StorageProviderUriSourceStatus +{ +}; diff --git a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/UriSource.cpp b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/UriSource.cpp new file mode 100644 index 0000000..6c8aa64 --- /dev/null +++ b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/UriSource.cpp @@ -0,0 +1,28 @@ +#include "pch.h" +#include "UriSource.h" + +using namespace winrt::Windows::Storage::Provider; + +namespace winrt::CommonWindowsRtShellExtenstion::implementation +{ + void UriSource::GetPathForContentUri(hstring const& contentUri, Windows::Storage::Provider::StorageProviderGetPathForContentUriResult const& result) + { + CommonShellExtensionRpc::UriSourceProxy uriSourceProxy; + + auto pathResult = uriSourceProxy.GetPathForContentUri(contentUri.c_str()); + + result.Path(pathResult.Path()); + result.Status((StorageProviderUriSourceStatus)pathResult.Status()); + } + + void UriSource::GetContentInfoForPath(hstring const& path, Windows::Storage::Provider::StorageProviderGetContentInfoForPathResult const& result) + { + CommonShellExtensionRpc::UriSourceProxy uriSourceProxy; + + auto pathResult = uriSourceProxy.GetContentInfoForPath(path); + + result.ContentId(pathResult.ContentId()); + result.ContentUri(pathResult.ContentUri()); + result.Status((StorageProviderUriSourceStatus)pathResult.Status()); + } +} diff --git a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/UriSource.h b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/UriSource.h new file mode 100644 index 0000000..8b76f26 --- /dev/null +++ b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/UriSource.h @@ -0,0 +1,27 @@ +#pragma once + +#include "CommonWindowsRtShellExtenstion.UriSource.g.h" +#include + +// {6D45BC7A-D0B7-4913-8984-FD7261550C08} +static const GUID CLSID_UriSource = +{ 0x6d45bc7a, 0xd0b7, 0x4913, { 0x89, 0x84, 0xfd, 0x72, 0x61, 0x55, 0xc, 0x8 } }; + + +namespace winrt::CommonWindowsRtShellExtenstion::implementation +{ + struct UriSource : UriSourceT + { + UriSource() = default; + + void GetPathForContentUri(_In_ hstring const& contentUri, _Out_ Windows::Storage::Provider::StorageProviderGetPathForContentUriResult const& result); + void GetContentInfoForPath(_In_ hstring const& path, _Out_ Windows::Storage::Provider::StorageProviderGetContentInfoForPathResult const& result); + }; +} + +namespace winrt::CommonWindowsRtShellExtenstion::factory_implementation +{ + struct UriSource : UriSourceT + { + }; +} diff --git a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/UriSource.idl b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/UriSource.idl new file mode 100644 index 0000000..8ed7a9c --- /dev/null +++ b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/UriSource.idl @@ -0,0 +1,7 @@ +namespace CommonWindowsRtShellExtenstion +{ + runtimeclass UriSource : [default] Windows.Storage.Provider.IStorageProviderUriSource + { + UriSource(); + } +} \ No newline at end of file diff --git a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/VirtualDrive.WinRT.ShellExtension.vcxproj b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/VirtualDrive.WinRT.ShellExtension.vcxproj index 3c9e07c..e3cc9a8 100644 --- a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/VirtualDrive.WinRT.ShellExtension.vcxproj +++ b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/VirtualDrive.WinRT.ShellExtension.vcxproj @@ -1,4 +1,4 @@ - + @@ -85,6 +85,9 @@ + + winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;cldapi.lib;onecore.lib;onecoreuap.lib;%(AdditionalDependencies) + @@ -101,6 +104,7 @@ true true + winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;cldapi.lib;onecore.lib;onecoreuap.lib;%(AdditionalDependencies) @@ -111,10 +115,14 @@ + + Create + + @@ -136,6 +144,7 @@ + diff --git a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/pch.h b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/pch.h index 81e55e0..203a095 100644 --- a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/pch.h +++ b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/pch.h @@ -6,6 +6,9 @@ #endif #include #include +#include +#include +#include #include #include @@ -18,4 +21,5 @@ #include -#include "CustomStateProvider.h" \ No newline at end of file +#include "CustomStateProvider.h" +#include "UriSource.h" \ No newline at end of file diff --git a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/x64/Debug/VirtualDrive.WinRT.ShellExtension.log b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/x64/Debug/VirtualDrive.WinRT.ShellExtension.log new file mode 100644 index 0000000..1f0a256 --- /dev/null +++ b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/x64/Debug/VirtualDrive.WinRT.ShellExtension.log @@ -0,0 +1,2 @@ +D:\Program Files\MSBuild\Current\Bin\Microsoft.Common.CurrentVersion.targets(1717,5): error : Project '..\..\Common\Rpc.Proto\Common.Windows.Rpc.Proto.csproj' targets 'net5.0-windows10.0.18362.0'. It cannot be referenced by a project that targets '.NETFramework,Version=v4.0'. +D:\Program Files\MSBuild\Current\Bin\Microsoft.Common.CurrentVersion.targets(1717,5): error : Project '..\..\Common\Rpc\Common.Windows.Rpc.csproj' targets 'net5.0-windows10.0.18362.0'. It cannot be referenced by a project that targets '.NETFramework,Version=v4.0'. diff --git a/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/x64/Debug/VirtualDrive.WinRT.ShellExtension.vcxproj.FileListAbsolute.txt b/Windows/VirtualDrive/VirtualDrive.WinRT.ShellExtension/x64/Debug/VirtualDrive.WinRT.ShellExtension.vcxproj.FileListAbsolute.txt new file mode 100644 index 0000000..e69de29 diff --git a/Windows/VirtualDrive/VirtualDrive/Program.cs b/Windows/VirtualDrive/VirtualDrive/Program.cs index b905902..23d25c7 100644 --- a/Windows/VirtualDrive/VirtualDrive/Program.cs +++ b/Windows/VirtualDrive/VirtualDrive/Program.cs @@ -15,7 +15,8 @@ using ITHit.FileSystem.Windows; using ITHit.FileSystem.Samples.Common; using ITHit.FileSystem.Samples.Common.Windows; - +using VirtualDrive.Common; +using System.Threading; namespace VirtualDrive { @@ -42,6 +43,10 @@ class Program /// public static VirtualEngine Engine; + private static CancellationTokenSource ctsProcessAsync = new CancellationTokenSource(); + private static CancellationToken ctProcessAsync = default; + + static async Task Main(string[] args) { // Load Settings. @@ -51,9 +56,6 @@ static async Task Main(string[] args) // Configure log4net and set log file path. LogFilePath = ConfigureLogger(); - // Enable UTF8 for Console Window. - Console.OutputEncoding = System.Text.Encoding.UTF8; - PrintHelp(); // Register sync root and create app folders. @@ -65,42 +67,41 @@ static async Task Main(string[] args) Logger.PrintHeader(log); - try + + using (Engine = new VirtualEngine( + Settings.UserFileSystemLicense, + Settings.UserFileSystemRootPath, + Settings.RemoteStorageRootPath, + Settings.IconsFolderPath, + Settings.RpcCommunicationChannelName, + Settings.SyncIntervalMs, + Settings.MaxDegreeOfParallelism, + log)) { - Engine = new VirtualEngine( - Settings.UserFileSystemLicense, - Settings.UserFileSystemRootPath, - Settings.RemoteStorageRootPath, - Settings.IconsFolderPath, - Settings.RpcCommunicationChannelName, - Settings.SyncIntervalMs, - Settings.MaxDegreeOfParallelism, - log); - Engine.AutoLock = Settings.AutoLock; - - // 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. - byte[] itemId = WindowsFileSystemItem.GetItemIdByPath(Settings.RemoteStorageRootPath); - Engine.Placeholders.GetItem(Settings.UserFileSystemRootPath).SetRemoteStorageItemId(itemId); - - // Start processing OS file system calls. - await Engine.StartAsync(); + try + { + Engine.AutoLock = Settings.AutoLock; + + // 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. + byte[] itemId = WindowsFileSystemItem.GetItemIdByPath(Settings.RemoteStorageRootPath); + Engine.Placeholders.GetRootItem().SetRemoteStorageItemId(itemId); + + // Start processing OS file system calls. + await Engine.StartAsync(); #if DEBUG - // Opens Windows File Manager with user file system folder and remote storage folder. - ShowTestEnvironment(); + // Opens Windows File Manager with user file system folder and remote storage folder. + ShowTestEnvironment(); #endif - // Keep this application running and reading user input. - await ProcessUserInputAsync(); - } - catch (Exception ex) - { - log.Error(ex); - await ProcessUserInputAsync(); - } - finally - { - Engine.Dispose(); + // Keep this application running and reading user input. + await ProcessUserInputAsync(); + } + catch (Exception ex) + { + log.Error(ex); + await ProcessUserInputAsync(); + } } } @@ -133,7 +134,8 @@ private static void PrintHelp() log.Info("\n\nPress Esc to unregister file system, delete all files/folders and exit (simulate uninstall)."); log.Info("\nPress Spacebar to exit without unregistering (simulate reboot)."); log.Info("\nPress 'e' to start/stop the Engine and all sync services."); - log.Info("\nPress 's' to start/stop full synchronization service."); + //log.Info("\nPress 's' to start/stop full synchronization service."); + //log.Info("\nPress 'u' to start/stop user file system to remote storage sync (EngineWindows.ProcessAsync())."); log.Info("\nPress 'm' to start/stop remote storage monitor."); log.Info($"\nPress 'l' to open log file. ({LogFilePath})"); log.Info($"\nPress 'b' to submit support tickets, report bugs, suggest features. (https://userfilesystem.com/support/)"); @@ -163,24 +165,45 @@ private static async Task ProcessUserInputAsync() await Engine.StartAsync(); } break; - - case 's': + /* + case 's': + // Start/stop full synchronization. + if (Engine.SyncService.SyncState == SynchronizationState.Disabled) + { + if(Engine.State != EngineState.Running) + { + Engine.SyncService.LogError("Failed to start. The Engine must be running."); + break; + } + await Engine.SyncService.StartAsync(); + } + else + { + await Engine.SyncService.StopAsync(); + } + break; + */ + /* + case 'u': // Start/stop full synchronization. - if (Engine.SyncService.SyncState == SynchronizationState.Disabled) + if (!ctProcessAsync.CanBeCanceled) { - if(Engine.State != EngineState.Running) + if (Engine.State != EngineState.Running) { - Engine.SyncService.LogError("Failed to start. The Engine must be running."); + Engine.LogError("Failed to start. The Engine must be running."); break; } - await Engine.SyncService.StartAsync(); + ctProcessAsync = ctsProcessAsync.Token; + await Engine.ProcessAsync(ctProcessAsync); + ctProcessAsync = default; } else { - await Engine.SyncService.StopAsync(); + ctsProcessAsync.Cancel(); + ctProcessAsync = default; } break; - + */ case 'm': // Start/stop remote storage monitor. if (Engine.RemoteStorageMonitor.SyncState == SynchronizationState.Disabled) @@ -225,7 +248,10 @@ private static async Task ProcessUserInputAsync() case (char)ConsoleKey.Escape: case 'Q': - Engine.Dispose(); + if (Engine.State == EngineState.Running) + { + await Engine.StopAsync(); + } // Call the code below during programm uninstall using classic msi. await UnregisterSyncRootAsync(); @@ -235,6 +261,10 @@ private static async Task ProcessUserInputAsync() return; case (char)ConsoleKey.Spacebar : + if (Engine.State == EngineState.Running) + { + await Engine.StopAsync(); + } log.Info("\n\nAll downloaded file / folder placeholders remain in file system. Restart the application to continue managing files.\n"); return; @@ -262,6 +292,9 @@ private static async Task RegisterSyncRootAsync() await Registrar.RegisterAsync(SyncRootId, Settings.UserFileSystemRootPath, Settings.ProductName, Path.Combine(Settings.IconsFolderPath, "Drive.ico")); + + log.Info("\nRegistering shell extensions...\n"); + ShellExtensionRegistrar.Register(SyncRootId); } else { @@ -285,6 +318,9 @@ private static async Task UnregisterSyncRootAsync() { log.Info($"\n\nUnregistering {Settings.UserFileSystemRootPath} sync root."); await Registrar.UnregisterAsync(SyncRootId); + + log.Info("\nUnregistering shell extensions...\n"); + ShellExtensionRegistrar.Unregister(); } private static async Task CleanupAppFoldersAsync() @@ -316,6 +352,11 @@ private static async Task CleanupAppFoldersAsync() /// This method is provided solely for the development and testing convenience. private static void ShowTestEnvironment() { + // 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, Console.BufferHeight); + // Open Windows File Manager with remote storage. ProcessStartInfo rsInfo = new ProcessStartInfo(Program.Settings.RemoteStorageRootPath); rsInfo.UseShellExecute = true; // Open window only if not opened already. diff --git a/Windows/VirtualDrive/VirtualDrive/RemoteStorage/General.docx b/Windows/VirtualDrive/VirtualDrive/RemoteStorage/General.docx index a29e91d..e6f73c1 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/RemoteStorage/Notes.txt b/Windows/VirtualDrive/VirtualDrive/RemoteStorage/Notes.txt index 6d07837..45a4503 100644 --- a/Windows/VirtualDrive/VirtualDrive/RemoteStorage/Notes.txt +++ b/Windows/VirtualDrive/VirtualDrive/RemoteStorage/Notes.txt @@ -1 +1 @@ -My notes file text s \ No newline at end of file +My notes file text \ No newline at end of file diff --git a/Windows/VirtualDrive/VirtualDrive/ShellExtensionRegistrar.cs b/Windows/VirtualDrive/VirtualDrive/ShellExtensionRegistrar.cs new file mode 100644 index 0000000..a63ab79 --- /dev/null +++ b/Windows/VirtualDrive/VirtualDrive/ShellExtensionRegistrar.cs @@ -0,0 +1,32 @@ +using ITHit.FileSystem.Samples.Common.Windows; +using System.IO; +using System.Reflection; +using VirtualDrive.ShellExtension; +using CommonShellExtension = ITHit.FileSystem.Samples.Common.Windows.ShellExtension; + +namespace VirtualDrive +{ + internal class ShellExtensionRegistrar + { + private static readonly string ComServerRelativePath = @"VirtualDrive.ShellExtension.exe"; + + public static void Register(string syncRootId) + { + if (!Registrar.IsRunningAsUwp()) + { + string comServerPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), ComServerRelativePath)); + CommonShellExtension.ShellExtensionRegistrar.Register(syncRootId, "ThumbnailProvider", typeof(ThumbnailProvider).GUID, comServerPath); + CommonShellExtension.ShellExtensionRegistrar.Register(syncRootId, "MenuVerbHandler_0", typeof(ContextMenusProvider).GUID, comServerPath); + } + } + + public static void Unregister() + { + if (!Registrar.IsRunningAsUwp()) + { + CommonShellExtension.ShellExtensionRegistrar.Unregister(typeof(ThumbnailProvider).GUID); + CommonShellExtension.ShellExtensionRegistrar.Unregister(typeof(ContextMenusProvider).GUID); + } + } + } +} diff --git a/Windows/VirtualDrive/VirtualDrive/VirtualDrive.csproj b/Windows/VirtualDrive/VirtualDrive/VirtualDrive.csproj index aca8227..2e75246 100644 --- a/Windows/VirtualDrive/VirtualDrive/VirtualDrive.csproj +++ b/Windows/VirtualDrive/VirtualDrive/VirtualDrive.csproj @@ -42,6 +42,8 @@ This is an advanced project with ETags support, Microsoft Office documents editi + + diff --git a/Windows/VirtualDrive/VirtualDrive/VirtualEngine.cs b/Windows/VirtualDrive/VirtualDrive/VirtualEngine.cs index 6230033..048513e 100644 --- a/Windows/VirtualDrive/VirtualDrive/VirtualEngine.cs +++ b/Windows/VirtualDrive/VirtualDrive/VirtualEngine.cs @@ -7,6 +7,7 @@ using ITHit.FileSystem.Windows; using ITHit.FileSystem.Samples.Common; using ITHit.FileSystem.Samples.Common.Windows; +using System.Threading; namespace VirtualDrive { @@ -62,9 +63,9 @@ public override async Task GetFileSystemItemAsync(string userFi //public override IMapping Mapping { get { return new Mapping(this); } } /// - public override async Task StartAsync(bool processModified = true) + public override async Task StartAsync(bool processModified = true, CancellationToken cancellationToken = default) { - await base.StartAsync(processModified); + await base.StartAsync(processModified, cancellationToken); RemoteStorageMonitor.Start(); } @@ -79,6 +80,15 @@ public override async Task GetThumbnailAsync(string userFileSystemPath, { // For this method to be called you need to run the Package project. + /* + // Get remote storage ID to read thumbnail from the remote storage. + if (PlaceholderItem.IsPlaceholder(userFileSystemPath)) + { + PlaceholderItem placeholder = this.Placeholders.GetItem(userFileSystemPath); + byte[] remoteStorageItemId = placeholder.GetRemoteStorageItemId(); + } + */ + byte[] thumbnail = ThumbnailExtractor.GetThumbnail(userFileSystemPath, size); string thumbnailResult = thumbnail != null ? "Success" : "Not Impl"; @@ -96,60 +106,62 @@ public override async Task> GetItemPrope IList props = new List(); - PlaceholderItem placeholder = this.Placeholders.GetItem(userFileSystemPath); - - // Read LockInfo. - if (placeholder.Properties.TryGetValue("LockInfo", out IDataItem propLockInfo)) + if (this.Placeholders.TryGetItem(userFileSystemPath, out PlaceholderItem placeholder)) { - if (propLockInfo.TryGetValue(out ServerLockInfo lockInfo)) + + // Read LockInfo. + if (placeholder.Properties.TryGetValue("LockInfo", out IDataItem propLockInfo)) { - // Get Lock Owner. - FileSystemItemPropertyData propertyLockOwner = new FileSystemItemPropertyData() + if (propLockInfo.TryGetValue(out ServerLockInfo lockInfo)) { - Id = (int)CustomColumnIds.LockOwnerIcon, - Value = lockInfo.Owner, - IconResource = System.IO.Path.Combine(this.IconsFolderPath, "Locked.ico") - }; - props.Add(propertyLockOwner); - - // Get Lock Expires. - FileSystemItemPropertyData propertyLockExpires = new FileSystemItemPropertyData() - { - Id = (int)CustomColumnIds.LockExpirationDate, - Value = lockInfo.LockExpirationDateUtc.ToString(), - IconResource = System.IO.Path.Combine(this.IconsFolderPath, "Empty.ico") - }; - props.Add(propertyLockExpires); + // Get Lock Owner. + FileSystemItemPropertyData propertyLockOwner = new FileSystemItemPropertyData() + { + Id = (int)CustomColumnIds.LockOwnerIcon, + Value = lockInfo.Owner, + IconResource = System.IO.Path.Combine(this.IconsFolderPath, "Locked.ico") + }; + props.Add(propertyLockOwner); + + // Get Lock Expires. + FileSystemItemPropertyData propertyLockExpires = new FileSystemItemPropertyData() + { + Id = (int)CustomColumnIds.LockExpirationDate, + Value = lockInfo.LockExpirationDateUtc.ToString(), + IconResource = System.IO.Path.Combine(this.IconsFolderPath, "Empty.ico") + }; + props.Add(propertyLockExpires); + } } - } - // Read LockMode. - if (placeholder.Properties.TryGetValue("LockMode", out IDataItem propLockMode)) - { - if (propLockMode.TryGetValue(out LockMode lockMode) && lockMode != LockMode.None) + // Read LockMode. + if (placeholder.Properties.TryGetValue("LockMode", out IDataItem propLockMode)) { - FileSystemItemPropertyData propertyLockMode = new FileSystemItemPropertyData() + if (propLockMode.TryGetValue(out LockMode lockMode) && lockMode != LockMode.None) { - Id = (int)CustomColumnIds.LockScope, - Value = "Locked", - IconResource = System.IO.Path.Combine(this.IconsFolderPath, "Empty.ico") - }; - props.Add(propertyLockMode); + FileSystemItemPropertyData propertyLockMode = new FileSystemItemPropertyData() + { + Id = (int)CustomColumnIds.LockScope, + Value = "Locked", + IconResource = System.IO.Path.Combine(this.IconsFolderPath, "Empty.ico") + }; + props.Add(propertyLockMode); + } } - } - // Read ETag. - if (placeholder.Properties.TryGetValue("ETag", out IDataItem propETag)) - { - if (propETag.TryGetValue(out string eTag)) + // Read ETag. + if (placeholder.Properties.TryGetValue("ETag", out IDataItem propETag)) { - FileSystemItemPropertyData propertyETag = new FileSystemItemPropertyData() + if (propETag.TryGetValue(out string eTag)) { - Id = (int)CustomColumnIds.ETag, - Value = eTag, - IconResource = System.IO.Path.Combine(this.IconsFolderPath, "Empty.ico") - }; - props.Add(propertyETag); + FileSystemItemPropertyData propertyETag = new FileSystemItemPropertyData() + { + Id = (int)CustomColumnIds.ETag, + Value = eTag, + IconResource = System.IO.Path.Combine(this.IconsFolderPath, "Empty.ico") + }; + props.Add(propertyETag); + } } } diff --git a/Windows/VirtualDrive/VirtualDrive/VirtualFile.cs b/Windows/VirtualDrive/VirtualDrive/VirtualFile.cs index dbe440a..40ede6a 100644 --- a/Windows/VirtualDrive/VirtualDrive/VirtualFile.cs +++ b/Windows/VirtualDrive/VirtualDrive/VirtualFile.cs @@ -13,7 +13,7 @@ namespace VirtualDrive { /// - public class VirtualFile : VirtualFileSystemItem, IFile + public class VirtualFile : VirtualFileSystemItem, IFileWindows { /// @@ -29,33 +29,41 @@ public VirtualFile(string path, byte[] remoteStorageItemId, VirtualEngine engine } /// - public async Task OpenAsync(IOperationContext operationContext, IResultContext context) + public async Task OpenCompletionAsync(IOperationContext operationContext, IResultContext context, CancellationToken cancellationToken) { - Logger.LogMessage($"{nameof(IFile)}.{nameof(OpenAsync)}()", UserFileSystemPath, default, operationContext); + Logger.LogMessage($"{nameof(IFileWindows)}.{nameof(OpenCompletionAsync)}()", UserFileSystemPath, default, operationContext); } /// - public async Task CloseAsync(IOperationContext operationContext, IResultContext context) + public async Task CloseCompletionAsync(IOperationContext operationContext, IResultContext context, CancellationToken cancellationToken) { - Logger.LogMessage($"{nameof(IFile)}.{nameof(CloseAsync)}()", UserFileSystemPath, default, operationContext); + Logger.LogMessage($"{nameof(IFileWindows)}.{nameof(CloseCompletionAsync)}()", UserFileSystemPath, default, operationContext); } /// - public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext) + 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 call the resultContext.ReportProgress() or resultContext.ReturnData() method. Logger.LogMessage($"{nameof(IFile)}.{nameof(ReadAsync)}({offset}, {length})", UserFileSystemPath, default, operationContext); - SimulateNetworkDelay(length, resultContext); + cancellationToken.Register(() => { Logger.LogMessage($"{nameof(IFile)}.{nameof(ReadAsync)}({offset}, {length}) cancelled", UserFileSystemPath, default, operationContext); }); string remoteStoragePath = Mapping.GetRemoteStoragePathById(RemoteStorageItemId); await using (FileStream stream = System.IO.File.OpenRead(remoteStoragePath)) { stream.Seek(offset, SeekOrigin.Begin); const int bufferSize = 0x500000; // 5Mb. Buffer size must be multiple of 4096 bytes for optimal performance. - await stream.CopyToAsync(output, bufferSize, length); + try + { + await stream.CopyToAsync(output, bufferSize, length, cancellationToken); + } + catch (OperationCanceledException) + { + // Operation was canceled by the calling Engine.StopAsync() or the operation timeout occured. + //Logger.LogMessage($"{nameof(IFile)}.{nameof(ReadAsync)}({offset}, {length}) canceled", UserFileSystemPath, default); + } } // Save ETag received from your remote storage in persistent placeholder properties. @@ -72,33 +80,31 @@ public async Task ValidateDataAsync(long offset, long length, IValidateDataOpera Logger.LogMessage($"{nameof(IFile)}.{nameof(ValidateDataAsync)}({offset}, {length})", UserFileSystemPath, default, operationContext); - //SimulateNetworkDelay(length, resultContext); - bool isValid = true; resultContext.ReturnValidationResult(offset, length, isValid); } /// - public async Task WriteAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null) + public async Task WriteAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(IFile)}.{nameof(WriteAsync)}()", UserFileSystemPath, default, operationContext); - PlaceholderItem placeholder = Engine.Placeholders.GetItem(UserFileSystemPath); - // 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. //string oldEtag = await placeholder.Properties["ETag"].GetValueAsync(); // Send the lock-token to the server as part of the update. string lockToken; - IDataItem propLockInfo; - if (placeholder.Properties.TryGetValue("LockInfo", out propLockInfo)) + if (Engine.Placeholders.TryGetItem(UserFileSystemPath, out PlaceholderItem placeholder)) { - ServerLockInfo lockInfo; - if (propLockInfo.TryGetValue(out lockInfo)) + if (placeholder.Properties.TryGetValue("LockInfo", out IDataItem propLockInfo)) { - lockToken = lockInfo.LockToken; + ServerLockInfo lockInfo; + if (propLockInfo.TryGetValue(out lockInfo)) + { + lockToken = lockInfo.LockToken; + } } } diff --git a/Windows/VirtualDrive/VirtualDrive/VirtualFileSystemItem.cs b/Windows/VirtualDrive/VirtualDrive/VirtualFileSystemItem.cs index c693439..a89275b 100644 --- a/Windows/VirtualDrive/VirtualDrive/VirtualFileSystemItem.cs +++ b/Windows/VirtualDrive/VirtualDrive/VirtualFileSystemItem.cs @@ -51,7 +51,7 @@ public VirtualFileSystemItem(string userFileSystemPath, byte[] remoteStorageItem } /// - public async Task MoveToAsync(string targetUserFileSystemPath, byte[] targetParentItemId, IOperationContext operationContext = null, IConfirmationResultContext resultContext = null) + public async Task MoveToAsync(string targetUserFileSystemPath, byte[] targetParentItemId, IOperationContext operationContext = null, IConfirmationResultContext resultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewPath = targetUserFileSystemPath; string userFileSystemOldPath = this.UserFileSystemPath; @@ -59,7 +59,7 @@ public async Task MoveToAsync(string targetUserFileSystemPath, byte[] targetPare } /// - public async Task MoveToCompletionAsync(string targetUserFileSystemPath, byte[] targetFolderRemoteStorageItemId, IMoveCompletionContext operationContext = null, IInSyncStatusResultContext resultContext = null) + public async Task MoveToCompletionAsync(string targetUserFileSystemPath, byte[] targetFolderRemoteStorageItemId, IMoveCompletionContext operationContext = null, IInSyncStatusResultContext resultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewPath = targetUserFileSystemPath; string userFileSystemOldPath = this.UserFileSystemPath; @@ -91,12 +91,13 @@ public async Task MoveToCompletionAsync(string targetUserFileSystemPath, byte[] } /// - public async Task DeleteAsync(IOperationContext operationContext, IConfirmationResultContext resultContext) + public async Task DeleteAsync(IOperationContext operationContext, IConfirmationResultContext resultContext, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(IFileSystemItem)}.{nameof(DeleteAsync)}()", UserFileSystemPath, default, operationContext); // To cancel the operation and prevent the file from being deleted, - // call the resultContext.ReturnErrorResult() method or throw any exception inside this method. + // call the resultContext.ReturnErrorResult() method or throw any exception inside this method: + // resultContext.ReturnErrorResult(CloudFileStatus.STATUS_CLOUD_FILE_REQUEST_TIMEOUT); // IMPOTRTANT! See Windows Cloud API delete prevention bug description here: // https://stackoverflow.com/questions/68887190/delete-in-cloud-files-api-stopped-working-on-windows-21h1 @@ -106,7 +107,7 @@ public async Task DeleteAsync(IOperationContext operationContext, IConfirmationR } /// - public async Task DeleteCompletionAsync(IOperationContext operationContext, IInSyncStatusResultContext resultContext) + public async Task DeleteCompletionAsync(IOperationContext operationContext, IInSyncStatusResultContext resultContext, CancellationToken cancellationToken = default) { // On Windows, for move with overwrite on folders to function correctly, // the deletion of the folder in the remote storage must be done in DeleteCompletionAsync() @@ -147,26 +148,8 @@ public Task GetMetadataAsync() throw new NotImplementedException(); } - /// - /// Simulates network delays and reports file transfer progress for demo purposes. - /// - /// Length of file. - /// Context to report progress to. - protected void SimulateNetworkDelay(long fileLength, IResultContext resultContext) - { - if (Program.Settings.NetworkSimulationDelayMs > 0) - { - int numProgressResults = 5; - for (int i = 0; i < numProgressResults; i++) - { - resultContext.ReportProgress(fileLength, i * fileLength / numProgressResults); - Thread.Sleep(Program.Settings.NetworkSimulationDelayMs); - } - } - } - /// - public async Task LockAsync(LockMode lockMode, IOperationContext operationContext = null) + public async Task LockAsync(LockMode lockMode, IOperationContext operationContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(ILock)}.{nameof(LockAsync)}()", UserFileSystemPath, default, operationContext); @@ -191,37 +174,37 @@ public async Task LockAsync(LockMode lockMode, IOperationContext operationContex } /// - public async Task GetLockModeAsync(IOperationContext operationContext = null) + public async Task GetLockModeAsync(IOperationContext operationContext = null, CancellationToken cancellationToken = default) { - PlaceholderItem placeholder = Engine.Placeholders.GetItem(UserFileSystemPath); - - IDataItem property; - if(placeholder.Properties.TryGetValue("LockMode", out property)) - { - return await property.GetValueAsync(); - } - else + if (Engine.Placeholders.TryGetItem(UserFileSystemPath, out PlaceholderItem placeholder)) { - return LockMode.None; + if (placeholder.Properties.TryGetValue("LockMode", out IDataItem property)) + { + return await property.GetValueAsync(); + } } + + return LockMode.None; } /// - public async Task UnlockAsync(IOperationContext operationContext = null) + public async Task UnlockAsync(IOperationContext operationContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(ILock)}.{nameof(UnlockAsync)}()", UserFileSystemPath, default, operationContext); + + if (Engine.Placeholders.TryGetItem(UserFileSystemPath, out PlaceholderItem placeholder)) + { + // Read the lock-token. + string lockToken = (await placeholder.Properties["LockInfo"].GetValueAsync())?.LockToken; - // Read the lock-token. - PlaceholderItem placeholder = Engine.Placeholders.GetItem(UserFileSystemPath); - string lockToken = (await placeholder.Properties["LockInfo"].GetValueAsync())?.LockToken; - - // Unlock the item in the remote storage here. + // Unlock the item in the remote storage here. - // Delete lock-mode and lock-token info. - placeholder.Properties.Remove("LockInfo"); - placeholder.Properties.Remove("LockMode"); + // Delete lock-mode and lock-token info. + placeholder.Properties.Remove("LockInfo"); + placeholder.Properties.Remove("LockMode"); - Logger.LogMessage("Unlocked in the remote storage succesefully", UserFileSystemPath, default, operationContext); + Logger.LogMessage("Unlocked in the remote storage succesefully", UserFileSystemPath, default, operationContext); + } } } } diff --git a/Windows/VirtualDrive/VirtualDrive/VirtualFolder.cs b/Windows/VirtualDrive/VirtualDrive/VirtualFolder.cs index 65093d6..4aacb95 100644 --- a/Windows/VirtualDrive/VirtualDrive/VirtualFolder.cs +++ b/Windows/VirtualDrive/VirtualDrive/VirtualFolder.cs @@ -30,7 +30,7 @@ public VirtualFolder(string path, byte[] remoteStorageItemId, VirtualEngine engi } /// - public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IInSyncResultContext inSyncResultContext = null) + public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewItemPath = Path.Combine(UserFileSystemPath, fileMetadata.Name); Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFileAsync)}()", userFileSystemNewItemPath); @@ -43,7 +43,15 @@ public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream con { if (content != null) { - await content.CopyToAsync(remoteStorageStream); + try + { + await content.CopyToAsync(remoteStorageStream, cancellationToken); + } + catch (OperationCanceledException) + { + // Operation was canceled by the calling Engine.StopAsync() or the operation timeout occured. + Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFileAsync)}() canceled", UserFileSystemPath, default); + } remoteStorageStream.SetLength(content.Length); } } @@ -67,7 +75,7 @@ public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream con } /// - public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IInSyncResultContext inSyncResultContext = null) + public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewItemPath = Path.Combine(UserFileSystemPath, folderMetadata.Name); Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFolderAsync)}()", userFileSystemNewItemPath); @@ -94,7 +102,7 @@ public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IInS } /// - public async Task GetChildrenAsync(string pattern, IOperationContext operationContext, IFolderListingResultContext resultContext) + public async Task GetChildrenAsync(string pattern, IOperationContext operationContext, IFolderListingResultContext resultContext, CancellationToken cancellationToken) { // This method has a 60 sec timeout. // To process longer requests and reset the timout timer call one of the following: @@ -103,24 +111,17 @@ public async Task GetChildrenAsync(string pattern, IOperationContext operationCo Logger.LogMessage($"{nameof(IFolder)}.{nameof(GetChildrenAsync)}({pattern})", UserFileSystemPath, default, operationContext); - IEnumerable remoteStorageChildren = await EnumerateChildrenAsync(pattern); + cancellationToken.Register(() => { Logger.LogMessage($"{nameof(IFolder)}.{nameof(GetChildrenAsync)}({pattern}) cancelled", UserFileSystemPath, default, operationContext); }); - List userFileSystemChildren = new List(); - foreach (FileSystemItemMetadataExt itemMetadata in remoteStorageChildren) - { - string userFileSystemItemPath = Path.Combine(UserFileSystemPath, itemMetadata.Name); + var watch = System.Diagnostics.Stopwatch.StartNew(); + IEnumerable remoteStorageChildren = await EnumerateChildrenAsync(pattern, cancellationToken); - // Filtering existing files/folders. This is only required to avoid extra errors in the log. - if (!FsPath.Exists(userFileSystemItemPath)) - { - Logger.LogMessage("Creating", userFileSystemItemPath); - userFileSystemChildren.Add(itemMetadata); - } - } + long totalCount = remoteStorageChildren.Count(); // 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(remoteStorageChildren.ToArray(), totalCount); + Engine.LogMessage($"Listed {totalCount} item(s). Took: {watch.ElapsedMilliseconds:N0}ms", UserFileSystemPath); // Save data that you wish to display in custom columns here. //foreach (FileSystemItemMetadataExt itemMetadata in userFileSystemChildren) @@ -131,22 +132,25 @@ public async Task GetChildrenAsync(string pattern, IOperationContext operationCo //} } - public async Task> EnumerateChildrenAsync(string pattern) + public async Task> EnumerateChildrenAsync(string pattern, CancellationToken cancellationToken) { string remoteStoragePath = Mapping.GetRemoteStoragePathById(RemoteStorageItemId); IEnumerable remoteStorageChildren = new DirectoryInfo(remoteStoragePath).EnumerateFileSystemInfos(pattern); - List userFileSystemChildren = new List(); + var userFileSystemChildren = new System.Collections.Concurrent.ConcurrentBag(); + + //Parallel.ForEach(remoteStorageChildren, new ParallelOptions() { CancellationToken = cancellationToken }, async (remoteStorageItem) => foreach (FileSystemInfo remoteStorageItem in remoteStorageChildren) { FileSystemItemMetadataExt itemInfo = Mapping.GetUserFileSysteItemMetadata(remoteStorageItem); userFileSystemChildren.Add(itemInfo); } + return userFileSystemChildren; } /// - public async Task WriteAsync(IFolderMetadata folderMetadata, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null) + public async Task WriteAsync(IFolderMetadata folderMetadata, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(IFolder)}.{nameof(WriteAsync)}()", UserFileSystemPath, default, operationContext); diff --git a/Windows/VirtualDrive/VirtualDrive/appsettings.json b/Windows/VirtualDrive/VirtualDrive/appsettings.json index 76f8cd5..d84c0bb 100644 --- a/Windows/VirtualDrive/VirtualDrive/appsettings.json +++ b/Windows/VirtualDrive/VirtualDrive/appsettings.json @@ -4,40 +4,48 @@ // (aka Corporate Drive and Personal Drive) set a unique ID for each instance. "AppID": "VirtualDrive", - // License to activate the IT Hit User File System Engine. If no license is specified the Engine will be activated + + // License to activate the IT Hit User File System Engine. + // Set the license content directly as value. Make sure to escape quotes: \": + // "UserFileSystemLicense": " 0 the file download is delayd to demonstrate file transfer progress. - // Set this parameter to 0 to avoid any network simulation delays. - "NetworkSimulationDelayMs": 0, // Automatically lock the file in the remote storage when a file handle is being opened for writing, unlock on close. "AutoLock": true, + // To test performance: // 1. Compile the project in the release mode. // 2. Run without debugger arrached (Ctrl-F5). // 3. Set the VerboseLogging to false. "VerboseLogging": true, + // Communication channel name is used by RPC to establish connection over named pipes. // The channel is used to comunicate between the main app and COM thumbnails handler, Win Explorer context menu, etc. "RpcCommunicationChannelName": "VirtualDrive.RPC", + // Gets or sets the maximum number of concurrent tasks "MaxDegreeOfParallelism": 1000 } \ No newline at end of file diff --git a/Windows/VirtualFileSystem/Mapping.cs b/Windows/VirtualFileSystem/Mapping.cs index e758a49..83dc1b2 100644 --- a/Windows/VirtualFileSystem/Mapping.cs +++ b/Windows/VirtualFileSystem/Mapping.cs @@ -15,20 +15,41 @@ namespace VirtualFileSystem /// /// You will change methods of this class to map to your own remote storage. /// - public static class Mapping + public class Mapping { + /// + /// Remote storage path. + /// + private readonly string remoteStorageRootPath; + + /// + /// User file system path. + /// + private readonly string userFileSystemRootPath; + + /// + /// Creates a Mapping. + /// + /// Remote storage path. + /// User file system path. + public Mapping(string userFileSystemRootPath, string remoteStorageRootPath) + { + this.userFileSystemRootPath = userFileSystemRootPath; + this.remoteStorageRootPath = remoteStorageRootPath; + } + /// /// Returns a user file system path that corresponds to the remote storage URI. /// /// Remote storage URI. /// Path in the user file system that corresponds to the . - public static string ReverseMapPath(string remoteStorageUri) + public string ReverseMapPath(string remoteStorageUri) { // Get path relative to the virtual root. string relativePath = Path.TrimEndingDirectorySeparator(remoteStorageUri).Substring( - Path.TrimEndingDirectorySeparator(Program.Settings.RemoteStorageRootPath).Length); + Path.TrimEndingDirectorySeparator(remoteStorageRootPath).Length); - string path = $"{Path.TrimEndingDirectorySeparator(Program.Settings.UserFileSystemRootPath)}{relativePath}"; + string path = $"{Path.TrimEndingDirectorySeparator(userFileSystemRootPath)}{relativePath}"; return path; } diff --git a/Windows/VirtualFileSystem/Program.cs b/Windows/VirtualFileSystem/Program.cs index db82229..c67c9eb 100644 --- a/Windows/VirtualFileSystem/Program.cs +++ b/Windows/VirtualFileSystem/Program.cs @@ -19,12 +19,22 @@ namespace VirtualFileSystem { - class Program + public class Program { /// /// Application settings. /// - internal static AppSettings Settings; + public static AppSettings Settings; + + /// + /// Process modified files and folders. + /// + public static bool ProcessModified = true; + + /// + /// Opens remote and virtual files folders + /// + public static bool OpenTestEnvironment = true; /// /// Log4Net logger. @@ -42,7 +52,7 @@ class Program /// public static VirtualEngine Engine; - static async Task Main(string[] args) + public static async Task Main(string[] args) { // Load Settings. IConfiguration configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json", false, true).Build(); @@ -51,9 +61,6 @@ static async Task Main(string[] args) // Configure log4net and set log file path. LogFilePath = ConfigureLogger(); - // Enable UTF8 for Console Window. - Console.OutputEncoding = System.Text.Encoding.UTF8; - PrintHelp(); // Register sync root and create app folders. @@ -65,38 +72,36 @@ static async Task Main(string[] args) Logger.PrintHeader(log); - try - { - Engine = new VirtualEngine( - Settings.UserFileSystemLicense, - Settings.UserFileSystemRootPath, + + using (Engine = new VirtualEngine( + Settings.UserFileSystemLicense, + Settings.UserFileSystemRootPath, Settings.RemoteStorageRootPath, Settings.MaxDegreeOfParallelism, - log); - - // 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. - byte[] itemId = WindowsFileSystemItem.GetItemIdByPath(Settings.RemoteStorageRootPath); - Engine.Placeholders.GetItem(Settings.UserFileSystemRootPath).SetRemoteStorageItemId(itemId); + log)) + { + try + { + // 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. + byte[] itemId = WindowsFileSystemItem.GetItemIdByPath(Settings.RemoteStorageRootPath); + Engine.Placeholders.GetRootItem().SetRemoteStorageItemId(itemId); - // Start processing OS file system calls. - await Engine.StartAsync(); + // Start processing OS file system calls. + await Engine.StartAsync(ProcessModified); #if DEBUG - // Opens Windows File Manager with user file system folder and remote storage folder. - ShowTestEnvironment(); + // Opens Windows File Manager with user file system folder and remote storage folder. + ShowTestEnvironment(); #endif - // Keep this application running and reading user input. - await ProcessUserInputAsync(); - } - catch (Exception ex) - { - log.Error(ex); - await ProcessUserInputAsync(); - } - finally - { - Engine.Dispose(); + // Keep this application running and reading user input. + await ProcessUserInputAsync(); + } + catch (Exception ex) + { + log.Error(ex); + await ProcessUserInputAsync(); + } } } @@ -204,7 +209,10 @@ private static async Task ProcessUserInputAsync() case (char)ConsoleKey.Escape: case 'Q': - Engine.Dispose(); + if (Engine.State == EngineState.Running) + { + await Engine.StopAsync(); + } // Call the code below during programm uninstall using classic msi. await UnregisterSyncRootAsync(); @@ -214,6 +222,10 @@ private static async Task ProcessUserInputAsync() return; case (char)ConsoleKey.Spacebar: + if (Engine.State == EngineState.Running) + { + await Engine.StopAsync(); + } log.Info("\n\nAll downloaded file / folder placeholders remain in file system. Restart the application to continue managing files.\n"); return; @@ -260,13 +272,13 @@ await Registrar.RegisterAsync(SyncRootId, Settings.UserFileSystemRootPath, Setti /// In the case of a regular installer (msi) call this method during uninstall. /// /// - private static async Task UnregisterSyncRootAsync() + public static async Task UnregisterSyncRootAsync() { log.Info($"\n\nUnregistering {Settings.UserFileSystemRootPath} sync root."); await Registrar.UnregisterAsync(SyncRootId); } - private static async Task CleanupAppFoldersAsync() + public static async Task CleanupAppFoldersAsync() { log.Info("\nDeleting all file and folder placeholders.\n"); try @@ -280,7 +292,7 @@ private static async Task CleanupAppFoldersAsync() try { - await((EngineWindows)Engine).UninstallCleanupAsync(); + await ((EngineWindows)Engine).UninstallCleanupAsync(); } catch (Exception ex) { @@ -295,6 +307,10 @@ private static async Task CleanupAppFoldersAsync() /// This method is provided solely for the development and testing convenience. private static void ShowTestEnvironment() { + // 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, Console.BufferHeight); // Open Windows File Manager with remote storage. ProcessStartInfo rsInfo = new ProcessStartInfo(Program.Settings.RemoteStorageRootPath); diff --git a/Windows/VirtualFileSystem/README.md b/Windows/VirtualFileSystem/README.md index ea8ba07..38cc850 100644 --- a/Windows/VirtualFileSystem/README.md +++ b/Windows/VirtualFileSystem/README.md @@ -6,7 +6,7 @@

You can download this sample and a trial license in the product download area. You can also clone it and browse the code on GitHub

Requirements

    -
  • .NET 5.0 or later.
  • +
  • .NET 6.0 or later.
  • Microsoft Windows 10 Creators Update or later version.
  • NTFS file system.
@@ -14,7 +14,8 @@

By default, the sample will use the \RemoteStorage\ folder, located under the project root, to simulate the remote storage file structure. To specify a different folder edit the "RemoteStorageRootPath" parameter in appsettings.json. This could be either an absolute path or a path relative to the application root.

This sample mounts the user file system under the %USERPROFILE%\VFS\ folder (typically C:\Users\<username>\VFS\). To specify a different folder edit the "UserFileSystemRootPath" parameter in appsettings.json.

Setting the License

-

To run the example, you will need a valid IT Hit User File System Engine for .NET License. You can download the license in the product download area. Note that the Engine is fully functional with a trial license and does not have any limitations. The trial license is valid for one month and the engine will stop working after this. You can check the expiration date inside the license file. Download the license file and specify its content in the UserFileSystemLicense field in appsettings.json file.

+

To run the example, you will need a valid IT Hit User File System Engine for .NET License. You can download the license in the product download area. Note that the Engine is fully functional with a trial license and does not have any limitations. The trial license is valid for one month and the engine will stop working after this. You can check the expiration date inside the license file. Download the license file and specify its content in the UserFileSystemLicense field in 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: \":

+
"UserFileSystemLicense": "<?xml version=\"1.0\" encoding=\"utf-8\"?><License…

You can also run the sample without explicitly specifying a license for 5 days. In this case, the Engine will automatically request the trial license from the IT Hit website https://www.userfilesystem.com. Make sure it is accessible via firewalls if any. After 5 days the Engine will stop working. To extend the trial period you will need to download a license in a product download area and specify it in appsettings.json

Running the Sample

To run the sample open the project in Visual Studio and run the project in debug mode. In the debug mode this sample provides additional support for the development and testing convenience. When starting in the debug mode, it will automatically create a folder in which the virtual file system will reside, register the user file system with the platform and then open two instances of Windows File Manager, one of which will show a user file system and another a folder simulating remote storage:

diff --git a/Windows/VirtualFileSystem/RemoteStorageMonitor.cs b/Windows/VirtualFileSystem/RemoteStorageMonitor.cs index d567515..d2aab18 100644 --- a/Windows/VirtualFileSystem/RemoteStorageMonitor.cs +++ b/Windows/VirtualFileSystem/RemoteStorageMonitor.cs @@ -47,6 +47,8 @@ public virtual SynchronizationState SyncState ///
private readonly FileSystemWatcherQueued watcher = new FileSystemWatcherQueued(); + private readonly Mapping mapping; + /// /// Creates instance of this class. /// @@ -56,6 +58,7 @@ public virtual SynchronizationState SyncState internal RemoteStorageMonitor(string remoteStorageRootPath, Engine engine, ILog log) : base("Remote Storage Monitor", log) { this.engine = engine; + mapping = new Mapping(engine.Path, remoteStorageRootPath); watcher.IncludeSubdirectories = true; watcher.Path = remoteStorageRootPath; @@ -97,7 +100,7 @@ private async void CreatedAsync(object sender, FileSystemEventArgs e) string remoteStoragePath = e.FullPath; try { - string userFileSystemPath = Mapping.ReverseMapPath(remoteStoragePath); + string userFileSystemPath = mapping.ReverseMapPath(remoteStoragePath); // This check is only required because we can not prevent circular calls because of the simplicity of this example. // In your real-life application you will not send updates from server back to client that issued the update. @@ -138,7 +141,7 @@ private async void ChangedAsync(object sender, FileSystemEventArgs e) string userFileSystemPath = null; try { - userFileSystemPath = Mapping.ReverseMapPath(remoteStoragePath); + userFileSystemPath = mapping.ReverseMapPath(remoteStoragePath); // This check is only required because we can not prevent circular calls because of the simplicity of this example. // In your real-life application you will not send updates from server back to client that issued the update. @@ -176,7 +179,7 @@ private async void DeletedAsync(object sender, FileSystemEventArgs e) string remoteStoragePath = e.FullPath; try { - string userFileSystemPath = Mapping.ReverseMapPath(remoteStoragePath); + string userFileSystemPath = mapping.ReverseMapPath(remoteStoragePath); // This check is only required because we can not prevent circular calls because of the simplicity of this example. // In your real-life application you will not send updates from server back to client that issued the update. @@ -207,8 +210,8 @@ private async void RenamedAsync(object sender, RenamedEventArgs e) string remoteStorageNewPath = e.FullPath; try { - string userFileSystemOldPath = Mapping.ReverseMapPath(remoteStorageOldPath); - string userFileSystemNewPath = Mapping.ReverseMapPath(remoteStorageNewPath); + string userFileSystemOldPath = mapping.ReverseMapPath(remoteStorageOldPath); + string userFileSystemNewPath = mapping.ReverseMapPath(remoteStorageNewPath); // This check is only required because we can not prevent circular calls because of the simplicity of this example. // In your real-life application you will not send updates from server back to client that issued the update. diff --git a/Windows/VirtualFileSystem/VirtualEngine.cs b/Windows/VirtualFileSystem/VirtualEngine.cs index 551d3dd..bd974ba 100644 --- a/Windows/VirtualFileSystem/VirtualEngine.cs +++ b/Windows/VirtualFileSystem/VirtualEngine.cs @@ -6,7 +6,7 @@ using ITHit.FileSystem.Windows; using ITHit.FileSystem.Samples.Common.Windows; - +using System.Threading; namespace VirtualFileSystem { @@ -64,9 +64,9 @@ public override async Task GetFileSystemItemAsync(string userFi } /// - public override async Task StartAsync(bool processModified = true) + public override async Task StartAsync(bool processModified = true, CancellationToken cancellationToken = default) { - await base.StartAsync(); + await base.StartAsync(processModified, cancellationToken); RemoteStorageMonitor.Start(); } diff --git a/Windows/VirtualFileSystem/VirtualFile.cs b/Windows/VirtualFileSystem/VirtualFile.cs index db73e6f..5c0294c 100644 --- a/Windows/VirtualFileSystem/VirtualFile.cs +++ b/Windows/VirtualFileSystem/VirtualFile.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using ITHit.FileSystem; +using ITHit.FileSystem.Windows; using ITHit.FileSystem.Samples.Common; using ITHit.FileSystem.Samples.Common.Windows; @@ -13,7 +14,7 @@ namespace VirtualFileSystem { /// - public class VirtualFile : VirtualFileSystemItem, IFile + public class VirtualFile : VirtualFileSystemItem, IFileWindows { /// @@ -28,36 +29,42 @@ public VirtualFile(string path, byte[] remoteStorageItemId, ILogger logger) : ba } /// - public async Task OpenAsync(IOperationContext operationContext, IResultContext context) + public async Task OpenCompletionAsync(IOperationContext operationContext, IResultContext context, CancellationToken cancellationToken) { - Logger.LogMessage($"{nameof(IFile)}.{nameof(OpenAsync)}()", UserFileSystemPath, default, operationContext); + Logger.LogMessage($"{nameof(IFileWindows)}.{nameof(OpenCompletionAsync)}()", UserFileSystemPath, default, operationContext); } /// - public async Task CloseAsync(IOperationContext operationContext, IResultContext context) + public async Task CloseCompletionAsync(IOperationContext operationContext, IResultContext context, CancellationToken cancellationToken) { - Logger.LogMessage($"{nameof(IFile)}.{nameof(CloseAsync)}()", UserFileSystemPath, default, operationContext); + Logger.LogMessage($"{nameof(IFileWindows)}.{nameof(CloseCompletionAsync)}()", UserFileSystemPath, default, operationContext); } /// - public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext) + 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 call the resultContext.ReportProgress() or resultContext.ReturnData() method. Logger.LogMessage($"{nameof(IFile)}.{nameof(ReadAsync)}({offset}, {length})", UserFileSystemPath, default, operationContext); - SimulateNetworkDelay(length, resultContext); - string remoteStoragePath = Mapping.GetRemoteStoragePathById(RemoteStorageItemId); await using (FileStream stream = System.IO.File.OpenRead(remoteStoragePath)) { stream.Seek(offset, SeekOrigin.Begin); const int bufferSize = 0x500000; // 5Mb. Buffer size must be multiple of 4096 bytes for optimal performance. - await stream.CopyToAsync(output, bufferSize, length); + try + { + await stream.CopyToAsync(output, bufferSize, length, cancellationToken); + } + catch (OperationCanceledException) + { + // Operation was canceled by the calling Engine.StopAsync() or the operation timeout occured. + Logger.LogMessage($"{nameof(ReadAsync)}({offset}, {length}) canceled", UserFileSystemPath, default); + } } } @@ -70,15 +77,13 @@ public async Task ValidateDataAsync(long offset, long length, IValidateDataOpera Logger.LogMessage($"{nameof(IFile)}.{nameof(ValidateDataAsync)}({offset}, {length})", UserFileSystemPath, default, operationContext); - //SimulateNetworkDelay(length, resultContext); - bool isValid = true; resultContext.ReturnValidationResult(offset, length, isValid); } /// - public async Task WriteAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null) + public async Task WriteAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(IFile)}.{nameof(WriteAsync)}()", UserFileSystemPath, default, operationContext); diff --git a/Windows/VirtualFileSystem/VirtualFileSystemItem.cs b/Windows/VirtualFileSystem/VirtualFileSystemItem.cs index c9bb67d..cc5474c 100644 --- a/Windows/VirtualFileSystem/VirtualFileSystemItem.cs +++ b/Windows/VirtualFileSystem/VirtualFileSystemItem.cs @@ -44,7 +44,7 @@ public VirtualFileSystemItem(string userFileSystemPath, byte[] remoteStorageItem /// - public async Task MoveToAsync(string targetUserFileSystemPath, byte[] targetFolderRemoteStorageItemId, IOperationContext operationContext = null, IConfirmationResultContext resultContext = null) + public async Task MoveToAsync(string targetUserFileSystemPath, byte[] targetFolderRemoteStorageItemId, IOperationContext operationContext = null, IConfirmationResultContext resultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewPath = targetUserFileSystemPath; string userFileSystemOldPath = this.UserFileSystemPath; @@ -52,7 +52,7 @@ public async Task MoveToAsync(string targetUserFileSystemPath, byte[] targetFold } /// - public async Task MoveToCompletionAsync(string targetUserFileSystemPath, byte[] targetFolderRemoteStorageItemId, IMoveCompletionContext operationContext = null, IInSyncStatusResultContext resultContext = null) + public async Task MoveToCompletionAsync(string targetUserFileSystemPath, byte[] targetFolderRemoteStorageItemId, IMoveCompletionContext operationContext = null, IInSyncStatusResultContext resultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewPath = targetUserFileSystemPath; string userFileSystemOldPath = this.UserFileSystemPath; @@ -82,12 +82,13 @@ public async Task MoveToCompletionAsync(string targetUserFileSystemPath, byte[] /// - public async Task DeleteAsync(IOperationContext operationContext = null, IConfirmationResultContext resultContext = null) + public async Task DeleteAsync(IOperationContext operationContext = null, IConfirmationResultContext resultContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(IFileSystemItem)}.{nameof(DeleteAsync)}()", this.UserFileSystemPath, default, operationContext); // To cancel the operation and prevent the file from being deleted, - // call the resultContext.ReturnErrorResult() method or throw any exception inside this method. + // call the resultContext.ReturnErrorResult() method or throw any exception inside this method: + // resultContext.ReturnErrorResult(CloudFileStatus.STATUS_CLOUD_FILE_REQUEST_TIMEOUT); // IMPOTRTANT! // Make sure you have all Windows updates installed. @@ -97,7 +98,7 @@ public async Task DeleteAsync(IOperationContext operationContext = null, IConfir } /// - public async Task DeleteCompletionAsync(IOperationContext operationContext = null, IInSyncStatusResultContext resultContext = null) + public async Task DeleteCompletionAsync(IOperationContext operationContext = null, IInSyncStatusResultContext resultContext = null, CancellationToken cancellationToken = default) { // On Windows, for rename with overwrite to function properly for folders, // the deletion of the folder in the remote storage must be done in DeleteCompletionAsync() @@ -137,24 +138,6 @@ public Task GetMetadataAsync() { // Return IFileMetadata for a file, IFolderMetadata for a folder. throw new NotImplementedException(); - } - - /// - /// Simulates network delays and reports file transfer progress for demo purposes. - /// - /// Length of file. - /// Context to report progress to. - protected void SimulateNetworkDelay(long fileLength, IResultContext resultContext) - { - if (Program.Settings.NetworkSimulationDelayMs > 0) - { - int numProgressResults = 5; - for (int i = 0; i < numProgressResults; i++) - { - resultContext.ReportProgress(fileLength, i * fileLength / numProgressResults); - Thread.Sleep(Program.Settings.NetworkSimulationDelayMs); - } - } - } + } } } diff --git a/Windows/VirtualFileSystem/VirtualFolder.cs b/Windows/VirtualFileSystem/VirtualFolder.cs index f590a60..5010c1d 100644 --- a/Windows/VirtualFileSystem/VirtualFolder.cs +++ b/Windows/VirtualFileSystem/VirtualFolder.cs @@ -29,7 +29,7 @@ public VirtualFolder(string path, byte[] remoteStorageItemId, ILogger logger) : } /// - public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IInSyncResultContext inSyncResultContext = null) + public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFileAsync)}()", Path.Combine(UserFileSystemPath, fileMetadata.Name)); @@ -59,7 +59,7 @@ public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream con } /// - public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IInSyncResultContext inSyncResultContext = null) + public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFolderAsync)}()", Path.Combine(UserFileSystemPath, folderMetadata.Name)); @@ -79,7 +79,7 @@ public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IInS } /// - public async Task GetChildrenAsync(string pattern, IOperationContext operationContext, IFolderListingResultContext resultContext) + public async Task GetChildrenAsync(string pattern, IOperationContext operationContext, IFolderListingResultContext resultContext, CancellationToken cancellationToken) { // This method has a 60 sec timeout. // To process longer requests and reset the timout timer call one of the following: @@ -112,7 +112,7 @@ public async Task GetChildrenAsync(string pattern, IOperationContext operationCo } /// - public async Task WriteAsync(IFolderMetadata folderMetadata, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null) + public async Task WriteAsync(IFolderMetadata folderMetadata, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(IFolder)}.{nameof(WriteAsync)}()", UserFileSystemPath, default, operationContext); diff --git a/Windows/VirtualFileSystem/appsettings.json b/Windows/VirtualFileSystem/appsettings.json index 7b84c5b..b076d14 100644 --- a/Windows/VirtualFileSystem/appsettings.json +++ b/Windows/VirtualFileSystem/appsettings.json @@ -4,33 +4,39 @@ // (aka Corporate Drive and Personal Drive) set a unique ID for each instance. "AppID": "VirtualFileSystem", - // License to activate the IT Hit User File System Engine. If no license is specified the Engine will be activated + + // License to activate the IT Hit User File System Engine. + // Set the license content directly as value. Make sure to escape quotes: \": + // "UserFileSystemLicense": " 0 the file download is delayd to demonstrate file transfer progress. - // Set this parameter to 0 to avoid any network simulation delays. - "NetworkSimulationDelayMs": 0, // Automatically lock the file in the remote storage when a file handle is being opened for writing, unlock on close. "AutoLock": true, + // To test performance: // 1. Compile the project in the release mode. // 2. Run without debugger arrached (Ctrl-F5). // 3. Set the VerboseLogging to false. "VerboseLogging": true, + // Gets or sets the maximum number of concurrent tasks "MaxDegreeOfParallelism": 1000 } \ No newline at end of file diff --git a/Windows/WebDAVDrive/README.md b/Windows/WebDAVDrive/README.md index ed6054d..12558a6 100644 --- a/Windows/WebDAVDrive/README.md +++ b/Windows/WebDAVDrive/README.md @@ -18,7 +18,8 @@

By default, this sample will mount the user file system under the %USERPROFILE%\DAV\ folder (typically C:\Users\<username>\DAV\). To specify a different folder edit the "UserFileSystemRootPath" parameter in appsettings.json.

Setting the 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 trial license in the IT Hit WebDAV Client Library product download area and in the IT Hit User File System product download area. Note that this sample is fully functional with a trial license 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 licenses file and specify license strings in the WebDAVClientLicense and UserFileSystemLicense fields respectively in appsettings.json file.

+

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 trial license in the IT Hit WebDAV Client Library product download area and in the IT Hit User File System product download area. Note that this sample is fully functional with a trial license 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 licenses file and specify license strings in the WebDAVClientLicense and UserFileSystemLicense fields respectively in 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: \":

+
"UserFileSystemLicense": "<?xml version=\"1.0\" encoding=\"utf-8\"?><License…

You can also run the sample without explicitly specifying a license for 5 days. In this case, the Engine will automatically request the trial licenses from the IT Hit website https://www.userfilesystem.com. Make sure it is accessible via firewalls if any. After 5 days the Engine will stop working. To extend the trial period you will need to download trial licenses and specify them in appsettings.json

Running the Sample

To run the sample open the project in Visual Studio and run the project in debug mode. In the debug mode this sample provides additional support for the development and testing convenience. When starting in the debug mode, it will automatically create a folder in which the virtual file system will reside, register the user file system with the platform and then open an instance of Windows File Manager with a mounted file system as well as will launch a default web browser navigating to the WebDAV server URL specified in your appsettings.json:

diff --git a/Windows/WebDAVDrive/WebDAVDrive.Package/Package.appxmanifest b/Windows/WebDAVDrive/WebDAVDrive.Package/Package.appxmanifest index 35bd626..74575d2 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.Package/Package.appxmanifest +++ b/Windows/WebDAVDrive/WebDAVDrive.Package/Package.appxmanifest @@ -44,35 +44,35 @@ - + - + - + - + - + - - + + - - - + + + - + diff --git a/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/ContextMenusProvider.cs b/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/ContextMenusProvider.cs index 35f77fa..53a8768 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/ContextMenusProvider.cs +++ b/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/ContextMenusProvider.cs @@ -1,5 +1,6 @@ using ITHit.FileSystem.Samples.Common.Windows.Rpc; using ITHit.FileSystem.Samples.Common.Windows.Rpc.Generated; +using ITHit.FileSystem.Samples.Common.Windows.ShellExtension; using ITHit.FileSystem.Samples.Common.Windows.ShellExtension.Interop; using System; using System.Collections.Generic; @@ -8,18 +9,16 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; -namespace ITHit.FileSystem.Samples.Common.Windows.ShellExtension +namespace WebDAVDrive.ShellExtension { /// /// Implements Windows Explorer context menu. /// [ComVisible(true)] - [ProgId("WebDAVDrive.ContextMenusProvider"), Guid(ContextMenusClass)] + [ProgId("WebDAVDrive.ContextMenusProvider")] + [Guid("A22EBD03-343E-433C-98DF-372C6B3A1538")] public class ContextMenusProvider : ContextMenusProviderBase { - public const string ContextMenusClass = "A22EBD03-343E-433C-98DF-372C6B3A1538"; - public static readonly Guid ContextMenusClassGuid = Guid.Parse(ContextMenusClass); - public const string LockCommandIcon = "Locked.ico"; public const string UnlockCommandIcon = "Unlocked.ico"; diff --git a/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/Program.cs b/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/Program.cs index ad5526f..bd03adf 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/Program.cs +++ b/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/Program.cs @@ -4,12 +4,6 @@ using ITHit.FileSystem.Samples.Common.Windows.ShellExtension; using ITHit.FileSystem.Samples.Common.Windows.ShellExtension.ComInfrastructure; using ITHit.FileSystem.Samples.Common; -using System.Reflection; -using System.IO; -using System.Linq; -using log4net; -using log4net.Config; -using log4net.Appender; using System.Diagnostics; namespace WebDAVDrive.ShellExtension @@ -18,14 +12,6 @@ class Program { static async Task Main(string[] args) { - // Typically you will register COM exe server in packaged installer, inside Package.appxmanifest. - // This code is used for testing purposes only, it will register COM exe if you run this program directly. - if (args.Length == 1 && - ShellExtensionInstaller.HandleRegCommand(args[0], ThumbnailProvider.ThumbnailClassGuid, ContextMenusProvider.ContextMenusClassGuid)) - { - return; - } - // Load and initialize settings. var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json", false, true).Build(); Settings settings = new Settings(); @@ -37,8 +23,8 @@ static async Task Main(string[] args) { using (var server = new LocalServer()) { - server.RegisterClass(ThumbnailProvider.ThumbnailClassGuid); - server.RegisterClass(ContextMenusProvider.ContextMenusClassGuid); + server.RegisterClass(typeof(ThumbnailProvider).GUID); + server.RegisterClass(typeof(ContextMenusProvider).GUID); await server.Run(); } diff --git a/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/ThumbnailProvider.cs b/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/ThumbnailProvider.cs index b66abdd..058be1b 100644 --- a/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/ThumbnailProvider.cs +++ b/Windows/WebDAVDrive/WebDAVDrive.ShellExtension/ThumbnailProvider.cs @@ -1,10 +1,7 @@ using System; -using System.IO; -using System.Drawing; using System.Runtime.InteropServices; using System.Threading.Tasks; using ITHit.FileSystem.Samples.Common.Windows.ShellExtension.Thumbnails; -using ITHit.FileSystem.Samples.Common.Windows.ShellExtension.Interop; using ITHit.FileSystem.Samples.Common.Windows.Rpc; using ITHit.FileSystem.Samples.Common.Windows.ShellExtension; using ITHit.FileSystem.Samples.Common.Windows.Rpc.Generated; @@ -15,12 +12,10 @@ namespace WebDAVDrive.ShellExtension /// Thumbnails provider Windows Shell Extension. ///
[ComVisible(true)] - [ProgId("WebDAVDrive.ThumbnailProvider"), Guid(ThumbnailClass)] + [ProgId("WebDAVDrive.ThumbnailProvider")] + [Guid("A5B0C82F-50AA-445C-A404-66DEB510E84B")] public class ThumbnailProvider : ThumbnailProviderBase { - public const string ThumbnailClass = "A5B0C82F-50AA-445C-A404-66DEB510E84B"; - public static readonly Guid ThumbnailClassGuid = Guid.Parse(ThumbnailClass); - public override async Task GetThumbnailsAsync(string filePath, uint size) { try diff --git a/Windows/WebDAVDrive/WebDAVDrive.WinRT.ShellExtension/x64/Debug/WebDAVDrive.WinRT.ShellExtension.log b/Windows/WebDAVDrive/WebDAVDrive.WinRT.ShellExtension/x64/Debug/WebDAVDrive.WinRT.ShellExtension.log new file mode 100644 index 0000000..1f0a256 --- /dev/null +++ b/Windows/WebDAVDrive/WebDAVDrive.WinRT.ShellExtension/x64/Debug/WebDAVDrive.WinRT.ShellExtension.log @@ -0,0 +1,2 @@ +D:\Program Files\MSBuild\Current\Bin\Microsoft.Common.CurrentVersion.targets(1717,5): error : Project '..\..\Common\Rpc.Proto\Common.Windows.Rpc.Proto.csproj' targets 'net5.0-windows10.0.18362.0'. It cannot be referenced by a project that targets '.NETFramework,Version=v4.0'. +D:\Program Files\MSBuild\Current\Bin\Microsoft.Common.CurrentVersion.targets(1717,5): error : Project '..\..\Common\Rpc\Common.Windows.Rpc.csproj' targets 'net5.0-windows10.0.18362.0'. It cannot be referenced by a project that targets '.NETFramework,Version=v4.0'. diff --git a/Windows/WebDAVDrive/WebDAVDrive.WinRT.ShellExtension/x64/Debug/WebDAVDrive.WinRT.ShellExtension.vcxproj.FileListAbsolute.txt b/Windows/WebDAVDrive/WebDAVDrive.WinRT.ShellExtension/x64/Debug/WebDAVDrive.WinRT.ShellExtension.vcxproj.FileListAbsolute.txt new file mode 100644 index 0000000..e69de29 diff --git a/Windows/WebDAVDrive/WebDAVDrive/Program.cs b/Windows/WebDAVDrive/WebDAVDrive/Program.cs index 4daf8fc..806a2be 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/Program.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/Program.cs @@ -81,9 +81,6 @@ static async Task Main(string[] args) // Configure log4net and set log file path. LogFilePath = ConfigureLogger(); - // Enable UTF8 for Console Window. - Console.OutputEncoding = System.Text.Encoding.UTF8; - PrintHelp(); // Register sync root and create app folders. @@ -158,6 +155,11 @@ private static async Task ConsoleReadKeyAsync() /// This method is provided solely for the development and testing convenience. private static void ShowTestEnvironment() { + // 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, Console.BufferHeight); + // Open Windows File Manager with user file system. ProcessStartInfo ufsInfo = new ProcessStartInfo(Program.Settings.UserFileSystemRootPath); ufsInfo.UseShellExecute = true; // Open window only if not opened already. @@ -173,14 +175,6 @@ private static void ShowTestEnvironment() { } - - //// Open Windows File Manager with custom data storage. Uncomment this to debug custom data storage management. - //ProcessStartInfo serverDataInfo = new ProcessStartInfo(Program.Settings.ServerDataFolderPath); - //serverDataInfo.UseShellExecute = true; // Open window only if not opened already. - //using (Process serverDataWinFileManager = Process.Start(serverDataInfo)) - //{ - - //} } #endif @@ -482,6 +476,9 @@ private static async Task RegisterSyncRootAsync() await Registrar.RegisterAsync(SyncRootId, Settings.UserFileSystemRootPath, Settings.ProductName, Path.Combine(Settings.IconsFolderPath, "Drive.ico")); + + log.Info("\nRegistering shell extensions...\n"); + ShellExtensionRegistrar.Register(SyncRootId); } else { @@ -505,6 +502,9 @@ private static async Task UnregisterSyncRootAsync() { log.Info($"\n\nUnregistering {Settings.UserFileSystemRootPath} sync root."); await Registrar.UnregisterAsync(SyncRootId); + + log.Info("\nUnregistering shell extensions...\n"); + ShellExtensionRegistrar.Unregister(); } private static async Task CleanupAppFoldersAsync() diff --git a/Windows/WebDAVDrive/WebDAVDrive/ShellExtensionRegistrar.cs b/Windows/WebDAVDrive/WebDAVDrive/ShellExtensionRegistrar.cs new file mode 100644 index 0000000..d63a1c1 --- /dev/null +++ b/Windows/WebDAVDrive/WebDAVDrive/ShellExtensionRegistrar.cs @@ -0,0 +1,33 @@ +using ITHit.FileSystem.Samples.Common.Windows; +using System.IO; +using System.Reflection; +using WebDAVDrive.ShellExtension; +using CommonShellExtension = ITHit.FileSystem.Samples.Common.Windows.ShellExtension; + +namespace WebDAVDrive +{ + internal class ShellExtensionRegistrar + { + private static readonly string ComServerRelativePath = @"WebDAVDrive.ShellExtension.exe"; + + public static void Register(string syncRootId) + { + if (!Registrar.IsRunningAsUwp()) + { + string comServerPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), ComServerRelativePath)); + CommonShellExtension.ShellExtensionRegistrar.Register(syncRootId, "ThumbnailProvider", typeof(ThumbnailProvider).GUID, comServerPath); + CommonShellExtension.ShellExtensionRegistrar.Register(syncRootId, "MenuVerbHandler_0", typeof(ContextMenusProvider).GUID, comServerPath); + } + } + + public static void Unregister() + { + if (!Registrar.IsRunningAsUwp()) + { + CommonShellExtension.ShellExtensionRegistrar.Unregister(typeof(ThumbnailProvider).GUID); + CommonShellExtension.ShellExtensionRegistrar.Unregister(typeof(ContextMenusProvider).GUID); + } + } + + } +} diff --git a/Windows/WebDAVDrive/WebDAVDrive/VirtualEngine.cs b/Windows/WebDAVDrive/WebDAVDrive/VirtualEngine.cs index 452e2ef..a11fbaf 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/VirtualEngine.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/VirtualEngine.cs @@ -12,6 +12,7 @@ using ITHit.FileSystem.Samples.Common; using ITHit.WebDAV.Client; +using System.Threading; namespace WebDAVDrive { @@ -74,9 +75,9 @@ public override async Task GetFileSystemItemAsync(string userFi //public override IMapping Mapping { get { return new Mapping(this); } } /// - public override async Task StartAsync(bool processModified = true) + public override async Task StartAsync(bool processModified = true, CancellationToken cancellationToken = default) { - await base.StartAsync(processModified); + await base.StartAsync(processModified, cancellationToken); await RemoteStorageMonitor.StartAsync(); } diff --git a/Windows/WebDAVDrive/WebDAVDrive/VirtualFile.cs b/Windows/WebDAVDrive/WebDAVDrive/VirtualFile.cs index f6c0541..38e9c7b 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/VirtualFile.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/VirtualFile.cs @@ -6,15 +6,15 @@ using System.Threading.Tasks; using ITHit.FileSystem; +using ITHit.FileSystem.Windows; using ITHit.FileSystem.Samples.Common; using ITHit.FileSystem.Samples.Common.Windows; -using ITHit.FileSystem.Windows; using Client = ITHit.WebDAV.Client; namespace WebDAVDrive { /// - public class VirtualFile : VirtualFileSystemItem, IFile + public class VirtualFile : VirtualFileSystemItem, IFileWindows { /// @@ -29,28 +29,26 @@ public VirtualFile(string path, VirtualEngine engine, ILogger logger) : base(pat } /// - public async Task OpenAsync(IOperationContext operationContext, IResultContext context) + public async Task OpenCompletionAsync(IOperationContext operationContext, IResultContext context, CancellationToken cancellationToken) { - Logger.LogMessage($"{nameof(IFile)}.{nameof(OpenAsync)}()", UserFileSystemPath, default, operationContext); + Logger.LogMessage($"{nameof(IFileWindows)}.{nameof(OpenCompletionAsync)}()", UserFileSystemPath, default, operationContext); } /// - public async Task CloseAsync(IOperationContext operationContext, IResultContext context) + public async Task CloseCompletionAsync(IOperationContext operationContext, IResultContext context, CancellationToken cancellationToken) { - Logger.LogMessage($"{nameof(IFile)}.{nameof(CloseAsync)}()", UserFileSystemPath, default, operationContext); + Logger.LogMessage($"{nameof(IFileWindows)}.{nameof(CloseCompletionAsync)}()", UserFileSystemPath, default, operationContext); } - + /// - public async Task ReadAsync(Stream output, long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext) + 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 call the resultContext.ReportProgress() or resultContext.ReturnData() method. Logger.LogMessage($"{nameof(IFile)}.{nameof(ReadAsync)}({offset}, {length})", UserFileSystemPath, default, operationContext); - SimulateNetworkDelay(length, resultContext); - - if (offset == 0 && length == operationContext.FileSize) + if (offset == 0 && length == operationContext.FileSize) { // If we read entire file, do not add Range header. Pass -1 to not add it. offset = -1; @@ -64,7 +62,15 @@ public async Task ReadAsync(Stream output, long offset, long length, ITransferDa { using (Stream stream = await response.GetResponseStreamAsync()) { - await stream.CopyToAsync(output, bufferSize, length); + try + { + await stream.CopyToAsync(output, bufferSize, length, cancellationToken); + } + catch (OperationCanceledException) + { + // Operation was canceled by the calling Engine.StopAsync() or the operation timeout occured. + Logger.LogMessage($"{nameof(ReadAsync)}({offset}, {length}) canceled", UserFileSystemPath, default); + } } eTag = response.GetHeaderValue("ETag"); } @@ -87,15 +93,13 @@ public async Task ValidateDataAsync(long offset, long length, IValidateDataOpera Logger.LogMessage($"{nameof(IFile)}.{nameof(ValidateDataAsync)}({offset}, {length})", UserFileSystemPath, default, operationContext); - //SimulateNetworkDelay(length, resultContext); - bool isValid = true; resultContext.ReturnValidationResult(offset, length, isValid); } /// - public async Task WriteAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null) + public async Task WriteAsync(IFileMetadata fileMetadata, Stream content = null, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(IFile)}.{nameof(WriteAsync)}()", UserFileSystemPath, default, operationContext); @@ -105,7 +109,7 @@ public async Task WriteAsync(IFileMetadata fileMetadata, Stream content = null, // the file in the remote storge is not modified since last read. PlaceholderItem placeholder = Engine.Placeholders.GetItem(UserFileSystemPath); - string oldEtag = null; + string oldEtag = null; if (placeholder.Properties.TryGetValue("ETag", out IDataItem propETag)) { @@ -127,7 +131,8 @@ public async Task WriteAsync(IFileMetadata fileMetadata, Stream content = null, // Update remote storage file content, // also get and save a new ETag returned by the server, if any. - string newEtag = await Program.DavClient.UploadAsync(new Uri(RemoteStoragePath), async (outputStream) => { + string newEtag = await Program.DavClient.UploadAsync(new Uri(RemoteStoragePath), async (outputStream) => + { // Setting position to 0 is required in case of retry. content.Position = 0; await content.CopyToAsync(outputStream); diff --git a/Windows/WebDAVDrive/WebDAVDrive/VirtualFileSystemItem.cs b/Windows/WebDAVDrive/WebDAVDrive/VirtualFileSystemItem.cs index 515e71b..afd269f 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/VirtualFileSystemItem.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/VirtualFileSystemItem.cs @@ -56,7 +56,7 @@ public VirtualFileSystemItem(string userFileSystemPath, VirtualEngine engine, IL } /// - public async Task MoveToAsync(string targetUserFileSystemPath, byte[] targetParentItemId, IOperationContext operationContext = null, IConfirmationResultContext resultContext = null) + public async Task MoveToAsync(string targetUserFileSystemPath, byte[] targetParentItemId, IOperationContext operationContext = null, IConfirmationResultContext resultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewPath = targetUserFileSystemPath; string userFileSystemOldPath = this.UserFileSystemPath; @@ -64,7 +64,7 @@ public async Task MoveToAsync(string targetUserFileSystemPath, byte[] targetPare } /// - public async Task MoveToCompletionAsync(string targetUserFileSystemPath, byte[] targetFolderRemoteStorageItemId, IMoveCompletionContext operationContext = null, IInSyncStatusResultContext resultContext = null) + public async Task MoveToCompletionAsync(string targetUserFileSystemPath, byte[] targetFolderRemoteStorageItemId, IMoveCompletionContext operationContext = null, IInSyncStatusResultContext resultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewPath = targetUserFileSystemPath; string userFileSystemOldPath = this.UserFileSystemPath; @@ -78,7 +78,7 @@ public async Task MoveToCompletionAsync(string targetUserFileSystemPath, byte[] } /// - public async Task DeleteAsync(IOperationContext operationContext, IConfirmationResultContext resultContext) + public async Task DeleteAsync(IOperationContext operationContext, IConfirmationResultContext resultContext, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(IFileSystemItem)}.{nameof(DeleteAsync)}()", UserFileSystemPath, default, operationContext); @@ -93,7 +93,7 @@ public async Task DeleteAsync(IOperationContext operationContext, IConfirmationR } /// - public async Task DeleteCompletionAsync(IOperationContext operationContext, IInSyncStatusResultContext resultContext) + public async Task DeleteCompletionAsync(IOperationContext operationContext, IInSyncStatusResultContext resultContext, CancellationToken cancellationToken = default) { // On Windows, for move with overwrite on folders to function correctly, // the deletion of the folder in the remote storage must be done in DeleteCompletionAsync() @@ -118,29 +118,11 @@ public Task GetMetadataAsync() { // Return IFileMetadata for a file, IFolderMetadata for a folder. throw new System.NotImplementedException(); - } - - /// - /// Simulates network delays and reports file transfer progress for demo purposes. - /// - /// Length of file. - /// Context to report progress to. - protected void SimulateNetworkDelay(long fileLength, IResultContext resultContext) - { - if (Program.Settings.NetworkSimulationDelayMs > 0) - { - int numProgressResults = 5; - for (int i = 0; i < numProgressResults; i++) - { - resultContext.ReportProgress(fileLength, i * fileLength / numProgressResults); - Thread.Sleep(Program.Settings.NetworkSimulationDelayMs); - } - } - } + } /// - public async Task LockAsync(LockMode lockMode, IOperationContext operationContext = null) + public async Task LockAsync(LockMode lockMode, IOperationContext operationContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(ILock)}.{nameof(LockAsync)}()", UserFileSystemPath, default, operationContext); @@ -168,7 +150,7 @@ public async Task LockAsync(LockMode lockMode, IOperationContext operationContex /// - public async Task GetLockModeAsync(IOperationContext operationContext = null) + public async Task GetLockModeAsync(IOperationContext operationContext = null, CancellationToken cancellationToken = default) { PlaceholderItem placeholder = Engine.Placeholders.GetItem(UserFileSystemPath); @@ -186,7 +168,7 @@ public async Task GetLockModeAsync(IOperationContext operationContext /// - public async Task UnlockAsync(IOperationContext operationContext = null) + public async Task UnlockAsync(IOperationContext operationContext = null, CancellationToken cancellationToken = default) { Logger.LogMessage($"{nameof(ILock)}.{nameof(UnlockAsync)}()", UserFileSystemPath, default, operationContext); diff --git a/Windows/WebDAVDrive/WebDAVDrive/VirtualFolder.cs b/Windows/WebDAVDrive/WebDAVDrive/VirtualFolder.cs index 1b20fe6..2319b61 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/VirtualFolder.cs +++ b/Windows/WebDAVDrive/WebDAVDrive/VirtualFolder.cs @@ -9,6 +9,7 @@ using ITHit.FileSystem.Samples.Common; using Client = ITHit.WebDAV.Client; using ITHit.FileSystem.Windows; +using System.Threading; namespace WebDAVDrive { @@ -27,7 +28,7 @@ public VirtualFolder(string path, VirtualEngine engine, ILogger logger) : base(p } /// - public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IInSyncResultContext inSyncResultContext = null) + public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream content = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewItemPath = Path.Combine(UserFileSystemPath, fileMetadata.Name); Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFileAsync)}()", userFileSystemNewItemPath); @@ -57,7 +58,7 @@ public async Task CreateFileAsync(IFileMetadata fileMetadata, Stream con } /// - public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IInSyncResultContext inSyncResultContext = null) + public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { string userFileSystemNewItemPath = Path.Combine(UserFileSystemPath, folderMetadata.Name); Logger.LogMessage($"{nameof(IFolder)}.{nameof(CreateFolderAsync)}()", userFileSystemNewItemPath); @@ -75,7 +76,7 @@ public async Task CreateFolderAsync(IFolderMetadata folderMetadata, IInS } /// - public async Task GetChildrenAsync(string pattern, IOperationContext operationContext, IFolderListingResultContext resultContext) + public async Task GetChildrenAsync(string pattern, IOperationContext operationContext, IFolderListingResultContext resultContext, CancellationToken cancellationToken) { // This method has a 60 sec timeout. // To process longer requests and reset the timout timer call one of the following: @@ -112,7 +113,7 @@ public async Task GetChildrenAsync(string pattern, IOperationContext operationCo } } - public async Task> EnumerateChildrenAsync(string pattern) + public async Task> EnumerateChildrenAsync(string pattern, CancellationToken cancellationToken = default) { // WebDAV Client lib will retry the request in case authentication is requested by the server. Client.IHierarchyItem[] remoteStorageChildren = await Program.DavClient.GetChildrenAsync(new Uri(RemoteStoragePath)); @@ -129,7 +130,7 @@ public async Task> EnumerateChildrenAsync } /// - public async Task WriteAsync(IFolderMetadata folderMetadata, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null) + public async Task WriteAsync(IFolderMetadata folderMetadata, IOperationContext operationContext = null, IInSyncResultContext inSyncResultContext = null, CancellationToken cancellationToken = default) { // 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); diff --git a/Windows/WebDAVDrive/WebDAVDrive/WebDAVDrive.csproj b/Windows/WebDAVDrive/WebDAVDrive/WebDAVDrive.csproj index 735f951..d3e4416 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/WebDAVDrive.csproj +++ b/Windows/WebDAVDrive/WebDAVDrive/WebDAVDrive.csproj @@ -33,7 +33,9 @@ + + diff --git a/Windows/WebDAVDrive/WebDAVDrive/appsettings.json b/Windows/WebDAVDrive/WebDAVDrive/appsettings.json index be11544..fad5e70 100644 --- a/Windows/WebDAVDrive/WebDAVDrive/appsettings.json +++ b/Windows/WebDAVDrive/WebDAVDrive/appsettings.json @@ -4,16 +4,27 @@ // (aka Corporate Drive and Personal Drive) set a unique ID for each instance. "AppID": "WebDAVDrive", - // License to activate the IT Hit User File System Engine. If no license is specified the Engine will be activated + + // License to activate the IT Hit User File System Engine. + // Set the license content directly as value. Make sure to escape quotes: \": + // "UserFileSystemLicense": " key. @@ -23,42 +34,47 @@ // https://webdavserver.net/User123456/ and specify it below. "WebDAVServerUrl": "https://server/", + // Your WebSocket server URL. "WebSocketServerUrl": "wss://server/", + //Your virtual file system will be mounted under this path. "UserFileSystemRootPath": "%USERPROFILE%\\DAV\\", + // Full synchronization interval in milliseconds. "SyncIntervalMs": 10000, - // Network delay in milliseconds. When this parameter is > 0 the file download is delayd to demonstrate file transfer progress. - // Set this parameter to 0 to avoid any network simulation delays. - "NetworkSimulationDelayMs": 0, // Automatically lock the file in remote storage when a file handle is being opened for writing, unlock on close. "AutoLock": true, + // To test performance: // 1. Compile the project in the release mode. // 2. Run without debugger arrached (Ctrl-F5). // 3. Set the VerboseLogging to false. "VerboseLogging": true, + // Communication channel name is used by RPC to establish connection over named pipes. // The channel is used to comunicate between the main app and COM thumbnails handler, Win Explorer context menu, etc. "RpcCommunicationChannelName": "WebDAVDrive.RPC", + // URL to get a thumbnail for Windows Explorer thumbnails mode. // Your server must return 404 Not Found if the thumbnail can not be generated. // If incorrect size is returned, the image will be resized by the platform automatically. "ThumbnailGeneratorUrl": "{path to file}?width={thumbnail width}&height={thumbnail height}", + // File types to request thumbnails for. // To request thumbnails for specific file types, list file types using '|' separator. // To request thumbnails for all file types set the value to "*". "RequestThumbnailsFor": "png|jpeg|gif|jpg|apng|avif|jfif|pjpeg|pjp|svg|webp|bmp|ico|cur|tif|tiff|heic|hif", + // Gets or sets the maximum number of concurrent tasks "MaxDegreeOfParallelism": 1000 } \ No newline at end of file