c++winapi

Enumerate "Internal Storage" File System of devices such as iPhone and MTP-USB digital camera


In order to enumerate the content of connected devices such as iPhones and a digital camera connected via MTP (USB), which aren't simple file drives, I looked at the example WPD API Sample. The relevant file there is ContentEnumeration.cpp.

The code generally is:

HRESULT                         hr = S_OK;
CComPtr<IPortableDeviceContent> pContent;
hr = pDevice->Content(&pContent);
RecursiveEnumerate(WPD_DEVICE_OBJECT_ID, pContent);

with RecursiveEnumerate defined as:

void RecursiveEnumerate(
    PCWSTR                  pszObjectID,
    IPortableDeviceContent* pContent)
{
    CComPtr<IEnumPortableDeviceObjectIDs> pEnumObjectIDs;

    // Print the object identifier being used as the parent during enumeration.
    printf("%ws\n",pszObjectID);


    HRESULT hr = pContent->EnumObjects(0,               // Flags are unused
                                       pszObjectID,     // Starting from the passed in object
                                       NULL,            // Filter is unused
                                       &pEnumObjectIDs);
    // Loop calling Next() while S_OK is being returned.
    while(hr == S_OK)
    {
        DWORD  cFetched = 0;
        PWSTR  szObjectIDArray[NUM_OBJECTS_TO_REQUEST] = {0};
        hr = pEnumObjectIDs->Next(NUM_OBJECTS_TO_REQUEST,   // Number of objects to request on each NEXT call
                                  szObjectIDArray,          // Array of PWSTR array which will be populated on each NEXT call
                                  &cFetched);               // Number of objects written to the PWSTR array
        if (SUCCEEDED(hr))
        {
            // Traverse the results of the Next() operation and recursively enumerate
            // Remember to free all returned object identifiers using CoTaskMemFree()
            for (DWORD dwIndex = 0; dwIndex < cFetched; dwIndex++)
            {
                RecursiveEnumerate(szObjectIDArray[dwIndex],pContent);

                // Free allocated PWSTRs after the recursive enumeration call has completed.
                CoTaskMemFree(szObjectIDArray[dwIndex]);
                szObjectIDArray[dwIndex] = NULL;
            }
        }
    }
}

For simple connected USB file drives, this gives the right file/folder contents, but for iPhones and MTP-USB devices it doesn't. For those devices the output is

o9900
o98FF
o98FE
o98FD
etc.

Windows Explorer allows those devices to be expanded to show an "Internal Storage" or "Device Storage" pseudo-folder, and the child objects of that folder are real folders such as 202403_ and files underneath that can be manipulated. Does anyone know how the real contents can be accessed?


Solution

  • The following example shows how to obtain filenames, instead of object names. In addition to the pContent structure we also need to maintain the pContentProperties structure as below. Filenames are available via pContentProperties as WPD_OBJECT_NAME.

    First, when opening the device and getting its Contents, also get its Content Properties:

    
            /*
               Important Note: include PortableDevice.h, PortableDeviceApi.h, atlbase.h
               Also link "PortableDeviceGuids.lib" via 
                  Project/Properties/Linker/Input/Additional Dependencies
            */ 
            CComPtr<IPortableDevice> pIPortableDevice;
            CComPtr<IEnumPortableDeviceObjectIDs> pEnumObjectIDs;
            CComPtr<IPortableDeviceContent> pContent;
            CComPtr<IPortableDeviceProperties> pContentProperties;
    
            //...
    
            HRESULT hrOpenDevice = pIPortableDevice->Open(pPnpDeviceIDs[dwIndex], pClientInformation);
            if (FAILED(hrOpenDevice)) return; // or throw error
            HRESULT hrContent = pIPortableDevice->Content(&pContent);
            if (FAILED(hrContent)) return; // or throw error
            HRESULT hrContentProperties = pContent->Properties(&pContentProperties);
            if (FAILED(hrContentProperties)) return; // or throw error
            ...
    

    We fetch a batch of items (in this example 10) using EnumObjects from a given source or top-level object, which in this example is s10001, followed by Next. But this only returns the 10 object names. The filenames are then obtained from pContentsProperties->GetValues and pContentsProperties->GetStringValue(WPD_OBJECT_NAME, &pwszObjectName) for the object names.

            PWSTR  pwszObjectName = NULL;
            DWORD  countFetched = 0;
            PWSTR  szObjectIDArray[10];
    
            HRESULT hrEnumBatch = pContent->EnumObjects(
                    0, L"s10001", NULL, &pEnumObjectIDs);
            if (FAILED(hrEnumBatch)) return; // or throw error
            HRESULT hrContentBatch = pEnumObjectIDs->Next(
                    10, szObjectIDArray, &countFetched);
            if (FAILED(hrContentBatch)) return; // or throw error
    
            std::vector<std::wstring> objectNames;
            std::vector<std::wstring> fileNames;
            for (DWORD dwIndex = 0; dwIndex < countFetched; dwIndex++) {
                std::wstring objectName = std::wstring(szObjectIDArray[dwIndex]);
                objectNames.push_back(objectName);
    
                // This is where we get filenames after the object names using Properties
                // Obtain filename from object name using Properties' WPD_OBJECT_NAME
                HRESULT hrPropertiesForContentObject = pContentProperties->GetValues(
                    szObjectIDArray[dwIndex], NULL, &pObjectProperties);
                if (FAILED(hrPropertiesForContentObject)) return; // or throw error
                HRESULT hrPropertiesFileName = pObjectProperties->GetStringValue(
                    WPD_OBJECT_NAME, &pwszObjectName);
                if (FAILED(hrPropertiesFileName)) return; // or throw error
                std::wstring fileName = std::wstring(pwszObjectName);
                fileNames.push_back(fileName);
    
                // Cleanup: pwszObjectName
                CoTaskMemFree(pwszObjectName);
                pwszObjectName = NULL;
                // Cleanup: pObjectProperties
                pObjectProperties = NULL;
            }
    
            // Cleanup: szObjectIDArray structures
            for (DWORD dwIndex = 0; dwIndex < countFetched; dwIndex++) {
                // Clear szObjectIDArray structures
                CoTaskMemFree(szObjectIDArray[dwIndex]);
                szObjectIDArray[dwIndex] = NULL;
            }
    
    
    

    At the end of this loop, objectNames will contain entries like o10000, o10100, o10200, while filenames will contain corresponding entries like 2025-01-01, 2025-02-01, etc., which in my case happen to be folder names.

    Whether a fileName is a folder or not can be tested by whether it itself succeeds with ->Next(..).