1

I'm currently using the below script taken from scriptingguys.com (all credit to them, I just added the bottom 2 lines.) That takes a directory and pulls the file path and comments field from the meta data of the files. Currently the script take's a little over 1.5 minutes to fully run. Is there anyway to speed this up or use a different method to get this data?

I am using this script at the start of some software I have written and 1.5+ minutes is too long for the script to complete. Any thoughts/comments?

Function Get-FileMetaData
{
 Param([string[]]$folder)
 foreach($sFolder in $folder)
  {
   $a = 0
   $objShell = New-Object -ComObject Shell.Application
   $objFolder = $objShell.namespace($sFolder)

   foreach ($File in $objFolder.items())
    { 
     $FileMetaData = New-Object PSOBJECT
      for ($a ; $a  -le 266; $a++)
       { 
         if($objFolder.getDetailsOf($File, $a))
           {
             $hash += @{$($objFolder.getDetailsOf($objFolder.items, $a))  =
                   $($objFolder.getDetailsOf($File, $a)) }
            $FileMetaData | Add-Member $hash
            $hash.clear() 
           } #end if
       } #end for 
     $a=0
     $FileMetaData
    } #end foreach $file
  } #end foreach $sfolder
} #end Get-FileMetaData

$fileMetaData = Get-FileMetaData -folder "C:\Pics" | select 'Name', 'Path', 'Comments' | Sort-Object 'Name'
$fileMetaData | select 'Name', 'Path', 'Comments' | Export-CSV "C:\SCRIPTS\TestDirectory.txt" -encoding Utf8 -NoTypeInformation

Solved by wOxxOm, thanks for your help! Running the below and now working.

Function Get-FileMetaData(
    [string[]]$folders,
    [string[]]$properties
) {
    $shellApp = New-Object -ComObject Shell.Application
    $supportsOrdered = $PSVersionTable.PSVersion.Major -ge 3
    $hash = if ($supportsOrdered) { [ordered]@{} } else { @{} }
    # walk the folders and get the properties by index found above
    $folders | ForEach {
        $shellFolder = $shellApp.namespace($_)
        # get all headers and find their indexes
        $allProps = @{}
        foreach ($index in 0..266) {
            $allProps[$shellFolder.getDetailsOf($shellFolder.items, $index)] = $index
        }
        $shellFolder.items() | ForEach {
            $file = $_
            $hash.Clear()
            foreach ($prop in $properties) { 
                if (($index = $allProps[$prop]) -ne $null) {
                    if ($value = $shellFolder.getDetailsOf($file, $index)) {
                        $hash[$prop] = $value
                    }
                }
            }
            if ($supportsOrdered) {
                [PSCustomObject]$hash
            } else {
                Select $properties -inputObject (
                    New-Object PSObject -Property $hash
                )
            }
        }
    }
}
Get-FileMetaData  -folders 'C:\PICS' -properties Name, Path, Comments | Sort-Object Name |
    select Name, Path, Comments | Export-Csv 'C:\Scripts\test.txt' -encoding UTF8 -NoTypeInformation
Carlos
  • 217
  • 1
  • 4
  • 13
  • Iterating all files for all possible metadata isn't necessary if you are after distinct values, do you know the numbers to go for? –  Mar 21 '17 at 11:54

1 Answers1

2
  1. getDetailsOf is slow, and your code needlessly invokes it 267 times for each file when you only need it for 3 properties.
  2. Collect the property names just once at the start of the function, don't do it on every file
  3. Add-Member is slow. Don't invoke it on every property. Collect all found properties in a hashtable and pass it once to Add-Member or, since you create an empty object, directly to New-Object. To enforce the order of properties use Select-Object in PowerShell 2. Note, PowerShell 3.0 and newer support [ordered] and [PSCustomObject] typecast (see the code below).
  4. Use pipelining instead of foreach statements so that the results appear immediately
  5. Files are already sorted by name, at least on NTFS file system in Windows, so no need to sort.

Function Get-FileMetaData(
    [string[]]$folders,
    [string[]]$properties
) {
    $shellApp = New-Object -ComObject Shell.Application
    # get all headers and find their indexes
    $shellFolder = $shellApp.namespace($folders[0])
    $allProps = @{}
    foreach ($index in 0..266) {
        $allProps[$shellFolder.getDetailsOf($shellFolder.items, $index)] = $index
    }
    $supportsOrdered = $PSVersionTable.PSVersion.Major -ge 3
    $hash = if ($supportsOrdered) { [ordered]@{} } else { @{} }
    # walk the folders and get the properties by index found above
    $folders | ForEach {
        $shellFolder = $shellApp.namespace($_)
        $shellFolder.items() | ForEach {
            $file = $_
            $hash.Clear()
            foreach ($prop in $properties) { 
                if (($index = $allProps[$prop]) -ne $null) {
                    $hash[$prop] = $shellFolder.getDetailsOf($file, $index)
                }
            }
            if ($supportsOrdered) {
                [PSCustomObject]$hash
            } else {
                Select $properties -inputObject (
                    New-Object PSObject -Property $hash
                )
            }
        }
    }
}

Usage example 1:

Get-FileMetaData -folders 'r:\folder1', 'r:\folder2' -properties Name, Path, Comments

Usage example 2:

Get-FileMetaData -folders 'r:\folder1', 'r:\folder2' -properties Name, Path, Comments |
    Export-Csv r:\results.csv -encoding UTF8 -NoTypeInformation

Usage example 3 gets all properties, which is slow:

Get-FileMetaData -folders 'r:\folder1', 'r:\folder2'
wOxxOm
  • 65,848
  • 11
  • 132
  • 136
  • Thank you very much for that, it runs a lot faster. Although, your script doesn't select the Comments field. When run it only pulls through Name and Path? – Carlos Mar 21 '17 at 20:15
  • It does select Comments field in my tests using `powershell -version 2 -file test.ps1`. I don't know why it doesn't for you. Try debugging in PowerShell ISE: set a breakpoint at the start of the function, run the script, then "step" line by line by pressing F10 and look at the variables. There are other debugging IDE for PowerShell like VSCode or Visual Studio. – wOxxOm Mar 21 '17 at 20:19
  • It's possible that Comments field appears dynamically for different directories in which case you might need to move the `# get all headers` part inside the foreach loop for $folders. But since I cannot reproduce the problem, I can't really fix it. – wOxxOm Mar 21 '17 at 20:23
  • I am using version 5.0. Would that affect it in anyway? I'll have a crack at the debugging. Happy for you to TeamViewer and have a look if you like. – Carlos Mar 21 '17 at 20:32
  • I'm using PowerShell 5.1. BTW why did you tag the question with 2.0 then? – wOxxOm Mar 21 '17 at 20:33
  • Error on my behalf. What do you suggest I try? – Carlos Mar 21 '17 at 20:36
  • Attempting to change as described above. Struggling to understand which part of the # get all headers I move to the foreach loop? – Carlos Mar 21 '17 at 20:44
  • Like this https://puu.sh/uTxIT/f368f67ad5.txt However, I think the issue may be solved by adding `| select Name, Path, Comments` after `Get-FileMetaData` and before `Export-Csv` because PowerShell uses the first item's properties to deduce which columns to output. – wOxxOm Mar 21 '17 at 20:52
  • I've removed the part that skipped adding empty properties because it's no longer needed in case we specify the properties explicitly. With this change there's no need for additional `Select`. – wOxxOm Mar 21 '17 at 20:59
  • I tried what you pasted into that text file and it didn't select the comments field. I also tried `Get-FileMetaData | select Name, Path, Comments -folders '\\otahnas02\Bekaert_Deslee' -properties Name, Path, Comments | Export-Csv '\\nzsdf01\SCRIPTS\TestDirectory.txt' -encoding UTF8 -NoTypeInformation` and it complains about the `-folders` parameter? – Carlos Mar 21 '17 at 20:59
  • I have no idea. The error about `-folders` makes no sense. Well, it works for me and I see no mistakes in the code so good luck debugging it :-) – wOxxOm Mar 21 '17 at 21:02
  • No, it doesn't because of invalid syntax. When I said to insert select between the two commands I wasn't expecting this... Here's what you should have tried: `Get-FileMetaData ...parameters... | select ...parameters... | Export-Csv ...parameters...` – wOxxOm Mar 21 '17 at 21:14
  • I managed to get it working by only adding `select | Name, Path, Comments` before the export. Example attached, if you want to update your post I can mark it as the answer. http://pastebin.com/39dxiHac – Carlos Mar 21 '17 at 21:15
  • I've already incorporated this in the updated code, see my comment above. Also `select | Name, Path, Comments` is invalid syntax too. – wOxxOm Mar 21 '17 at 21:19
  • Typo, what I have in the most recent pastebin is what I have running. Appears to be working correctly. – Carlos Mar 21 '17 at 21:22