From ca45391037165f0aa21c202edf4688ef4ff54f0b Mon Sep 17 00:00:00 2001 From: IT Hit Date: Sat, 20 Mar 2021 00:52:14 +0200 Subject: [PATCH] v2.0.4465.0 --- .../ClientLockFailedException.cs | 2 +- .../CustomData.cs | 14 +- .../ETag.cs | 12 +- .../FileBasicInfo.cs | 4 +- .../FileSystemItemBasicInfo.cs | 9 +- .../FolderBasicInfo.cs | 4 +- .../FsPath.cs | 10 +- .../ITHit.FileSystem.Samples.Common.csproj | 11 + ITHit.FileSystem.Samples.Common/IUserFile.cs | 13 ++ .../IUserFileSystemItem.cs | 12 + .../IUserFolder.cs | 15 ++ .../Locks/Lock.cs | 23 +- .../Locks/LockMode.cs | 2 +- .../Locks/ServerLockInfo.cs | 41 ++++ .../Logger.cs | 8 +- .../Registrar.cs | 25 +- ITHit.FileSystem.Samples.Common/Settings.cs | 79 +++++++ .../Syncronyzation/ClientToServerSync.cs | 21 +- .../Syncronyzation/FileAttributesExt.cs | 2 +- .../Syncronyzation/FullSyncService.cs | 75 ++++-- .../Syncronyzation/RemoteStorageRawItem.cs | 50 ++-- .../Syncronyzation/ServerToClientSync.cs | 37 ++- .../Syncronyzation/UserFileSystemMonitor.cs | 29 ++- .../Syncronyzation/UserFileSystemRawItem.cs | 116 +++++---- .../VfsEngine.cs | 29 ++- .../VfsFile.cs | 20 +- .../VfsFileSystemItem.cs | 30 ++- .../VfsFolder.cs | 27 ++- .../VirtualDriveBase.cs | 146 ++++++++++++ UserFileSystemSamples.sln | 43 ++++ .../{Settings.cs => AppSettings.cs} | 50 +--- .../Framework/VfsFileSystemItem.cs | 8 + VirtualFileSystem/Images/Blank.ico | Bin 0 -> 1150 bytes VirtualFileSystem/Mapping.cs | 26 ++- VirtualFileSystem/Program.cs | 62 +++-- VirtualFileSystem/README.md | 2 +- VirtualFileSystem/RemoteStorageMonitor.cs | 3 + VirtualFileSystem/UserFile.cs | 13 +- VirtualFileSystem/UserFileSystemItem.cs | 33 ++- VirtualFileSystem/UserFolder.cs | 13 +- VirtualFileSystem/VirtualDrive.cs | 38 +++ VirtualFileSystem/VirtualFileSystem.csproj | 17 +- VirtualFileSystem/appsettings.json | 5 + .../FileProviderExtension/Entitlements.plist | 4 +- .../FileProviderExtension.csproj | 11 +- .../FileProviderExtension/Info.plist | 2 +- .../VfsFileSystemItem.cs | 5 + .../FileProviderExtension/packages.config | 3 +- VirtualFileSystemMac/README.md | 138 ++++++----- .../AppGroupSettings.cs | 2 +- .../VirtualFilesystemCommon.csproj | 7 +- .../VirtualFilesystemCommon/packages.config | 3 +- VirtualFileSystemMac/VirtualFilesystemMac.sln | 20 +- .../VirtualFilesystemMacApp/AppDelegate.cs | 2 +- .../Entitlements.plist | 4 +- .../VirtualFilesystemMacApp/Info.plist | 2 +- .../VirtualFilesystemMacApp.csproj | 5 +- .../AppGroupSettings.cs | 41 ---- .../ConsoleLogger.cs | 61 ----- .../FileSystemItemBasicInfo.cs | 34 --- .../VirtualFilesystemMacCommon/FsEngine.cs | 56 ----- .../ItemMetadata.cs | 38 --- .../LocationMapper.cs | 87 ------- .../Properties/AssemblyInfo.cs | 26 --- .../RemoteStorageManager.cs | 81 ------- .../VirtualFilesystemMacCommon/UserFile.cs | 79 ------- .../VirtualFilesystemMacCommon/UserFolder.cs | 64 ----- .../VirtualFilesystemMacCommon.csproj | 77 ------ ...emMacCommon.csproj.CoreCompileInputs.cache | 1 - ...ystemMacCommon.csproj.FileListAbsolute.txt | 2 - ...rin.Mac,Version=v2.0.AssemblyAttributes.cs | 4 - ...rin.Mac,Version=v2.0.AssemblyAttributes.cs | 4 - .../packages.config | 5 - WebDAVDrive.LoginWPF/ChallengeLogin.xaml | 220 ------------------ .../WebDAVDrive.LoginWPF.csproj | 22 -- .../AssemblyInfo.cs | 0 WebDAVDrive.UI/ChallengeLogin.xaml | 94 ++++++++ .../ChallengeLogin.xaml.cs | 4 +- WebDAVDrive.UI/ConnectFrom.xaml | 86 +++++++ WebDAVDrive.UI/ConnectFrom.xaml.cs | 50 ++++ WebDAVDrive.UI/ConsoleManager.cs | 49 ++++ .../Drive.ico | Bin .../Localization/Resources.Designer.cs | 216 +++++++++++++++++ .../Localization/Resources.es-ES.resx | 171 ++++++++++++++ WebDAVDrive.UI/Localization/Resources.resx | 171 ++++++++++++++ .../Localization/Resources.uk-UA.resx | 171 ++++++++++++++ WebDAVDrive.UI/RegistryManager.cs | 84 +++++++ WebDAVDrive.UI/Styles.xaml | 136 +++++++++++ .../Themes/Generic.xaml | 2 +- .../ValidationRules.cs | 29 ++- .../ViewModels/BaseViewModel.cs | 7 +- .../ViewModels/ChallengeLoginViewModel.cs | 7 +- WebDAVDrive.UI/ViewModels/ConnectViewModel.cs | 34 +++ .../WebBrowserLogin.xaml | 6 +- .../WebBrowserLogin.xaml.cs | 31 +-- WebDAVDrive.UI/WebDAVDrive.UI.csproj | 57 +++++ WebDAVDrive.UI/WindowsTrayInterface.cs | 164 +++++++++++++ WebDAVDrive/{Settings.cs => AppSettings.cs} | 82 ++----- WebDAVDrive/CredentialManager.cs | 35 +-- WebDAVDrive/Framework/Locks/LockInfo.cs | 22 -- WebDAVDrive/Images/Blank.ico | Bin 0 -> 1150 bytes WebDAVDrive/Images/Drive.ico | Bin 122800 -> 454276 bytes WebDAVDrive/Images/DrivePause.ico | Bin 0 -> 410598 bytes WebDAVDrive/Images/DriveSync.ico | Bin 0 -> 410598 bytes WebDAVDrive/Mapping.cs | 45 ++-- WebDAVDrive/Program.cs | 117 +++++----- WebDAVDrive/RemoteStorageMonitor.cs | 12 +- WebDAVDrive/UserFile.cs | 23 +- WebDAVDrive/UserFileSystemItem.cs | 53 +++-- WebDAVDrive/UserFolder.cs | 24 +- WebDAVDrive/VirtualDrive.cs | 38 +++ WebDAVDrive/WebDAVDrive.csproj | 27 ++- WebDAVDrive/appsettings.json | 5 + 113 files changed, 2743 insertions(+), 1598 deletions(-) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/ClientLockFailedException.cs (97%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/CustomData.cs (94%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/ETag.cs (86%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/FileBasicInfo.cs (65%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/FileSystemItemBasicInfo.cs (79%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/FolderBasicInfo.cs (57%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/FsPath.cs (97%) create mode 100644 ITHit.FileSystem.Samples.Common/ITHit.FileSystem.Samples.Common.csproj create mode 100644 ITHit.FileSystem.Samples.Common/IUserFile.cs create mode 100644 ITHit.FileSystem.Samples.Common/IUserFileSystemItem.cs create mode 100644 ITHit.FileSystem.Samples.Common/IUserFolder.cs rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/Locks/Lock.cs (92%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/Locks/LockMode.cs (94%) create mode 100644 ITHit.FileSystem.Samples.Common/Locks/ServerLockInfo.cs rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/Logger.cs (91%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/Registrar.cs (89%) create mode 100644 ITHit.FileSystem.Samples.Common/Settings.cs rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/Syncronyzation/ClientToServerSync.cs (90%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/Syncronyzation/FileAttributesExt.cs (95%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/Syncronyzation/FullSyncService.cs (63%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/Syncronyzation/RemoteStorageRawItem.cs (88%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/Syncronyzation/ServerToClientSync.cs (80%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/Syncronyzation/UserFileSystemMonitor.cs (95%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/Syncronyzation/UserFileSystemRawItem.cs (82%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/VfsEngine.cs (83%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/VfsFile.cs (88%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/VfsFileSystemItem.cs (82%) rename {WebDAVDrive/Framework => ITHit.FileSystem.Samples.Common}/VfsFolder.cs (68%) create mode 100644 ITHit.FileSystem.Samples.Common/VirtualDriveBase.cs create mode 100644 UserFileSystemSamples.sln rename VirtualFileSystem/{Settings.cs => AppSettings.cs} (67%) create mode 100644 VirtualFileSystem/Images/Blank.ico create mode 100644 VirtualFileSystem/VirtualDrive.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/AppGroupSettings.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/ConsoleLogger.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/FileSystemItemBasicInfo.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/FsEngine.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/ItemMetadata.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/LocationMapper.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/Properties/AssemblyInfo.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/RemoteStorageManager.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/UserFile.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/UserFolder.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/VirtualFilesystemMacCommon.csproj delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Debug/VirtualFilesystemMacCommon.csproj.CoreCompileInputs.cache delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Debug/VirtualFilesystemMacCommon.csproj.FileListAbsolute.txt delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Debug/Xamarin.Mac,Version=v2.0.AssemblyAttributes.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Release/Xamarin.Mac,Version=v2.0.AssemblyAttributes.cs delete mode 100644 VirtualFileSystemMac/VirtualFilesystemMacCommon/packages.config delete mode 100644 WebDAVDrive.LoginWPF/ChallengeLogin.xaml delete mode 100644 WebDAVDrive.LoginWPF/WebDAVDrive.LoginWPF.csproj rename {WebDAVDrive.LoginWPF => WebDAVDrive.UI}/AssemblyInfo.cs (100%) create mode 100644 WebDAVDrive.UI/ChallengeLogin.xaml rename {WebDAVDrive.LoginWPF => WebDAVDrive.UI}/ChallengeLogin.xaml.cs (98%) create mode 100644 WebDAVDrive.UI/ConnectFrom.xaml create mode 100644 WebDAVDrive.UI/ConnectFrom.xaml.cs create mode 100644 WebDAVDrive.UI/ConsoleManager.cs rename {WebDAVDrive.LoginWPF => WebDAVDrive.UI}/Drive.ico (100%) create mode 100644 WebDAVDrive.UI/Localization/Resources.Designer.cs create mode 100644 WebDAVDrive.UI/Localization/Resources.es-ES.resx create mode 100644 WebDAVDrive.UI/Localization/Resources.resx create mode 100644 WebDAVDrive.UI/Localization/Resources.uk-UA.resx create mode 100644 WebDAVDrive.UI/RegistryManager.cs create mode 100644 WebDAVDrive.UI/Styles.xaml rename {WebDAVDrive.LoginWPF => WebDAVDrive.UI}/Themes/Generic.xaml (76%) rename {WebDAVDrive.LoginWPF => WebDAVDrive.UI}/ValidationRules.cs (71%) rename {WebDAVDrive.LoginWPF => WebDAVDrive.UI}/ViewModels/BaseViewModel.cs (73%) rename {WebDAVDrive.LoginWPF => WebDAVDrive.UI}/ViewModels/ChallengeLoginViewModel.cs (91%) create mode 100644 WebDAVDrive.UI/ViewModels/ConnectViewModel.cs rename {WebDAVDrive.LoginWPF => WebDAVDrive.UI}/WebBrowserLogin.xaml (76%) rename {WebDAVDrive.LoginWPF => WebDAVDrive.UI}/WebBrowserLogin.xaml.cs (87%) create mode 100644 WebDAVDrive.UI/WebDAVDrive.UI.csproj create mode 100644 WebDAVDrive.UI/WindowsTrayInterface.cs rename WebDAVDrive/{Settings.cs => AppSettings.cs} (53%) delete mode 100644 WebDAVDrive/Framework/Locks/LockInfo.cs create mode 100644 WebDAVDrive/Images/Blank.ico create mode 100644 WebDAVDrive/Images/DrivePause.ico create mode 100644 WebDAVDrive/Images/DriveSync.ico create mode 100644 WebDAVDrive/VirtualDrive.cs diff --git a/WebDAVDrive/Framework/ClientLockFailedException.cs b/ITHit.FileSystem.Samples.Common/ClientLockFailedException.cs similarity index 97% rename from WebDAVDrive/Framework/ClientLockFailedException.cs rename to ITHit.FileSystem.Samples.Common/ClientLockFailedException.cs index c94fbdd..e8f01e1 100644 --- a/WebDAVDrive/Framework/ClientLockFailedException.cs +++ b/ITHit.FileSystem.Samples.Common/ClientLockFailedException.cs @@ -3,7 +3,7 @@ using System.IO; using System.Text; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { /// /// Thrown when a file can not be locked. For example when a lock-token file is blocked diff --git a/WebDAVDrive/Framework/CustomData.cs b/ITHit.FileSystem.Samples.Common/CustomData.cs similarity index 94% rename from WebDAVDrive/Framework/CustomData.cs rename to ITHit.FileSystem.Samples.Common/CustomData.cs index 4fe342e..3b27f57 100644 --- a/WebDAVDrive/Framework/CustomData.cs +++ b/ITHit.FileSystem.Samples.Common/CustomData.cs @@ -6,26 +6,26 @@ using System.Text; using System.Threading.Tasks; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { /// /// Custom data stored with a file or folder placeholder, such original file/folder path. Max 4KB. /// /// To avoid storing metatadata and keep footprit small, this class is is using custom serialization. - internal class CustomData + public class CustomData { /// /// Keeps the original file/folder path. Used to sync file/folder from user file system to remote storage /// if this app was not running when the file/folder was moved or renamed. This field allows to avoid /// delete-create sequence during client to server synchronization after app failure. /// - internal string OriginalPath = ""; + public string OriginalPath = ""; /// /// Serializes all custom data fields into the byte array. /// /// Byte array representing custom data. - internal byte[] Serialize() + public byte[] Serialize() { using (MemoryStream m = new MemoryStream()) { @@ -42,7 +42,7 @@ internal byte[] Serialize() /// /// Byte array representing custom data. /// - internal static CustomData Desserialize(byte[] data) + public static CustomData Deserialize(byte[] data) { if(data == null) { @@ -81,7 +81,7 @@ public static void SetCustomData(Microsoft.Win32.SafeHandles.SafeFileHandle safe public static void SetOriginalPath(this PlaceholderItem placeholder, string originalPath) { byte[] customDataRaw = placeholder.GetCustomData(); - CustomData customData = (customDataRaw.Length > 0) ? CustomData.Desserialize(customDataRaw) : new CustomData(); + CustomData customData = (customDataRaw.Length > 0) ? CustomData.Deserialize(customDataRaw) : new CustomData(); customData.OriginalPath = originalPath; placeholder.SetCustomData(customData.Serialize()); @@ -90,7 +90,7 @@ public static void SetOriginalPath(this PlaceholderItem placeholder, string orig public static string GetOriginalPath(this PlaceholderItem placeholder) { byte[] customDataRaw = placeholder.GetCustomData(); - CustomData customData = (customDataRaw.Length > 0) ? CustomData.Desserialize(customDataRaw) : new CustomData(); + CustomData customData = (customDataRaw.Length > 0) ? CustomData.Deserialize(customDataRaw) : new CustomData(); return customData.OriginalPath; } diff --git a/WebDAVDrive/Framework/ETag.cs b/ITHit.FileSystem.Samples.Common/ETag.cs similarity index 86% rename from WebDAVDrive/Framework/ETag.cs rename to ITHit.FileSystem.Samples.Common/ETag.cs index 636ba85..9d91de6 100644 --- a/WebDAVDrive/Framework/ETag.cs +++ b/ITHit.FileSystem.Samples.Common/ETag.cs @@ -4,12 +4,12 @@ using System.Text; using System.Threading.Tasks; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { /// /// Provides method for reading and writing ETags. /// - internal static class ETag + public static class ETag { /// /// Creates or updates ETag associated with the file. @@ -56,10 +56,10 @@ public static void DeleteETag(string userFileSystemPath) public static string GetETagFilePath(string userFileSystemPath) { // Get path relative to the virtual root. - string relativePath = Path.TrimEndingDirectorySeparator(userFileSystemPath).Substring( - Path.TrimEndingDirectorySeparator(Program.Settings.UserFileSystemRootPath).Length); + string relativePath = userFileSystemPath.TrimEnd(Path.DirectorySeparatorChar).Substring( + Config.Settings.UserFileSystemRootPath.TrimEnd(Path.DirectorySeparatorChar).Length); - string path = $"{Path.TrimEndingDirectorySeparator(Program.Settings.ServerDataFolderPath)}{relativePath}.etag"; + string path = $"{Config.Settings.ServerDataFolderPath.TrimEnd(Path.DirectorySeparatorChar)}{relativePath}.etag"; return path; } @@ -73,7 +73,7 @@ public static string GetETagFilePath(string userFileSystemPath) /// During client->server update it is sent back to the remote storage together with a modified content. /// This ensures the changes on the server are not overwritten if the document on the server is modified. /// - internal static async Task ETagEqualsAsync(string userFileSystemPath, FileSystemItemBasicInfo remoteStorageItem) + public static async Task ETagEqualsAsync(string userFileSystemPath, FileSystemItemBasicInfo remoteStorageItem) { string remoteStorageETag = remoteStorageItem.ETag; string userFileSystemETag = await ETag.GetETagAsync(userFileSystemPath); diff --git a/WebDAVDrive/Framework/FileBasicInfo.cs b/ITHit.FileSystem.Samples.Common/FileBasicInfo.cs similarity index 65% rename from WebDAVDrive/Framework/FileBasicInfo.cs rename to ITHit.FileSystem.Samples.Common/FileBasicInfo.cs index 541e71f..4c78f80 100644 --- a/WebDAVDrive/Framework/FileBasicInfo.cs +++ b/ITHit.FileSystem.Samples.Common/FileBasicInfo.cs @@ -3,10 +3,10 @@ using System.Collections.Generic; using System.Text; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { /// - internal class FileBasicInfo : FileSystemItemBasicInfo, IFileBasicInfo + public class FileBasicInfo : FileSystemItemBasicInfo, IFileBasicInfo { /// public long Length { get; set; } diff --git a/WebDAVDrive/Framework/FileSystemItemBasicInfo.cs b/ITHit.FileSystem.Samples.Common/FileSystemItemBasicInfo.cs similarity index 79% rename from WebDAVDrive/Framework/FileSystemItemBasicInfo.cs rename to ITHit.FileSystem.Samples.Common/FileSystemItemBasicInfo.cs index b14fc2c..ee8ad23 100644 --- a/WebDAVDrive/Framework/FileSystemItemBasicInfo.cs +++ b/ITHit.FileSystem.Samples.Common/FileSystemItemBasicInfo.cs @@ -4,13 +4,13 @@ using System.IO; using System.Text; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { /// /// Represents a basic information about the file or the folder in the user file system. /// In addition to properties provided by this class contains Etag property. /// - internal class FileSystemItemBasicInfo : IFileSystemItemBasicInfo + public class FileSystemItemBasicInfo : IFileSystemItemBasicInfo { /// public string Name { get; set; } @@ -42,5 +42,10 @@ internal class FileSystemItemBasicInfo : IFileSystemItemBasicInfo /// Indicates if the item is locked by another user in the remote storage. /// public bool LockedByAnotherUser { get; set; } + + /// + /// Custom columns data to be displayed in the file manager. + /// + public IEnumerable CustomProperties { get; set; } } } diff --git a/WebDAVDrive/Framework/FolderBasicInfo.cs b/ITHit.FileSystem.Samples.Common/FolderBasicInfo.cs similarity index 57% rename from WebDAVDrive/Framework/FolderBasicInfo.cs rename to ITHit.FileSystem.Samples.Common/FolderBasicInfo.cs index fec22cb..1436720 100644 --- a/WebDAVDrive/Framework/FolderBasicInfo.cs +++ b/ITHit.FileSystem.Samples.Common/FolderBasicInfo.cs @@ -3,10 +3,10 @@ using System.Collections.Generic; using System.Text; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { /// - internal class FolderBasicInfo : FileSystemItemBasicInfo, IFolderBasicInfo + public class FolderBasicInfo : FileSystemItemBasicInfo, IFolderBasicInfo { } diff --git a/WebDAVDrive/Framework/FsPath.cs b/ITHit.FileSystem.Samples.Common/FsPath.cs similarity index 97% rename from WebDAVDrive/Framework/FsPath.cs rename to ITHit.FileSystem.Samples.Common/FsPath.cs index 85e3b3e..c8001e1 100644 --- a/WebDAVDrive/Framework/FsPath.cs +++ b/ITHit.FileSystem.Samples.Common/FsPath.cs @@ -3,8 +3,10 @@ using System.IO; using System.Text; using System.Threading.Tasks; +using Windows.Storage; +using FileAttributes = System.IO.FileAttributes; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { /// /// Provides file system operations. Helps determining file and folder existence and creating file and folder items. @@ -62,15 +64,15 @@ public static bool IsRecycleBin(string path) /// Instance of or /// that corresponds to path or null if item does not exists. /// - public static async Task GetStorageItemAsync(string path) + public static async Task GetStorageItemAsync(string path) { if (File.Exists(path)) { - return await Windows.Storage.StorageFile.GetFileFromPathAsync(path); + return await StorageFile.GetFileFromPathAsync(path); } if (Directory.Exists(path)) { - return await Windows.Storage.StorageFolder.GetFolderFromPathAsync(path); + return await StorageFolder.GetFolderFromPathAsync(path); } return null; } diff --git a/ITHit.FileSystem.Samples.Common/ITHit.FileSystem.Samples.Common.csproj b/ITHit.FileSystem.Samples.Common/ITHit.FileSystem.Samples.Common.csproj new file mode 100644 index 0000000..05f66f1 --- /dev/null +++ b/ITHit.FileSystem.Samples.Common/ITHit.FileSystem.Samples.Common.csproj @@ -0,0 +1,11 @@ + + + netstandard2.1 + + + + + + + + \ No newline at end of file diff --git a/ITHit.FileSystem.Samples.Common/IUserFile.cs b/ITHit.FileSystem.Samples.Common/IUserFile.cs new file mode 100644 index 0000000..954a484 --- /dev/null +++ b/ITHit.FileSystem.Samples.Common/IUserFile.cs @@ -0,0 +1,13 @@ +using ITHit.FileSystem; +using System.IO; +using System.Threading.Tasks; + +namespace ITHit.FileSystem.Samples.Common +{ + public interface IUserFile : IUserFileSystemItem + { + Task ReadAsync(long offset, long length); + Task UpdateAsync(IFileBasicInfo fileInfo, Stream content = null, ServerLockInfo lockInfo = null); + Task ValidateDataAsync(long offset, long length); + } +} \ No newline at end of file diff --git a/ITHit.FileSystem.Samples.Common/IUserFileSystemItem.cs b/ITHit.FileSystem.Samples.Common/IUserFileSystemItem.cs new file mode 100644 index 0000000..bd73404 --- /dev/null +++ b/ITHit.FileSystem.Samples.Common/IUserFileSystemItem.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace ITHit.FileSystem.Samples.Common +{ + public interface IUserFileSystemItem + { + Task DeleteAsync(); + Task LockAsync(); + Task MoveToAsync(string userFileSystemNewPath); + Task UnlockAsync(string lockToken); + } +} \ No newline at end of file diff --git a/ITHit.FileSystem.Samples.Common/IUserFolder.cs b/ITHit.FileSystem.Samples.Common/IUserFolder.cs new file mode 100644 index 0000000..299b0b9 --- /dev/null +++ b/ITHit.FileSystem.Samples.Common/IUserFolder.cs @@ -0,0 +1,15 @@ +using ITHit.FileSystem; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace ITHit.FileSystem.Samples.Common +{ + public interface IUserFolder : IUserFileSystemItem + { + Task CreateFileAsync(IFileBasicInfo fileInfo, Stream content); + Task CreateFolderAsync(IFolderBasicInfo folderInfo); + Task> EnumerateChildrenAsync(string pattern); + Task UpdateAsync(IFolderBasicInfo folderInfo, ServerLockInfo lockInfo = null); + } +} \ No newline at end of file diff --git a/WebDAVDrive/Framework/Locks/Lock.cs b/ITHit.FileSystem.Samples.Common/Locks/Lock.cs similarity index 92% rename from WebDAVDrive/Framework/Locks/Lock.cs rename to ITHit.FileSystem.Samples.Common/Locks/Lock.cs index 74cd97c..2a1694c 100644 --- a/WebDAVDrive/Framework/Locks/Lock.cs +++ b/ITHit.FileSystem.Samples.Common/Locks/Lock.cs @@ -1,5 +1,4 @@ using ITHit.FileSystem; -using Microsoft.VisualBasic.FileIO; using System; using System.Collections.Generic; using System.IO; @@ -9,7 +8,7 @@ using System.Threading.Tasks; using Windows.System; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { /// /// Represents file lock. @@ -183,17 +182,17 @@ internal bool IsNew() return lockTokenFileStream.Length == 0; } - internal async Task SetLockInfoAsync(LockInfo lockInfo) + internal async Task SetLockInfoAsync(ServerLockInfo lockInfo) { lockTokenFileStream.Seek(0, SeekOrigin.Begin); await JsonSerializer.SerializeAsync(lockTokenFileStream, lockInfo); lockTokenFileStream.SetLength(lockTokenFileStream.Position); } - internal async Task GetLockInfoAsync() + internal async Task GetLockInfoAsync() { lockTokenFileStream.Seek(0, SeekOrigin.Begin); - return await JsonSerializer.DeserializeAsync(lockTokenFileStream); + return await JsonSerializer.DeserializeAsync(lockTokenFileStream); } /// @@ -203,11 +202,11 @@ internal async Task GetLockInfoAsync() /// Path to the file that contains the lock mode. private static string GetLockModeFilePath(string userFileSystemPath) { + // Get path relative to the virtual root. - string relativePath = Path.TrimEndingDirectorySeparator(userFileSystemPath).Substring( - Path.TrimEndingDirectorySeparator(Program.Settings.UserFileSystemRootPath).Length); - - string path = $"{Path.TrimEndingDirectorySeparator(Program.Settings.ServerDataFolderPath)}{relativePath}.lockmode"; + string relativePath = userFileSystemPath.TrimEnd(Path.DirectorySeparatorChar).Substring( + Config.Settings.UserFileSystemRootPath.TrimEnd(Path.DirectorySeparatorChar).Length); + string path = $"{Config.Settings.ServerDataFolderPath.TrimEnd(Path.DirectorySeparatorChar)}{relativePath}.lockmode"; return path; } @@ -219,10 +218,10 @@ private static string GetLockModeFilePath(string userFileSystemPath) private static string GetLockTokenFilePath(string userFileSystemPath) { // Get path relative to the virtual root. - string relativePath = Path.TrimEndingDirectorySeparator(userFileSystemPath).Substring( - Path.TrimEndingDirectorySeparator(Program.Settings.UserFileSystemRootPath).Length); + string relativePath = userFileSystemPath.TrimEnd(Path.DirectorySeparatorChar).Substring( + Config.Settings.UserFileSystemRootPath.TrimEnd(Path.DirectorySeparatorChar).Length); - string path = $"{Path.TrimEndingDirectorySeparator(Program.Settings.ServerDataFolderPath)}{relativePath}.locktoken"; + string path = $"{Config.Settings.ServerDataFolderPath.TrimEnd(Path.DirectorySeparatorChar)}{relativePath}.locktoken"; return path; } diff --git a/WebDAVDrive/Framework/Locks/LockMode.cs b/ITHit.FileSystem.Samples.Common/Locks/LockMode.cs similarity index 94% rename from WebDAVDrive/Framework/Locks/LockMode.cs rename to ITHit.FileSystem.Samples.Common/Locks/LockMode.cs index 681694e..8e104e6 100644 --- a/WebDAVDrive/Framework/Locks/LockMode.cs +++ b/ITHit.FileSystem.Samples.Common/Locks/LockMode.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { /// /// Indicates how the file was locked and how to unlock the file. diff --git a/ITHit.FileSystem.Samples.Common/Locks/ServerLockInfo.cs b/ITHit.FileSystem.Samples.Common/Locks/ServerLockInfo.cs new file mode 100644 index 0000000..f12d195 --- /dev/null +++ b/ITHit.FileSystem.Samples.Common/Locks/ServerLockInfo.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ITHit.FileSystem.Samples.Common +{ + /// + /// Information about the lock returned from the remote storage as a result of the lock operation. + /// + public class ServerLockInfo + { + /// + /// Lock-token. Must be supplied during the item update and unlock operations. + /// + public string LockToken { get; set; } + + /// + /// Lock expidation date/time returned by the server. + /// + public DateTimeOffset LockExpirationDateUtc { get; set; } + + /// + /// Name of the user that locked the item. + /// + public string Owner { get; set; } + + /// + /// True if the item is locked exclusively. False in case the item has a shared lock. + /// + public bool Exclusive { get; set; } + + public IEnumerable GetLockProperties(string lockIconPath) + { + List lockProps = new List(); + lockProps.Add(new FileSystemItemPropertyData(2, Owner, lockIconPath)); + lockProps.Add(new FileSystemItemPropertyData(3, Exclusive ? "Exclusive" : "Shared")); + lockProps.Add(new FileSystemItemPropertyData(4, LockExpirationDateUtc.ToString())); + return lockProps; + } + } +} diff --git a/WebDAVDrive/Framework/Logger.cs b/ITHit.FileSystem.Samples.Common/Logger.cs similarity index 91% rename from WebDAVDrive/Framework/Logger.cs rename to ITHit.FileSystem.Samples.Common/Logger.cs index ded01ca..286f214 100644 --- a/WebDAVDrive/Framework/Logger.cs +++ b/ITHit.FileSystem.Samples.Common/Logger.cs @@ -4,12 +4,14 @@ using System.Text; using System.Threading; -namespace VirtualFileSystem +using ITHit.FileSystem; + +namespace ITHit.FileSystem.Samples.Common { /// /// Implements unified logging. /// - internal class Logger : ITHit.FileSystem.ILogger + public class Logger : ILogger { /// /// Name of the component that is writing to the log. @@ -26,7 +28,7 @@ internal class Logger : ITHit.FileSystem.ILogger /// /// Name of the component that is writing to the log. /// Log4Net Logger. - internal Logger(string componentName, ILog logger) + public Logger(string componentName, ILog logger) { this.componentName = componentName; this.Log = logger; diff --git a/WebDAVDrive/Framework/Registrar.cs b/ITHit.FileSystem.Samples.Common/Registrar.cs similarity index 89% rename from WebDAVDrive/Framework/Registrar.cs rename to ITHit.FileSystem.Samples.Common/Registrar.cs index 35e7e87..c0c0511 100644 --- a/WebDAVDrive/Framework/Registrar.cs +++ b/ITHit.FileSystem.Samples.Common/Registrar.cs @@ -8,9 +8,12 @@ using Windows.Storage; using Windows.Storage.Provider; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { - internal static class Registrar + /// + /// Registers and unregisters sync root. + /// + public static class Registrar { /// /// Registers sync root. @@ -18,14 +21,15 @@ internal static class Registrar /// ID of the sync root. /// A root folder of your user file system. Your file system tree will be located under this folder. /// Human readable display name. + /// Path to the drive ico file. /// Call this method during application installation. - public static async Task RegisterAsync(string syncRootId, string path, string displayName) + public static async Task RegisterAsync(string syncRootId, string path, string displayName, string iconPath) { StorageProviderSyncRootInfo storageInfo = new StorageProviderSyncRootInfo(); storageInfo.Path = await StorageFolder.GetFolderFromPathAsync(path); storageInfo.Id = syncRootId; storageInfo.DisplayNameResource = displayName; - storageInfo.IconResource = Path.Combine(Program.Settings.IconsFolderPath, "Drive.ico"); + storageInfo.IconResource = iconPath; storageInfo.Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); storageInfo.RecycleBinUri = new Uri("https://userfilesystem.com/recyclebin"); storageInfo.Context = CryptographicBuffer.ConvertStringToBinary(path, BinaryStringEncoding.Utf8); @@ -52,16 +56,15 @@ public static async Task RegisterAsync(string syncRootId, string path, string di // Adds columns to Windows File Manager. // Show/hide columns in the "More..." context menu on the columns header. var proDefinitions = storageInfo.StorageProviderItemPropertyDefinitions; - proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "Header 0", Id = 0, }); - proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "Header 1", Id = 1, }); - //proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "Column 2", Id = 2, }); - + proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "Lock Owner" , Id = 2 }); + proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "Lock Scope" , Id = 3 }); + proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "Lock Expires" , Id = 4 }); + proDefinitions.Add(new StorageProviderItemPropertyDefinition { DisplayNameResource = "ETag" , Id = 5 }); + ValidateStorageProviderSyncRootInfo(storageInfo); StorageProviderSyncRootManager.Register(storageInfo); - - Directory.CreateDirectory(Program.Settings.ServerDataFolderPath); } /// @@ -144,8 +147,6 @@ public static async Task IsRegisteredAsync(string path) public static async Task UnregisterAsync(string syncRootId) { StorageProviderSyncRootManager.Unregister(syncRootId); - - Directory.Delete(Program.Settings.ServerDataFolderPath, true); } } } diff --git a/ITHit.FileSystem.Samples.Common/Settings.cs b/ITHit.FileSystem.Samples.Common/Settings.cs new file mode 100644 index 0000000..744114c --- /dev/null +++ b/ITHit.FileSystem.Samples.Common/Settings.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace ITHit.FileSystem.Samples.Common +{ + /// + /// Glogal object for accessing configuration. + /// + public static class Config + { + /// + /// Application settings. + /// + public static Settings Settings { get; set; } + } + + /// + /// Strongly binded project settings. + /// + public class Settings + { + /// + /// Unique ID of this application. + /// + /// + /// If you must to run more than one instance of this application side-by-side on same client machine + /// (aka Corporate Drive and Personal Drive) set unique ID for each instance. + /// + public string AppID { get; set; } + + /// + /// IT Hit User File System license string. + /// + public string UserFileSystemLicense { get; set; } + + /// + /// Your virtual file system will be mounted under this path. + /// + public string UserFileSystemRootPath { get; set; } + + /// + /// Full synchronization interval in milliseconds. + /// + public double SyncIntervalMs { 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. + /// + public string IconsFolderPath { get; set; } + + /// + /// Product name. Displayed as a mounted folder name under Desktop as + /// well in every location where product name is required. + /// + public string ProductName { get; set; } + + /// + /// Path to the folder that stores ETags, locks and other data associated with files and folders. + /// + public string ServerDataFolderPath { get; set; } + + /// + /// Automatically lock the file in remote storage when a file handle is being opened for writing, unlock on close. + /// + public bool AutoLock { get; set; } + } +} diff --git a/WebDAVDrive/Framework/Syncronyzation/ClientToServerSync.cs b/ITHit.FileSystem.Samples.Common/Syncronyzation/ClientToServerSync.cs similarity index 90% rename from WebDAVDrive/Framework/Syncronyzation/ClientToServerSync.cs rename to ITHit.FileSystem.Samples.Common/Syncronyzation/ClientToServerSync.cs index 89aa33d..acb5763 100644 --- a/WebDAVDrive/Framework/Syncronyzation/ClientToServerSync.cs +++ b/ITHit.FileSystem.Samples.Common/Syncronyzation/ClientToServerSync.cs @@ -12,7 +12,7 @@ using Windows.Storage; using Windows.Storage.Provider; -namespace VirtualFileSystem.Syncronyzation +namespace ITHit.FileSystem.Samples.Common.Syncronyzation { /// /// User File System to Remote Storage synchronization. @@ -20,17 +20,23 @@ namespace VirtualFileSystem.Syncronyzation /// In most cases you can use this class in your project without any changes. internal class ClientToServerSync : Logger { + /// + /// Virtual drive. + /// + private VirtualDriveBase virtualDrive; + /// /// Creates instance of this class. /// /// Logger. - internal ClientToServerSync(ILog log) : base("UFS -> RS Sync", log) + internal ClientToServerSync(VirtualDriveBase virtualDrive, ILog log) : base("UFS -> RS Sync", log) { - + this.virtualDrive = virtualDrive; } /// /// Recursively synchronizes all files and folders moved in user file system with the remote storage. + /// Restores Original Path and 'locked' icon that are lost during MS Office transactional save. /// Synchronizes only folders already loaded into the user file system. /// /// Folder path in user file system. @@ -70,7 +76,7 @@ internal async Task SyncronizeMovedAsync(string userFileSystemFolderPath) { // Process items moved in user file system. userFileSystemOldPath = userFileSystemItem.GetOriginalPath(); - await new RemoteStorageRawItem(userFileSystemOldPath, this).MoveToAsync(userFileSystemPath); + await new RemoteStorageRawItem(userFileSystemOldPath, virtualDrive, this).MoveToAsync(userFileSystemPath); } else { @@ -85,7 +91,8 @@ internal async Task SyncronizeMovedAsync(string userFileSystemFolderPath) // Restore the 'locked' icon. bool isLocked = await Lock.IsLockedAsync(userFileSystemPath); - await new UserFileSystemRawItem(userFileSystemPath).SetLockIconAsync(isLocked); + ServerLockInfo existingLock = isLocked ? await (await Lock.LockAsync(userFileSystemPath, FileMode.Open, LockMode.None, this)).GetLockInfoAsync() : null; + await new UserFileSystemRawItem(userFileSystemPath).SetLockInfoAsync(existingLock); } } } @@ -142,12 +149,12 @@ internal async Task SyncronizeFolderAsync(string userFileSystemFolderPath) if (PlaceholderItem.GetItem(userFileSystemPath).IsNew()) { // Create a file/folder in the remote storage. - await RemoteStorageRawItem.CreateAsync(userFileSystemPath, this); + await RemoteStorageRawItem.CreateAsync(userFileSystemPath, virtualDrive, this); } else { // Update file/folder in the remote storage. Unlock if auto-locked. - await new RemoteStorageRawItem(userFileSystemPath, this).UpdateAsync(); + await new RemoteStorageRawItem(userFileSystemPath, virtualDrive, this).UpdateAsync(); } } } diff --git a/WebDAVDrive/Framework/Syncronyzation/FileAttributesExt.cs b/ITHit.FileSystem.Samples.Common/Syncronyzation/FileAttributesExt.cs similarity index 95% rename from WebDAVDrive/Framework/Syncronyzation/FileAttributesExt.cs rename to ITHit.FileSystem.Samples.Common/Syncronyzation/FileAttributesExt.cs index 16bd5a8..9ad6afe 100644 --- a/WebDAVDrive/Framework/Syncronyzation/FileAttributesExt.cs +++ b/ITHit.FileSystem.Samples.Common/Syncronyzation/FileAttributesExt.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace VirtualFileSystem.Syncronyzation +namespace ITHit.FileSystem.Samples.Common.Syncronyzation { /// diff --git a/WebDAVDrive/Framework/Syncronyzation/FullSyncService.cs b/ITHit.FileSystem.Samples.Common/Syncronyzation/FullSyncService.cs similarity index 63% rename from WebDAVDrive/Framework/Syncronyzation/FullSyncService.cs rename to ITHit.FileSystem.Samples.Common/Syncronyzation/FullSyncService.cs index 0d01339..49996c7 100644 --- a/WebDAVDrive/Framework/Syncronyzation/FullSyncService.cs +++ b/ITHit.FileSystem.Samples.Common/Syncronyzation/FullSyncService.cs @@ -12,19 +12,50 @@ using Windows.Storage; using Windows.Storage.Provider; -namespace VirtualFileSystem.Syncronyzation +namespace ITHit.FileSystem.Samples.Common.Syncronyzation { /// - /// Doing full synchronization between the user file system and the remote storage, recursively going through all folders. + /// Performs a full synchronization between the user file system and the remote storage, recursively processing all folders. /// /// /// This is a simple full synchronyzation example. /// - /// You can use this class in your project out of the box or replace with a more advanced algorithm. + /// You can use this class in your project out of the box or replace it with a more advanced algorithm. /// - internal class FullSyncService : Logger, IDisposable + public class FullSyncService : Logger, IDisposable { - System.Timers.Timer timer = null; + /// + /// FullSyncService states. + /// + public enum SynchronizationState + { + Started, + Stopped, + Idle + } + /// + /// Virtual drive. + /// + private VirtualDriveBase virtualDrive; + + /// + /// Timer to start synchronization. + /// + private System.Timers.Timer timer = null; + + + public class SynchEventArgs : EventArgs + { + public SynchronizationState state; + + public SynchEventArgs(SynchronizationState state) + { + this.state = state; + } + } + + public delegate void SyncEvent(object sender, SynchEventArgs synchEventArgs); + public event SyncEvent syncEvent; /// /// User file system path. @@ -37,38 +68,56 @@ internal class FullSyncService : Logger, IDisposable /// Synchronization interval in milliseconds. /// User file system root path. /// Logger. - internal FullSyncService(double syncIntervalMs, string userFileSystemRootPath, ILog log) : base("Sync Service", log) + internal FullSyncService(double syncIntervalMs, string userFileSystemRootPath, VirtualDriveBase virtualDrive, ILog log) : base("Sync Service", log) { timer = new System.Timers.Timer(syncIntervalMs); + timer.Elapsed += Timer_ElapsedAsync; this.userFileSystemRootPath = userFileSystemRootPath; + this.virtualDrive = virtualDrive; } /// /// Starts synchronization. /// - internal async Task StartAsync() + public async Task StartAsync() { - timer.Elapsed += Timer_ElapsedAsync; - // Do not start next synchronyzation automatically, wait until previous synchronyzation completed. timer.AutoReset = false; timer.Start(); - LogMessage($"Started"); } + /// + /// Stops synchronization. + /// + public async Task StopAsync() + { + timer.Stop(); + LogMessage($"Stopped"); + } + + private void InvokeSyncEvent(SynchronizationState state) + { + if (syncEvent != null) + { + syncEvent.Invoke(this, new SynchEventArgs(state)); + } + } + private async void Timer_ElapsedAsync(object sender, System.Timers.ElapsedEventArgs e) { try { + InvokeSyncEvent(SynchronizationState.Started); // UFS -> RS. Recursivery synchronize all moved files and folders present in the user file system. Restore OriginalPath. - await new ClientToServerSync(Log).SyncronizeMovedAsync(userFileSystemRootPath); + await new ClientToServerSync(virtualDrive, Log).SyncronizeMovedAsync(userFileSystemRootPath); // UFS -> RS. Recursivery synchronize all updated/created/deleted file and folders present in the user file system. - await new ClientToServerSync(Log).SyncronizeFolderAsync(userFileSystemRootPath); + await new ClientToServerSync(virtualDrive, Log).SyncronizeFolderAsync(userFileSystemRootPath); // UFS <- RS. Recursivery synchronize all updated/created/deleted file and folders present in the user file system. - await new ServerToClientSync(Log).SyncronizeFolderAsync(userFileSystemRootPath); + await new ServerToClientSync(virtualDrive, Log).SyncronizeFolderAsync(userFileSystemRootPath); + InvokeSyncEvent(SynchronizationState.Stopped); } catch(Exception ex) { diff --git a/WebDAVDrive/Framework/Syncronyzation/RemoteStorageRawItem.cs b/ITHit.FileSystem.Samples.Common/Syncronyzation/RemoteStorageRawItem.cs similarity index 88% rename from WebDAVDrive/Framework/Syncronyzation/RemoteStorageRawItem.cs rename to ITHit.FileSystem.Samples.Common/Syncronyzation/RemoteStorageRawItem.cs index a86ecb3..4ef9f01 100644 --- a/WebDAVDrive/Framework/Syncronyzation/RemoteStorageRawItem.cs +++ b/ITHit.FileSystem.Samples.Common/Syncronyzation/RemoteStorageRawItem.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Windows.ApplicationModel.Activation; -namespace VirtualFileSystem.Syncronyzation +namespace ITHit.FileSystem.Samples.Common.Syncronyzation { /// /// Provides methods for synching the user file system to the remote storage. @@ -22,6 +22,11 @@ public class RemoteStorageRawItem /// private string userFileSystemPath; + /// + /// Virtual drive. + /// + private VirtualDriveBase virtualDrive; + /// /// Logger. /// @@ -32,7 +37,7 @@ public class RemoteStorageRawItem /// /// File or folder path in the user file system. /// Logger. - internal RemoteStorageRawItem(string userFileSystemPath, ILogger logger) + internal RemoteStorageRawItem(string userFileSystemPath, VirtualDriveBase virtualDrive, ILogger logger) { if (string.IsNullOrEmpty(userFileSystemPath)) { @@ -44,6 +49,7 @@ internal RemoteStorageRawItem(string userFileSystemPath, ILogger logger) } this.userFileSystemPath = userFileSystemPath; + this.virtualDrive = virtualDrive; this.logger = logger; } @@ -52,12 +58,12 @@ internal RemoteStorageRawItem(string userFileSystemPath, ILogger logger) /// /// Path to the file or folder in the user file system to be created in the remote storage. /// Logger - public static async Task CreateAsync(string userFileSystemNewItemPath, ILogger logger) + public static async Task CreateAsync(string userFileSystemNewItemPath, VirtualDriveBase userEngine, ILogger logger) { try { logger.LogMessage("Creating item in the remote storage", userFileSystemNewItemPath); - await new RemoteStorageRawItem(userFileSystemNewItemPath, logger).CreateOrUpdateAsync(FileMode.CreateNew); + await new RemoteStorageRawItem(userFileSystemNewItemPath, userEngine, logger).CreateOrUpdateAsync(FileMode.CreateNew); await new UserFileSystemRawItem(userFileSystemNewItemPath).ClearStateAsync(); logger.LogMessage("Created in the remote storage succesefully", userFileSystemNewItemPath); } @@ -83,13 +89,13 @@ public async Task UpdateAsync() { // Lock file if auto-locking is enabled. - if (Program.Settings.AutoLock) + if (Config.Settings.AutoLock) { // Get existing lock or create a new lock. fileLock = await LockAsync(FileMode.OpenOrCreate, LockMode.Auto); } - LockInfo lockInfo = fileLock!=null ? await fileLock.GetLockInfoAsync() : null; + ServerLockInfo lockInfo = fileLock!=null ? await fileLock.GetLockInfoAsync() : null; // Update item in remote storage. logger.LogMessage("Sending to remote storage", userFileSystemPath); @@ -139,7 +145,7 @@ public async Task UpdateAsync() /// Supported modes are and /// /// Information about the lock. Pass null if the item is not locked. - private async Task CreateOrUpdateAsync(FileMode mode, LockInfo lockInfo = null) + private async Task CreateOrUpdateAsync(FileMode mode, ServerLockInfo lockInfo = null) { if ((mode != FileMode.CreateNew) && (mode != FileMode.Open)) { @@ -181,11 +187,13 @@ private async Task CreateOrUpdateAsync(FileMode mode, LockInfo lockInfo = null) if (mode == FileMode.CreateNew) { string userFileSystemParentPath = Path.GetDirectoryName(userFileSystemPath); - eTag = await new UserFolder(userFileSystemParentPath).CreateFileAsync((IFileBasicInfo)info, userFileSystemStream); + IUserFolder userFolder = await virtualDrive.GetItemAsync(userFileSystemParentPath); + eTag = await userFolder.CreateFileAsync((IFileBasicInfo)info, userFileSystemStream); } else { - eTag = await new UserFile(userFileSystemPath, lockInfo).UpdateAsync((IFileBasicInfo)info, userFileSystemStream); + IUserFile userFile = await virtualDrive.GetItemAsync(userFileSystemPath); + eTag = await userFile.UpdateAsync((IFileBasicInfo)info, userFileSystemStream, lockInfo); } } else @@ -193,11 +201,13 @@ private async Task CreateOrUpdateAsync(FileMode mode, LockInfo lockInfo = null) if (mode == FileMode.CreateNew) { string userFileSystemParentPath = Path.GetDirectoryName(userFileSystemPath); - eTag = await new UserFolder(userFileSystemParentPath).CreateFolderAsync((IFolderBasicInfo)info); + IUserFolder userFolder = await virtualDrive.GetItemAsync(userFileSystemParentPath); + eTag = await userFolder.CreateFolderAsync((IFolderBasicInfo)info); } else { - eTag = await new UserFolder(userFileSystemPath, lockInfo).UpdateAsync((IFolderBasicInfo)info); + IUserFolder userFolder = await virtualDrive.GetItemAsync(userFileSystemPath); + eTag = await userFolder.UpdateAsync((IFolderBasicInfo)info, lockInfo); } } await ETag.SetETagAsync(userFileSystemPath, eTag); @@ -286,7 +296,8 @@ internal async Task MoveToAsync(string userFileSystemNewPath, IConfirmationResul eTag = await ETag.GetETagAsync(userFileSystemOldPath); ETag.DeleteETag(userFileSystemOldPath); - await new UserFileSystemItem(userFileSystemOldPath).MoveToAsync(userFileSystemNewPath); + IUserFileSystemItem userFileSystemItemOld = await virtualDrive.GetItemAsync(userFileSystemOldPath); + await userFileSystemItemOld.MoveToAsync(userFileSystemNewPath); updateTargetOnSuccess = true; logger.LogMessage("Moved succesefully in remote storage", userFileSystemOldPath, userFileSystemNewPath); } @@ -349,7 +360,8 @@ internal async Task DeleteAsync() { if (!FsPath.AvoidSync(userFileSystemPath)) { - await new VirtualFileSystem.UserFileSystemItem(userFileSystemPath).DeleteAsync(); + IUserFileSystemItem userFileSystemItem = await virtualDrive.GetItemAsync(userFileSystemPath); + await userFileSystemItem.DeleteAsync(); ETag.DeleteETag(userFileSystemPath); @@ -404,13 +416,14 @@ private async Task LockAsync(FileMode lockFileOpenMode, LockMode lockMode await new UserFileSystemRawItem(userFileSystemPath).SetLockPendingIconAsync(true); // Lock file in remote storage. - LockInfo lockInfo = await new UserFileSystemItem(userFileSystemPath).LockAsync(); + IUserFileSystemItem userFileSystemItem = await virtualDrive.GetItemAsync(userFileSystemPath); + ServerLockInfo lockInfo = await userFileSystemItem.LockAsync(); // Save lock-token in lock-file. await fileLock.SetLockInfoAsync(lockInfo); - // Set locked icon. - await new UserFileSystemRawItem(userFileSystemPath).SetLockIconAsync(true); + // Set locked icon and all lock properties. + await new UserFileSystemRawItem(userFileSystemPath).SetLockInfoAsync(lockInfo); logger.LogMessage($"Locked succesefully. Mode: {lockMode}", userFileSystemPath); } @@ -444,10 +457,11 @@ private async Task UnlockAsync(Lock fileLock) string lockToken = (await fileLock.GetLockInfoAsync()).LockToken; // Ulock file in remote storage. - await new UserFileSystemItem(userFileSystemPath).UnlockAsync(lockToken); + IUserFileSystemItem userFileSystemItem = await virtualDrive.GetItemAsync(userFileSystemPath); + await userFileSystemItem.UnlockAsync(lockToken); // Remove lock icon. - await new UserFileSystemRawItem(userFileSystemPath).SetLockIconAsync(false); + await new UserFileSystemRawItem(userFileSystemPath).SetLockInfoAsync(null); // Operation completed succesefully, delete lock files. fileLock.Unlock(); diff --git a/WebDAVDrive/Framework/Syncronyzation/ServerToClientSync.cs b/ITHit.FileSystem.Samples.Common/Syncronyzation/ServerToClientSync.cs similarity index 80% rename from WebDAVDrive/Framework/Syncronyzation/ServerToClientSync.cs rename to ITHit.FileSystem.Samples.Common/Syncronyzation/ServerToClientSync.cs index 1f75c65..30c96e2 100644 --- a/WebDAVDrive/Framework/Syncronyzation/ServerToClientSync.cs +++ b/ITHit.FileSystem.Samples.Common/Syncronyzation/ServerToClientSync.cs @@ -12,7 +12,7 @@ using Windows.Storage; using Windows.Storage.Provider; -namespace VirtualFileSystem.Syncronyzation +namespace ITHit.FileSystem.Samples.Common.Syncronyzation { /// /// Synchronizes files and folders from remote storage to user file system. @@ -20,12 +20,19 @@ namespace VirtualFileSystem.Syncronyzation /// In most cases you can use this class in your project without any changes. internal class ServerToClientSync : Logger { + /// + /// Virtual drive. + /// + private VirtualDriveBase virtualDrive; + /// /// Creates instance of this class. /// + /// instance. /// Logger. - internal ServerToClientSync(ILog log) : base("UFS <- RS Sync", log) + internal ServerToClientSync(VirtualDriveBase virtualDrive, ILog log) : base("UFS <- RS Sync", log) { + this.virtualDrive = virtualDrive; } /// @@ -47,10 +54,10 @@ internal async Task SyncronizeFolderAsync(string userFileSystemFolderPath) IEnumerable userFileSystemChildren = Directory.EnumerateFileSystemEntries(userFileSystemFolderPath, "*"); //LogMessage("Synchronizing:", userFileSystemFolderPath); - IEnumerable remoteStorageChildrenItems = await new UserFolder(userFileSystemFolderPath).EnumerateChildrenAsync("*"); + IUserFolder userFolder = await virtualDrive.GetItemAsync(userFileSystemFolderPath); + IEnumerable remoteStorageChildrenItems = await userFolder.EnumerateChildrenAsync("*"); // Create new files/folders in the user file system. - string remoteStorageFolderPath = Mapping.MapPath(userFileSystemFolderPath); foreach (FileSystemItemBasicInfo remoteStorageItem in remoteStorageChildrenItems) { string userFileSystemPath = Path.Combine(userFileSystemFolderPath, remoteStorageItem.Name); @@ -58,12 +65,19 @@ internal async Task SyncronizeFolderAsync(string userFileSystemFolderPath) { // We do not want to sync MS Office temp files, etc. from remote storage. // We also do not want to create MS Office files during transactional save in user file system. - string remoteStorageItemFullPath = Path.Combine(remoteStorageFolderPath, remoteStorageItem.Name); - if (!FsPath.AvoidSync(remoteStorageItemFullPath) && !FsPath.AvoidSync(userFileSystemPath)) + if (!FsPath.AvoidSync(remoteStorageItem.Name) && !FsPath.AvoidSync(userFileSystemPath)) { if (!FsPath.Exists(userFileSystemPath)) { LogMessage($"Creating", userFileSystemPath); + + // If the file is moved/renamed and the app is not running this will help us + // to sync the file/folder to remote storage after app starts. + remoteStorageItem.CustomData = new CustomData + { + OriginalPath = userFileSystemPath + }.Serialize(); + await UserFileSystemRawItem.CreateAsync(userFileSystemFolderPath, new[] { remoteStorageItem }); LogMessage($"Created succesefully", userFileSystemPath); } @@ -82,11 +96,10 @@ internal async Task SyncronizeFolderAsync(string userFileSystemFolderPath) try { string itemName = Path.GetFileName(userFileSystemPath); - string remoteStoragePath = Mapping.MapPath(userFileSystemPath); FileSystemItemBasicInfo remoteStorageItem = remoteStorageChildrenItems.FirstOrDefault(x => x.Name.Equals(itemName, StringComparison.InvariantCultureIgnoreCase)); - if (!FsPath.AvoidSync(userFileSystemPath) && !FsPath.AvoidSync(remoteStoragePath)) + if (!FsPath.AvoidSync(userFileSystemPath)) { if (remoteStorageItem == null) { @@ -104,20 +117,20 @@ internal async Task SyncronizeFolderAsync(string userFileSystemFolderPath) && !await ETag.ETagEqualsAsync(userFileSystemPath, remoteStorageItem)) { // User file system <- remote storage update. - LogMessage("Item modified", remoteStoragePath); + LogMessage("Remote item modified", userFileSystemPath); await new UserFileSystemRawItem(userFileSystemPath).UpdateAsync(remoteStorageItem); LogMessage("Updated succesefully", userFileSystemPath); } - // Update "locked by another user" icon. + // Set the "locked by another user" icon and all custom columns data. if(PlaceholderItem.GetItem(userFileSystemPath).GetInSync()) - { await new UserFileSystemRawItem(userFileSystemPath).SetLockedByAnotherUserAsync(remoteStorageItem.LockedByAnotherUser); + await new UserFileSystemRawItem(userFileSystemPath).SetCustomColumnsDataAsync(remoteStorageItem.CustomProperties); } // Hydrate / dehydrate the file. - if(new UserFileSystemRawItem(userFileSystemPath).HydrationRequired()) + if (new UserFileSystemRawItem(userFileSystemPath).HydrationRequired()) { LogMessage("Hydrating", userFileSystemPath); new PlaceholderFile(userFileSystemPath).Hydrate(0, -1); diff --git a/WebDAVDrive/Framework/Syncronyzation/UserFileSystemMonitor.cs b/ITHit.FileSystem.Samples.Common/Syncronyzation/UserFileSystemMonitor.cs similarity index 95% rename from WebDAVDrive/Framework/Syncronyzation/UserFileSystemMonitor.cs rename to ITHit.FileSystem.Samples.Common/Syncronyzation/UserFileSystemMonitor.cs index 1945fbd..a7f5dc2 100644 --- a/WebDAVDrive/Framework/Syncronyzation/UserFileSystemMonitor.cs +++ b/ITHit.FileSystem.Samples.Common/Syncronyzation/UserFileSystemMonitor.cs @@ -12,7 +12,7 @@ using Windows.Storage.Provider; using Windows.System.Update; -namespace VirtualFileSystem.Syncronyzation +namespace ITHit.FileSystem.Samples.Common.Syncronyzation { /// /// Monitors files and folders creation as well as attributes change in the user file system. @@ -36,21 +36,22 @@ internal class UserFileSystemMonitor : Logger, IDisposable /// private string userFileSystemRootPath; + /// + /// Virtual drive. + /// + private VirtualDriveBase vilrtualDrive; + /// /// Creates instance of this class. /// /// User file system root path. Attributes are monitored in this folder. /// Logger. - internal UserFileSystemMonitor(string userFileSystemRootPath, ILog log) : base("User File System Monitor", log) + internal UserFileSystemMonitor(string userFileSystemRootPath, VirtualDriveBase virtualDrive, ILog log) : base("User File System Monitor", log) { + this.userFileSystemRootPath = userFileSystemRootPath; - } + this.vilrtualDrive = virtualDrive; - /// - /// Starts monitoring attributes changes in user file system. - /// - internal async Task StartAsync() - { watcher.IncludeSubdirectories = true; watcher.Path = userFileSystemRootPath; //watcher.Filter = "*.*"; @@ -60,12 +61,18 @@ internal async Task StartAsync() watcher.Changed += ChangedAsync; watcher.Deleted += DeletedAsync; watcher.Renamed += RenamedAsync; + } + + /// + /// Starts monitoring attributes changes in user file system. + /// + internal async Task StartAsync() + { watcher.EnableRaisingEvents = true; - LogMessage($"Started"); + LogMessage("Started"); } - /// /// Called when a file or folder is created in the user file system. /// @@ -93,7 +100,7 @@ private async void CreatedAsync(object sender, FileSystemEventArgs e) // Create the file/folder in the remote storage. try { - await RemoteStorageRawItem.CreateAsync(userFileSystemPath, this); + await RemoteStorageRawItem.CreateAsync(userFileSystemPath, vilrtualDrive, this); } catch (IOException ex) { diff --git a/WebDAVDrive/Framework/Syncronyzation/UserFileSystemRawItem.cs b/ITHit.FileSystem.Samples.Common/Syncronyzation/UserFileSystemRawItem.cs similarity index 82% rename from WebDAVDrive/Framework/Syncronyzation/UserFileSystemRawItem.cs rename to ITHit.FileSystem.Samples.Common/Syncronyzation/UserFileSystemRawItem.cs index d2a8c17..085ac0f 100644 --- a/WebDAVDrive/Framework/Syncronyzation/UserFileSystemRawItem.cs +++ b/ITHit.FileSystem.Samples.Common/Syncronyzation/UserFileSystemRawItem.cs @@ -11,14 +11,14 @@ using Windows.Storage; using Windows.Storage.Provider; -namespace VirtualFileSystem.Syncronyzation +namespace ITHit.FileSystem.Samples.Common.Syncronyzation { /// /// Provides methods for synching the from remote storage to the user file system. /// Creates, updates and deletes placeholder files and folders based on the info from the remote storage. /// /// In most cases you can use this class in your project without any changes. - internal class UserFileSystemRawItem + public class UserFileSystemRawItem { /// /// Path to the file or folder placeholder in user file system. @@ -30,7 +30,7 @@ internal class UserFileSystemRawItem /// /// File or folder path in user file system. /// Logger. - internal UserFileSystemRawItem(string userFileSystemPath) + public UserFileSystemRawItem(string userFileSystemPath) { if(string.IsNullOrEmpty(userFileSystemPath)) { @@ -40,20 +40,6 @@ internal UserFileSystemRawItem(string userFileSystemPath) this.userFileSystemPath = userFileSystemPath; } - /* - /// - /// Compares user file system item and remote storage item. Returns true if items are equal. False - otherwise. - /// - /// Remote storage item info. - /// Returns true if user file system item and remote storage item are equal. False - otherwise. - public async Task EqualsAsync(FileSystemInfo remoteStorageItem) - { - // Here you will typically compare file content. - // For the sake of simplicity we just compare LastWriteTime. - return remoteStorageItem.LastWriteTime.Equals(FsPath.GetFileSystemItem(userFileSystemPath).LastWriteTime); - } - */ - //$ /// Creates a new file or folder placeholder in user file system. @@ -61,7 +47,7 @@ public async Task EqualsAsync(FileSystemInfo remoteStorageItem) /// User file system folder path in which the new item will be created. /// Array of new files and folders. /// Number of items created. - internal static async Task CreateAsync(string userFileSystemParentPath, FileSystemItemBasicInfo[] newItemsInfo) + public static async Task CreateAsync(string userFileSystemParentPath, FileSystemItemBasicInfo[] newItemsInfo) { try { @@ -79,8 +65,9 @@ internal static async Task CreateAsync(string userFileSystemParentPath, Fi string userFileSystemItemPath = Path.Combine(userFileSystemParentPath, child.Name); await ETag.SetETagAsync(userFileSystemItemPath, child.ETag); - // Set the lock icon and read-only attribute, to indicate that the item is locked by another user. + // Set the "locked by another user" icon and all custom columns data. await new UserFileSystemRawItem(userFileSystemItemPath).SetLockedByAnotherUserAsync(child.LockedByAnotherUser); + await new UserFileSystemRawItem(userFileSystemItemPath).SetCustomColumnsDataAsync(child.CustomProperties); } return created; @@ -107,7 +94,7 @@ internal static async Task CreateAsync(string userFileSystemParentPath, Fi /// This method failes if the file or folder in user file system is modified (not in-sync with the remote storage). /// New file or folder info. /// True if the file was updated. False - otherwise. - internal async Task UpdateAsync(FileSystemItemBasicInfo itemInfo) + public async Task UpdateAsync(FileSystemItemBasicInfo itemInfo) { try { @@ -131,8 +118,9 @@ internal async Task UpdateAsync(FileSystemItemBasicInfo itemInfo) // Clear icon. //await ClearStateAsync(); - // Set the lock icon and read-only attribute, to indicate that the item is locked by another user. + // Set the "locked by another user" icon and all custom columns data. await SetLockedByAnotherUserAsync(itemInfo.LockedByAnotherUser); + await SetCustomColumnsDataAsync(itemInfo.CustomProperties); return true; } @@ -372,12 +360,17 @@ private async Task SetUploadPendingIconAsync(bool set) } /// - /// Sets or removes "Lock" icon. + /// Sets or removes "Lock" icon and all lock properties. /// /// True to display the icon. False - to remove the icon. - internal async Task SetLockIconAsync(bool set) + internal async Task SetLockInfoAsync(ServerLockInfo lockInfo) { - await SetIconAsync(set, 2, "Locked.ico", "The item is locked"); + IEnumerable lockProps = null; + if (lockInfo != null) + { + lockProps = lockInfo.GetLockProperties(Path.Combine(Config.Settings.IconsFolderPath, "Locked.ico")); + } + await SetCustomColumnsDataAsync(lockProps); } /// @@ -389,15 +382,6 @@ internal async Task SetLockPendingIconAsync(bool set) await SetIconAsync(set, 2, "LockedPending.ico", "Updating lock..."); } - /// - /// Sets or removes "Locked another another user" icon. - /// - /// True to display the icon. False - to remove the icon. - private async Task SetLockedByAnotherUserIconAsync(bool set) - { - await SetIconAsync(set, 2, "LockedByAnotherUser.ico", "The item is locked by another user"); - } - /// /// Sets or removes icon. /// @@ -419,7 +403,7 @@ private async Task SetIconAsync(bool set, int? id = null, string iconFile = null { Id = id.Value, Value = description, - IconResource = Path.Combine(Program.Settings.IconsFolderPath, iconFile) + IconResource = Path.Combine(Config.Settings.IconsFolderPath, iconFile) }; await StorageProviderItemProperties.SetAsync(storageItem, new StorageProviderItemProperty[] { propState }); } @@ -453,12 +437,11 @@ private async Task SetIconAsync(bool set, int? id = null, string iconFile = null } /// - /// Sets or removes "Locked another another user" icon and read-only attribute on files. + /// Sets or removes read-only attribute on files. /// - /// True to display the icon and read-only attribute. False - to remove the icon and read-only attribute. - internal async Task SetLockedByAnotherUserAsync(bool set) + /// True to set the read-only attribute. False - to remove the read-only attribute. + public async Task SetLockedByAnotherUserAsync(bool set) { - // Can not set icon on read-only files. // Changing read-only attribute on folders triggers folders listing. Changing it on files only. if(FsPath.IsFile(userFileSystemPath)) @@ -466,25 +449,60 @@ internal async Task SetLockedByAnotherUserAsync(bool set) FileInfo file = new FileInfo(userFileSystemPath); if(set != file.IsReadOnly) { - // Remove read-only attribute. - new FileInfo(userFileSystemPath).Attributes &= ~System.IO.FileAttributes.ReadOnly; - - // Update the lock icon, to indicate that the item is locked by another user. - await SetLockedByAnotherUserIconAsync(set); - - // Set read-only attribute. + // Set/Remove read-only attribute. if (set) { new FileInfo(userFileSystemPath).Attributes |= System.IO.FileAttributes.ReadOnly; } + else + { + new FileInfo(userFileSystemPath).Attributes &= ~System.IO.FileAttributes.ReadOnly; + } } } - else + } + + internal async Task SetCustomColumnsDataAsync(IEnumerable customColumnsData) + { + List customColumns = new List(); + + if (customColumnsData != null) { - // Update the lock icon, to indicate that the item is locked by another user. - await SetLockedByAnotherUserIconAsync(set); + foreach (FileSystemItemPropertyData column in customColumnsData) + { + customColumns.Add(new StorageProviderItemProperty() + { + Id = column.Id, + // If value is empty Windows File Manager crushes. + Value = string.IsNullOrEmpty(column.Value) ? "-" : column.Value, + // If icon is not set Windows File Manager crushes. + IconResource = column.IconResource ?? Path.Combine(Config.Settings.IconsFolderPath, "Blank.ico") + }); + } } - } + // Can not set provider properties on read-only files. + // Changing read-only attribute on folders triggers folders listing. Changing it on files only. + FileInfo file = new FileInfo(userFileSystemPath); + bool readOnly = file.IsReadOnly; + + // Remove read-only attribute. + if (readOnly && ((file.Attributes & System.IO.FileAttributes.Directory) == 0)) + { + file.IsReadOnly = false; + //new FileInfo(userFileSystemPath).Attributes &= ~System.IO.FileAttributes.ReadOnly; + } + + // Update columns data. + IStorageItem storageItem = await FsPath.GetStorageItemAsync(userFileSystemPath); + await StorageProviderItemProperties.SetAsync(storageItem, customColumns); + + // Set read-only attribute. + if (readOnly && ((file.Attributes & System.IO.FileAttributes.Directory) == 0)) + { + file.IsReadOnly = true; + //new FileInfo(userFileSystemPath).Attributes |= System.IO.FileAttributes.ReadOnly; + } + } } } diff --git a/WebDAVDrive/Framework/VfsEngine.cs b/ITHit.FileSystem.Samples.Common/VfsEngine.cs similarity index 83% rename from WebDAVDrive/Framework/VfsEngine.cs rename to ITHit.FileSystem.Samples.Common/VfsEngine.cs index 117a971..082e835 100644 --- a/WebDAVDrive/Framework/VfsEngine.cs +++ b/ITHit.FileSystem.Samples.Common/VfsEngine.cs @@ -1,22 +1,19 @@ -using ITHit.FileSystem; -using ITHit.FileSystem.Windows; +using ITHit.FileSystem.Windows; using log4net; -using System; -using System.Collections.Generic; using System.IO; -using System.Text; -using System.Threading; using System.Threading.Tasks; -using Windows.Media.Playback; -using Windows.Storage; -using Windows.Storage.Provider; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { // In most cases you can use this class in your project without any changes. /// internal class VfsEngine : EngineWindows { + /// + /// Virtual drive. + /// + private VirtualDriveBase virtualDrive; + /// /// Logger. /// @@ -34,10 +31,12 @@ internal class VfsEngine : EngineWindows /// A license string. /// A root folder of your user file system. Your file system tree will be located under this folder. /// Logger. - internal VfsEngine(string license, string path, ILog log) : base(license, path) + internal VfsEngine(string license, string path, VirtualDriveBase virtualDrive, ILog log) : base(license, path) { logger = new Logger("File System Engine", log); + this.virtualDrive = virtualDrive; + // We want our file system to run regardless of any errors. // If any request to file system fails in user code or in Engine itself we continue processing. ThrowExceptions = false; @@ -53,11 +52,11 @@ public override async Task GetFileSystemItemAsync(string path) { if(File.Exists(path)) { - return new VfsFile(path, this, this); + return new VfsFile(path, this, this, virtualDrive); } if(Directory.Exists(path)) { - return new VfsFolder(path, this, this); + return new VfsFolder(path, this, this, virtualDrive); } // When a file handle is being closed during delete, the file does not exist, return null. @@ -70,7 +69,7 @@ public override async Task GetFileSystemItemAsync(string path) /// //private static string lastMessage = null; - private void Engine_Message(Engine sender, Engine.MessageEventArgs e) + private void Engine_Message(IEngine sender, EngineMessageEventArgs e) { logger.LogMessage(e.Message, e.SourcePath, e.TargetPath); @@ -89,7 +88,7 @@ private void Engine_Message(Engine sender, Engine.MessageEventArgs e) */ } - private void Engine_Error(Engine sender, Engine.ErrorEventArgs e) + private void Engine_Error(IEngine sender, EngineErrorEventArgs e) { logger.LogError(e.Message, e.SourcePath, e.TargetPath, e.Exception); } diff --git a/WebDAVDrive/Framework/VfsFile.cs b/ITHit.FileSystem.Samples.Common/VfsFile.cs similarity index 88% rename from WebDAVDrive/Framework/VfsFile.cs rename to ITHit.FileSystem.Samples.Common/VfsFile.cs index e156780..a52865f 100644 --- a/WebDAVDrive/Framework/VfsFile.cs +++ b/ITHit.FileSystem.Samples.Common/VfsFile.cs @@ -6,9 +6,9 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using VirtualFileSystem.Syncronyzation; +using ITHit.FileSystem.Samples.Common.Syncronyzation; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { // In most cases you can use this class in your project without any changes. /// @@ -20,7 +20,7 @@ internal class VfsFile : VfsFileSystemItem, IFile /// /// File path in user file system. /// Logger. - public VfsFile(string path, ILogger logger, VfsEngine engine) : base(path, logger, engine) + public VfsFile(string path, ILogger logger, VfsEngine engine, VirtualDriveBase userEngine) : base(path, logger, engine, userEngine) { } @@ -34,7 +34,7 @@ public async Task OpenAsync(IOperationContext operationContext, IResultContext c string userFileSystemFilePath = UserFileSystemPath; if (Engine.ChangesProcessingEnabled && FsPath.Exists(userFileSystemFilePath)) { - if (Program.Settings.AutoLock + if (Config.Settings.AutoLock && !FsPath.AvoidAutoLock(userFileSystemFilePath) && ! await Lock.IsLockedAsync(userFileSystemFilePath) && FsPath.IsWriteLocked(userFileSystemFilePath) @@ -42,7 +42,7 @@ public async Task OpenAsync(IOperationContext operationContext, IResultContext c { try { - await new RemoteStorageRawItem(userFileSystemFilePath, Logger).LockAsync(LockMode.Auto); + await new RemoteStorageRawItem(userFileSystemFilePath, VirtualDrive, Logger).LockAsync(LockMode.Auto); } catch(ClientLockFailedException ex) { @@ -84,12 +84,12 @@ public async Task CloseAsync(IOperationContext operationContext, IResultContext if (PlaceholderItem.GetItem(userFileSystemFilePath).IsNew()) { // Create new file in the remote storage. - await RemoteStorageRawItem.CreateAsync(userFileSystemFilePath, Logger); + await RemoteStorageRawItem.CreateAsync(userFileSystemFilePath, VirtualDrive, Logger); } else { // Send content to remote storage. Unlock if auto-locked. - await new RemoteStorageRawItem(userFileSystemFilePath, Logger).UpdateAsync(); + await new RemoteStorageRawItem(userFileSystemFilePath, VirtualDrive, Logger).UpdateAsync(); } } catch (IOException ex) @@ -116,7 +116,8 @@ public async Task TransferDataAsync(long offset, long length, ITransferDataOpera long optionalLength = length + operationContext.OptionalLength; - byte[] buffer = await new UserFile(UserFileSystemPath).ReadAsync(offset, optionalLength); + IUserFile userFile = await VirtualDrive.GetItemAsync(UserFileSystemPath); + byte[] buffer = await userFile.ReadAsync(offset, optionalLength); resultContext.ReturnData(buffer, offset, optionalLength); } @@ -132,7 +133,8 @@ public async Task ValidateDataAsync(long offset, long length, IValidateDataOpera //SimulateNetworkDelay(length, resultContext); - bool isValid = await new UserFile(UserFileSystemPath).ValidateDataAsync(offset, length); + IUserFile userFile = await VirtualDrive.GetItemAsync(UserFileSystemPath); + bool isValid = await userFile.ValidateDataAsync(offset, length); resultContext.ReturnValidationResult(offset, length, isValid); } diff --git a/WebDAVDrive/Framework/VfsFileSystemItem.cs b/ITHit.FileSystem.Samples.Common/VfsFileSystemItem.cs similarity index 82% rename from WebDAVDrive/Framework/VfsFileSystemItem.cs rename to ITHit.FileSystem.Samples.Common/VfsFileSystemItem.cs index ae1f2da..ad47417 100644 --- a/WebDAVDrive/Framework/VfsFileSystemItem.cs +++ b/ITHit.FileSystem.Samples.Common/VfsFileSystemItem.cs @@ -6,11 +6,11 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using VirtualFileSystem.Syncronyzation; +using ITHit.FileSystem.Samples.Common.Syncronyzation; using Windows.Storage; using Windows.Storage.Provider; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { // In most cases you can use this class in your project without any changes. /// @@ -31,12 +31,17 @@ internal abstract class VfsFileSystemItem : IFileSystemItem /// protected readonly VfsEngine Engine; + /// + /// Virtual drive. + /// + protected VirtualDriveBase VirtualDrive; + /// /// Creates instance of this class. /// /// File or folder path in user file system. /// Logger. - public VfsFileSystemItem(string userFileSystemPath, ILogger logger, VfsEngine engine) + public VfsFileSystemItem(string userFileSystemPath, ILogger logger, VfsEngine engine, VirtualDriveBase virtualDrive) { if (string.IsNullOrEmpty(userFileSystemPath)) { @@ -51,6 +56,7 @@ public VfsFileSystemItem(string userFileSystemPath, ILogger logger, VfsEngine en UserFileSystemPath = userFileSystemPath; Logger = logger; Engine = engine; + VirtualDrive = virtualDrive; } //$ + /// + public Task GetBasicInfoAsync() + { + // Return IFileBasicInfo for a file, IFolderBasicInfo for a folder. + throw new NotImplementedException(); + } + /// /// Simulates network delays and reports file transfer progress for demo purposes. /// @@ -126,13 +140,13 @@ public async Task DeleteAsync(IOperationContext operationContext, IConfirmationR /// Context to report progress to. protected void SimulateNetworkDelay(long fileLength, IResultContext resultContext) { - if (Program.Settings.NetworkSimulationDelayMs > 0) + if (Config.Settings.NetworkSimulationDelayMs > 0) { int numProgressResults = 5; for (int i = 0; i < numProgressResults; i++) { resultContext.ReportProgress(fileLength, i * fileLength / numProgressResults); - Thread.Sleep(Program.Settings.NetworkSimulationDelayMs); + Thread.Sleep(Config.Settings.NetworkSimulationDelayMs); } } } diff --git a/WebDAVDrive/Framework/VfsFolder.cs b/ITHit.FileSystem.Samples.Common/VfsFolder.cs similarity index 68% rename from WebDAVDrive/Framework/VfsFolder.cs rename to ITHit.FileSystem.Samples.Common/VfsFolder.cs index 729f46a..1e7f8d2 100644 --- a/WebDAVDrive/Framework/VfsFolder.cs +++ b/ITHit.FileSystem.Samples.Common/VfsFolder.cs @@ -8,17 +8,18 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using VirtualFileSystem.Syncronyzation; +using ITHit.FileSystem.Samples.Common.Syncronyzation; using Windows.Storage; +using Windows.Storage.Provider; -namespace VirtualFileSystem +namespace ITHit.FileSystem.Samples.Common { // In most cases you can use this class in your project without any changes. //$ internal class VfsFolder : VfsFileSystemItem, IFolder { - public VfsFolder(string path, ILogger logger, VfsEngine engine) : base(path, logger, engine) + public VfsFolder(string path, ILogger logger, VfsEngine engine, VirtualDriveBase userEngine) : base(path, logger, engine, userEngine) { } @@ -33,16 +34,25 @@ public async Task GetChildrenAsync(string pattern, IOperationContext operationCo Logger.LogMessage($"IFolder.GetChildrenAsync({pattern})", UserFileSystemPath); - IEnumerable children = await new UserFolder(UserFileSystemPath).EnumerateChildrenAsync(pattern); + IUserFolder userFolder = await VirtualDrive.GetItemAsync(UserFileSystemPath); + IEnumerable children = await userFolder.EnumerateChildrenAsync(pattern); // Filtering existing files/folders. This is only required to avoid extra errors in the log. List newChildren = new List(); - foreach (IFileSystemItemBasicInfo child in children) + foreach (FileSystemItemBasicInfo child in children) { string userFileSystemItemPath = Path.Combine(UserFileSystemPath, child.Name); if (!FsPath.Exists(userFileSystemItemPath)) { Logger.LogMessage("Creating", child.Name); + + // If the file is moved/renamed and the app is not running this will help us + // to sync the file/folder to remote storage after app starts. + child.CustomData = new CustomData + { + OriginalPath = userFileSystemItemPath + }.Serialize(); + newChildren.Add(child); } } @@ -61,10 +71,11 @@ public async Task GetChildrenAsync(string pattern, IOperationContext operationCo // ETags must correspond with a server file/folder, NOT with a client placeholder. // It should NOT be moved/deleted/updated when a placeholder in the user file system is moved/deleted/updated. // It should be moved/deleted when a file/folder in the remote storage is moved/deleted. - ETag.SetETagAsync(userFileSystemItemPath, child.ETag); + await ETag.SetETagAsync(userFileSystemItemPath, child.ETag); - // Set the lock icon and read-only attribute, to indicate that the item is locked by another user. - new UserFileSystemRawItem(userFileSystemItemPath).SetLockedByAnotherUserAsync(child.LockedByAnotherUser); + // Set the "locked by another user" icon and all custom columns data. + await new UserFileSystemRawItem(userFileSystemItemPath).SetLockedByAnotherUserAsync(child.LockedByAnotherUser); + await new UserFileSystemRawItem(userFileSystemItemPath).SetCustomColumnsDataAsync(child.CustomProperties); } } } diff --git a/ITHit.FileSystem.Samples.Common/VirtualDriveBase.cs b/ITHit.FileSystem.Samples.Common/VirtualDriveBase.cs new file mode 100644 index 0000000..7689f0b --- /dev/null +++ b/ITHit.FileSystem.Samples.Common/VirtualDriveBase.cs @@ -0,0 +1,146 @@ +using ITHit.FileSystem; +using log4net; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using ITHit.FileSystem.Samples.Common.Syncronyzation; + +namespace ITHit.FileSystem.Samples.Common +{ + /// + /// Represents a virtual drive. Processes OS file system calls, + /// synchronizes user file system to remote storage and back, + /// monitors files pinning and unpinning. + /// + /// + /// This class calls and interfaces. + /// + public abstract class VirtualDriveBase : IDisposable + { + /// + /// Processes file system calls, implements on-demand folders listing + /// and initial on-demand file content transfer from remote storage to client. + /// + private VfsEngine engine; + + /// + /// Monitors pinned and unpinned attributes in user file system. + /// + private UserFileSystemMonitor userFileSystemMonitor; + + /// + /// Performs complete synchronyzation of the folders and files that are already synched to user file system. + /// + public FullSyncService SyncService; + + /// + /// Creates instance of this class. + /// + /// A license string. + /// + /// A root folder of your user file system. Your file system tree will be located under this folder. + /// + /// Log4net logger. + /// Full synchronization interval in milliseconds. + public VirtualDriveBase(string license, string userFileSystemRootPath, ILog log, double syncIntervalMs) + { + engine = new VfsEngine(license, userFileSystemRootPath, this, log); + SyncService = new FullSyncService(syncIntervalMs, userFileSystemRootPath, this, log); + userFileSystemMonitor = new UserFileSystemMonitor(userFileSystemRootPath, this, log); + } + + /// + /// Gets file or folder object corresponding to path in user file system. + /// + /// + /// Path in user file system for which your will return a file or a folder. + /// + /// + /// + /// This is a factory method that returns files and folders in your user file system. + /// In your implementation you will return file or folder object that corresponds to provided parameter. + /// Your file must implement interface. Your folder must implement interface. + /// + /// + /// The will then call and methods to get the + /// required information and pass it to the platform. + /// + /// + /// Note that this method may be called for files that does not exist in the user file system, + /// for example when a file handle is closed after the file has been deleted. + /// + /// + /// File or folder object that corresponds to the path in user file system. + public abstract Task GetUserFileSystemItemAsync(string userFileSystemPath); + + /// + /// This is a strongly typed variant of GetUserFileSystemItemAsync() method for internal use. + /// + /// This is either or or . + /// Path in user file system for which your will return a file or a folder. + /// File or folder object that corresponds to the path in user file system. + internal async Task GetItemAsync(string userFileSystemPath) where T : class, IUserFileSystemItem + { + IUserFileSystemItem userItem = await GetUserFileSystemItemAsync(userFileSystemPath); + if (userItem as T == null) + { + throw new NotImplementedException($"{typeof(T).Name}"); + } + + return userItem as T; + } + + /// + /// Starts processing OS file system calls, starts user file system to remote storage synchronization + /// and remote storage to user file system synchronization + /// as well as starts monitoring for pinned/unpinned files. + /// + public async Task StartAsync() + { + // Start processing OS file system calls. + engine.ChangesProcessingEnabled = true; + await engine.StartAsync(); + + // Start periodical synchronyzation between client and server, + // in case any changes are lost because the client or the server were unavailable. + await SyncService.StartAsync(); + + // Start monitoring pinned/unpinned attributes and files/folders creation in user file system. + await userFileSystemMonitor.StartAsync(); + } + + private bool disposedValue; + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + engine.Dispose(); + SyncService.Dispose(); + userFileSystemMonitor.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~UserEngine() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/UserFileSystemSamples.sln b/UserFileSystemSamples.sln new file mode 100644 index 0000000..cdc2b32 --- /dev/null +++ b/UserFileSystemSamples.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31019.35 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebDAVDrive", "WebDAVDrive\WebDAVDrive.csproj", "{E23287B1-C11F-45F4-A0E0-F86F0DE9A719}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ITHit.FileSystem.Samples.Common", "ITHit.FileSystem.Samples.Common\ITHit.FileSystem.Samples.Common.csproj", "{63C11274-D693-4C19-A83C-390B26D8B65B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebDAVDrive.UI", "WebDAVDrive.UI\WebDAVDrive.UI.csproj", "{9729AEEC-2642-4011-9849-1C957141C6FF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualFileSystem", "VirtualFileSystem\VirtualFileSystem.csproj", "{F83D05C4-EF6D-4BD9-99C4-B404443437F0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E23287B1-C11F-45F4-A0E0-F86F0DE9A719}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E23287B1-C11F-45F4-A0E0-F86F0DE9A719}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E23287B1-C11F-45F4-A0E0-F86F0DE9A719}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E23287B1-C11F-45F4-A0E0-F86F0DE9A719}.Release|Any CPU.Build.0 = Release|Any CPU + {63C11274-D693-4C19-A83C-390B26D8B65B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63C11274-D693-4C19-A83C-390B26D8B65B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63C11274-D693-4C19-A83C-390B26D8B65B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63C11274-D693-4C19-A83C-390B26D8B65B}.Release|Any CPU.Build.0 = Release|Any CPU + {9729AEEC-2642-4011-9849-1C957141C6FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9729AEEC-2642-4011-9849-1C957141C6FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9729AEEC-2642-4011-9849-1C957141C6FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9729AEEC-2642-4011-9849-1C957141C6FF}.Release|Any CPU.Build.0 = Release|Any CPU + {F83D05C4-EF6D-4BD9-99C4-B404443437F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F83D05C4-EF6D-4BD9-99C4-B404443437F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F83D05C4-EF6D-4BD9-99C4-B404443437F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F83D05C4-EF6D-4BD9-99C4-B404443437F0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {740A716A-38A7-46BC-A21F-18336D0023B7} + EndGlobalSection +EndGlobal diff --git a/VirtualFileSystem/Settings.cs b/VirtualFileSystem/AppSettings.cs similarity index 67% rename from VirtualFileSystem/Settings.cs rename to VirtualFileSystem/AppSettings.cs index 5921ae1..9268baf 100644 --- a/VirtualFileSystem/Settings.cs +++ b/VirtualFileSystem/AppSettings.cs @@ -7,18 +7,15 @@ using System.Text; using System.Threading.Tasks; +using ITHit.FileSystem.Samples.Common; + namespace VirtualFileSystem { /// /// Strongly binded project settings. /// - public class Settings + public class AppSettings : Settings { - /// - /// License string; - /// - public string License { get; set; } - /// /// Folder that contains file structure to simulate data for your virtual file system. /// @@ -26,43 +23,6 @@ public class Settings /// In your real-life application you will read data from your cloud storage, database or any other location, instead of this folder. /// public string RemoteStorageRootPath { get; set; } - - /// - /// Your virtual file system will be mounted under this path. - /// - public string UserFileSystemRootPath { get; set; } - - /// - /// Full synchronization interval in milliseconds. - /// - public double SyncIntervalMs { 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. - /// - public string IconsFolderPath { get; set; } - - /// - /// Product name. Displayed as a mounted folder name under Desktop as - /// well in every location where product name is required. - /// - public string ProductName { get; set; } - - /// - /// Path to the folder that stores ETags, locks and other data associated with files and folders. - /// - public string ServerDataFolderPath{ get; set; } - - /// - /// Automatically lock the file in remote storage when a file handle is being opened for writing, unlock on close. - /// - public bool AutoLock { get; set; } } /// @@ -75,9 +35,9 @@ public static class SettingsConfigValidator /// /// Instance of . /// Virtual File System Settings. - public static Settings ReadSettings(this IConfiguration configuration) + public static AppSettings ReadSettings(this IConfiguration configuration) { - Settings settings = new Settings(); + AppSettings settings = new AppSettings(); if (configuration == null) { diff --git a/VirtualFileSystem/Framework/VfsFileSystemItem.cs b/VirtualFileSystem/Framework/VfsFileSystemItem.cs index ae1f2da..b3be7d0 100644 --- a/VirtualFileSystem/Framework/VfsFileSystemItem.cs +++ b/VirtualFileSystem/Framework/VfsFileSystemItem.cs @@ -119,6 +119,14 @@ public async Task DeleteAsync(IOperationContext operationContext, IConfirmationR } //$> + /// + public Task GetBasicInfoAsync() + { + // Read information about this file or folder from your remote storage + // and return IFileBasicInfo for a file, IFolderBasicInfo for a folder. + throw new NotImplementedException(); + } + /// /// Simulates network delays and reports file transfer progress for demo purposes. /// diff --git a/VirtualFileSystem/Images/Blank.ico b/VirtualFileSystem/Images/Blank.ico new file mode 100644 index 0000000000000000000000000000000000000000..cf158e1c78b3fcbff84f102d29e790128e6ac56c GIT binary patch literal 1150 zcmZQzU<5(|0R|vYV8~!$U=RbcG=LZ+qyWT>V3L8s0Vp>LMniyv5MV@7i_At69%T;Y Q5cvO}fdL=>cOSnz0MINULjV8( literal 0 HcmV?d00001 diff --git a/VirtualFileSystem/Mapping.cs b/VirtualFileSystem/Mapping.cs index 60e3f8e..a536a9d 100644 --- a/VirtualFileSystem/Mapping.cs +++ b/VirtualFileSystem/Mapping.cs @@ -6,6 +6,8 @@ using System.Text; using System.Threading.Tasks; +using ITHit.FileSystem.Samples.Common; + namespace VirtualFileSystem { /// @@ -80,18 +82,28 @@ public static FileSystemItemBasicInfo GetUserFileSysteItemBasicInfo(FileSystemIn // Here we just use the read-only attribute from remote storage item for demo purposes. userFileSystemItem.LockedByAnotherUser = (remoteStorageItem.Attributes & System.IO.FileAttributes.ReadOnly) != 0; - // If the file is moved/renamed and the app is not running this will help us - // to sync the file/folder to remote storage after app starts. - userFileSystemItem.CustomData = new CustomData - { - OriginalPath = Mapping.ReverseMapPath(remoteStorageItem.FullName) - }.Serialize(); - if (remoteStorageItem is FileInfo) { ((FileBasicInfo)userFileSystemItem).Length = ((FileInfo)remoteStorageItem).Length; }; + // Set custom columns to be displayed in file manager. + // We create property definitions when registering the sync root with corresponding IDs. + List customProps = new List(); + if (userFileSystemItem.LockedByAnotherUser) + { + customProps.AddRange( + new ServerLockInfo() + { + LockToken = "token", + Owner = "User Name", + Exclusive = true, + LockExpirationDateUtc = DateTimeOffset.Now.AddMinutes(30) + }.GetLockProperties(Path.Combine(Config.Settings.IconsFolderPath, "LockedByAnotherUser.ico")) + ); + } + userFileSystemItem.CustomProperties = customProps; + return userFileSystemItem; } } diff --git a/VirtualFileSystem/Program.cs b/VirtualFileSystem/Program.cs index f05dbaf..d1a634a 100644 --- a/VirtualFileSystem/Program.cs +++ b/VirtualFileSystem/Program.cs @@ -1,6 +1,4 @@ -using ITHit.FileSystem; -using ITHit.FileSystem.Windows; -using log4net; +using log4net; using log4net.Config; using Microsoft.Extensions.Configuration; using System; @@ -14,6 +12,8 @@ using Windows.Storage; using Windows.Storage.Provider; +using ITHit.FileSystem.Samples.Common; + namespace VirtualFileSystem { class Program @@ -21,7 +21,7 @@ class Program /// /// Application settings. /// - internal static Settings Settings; + internal static AppSettings Settings; /// /// Log4Net logger. @@ -29,38 +29,32 @@ class Program private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); /// - /// Processes file system calls, implements on-demand loading and initial data transfer from remote storage to client. + /// Processes OS file system calls, + /// synchronizes user file system to remote storage and back, + /// monitors files pinning and unpinning. /// - private static VfsEngine engine; + private static VirtualDrive virtualDrive; /// /// Monitores changes in the remote file system. /// internal static RemoteStorageMonitor RemoteStorageMonitorInstance; - /// - /// Monitors pinned and unpinned attributes in user file system. - /// - private static UserFileSystemMonitor userFileSystemMonitor; - - /// - /// Performs complete synchronyzation of the folders and files that are already synched to user file system. - /// - private static FullSyncService syncService; - static async Task Main(string[] args) { // Load Settings. IConfiguration configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json", false, true).Build(); Settings = configuration.ReadSettings(); + Config.Settings = Settings; // Load Log4Net for net configuration. var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config")); + // Enable UTF8 for Console Window Console.OutputEncoding = System.Text.Encoding.UTF8; - log.Info($"\n{System.Diagnostics.Process.GetCurrentProcess().ProcessName}"); + log.Info($"\n{System.Diagnostics.Process.GetCurrentProcess().ProcessName} {Settings.AppID}"); log.Info("\nPress 'Q' to unregister file system, delete all files/folders and exit (simulate uninstall with full cleanup)."); log.Info("\nPress 'q' to unregister file system and exit (simulate uninstall)."); log.Info("\nPress any other key to exit without unregistering (simulate reboot)."); @@ -70,9 +64,12 @@ static async Task Main(string[] args) // Here we register it during first program start for the sake of the development convenience. if (!await Registrar.IsRegisteredAsync(Settings.UserFileSystemRootPath)) { - Directory.CreateDirectory(Settings.UserFileSystemRootPath); log.Info($"\nRegistering {Settings.UserFileSystemRootPath} sync root."); - await Registrar.RegisterAsync(SyncRootId, Settings.UserFileSystemRootPath, Settings.ProductName); + Directory.CreateDirectory(Settings.UserFileSystemRootPath); + Directory.CreateDirectory(Settings.ServerDataFolderPath); + + await Registrar.RegisterAsync(SyncRootId, Settings.UserFileSystemRootPath, Settings.ProductName, + Path.Combine(Config.Settings.IconsFolderPath, "Drive.ico")); } else { @@ -87,24 +84,16 @@ static async Task Main(string[] args) try { - engine = new VfsEngine(Settings.License, Settings.UserFileSystemRootPath, log); + virtualDrive = new VirtualDrive(Settings.UserFileSystemLicense, Settings.UserFileSystemRootPath, log, Settings.SyncIntervalMs); RemoteStorageMonitorInstance = new RemoteStorageMonitor(Settings.RemoteStorageRootPath, log); - syncService = new FullSyncService(Settings.SyncIntervalMs, Settings.UserFileSystemRootPath, log); - userFileSystemMonitor = new UserFileSystemMonitor(Settings.UserFileSystemRootPath, log); // Start processing OS file system calls. //engine.ChangesProcessingEnabled = false; - await engine.StartAsync(); + await virtualDrive.StartAsync(); // Start monitoring changes in remote file system. await RemoteStorageMonitorInstance.StartAsync(); - // Start periodical synchronyzation between client and server, - // in case any changes are lost because the client or the server were unavailable. - await syncService.StartAsync(); - - // Start monitoring pinned/unpinned attributes and files/folders creation in user file system. - await userFileSystemMonitor.StartAsync(); #if DEBUG // Opens Windows File Manager with user file system folder and remote storage folder. ShowTestEnvironment(); @@ -114,10 +103,8 @@ static async Task Main(string[] args) } finally { - engine.Dispose(); + virtualDrive.Dispose(); RemoteStorageMonitorInstance.Dispose(); - syncService.Dispose(); - userFileSystemMonitor.Dispose(); } if (exitKey.KeyChar == 'q') @@ -142,6 +129,15 @@ static async Task Main(string[] args) { log.Error($"\n{ex}"); } + + try + { + Directory.Delete(Config.Settings.ServerDataFolderPath, true); + } + catch (Exception ex) + { + log.Error($"\n{ex}"); + } } else { @@ -193,7 +189,7 @@ private static string SyncRootId { get { - return $"{System.Diagnostics.Process.GetCurrentProcess().ProcessName}!{System.Security.Principal.WindowsIdentity.GetCurrent().User}!User"; + return $"{Settings.AppID}!{System.Security.Principal.WindowsIdentity.GetCurrent().User}!User"; } } } diff --git a/VirtualFileSystem/README.md b/VirtualFileSystem/README.md index 53183fc..42e4038 100644 --- a/VirtualFileSystem/README.md +++ b/VirtualFileSystem/README.md @@ -66,5 +66,5 @@

The server to client synchronization is performed only if the file on the client is marked as In-Sync with the server (it is marked withIn-Sync icon or Pinned file or Cloud fileicon). If the file is modified on the server and is marked as not In-Sync on the client, the files are considered to be in conflict and the conflict icon is displayed.

When any file or folder on the client is updated, it is marked as not In-SyncNot in sync icon, which means the content must be sent to the server. When the updated file is being sent from client to server, the server compares the ETag sent from the client with the ETag stored on the server, to avoid the server changes to be overwritten. The file/folder is updated only if the ETags match. Otherwise, the file/folder is considered to be in conflict and marked with the conflict icon.

Next Article:

-WebDAV Drive Sample in .NET, C# +Virtual File System Sample for Mac in .NET, C# diff --git a/VirtualFileSystem/RemoteStorageMonitor.cs b/VirtualFileSystem/RemoteStorageMonitor.cs index 3fbe609..1891bc1 100644 --- a/VirtualFileSystem/RemoteStorageMonitor.cs +++ b/VirtualFileSystem/RemoteStorageMonitor.cs @@ -12,6 +12,9 @@ using Windows.Storage.Provider; using Windows.System.Update; +using ITHit.FileSystem.Samples.Common; +using ITHit.FileSystem.Samples.Common.Syncronyzation; + namespace VirtualFileSystem.Syncronyzation { /// diff --git a/VirtualFileSystem/UserFile.cs b/VirtualFileSystem/UserFile.cs index d95e55a..06bd2e1 100644 --- a/VirtualFileSystem/UserFile.cs +++ b/VirtualFileSystem/UserFile.cs @@ -1,24 +1,26 @@ -using ITHit.FileSystem; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; +using ITHit.FileSystem; +using ITHit.FileSystem.Samples.Common; + namespace VirtualFileSystem { /// /// Represents a file in the remote storage. /// /// You will change methods of this class to read/write data from/to your remote storage. - internal class UserFile : UserFileSystemItem + internal class UserFile : UserFileSystemItem, IUserFile { /// /// Creates instance of this class. /// /// Path of this file in the user file system. /// Information about file lock. Pass null if the item is not locked. - public UserFile(string userFileSystemFilePath, LockInfo lockInfo = null) : base(userFileSystemFilePath, lockInfo) + public UserFile(string userFileSystemFilePath) : base(userFileSystemFilePath) { } @@ -53,8 +55,9 @@ public async Task ValidateDataAsync(long offset, long length) /// /// New information about the file, such as creation date, modification date, attributes, etc. /// New file content or null if the file content is not modified. + /// Information about the lock. Caller passes null if the item is not locked. /// New ETag returned from the remote storage. - public async Task UpdateAsync(IFileBasicInfo fileInfo, Stream content = null) + public async Task UpdateAsync(IFileBasicInfo fileInfo, Stream content = null, ServerLockInfo lockInfo = null) { return await CreateOrUpdateFileAsync(RemoteStoragePath, fileInfo, FileMode.Open, content); } diff --git a/VirtualFileSystem/UserFileSystemItem.cs b/VirtualFileSystem/UserFileSystemItem.cs index 7b9e233..c33783c 100644 --- a/VirtualFileSystem/UserFileSystemItem.cs +++ b/VirtualFileSystem/UserFileSystemItem.cs @@ -1,19 +1,22 @@ -using ITHit.FileSystem; -using ITHit.FileSystem.Windows; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; +using ITHit.FileSystem; +using ITHit.FileSystem.Windows; +using ITHit.FileSystem.Samples.Common; + + namespace VirtualFileSystem { /// /// Represents a file or a folder in the remote storage. Contains methods common for both files and folders. /// /// You will change methods of this class to read/write data from/to your remote storage. - internal class UserFileSystemItem + internal class UserFileSystemItem : IUserFileSystemItem { /// /// Path of this file of folder in the user file system. @@ -25,21 +28,15 @@ internal class UserFileSystemItem /// protected string RemoteStoragePath; - /// - /// Information about the lock. Null if the item is not locked. - /// - protected LockInfo Lock; - /// /// Creates instance of this class. /// /// Path of this file of folder in the user file system. /// Information about the lock. Pass null if the item is not locked. - public UserFileSystemItem(string userFileSystemPath, LockInfo lockInfo = null) + public UserFileSystemItem(string userFileSystemPath) { this.UserFileSystemPath = userFileSystemPath; this.RemoteStoragePath = Mapping.MapPath(userFileSystemPath); - this.Lock = lockInfo; } /// @@ -113,8 +110,9 @@ public async Task DeleteAsync() /// New information about the file, such as modification date, attributes, custom data, etc. /// Specifies if a new file should be created or existing file should be updated. /// New file content or null if the file content is not modified. + /// Information about the lock. Caller passes null if the item is not locked. /// New ETag returned from the remote storage. - protected async Task CreateOrUpdateFileAsync(string remoteStoragePath, IFileBasicInfo newInfo, FileMode mode, Stream newContentStream = null) + protected async Task CreateOrUpdateFileAsync(string remoteStoragePath, IFileBasicInfo newInfo, FileMode mode, Stream newContentStream = null, ServerLockInfo lockInfo = null) { // Get ETag and lock-token here and send it to the remote storage with the new item content/info if needed. if (mode == FileMode.Open) @@ -123,7 +121,7 @@ protected async Task CreateOrUpdateFileAsync(string remoteStoragePath, I string eTag = await ETag.GetETagAsync(UserFileSystemPath); // Get lock-token. - string lockToken = Lock?.LockToken; + string lockToken = lockInfo?.LockToken; } FileInfo remoteStorageItem = new FileInfo(remoteStoragePath); @@ -188,8 +186,9 @@ protected async Task CreateOrUpdateFileAsync(string remoteStoragePath, I /// Path of the folder to be created or updated in the remote storage. /// New information about the folder, such as modification date, attributes, custom data, etc. /// Specifies if a new folder should be created or existing folder should be updated. + /// Information about the lock. Caller passes null if the item is not locked. /// New ETag returned from the remote storage. - protected async Task CreateOrUpdateFolderAsync(string remoteStoragePath, IFolderBasicInfo newInfo, FileMode mode) + protected async Task CreateOrUpdateFolderAsync(string remoteStoragePath, IFolderBasicInfo newInfo, FileMode mode, ServerLockInfo lockInfo = null) { // Get ETag and lock-token here and send it to the remote storage with the new item content/info if needed. if (mode == FileMode.Open) @@ -198,7 +197,7 @@ protected async Task CreateOrUpdateFolderAsync(string remoteStoragePath, string eTag = await ETag.GetETagAsync(UserFileSystemPath); // Get lock-token. - string lockToken = Lock?.LockToken; + string lockToken = lockInfo?.LockToken; } DirectoryInfo remoteStorageItem = new DirectoryInfo(remoteStoragePath); @@ -252,9 +251,9 @@ protected async Task CreateOrUpdateFolderAsync(string remoteStoragePath, /// item in the remote storage should be updated. Supply the lock-token during the update request in /// and method calls. /// - public async Task LockAsync() + public async Task LockAsync() { - return new LockInfo { LockToken = "token" }; + return new ServerLockInfo { LockToken = "token", Exclusive = true, LockExpirationDateUtc = DateTimeOffset.Now.AddMinutes(30), Owner = "You" }; } /// diff --git a/VirtualFileSystem/UserFolder.cs b/VirtualFileSystem/UserFolder.cs index 2902d3a..ba84600 100644 --- a/VirtualFileSystem/UserFolder.cs +++ b/VirtualFileSystem/UserFolder.cs @@ -1,11 +1,13 @@ -using ITHit.FileSystem; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using ITHit.FileSystem; +using ITHit.FileSystem.Samples.Common; + namespace VirtualFileSystem { /// @@ -13,14 +15,14 @@ namespace VirtualFileSystem /// creating files and folders and updating this folder information (creatin date, modification date, attributes, etc.). /// /// You will change methods of this class to read/write data from/to your remote storage. - internal class UserFolder : UserFileSystemItem + internal class UserFolder : UserFileSystemItem, IUserFolder { /// /// Creates instance of this class. /// /// Path of this folder in the user file system. /// Information about file lock. Pass null if the item is not locked. - public UserFolder(string userfileSystemFolderPath, LockInfo lockInfo = null) : base(userfileSystemFolderPath, lockInfo) + public UserFolder(string userfileSystemFolderPath) : base(userfileSystemFolderPath) { } @@ -77,8 +79,9 @@ public async Task CreateFolderAsync(IFolderBasicInfo folderInfo) /// Updates folder in the remote storage. /// /// New folder information. + /// Information about the lock. Caller passes null if the item is not locked. /// New ETag returned from the remote storage. - public async Task UpdateAsync(IFolderBasicInfo folderInfo) + public async Task UpdateAsync(IFolderBasicInfo folderInfo, ServerLockInfo lockInfo = null) { return await CreateOrUpdateFolderAsync(RemoteStoragePath, folderInfo, FileMode.Open); } diff --git a/VirtualFileSystem/VirtualDrive.cs b/VirtualFileSystem/VirtualDrive.cs new file mode 100644 index 0000000..95575ec --- /dev/null +++ b/VirtualFileSystem/VirtualDrive.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using log4net; + +using ITHit.FileSystem.Samples.Common; +using System.IO; + +namespace VirtualFileSystem +{ + /// + internal class VirtualDrive : VirtualDriveBase + { + /// + public VirtualDrive(string license, string userFileSystemRootPath, ILog log, double syncIntervalMs) + : base(license, userFileSystemRootPath, log, syncIntervalMs) + { + + } + + /// + public override async Task GetUserFileSystemItemAsync(string userFileSystemPath) + { + if (File.Exists(userFileSystemPath)) + { + return new UserFile(userFileSystemPath); + } + if (Directory.Exists(userFileSystemPath)) + { + return new UserFolder(userFileSystemPath); + } + + // When a file handle is being closed during delete, the file does not exist, return null. + return null; + } + } +} diff --git a/VirtualFileSystem/VirtualFileSystem.csproj b/VirtualFileSystem/VirtualFileSystem.csproj index 3a794d8..8faff0e 100644 --- a/VirtualFileSystem/VirtualFileSystem.csproj +++ b/VirtualFileSystem/VirtualFileSystem.csproj @@ -1,7 +1,7 @@ Exe - net5.0-windows10.0.19041.0 + netcoreapp3.1 10.0.19041.0 IT Hit LTD. IT Hit LTD. @@ -16,29 +16,30 @@ To simulate the remote storage, this sample is using a folder in the local file false + + + - - - - - + - - + Always + + PreserveNewest + PreserveNewest diff --git a/VirtualFileSystem/appsettings.json b/VirtualFileSystem/appsettings.json index 5489255..3e8e42d 100644 --- a/VirtualFileSystem/appsettings.json +++ b/VirtualFileSystem/appsettings.json @@ -1,4 +1,9 @@ { + // Unique ID of this application. + // If you must to run more than one instance of this application side-by-side on same client machine + // (aka Corporate Drive and Personal Drive) set unique ID for each instance. + "AppID": "VFSDrive", + // License to activate the User File System Engine. If no license is specified the Engine will be activated // automatically via internet and will function for 5 days. The Engine will stop working after that. // To enable a 1-month trial period, download a trial license here: https://userfilesystem.com/download/ diff --git a/VirtualFileSystemMac/FileProviderExtension/Entitlements.plist b/VirtualFileSystemMac/FileProviderExtension/Entitlements.plist index 70a2df1..7c29b9c 100644 --- a/VirtualFileSystemMac/FileProviderExtension/Entitlements.plist +++ b/VirtualFileSystemMac/FileProviderExtension/Entitlements.plist @@ -6,7 +6,7 @@ com.apple.security.application-groups - $(TeamIdentifierPrefix)group.com.ithit.virtualfilesystem + $(TeamIdentifierPrefix)group.com.userfilesystem.vfs com.apple.security.files.downloads.read-write @@ -18,5 +18,7 @@ com.apple.security.files.user-selected.read-write + com.apple.security.cs.allow-jit + diff --git a/VirtualFileSystemMac/FileProviderExtension/FileProviderExtension.csproj b/VirtualFileSystemMac/FileProviderExtension/FileProviderExtension.csproj index af9c511..f196f63 100644 --- a/VirtualFileSystemMac/FileProviderExtension/FileProviderExtension.csproj +++ b/VirtualFileSystemMac/FileProviderExtension/FileProviderExtension.csproj @@ -11,7 +11,7 @@ v2.0 Xamarin.Mac Resources - 0.5 + 2.0.4465.0 true @@ -32,10 +32,11 @@ None None 3rd Party Mac Developer Installer - ITHit Profile + Automatic 9.0 true x86_64 + Entitlements.plist pdbonly @@ -62,10 +63,10 @@ - ..\packages\ITHit.FileSystem.1.4.4298\lib\netstandard2.1\ITHit.FileSystem.dll + ..\packages\ITHit.FileSystem.2.0.4465\lib\netstandard2.1\ITHit.FileSystem.dll - - ..\packages\ITHit.FileSystem.Mac.1.4.4298-Alpha\lib\xamarinmac74\ITHit.FileSystem.Mac.dll + + ..\packages\ITHit.FileSystem.Mac.2.0.4465-Alpha\lib\xamarinmac20\ITHit.FileSystem.Mac.dll diff --git a/VirtualFileSystemMac/FileProviderExtension/Info.plist b/VirtualFileSystemMac/FileProviderExtension/Info.plist index 62b62de..aa85175 100644 --- a/VirtualFileSystemMac/FileProviderExtension/Info.plist +++ b/VirtualFileSystemMac/FileProviderExtension/Info.plist @@ -7,7 +7,7 @@ CFBundleDisplayName FileProviderExtension CFBundleIdentifier - com.ithit.virtualfilesystem.app.extension + com.userfilesystem.vfs.app.extension CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/VirtualFileSystemMac/FileProviderExtension/VfsFileSystemItem.cs b/VirtualFileSystemMac/FileProviderExtension/VfsFileSystemItem.cs index 475bd47..e0c89d5 100644 --- a/VirtualFileSystemMac/FileProviderExtension/VfsFileSystemItem.cs +++ b/VirtualFileSystemMac/FileProviderExtension/VfsFileSystemItem.cs @@ -88,5 +88,10 @@ protected void SimulateNetworkDelay(long fileLength, IResultContext resultContex { throw new NotImplementedException(); } + + public Task GetBasicInfoAsync() + { + throw new NotImplementedException(); + } } } diff --git a/VirtualFileSystemMac/FileProviderExtension/packages.config b/VirtualFileSystemMac/FileProviderExtension/packages.config index 221b4bd..aa3751a 100644 --- a/VirtualFileSystemMac/FileProviderExtension/packages.config +++ b/VirtualFileSystemMac/FileProviderExtension/packages.config @@ -1,4 +1,5 @@  - + + \ No newline at end of file diff --git a/VirtualFileSystemMac/README.md b/VirtualFileSystemMac/README.md index 073ee1a..9b68f50 100644 --- a/VirtualFileSystemMac/README.md +++ b/VirtualFileSystemMac/README.md @@ -1,73 +1,69 @@ -## Requirements -macOS 11.0+ -Mono 6.12.0.122+ -Xamarin.Mac: 7.4.0.10+ -Xcode 12.4+ -Visual Studio Community 2019 for Mac v8.8.10+, Stable Channel. +

Virtual File System Sample for Mac in .NET, C#

+

This sample implements a virtual file system for Mac with synchronization support and folders on-demand listing. It synchronizes files and folders from remote storage to the user file system. This sample is written in Xamarin, .NET/C#. To simulate the remote storage, this sample is using a folder in the local file system on the same machine. 

+

The purpose of this sample is to demonstrate the major features of the IT Hit User File System for .NET and provide patterns for its programming. You will use this sample as a starting point for creating an OneDrive-like file system for your DMS/CRM/ERP and will reprogram it to publish data from your real storage instead of the local file system.

+

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

+

Currently, this sample is in the Alpha stage of development. The current version of the extension can only list folders and files without the ability to access any file's contents.

+

Requirements

+
    +
  • macOS 11.0+
  • +
  • Mono 6.12.0.122+
  • +
  • Xamarin.Mac: 7.4.0.10+
  • +
  • Xcode 12.4+
  • +
  • Visual Studio Community 2019 for Mac v8.8.10+, Stable Channel.
  • +
+

Solution Structure

+

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

+

The container application provides a Menu Bar icon to install/uninstall the system extension. Inside the container application, you should change the hardcoded directory to replicate in your Virtual Disk manually. Consider that the system extension can only access sandbox folders (including Downloads, Pictures, Music, Movies). It means that it won't be able to show the contents of the folder outside of the sandbox.

+

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

+

Configuring the Sample

+

In the following steps, we will describe how to configure and run this sample in the development environment. You will create an Apple group ID, Apple app identifies, and Apple provisioning profiles. Then you will update the sample container application project and extension project to use the created IDs and profiles.

+

Log-in to the Apple developer account here: https://developer.apple.com/. To complete the steps below you must have an App Manager role.

+
    +
  1. +

    Create App Group. Navigate to Certificates, IDs, Profiles -> Identifiers -> App Groups and create a new group.

    +
  2. +
  3. +

    Create Apple macOS App IDs. Navigate to Certificates, IDs, Profile -> Identifiers -> App IDs. Create 2 identifiers that will be unique for your project. One will be used for container application another for the system extension.

    +
  4. +
  5. +

    Add app identifiers to the group. Add both identifiers created in Step 2 to the group created in Step 1. Select identifier and click on Edit. Then check the App Groups checkbox, select the Edit button and select the group created in Step 1.

    +
  6. +
  7. +

    Create Provisioning Profiles. Navigate to Certificates, Identifiers & Profiles -> Profiles -> Development and create profile. Associate profile with extension ID and container ID respectively.

    +
  8. +
  9. +

    Download profiles and certificates in XCode. Run XCode and go to Xcode Menu > Preferences -> Accounts tab. Select team and click on Download Manual Profiles. You can find more detailed instructions: here

    +
  10. +
  11. +

    Set bundle identifier name in Container project. The bundle identifier is located in VirtualFilesystemMacApp/Info.plist file. You can edit it either in Visual Studio or directly in Info.plist file in the CFBundleIdentifier field (by default it is set to com.ithit.virtualfilesystem.app). You must set this identifier to the value specified in Step 1.

    +
  12. +
  13. +

    Set bundle identifier name in the Extension project. The bundle identifier is located in FileProviderExtension/Info.plist file. You can edit it either in Visual Studio or directly in Info.plist file in the CFBundleIdentifier field (by default it is set to com.ithit.virtualfilesystem.app.extension). You must set this identifier to the value specified in Step 1.

    +
  14. +
  15. +

    Configure macOS bundle signing in Container and Extension projects. For each project in Visual Studio go to the project Options. Select Mac Signing and check 'Sign the application bundle'. Select Identity and Provisioning profile.

    +
  16. +
  17. +

    Configure application permissions in Container and Extension projects. Select Entitlements.plist and select group created in Step 1 in App Groups field for each project.

    +
  18. +
  19. +

    Set App Group ID in code. Edit AppGroupsSettings.cs file located in /VirtualFilesystemCommon/ folder. Specify AppGroupId.

    +
  20. +
  21. +

    Set the license. Download the license file here. With the trial license, the product is fully functional and does not have any limitations. As soon as the trial license expires the product will stop working. Open the VirtualFilesystemMacApp/Resources/appsettings.json file and set the License string: 

    +
    "License": "<?xml version='1.0'...",
    +
  22. +
  23. +

    Set the remote storage directory. Here you must set the path to the folder that simulates your remote storage. Your virtual drive will mirror files and folders from this folder. Note that the extension runs as a sandboxed application and has access to a limited number of locations in the local file system. To simulate the remote storage structure you can copy the \RemoteStorage\ folder provided with the project under the ~/Pictures/ folder. To set the remote storage directory open the VirtualFilesystemMacApp/Resources/appsettings.json file and set RemoteStorageRootPath string. Make sure to replace the ${USER} with a real user name:

    +
    "RemoteStorageRootPath": "/Users/User1/Pictures/RemoteStorage"
    +
  24. +
+

Now you are ready to compile and run the project.

+

Running the Sample

+

To run the sample open the project in Visual Studio and run the project. The application adds an application to the macOS Status Bar. To mount the file system select the 'Install Extension' command in the Status Bar.

+

Virtual File System Mac in .NET/C#

+

Note, that every File Provider Extension runs in a sandbox, so access to the local filesystem restricted by OS except Downloads, Pictures, Music, Movies public directories.

+

Next Article:

+WebDAV Drive Sample in .NET, C# -## Solution Structure - -The macOS sample solution consists of 3 projects: container application, an extension project and a common code. - -The container application provides Menu Bar icon to install/uninstall extension. Inside container application you should change hardcoded directory to replicate in your Virtual Disk manually. Consider that extension can only access sandbox folders (including Downloads, Pictures, Music, Movies). It means that it won't be able to show contents of folder outside of sandbox. - -The extension project runs in the background and implements a virtual file system on macOS (File Provider). It processes requests from macOS applications sent via macOS file system API and lists folders content. The macOS extension can be installed only as part of a container application, you can not install the extension application by itself. - -## Running the Sample - -In the following steps we will describe how to configure and run macOS sample in the development environment. You will create an Apple group ID, Apple app identifies and Apple provisioning profiles. Than you will update the sample container application project and extension project to use the created IDs and profiles. - -Login to Apple developer account here: https://developer.apple.com/. To complete steps below you must have an App Manager role. - -1. **Create App Group.** - Navigate to Certificates, IDs, Profile -> Identifiers -> App Groups and create a new group. - -2. **Create Apple macOS App IDs.** - Navigate to Certificates, IDs, Profile -> Identifiers -> App IDs. Create 2 identifiers that will be unique for your project. One will be used for container application another – for extension. - -3. **Add app identifiers to group.** - Add both identifiers created in Step 2 to group created in Step 1. Select identifier and click on Edit. Than check App Groups checkbox, select Edit button and select the group created in Step 1. - -4. **Create Provisioning Profiles.** - Navigate to Certificates, Identifiers & Profiles -> Profiles -> Development and create profile. Associate profile with extension ID and container ID respectively. - -5. **Download profiles and certificates in XCode.** - Run XCode and go to Xcode Menu > Preferences -> Accounts tab. Select team and click on “Download Manual Profiles”. You can find more detailed instructions: [here](https://docs.microsoft.com/en-us/xamarin/mac/deploy-test/publishing-to-the-app-store/profiles) - -6. **Set bundle identifier name in Container project.** - The bundle identifier is located in VirtualFilesystemMacApp/Info.plist file. You can edit it either in Visual Studio or directly in Info.plist file in CFBundleIdentifier field (by default it is set to com.ithit.virtualfilesystem.app). You must set this identifier to the value specified in Step 1. - -7. **Set bundle identifier name in Extension project.** - The bundle identifier is located in FileProviderExtension/Info.plist file. You can edit it either in Visual Studio or directly in Info.plist file in in CFBundleIdentifier field (by default it is set to com.ithit.virtualfilesystem.app.extension). You must set this identifier to the value specified in Step 1. - -8. **Configure macOS bundle signing in Container and Extension projects.** - For each project in Visual Studio go to project Options. Select Mac Signing and check 'Sign the application bundle'. Select Identity and Providioning profile. - -9. **Configure application permissions in Container and Extension projects.** - Select Entitlements.plist and select group created in Step 1 in App Groups field for each project. - -10. **Set App Group ID in code.** - Edit AppGroupsSettings.cs in /VirtualFilesystemCommon/ specify AppGroupId. - -11. **Set license** - Download the license file [here](https://www.userfilesystem.com/download/downloads/). With the trial license the product is fully functional and does not have any limitations. As soon as the trial license expires the product will stop working. - Open 'VirtualFilesystemMacApp/Resources/appsettings.json' file and set License string: - `"License": "v2.0 Xamarin.Mac Resources - 0.5 + 2.0.4465.0 true @@ -52,10 +52,7 @@ - ..\packages\ITHit.FileSystem.1.4.4298\lib\netstandard2.1\ITHit.FileSystem.dll - - - ..\packages\ITHit.FileSystem.Mac.1.4.4298-Alpha\lib\xamarinmac74\ITHit.FileSystem.Mac.dll + ..\packages\ITHit.FileSystem.2.0.4465\lib\netstandard2.1\ITHit.FileSystem.dll
diff --git a/VirtualFileSystemMac/VirtualFilesystemCommon/packages.config b/VirtualFileSystemMac/VirtualFilesystemCommon/packages.config index 410616a..df98d5a 100644 --- a/VirtualFileSystemMac/VirtualFilesystemCommon/packages.config +++ b/VirtualFileSystemMac/VirtualFilesystemCommon/packages.config @@ -1,5 +1,4 @@  - - + \ No newline at end of file diff --git a/VirtualFileSystemMac/VirtualFilesystemMac.sln b/VirtualFileSystemMac/VirtualFilesystemMac.sln index e1476a3..11b56a3 100644 --- a/VirtualFileSystemMac/VirtualFilesystemMac.sln +++ b/VirtualFileSystemMac/VirtualFilesystemMac.sln @@ -3,36 +3,30 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.808.8 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualFilesystemMacApp", "VirtualFilesystemMacApp\VirtualFilesystemMacApp.csproj", "{9D231FE1-44AD-42B5-BA37-A22A1104A79E}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileProviderExtension", "FileProviderExtension\FileProviderExtension.csproj", "{30EDD98F-A449-4A9F-A718-EE480181C619}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ITHit.FileSystem.Mac", "ITHit.FileSystem.Mac\ITHit.FileSystem.Mac.csproj", "{32B330A5-C736-4962-93B7-E2261770497C}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualFilesystemCommon", "VirtualFilesystemCommon\VirtualFilesystemCommon.csproj", "{8A146BCC-DE6F-436F-9C25-67AD964E473F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualFilesystemMacApp", "VirtualFilesystemMacApp\VirtualFilesystemMacApp.csproj", "{9D231FE1-44AD-42B5-BA37-A22A1104A79E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9D231FE1-44AD-42B5-BA37-A22A1104A79E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9D231FE1-44AD-42B5-BA37-A22A1104A79E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9D231FE1-44AD-42B5-BA37-A22A1104A79E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9D231FE1-44AD-42B5-BA37-A22A1104A79E}.Release|Any CPU.Build.0 = Release|Any CPU {30EDD98F-A449-4A9F-A718-EE480181C619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {30EDD98F-A449-4A9F-A718-EE480181C619}.Debug|Any CPU.Build.0 = Debug|Any CPU {30EDD98F-A449-4A9F-A718-EE480181C619}.Release|Any CPU.ActiveCfg = Release|Any CPU {30EDD98F-A449-4A9F-A718-EE480181C619}.Release|Any CPU.Build.0 = Release|Any CPU - {32B330A5-C736-4962-93B7-E2261770497C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {32B330A5-C736-4962-93B7-E2261770497C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {32B330A5-C736-4962-93B7-E2261770497C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {32B330A5-C736-4962-93B7-E2261770497C}.Release|Any CPU.Build.0 = Release|Any CPU {8A146BCC-DE6F-436F-9C25-67AD964E473F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8A146BCC-DE6F-436F-9C25-67AD964E473F}.Debug|Any CPU.Build.0 = Debug|Any CPU {8A146BCC-DE6F-436F-9C25-67AD964E473F}.Release|Any CPU.ActiveCfg = Release|Any CPU {8A146BCC-DE6F-436F-9C25-67AD964E473F}.Release|Any CPU.Build.0 = Release|Any CPU + {9D231FE1-44AD-42B5-BA37-A22A1104A79E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D231FE1-44AD-42B5-BA37-A22A1104A79E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D231FE1-44AD-42B5-BA37-A22A1104A79E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D231FE1-44AD-42B5-BA37-A22A1104A79E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -41,6 +35,6 @@ Global SolutionGuid = {EC725A74-F3CF-4F79-BEC5-4F6234C3687D} EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution - version = 0.5 + version = 2.0.4465.0 EndGlobalSection EndGlobal diff --git a/VirtualFileSystemMac/VirtualFilesystemMacApp/AppDelegate.cs b/VirtualFileSystemMac/VirtualFilesystemMacApp/AppDelegate.cs index 6f6e95f..14a00b1 100644 --- a/VirtualFileSystemMac/VirtualFilesystemMacApp/AppDelegate.cs +++ b/VirtualFileSystemMac/VirtualFilesystemMacApp/AppDelegate.cs @@ -17,7 +17,7 @@ public AppDelegate() public override void DidFinishLaunching(NSNotification notification) { - LocalExtensionManager = new ExtensionManager("com.ithit.virtualfilesystem.app", "ITHitFS"); + LocalExtensionManager = new ExtensionManager("com.userfilesystem.vfs.app", "ITHitFS"); NSMenu menu = new NSMenu(); diff --git a/VirtualFileSystemMac/VirtualFilesystemMacApp/Entitlements.plist b/VirtualFileSystemMac/VirtualFilesystemMacApp/Entitlements.plist index 09226d8..7338100 100644 --- a/VirtualFileSystemMac/VirtualFilesystemMacApp/Entitlements.plist +++ b/VirtualFileSystemMac/VirtualFilesystemMacApp/Entitlements.plist @@ -4,7 +4,7 @@ com.apple.security.application-groups - $(TeamIdentifierPrefix)group.com.ithit.virtualfilesystem + $(TeamIdentifierPrefix)group.com.userfilesystem.vfs com.apple.security.app-sandbox @@ -18,5 +18,7 @@ com.apple.security.assets.movies.read-write + com.apple.security.cs.allow-jit + diff --git a/VirtualFileSystemMac/VirtualFilesystemMacApp/Info.plist b/VirtualFileSystemMac/VirtualFilesystemMacApp/Info.plist index 21943dc..929a901 100644 --- a/VirtualFileSystemMac/VirtualFilesystemMacApp/Info.plist +++ b/VirtualFileSystemMac/VirtualFilesystemMacApp/Info.plist @@ -5,7 +5,7 @@ CFBundleName VirtualFilesystemMacApp CFBundleIdentifier - com.ithit.virtualfilesystem.app + com.userfilesystem.vfs.app CFBundleShortVersionString 1.0 CFBundleVersion diff --git a/VirtualFileSystemMac/VirtualFilesystemMacApp/VirtualFilesystemMacApp.csproj b/VirtualFileSystemMac/VirtualFilesystemMacApp/VirtualFilesystemMacApp.csproj index 4a04b34..ea770ac 100644 --- a/VirtualFileSystemMac/VirtualFilesystemMacApp/VirtualFilesystemMacApp.csproj +++ b/VirtualFileSystemMac/VirtualFilesystemMacApp/VirtualFilesystemMacApp.csproj @@ -10,7 +10,7 @@ Virtual Filesystem v2.0 Resources - 0.5 + 2.0.4465.0 Xamarin.Mac @@ -32,7 +32,7 @@ 3rd Party Mac Developer Installer None None - ITHit Profile + Automatic true @@ -48,6 +48,7 @@ 9.0 x86_64 + Entitlements.plist pdbonly diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/AppGroupSettings.cs b/VirtualFileSystemMac/VirtualFilesystemMacCommon/AppGroupSettings.cs deleted file mode 100644 index 01f0e4f..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/AppGroupSettings.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.IO; -using Foundation; - -namespace VirtualFilesystemMacCommon -{ - public static class AppGroupSettings - { - private const string AppGroupId = "DHSP75SFA6.group.com.ithit.virtualfilesystem"; - private const string SettingFile = "data.out"; - private const string LocalPathId = "LocalPath"; - - public static string GetRootPath() - { - NSUrl userDataPath = GetSharedContainerUrl(); - NSDictionary userData = NSDictionary.FromFile(Path.Combine(userDataPath.Path, SettingFile)); - if (userData is null) - { - return null; - } - - return userData.ValueForKey((NSString)LocalPathId).ToString(); - } - - public static void SaveRootPath(string rootPath) - { - NSUrl userDataPath = GetSharedContainerUrl(); - NSDictionary userData = new NSDictionary(new NSString(LocalPathId), new NSString(rootPath)); - - if (!userData.WriteToFile(Path.Combine(userDataPath.Path, SettingFile), true)) - { - throw new Exception("Failed to save server setting"); - } - } - - private static NSUrl GetSharedContainerUrl() - { - return NSFileManager.DefaultManager.GetContainerUrl(AppGroupId); - } - } -} \ No newline at end of file diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/ConsoleLogger.cs b/VirtualFileSystemMac/VirtualFilesystemMacCommon/ConsoleLogger.cs deleted file mode 100644 index 7fd7dd7..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/ConsoleLogger.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Foundation; - -namespace VirtualFilesystemMacCommon -{ - public static class NSLogHelper - { - [DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")] - extern static void NSLog(IntPtr format, [MarshalAs(UnmanagedType.LPStr)] string s); - - public static void NSLog(string format, params object[] args) - { - var fmt = NSString.CreateNative("%s"); - var val = (args is null || args.Length == 0) ? format : string.Format(format, args); - - NSLog(fmt, val); - NSString.ReleaseNative(fmt); - } - } - - public class ConsoleLogger - { - private const string AppName = "[ITHit]"; - private readonly string ModuleName; - - public ConsoleLogger(string moduleName) - { - ModuleName = moduleName; - } - - public void LogError(Exception exception) - { - LogError(exception.ToString()); - } - - public void LogError(string str) - { - LogWithLevel("[ERROR]", str); - } - - public void LogDebug(Exception exception) - { -#if DEBUG - LogDebug(exception.ToString()); -#endif - } - - public void LogDebug(string str) - { -#if DEBUG - LogWithLevel("[DEBUG]", str); -#endif - } - - private void LogWithLevel(string logLevelStr, string str) - { - NSLogHelper.NSLog(logLevelStr + " " + AppName + " " + ModuleName + ": " + str); - } - } -} diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/FileSystemItemBasicInfo.cs b/VirtualFileSystemMac/VirtualFilesystemMacCommon/FileSystemItemBasicInfo.cs deleted file mode 100644 index 6371406..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/FileSystemItemBasicInfo.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.IO; -using ITHit.FileSystem; - -namespace VirtualFilesystemMacCommon -{ - public class FileSystemItemBasicInfo : IFileSystemItemBasicInfo - { - public FileSystemItemBasicInfo(string name, FileAttributes attributes, DateTime creationTime, DateTime changeTime, ulong size = 0) - { - Name = name; - Attributes = attributes; - CreationTime = creationTime; - ChangeTime = changeTime; - Size = size; - } - - public string Name { get; } - - public FileAttributes Attributes { get; } - - public byte[] CustomData { get; } - - public DateTime CreationTime { get; } - - public DateTime LastWriteTime { get; } - - public DateTime LastAccessTime { get; } - - public DateTime ChangeTime { get; } - - public ulong Size { get; } - } -} diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/FsEngine.cs b/VirtualFileSystemMac/VirtualFilesystemMacCommon/FsEngine.cs deleted file mode 100644 index 15e98b0..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/FsEngine.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Threading.Tasks; -using ITHit.FileSystem; -using System.IO; - -namespace VirtualFilesystemMacCommon -{ - public class FsEngine : Engine - { - private const string TestLicense = "IT Hit User File System1Evaluation22c3c7be-a10f-4eec-bb52-5a2b92b0f24b"; - private ConsoleLogger Logger; - - public FsEngine(string localRootPath) - : base (TestLicense, localRootPath) - { - Logger = new ConsoleLogger(GetType().Name); - } - - public override Task GetFileSystemItemAsync(string path) - { - return null; - } - - public IFileSystemItem GetFileSystemItem(string path) - { - try - { - if (File.GetAttributes(path).HasFlag(FileAttributes.Directory)) - { - return new UserFolder(path); - } - } - catch (FileNotFoundException exception) - { - Logger.LogError("Specified file not found: " + path + ". With error: " + exception.ToString()); - return null; - } - - return new UserFile(path); - } - - public override Task StartAsync() - { - Task task = new Task(() => - { - return null; - }); - - return task; - } - - protected override void Stop() - { - } - } -} diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/ItemMetadata.cs b/VirtualFileSystemMac/VirtualFilesystemMacCommon/ItemMetadata.cs deleted file mode 100644 index 7632922..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/ItemMetadata.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using Foundation; - -namespace VirtualFilesystemMacCommon -{ - public class ItemMetadata - { - public enum ItemMetadataType - { - Unknown, - Dir, - File - } - - public ItemMetadata(string identifier) - { - Identifier = identifier; - ItemType = ItemMetadataType.Unknown; - } - - public ItemMetadata(string identifier, string parentIdentifier, NSDate creationDate, NSDate modificationDate, ItemMetadataType itemType = ItemMetadataType.Unknown, ulong fileSize = 0) - { - Identifier = identifier; - ParentIdentifier = parentIdentifier; - CreationDate = creationDate; - ModificationDate = modificationDate; - ItemType = itemType; - FileSize = new NSNumber(fileSize); - } - - public string Identifier { get; set; } - public string ParentIdentifier { get; set; } - public NSDate CreationDate { get; set; } - public NSDate ModificationDate { get; set; } - public NSNumber FileSize { get; set; } - public ItemMetadataType ItemType { get; set; } - } -} diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/LocationMapper.cs b/VirtualFileSystemMac/VirtualFilesystemMacCommon/LocationMapper.cs deleted file mode 100644 index e4080ef..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/LocationMapper.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.IO; -using FileProvider; -using UniformTypeIdentifiers; - -namespace VirtualFilesystemMacCommon -{ - public class LocationMapper - { - public string RemoteRootPath { get; } - - public LocationMapper(string remoteRootPath) - { - RemoteRootPath = remoteRootPath; - } - - public string GetIdentifierFromRemotePath(string path) - { - if (path == RemoteRootPath) - { - return NSFileProviderItemIdentifier.RootContainer; - } - - return path; - } - - public string GetPathFromRemotePath(string path) - { - if (path == RemoteRootPath) - { - return "/"; - } - - return path; - } - - public string GetRemotePathFromIdentifier(string identifier) - { - if (identifier == NSFileProviderItemIdentifier.RootContainer) - { - return RemoteRootPath; - } - - if (identifier == NSFileProviderItemIdentifier.WorkingSetContainer) - { - return NSFileProviderItemIdentifier.WorkingSetContainer; - } - - if (identifier == NSFileProviderItemIdentifier.TrashContainer) - { - return NSFileProviderItemIdentifier.TrashContainer; - } - - return identifier; - } - - public string GetParentForIdentifier(string identifier) - { - DirectoryInfo parentInfo = Directory.GetParent(identifier); - if (parentInfo == null || parentInfo.FullName == RemoteRootPath || identifier == RemoteRootPath) - { - return NSFileProviderItemIdentifier.RootContainer; - } - - return parentInfo.FullName; - } - - public static string GetFileNameFromIdentifier(string identifier) - { - if (identifier == NSFileProviderItemIdentifier.RootContainer || identifier == NSFileProviderItemIdentifier.WorkingSetContainer || identifier == NSFileProviderItemIdentifier.TrashContainer) - { - return "/"; - } - - return Path.GetFileName(identifier); - } - - public static bool IsRootItem(string identifier) - { - return (identifier == NSFileProviderItemIdentifier.RootContainer); - } - - public static bool IsWorkingSetOrTrashItem(string identifier) - { - return identifier == NSFileProviderItemIdentifier.WorkingSetContainer || identifier == NSFileProviderItemIdentifier.TrashContainer; - } - } -} diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/Properties/AssemblyInfo.cs b/VirtualFileSystemMac/VirtualFilesystemMacCommon/Properties/AssemblyInfo.cs deleted file mode 100644 index 9b677e6..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("FileCloudSyncCommon")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("${AuthorCopyright}")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - -[assembly: AssemblyVersion("1.0.*")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/RemoteStorageManager.cs b/VirtualFileSystemMac/VirtualFilesystemMacCommon/RemoteStorageManager.cs deleted file mode 100644 index 8414cc0..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/RemoteStorageManager.cs +++ /dev/null @@ -1,81 +0,0 @@ -using ITHit.FileSystem; -using Foundation; -using System.Collections.Generic; -using System.IO; - -namespace VirtualFilesystemMacCommon -{ - public class RemoteStorageManager - { - private FsEngine RemoteEngine; - private LocationMapper RemoteLocationMapper; - private ConsoleLogger Logger; - - public RemoteStorageManager(string remotePath) - { - RemoteEngine = new FsEngine(remotePath); - RemoteLocationMapper = new LocationMapper(remotePath); - Logger = new ConsoleLogger(GetType().Name); - } - - public List GetFolderContents(string identifier) - { - List items = new List(); - string remotePath = RemoteLocationMapper.GetRemotePathFromIdentifier(identifier); - - IFileSystemItem item = RemoteEngine.GetFileSystemItem(remotePath); - if (item is null || !item.GetType().Equals(typeof(UserFolder))) - { - return items; - } - - Logger.LogDebug("[TEST] GetFolderContents remote root path: " + RemoteLocationMapper.GetParentForIdentifier(remotePath) + ". from remote root: " + remotePath); - - UserFolder folder = (UserFolder)item; - FileSystemItemBasicInfo[] folderChildren = folder.GetChildren("*"); - for (long childIndex = 0; childIndex < folderChildren.Length; ++childIndex) - { - FileSystemItemBasicInfo child = folderChildren[childIndex]; - items.Add(new ItemMetadata(RemoteLocationMapper.GetIdentifierFromRemotePath(child.Name), RemoteLocationMapper.GetParentForIdentifier(child.Name), (NSDate)child.CreationTime, - (NSDate)child.ChangeTime, GetPathType(child), child.Size)); - } - - return items; - } - - public ItemMetadata GetItem(string identifier) - { - string remotePath = RemoteLocationMapper.GetRemotePathFromIdentifier(identifier); - ItemMetadata emptyItem = new ItemMetadata(remotePath); - - IFileSystemItem item = RemoteEngine.GetFileSystemItem(remotePath); - if (item is null) - { - Logger.LogError("Filesystem error getting item at path: " + remotePath); - return emptyItem; - } - - Logger.LogDebug("[TEST] GetItem remote root path: " + RemoteLocationMapper.GetParentForIdentifier(remotePath) + ". from remote root: " + remotePath); - - if (item.GetType().Equals(typeof(UserFolder))) - { - UserFolder folder = (UserFolder)item; - return new ItemMetadata(RemoteLocationMapper.GetIdentifierFromRemotePath(remotePath), RemoteLocationMapper.GetParentForIdentifier(remotePath), (NSDate)folder.FolderInfo.CreationTime, - (NSDate)folder.FolderInfo.ChangeTime, GetPathType(folder.FolderInfo), folder.FolderInfo.Size); - } - else if (item.GetType().Equals(typeof(UserFile))) - { - UserFile file = (UserFile)item; - return new ItemMetadata(RemoteLocationMapper.GetIdentifierFromRemotePath(remotePath), RemoteLocationMapper.GetParentForIdentifier(remotePath), (NSDate)file.FileInfo.CreationTime, - (NSDate)file.FileInfo.ChangeTime, GetPathType(file.FileInfo), file.FileInfo.Size); - } - - return emptyItem; - } - - private ItemMetadata.ItemMetadataType GetPathType(IFileSystemItemBasicInfo fileInfo) - { - return (fileInfo.Attributes.HasFlag(FileAttributes.Directory) ? ItemMetadata.ItemMetadataType.Dir : ItemMetadata.ItemMetadataType.File); - } - } -} diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/UserFile.cs b/VirtualFileSystemMac/VirtualFilesystemMacCommon/UserFile.cs deleted file mode 100644 index 95ece59..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/UserFile.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using ITHit.FileSystem; - -namespace VirtualFilesystemMacCommon -{ - public class UserFile : IFile - { - public readonly FileSystemItemBasicInfo FileInfo; - - public UserFile(string path) - { - ulong fileSize = (ulong)(new FileInfo(path)).Length; - FileInfo = new FileSystemItemBasicInfo(path, System.IO.File.GetAttributes(path), System.IO.File.GetCreationTime(path), - System.IO.File.GetLastWriteTime(path), fileSize); - } - - public Task CloseAsync(IOperationContext operationContext, IResultContext context) - { - Task task = new Task(() => - { - return null; - }); - - return task; - } - - public Task DeleteAsync(IOperationContext operationContext, IConfirmationResultContext resultContext) - { - Task task = new Task(() => - { - return null; - }); - - return task; - } - - public Task MoveToAsync(string targetPath, IOperationContext operationContext, IConfirmationResultContext resultContext) - { - Task task = new Task(() => - { - return null; - }); - - return task; - } - - public Task OpenAsync(IOperationContext operationContext, IResultContext context) - { - Task task = new Task(() => - { - return null; - }); - - return task; - } - - public Task TransferDataAsync(long offset, long length, ITransferDataOperationContext operationContext, ITransferDataResultContext resultContext) - { - Task task = new Task(() => - { - return null; - }); - - return task; - } - - public Task ValidateDataAsync(long offset, long length, IValidateDataOperationContext operationContext, IValidateDataResultContext resultContext) - { - Task task = new Task(() => - { - return null; - }); - - return task; - } - } -} diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/UserFolder.cs b/VirtualFileSystemMac/VirtualFilesystemMacCommon/UserFolder.cs deleted file mode 100644 index 87e02e0..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/UserFolder.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.IO; -using ITHit.FileSystem; - -namespace VirtualFilesystemMacCommon -{ - public class UserFolder : IFolder - { - public readonly FileSystemItemBasicInfo FolderInfo; - - public UserFolder(string path) - { - FolderInfo = new FileSystemItemBasicInfo(path, File.GetAttributes(path), File.GetCreationTime(path), File.GetLastWriteTime(path), 0); - } - - public Task GetChildrenAsync(string pattern, IOperationContext operationContext, IFolderListingResultContext resultContext) - { - return null; - } - - - public FileSystemItemBasicInfo[] GetChildren(string pattern) - { - string[] entries = Directory.GetFileSystemEntries(FolderInfo.Name, pattern); - FileSystemItemBasicInfo[] infos = new FileSystemItemBasicInfo[entries.Length]; - - for (int entryIndex = 0; entryIndex < entries.Length; ++entryIndex) - { - string currentFile = entries[entryIndex]; - ulong fileSize = 0; - if (!File.GetAttributes(currentFile).HasFlag(FileAttributes.Directory)) - { - fileSize = (ulong)(new FileInfo(currentFile)).Length; - } - - infos[entryIndex] = new FileSystemItemBasicInfo(currentFile, File.GetAttributes(currentFile), File.GetCreationTime(currentFile), - File.GetLastWriteTime(currentFile), fileSize); - } - - return infos; - } - - public Task DeleteAsync(IOperationContext operationContext, IConfirmationResultContext resultContext) - { - Task task = new Task(() => - { - return null; - }); - - return task; - } - - public Task MoveToAsync(string targetPath, IOperationContext operationContext, IConfirmationResultContext resultContext) - { - Task task = new Task(() => - { - return null; - }); - - return task; - } - } -} diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/VirtualFilesystemMacCommon.csproj b/VirtualFileSystemMac/VirtualFilesystemMacCommon/VirtualFilesystemMacCommon.csproj deleted file mode 100644 index aa846ad..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/VirtualFilesystemMacCommon.csproj +++ /dev/null @@ -1,77 +0,0 @@ - - - - Debug - AnyCPU - {A48CC336-6AC9-43B1-A948-58ACAFD105E2} - {A3F8F2AB-B479-4A4A-A458-A89E7DC349F1};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - VirtualFilesystemMacCommon - FileCloudSyncCommon - v2.0 - Xamarin.Mac - Resources - 0.5 - - - true - full - false - bin\Debug - DEBUG; - prompt - 4 - false - false - false - false - false - - None - None - 9.0 - - - true - bin\Release - - prompt - 4 - false - false - false - false - false - - None - None - 9.0 - - - - - - - ..\packages\ITHit.FileSystem.1.3.4108\lib\netstandard2.1\ITHit.FileSystem.dll - - - ..\packages\ITHit.FileSystem.Mac.1.4.4298-Alpha\lib\xamarinmac74\ITHit.FileSystem.Mac.dll - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Debug/VirtualFilesystemMacCommon.csproj.CoreCompileInputs.cache b/VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Debug/VirtualFilesystemMacCommon.csproj.CoreCompileInputs.cache deleted file mode 100644 index 857bd20..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Debug/VirtualFilesystemMacCommon.csproj.CoreCompileInputs.cache +++ /dev/null @@ -1 +0,0 @@ -d1096a34afe172ea259b451712a384de6b8aa3c5 diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Debug/VirtualFilesystemMacCommon.csproj.FileListAbsolute.txt b/VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Debug/VirtualFilesystemMacCommon.csproj.FileListAbsolute.txt deleted file mode 100644 index 43d528f..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Debug/VirtualFilesystemMacCommon.csproj.FileListAbsolute.txt +++ /dev/null @@ -1,2 +0,0 @@ -D:\Projects\WebDAV\fusemacnet\VirtualFilesystemMacCommon\obj\Debug\VirtualFilesystemMacCommon.csprojAssemblyReference.cache -D:\Projects\WebDAV\fusemacnet\VirtualFilesystemMacCommon\obj\Debug\VirtualFilesystemMacCommon.csproj.CoreCompileInputs.cache diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Debug/Xamarin.Mac,Version=v2.0.AssemblyAttributes.cs b/VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Debug/Xamarin.Mac,Version=v2.0.AssemblyAttributes.cs deleted file mode 100644 index b3553f5..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Debug/Xamarin.Mac,Version=v2.0.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute("Xamarin.Mac,Version=v2.0", FrameworkDisplayName = "Xamarin.Mac")] diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Release/Xamarin.Mac,Version=v2.0.AssemblyAttributes.cs b/VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Release/Xamarin.Mac,Version=v2.0.AssemblyAttributes.cs deleted file mode 100644 index b3553f5..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/obj/Release/Xamarin.Mac,Version=v2.0.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute("Xamarin.Mac,Version=v2.0", FrameworkDisplayName = "Xamarin.Mac")] diff --git a/VirtualFileSystemMac/VirtualFilesystemMacCommon/packages.config b/VirtualFileSystemMac/VirtualFilesystemMacCommon/packages.config deleted file mode 100644 index 4070e61..0000000 --- a/VirtualFileSystemMac/VirtualFilesystemMacCommon/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/WebDAVDrive.LoginWPF/ChallengeLogin.xaml b/WebDAVDrive.LoginWPF/ChallengeLogin.xaml deleted file mode 100644 index f5f7d8d..0000000 --- a/WebDAVDrive.LoginWPF/ChallengeLogin.xaml +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WebDAVDrive.LoginWPF/WebDAVDrive.LoginWPF.csproj b/WebDAVDrive.LoginWPF/WebDAVDrive.LoginWPF.csproj deleted file mode 100644 index f46df03..0000000 --- a/WebDAVDrive.LoginWPF/WebDAVDrive.LoginWPF.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - netcoreapp3.1 - true - - - - - - - - - - - - - - - - - diff --git a/WebDAVDrive.LoginWPF/AssemblyInfo.cs b/WebDAVDrive.UI/AssemblyInfo.cs similarity index 100% rename from WebDAVDrive.LoginWPF/AssemblyInfo.cs rename to WebDAVDrive.UI/AssemblyInfo.cs diff --git a/WebDAVDrive.UI/ChallengeLogin.xaml b/WebDAVDrive.UI/ChallengeLogin.xaml new file mode 100644 index 0000000..bbae9fe --- /dev/null +++ b/WebDAVDrive.UI/ChallengeLogin.xaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebDAVDrive.LoginWPF/ChallengeLogin.xaml.cs b/WebDAVDrive.UI/ChallengeLogin.xaml.cs similarity index 98% rename from WebDAVDrive.LoginWPF/ChallengeLogin.xaml.cs rename to WebDAVDrive.UI/ChallengeLogin.xaml.cs index f5d0755..5b2c32e 100644 --- a/WebDAVDrive.LoginWPF/ChallengeLogin.xaml.cs +++ b/WebDAVDrive.UI/ChallengeLogin.xaml.cs @@ -9,11 +9,11 @@ using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; -using WebDAVDrive.LoginWPF.ViewModels; +using WebDAVDrive.UI.ViewModels; using System.Security; using System.Runtime.InteropServices; -namespace WebDAVDrive.LoginWPF +namespace WebDAVDrive.UI { /// /// Interaction logic for ChallengeLogin.xaml diff --git a/WebDAVDrive.UI/ConnectFrom.xaml b/WebDAVDrive.UI/ConnectFrom.xaml new file mode 100644 index 0000000..f939a77 --- /dev/null +++ b/WebDAVDrive.UI/ConnectFrom.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebDAVDrive.UI/ConnectFrom.xaml.cs b/WebDAVDrive.UI/ConnectFrom.xaml.cs new file mode 100644 index 0000000..d05bb3a --- /dev/null +++ b/WebDAVDrive.UI/ConnectFrom.xaml.cs @@ -0,0 +1,50 @@ +using System.Windows; +using System.Windows.Media; +using WebDAVDrive.UI.ViewModels; + +namespace WebDAVDrive.UI +{ + /// + /// Interaction logic for ChallengeLogin.xaml + /// + public partial class ConnectForm : Window + { + public ConnectForm() + { + InitializeComponent(); + + this.DataContext = new ConnectViewModel(); + + //Setting backgorund color from windows settings + Brush brush = SystemParameters.WindowGlassBrush.Clone(); + Color backColor = ((SolidColorBrush)brush).Color; + + //Calculating text color from backgorund + //Using of luma, for more see https://en.wikipedia.org/wiki/Luma_%28video%29 + var l = 0.2126 * backColor.ScR + 0.7152 * backColor.ScG + 0.0722 * backColor.ScB; + Brush textColor = l < 0.5 ? Brushes.White : Brushes.Black; + + //set form background and foreground color + this.Resources["FormBackground"] = brush; + this.Resources["FormForeground"] = textColor; + } + + private void Button_Ok_Click(object sender, RoutedEventArgs e) + { + Window.GetWindow(this).DialogResult = true; + Close(); + } + + private void Button_Cancel_Click(object sender, RoutedEventArgs e) + { + Window.GetWindow(this).DialogResult = false; + Close(); + } + + private void OnCloseButtonClick(object sender, RoutedEventArgs e) + { + this.Close(); + } + } + +} diff --git a/WebDAVDrive.UI/ConsoleManager.cs b/WebDAVDrive.UI/ConsoleManager.cs new file mode 100644 index 0000000..b7a9538 --- /dev/null +++ b/WebDAVDrive.UI/ConsoleManager.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +namespace WebDAVDrive.UI +{ + public class ConsoleManager + { + #region console-show-hide + [DllImport("user32.dll")] + public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + [DllImport("user32.dll")] + static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + /// + /// Hide/Show console + /// + /// Console visibility + public static void SetConsoleWindowVisibility(bool visible) + { + IntPtr hWnd = FindWindow(null, Console.Title); + if (hWnd != IntPtr.Zero) + { + if (visible) ShowWindow(hWnd, 1); //1 = SW_SHOWNORMAL + else ShowWindow(hWnd, 0); //0 = SW_HIDE + } + } + #endregion + + /// + /// Starts new thread and waits while any key will be pressed in console. + /// + /// ManualResetEvent, invokes when any key will be pressed + /// + public static ConsoleKeyInfo WaitConsoleReadKey(ManualResetEvent exitEvent) + { + ConsoleKeyInfo exitKey = new ConsoleKeyInfo(); + Thread readKeyThread = new Thread(() => + { + exitKey = Console.ReadKey(); + exitEvent.Set(); + }); + readKeyThread.IsBackground = true; + readKeyThread.Start(); + return exitKey; + } + } +} diff --git a/WebDAVDrive.LoginWPF/Drive.ico b/WebDAVDrive.UI/Drive.ico similarity index 100% rename from WebDAVDrive.LoginWPF/Drive.ico rename to WebDAVDrive.UI/Drive.ico diff --git a/WebDAVDrive.UI/Localization/Resources.Designer.cs b/WebDAVDrive.UI/Localization/Resources.Designer.cs new file mode 100644 index 0000000..73965a0 --- /dev/null +++ b/WebDAVDrive.UI/Localization/Resources.Designer.cs @@ -0,0 +1,216 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace WebDAVDrive.UI.Localization { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WebDAVDrive.UI.Localization.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Cancel. + /// + public static string Cancel { + get { + return ResourceManager.GetString("Cancel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exit. + /// + public static string Exit { + get { + return ResourceManager.GetString("Exit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hide log. + /// + public static string HideLog { + get { + return ResourceManager.GetString("HideLog", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Idle. + /// + public static string Idle { + get { + return ResourceManager.GetString("Idle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keep me loged-in. + /// + public static string KeepLogined { + get { + return ResourceManager.GetString("KeepLogined", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login. + /// + public static string Login { + get { + return ResourceManager.GetString("Login", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LoginError Please enter login. + /// + public static string LoginError { + get { + return ResourceManager.GetString("LoginError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enter your credentials. + /// + public static string LoginMessage { + get { + return ResourceManager.GetString("LoginMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OK. + /// + public static string OK { + get { + return ResourceManager.GetString("OK", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password. + /// + public static string Password { + get { + return ResourceManager.GetString("Password", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show log. + /// + public static string ShowLog { + get { + return ResourceManager.GetString("ShowLog", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Start synchriniaztion. + /// + public static string StartSync { + get { + return ResourceManager.GetString("StartSync", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Status - Synching. + /// + public static string StatusSync { + get { + return ResourceManager.GetString("StatusSync", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Status - Synchronization stopped. + /// + public static string StatusSyncStopped { + get { + return ResourceManager.GetString("StatusSyncStopped", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stop synchronization. + /// + public static string StopSync { + get { + return ResourceManager.GetString("StopSync", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please enter correct WebDAV server URL. + /// + public static string URLError { + get { + return ResourceManager.GetString("URLError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enter WebDAV server URL. + /// + public static string URLMessage { + get { + return ResourceManager.GetString("URLMessage", resourceCulture); + } + } + } +} diff --git a/WebDAVDrive.UI/Localization/Resources.es-ES.resx b/WebDAVDrive.UI/Localization/Resources.es-ES.resx new file mode 100644 index 0000000..57c1fec --- /dev/null +++ b/WebDAVDrive.UI/Localization/Resources.es-ES.resx @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cancelar + + + Cerrar + + + Ocultar registro + + + Inactivo + + + Mantenme conectado + + + Login + + + Por favor ingrese su nombre de usuario + + + Ingrese sus credenciales + + + OK + + + Password + + + Mostrar registro + + + Iniciar sincronización + + + Sincronización + + + Se detuvo la sincronización + + + Detener la sincronización + + + Ingrese la URL correcta del servidor WebDAV + + + Ingrese la URL del servidor WebDAV + + \ No newline at end of file diff --git a/WebDAVDrive.UI/Localization/Resources.resx b/WebDAVDrive.UI/Localization/Resources.resx new file mode 100644 index 0000000..6f32837 --- /dev/null +++ b/WebDAVDrive.UI/Localization/Resources.resx @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cancel + + + Exit + + + Hide log + + + Idle + + + Keep me loged-in + + + Login + + + LoginError Please enter login + + + Enter your credentials + + + OK + + + Password + + + Show log + + + Start synchriniaztion + + + Status - Synching + + + Status - Synchronization stopped + + + Stop synchronization + + + Please enter correct WebDAV server URL + + + Enter WebDAV server URL + + \ No newline at end of file diff --git a/WebDAVDrive.UI/Localization/Resources.uk-UA.resx b/WebDAVDrive.UI/Localization/Resources.uk-UA.resx new file mode 100644 index 0000000..fd4dc49 --- /dev/null +++ b/WebDAVDrive.UI/Localization/Resources.uk-UA.resx @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Скасувати + + + Закрити + + + Приховати лог + + + Очікує + + + Запам'ятати мене + + + Логін + + + Введіть коректний логін + + + Введіть ваші дані + + + Гаразд + + + Пароль + + + Показати лог + + + Почати синхронізацію + + + Синхронізується + + + Синхронізацію зупинено + + + Зупинити синхронізацію + + + Введіть валідний URL WebDAV сервера + + + Введіть URL-адресу WebDAV сервера + + \ No newline at end of file diff --git a/WebDAVDrive.UI/RegistryManager.cs b/WebDAVDrive.UI/RegistryManager.cs new file mode 100644 index 0000000..3d76366 --- /dev/null +++ b/WebDAVDrive.UI/RegistryManager.cs @@ -0,0 +1,84 @@ +using ITHit.FileSystem.Samples.Common; +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace WebDAVDrive.UI +{ + public class RegistryManager + { + /// + /// Reads url from registry, if it will be null shows ConnectWindow. + /// + /// + /// WebDAV server url. + public static string GetURL(Settings settings) + { + //Try to load URL from Windows Registry + string webDAVServerUrl = GetURLFromRegistry(settings); + + //Try to get URL from URL form + if (string.IsNullOrEmpty(webDAVServerUrl)) + { + webDAVServerUrl = GetURLFromUser(settings); + } + + if (string.IsNullOrEmpty(webDAVServerUrl)) + { + throw new ArgumentNullException("Settings.WebDAVServerUrl"); + } + return webDAVServerUrl; + } + + /// + /// Returns WebDAV server URL from user dialog and stores this URL to windows registry (HKCU\SOFTWARE\AppID\WeDAVServerUrl). + /// + /// WebDAV server URL. + private static string GetURLFromUser(Settings settings) + { + string url = null; + bool dialogResult = false; + // Show URL dialog + WebDAVDrive.UI.ConnectForm connectForm = null; + Thread thread = new Thread(() => + { + connectForm = new WebDAVDrive.UI.ConnectForm(); + ((WebDAVDrive.UI.ViewModels.ConnectViewModel)connectForm.DataContext).WindowTitle = settings.ProductName; + connectForm.ShowDialog(); + + url = ((WebDAVDrive.UI.ViewModels.ConnectViewModel)connectForm.DataContext).Url; + dialogResult = (bool)connectForm.DialogResult; + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + + if (dialogResult) + { + using (RegistryKey key = Registry.CurrentUser.CreateSubKey($@"SOFTWARE\{settings.AppID}")) + { + key.SetValue("WeDAVServerUrl", url); + } + } + return (dialogResult) ? url : null; + } + + /// + /// Read URL from Windows Registry (HCKU\SOFTWARE\AppID\WeDAVServerUrl). + /// + /// + /// WebDAV server URL. + private static string GetURLFromRegistry(Settings settings) + { + string url = null; + RegistryKey key = Registry.CurrentUser.OpenSubKey($@"SOFTWARE\{settings.AppID}"); + if (key != null && key.GetValue("WeDAVServerUrl") != null) + { + url = key.GetValue("WeDAVServerUrl").ToString(); + } + return url; + } + } +} diff --git a/WebDAVDrive.UI/Styles.xaml b/WebDAVDrive.UI/Styles.xaml new file mode 100644 index 0000000..749535a --- /dev/null +++ b/WebDAVDrive.UI/Styles.xaml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WebDAVDrive.LoginWPF/Themes/Generic.xaml b/WebDAVDrive.UI/Themes/Generic.xaml similarity index 76% rename from WebDAVDrive.LoginWPF/Themes/Generic.xaml rename to WebDAVDrive.UI/Themes/Generic.xaml index 7edbbea..81945d8 100644 --- a/WebDAVDrive.LoginWPF/Themes/Generic.xaml +++ b/WebDAVDrive.UI/Themes/Generic.xaml @@ -1,6 +1,6 @@ + xmlns:local="clr-namespace:WebDAVDrive.UI"> diff --git a/WebDAVDrive.LoginWPF/ValidationRules.cs b/WebDAVDrive.UI/ValidationRules.cs similarity index 71% rename from WebDAVDrive.LoginWPF/ValidationRules.cs rename to WebDAVDrive.UI/ValidationRules.cs index 21b5915..269e964 100644 --- a/WebDAVDrive.LoginWPF/ValidationRules.cs +++ b/WebDAVDrive.UI/ValidationRules.cs @@ -1,16 +1,11 @@ using System.Globalization; using System.Windows.Controls; - using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Data; using System.Collections.ObjectModel; using System.Windows; -namespace WebDAVDrive.LoginWPF +namespace WebDAVDrive.UI { /// /// Login validation class @@ -20,14 +15,30 @@ class LoginValidationRule : ValidationRule public override ValidationResult Validate(object value, CultureInfo cultureInfo) { var str = value as string; - if (str == null || str == "") + if (string.IsNullOrEmpty(str)) + { + return new ValidationResult(false, Localization.Resources.LoginError); + } + return new ValidationResult(true, null); + } + } + + /// + /// Url validation class + /// + class UrlValidationRule : ValidationRule + { + public override ValidationResult Validate(object value, CultureInfo cultureInfo) + { + var str = value as string; + if (string.IsNullOrEmpty(str) || !Uri.IsWellFormedUriString(str, UriKind.RelativeOrAbsolute)) { - return new ValidationResult(false, "Please enter login"); + return new ValidationResult(false, Localization.Resources.URLError); } return new ValidationResult(true, null); } } - + /// /// This class used to convert validation errors to text and display on form as red text boxes /// diff --git a/WebDAVDrive.LoginWPF/ViewModels/BaseViewModel.cs b/WebDAVDrive.UI/ViewModels/BaseViewModel.cs similarity index 73% rename from WebDAVDrive.LoginWPF/ViewModels/BaseViewModel.cs rename to WebDAVDrive.UI/ViewModels/BaseViewModel.cs index e47c447..3631a88 100644 --- a/WebDAVDrive.LoginWPF/ViewModels/BaseViewModel.cs +++ b/WebDAVDrive.UI/ViewModels/BaseViewModel.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.ComponentModel; +using System.ComponentModel; -namespace WebDAVDrive.LoginWPF.ViewModels +namespace WebDAVDrive.UI.ViewModels { /// /// Base MVVM ViewModel class diff --git a/WebDAVDrive.LoginWPF/ViewModels/ChallengeLoginViewModel.cs b/WebDAVDrive.UI/ViewModels/ChallengeLoginViewModel.cs similarity index 91% rename from WebDAVDrive.LoginWPF/ViewModels/ChallengeLoginViewModel.cs rename to WebDAVDrive.UI/ViewModels/ChallengeLoginViewModel.cs index 9201226..c2cb16a 100644 --- a/WebDAVDrive.LoginWPF/ViewModels/ChallengeLoginViewModel.cs +++ b/WebDAVDrive.UI/ViewModels/ChallengeLoginViewModel.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Security; +using System.Security; -namespace WebDAVDrive.LoginWPF.ViewModels +namespace WebDAVDrive.UI.ViewModels { /// /// ViewModel for login window diff --git a/WebDAVDrive.UI/ViewModels/ConnectViewModel.cs b/WebDAVDrive.UI/ViewModels/ConnectViewModel.cs new file mode 100644 index 0000000..b030e2c --- /dev/null +++ b/WebDAVDrive.UI/ViewModels/ConnectViewModel.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Security; + + +namespace WebDAVDrive.UI.ViewModels +{ + /// + /// ViewModel for login window + /// + public class ConnectViewModel : BaseViewModel + { + private string url; + + /// + /// Url of WebDAV server + /// + public string Url + { + get => url; + set + { + url = value; + OnPropertyChanged(nameof(Url)); + } + } + + /// + /// Titile, displayd on head of login window + /// + public string WindowTitle { get; set; } + } +} diff --git a/WebDAVDrive.LoginWPF/WebBrowserLogin.xaml b/WebDAVDrive.UI/WebBrowserLogin.xaml similarity index 76% rename from WebDAVDrive.LoginWPF/WebBrowserLogin.xaml rename to WebDAVDrive.UI/WebBrowserLogin.xaml index 84e72fe..5095dac 100644 --- a/WebDAVDrive.LoginWPF/WebBrowserLogin.xaml +++ b/WebDAVDrive.UI/WebBrowserLogin.xaml @@ -1,11 +1,11 @@ - + Title="WebBrowserLogin" Height="1070" Width="1250" Icon="/WebDAVDrive.UI;component/Drive.ico"> diff --git a/WebDAVDrive.LoginWPF/WebBrowserLogin.xaml.cs b/WebDAVDrive.UI/WebBrowserLogin.xaml.cs similarity index 87% rename from WebDAVDrive.LoginWPF/WebBrowserLogin.xaml.cs rename to WebDAVDrive.UI/WebBrowserLogin.xaml.cs index f947400..0e65dda 100644 --- a/WebDAVDrive.LoginWPF/WebBrowserLogin.xaml.cs +++ b/WebDAVDrive.UI/WebBrowserLogin.xaml.cs @@ -3,20 +3,11 @@ using Microsoft.Web.WebView2.Core; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Net; -using System.Text; using System.Threading.Tasks; using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; - -namespace WebDAVDrive.LoginWPF + +namespace WebDAVDrive.UI { /// /// Interaction logic for WebBrowserLogin.xaml @@ -57,7 +48,7 @@ public partial class WebBrowserLogin : Window /// /// Task for initializign of WebView2 /// - private Task BrowserInitializeTask; + private Task WebViewInitializeTask; /// /// Navigation events count @@ -98,20 +89,20 @@ public void WebBrowserLogin_Load(object sender, EventArgs e) webView.Source = new Uri(this.url.ToString(), UriKind.Absolute); }; CountOfNavigationStartingEvents = 0; - webView.CoreWebView2InitializationCompleted += Browser_CoreWebView2Ready; - BrowserInitializeTask = webView.EnsureCoreWebView2Async(); + webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2Ready; + WebViewInitializeTask = webView.EnsureCoreWebView2Async(); } /// /// CoreWebView2InitializationCompleted event handler /// - private void Browser_CoreWebView2Ready(object sender, EventArgs e) + private void WebView_CoreWebView2Ready(object sender, EventArgs e) { webView.CoreWebView2.DocumentTitleChanged += DocumentTitleChanged; webView.CoreWebView2.WebResourceResponseReceived += WebResourceResponseReceived; - BrowserInitializeTask.ContinueWith((t) => + WebViewInitializeTask.ContinueWith((t) => { - webView.NavigationStarting += Browser_NavigationStarting; + webView.NavigationStarting += WebView_NavigationStarting; try { webView.Dispatcher.Invoke(DoNavigation); @@ -125,7 +116,7 @@ private void Browser_CoreWebView2Ready(object sender, EventArgs e) /// /// CoreWebView2Initialization event handler /// - private void Browser_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e) + private void WebView_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e) { CountOfNavigationStartingEvents += 1; } @@ -170,7 +161,7 @@ private async void WebResourceResponseReceived(object sender, Microsoft.Web.WebV } catch { - // Request failed. Login failed or did not complete yet. + log.Error("Request failed. Login failed or did not complete yet."); } } } @@ -186,7 +177,7 @@ private bool IsSuccess(int statusCode) /// private void DocumentTitleChanged(object sender, object e) { - this.Title = this.Title + " - " + webView.CoreWebView2.DocumentTitle; + Title = Title+" - "+webView.CoreWebView2.DocumentTitle; } } diff --git a/WebDAVDrive.UI/WebDAVDrive.UI.csproj b/WebDAVDrive.UI/WebDAVDrive.UI.csproj new file mode 100644 index 0000000..6cb91b5 --- /dev/null +++ b/WebDAVDrive.UI/WebDAVDrive.UI.csproj @@ -0,0 +1,57 @@ + + + + netcoreapp3.1 + true + True + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + Designer + Resources.es-ES.Designer.cs + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + Resources..Designer.cs + PublicResXFileCodeGenerator + + + + + + $(DefaultXamlRuntime) + + + + diff --git a/WebDAVDrive.UI/WindowsTrayInterface.cs b/WebDAVDrive.UI/WindowsTrayInterface.cs new file mode 100644 index 0000000..fa2796d --- /dev/null +++ b/WebDAVDrive.UI/WindowsTrayInterface.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Windows.Forms; + +using ITHit.FileSystem.Samples.Common; +using ITHit.FileSystem.Samples.Common.Syncronyzation; +using static ITHit.FileSystem.Samples.Common.Syncronyzation.FullSyncService; + +namespace WebDAVDrive.UI +{ + /// + /// Represents tray application. + /// + public class WindowsTrayInterface + { + /// + /// Create new tray icon. + /// + /// Product name. + /// Sync service + /// ManualResetEvent, used to stop application + /// + public static Thread CreateTrayInterface(string productName, FullSyncService syncService, ManualResetEvent exitEvent) + { + // Start tray application. + Thread thread = new Thread(() => { + WindowsTrayInterface windowsTrayInterface = new WindowsTrayInterface($"{productName}", syncService); + syncService.syncEvent += windowsTrayInterface.HandleStatusChange; + Application.Run(); + exitEvent.Set(); + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.IsBackground = true; + return thread; + } + + /// + /// Changes button status to Idle + /// + private void StatusToIdle() + { + notifyIcon.Text = Title + $"\n{Localization.Resources.Idle}"; + notifyIcon.ContextMenuStrip.Items[0].Text = Localization.Resources.StopSync; + } + + /// + /// Changes button status to Synching + /// + private void StatusToSynching() + { + notifyIcon.Text = Title + $"\n{Localization.Resources.StatusSync}"; + notifyIcon.ContextMenuStrip.Items[0].Text = Localization.Resources.StopSync; + } + + private void StatusToSyncStopped() + { + notifyIcon.Text = Title + $"\n{Localization.Resources.StopSync}"; + notifyIcon.ContextMenuStrip.Items[0].Text = Localization.Resources.StartSync; + } + /// + /// Icon in the status bar notification area. + /// + public NotifyIcon notifyIcon; + + /// + /// Visibility of notify icon. + /// + public static bool Visible = true; + + /// + /// Notify icon title + /// + public string Title { get; set; } + + /// + /// Icon click handler delegete + /// + public delegate void ItemClickHanler(); + + /// + /// Creates a tray application instance. + /// + /// Tray application title. + /// + /// Synchronization service instance. The tray application will enable/disable this application and show its status. + /// + public WindowsTrayInterface(string title, FullSyncService syncService) + { + Title = title; + notifyIcon = new NotifyIcon(); + + notifyIcon.Visible = true; + notifyIcon.Icon = new System.Drawing.Icon("Images\\Drive.ico"); + notifyIcon.Text = title; + + ContextMenuStrip contextMenu = new ContextMenuStrip(); + contextMenu.Items.Add(Localization.Resources.StopSync, null, (s, e) => { StartStopSync(syncService); }); +#if !DEBUG + // Hide console on app start. + Visible = false; + SetConsoleWindowVisibility(false); + contextMenu.Items.Add(Localization.Resources.ShowLog, null, (s, e) => { +#else + contextMenu.Items.Add(Localization.Resources.HideLog, null, (s, e) => { +#endif + Visible = !Visible; + ConsoleManager.SetConsoleWindowVisibility(Visible); + contextMenu.Items[1].Text = (Visible)? Localization.Resources.HideLog : Localization.Resources.ShowLog; + }); + + contextMenu.Items.Add($"{Localization.Resources.Exit} {title}",null, (s,e) => { Application.Exit(); }); + + notifyIcon.ContextMenuStrip = contextMenu; + + notifyIcon.MouseClick += (object sender, MouseEventArgs e) => + { + //MessageBox.Show("Clicked"); + }; + } + + /// + /// Defines StartStop sync button in tray app. + /// + private static bool sycnStopped = false; + + /// + /// This method handles StartStop Sycn button in tray menu. + /// + private async void StartStopSync(FullSyncService syncService) + { + if (!sycnStopped) + { + await syncService.StopAsync(); + sycnStopped = true; + StatusToSyncStopped(); + } + else + { + await syncService.StartAsync(); + sycnStopped = false; + StatusToIdle(); + } + } + + /// + /// Start/stop synching evenet handler. + /// + public void HandleStatusChange(object sender, SynchEventArgs synchEventArgs) + { + if (synchEventArgs.state == SynchronizationState.Started) + { + StatusToSynching(); + } + else + { + StatusToIdle(); + } + } + } +} diff --git a/WebDAVDrive/Settings.cs b/WebDAVDrive/AppSettings.cs similarity index 53% rename from WebDAVDrive/Settings.cs rename to WebDAVDrive/AppSettings.cs index daafb73..ac8c41c 100644 --- a/WebDAVDrive/Settings.cs +++ b/WebDAVDrive/AppSettings.cs @@ -1,24 +1,24 @@ using Microsoft.Extensions.Configuration; +using Microsoft.Win32; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; -namespace VirtualFileSystem +using ITHit.FileSystem.Samples.Common; +using WebDAVDrive.UI; + +namespace WebDAVDrive { /// - /// Strongly binded project settings. + /// Strongly binded application settings. /// - public class Settings + public class AppSettings : Settings { - /// - /// IT Hit User File System License string; - /// - public string UserFileSystemLicense { get; set; } - /// /// IT Hit WebDAV Client Library for .NET License string; /// @@ -28,69 +28,37 @@ public class Settings /// WebDAV server URL. /// public string WebDAVServerUrl { get; set; } - - /// - /// Your virtual file system will be mounted under this path. - /// - public string UserFileSystemRootPath { get; set; } - - /// - /// Full synchronization interval in milliseconds. - /// - public double SyncIntervalMs { 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. - /// - public string IconsFolderPath { get; set; } - - /// - /// Product name. Displayed as a mounted folder name under Desktop as - /// well in every location where product name is required. - /// - public string ProductName { get; set; } - - /// - /// Path to the folder that stores ETags, locks and other data associated with files and folders. - /// - public string ServerDataFolderPath { get; set; } - - /// - /// Automatically lock the file in remote storage when a file handle is being opened for writing, unlock on close. - /// - public bool AutoLock { get; set; } } /// - /// Binds, validates and normalizes Settings configuration. + /// Binds, validates and normalizes application settings. /// public static class SettingsConfigValidator { /// - /// Binds, validates and normalizes WebDAV Context configuration. + /// Binds, validates and normalizes application configuration. /// - /// Instance of . - /// Virtual File System Settings. - public static Settings ReadSettings(this IConfiguration configuration) + /// Application configuration properties. + /// Application settings. + public static AppSettings ReadSettings(this IConfiguration configuration) { - Settings settings = new Settings(); + AppSettings settings = new AppSettings(); if (configuration == null) { throw new ArgumentNullException("configurationSection"); } + string assemblyLocation = Assembly.GetEntryAssembly().Location; + + // Load product name from entry exe file. + settings.ProductName = FileVersionInfo.GetVersionInfo(assemblyLocation).ProductName; + configuration.Bind(settings); if (string.IsNullOrEmpty(settings.WebDAVServerUrl)) { - throw new ArgumentNullException("Settings.WebDAVServerUrl"); + settings.WebDAVServerUrl = RegistryManager.GetURL(settings); } settings.WebDAVServerUrl = $"{settings.WebDAVServerUrl.TrimEnd('/')}/"; @@ -111,20 +79,14 @@ public static Settings ReadSettings(this IConfiguration configuration) { Directory.CreateDirectory(settings.UserFileSystemRootPath); } - - string assemblyLocation = Assembly.GetEntryAssembly().Location; - + // Icons folder. settings.IconsFolderPath = Path.Combine(Path.GetDirectoryName(assemblyLocation), @"Images"); - // Load product name from entry exe file. - settings.ProductName = FileVersionInfo.GetVersionInfo(assemblyLocation).ProductName; - // Folder where eTags and file locks are stored. string localApplicationDataFolderPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); settings.ServerDataFolderPath = Path.Combine(localApplicationDataFolderPath, settings.ProductName, settings.UserFileSystemRootPath.Replace(":", ""), "ServerData"); - return settings; - } + } } } diff --git a/WebDAVDrive/CredentialManager.cs b/WebDAVDrive/CredentialManager.cs index 4548005..4fa1e5f 100644 --- a/WebDAVDrive/CredentialManager.cs +++ b/WebDAVDrive/CredentialManager.cs @@ -5,27 +5,30 @@ using System.Security; using System.Text; using System.Threading.Tasks; -using VirtualFileSystem; +using Windows.Security.Credentials; namespace WebDAVDrive { - class CredentialManager + /// + /// Saves and reads login and password to/from Windows Credentials Manger. + /// + public class CredentialManager { /// - /// Save credential from login form to Windows Credentials Manger + /// Saves login and password to the Windows Credentials Manger. /// - /// Name of credential - /// User login - /// User password - public static void SaveCredentials(string resource,string login, SecureString securePassword) + /// Resource name under which credentials will be saved. + /// User name to be saved. + /// Password to be saved. + public static void SaveCredentials(string resource, string login, SecureString securePassword) { - Windows.Security.Credentials.PasswordVault vault = new Windows.Security.Credentials.PasswordVault(); + PasswordVault vault = new PasswordVault(); //retrive string password form SecureString (password can not be retrived directly from Security string) string password = new System.Net.NetworkCredential(string.Empty, securePassword).Password; - Windows.Security.Credentials.PasswordCredential credential = new Windows.Security.Credentials.PasswordCredential() + PasswordCredential credential = new PasswordCredential() { UserName = login, Password = password, @@ -37,16 +40,16 @@ public static void SaveCredentials(string resource,string login, SecureString se } /// - /// Get credentials from windows credentials manger + /// Gets credentials from the Windows Credentials Manger. /// - /// Name of credential - /// Name of credential - /// Credentials for current application - public static Windows.Security.Credentials.PasswordCredential GetCredentials(string resource, ILog log) + /// Resource name from which ro retrieve the credentials. + /// Logger. + /// Credentials for current application. + public static PasswordCredential GetCredentials(string resource, ILog log) { - Windows.Security.Credentials.PasswordCredential credential = null; + PasswordCredential credential = null; - var vault = new Windows.Security.Credentials.PasswordVault(); + PasswordVault vault = new PasswordVault(); try { diff --git a/WebDAVDrive/Framework/Locks/LockInfo.cs b/WebDAVDrive/Framework/Locks/LockInfo.cs deleted file mode 100644 index b73c46c..0000000 --- a/WebDAVDrive/Framework/Locks/LockInfo.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace VirtualFileSystem -{ - /// - /// Information about the lock returned from the remote storage as a result of the lock operation. - /// - public class LockInfo - { - /// - /// Lock-token. Must be supplied during the item update and unlock operations. - /// - public string LockToken { get; set; } - - /// - /// Lock expidation date/time returned by the server. - /// - public DateTimeOffset LockExpirationDateUtc { get; set; } - } -} diff --git a/WebDAVDrive/Images/Blank.ico b/WebDAVDrive/Images/Blank.ico new file mode 100644 index 0000000000000000000000000000000000000000..cf158e1c78b3fcbff84f102d29e790128e6ac56c GIT binary patch literal 1150 zcmZQzU<5(|0R|vYV8~!$U=RbcG=LZ+qyWT>V3L8s0Vp>LMniyv5MV@7i_At69%T;Y Q5cvO}fdL=>cOSnz0MINULjV8( literal 0 HcmV?d00001 diff --git a/WebDAVDrive/Images/Drive.ico b/WebDAVDrive/Images/Drive.ico index d6aad0c95b7372eaf8ecbf875d31f4bb37c58d16..0f2b936202380d950303b953e61e2b5fc228d500 100644 GIT binary patch literal 454276 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�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

o~cW1|32y_Bj#Ozxct@n}V902{zoCKT# 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 n9cpWAk&zHxJxe{qjU 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 W8F)88pu89Y{uE?YWJ;cz1^q84=`0ce literal 122800 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 F{2%r&8R-B3 diff --git a/WebDAVDrive/Images/DrivePause.ico b/WebDAVDrive/Images/DrivePause.ico new file mode 100644 index 0000000000000000000000000000000000000000..1b3e2fd9f93a9cfbf1c7508a889e936d1c7b8579 GIT binary patch literal 410598 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%0Dl2Ec76|30PX;O1Kb7N2RsPybNqZ4 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 /// Maps a user file system path to the remote storage path and back. @@ -74,7 +76,7 @@ public static string ReverseMapPath(string remoteStorageUri) ///

/// Remote storage item info. /// User file system item info. - public static FileSystemItemBasicInfo GetUserFileSysteItemBasicInfo(IHierarchyItemAsync remoteStorageItem) + public static FileSystemItemBasicInfo GetUserFileSystemItemBasicInfo(IHierarchyItemAsync remoteStorageItem) { FileSystemItemBasicInfo userFileSystemItem; @@ -95,15 +97,10 @@ public static FileSystemItemBasicInfo GetUserFileSysteItemBasicInfo(IHierarchyIt userFileSystemItem.ChangeTime = remoteStorageItem.LastModified; // If the item is locked by another user, set the LockedByAnotherUser to true. + // This will set read-only arrtibute on files. + // Note that read-only attribute is a convenience feture, it does not protect files from modification. userFileSystemItem.LockedByAnotherUser = remoteStorageItem.ActiveLocks.Length > 0; - // If the file is moved/renamed and the app is not running this will help us - // to sync the file/folder to remote storage after app starts. - userFileSystemItem.CustomData = new CustomData - { - OriginalPath = Mapping.ReverseMapPath(remoteStorageItem.Href.ToString()) - }.Serialize(); - if (remoteStorageItem is IFileAsync) { // We send the ETag to @@ -114,6 +111,28 @@ public static FileSystemItemBasicInfo GetUserFileSysteItemBasicInfo(IHierarchyIt ((FileBasicInfo)userFileSystemItem).Length = ((IFileAsync)remoteStorageItem).ContentLength; }; + // Set custom columns to be displayed in file manager. + // We create property definitions when registering the sync root with corresponding IDs. + List customProps = new List(); + LockInfo lockInfo = remoteStorageItem.ActiveLocks.FirstOrDefault(); + if (lockInfo != null) + { + customProps.AddRange( + new ServerLockInfo() + { + LockToken = lockInfo.LockToken.LockToken, + Owner = lockInfo.Owner, + Exclusive = lockInfo.LockScope == LockScope.Exclusive, + LockExpirationDateUtc = DateTimeOffset.Now.Add(lockInfo.TimeOut) + }.GetLockProperties(Path.Combine(Config.Settings.IconsFolderPath, "LockedByAnotherUser.ico")) + ); + } + if (remoteStorageItem is IFileAsync) + { + customProps.Add(new FileSystemItemPropertyData(5, ((IFileAsync)remoteStorageItem).Etag)); + }; + userFileSystemItem.CustomProperties = customProps; + return userFileSystemItem; } } diff --git a/WebDAVDrive/Program.cs b/WebDAVDrive/Program.cs index 744f562..91eb1ca 100644 --- a/WebDAVDrive/Program.cs +++ b/WebDAVDrive/Program.cs @@ -1,10 +1,4 @@ -using ITHit.FileSystem; -using ITHit.FileSystem.Windows; -using ITHit.WebDAV.Client; -using ITHit.WebDAV.Client.Exceptions; -using log4net; -using log4net.Config; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; using System.Diagnostics; @@ -15,20 +9,29 @@ using System.Security; using System.Threading; using System.Threading.Tasks; -using VirtualFileSystem.Syncronyzation; -using WebDAVDrive; -using WebDAVDrive.LoginWPF.ViewModels; +using WebDAVDrive.UI.ViewModels; using Windows.Storage; using Windows.Storage.Provider; +using System.Windows.Forms; + +using log4net; +using log4net.Config; + +using ITHit.FileSystem; +using ITHit.FileSystem.Windows; +using ITHit.FileSystem.Samples.Common; +using ITHit.WebDAV.Client; +using ITHit.WebDAV.Client.Exceptions; +using WebDAVDrive.UI; -namespace VirtualFileSystem +namespace WebDAVDrive { class Program { /// /// Application settings. /// - internal static Settings Settings; + internal static AppSettings Settings; /// /// WebDAV client for accessing the WebDAV server. @@ -41,36 +44,30 @@ class Program private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); /// - /// Processes file system calls, implements on-demand loading and initial data transfer from remote storage to client. + /// Processes OS file system calls, + /// synchronizes user file system to remote storage and back, + /// monitors files pinning and unpinning. /// - private static VfsEngine engine; + private static VirtualDrive virtualDrive; /// /// Monitores changes in the remote file system. /// internal static RemoteStorageMonitor RemoteStorageMonitorInstance; - /// - /// Monitors pinned and unpinned attributes in user file system. - /// - private static UserFileSystemMonitor userFileSystemMonitor; - - /// - /// Performs complete synchronyzation of the folders and files that are already synched to user file system. - /// - private static FullSyncService syncService; - //[STAThread] static async Task Main(string[] args) { // Load Settings. IConfiguration configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json", false, true).Build(); Settings = configuration.ReadSettings(); + Config.Settings = Settings; // Load Log4Net for net configuration. var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config")); - // Enable UTF8 for Console Window + + // Enable UTF8 for Console window. Console.OutputEncoding = System.Text.Encoding.UTF8; log.Info($"\n{Settings.ProductName}"); @@ -83,58 +80,58 @@ static async Task Main(string[] args) // Here we register it during first program start for the sake of the development convenience. if (!await Registrar.IsRegisteredAsync(Settings.UserFileSystemRootPath)) { - Directory.CreateDirectory(Settings.UserFileSystemRootPath); log.Info($"\nRegistering {Settings.UserFileSystemRootPath} sync root."); - - - await Registrar.RegisterAsync(SyncRootId, Settings.UserFileSystemRootPath, Settings.ProductName); + Directory.CreateDirectory(Settings.UserFileSystemRootPath); + Directory.CreateDirectory(Settings.ServerDataFolderPath); + + await Registrar.RegisterAsync(SyncRootId, Settings.UserFileSystemRootPath, Settings.ProductName, + Path.Combine(Config.Settings.IconsFolderPath, "Drive.ico")); } else { log.Info($"\n{Settings.UserFileSystemRootPath} sync root already registered."); } - // Log indexed state. + // Log indexed state. Indexing must be enabled for the sync root to function. StorageFolder userFileSystemRootFolder = await StorageFolder.GetFolderFromPathAsync(Settings.UserFileSystemRootPath); log.Info($"\nIndexed state: {(await userFileSystemRootFolder.GetIndexedStateAsync())}\n"); ConfigureWebDAVClient(); - ConsoleKeyInfo exitKey; + ConsoleKeyInfo exitKey = new ConsoleKeyInfo(); + + // Event to be fired when any key will be pressed in the console or when the tray application exits. + ManualResetEvent exitEvent = new ManualResetEvent(false); try { - engine = new VfsEngine(Settings.UserFileSystemLicense, Settings.UserFileSystemRootPath, log); + virtualDrive = new VirtualDrive(Settings.UserFileSystemLicense, Settings.UserFileSystemRootPath, log, Settings.SyncIntervalMs); RemoteStorageMonitorInstance = new RemoteStorageMonitor(Settings.WebDAVServerUrl, log); - syncService = new FullSyncService(Settings.SyncIntervalMs, Settings.UserFileSystemRootPath, log); - userFileSystemMonitor = new UserFileSystemMonitor(Settings.UserFileSystemRootPath, log); + + // Start tray application. + Thread tryIconThread = WindowsTrayInterface.CreateTrayInterface(Settings.ProductName, virtualDrive.SyncService, exitEvent); // Start processing OS file system calls. //engine.ChangesProcessingEnabled = false; - await engine.StartAsync(); + await virtualDrive.StartAsync(); // Start monitoring changes in remote file system. //await RemoteStorageMonitorInstance.StartAsync(); - - // Start periodical synchronyzation between client and server, - // in case any changes are lost because the client or the server were unavailable. - await syncService.StartAsync(); - - // Start monitoring pinned/unpinned attributes and files/folders creation in user file system. - await userFileSystemMonitor.StartAsync(); + #if DEBUG // Opens Windows File Manager with user file system folder and remote storage folder. ShowTestEnvironment(); #endif // Keep this application running until user input. - exitKey = Console.ReadKey(); + exitKey = WebDAVDrive.UI.ConsoleManager.WaitConsoleReadKey(exitEvent); + + //wait until the button "Exit" is pressed or any key in console is peressed to stop application + exitEvent.WaitOne(); } finally { - engine.Dispose(); + virtualDrive.Dispose(); RemoteStorageMonitorInstance.Dispose(); - syncService.Dispose(); - userFileSystemMonitor.Dispose(); } if (exitKey.KeyChar == 'q') @@ -159,6 +156,15 @@ static async Task Main(string[] args) { log.Error($"\n{ex}"); } + + try + { + Directory.Delete(Config.Settings.ServerDataFolderPath, true); + } + catch (Exception ex) + { + log.Error($"\n{ex}"); + } } else { @@ -192,7 +198,7 @@ private static void ShowTestEnvironment() } - // Open Windows File Manager with ETags and locks storage. + // Open Windows File Manager with ETags and locks storage. Uncomment this to debug locks and ETags management. //ProcessStartInfo serverDataInfo = new ProcessStartInfo(Program.Settings.ServerDataFolderPath); //serverDataInfo.UseShellExecute = true; // Open window only if not opened already. //using (Process serverDataWinFileManager = Process.Start(serverDataInfo)) @@ -210,7 +216,7 @@ private static string SyncRootId { get { - return $"{System.Diagnostics.Process.GetCurrentProcess().ProcessName}!{System.Security.Principal.WindowsIdentity.GetCurrent().User}!User"; + return $"{Settings.AppID}!{System.Security.Principal.WindowsIdentity.GetCurrent().User}!User"; } } @@ -243,7 +249,12 @@ private static void ConfigureWebDAVClient() /// private static uint loginRetriesCurrent = 0; - + /// + /// Even handler to process WebDAV errors. + /// If server returns 401 or 302 response here we show the login dialog. + /// + /// Request to the WebDAV server. + /// WebDAV error details. private static void DavClient_WebDavError(IWebRequestAsync sender, WebDavErrorEventArgs e) { WebDavHttpException httpException = e.Exception as WebDavHttpException; @@ -266,9 +277,9 @@ private static void DavClient_WebDavError(IWebRequestAsync sender, WebDavErrorEv Uri failedUri = (e.Exception as WebDavHttpException).Uri; - WebDAVDrive.LoginWPF.WebBrowserLogin webBrowserLogin = null; + WebDAVDrive.UI.WebBrowserLogin webBrowserLogin = null; Thread thread = new Thread(() => { - webBrowserLogin = new WebDAVDrive.LoginWPF.WebBrowserLogin(failedUri, e.Request, DavClient, log); + webBrowserLogin = new WebDAVDrive.UI.WebBrowserLogin(failedUri, e.Request, DavClient, log); webBrowserLogin.Title = Settings.ProductName; webBrowserLogin.ShowDialog(); }); @@ -314,10 +325,10 @@ private static void DavClient_WebDavError(IWebRequestAsync sender, WebDavErrorEv bool keepLogedin = false; // Show login dialog - WebDAVDrive.LoginWPF.ChallengeLogin loginForm = null; + WebDAVDrive.UI.ChallengeLogin loginForm = null; Thread thread = new Thread(() => { - loginForm = new WebDAVDrive.LoginWPF.ChallengeLogin(); + loginForm = new WebDAVDrive.UI.ChallengeLogin(); ((ChallengeLoginViewModel)loginForm.DataContext).Url = failedUri.OriginalString; ((ChallengeLoginViewModel)loginForm.DataContext).WindowTitle = Settings.ProductName; loginForm.ShowDialog(); diff --git a/WebDAVDrive/RemoteStorageMonitor.cs b/WebDAVDrive/RemoteStorageMonitor.cs index 21cec5b..f87eab9 100644 --- a/WebDAVDrive/RemoteStorageMonitor.cs +++ b/WebDAVDrive/RemoteStorageMonitor.cs @@ -1,7 +1,4 @@ -using ITHit.FileSystem; -using ITHit.FileSystem.Windows; -using log4net; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -11,8 +8,13 @@ using Windows.Storage.FileProperties; using Windows.Storage.Provider; using Windows.System.Update; +using log4net; + +using ITHit.FileSystem; +using ITHit.FileSystem.Windows; +using ITHit.FileSystem.Samples.Common; -namespace VirtualFileSystem.Syncronyzation +namespace WebDAVDrive { /// /// Monitors changes in the remote storage, notifies the client and updates the user file system. diff --git a/WebDAVDrive/UserFile.cs b/WebDAVDrive/UserFile.cs index d368154..7a361ae 100644 --- a/WebDAVDrive/UserFile.cs +++ b/WebDAVDrive/UserFile.cs @@ -1,25 +1,27 @@ -using ITHit.FileSystem; -using ITHit.WebDAV.Client; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; -namespace VirtualFileSystem +using ITHit.FileSystem; +using ITHit.FileSystem.Samples.Common; +using ITHit.WebDAV.Client; + +namespace WebDAVDrive { /// /// Represents a file in the remote storage. /// /// You will change methods of this class to read/write data from/to your remote storage. - internal class UserFile : UserFileSystemItem + internal class UserFile : UserFileSystemItem, IUserFile { /// /// Creates instance of this class. /// /// Path of this file in the user file system. /// Information about file lock. Pass null if the item is not locked. - public UserFile(string userFileSystemFilePath, LockInfo lockInfo = null) : base(userFileSystemFilePath, lockInfo) + public UserFile(string userFileSystemFilePath) : base(userFileSystemFilePath) { } @@ -41,11 +43,11 @@ public async Task ReadAsync(long offset, long length) byte[] buffer = new byte[length]; int bufferPos = 0; int bytesRead = 0; - while ((bytesRead = await stream.ReadAsync(buffer, bufferPos, (int)length)) > 0 ) + while ((bytesRead = await stream.ReadAsync(buffer, bufferPos, (int)length)) > 0) { bufferPos += bytesRead; length -= bytesRead; - } + } return buffer; } } @@ -60,10 +62,11 @@ public async Task ValidateDataAsync(long offset, long length) /// /// New information about the file, such as creation date, modification date, attributes, etc. /// New file content or null if the file content is not modified. + /// Information about the lock. Caller passes null if the item is not locked. /// New ETag returned from the remote storage. - public async Task UpdateAsync(IFileBasicInfo fileInfo, Stream content = null) + public async Task UpdateAsync(IFileBasicInfo fileInfo, Stream content = null, ServerLockInfo lockInfo = null) { - return await CreateOrUpdateFileAsync(new Uri(RemoteStorageUri), fileInfo, FileMode.Open, content); + return await CreateOrUpdateFileAsync(new Uri(RemoteStorageUri), fileInfo, FileMode.Open, content, lockInfo); } } } diff --git a/WebDAVDrive/UserFileSystemItem.cs b/WebDAVDrive/UserFileSystemItem.cs index 9d84b74..686d8d8 100644 --- a/WebDAVDrive/UserFileSystemItem.cs +++ b/WebDAVDrive/UserFileSystemItem.cs @@ -1,20 +1,21 @@ -using ITHit.FileSystem; -using ITHit.FileSystem.Windows; -using ITHit.WebDAV.Client; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; -namespace VirtualFileSystem +using ITHit.FileSystem; +using ITHit.FileSystem.Samples.Common; +using ITHit.WebDAV.Client; + +namespace WebDAVDrive { /// /// Represents a file or a folder in the remote storage. Contains methods common for both files and folders. /// /// You will change methods of this class to read/write data from/to your remote storage. - internal class UserFileSystemItem + internal class UserFileSystemItem : IUserFileSystemItem { /// /// Path of this file of folder in the user file system. @@ -26,21 +27,14 @@ internal class UserFileSystemItem /// protected string RemoteStorageUri; - /// - /// Information about the lock. Null if the item is not locked. - /// - protected LockInfo Lock; - /// /// Creates instance of this class. /// /// Path of this file of folder in the user file system. - /// Information about the lock. Pass null if the item is not locked. - public UserFileSystemItem(string userFileSystemPath, LockInfo lockInfo = null) + public UserFileSystemItem(string userFileSystemPath) { this.UserFileSystemPath = userFileSystemPath; this.RemoteStorageUri = Mapping.MapPath(userFileSystemPath); - this.Lock = lockInfo; } /// @@ -70,8 +64,9 @@ public async Task DeleteAsync() /// New information about the file, such as modification date, attributes, custom data, etc. /// Specifies if a new file should be created or existing file should be updated. /// New file content or null if the file content is not modified. + /// Information about the lock. Caller passes null if the item is not locked. /// New ETag returned from the remote storage. - protected async Task CreateOrUpdateFileAsync(Uri remoteStorageUri, IFileBasicInfo newInfo, FileMode mode, Stream content = null) + protected async Task CreateOrUpdateFileAsync(Uri remoteStorageUri, IFileBasicInfo newInfo, FileMode mode, Stream content = null, ServerLockInfo lockInfo = null) { string eTag = null; string lockToken = null; @@ -82,12 +77,12 @@ protected async Task CreateOrUpdateFileAsync(Uri remoteStorageUri, IFile eTag = await ETag.GetETagAsync(UserFileSystemPath); // Get lock-token. - lockToken = Lock?.LockToken; + lockToken = lockInfo?.LockToken; } if (content != null || mode == FileMode.CreateNew) { - long contentLength = content!=null ? content.Length : 0; + long contentLength = content != null ? content.Length : 0; IWebRequestAsync request = await Program.DavClient.GetFileWriteRequestAsync(remoteStorageUri, null, contentLength, 0, -1, lockToken, eTag); @@ -115,15 +110,20 @@ protected async Task CreateOrUpdateFileAsync(Uri remoteStorageUri, IFile /// Lock info that conains lock-token returned by the remote storage. /// /// Lock your item in the remote storage in this method and receive the lock-token. - /// Return a new object with the being set from this function. - /// The will become available via the property when the + /// Return a new object with the being set from this function. + /// The will become available via methods parameter when the /// item in the remote storage should be updated. Supply the lock-token during the update request in /// and method calls. /// - public async Task LockAsync() + public async Task LockAsync() { - ITHit.WebDAV.Client.LockInfo lockInfo = await Program.DavClient.LockAsync(new Uri(RemoteStorageUri), LockScope.Exclusive, false, null, TimeSpan.MaxValue); - return new LockInfo { LockToken = lockInfo.LockToken.LockToken }; + LockInfo lockInfo = await Program.DavClient.LockAsync(new Uri(RemoteStorageUri), LockScope.Exclusive, false, null, TimeSpan.MaxValue); + return new ServerLockInfo { + LockToken = lockInfo.LockToken.LockToken, + Exclusive = lockInfo.LockScope == LockScope.Exclusive, + Owner = lockInfo.Owner, + LockExpirationDateUtc = DateTimeOffset.Now.Add(lockInfo.TimeOut) + }; } /// @@ -136,7 +136,14 @@ public async Task LockAsync() /// public async Task UnlockAsync(string lockToken) { - await Program.DavClient.UnlockAsync(new Uri(RemoteStorageUri), lockToken); + try + { + await Program.DavClient.UnlockAsync(new Uri(RemoteStorageUri), lockToken); + } + catch(ITHit.WebDAV.Client.Exceptions.ConflictException) + { + // The item is already unlocked. + } } } } diff --git a/WebDAVDrive/UserFolder.cs b/WebDAVDrive/UserFolder.cs index 8c21c3f..6b29afb 100644 --- a/WebDAVDrive/UserFolder.cs +++ b/WebDAVDrive/UserFolder.cs @@ -1,27 +1,29 @@ -using ITHit.FileSystem; -using ITHit.WebDAV.Client; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace VirtualFileSystem +using ITHit.FileSystem; +using ITHit.FileSystem.Samples.Common; +using ITHit.WebDAV.Client; + +namespace WebDAVDrive { /// /// Represents a folder in the remote storage. Provides methods for enumerating this folder children, /// creating files and folders and updating this folder information (creatin date, modification date, attributes, etc.). /// /// You will change methods of this class to read/write data from/to your remote storage. - internal class UserFolder : UserFileSystemItem + internal class UserFolder : UserFileSystemItem, IUserFolder { /// /// Creates instance of this class. /// /// Path of this folder in the user file system. /// Information about file lock. Pass null if the item is not locked. - public UserFolder(string userfileSystemFolderPath, LockInfo lockInfo = null) : base(userfileSystemFolderPath, lockInfo) + public UserFolder(string userfileSystemFolderPath) : base(userfileSystemFolderPath) { } @@ -45,7 +47,7 @@ public async Task> EnumerateChildrenAsync(s { remoteStorageChildren = await Program.DavClient.GetChildrenAsync(new Uri(RemoteStorageUri), false); } - catch (ITHit.WebDAV.Client.Exceptions.Redirect302Exception) + catch (ITHit.WebDAV.Client.Exceptions.Redirect302Exception) { remoteStorageChildren = await Program.DavClient.GetChildrenAsync(new Uri(RemoteStorageUri), false); } @@ -53,11 +55,10 @@ public async Task> EnumerateChildrenAsync(s List userFileSystemChildren = new List(); foreach (IHierarchyItemAsync remoteStorageItem in remoteStorageChildren) { - FileSystemItemBasicInfo itemInfo = Mapping.GetUserFileSysteItemBasicInfo(remoteStorageItem); + FileSystemItemBasicInfo itemInfo = Mapping.GetUserFileSystemItemBasicInfo(remoteStorageItem); userFileSystemChildren.Add(itemInfo); } - return userFileSystemChildren; } @@ -80,7 +81,7 @@ public async Task CreateFileAsync(IFileBasicInfo fileInfo, Stream conten /// New ETag returned from the remote storage. public async Task CreateFolderAsync(IFolderBasicInfo folderInfo) { - Uri newFolderUri = new Uri(new Uri(RemoteStorageUri), folderInfo.Name); + Uri newFolderUri = new Uri(new Uri(RemoteStorageUri), folderInfo.Name); await Program.DavClient.CreateFolderAsync(newFolderUri); return null; // This implementation does not support ETags on folders. } @@ -89,8 +90,9 @@ public async Task CreateFolderAsync(IFolderBasicInfo folderInfo) /// Updates folder in the remote storage. /// /// New folder information. + /// Information about the lock. Caller passes null if the item is not locked. /// New ETag returned from the remote storage. - public async Task UpdateAsync(IFolderBasicInfo folderInfo) + public async Task UpdateAsync(IFolderBasicInfo folderInfo, ServerLockInfo lockInfo = null) { return null; } diff --git a/WebDAVDrive/VirtualDrive.cs b/WebDAVDrive/VirtualDrive.cs new file mode 100644 index 0000000..2a20280 --- /dev/null +++ b/WebDAVDrive/VirtualDrive.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using log4net; + +using ITHit.FileSystem.Samples.Common; +using System.IO; + +namespace WebDAVDrive +{ + /// + internal class VirtualDrive : VirtualDriveBase + { + /// + public VirtualDrive(string license, string userFileSystemRootPath, ILog log, double syncIntervalMs) + : base(license, userFileSystemRootPath, log, syncIntervalMs) + { + + } + + /// + public override async Task GetUserFileSystemItemAsync(string userFileSystemPath) + { + if (File.Exists(userFileSystemPath)) + { + return new UserFile(userFileSystemPath); + } + if (Directory.Exists(userFileSystemPath)) + { + return new UserFolder(userFileSystemPath); + } + + // When a file handle is being closed during delete, the file does not exist, return null. + return null; + } + } +} diff --git a/WebDAVDrive/WebDAVDrive.csproj b/WebDAVDrive/WebDAVDrive.csproj index 00835d9..c855a02 100644 --- a/WebDAVDrive/WebDAVDrive.csproj +++ b/WebDAVDrive/WebDAVDrive.csproj @@ -13,39 +13,48 @@ false + + + - - - - - + + + + - - - - + + Always + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/WebDAVDrive/appsettings.json b/WebDAVDrive/appsettings.json index 9313ab3..d9eaff7 100644 --- a/WebDAVDrive/appsettings.json +++ b/WebDAVDrive/appsettings.json @@ -1,4 +1,9 @@ { + // Unique ID of this application. + // If you must to run more than one instance of this application side-by-side on same client machine + // (aka Corporate Drive and Personal Drive) set 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 // automatically via internet and will function for 5 days. The Engine will stop working after that. // To enable a 1-month trial period, download a trial license here: https://userfilesystem.com/download/