7

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?

andrzejtp
  • 73
  • 5
  • [Here's another example](https://blog.daiyanyingyu.uk/2018/03/20/powershell-mtp/) that used the Items() collection to reliably copy. Personally, I'm a fan of Beyond Compare for file management. I use it every day at home and work. Version 4 supports MTP by using the `mtp://[phone name]/Card` syntax. – Rich Moss Apr 12 '19 at 15:23
  • Can Beyond Compare be used for scripting and unattended running? Can it be used permanently without purchasing it (I mean using it legally)? – andrzejtp Apr 15 '19 at 09:48
  • The example you mention does only mtp->pc transfers, but I'm interested in transferring in the opposite direction. – andrzejtp Apr 15 '19 at 09:54
  • I updated my post to add a simpler script that's adapted to your needs. Off-topic: Beyond Compare has command line options to start a sync, but I'm not very familiar with unattended execution and can't provide an example. It's not free, but it's worth far more than the $40 I paid for the perpetual license. – Rich Moss Apr 15 '19 at 18:50

1 Answers1

3

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)"
}
Rich Moss
  • 1,810
  • 1
  • 9
  • 15
  • This answer is much better than the one in the linked thread. You mean to periodically poll with ```$destinationFolder.GetFolder.Items()``` until the desired item appears at the destination (mtp device)? – andrzejtp Apr 15 '19 at 09:46
  • Yes, that's what I meant - the Do-While loop is periodically polling. `$PhoneObject` has a `Path` property with embedded GUIDs that's hard to parse but can be joined with the `$targetPath` to get a COM `FolderItem` object. It would be pretty easy to adapt this script to copy or move from your phone. Please accept this answer if it meets your needs. – Rich Moss Apr 15 '19 at 19:00
  • Thanks for accepting. Would you please test the updated script to make sure it works with nested folders and lots of data? It should be significantly faster than the previous version because it doesn't Sleep if the file is already copied. – Rich Moss Apr 17 '19 at 18:11
  • I had an error in the line with $DestFolder assignment: "Object ... does not have a method with the name GetFolder()" I had to use GetFolder without braces. – Eliahu Sep 11 '20 at 14:41