/// <summary> /// Called when a file content changed or file/folder attributes changed in the remote storage. /// </summary> /// <remarks> /// In this method we update corresponding file/folder information in user file system. /// We also dehydrate the file if it is not blocked. /// </remarks> private async void ChangedAsync(object sender, FileSystemEventArgs e) { LogMessage(e.ChangeType.ToString(), e.FullPath); string remoteStoragePath = e.FullPath; try { // We do not want to sync MS Office temp files, etc. from remote storage. if (!FsPath.AvoidSync(remoteStoragePath)) { string userFileSystemPath = Mapping.ReverseMapPath(remoteStoragePath); // Because of the on-demand population the file or folder placeholder may not exist in the user file system. if (FsPath.Exists(userFileSystemPath)) { FileSystemInfo remoteStorageItem = FsPath.GetFileSystemItem(remoteStoragePath); // This check is only required because we can not prevent circular calls because of the simplicity of this example. // In your real-life application you will not sent updates from server back to client that issued the update. FileSystemItemMetadataExt itemInfo = Mapping.GetUserFileSysteItemMetadata(remoteStorageItem); if (!await virtualDrive.GetETagManager(userFileSystemPath, this).ETagEqualsAsync(itemInfo)) { await virtualDrive.ServerNotifications(userFileSystemPath, this).UpdateAsync(itemInfo); LogMessage("Updated succesefully", userFileSystemPath); } } } } catch (Exception ex) { LogError($"{e.ChangeType} failed", remoteStoragePath, null, ex); } }
/// <summary> /// Gets list of files and folders in this folder. /// </summary> /// <param name="pattern">Search pattern.</param> /// <returns> /// List of files and folders located in this folder in the remote /// storage that correstonds with the provided search pattern. /// </returns> public async Task <IEnumerable <FileSystemItemMetadataExt> > EnumerateChildrenAsync(string pattern) { // This method has a 60 sec timeout. // To process longer requests modify the IFolder.GetChildrenAsync() implementation. IHierarchyItemAsync[] remoteStorageChildren = null; // Retry the request in case the log-in dialog is shown. try { remoteStorageChildren = await Program.DavClient.GetChildrenAsync(new Uri(RemoteStorageUri), false); } catch (ITHit.WebDAV.Client.Exceptions.Redirect302Exception) { remoteStorageChildren = await Program.DavClient.GetChildrenAsync(new Uri(RemoteStorageUri), false); } List <FileSystemItemMetadataExt> userFileSystemChildren = new List <FileSystemItemMetadataExt>(); foreach (IHierarchyItemAsync remoteStorageItem in remoteStorageChildren) { FileSystemItemMetadataExt itemInfo = Mapping.GetUserFileSystemItemMetadata(remoteStorageItem); userFileSystemChildren.Add(itemInfo); } return(userFileSystemChildren); }
/// <summary> /// Called when a file or folder is created in the remote storage. /// </summary> /// <remarks>In this method we create a new file/folder in user file system.</remarks> private async void CreatedAsync(object sender, FileSystemEventArgs e) { LogMessage(e.ChangeType.ToString(), e.FullPath); string remoteStoragePath = e.FullPath; try { // We do not want to sync MS Office temp files from remote storage. if (!FsPath.AvoidSync(remoteStoragePath)) { string userFileSystemPath = Mapping.ReverseMapPath(remoteStoragePath); string userFileSystemParentPath = Path.GetDirectoryName(userFileSystemPath); // Because of the on-demand population the file or folder placeholder may not exist in the user file system // or the folder may be offline. FileSystemInfo remoteStorageItem = FsPath.GetFileSystemItem(remoteStoragePath); FileSystemItemMetadataExt newItemInfo = Mapping.GetUserFileSysteItemMetadata(remoteStorageItem); if (await virtualDrive.ServerNotifications(userFileSystemParentPath, this).CreateAsync(new[] { newItemInfo }) > 0) { LogMessage($"Created succesefully", userFileSystemPath); } } } catch (Exception ex) { LogError($"{e.ChangeType} failed", remoteStoragePath, null, ex); } }
/// <summary> /// Creates or updates file in the remote storage. /// </summary> /// <param name="remoteStoragePath">Path of the file to be created or updated in the remote storage.</param> /// <param name="newInfo">New information about the file, such as modification date, attributes, custom data, etc.</param> /// <param name="mode">Specifies if a new file should be created or existing file should be updated.</param> /// <param name="newContentStream">New file content or null if the file content is not modified.</param> /// <param name="eTagOld">The ETag to be sent to the remote storage as part of the update request to make sure the content is not overwritten.</param> /// <param name="lockInfo">Information about the lock. Null if the item is not locked.</param> /// <returns>The new ETag returned from the remote storage.</returns> protected async Task <string> CreateOrUpdateFileAsync( string remoteStoragePath, IFileMetadata newInfo, FileMode mode, Stream newContentStream = null, string eTagOld = null, ServerLockInfo lockInfo = null) { FileInfo remoteStorageItem = new FileInfo(remoteStoragePath); try { Program.VirtualDrive.RemoteStorageMonitor.Enabled = false; // Disable RemoteStorageMonitor to avoid circular calls. // If another thread is trying to sync the same file, this call will fail in other threads. // In your implementation you must lock your remote storage file, or block it for reading and writing by other means. await using (FileStream remoteStorageStream = remoteStorageItem.Open(mode, FileAccess.Write, FileShare.None)) { string userFileSystemPath = Mapping.ReverseMapPath(remoteStoragePath); if (mode == FileMode.Open) { // Verify that the item in the remote storage is not modified since it was downloaded to the user file system. // In your real-life application you will send the ETag to the server as part of the update request. FileSystemItemMetadataExt itemInfo = Mapping.GetUserFileSysteItemMetadata(remoteStorageItem); if (!(await VirtualDrive.GetETagManager(userFileSystemPath).ETagEqualsAsync(itemInfo))) { throw new ConflictException(Modified.Server, "Item is modified in the remote storage, ETags not equal."); } } // Update ETag/LastWriteTime in user file system, so the synchronyzation or remote storage monitor would not start the new update. // This is only required to avoid circular updates because of the simplicity of this sample. // In your real-life application you will receive a new ETag from the server in the update response // and return it from this method. string eTagNew = newInfo.LastWriteTime.ToUniversalTime().ToString("o"); await VirtualDrive.GetETagManager(userFileSystemPath).SetETagAsync(eTagNew); // Update remote storage file content. if (newContentStream != null) { await newContentStream.CopyToAsync(remoteStorageStream); remoteStorageStream.SetLength(newContentStream.Length); } // Update remote storage file basic info. WindowsFileSystemItem.SetFileInformation( remoteStorageStream.SafeFileHandle, newInfo.Attributes, newInfo.CreationTime, newInfo.LastWriteTime, newInfo.LastAccessTime, newInfo.LastWriteTime); return(eTagNew); } } finally { Program.VirtualDrive.RemoteStorageMonitor.Enabled = true; } }
/// <summary> /// Creates new file and folder placeholders in the user file system in this folder. /// </summary> /// <param name="newItemsInfo">Array of new files and folders.</param> /// <returns>Number of items created.</returns> public async Task <uint> CreateAsync(IFileSystemItemMetadata[] newItemsInfo) { string userFileSystemParentPath = userFileSystemPath; try { // Because of the on-demand population the file or folder placeholder may not exist in the user file system. // Here we also check that the folder content was loaded into user file system (the folder is not offline). if (Directory.Exists(userFileSystemParentPath) && !new DirectoryInfo(userFileSystemParentPath).Attributes.HasFlag(System.IO.FileAttributes.Offline)) { // Here we save current user file system path inside file/folder. // If the file/folder is moved or renamed while this app is not running, // we extract the saved path when this app starts and sync the file/folder to the remote storage. foreach (var newItemInfo in newItemsInfo) { string userFileSystemPath = Path.Combine(userFileSystemParentPath, newItemInfo.Name); newItemInfo.CustomData = new CustomData { OriginalPath = userFileSystemPath }.Serialize(); } // Create placeholders. uint created = await virtualDrive.Engine.ServerNotifications(userFileSystemParentPath).CreateAsync(newItemsInfo); // Create ETags. foreach (IFileSystemItemMetadata newItemInfo in newItemsInfo) { FileSystemItemMetadataExt newItemInfoExt = newItemInfo as FileSystemItemMetadataExt ?? throw new NotImplementedException($"{nameof(FileSystemItemMetadataExt)}"); string userFileSystemNewItemPath = Path.Combine(userFileSystemParentPath, newItemInfoExt.Name); await virtualDrive.GetETagManager(userFileSystemNewItemPath).SetETagAsync(newItemInfoExt.ETag); // Set the read-only attribute and all custom columns data. UserFileSystemRawItem newUserFileSystemRawItem = virtualDrive.GetUserFileSystemRawItem(userFileSystemNewItemPath, logger); await newUserFileSystemRawItem.SetLockedByAnotherUserAsync(newItemInfoExt.LockedByAnotherUser); await newUserFileSystemRawItem.SetCustomColumnsDataAsync(newItemInfoExt.CustomProperties); } return(created); } } catch (ExistsException ex) { // "Cannot create a file when that file already exists." //await new UserFileSystemItem(userFileSystemParentPath).ShowDownloadErrorAsync(ex); // Rethrow the exception preserving stack trace of the original exception. System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex).Throw(); } return(0); }
/// <summary> /// Returns true if the remote storage ETag and user file system ETags are equal. False - otherwise. /// </summary> /// <param name="remoteStorageItem">Remote storage item info.</param> /// <remarks> /// ETag is updated on the server during every document update and is sent to client with a file. /// During user file system to remote storage update it is sent back to the remote storage together with a modified content. /// This ensures the changes in the remote storage are not overwritten if the document on the server is modified. /// </remarks> public async Task <bool> ETagEqualsAsync(FileSystemItemMetadataExt remoteStorageItem) { string remoteStorageETag = remoteStorageItem.ETag; string userFileSystemETag = await GetETagAsync(); if (string.IsNullOrEmpty(remoteStorageETag) && string.IsNullOrEmpty(userFileSystemETag)) { // We assume the remote storage is not using ETags or no ETag is ssociated with this file/folder. return(true); } return(remoteStorageETag == userFileSystemETag); }
/// <summary> /// Gets list of files and folders in this folder in the remote storage. /// </summary> /// <param name="pattern">Search pattern.</param> /// <returns> /// List of files and folders located in this folder in the remote /// storage that correstonds with the provided search pattern. /// </returns> public async Task <IEnumerable <FileSystemItemMetadataExt> > EnumerateChildrenAsync(string pattern) { // This method has a 60 sec timeout. // To process longer requests modify the IFolder.GetChildrenAsync() implementation. IEnumerable <FileSystemInfo> remoteStorageChildren = new DirectoryInfo(RemoteStoragePath).EnumerateFileSystemInfos(pattern); List <FileSystemItemMetadataExt> userFileSystemChildren = new List <FileSystemItemMetadataExt>(); foreach (FileSystemInfo remoteStorageItem in remoteStorageChildren) { FileSystemItemMetadataExt itemInfo = Mapping.GetUserFileSysteItemMetadata(remoteStorageItem); userFileSystemChildren.Add(itemInfo); } return(userFileSystemChildren); }
/// <summary> /// Creates or updates folder in the remote storage. /// </summary> /// <param name="remoteStoragePath">Path of the folder to be created or updated in the remote storage.</param> /// <param name="newInfo">New information about the folder, such as modification date, attributes, custom data, etc.</param> /// <param name="mode">Specifies if a new folder should be created or existing folder should be updated.</param> /// <param name="eTagOld">The ETag to be sent to the remote storage as part of the update request to make sure the content is not overwritten.</param> /// <param name="lockInfo">Information about the lock. Null if the item is not locked.</param> /// <returns>The new ETag returned from the remote storage.</returns> protected async Task <string> CreateOrUpdateFolderAsync( string remoteStoragePath, IFolderMetadata newInfo, FileMode mode, string eTagOld = null, ServerLockInfo lockInfo = null) { DirectoryInfo remoteStorageItem = new DirectoryInfo(remoteStoragePath); try { Program.VirtualDrive.RemoteStorageMonitor.Enabled = false; // Disable RemoteStorageMonitor to avoid circular calls. remoteStorageItem.Create(); string userFileSystemPath = Mapping.ReverseMapPath(remoteStoragePath); if (mode == FileMode.Open) { // Verify that the item in the remote storage is not modified since it was downloaded to the user file system. // In your real-life application you will send the ETag to the server as part of the update request. FileSystemItemMetadataExt itemInfo = Mapping.GetUserFileSysteItemMetadata(remoteStorageItem); if (!(await VirtualDrive.GetETagManager(userFileSystemPath).ETagEqualsAsync(itemInfo))) { throw new ConflictException(Modified.Server, "Item is modified in the remote storage, ETags not equal."); } } // Update ETag/LastWriteTime in user file system, so the synchronyzation or remote storage monitor would not start the new update. // This is only required to avoid circular updates because of the simplicity of this sample. // In your real-life application you will receive a new ETag from server in the update response. string eTagNew = newInfo.LastWriteTime.ToUniversalTime().ToString("o"); await VirtualDrive.GetETagManager(userFileSystemPath).SetETagAsync(eTagNew); remoteStorageItem.Attributes = newInfo.Attributes; remoteStorageItem.CreationTimeUtc = newInfo.CreationTime.UtcDateTime; remoteStorageItem.LastWriteTimeUtc = newInfo.LastWriteTime.UtcDateTime; remoteStorageItem.LastAccessTimeUtc = newInfo.LastAccessTime.UtcDateTime; remoteStorageItem.LastWriteTimeUtc = newInfo.LastWriteTime.UtcDateTime; return(eTagNew); } finally { Program.VirtualDrive.RemoteStorageMonitor.Enabled = true; } }
/// <summary> /// Updates information about the file or folder placeholder in the user file system. /// This method automatically hydrates and dehydrate files. /// </summary> /// <remarks>This method failes if the file or folder in user file system is modified (not in-sync with the remote storage).</remarks> /// <param name="itemInfo">New file or folder info.</param> /// <returns>True if the file was updated. False - otherwise.</returns> public async Task <bool> UpdateAsync(IFileSystemItemMetadata itemInfo) { FileSystemItemMetadataExt itemInfoExt = itemInfo as FileSystemItemMetadataExt ?? throw new NotImplementedException($"{nameof(FileSystemItemMetadataExt)}"); try { // Because of the on-demand population the file or folder placeholder may not exist in the user file system. if (FsPath.Exists(userFileSystemPath)) { PlaceholderItem placeholderItem = PlaceholderItem.GetItem(userFileSystemPath); // To be able to update the item we need to remove the read-only attribute. if ((FsPath.GetFileSystemItem(userFileSystemPath).Attributes | System.IO.FileAttributes.ReadOnly) != 0) { FsPath.GetFileSystemItem(userFileSystemPath).Attributes &= ~System.IO.FileAttributes.ReadOnly; } // Dehydrate/hydrate the file, update file size, custom data, creation date, modification date, attributes. await virtualDrive.Engine.ServerNotifications(userFileSystemPath).UpdateAsync(itemInfoExt); // Set ETag. await eTagManager.SetETagAsync(itemInfoExt.ETag); // Clear icon. //await ClearStateAsync(); // Set the read-only attribute and all custom columns data. await SetLockedByAnotherUserAsync(itemInfoExt.LockedByAnotherUser); await SetCustomColumnsDataAsync(itemInfoExt.CustomProperties); return(true); } } catch (Exception ex) { await SetDownloadErrorStateAsync(ex); // Rethrow the exception preserving stack trace of the original exception. System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex).Throw(); } return(false); }
/// <summary> /// Recursively synchronizes all files and folders from server to client. /// Synchronizes only folders already loaded into the user file system. /// </summary> /// <param name="userFileSystemFolderPath">Folder path in user file system.</param> internal async Task SyncronizeFolderAsync(string userFileSystemFolderPath) { // In case of on-demand loading the user file system contains only a subset of the server files and folders. // Here we sync folder only if its content already loaded into user file system (folder is not offline). // The folder content is loaded inside IFolder.GetChildrenAsync() method. if (new DirectoryInfo(userFileSystemFolderPath).Attributes.HasFlag(System.IO.FileAttributes.Offline)) { // LogMessage("Folder offline, skipping:", userFileSystemFolderPath); return; } IEnumerable <string> userFileSystemChildren = Directory.EnumerateFileSystemEntries(userFileSystemFolderPath, "*"); //LogMessage("Synchronizing:", userFileSystemFolderPath); IVirtualFolder userFolder = await virtualDrive.GetItemAsync <IVirtualFolder>(userFileSystemFolderPath, this); IEnumerable <FileSystemItemMetadataExt> remoteStorageChildrenItems = await userFolder.EnumerateChildrenAsync("*"); // Create new files/folders in the user file system. foreach (FileSystemItemMetadataExt remoteStorageItem in remoteStorageChildrenItems) { string userFileSystemPath = Path.Combine(userFileSystemFolderPath, remoteStorageItem.Name); try { // 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. if (!FsPath.AvoidSync(remoteStorageItem.Name) && !FsPath.AvoidSync(userFileSystemPath)) { if (!FsPath.Exists(userFileSystemPath)) { LogMessage($"Creating", userFileSystemPath); await virtualDrive.GetUserFileSystemRawItem(userFileSystemFolderPath, this).CreateAsync(new[] { remoteStorageItem }); LogMessage($"Created succesefully", userFileSystemPath); } } } catch (Exception ex) { LogError("Creation failed", userFileSystemPath, null, ex); } } // Update files/folders in user file system and sync subfolders. userFileSystemChildren = Directory.EnumerateFileSystemEntries(userFileSystemFolderPath, "*"); foreach (string userFileSystemPath in userFileSystemChildren) { try { string itemName = Path.GetFileName(userFileSystemPath); FileSystemItemMetadataExt remoteStorageItem = remoteStorageChildrenItems.FirstOrDefault(x => x.Name.Equals(itemName, StringComparison.InvariantCultureIgnoreCase)); if (!FsPath.AvoidSync(userFileSystemPath)) { UserFileSystemRawItem userFileSystemRawItem = virtualDrive.GetUserFileSystemRawItem(userFileSystemPath, this); if (remoteStorageItem == null) { if (PlaceholderItem.GetItem(userFileSystemPath).GetInSync() //&& !PlaceholderItem.GetItem(userFileSystemPath).IsNew(virtualDrive) // Extra check to protect from deletion files that were deleted becuse of incorrect folder merging operation. ) { // Delete the file/folder in user file system. LogMessage("Deleting item", userFileSystemPath); await userFileSystemRawItem.DeleteAsync(); LogMessage("Deleted succesefully", userFileSystemPath); } } else { if (PlaceholderItem.GetItem(userFileSystemPath).GetInSync() && !await virtualDrive.GetETagManager(userFileSystemPath).ETagEqualsAsync(remoteStorageItem)) { // User file system <- remote storage update. LogMessage("Remote item modified", userFileSystemPath); await userFileSystemRawItem.UpdateAsync(remoteStorageItem); LogMessage("Updated succesefully", userFileSystemPath); } // Set the read-only attribute and all custom columns data. if (PlaceholderItem.GetItem(userFileSystemPath).GetInSync()) { await userFileSystemRawItem.SetLockedByAnotherUserAsync(remoteStorageItem.LockedByAnotherUser); await userFileSystemRawItem.SetCustomColumnsDataAsync(remoteStorageItem.CustomProperties); } // Hydrate / dehydrate the file. if (userFileSystemRawItem.HydrationRequired()) { LogMessage("Hydrating", userFileSystemPath); try { new PlaceholderFile(userFileSystemPath).Hydrate(0, -1); LogMessage("Hydrated succesefully", userFileSystemPath); } catch (FileLoadException) { // Typically this happens if another thread already hydrating the file. LogMessage("Failed to hydrate. The file is blocked.", userFileSystemPath); } } else if (userFileSystemRawItem.DehydrationRequired()) { LogMessage("Dehydrating", userFileSystemPath); new PlaceholderFile(userFileSystemPath).Dehydrate(0, -1); LogMessage("Dehydrated succesefully", userFileSystemPath); } } } } catch (Exception ex) { LogError("Update failed", userFileSystemPath, null, ex); } // Synchronize subfolders. try { if (Directory.Exists(userFileSystemPath)) { await SyncronizeFolderAsync(userFileSystemPath); } } catch (Exception ex) { LogError("Folder sync failed:", userFileSystemPath, null, ex); } } }
/// <inheritdoc/> public async Task GetChildrenAsync(string pattern, IOperationContext operationContext, IFolderListingResultContext resultContext) { // This method has a 60 sec timeout. // To process longer requests and reset the timout timer call one of the following: // - resultContext.ReturnChildren() method. // - resultContext.ReportProgress() method. Logger.LogMessage($"{nameof(IFolder)}.{nameof(GetChildrenAsync)}({pattern})", UserFileSystemPath); IHierarchyItemAsync[] remoteStorageChildren = null; // Retry the request in case the log-in dialog is shown. try { remoteStorageChildren = await Program.DavClient.GetChildrenAsync(new Uri(RemoteStoragePath), false); } catch (ITHit.WebDAV.Client.Exceptions.Redirect302Exception) { remoteStorageChildren = await Program.DavClient.GetChildrenAsync(new Uri(RemoteStoragePath), false); } List <FileSystemItemMetadataExt> userFileSystemChildren = new List <FileSystemItemMetadataExt>(); foreach (IHierarchyItemAsync remoteStorageItem in remoteStorageChildren) { FileSystemItemMetadataExt itemInfo = Mapping.GetUserFileSystemItemMetadata(remoteStorageItem); string userFileSystemItemPath = Path.Combine(UserFileSystemPath, itemInfo.Name); // Filtering existing files/folders. This is only required to avoid extra errors in the log. if (!FsPath.Exists(userFileSystemItemPath)) { Logger.LogMessage("Creating", userFileSystemItemPath); userFileSystemChildren.Add(itemInfo); } ExternalDataManager customDataManager = Engine.CustomDataManager(userFileSystemItemPath); // Mark this item as not new, which is required for correct MS Office saving opertions. customDataManager.IsNew = false; } // To signal that the children enumeration is completed // always call ReturnChildren(), even if the folder is empty. resultContext.ReturnChildren(userFileSystemChildren.ToArray(), userFileSystemChildren.Count()); // Save ETags, the read-only attribute and all custom columns data. foreach (FileSystemItemMetadataExt child in userFileSystemChildren) { string userFileSystemItemPath = Path.Combine(UserFileSystemPath, child.Name); ExternalDataManager customDataManager = Engine.CustomDataManager(userFileSystemItemPath); // Save ETag on the client side, to be sent to the remote storage as part of the update. // Setting ETag also marks an item as not new. // 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. await customDataManager.ETagManager.SetETagAsync(child.ETag); // Set the read-only attribute and all custom columns data. await customDataManager.SetLockedByAnotherUserAsync(child.LockedByAnotherUser); await customDataManager.SetCustomColumnsAsync(child.CustomProperties); } }