diff --git a/PInvoke/CldApi/cfapi.Funcs.cs b/PInvoke/CldApi/cfapi.Funcs.cs index 5d22fa54..4ea6b1ff 100644 --- a/PInvoke/CldApi/cfapi.Funcs.cs +++ b/PInvoke/CldApi/cfapi.Funcs.cs @@ -551,7 +551,7 @@ namespace Vanara.PInvoke // FileHandle, LARGE_INTEGER StartingOffset, LARGE_INTEGER Length, CF_HYDRATE_FLAGS HydrateFlags, LPOVERLAPPED Overlapped ); [DllImport(Lib.CldApi, SetLastError = false, ExactSpelling = true)] [PInvokeData("cfapi.h", MSDNShortId = "4FFD7580-BF59-48D0-B6D7-516559914096")] - public static extern HRESULT CfHydratePlaceholder(HFILE FileHandle, long StartingOffset, long Length, CF_HYDRATE_FLAGS HydrateFlags, [In, Out, Optional] IntPtr Overlapped); + public static extern HRESULT CfHydratePlaceholder(HFILE FileHandle, long StartingOffset = 0, long Length = -1, CF_HYDRATE_FLAGS HydrateFlags = 0, [In, Out] IntPtr Overlapped = default); /// /// Hydrates a placeholder file by ensuring that the specified byte range is present on-disk in the placeholder. This is valid for diff --git a/PInvoke/CldApi/cfapi.cs b/PInvoke/CldApi/cfapi.cs index 524fc1d4..620912d5 100644 --- a/PInvoke/CldApi/cfapi.cs +++ b/PInvoke/CldApi/cfapi.cs @@ -1348,7 +1348,7 @@ namespace Vanara.PInvoke /// public uint ParamSize; - private uint pad; + private readonly uint pad; /// [MarshalAs(UnmanagedType.ByValArray, SizeConst = 56)] @@ -1395,8 +1395,8 @@ namespace Vanara.PInvoke /// The requested structure. public T GetParam() where T : struct { - using var ptr = new SafeHGlobalHandle(Content); - return ptr.ToStructure(); + using var ptr = new PinnedObject(Content); + return ((IntPtr)ptr).ToStructure(); } /// Contains callback specific parameters such as file offset, length, flags, etc. @@ -1607,7 +1607,7 @@ namespace Vanara.PInvoke [StructLayout(LayoutKind.Sequential)] public struct CF_CONNECTION_KEY { - private long Internal; + private readonly long Internal; } /// Specifies a range of data in a placeholder file. @@ -1730,12 +1730,12 @@ namespace Vanara.PInvoke public uint ParamSize; // Yes, this is strange, but needed to deal with struct size changing based on pointer size (40/48). - private uint pad4_8; - private ulong pad8_16; - private ulong pad16_24; - private ulong pad24_32; - private IntPtr padp1; - private IntPtr padp2; + private readonly uint pad4_8; + private readonly ulong pad8_16; + private readonly ulong pad16_24; + private readonly ulong pad24_32; + private readonly IntPtr padp1; + private readonly IntPtr padp2; /// public TRANSFERDATA TransferData { get => GetParam(); set => SetParam(value); } @@ -1999,6 +1999,27 @@ namespace Vanara.PInvoke /// The final USN value after create actions are performed. public int CreateUsn; + + /// + /// Initializes a new instance of the struct with info from a file. + /// + /// The file information. + public CF_PLACEHOLDER_CREATE_INFO(System.IO.FileInfo fileInfo) : this() + { + RelativeFileName = fileInfo.FullName; + FsMetadata = new CF_FS_METADATA + { + FileSize = fileInfo.Length, + BasicInfo = new FILE_BASIC_INFO + { + FileAttributes = (FileFlagsAndAttributes)fileInfo.Attributes, + CreationTime = fileInfo.CreationTime.ToFileTimeStruct(), + LastWriteTime = fileInfo.LastWriteTime.ToFileTimeStruct(), + LastAccessTime = fileInfo.LastAccessTime.ToFileTimeStruct(), + ChangeTime = fileInfo.LastWriteTime.ToFileTimeStruct() + } + }; + } } /// Standard placeholder information. @@ -2130,7 +2151,7 @@ namespace Vanara.PInvoke [StructLayout(LayoutKind.Sequential)] public struct CF_REQUEST_KEY { - private long Internal; + private readonly long Internal; } /// Defines the sync policies used by a sync root. @@ -2323,7 +2344,7 @@ namespace Vanara.PInvoke [StructLayout(LayoutKind.Sequential)] public struct CF_TRANSFER_KEY { - private long Internal; + private readonly long Internal; } } } \ No newline at end of file diff --git a/UnitTests/PInvoke/CldApi/CldApiTests.cs b/UnitTests/PInvoke/CldApi/CldApiTests.cs index 9ed04b71..5b5b4138 100644 --- a/UnitTests/PInvoke/CldApi/CldApiTests.cs +++ b/UnitTests/PInvoke/CldApi/CldApiTests.cs @@ -35,17 +35,11 @@ namespace Vanara.PInvoke.Tests var destDirPath = SetupTempDir(dest); try { - using var csp = new CloudSyncProvider(destDirPath, "TestSync"); - csp.Status = CF_SYNC_PROVIDER_STATUS.CF_PROVIDER_STATUS_IDLE; + using var csp = new CloudSyncProvider(destDirPath, "TestSync") { Status = CF_SYNC_PROVIDER_STATUS.CF_PROVIDER_STATUS_IDLE }; csp.Status.WriteValues(); const string desc = "SyncStatus is good."; - uint descLen = (uint)(desc.Length + 1) * 2; - var ss = new CF_SYNC_STATUS { StructSize = (uint)Marshal.SizeOf() + descLen, Code = 1, DescriptionLength = descLen }; - var mem = new SafeHGlobalHandle(Marshal.SizeOf() + descLen); - mem.Write(ss); - StringHelper.Write(desc, ((IntPtr)mem).Offset(Marshal.SizeOf()), out _, true, CharSet.Unicode, descLen); - Assert.That(CfReportSyncStatus(destDirPath, mem), ResultIs.Successful); + Assert.That(() => csp.ReportSyncStatus(desc, 1), Throws.Nothing); } finally { @@ -64,6 +58,7 @@ namespace Vanara.PInvoke.Tests { var tokSrc = new CancellationTokenSource(); var token = tokSrc.Token; + var evt = new ManualResetEventSlim(); var task = Task.Run(() => { using var csp = new CloudSyncProvider(destDirPath, "TestSync"); @@ -88,8 +83,8 @@ namespace Vanara.PInvoke.Tests Assert.That(new FileInfo(fpath).Length, Is.EqualTo(origFileInfo.Length)); Debug.WriteLine("CSP is running...................................\n"); - - while (!token.IsCancellationRequested) { Thread.Sleep(100); } + evt.Set(); + while (!token.IsCancellationRequested) { Task.Delay(100); } static void ShowInfo(object s, CloudSyncCallbackArgs e) where T : struct { @@ -124,16 +119,17 @@ namespace Vanara.PInvoke.Tests } }, tokSrc.Token); - Thread.Sleep(5000); // Let CSP get loaded + evt.Wait(); // Let CSP get loaded using var hFile = GetHFILE(fpath); - Assert.That(CfHydratePlaceholder(hFile, 0, -1, 0), ResultIs.Successful); + Assert.That(hFile, ResultIs.ValidHandle); + Assert.That(CfHydratePlaceholder(hFile), ResultIs.Successful); Assert.That(CfGetCorrelationVector(hFile, out var cv), ResultIs.Successful); using var buf = new SafeHGlobalHandle(128); Assert.That(CfGetPlaceholderRangeInfo(hFile, CF_PLACEHOLDER_RANGE_INFO_CLASS.CF_PLACEHOLDER_RANGE_INFO_ONDISK, 0, buf.Size, buf, buf.Size, out var rngLen), ResultIs.Successful); Assert.That(CfGetTransferKey(hFile, out var txKey), ResultIs.Successful); Assert.That(() => CfReleaseTransferKey(hFile, txKey), Throws.Nothing); Assert.That(CfDehydratePlaceholder(hFile, 0, -1, 0), ResultIs.Successful); - Thread.Sleep(2000); // Wait for user interaction + //Thread.Sleep(2000); // Wait for user interaction tokSrc.Cancel(); task.Wait(); @@ -273,11 +269,9 @@ namespace Vanara.PInvoke.Tests [Test] public void CfGetSyncRootInfoByPathTest() { - using (var mem = SafeHGlobalHandle.CreateFromStructure()) - { - Assert.That(CfGetSyncRootInfoByPath(syncRootPath, CF_SYNC_ROOT_INFO_CLASS.CF_SYNC_ROOT_INFO_BASIC, mem, mem.Size, out var len), ResultIs.Successful); - mem.ToStructure().WriteValues(); - } + using var mem = new SafeCoTaskMemStruct(); + Assert.That(CfGetSyncRootInfoByPath(syncRootPath, CF_SYNC_ROOT_INFO_CLASS.CF_SYNC_ROOT_INFO_BASIC, mem, mem.Size, out var len), ResultIs.Successful); + mem.Value.WriteValues(); Assert.That(() => CfGetSyncRootInfoByPath(syncRootPath).WriteValues(), Throws.Nothing); Assert.That(() => CfGetSyncRootInfoByPath(syncRootPath).WriteValues(), Throws.Nothing); } diff --git a/UnitTests/PInvoke/CldApi/CloudSyncProvider.cs b/UnitTests/PInvoke/CldApi/CloudSyncProvider.cs index a3f1a6cd..417bc6b6 100644 --- a/UnitTests/PInvoke/CldApi/CloudSyncProvider.cs +++ b/UnitTests/PInvoke/CldApi/CloudSyncProvider.cs @@ -1,5 +1,4 @@ -using ICSharpCode.Decompiler.IL; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -18,80 +17,79 @@ using static Vanara.PInvoke.SearchApi; namespace Vanara.PInvoke.Tests { + /// Event arguments for a cloud file callback. + /// The type of data handled by the callback. + /// public class CloudSyncCallbackArgs : EventArgs where T : struct { + private readonly CF_CALLBACK_INFO info; + + /// Initializes a new instance of the class. + /// The callback information. + /// The callback parameters. public CloudSyncCallbackArgs(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) { //System.Diagnostics.Debug.WriteLine($"CloudSyncCallbackArgs<{typeof(T).Name}> : {string.Join(" ", CallbackParameters.Content.Select(b => b.ToString("X2")))}"); - ConnectionKey = CallbackInfo.ConnectionKey; - CallbackContext = CallbackInfo.CallbackContext; - VolumeGuidName = CallbackInfo.VolumeGuidName; - VolumeDosName = CallbackInfo.VolumeDosName; - VolumeSerialNumber = CallbackInfo.VolumeSerialNumber; - SyncRootFileId = CallbackInfo.SyncRootFileId; + info = CallbackInfo; SyncRootIdentity = new SafeHGlobalHandle(CallbackInfo.SyncRootIdentity, CallbackInfo.SyncRootIdentityLength, false); - FileId = CallbackInfo.FileId; - FileSize = CallbackInfo.FileSize; FileIdentity = new SafeHGlobalHandle(CallbackInfo.FileIdentity, CallbackInfo.FileIdentityLength, false); - NormalizedPath = CallbackInfo.NormalizedPath; - TransferKey = CallbackInfo.TransferKey; - PriorityHint = CallbackInfo.PriorityHint; - pCorrelationVector = CallbackInfo.CorrelationVector; ProcessInfo = CallbackInfo.ProcessInfo.ToNullableStructure(); - RequestKey = CallbackInfo.RequestKey; try { ParamData = CallbackParameters.GetParam(); } catch { } //catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"{ex.Message}"); } } /// points to an opaque blob that the sync provider provides at the sync root connect time. - public IntPtr CallbackContext { get; } + public IntPtr CallbackContext => info.CallbackContext; /// An opaque handle created by CfConnectSyncRoot for a sync root managed by the sync provider. - public CF_CONNECTION_KEY ConnectionKey { get; } - - /// An optional correlation vector. - public IntPtr pCorrelationVector { get; } + public CF_CONNECTION_KEY ConnectionKey => info.ConnectionKey; /// An optional correlation vector. public CORRELATION_VECTOR? CorrelationVector => pCorrelationVector.ToNullableStructure(); /// A 64 bit file system maintained, volume-wide unique ID of the placeholder file/directory to be serviced. - public long FileId { get; } + public long FileId => info.FileId; /// Points to the opaque blob that the sync provider provides at the placeholder creation/conversion/update time. public SafeAllocatedMemoryHandle FileIdentity { get; } /// The logical size of the placeholder file to be serviced. It is always 0 if the subject of the callback is a directory. - public long FileSize { get; } + public long FileSize => info.FileSize; /// /// The absolute path of the placeholder file/directory to be serviced on the volume identified by VolumeGuidName/VolumeDosName. It /// starts from the root directory of the volume. See the Remarks section for more details. /// - public string NormalizedPath { get; } + public string NormalizedPath => info.NormalizedPath; + + /// The type of operation performed. + public CF_OPERATION_TYPE OperationType { get; internal set; } + + /// Parameters of an operation on a placeholder file or folder. + public CF_OPERATION_PARAMETERS? OpParam { get; internal set; } /// Contains callback specific parameters for this action. public T ParamData { get; } - /// Parameters of an operation on a placeholder file or folder. - public CF_OPERATION_PARAMETERS? OpParam { get; set; } + /// An optional correlation vector. + public IntPtr pCorrelationVector => info.CorrelationVector; /// /// A numeric scale given to the sync provider to describe the relative priority of one fetch compared to another fetch, in order to /// provide the most responsive experience to the user. The values range from 0 (lowest possible priority) to 15 (highest possible priority). /// - public byte PriorityHint { get; } + public byte PriorityHint => info.PriorityHint; /// Points to a structure that contains the information about the user process that triggers this callback. public CF_PROCESS_INFO? ProcessInfo { get; } /// - public CF_REQUEST_KEY RequestKey { get; } + public CF_REQUEST_KEY RequestKey => info.RequestKey; /// /// A 64 bit file system maintained volume-wide unique ID of the sync root under which the placeholder file/directory to be serviced resides. /// - public long SyncRootFileId { get; } + public long SyncRootFileId => info.SyncRootFileId; /// Points to the opaque blob provided by the sync provider at the sync root registration time. public SafeAllocatedMemoryHandle SyncRootIdentity { get; } @@ -100,31 +98,16 @@ namespace Vanara.PInvoke.Tests /// An opaque handle to the placeholder file/directory to be serviced. The sync provider must pass it back to the CfExecute call in /// order to perform the desired operation on the file/directory. /// - public CF_TRANSFER_KEY TransferKey { get; } + public CF_TRANSFER_KEY TransferKey => info.TransferKey; /// DOS drive letter of the volume in the form of “X:” where X is the drive letter. - public string VolumeDosName { get; } + public string VolumeDosName => info.VolumeDosName; /// GUID name of the volume on which the placeholder file/directory to be serviced resides. It is in the form: "\?\Volume{GUID}". - public string VolumeGuidName { get; } + public string VolumeGuidName => info.VolumeGuidName; /// The serial number of the volume. - public uint VolumeSerialNumber { get; } - - /// The type of operation performed. - public CF_OPERATION_TYPE OperationType { get; set; } - - /// - /// This member is new for Windows 10, version 1803. - /// The current sync status of the platform. - /// - /// The platform queries this information upon any failed operations on a cloud file placeholder. If a structure is available, the - /// platform will use the information provided to construct a more meaningful and actionable message to the user. The platform will - /// keep this information on the file until the last handle on it goes away. If , the platform will clear the - /// previously set sync status, if there is one. - /// - /// - public IntPtr SyncStatus { get; set; } + public uint VolumeSerialNumber => info.VolumeSerialNumber; /// Makes a CF_OPERATION_INFO instance from the properties. /// Type of the operation to set. @@ -141,12 +124,16 @@ namespace Vanara.PInvoke.Tests }; } + /// Information for a placeholder representing a directory. + /// public class PlaceHolderDirectoryInfo : PlaceholderInfo { /// The newly created child placeholder directory is considered to have all of its children present locally. public bool DisableOnDemandPopulation; } + /// Information for a placeholder representing a file. + /// public class PlaceHolderFileInfo : PlaceholderInfo { /// The size of the file, in bytes. @@ -155,6 +142,11 @@ namespace Vanara.PInvoke.Tests /// The newly created placeholder is marked as in-sync. Applicable to both placeholder files and directories. public bool InSync; + /// Creates a placeholder from a file. + /// The file information. + /// if set to , information is synchronized. + /// The relative file path. + /// A platholder for the file. public static PlaceHolderFileInfo CreateFromFile(FileInfo fileInfo, bool inSync = true, string relativeFilePath = null) { return new PlaceHolderFileInfo @@ -171,8 +163,11 @@ namespace Vanara.PInvoke.Tests } } + /// Information about a placeholder. public abstract class PlaceholderInfo { + protected CF_PLACEHOLDER_CREATE_INFO info; + /// The time the file was changed in FILETIME format. public DateTime ChangeTime; @@ -212,12 +207,22 @@ namespace Vanara.PInvoke.Tests public HRESULT Result; } + /// Represents a provider (server) of a cloud file system. + /// internal class CloudSyncProvider : IDisposable { private const string MSSEARCH_INDEX = "SystemIndex"; - + private CF_CALLBACK_REGISTRATION[] callbackTable; + private bool disposed = false; private CF_CONNECTION_KEY? key = null; + /// Initializes a new instance of the class. + /// The path to the sync root. + /// An optional display name that maps to the existing sync root registration. + /// A path to an icon resource for the custom state of a file or folder. + /// The version number of the sync root. + /// The custom properties. + /// Name cannot have spaces. - name public CloudSyncProvider(string syncRootPath, string name, string iconResource = ",0", Version version = null, IEnumerable<(string name, int id)> customProperties = null) { if (name.Contains(" ")) @@ -262,24 +267,31 @@ namespace Vanara.PInvoke.Tests public event EventHandler> ValidateData; + /// Gets an optional display name that maps to the existing sync root registration. public string DisplayName { get; } + /// Gets a path to an icon resource for the custom state of a file or folder. public string IconResource { get; } public IEnumerable PropertyDefinitions { get; } + /// Gets a Uri to a cloud storage recycle bin. public Uri RecycleBinUri { get; } + /// Gets or sets the current status of the sync provider. public CF_SYNC_PROVIDER_STATUS Status { get => CfQuerySyncProviderStatus(key.Value, out var stat).Succeeded ? stat : CF_SYNC_PROVIDER_STATUS.CF_PROVIDER_STATUS_ERROR; set => CfUpdateSyncProviderStatus(key.Value, value); } + /// Gets an identifier for the sync root. public string SyncRootId { get; } + /// Gets the path to the sync root. public string SyncRootPath { get; } + /// Gets the version number of the sync root. public Version Version { get; } public static bool IsSyncRoot(string syncRootPath) @@ -288,12 +300,119 @@ namespace Vanara.PInvoke.Tests return CfGetSyncRootInfoByPath(syncRootPath, CF_SYNC_ROOT_INFO_CLASS.CF_SYNC_ROOT_INFO_BASIC, mem, mem.Size, out var len).Succeeded; } + /// Converts a normal file/directory to a placeholder file/directory using oplocks. + /// Handle to the file or directory to be converted. + /// + /// if set to , the platform marks the converted placeholder as in sync with cloud upon a successful + /// conversion of the file. + /// + /// + /// if set to the platform dehydrates the file after converting it to a placeholder successfully. The caller + /// must acquire an exclusive handle when specifying this flag or data corruptions can occur. + /// + /// + /// A user mode buffer that contains the opaque file or directory information supplied by the caller. Optional if the caller is not + /// dehydrating the file at the same time, or if the caller is converting a directory. Cannot exceed 4KB in size. + /// + /// Length, in bytes, of the FileIdentity. + /// The final USN value after convert actions are performed. + /// + /// To convert a placeholder: + /// + /// + /// + /// The file or directory to be converted must be contained in a registered sync root tree; it can be the sync root directory + /// itself, or any descendant directory; otherwise, the call with be failed with HRESULT(ERROR_CLOUD_FILE_NOT_UNDER_SYNC_ROOT). + /// + /// + /// + /// + /// If dehydration is requested, the sync root must be registered with a valid hydration policy that is not + /// CF_HYDRATION_POLICY_ALWAYS_FULL; otherwise the call will be failed with HRESULT(ERROR_CLOUD_FILE_NOT_SUPPORTED). + /// + /// + /// + /// If dehydration is requested, the placeholder must not be pinned locally or the call with be failed with HRESULT(ERROR_CLOUD_FILE_PINNED). + /// + /// + /// If dehydration is requested, the placeholder must be in sync or the call with be failed with HRESULT(ERROR_CLOUD_FILE_NOT_IN_SYNC). + /// + /// + /// + /// The caller must have WRITE_DATA or WRITE_DAC access to the file or directory to be converted. Otherwise the operation will be + /// failed with HRESULT(ERROR_CLOUD_FILE_ACCESS_DENIED). + /// + /// + /// + /// public int ConvertToPlaceholder(string relativeFilePath, bool inSync = true, bool dehydrate = false, IntPtr fileIdentity = default, uint fileIdentityLength = 0) { - using var hFile = CreateFile(Path.Combine(SyncRootPath, relativeFilePath), Kernel32.FileAccess.FILE_READ_ATTRIBUTES, FileShare.Read, null, FileMode.Open, 0); - return ConvertToPlaceholder(hFile, inSync, dehydrate, fileIdentity, fileIdentityLength); + CfOpenFileWithOplock(relativeFilePath, CF_OPEN_FILE_FLAGS.CF_OPEN_FILE_FLAG_EXCLUSIVE, out var hCfFile).ThrowIfFailed(); + using (hCfFile) + { + CfReferenceProtectedHandle(hCfFile); + try + { + var hFile = CfGetWin32HandleFromProtectedHandle(hCfFile); + return ConvertToPlaceholder(hFile, inSync, dehydrate, fileIdentity, fileIdentityLength); + } + finally + { + CfReleaseProtectedHandle(hCfFile); + } + } } + /// Converts a normal file/directory to a placeholder file/directory. + /// Handle to the file or directory to be converted. + /// + /// if set to , the platform marks the converted placeholder as in sync with cloud upon a successful + /// conversion of the file. + /// + /// + /// if set to the platform dehydrates the file after converting it to a placeholder successfully. The caller + /// must acquire an exclusive handle when specifying this flag or data corruptions can occur. + /// + /// + /// A user mode buffer that contains the opaque file or directory information supplied by the caller. Optional if the caller is not + /// dehydrating the file at the same time, or if the caller is converting a directory. Cannot exceed 4KB in size. + /// + /// Length, in bytes, of the FileIdentity. + /// The final USN value after convert actions are performed. + /// + /// + /// In the file case, the caller must acquire an exclusive handle to the file if it also intends to dehydrate the file at the same + /// time or data corruption can occur. To minimize the impact on user applications it is highly recommended that the caller obtain + /// the exclusiveness using proper oplocks (via CfOpenFileWithOplock) as opposed to using a share-nothing handle. + /// + /// To convert a placeholder: + /// + /// + /// + /// The file or directory to be converted must be contained in a registered sync root tree; it can be the sync root directory + /// itself, or any descendant directory; otherwise, the call with be failed with HRESULT(ERROR_CLOUD_FILE_NOT_UNDER_SYNC_ROOT). + /// + /// + /// + /// + /// If dehydration is requested, the sync root must be registered with a valid hydration policy that is not + /// CF_HYDRATION_POLICY_ALWAYS_FULL; otherwise the call will be failed with HRESULT(ERROR_CLOUD_FILE_NOT_SUPPORTED). + /// + /// + /// + /// If dehydration is requested, the placeholder must not be pinned locally or the call with be failed with HRESULT(ERROR_CLOUD_FILE_PINNED). + /// + /// + /// If dehydration is requested, the placeholder must be in sync or the call with be failed with HRESULT(ERROR_CLOUD_FILE_NOT_IN_SYNC). + /// + /// + /// + /// The caller must have WRITE_DATA or WRITE_DAC access to the file or directory to be converted. Otherwise the operation will be + /// failed with HRESULT(ERROR_CLOUD_FILE_ACCESS_DENIED). + /// + /// + /// + /// public int ConvertToPlaceholder(HFILE fileHandle, bool inSync = true, bool dehydrate = false, IntPtr fileIdentity = default, uint fileIdentityLength = 0) { var flags = (inSync ? CF_CONVERT_FLAGS.CF_CONVERT_FLAG_MARK_IN_SYNC : 0) | (dehydrate ? CF_CONVERT_FLAGS.CF_CONVERT_FLAG_DEHYDRATE : 0); @@ -301,6 +420,11 @@ namespace Vanara.PInvoke.Tests return usn; } + /// Creates a new placeholder files under a sync root tree. + /// The relative file path of the file. + /// The file information of the file. + /// if set to , the file is synchronized. + /// The final USN value for the file. public int CreatePlaceholderFromFile(string relativeFilePath, FileInfo fileInfo, bool inSync = true) { using var pRelativeName = new SafeCoTaskMemString(relativeFilePath); @@ -312,6 +436,23 @@ namespace Vanara.PInvoke.Tests return ph[0].CreateUsn; } + /// Creates one or more new placeholder files or directories under a sync root tree. + /// + /// On successful creation, the PlaceholderArray contains the final USN value and a STATUS_OK message. On return, this array + /// contains an HRESULT value describing whether the placeholder was created or not. + /// + /// The number of entries processed, including failed entries. + /// + /// + /// Creating a placeholder with this function is preferred compared to creating a new file with CreateFile and then converting it to + /// a placeholder with CfConvertToPlaceholder; both for efficiency and because it eliminates the time window where the file is not a + /// placeholder. The function can also create multiple files or directories in a batch, which can also be more efficient. + /// + /// + /// This function is useful when performing an initial sync of files or directories from the cloud down to the client, or when + /// syncing down a newly created single file or directory from the cloud. + /// + /// public uint CreatePlaceholders(IList placeholders) { var entries = new CF_PLACEHOLDER_CREATE_INFO[placeholders.Count]; @@ -378,43 +519,52 @@ namespace Vanara.PInvoke.Tests //} } + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { + disposed = true; if (!key.HasValue) return; CfDisconnectSyncRoot(key.Value).ThrowIfFailed(); key = null; UnregisterWithShell(); } - protected virtual void OnCancelFetchData(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(CancelFetchData, CallbackInfo, CallbackParameters); + /// Allows a sync provider to notify the platform of its status. + /// + /// A localized string that is expected to contain more meaningful and actionable information about the file in question. Sync + /// providers are expected to balance the requirement of providing more actionable information and maintaining an as small as + /// possible memory footprint. + /// + /// + /// The use of this parameter is completely up to the sync provider that supports this rich sync status construct. + /// For a particular sync provider, it is expected that there is a 1:1 mapping between the code and the description string. + /// + /// It is recommended that you use the highest bit order to describe the type of error code: 1 for an error-level code, and 0 + /// for an information-level code. + /// + /// NoteCode is opaque to the platform, and is used only for tracking purposes. + /// + public void ReportSyncStatus(string description, uint code) + { + var ssLen = (uint)Marshal.SizeOf(); + uint descLen = (uint)(description.Length + 1) * 2; + var ss = new CF_SYNC_STATUS { StructSize = ssLen + descLen, Code = code, DescriptionLength = descLen, DescriptionOffset = ssLen }; + using var mem = new SafeCoTaskMemStruct(ss, ss.StructSize); + StringHelper.Write(description, ((IntPtr)mem).Offset(Marshal.SizeOf()), out _, true, CharSet.Unicode, descLen); + CfReportSyncStatus(SyncRootPath, mem).ThrowIfFailed(); + } - protected virtual void OnCancelFetchPlaceholders(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(CancelFetchPlaceholders, CallbackInfo, CallbackParameters); - - protected virtual void OnFetchData(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(FetchData, CallbackInfo, CallbackParameters); - - protected virtual void OnFetchPlaceholders(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(FetchPlaceholders, CallbackInfo, CallbackParameters); - - protected virtual void OnNotifyDehydrate(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyDehydrate, CallbackInfo, CallbackParameters); - - protected virtual void OnNotifyDehydrateCompletion(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyDehydrateCompletion, CallbackInfo, CallbackParameters); - - protected virtual void OnNotifyDelete(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyDelete, CallbackInfo, CallbackParameters); - - protected virtual void OnNotifyDeleteCompletion(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyDeleteCompletion, CallbackInfo, CallbackParameters); - - protected virtual void OnNotifyFileCloseCompletion(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyFileCloseCompletion, CallbackInfo, CallbackParameters); - - protected virtual void OnNotifyFileOpenCompletion(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyFileOpenCompletion, CallbackInfo, CallbackParameters); - - protected virtual void OnNotifyRename(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyRename, CallbackInfo, CallbackParameters); - - protected virtual void OnNotifyRenameCompletion(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyRenameCompletion, CallbackInfo, CallbackParameters); - - protected virtual void OnValidateData(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(ValidateData, CallbackInfo, CallbackParameters); + /// Clears the previously-saved sync status. + public void ClearSyncStatus() => CfReportSyncStatus(SyncRootPath).ThrowIfFailed(); + /// Handles the registered event. + /// The type contained with CF_CALLBACK_PARAMETERS. + /// The handler method. + /// The callback information. + /// The callback parameters. protected virtual void HandleEvent(EventHandler> handler, in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) where T : struct { - if (handler != null) + if (handler != null && !disposed) { var args = new CloudSyncCallbackArgs(CallbackInfo, CallbackParameters); handler.Invoke(this, args); @@ -427,6 +577,55 @@ namespace Vanara.PInvoke.Tests } } + /// Callback to cancel an ongoing placeholder hydration. + protected virtual void OnCancelFetchData(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(CancelFetchData, CallbackInfo, CallbackParameters); + + /// Callback to cancel a request for the contents of placeholder files. + protected virtual void OnCancelFetchPlaceholders(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(CancelFetchPlaceholders, CallbackInfo, CallbackParameters); + + /// Callback to satisfy an I/O request, or a placeholder hydration request. + protected virtual void OnFetchData(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(FetchData, CallbackInfo, CallbackParameters); + + /// Callback to request information about the contents of placeholder files. + protected virtual void OnFetchPlaceholders(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(FetchPlaceholders, CallbackInfo, CallbackParameters); + + /// Callback to inform the sync provider that a placeholder under one of its sync roots is about to be dehydrated. + protected virtual void OnNotifyDehydrate(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyDehydrate, CallbackInfo, CallbackParameters); + + /// Callback to inform the sync provider that a placeholder under one of its sync roots has been successfully dehydrated. + protected virtual void OnNotifyDehydrateCompletion(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyDehydrateCompletion, CallbackInfo, CallbackParameters); + + /// Callback to inform the sync provider that a placeholder under one of its sync roots is about to be deleted. + protected virtual void OnNotifyDelete(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyDelete, CallbackInfo, CallbackParameters); + + /// Callback to inform the sync provider that a placeholder under one of its sync roots has been successfully deleted. + protected virtual void OnNotifyDeleteCompletion(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyDeleteCompletion, CallbackInfo, CallbackParameters); + + /// + /// Callback to inform the sync provider that a placeholder under one of its sync roots that has been previously opened for + /// read/write/delete access is now closed. + /// + protected virtual void OnNotifyFileCloseCompletion(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyFileCloseCompletion, CallbackInfo, CallbackParameters); + + /// + /// Callback to inform the sync provider that a placeholder under one of its sync roots has been successfully opened for + /// read/write/delete access. + /// + protected virtual void OnNotifyFileOpenCompletion(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyFileOpenCompletion, CallbackInfo, CallbackParameters); + + /// + /// Callback to inform the sync provider that a placeholder under one of its sync roots is about to be renamed or moved. + /// + protected virtual void OnNotifyRename(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyRename, CallbackInfo, CallbackParameters); + + /// + /// Callback to inform the sync provider that a placeholder under one of its sync roots has been successfully renamed or moved. + /// + protected virtual void OnNotifyRenameCompletion(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(NotifyRenameCompletion, CallbackInfo, CallbackParameters); + + /// Callback to validate placeholder data. + protected virtual void OnValidateData(in CF_CALLBACK_INFO CallbackInfo, in CF_CALLBACK_PARAMETERS CallbackParameters) => HandleEvent(ValidateData, CallbackInfo, CallbackParameters); + private static void AddFolderToSearchIndexer(string folder) { var url = "file:///" + folder; @@ -440,7 +639,7 @@ namespace Vanara.PInvoke.Tests private void ConnectSyncRootTransferCallbacks() { - CF_CALLBACK_REGISTRATION[] callbackTable = + callbackTable ??= new CF_CALLBACK_REGISTRATION[] { new CF_CALLBACK_REGISTRATION { Type = CF_CALLBACK_TYPE.CF_CALLBACK_TYPE_CANCEL_FETCH_DATA, Callback = OnCancelFetchData }, new CF_CALLBACK_REGISTRATION { Type = CF_CALLBACK_TYPE.CF_CALLBACK_TYPE_CANCEL_FETCH_PLACEHOLDERS, Callback = OnCancelFetchPlaceholders },