windowspowershellmtp

How to reliably copy items with PowerShell to an mtp device?


I want to be able to copy items (files, folders) from a Windows PC to an MTP device. I want to do it with PowerShell for scripting purposes.

I found this thread: shell32 copyhere not working neither in .Net nor powershell script but the answer there does not help understanding the solution (and was given by the same person who asked that question). Below there is a minimal code example:

param ($itemName)

$shell = New-Object -com Shell.Application

$sourceFolder = $shell.Namespace($(Get-Location).toString()).self
$item = $sourceFolder.GetFolder.Items() | where { $_.Name -eq $itemName }

$mtpDevice = $shell.NameSpace(0x11).items() | where { $_.name -eq 'DUMMY_MODEL' }
$destination = $mtpDevice.GetFolder.items() | where { $_.Name -eq 'Card' }
$destinationFolder=$shell.Namespace($destination).self

$destinationFolder.GetFolder.CopyHere($item)

$shell.open($destinationFolder)
Start-Sleep -s 1

I assume that the item to be copied ($itemName) exists on the Windows machine. I assume that the mtp device is seen as "DUMMY_MODEL" in Windows Explorer and that it contains an empty, top-level folder "Card".

I expect that the line

$destinationFolder.GetFolder.CopyHere($item)

should do the job. But it is not the case. In order for it to work I need to programmatically open the destination folder window and use sleep. Why? The above mentioned thread says that it is in order for the copying thread to finish. Why can't it finish without opening the window? Can this be done without programmatically opening the window? And even if I do open the window and do the sleep, copying does not work 100% reliably. Why?


Solution

  • This is based on my limited knowledge of the Shell.Application object that isn't well documented. If anyone knows better, please feel free to correct.

    The Shell.Application COM object is a copy of the Windows Explorer shell, which performs file operations asynchronously. A new thread is created for each 'copy' action, and the $shell instance manages those threads and receives the events - completed/failed/needs user input/etc. When the script terminates, $shell is cleaned up and can't receive events. The copy threads it created will terminate abnormally, like if you had turned off your computer while copying files from one drive to another.

    Note that CopyHere doesn't raise a completed event. That makes it difficult to capture failure or wait for completion in a script. Ideally you would use Powershell built in Copy-Item instead of the Shell, but it may not be possible with MTP devices.

    The quick fix is probably to add System.Console.ReadKey() like the linked answer, or extend the sleep duration if you want to run unattended.

    Edit: Instead of waiting, you can confirm the existence of each of the files in the destination path: $destinationFolder.GetFolder.Items() contains the list of files in the destination. From this thread WMI is also an option, but the examples are sparse.

    Edit 2: Here's a simple script that copies from hard drive to phone and confirms that it completed:

    param([string]$phoneName = 'Nexus 5X', #beyond compare path: 'mtp://Nexus 5X'
        [string]$sourcePath = 'C:\Temp\',
        [string]$targetPath = '\Card\DCIM\',
        [string]$filter='(.jpg)|(.mp4)$'
    )
    
    $Shell = New-Object -ComObject Shell.Application
    $PhoneObject = $shell.NameSpace(17).self.GetFolder.items() | where { $_.name -eq $phoneName } #gets the phone special folder
    
    $SourceFolder = $Shell.NameSpace($sourcePath).self.GetFolder
    $DestFolder = $Shell.NameSpace((Join-path $PhoneObject.Path $targetPath)).self.GetFolder
    
    foreach($Item in $SourceFolder.Items() | ?{$_.Name -match $filter}){
        $DestFolder.CopyHere($Item)
        Do {
            $CopiedFile = $null 
            $CopiedFile = $DestFolder.Items() | ?{$_.Name -eq $Item.Name}
        }While( ($CopiedFile -eq $null) -and ($null -eq (Sleep -Milliseconds 100)) )#skip sleeping if it's already copied
        Write-Host "Copied $($item.Name)"
    }