Providing File Data

When a provider first creates a virtualization root it is empty on the local system. That is, none of the items in the backing data store have yet been cached to disk. As items are opened, ProjFS requests information from the provider to allow placeholders for those items to be created in the local file system. As item contents are accessed, ProjFS requests those contents from the provider. The result is that from the user's perspective, virtualized files and directories appear similar to normal files and directories that already reside on the local file system.

Placeholder Creation

When an application attempts to open a handle to a virtualized file, ProjFS calls the PRJ_GET_PLACEHOLDER_INFO_CB callback for each item of the path that does not yet exist on disk. For example, if an application tries to open C:\virtRoot\dir1\dir2\file.txt, but only the path C:\virtRoot\dir1 exists on disk, then the provider will receive a callback for C:\virtRoot\dir1\dir2, then for C:\virtRoot\dir1\dir2\file.txt.

When ProjFS calls the provider's PRJ_GET_PLACEHOLDER_INFO_CB callback, the provider performs the following actions:

  1. The provider determines whether the requested name exists in its backing store. The provider should use PrjFileNameCompare as the comparison routine when searching its backing store to determine whether the requested name exists in the backing store. If it does not, the provider returns HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) from the callback.

  2. If the requested name does exist in the backing store, the provider populates a PRJ_PLACEHOLDER_INFO structure with the item's file system metadata and calls PrjWritePlaceholderInfo to send the data to ProjFS. ProjFS will use that information to create a placeholder in the local file system for the item.

    ProjFS will use whatever FILE_ATTRIBUTE flags the provider sets in the FileBasicInfo.FileAttributes member of PRJ_PLACEHOLDER_INFO except for FILE_ATTRIBUTE_DIRECTORY; it will set the correct value for FILE_ATTRIBUTE_DIRECTORY in the FileBasicInfo.FileAttributes member according to the supplied value of the FileBasicInfo.IsDirectory member.

    If the backing store supports symbolic links, the provider must use PrjWritePlaceholderInfo2 to send the placeholder data to ProjFS. PrjWritePlaceholderInfo2 supports an extra buffer input that allows the provider to specify that the placeholder is a symbolic link and what its target is. It otherwise behaves as described above for PrjWritePlaceholderInfo. The following sample illustrates how to use PrjWritePlaceholderInfo2 to provide support for symbolic links.

    Note that PrjWritePlaceholderInfo2 is supported as of Windows 10, version 2004. A provider should probe for the existence of PrjWritePlaceholderInfo2, for instance by using GetProcAddress.

HRESULT
MyGetPlaceholderInfoCallback(
    _In_ const PRJ_CALLBACK_DATA* callbackData
    )
{
    // MyGetItemInfo is a routine the provider might implement to get
    // information from its backing store for a given file path.  The first
    // parameter is an _In_ parameter that supplies the name to look for.
    // If the item exists the routine provides the file information in the
    // remaining parameters, all of which are annotated _Out_:
    // * 2nd parameter: the name as it appears in the backing store
    // * 3rd-9th parameters: basic file info
    // * 10th parameter: if the item is a symbolic link, a pointer to a 
    //   NULL-terminated string identifying the link's target
    //
    // Note that the routine returns the name that is in the backing
    // store.  This is because the input file path may not be in the same
    // case as what is in the backing store.  The provider should create
    // the placeholder with the name it has in the backing store.
    //
    // Note also that this example does not provide anything beyond basic
    // file information and a possible symbolic link target.
    HRESULT hr;
    WCHAR* backingStoreName = NULL;
    WCHAR* symlinkTarget = NULL;
    PRJ_PLACEHOLDER_INFO placeholderInfo = {};
    hr = MyGetItemInfo(callbackData->FilePathName,
                       &backingStoreName,
                       &placeholderInfo.FileBasicInfo.IsDirectory,
                       &placeholderInfo.FileBasicInfo.FileSize,
                       &placeholderInfo.FileBasicInfo.CreationTime,
                       &placeholderInfo.FileBasicInfo.LastAccessTime,
                       &placeholderInfo.FileBasicInfo.LastWriteTime,
                       &placeholderInfo.FileBasicInfo.ChangeTime,
                       &placeholderInfo.FileBasicInfo.FileAttributes,
                       &symlinkTarget);

    if (FAILED(hr))
    {
        // If callbackData->FilePathName doesn't exist in our backing store then
        // MyGetItemInfo should HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND).
        // If this is some other error, e.g. E_OUTOFMEMORY because MyGetItemInfo
        // couldn't allocate space for backingStoreName, we return that.
        return hr;
    }

    // If the file path is for a symbolic link, pass that in with the placeholder info.
    if (symlinkTarget != NULL)
    {
        PRJ_EXTENDED_INFO extraInfo = {};

        extraInfo.InfoType = PRJ_EXT_INFO_SYMLINK;
        extraInfo.Symlink.TargetName = symlinkTarget;

        // If this returns an error we'll want to return that error from the callback.
        hr = PrjWritePlaceholderInfo2(callbackData->NamespaceVirtualizationContext,
                                      backingStoreName,
                                      &placeholderInfo,
                                      sizeof(placeholderInfo),
                                      &extraInfo);
    }
    else
    {
        // If this returns an error we'll want to return that error from the callback.
        hr = PrjWritePlaceholderInfo(callbackData->NamespaceVirtualizationContext,
                                     backingStoreName,
                                     &placeholderInfo,
                                     sizeof(placeholderInfo));
    }

    free(backingStoreName);

    if (symlinkTarget != NULL)
    {
        free(symlinkTarget);
    }

    return hr;
}

Providing File Contents

When ProjFS needs to ensure that a virtualized file contains data, such as when an application attempts to read from the file, ProjFS calls the PRJ_GET_FILE_DATA_CB callback for that item to request that the provider supply the file's contents. The provider retrieves the file's data from its backing store and uses PrjWriteFileData to send the data to the local file system.

When ProjFS calls this callback, the FilePathName member of the callbackData parameter supplies the name the file had when its placeholder was created. That is, if the file has been renamed since its placeholder was created, the callback supplies the original (pre-rename) name, not the current (post-rename) name. If necessary, the provider can use the VersionInfo member of the callbackData parameter to determine which file's data is being requested.

For more information on how the VersionInfo member of PRJ_CALLBACK_DATA may be used, see the documentation for PRJ_PLACEHOLDER_VERSION_INFO and the Handling View Changes topic.

The provider is allowed to split the range of data requested in the PRJ_GET_FILE_DATA_CB callback into multiple calls to PrjWriteFileData, each supplying a portion of the requested range. However, the provider must supply the entire requested range before completing the callback. For example, if the callback requests 10 MB of data from byteOffset 0 for length 10,485,760, the provider may choose to supply the data in 10 calls to PrjWriteFileData, each sending 1 MB.

The provider is also free to supply more than the requested range, up to the length of the file. The range the provider supplies must cover the requested range. For example, if the callback requests 1 MB of data from byteOffset 4096 for length 1,052,672, and the file has a total size of 10 MB, the provider could choose to return 2 MB of data starting at offset 0.

Buffer Alignment Considerations

ProjFS uses the FILE_OBJECT from the caller who requires the data to write the data to the local file system. However ProjFS cannot control whether that FILE_OBJECT was opened for buffered or unbuffered I/O. If the FILE_OBJECT was opened for unbuffered I/O, reads and writes to the file must adhere to certain alignment requirements. The provider can meet those alignment requirements by doing two things:

  1. Use PrjAllocateAlignedBuffer to allocate the buffer to pass in PrjWriteFileData's buffer parameter.
  2. Ensure that the byteOffset and length parameters of PrjWriteFileData are integer multiples of the storage device's alignment requirement (note that the length parameter does not have to meet this requirement if byteOffset + length is equal to the end of the file). The provider can use PrjGetVirtualizationInstanceInfo to retrieve the storage device's alignment requirement.

ProjFS leaves it up to the provider to calculate proper alignment. This is because when processing a PRJ_GET_FILE_DATA_CB callback the provider may opt to return the requested data across multiple PrjWriteFileData calls, each returning part of the total requested data, before completing the callback.

If the provider is going to use a single call to PrjWriteFileData to either write the entire file, i.e. from byteOffset = 0 to length = size of the file, or to return the exact range requested in the PRJ_GET_FILE_DATA_CB callback, the provider does not have to do any alignment calculations. However, it must still use PrjAllocateAlignedBuffer to ensure that buffer meets the storage device’s alignment requirements.

See the topic File Buffering for more information on buffered vs. unbuffered I/O.

//  BlockAlignTruncate(): Aligns P on the previous V boundary (V must be != 0).
#define BlockAlignTruncate(P,V) ((P) & (0-((UINT64)(V))))

// This sample illustrates both returning the entire requested range in a
// single call to PrjWriteFileData(), and splitting it up into smaller 
// units.  Note that the provider must return all the requested data before
// completing the PRJ_GET_FILE_DATA_CB callback with S_OK.
HRESULT
MyGetFileDataCallback(
    _In_ const PRJ_CALLBACK_DATA* callbackData,
    _In_ UINT64 byteOffset,
    _In_ UINT32 length
    )
{
    HRESULT hr;

    // For the purposes of this sample our provider has a 1 MB limit to how
    // much data it can return at once (perhaps its backing store imposes such
    // a limit).
    UINT64 writeStartOffset;
    UINT32 writeLength;
    if (length <= 1024*1024)
    {
        // The range requested in the callback is less than 1MB, so we can return
        // the data in a single call, without doing any alignment calculations.
        writeStartOffset = byteOffset;
        writeLength = length;
    }
    else
    {
        // The range requested is more than 1MB.  Retrieve the device alignment
        // and calculate a transfer size that conforms to the device alignment and
        // is <= 1MB.
        PRJ_VIRTUALIZATION_INSTANCE_INFO instanceInfo;
        UINT32 infoSize = sizeof(instanceInfo);
        hr = PrjGetVirtualizationInstanceInfo(callbackData->NamespaceVirtualizationContext,
                                              &infoSize,
                                              &instanceInfo);

        if (FAILED(hr))
        {
            return hr;
        }

        // The first transfer will start at the beginning of the requested range,
        // which is guaranteed to have the correct alignment.
        writeStartOffset = byteOffset;

        // Ensure our transfer size is aligned to the device alignment, and is
        // no larger than 1 MB (note this assumes the device alignment is less
        // than 1 MB).
        UINT64 writeEndOffset = BlockAlignTruncate(writeStartOffset + 1024*1024,
                                                   instanceInfo->WriteAlignment);
        assert(writeEndOffset > 0);
        assert(writeEndOffset > writeStartOffset);

        writeLength = writeEndOffset - writeStartOffset;
    }

    // Allocate a buffer that adheres to the needed memory alignment.
    void* writeBuffer = NULL;
    writeBuffer = PrjAllocateAlignedBuffer(callbackData->NamespaceVirtualizationContext,
                                           writeLength);

    if (writeBuffer == NULL)
    {
        return E_OUTOFMEMORY;
    }

    do
    {
        // MyGetFileDataFromStore is a routine the provider might implement to copy
        // data for the specified file from the provider's backing store to a
        // buffer.  The routine finds the file located at callbackData->FilePathName
        // and copies writeLength bytes of its data, starting at writeStartOffset,
        // to the buffer pointed to by writeBuffer.
        hr = MyGetFileDataFromStore(callbackData->FilePathName,
                                    writeStartOffset,
                                    writeLength,
                                    writeBuffer);

        if (FAILED(hr))
        {
            PrjFreeAlignedBuffer(writeBuffer);
            return hr;
        }

        // Write the data to the file in the local file system.
        hr = PrjWriteFileData(callbackData->NamespaceVirtualizationContext,
                              callbackData->DataStreamId,
                              writeBuffer,
                              writeStartOffset,
                              writeLength);

        if (FAILED(hr))
        {
            PrjFreeAlignedBuffer(writeBuffer);
            return hr;
        }

        // The length parameter to the callback is guaranteed to be either
        // correctly aligned or to result in a write to the end of the file.
        length -= writeLength;
        if (length < writeLength)
        {
            writeLength = length;
        }
    }
    while (writeLength > 0);

    PrjFreeAlignedBuffer(writeBuffer);
    return hr;
}