Powershell MTP

Welcome to my new blog!  I wanted my first blog post to be something wholly positive, so I decided to share this Powershell script, in case there is anyone else out there who has struggled with automating the copying of files from their phone.  I find I am regularly cleaning pictures and other files off of my phone, due to the limited space, and for a long time I have wanted to make this process simpler and quicker – a one-click process would be ideal.

After searching online, I discovered this script, but from this page where there is a complaint that this script doesn’t work.  Finding the same problem, I was able to use the provided Powershell and come up with a solution that does the job.

The problem with the script appeared to be in the Get-ChildShellItem function.  The usage explanation in the page says to do the following:

$phone = Get-ChildShellItem | where { $_.name -eq 'a820t' }
Get-ChildShellItem -Path "$($phone.Path)\Phone\DCIM" -Filter '(.jpg)|(.3gp)$'

This just causes the script to hang.  The problem seems to be that even though the Path to the phone can be retrieved, Windows cannot use it to access the folder.

My solution is to get the phone object, then split the path and step down the tree, using the GetFolder member of each item, until we reach the folder object in question.  See the functions, “Get-Phone” and “Get-SubFolder” respectively.

The next problem was how to move the files.  The original script uses the “MoveHere” shell method, passing the path of the source file.  Again, this doesn’t work because Windows doesn’t like the path of the MTP device.  But looking at Microsoft’s website, I noticed that you can pass the folder item (the file COM object) directly.  I do not know if this is the best way to do this (e.g. it can cause a file-copy dialogue to appear), but it works.

So, with that short explanation, here is the script.  An example of how to use it (with a command script) is also included below.

# Windows Powershell Script to move a set of files (based on a filter) from a folder
# on a MTP device (e.g. Android phone) to a folder on a computer, using the Windows Shell.
# By Daiyan Yingyu, 19 March 2018, based on the (non-working) script found here:
#   https://www.pstips.net/access-file-system-against-mtp-connection.html
# as referenced here:
#   https://powershell.org/forums/topic/powershell-mtp-connections/
#
param([string]$phoneName,[string]$sourceFolder,[string]$targetFolder,[string]$filter='(.jpg)|(.mp4)$')

function Get-ShellProxy
{
	if( -not $global:ShellProxy)
	{
		$global:ShellProxy = new-object -com Shell.Application
	}
	$global:ShellProxy
}

function Get-Phone
{
	param($phoneName)
	$shell = Get-ShellProxy
	# 17 (0x11) = ssfDRIVES from the ShellSpecialFolderConstants (https://msdn.microsoft.com/en-us/library/windows/desktop/bb774096(v=vs.85).aspx)
	# => "My Computer" — the virtual folder that contains everything on the local computer: storage devices, printers, and Control Panel.
	# This folder can also contain mapped network drives.
	$shellItem = $shell.NameSpace(17).self
	$phone = $shellItem.GetFolder.items() | where { $_.name -eq $phoneName }
	return $phone
}

function Get-SubFolder
{
	param($parent,[string]$path)
	$pathParts = @( $path.Split([system.io.path]::DirectorySeparatorChar) )
	$current = $parent
	foreach ($pathPart in $pathParts)
	{
		if ($pathPart)
		{
			$current = $current.GetFolder.items() | where { $_.Name -eq $pathPart }
		}
	}
	return $current
}

$phoneFolderPath = $sourceFolder
$destinationFolderPath = $targetFolder
# Optionally add additional sub-folders to the destination path, such as one based on date

$phone = Get-Phone -phoneName $phoneName
$folder = Get-SubFolder -parent $phone -path $phoneFolderPath

$items = @( $folder.GetFolder.items() | where { $_.Name -match $filter } )
if ($items)
{
	$totalItems = $items.count
	if ($totalItems -gt 0)
	{
		# If destination path doesn't exist, create it only if we have some items to copy
		if (-not (test-path $destinationFolderPath) )
		{
			$created = new-item -itemtype directory -path $destinationFolderPath
		}

		Write-Verbose "Processing Path : $phoneName\$phoneFolderPath"
		Write-Verbose "Copying to : $destinationFolderPath"

		$shell = Get-ShellProxy
		$destinationFolder = $shell.Namespace($destinationFolderPath).self
		$count = 0;
		foreach ($item in $items)
		{
			$fileName = $item.Name

			++$count
			$percent = [int](($count * 100) / $totalItems)
			Write-Progress -Activity "Processing Files in $phoneName\$phoneFolderPath" `
				-status "Processing File ${count} / ${totalItems} (${percent}%)" `
				-CurrentOperation $fileName `
				-PercentComplete $percent

			# Check the target file doesn't exist:
			$targetFilePath = join-path -path $destinationFolderPath -childPath $fileName
			if (test-path -path $targetFilePath)
			{
				write-error "Destination file exists - file not moved:`n`t$targetFilePath"
			}
			else
			{
				$destinationFolder.GetFolder.MoveHere($item)
				if (test-path -path $targetFilePath)
				{
					# Optionally do something with the file, such as modify the name (e.g. removed phone-added prefix, etc.)
				}
				else
				{
					write-error "Failed to move file to destination:`n`t$targetFilePath"
				}
			}
		}
	}
}

To call the script you need to supply:

  • the name of the phone (or MTP device).  This is as it appears in Windows Explorer, and is usually the name you have given to the device in its settings
  • the path to the folder in the device.  Usually starts with something like “Phone” or “Internal shared storage”, etc.
  • the fully-qualified path to the folder on the computer to where you want to move the files.
  • a regular expression for all the files you want to move.  For example, for images and videos you might want ‘(.jpg)|(.mp4)$’

You can put the commands into a batch (command) script to move the files from several locations.  Here is an example:

 

@echo off
REM Example use of MoveFromPhone.ps1 to move pictures from a phone onto computer
powershell Set-ExecutionPolicy RemoteSigned -scope currentuser
REM Camera files
powershell.exe "& '%~dp0MoveFromPhone.ps1' -phoneName 'MyPhone' -sourceFolder 'Internal shared storage\DCIM\Camera' -targetFolder 'C:\Users\Public\Pictures\Camera' -filter '(.jpg)|(.mp4)$'"
REM Facebook
powershell.exe "& '%~dp0MoveFromPhone.ps1' -phoneName 'MyPhone' -sourceFolder 'Internal shared storage\DCIM\Facebook' -targetFolder 'C:\Users\Public\Pictures\Facebook' -filter '(.jpg)|(.mp4)$'"
REM Screenshots
powershell.exe "& '%~dp0MoveFromPhone.ps1' -phoneName 'MyPhone' -sourceFolder 'Internal shared storage\DCIM\Screenshots' -targetFolder 'C:\Users\Public\Pictures\Screenshots' -filter '(.png)$'"
pause

Please feel free to comment if you spot any mistakes or have any ideas or suggestions for improvements.

5 thoughts on “Powershell MTP

  1. Thank you. It is a very nice script

    I have a question:
    is it possible to retrieve modifieddate of the picture on the phone?
    because I like to move/copy files after a certain datetime.

    1. Hi Paul,
      Thank you for your comment. I am sorry I didn’t spot it earlier. I haven’t been checking, because most comments are spam. Yours was the only one that even mentioned the content of the post.

      Perhaps you have already got the answer. I did not post the full script that I use, because I wanted to focus on the actual method of copying or moving the files, but in fact my own script does indeed sort pictures by date/time.

      I did not find a way to get the information from the file while it is on the phone, so it is copied or moved to a location on the computer, then the information obtained from the file there, a new name/location determined and the file moved (renamed) to the new location.

      I first attempt to get the date taken property from within the picture itself. If that fails (i.e, no date-taken tag is available), get the modified time of the file (the original modified time should have been maintained by the copy or move, so this does seem to work for me).

      So I have two functions, which take the path of the file on the computer. The first is called, and the second called if the first returns an empty string:

      function GetPictureDateTakenString
      {
      param([string]$filePath)
      $dateTakenString = ”

      $OldErrorActionPref = $ErrorActionPreference
      $ErrorActionPreference = “SilentlyContinue”
      $pic = New-Object System.Drawing.Bitmap($filePath)
      if ($pic)
      {
      $dateTakenBytes = $pic.GetPropertyItem(36867).Value
      if ($dateTakenBytes)
      {
      $dateTakenString = [System.Text.Encoding]::ASCII.GetString($dateTakenBytes)
      $dateTaken = [datetime]::ParseExact($dateTakenString,”yyyy:MM:dd HH:mm:ss`0″,$Null)
      $dateTakenString = $dateTaken.tostring(“yyyyMMdd_HHmmss”)
      }
      $pic.Dispose()
      $pic = $null
      }
      $ErrorActionPreference = $OldErrorActionPref
      return $dateTakenString
      }

      function GetModifiedFileTimeString
      {
      param([string]$filePath)
      $lastModifiedDate = (Get-Item $filePath).LastWriteTime
      $dateTimeModifiedString = $lastModifiedDate.tostring(“yyyyMMdd_HHmmss”)
      return $dateTimeModifiedString
      }

      I hope that’s helpful, even if it is rather late.

  2. Nice! But, having played with the shell object some, I can suggest a slight simplification in the Get-Phone function. The folder object returned the Namespace method is identical to $ShellItem.GetFolder. Try the following:

    $Shell = new-object -comobject Shell.Application
    $Shell.Namespace(17).Items | select name
    $Shell.Namespace(17).self.GetFolder.Items() | select name
    $shell.namespace(17) | gm
    $Shell.Namespace(17).Self.GetFolder | gm

    Peace!

    1. I don’t quite understand what you are trying to achieve there.
      The GetPhone method returns the folder object representing the supplied phone name. By doing “select name”, you’ll only return the name, which we already know. The code I supplied is making similar calls, but returning the phone object for iterating through the folders in the phone.
      Could you explain more where you think it could be simplified?

  3. Is there any possibility to use the “-Recurse”?
    Similar to there:
    “Get-ChildItem $sourceFolder -Recurse -Include *.jpg, *.raw, *.jpeg”
    I starded in this way
    It seemed very simple and it worked until I tested with my phone.
    I also used the folder tree based on the Year and Month.

Leave a Reply

Your email address will not be published. Required fields are marked *