Get-IndexedItem.ps1
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] #region define field aliases #Alias definitions take the form AliasName = "Full.Cannonical.Name" ; #Any defined here will be accepted as input field names in -Filter and -OrderBy parameters #and will be added to output objects as AliasProperties. $PropertyAliases = @{Width = "System.Image.HorizontalSize"; Height = "System.Image.VerticalSize"; Name = "System.FileName"; Extension = "System.FileExtension"; CreationTime = "System.DateCreated"; Length = "System.Size"; LastWriteTime = "System.DateModified"; Keyword = "System.Keywords"; Tag = "System.Keywords" CameraMaker = "System.Photo.Cameramanufacturer"; Software = "System.ApplicationName"} $FieldTypes = "System", "Photo", "Image", "Music", "Media", "RecordedTv", "Search", "Audio" #For each of the field types listed above, define a prefix & a list of fields, formatted as "Bare_fieldName1|Bare_fieldName2|Bare_fieldName3" #Anything which appears in FieldTypes must have a prefix and fields definition. #Any definitions which don't appear in FieldTypes will be ignored. #See http://msdn.microsoft.com/en-us/library/dd561977(v=VS.85).aspx for property info. #https://docs.microsoft.com/en-gb/windows/desktop/properties/document-bumper $SystemPrefix = "System." ; $SystemFields = "ItemName|ItemUrl|FileExtension|FileName|FileAttributes|FileOwner|ItemType|ItemTypeText|KindText|Kind|MIMEType|Size|DateModified|DateAccessed|DateImported|DateAcquired|DateCreated|Author|Company|Copyright|Subject|Title|Keywords|Comment|SoftwareUsed|Rating|RatingText|ApplicationName|ItemPathDisplay" $PhotoPrefix = "System.Photo." ; $PhotoFields = "fNumber|ExposureTime|FocalLength|IsoSpeed|PeopleNames|DateTaken|Cameramodel|Cameramanufacturer|orientation"#https://docs.microsoft.com/en-gb/windows/desktop/properties/photo-bumper $ImagePrefix = "System.Image." ; $ImageFields = "Dimensions|HorizontalSize|VerticalSize" #https://docs.microsoft.com/en-gb/windows/desktop/properties/image-bumper $MusicPrefix = "System.Music." ; $MusicFields = "AlbumArtist|AlbumID|AlbumTitle|Artist|BeatsPerMinute|Composer|Conductor|DisplayArtist|Genre|PartOfSet|TrackNumber" #https://docs.microsoft.com/en-gb/windows/desktop/properties/music-bumper $AudioPrefix = "System.Audio." ; $AudioFields = "ChannelCount|EncodingBitrate|PeakValue|SampleRate|SampleSize" $MediaPrefix = "System.Media." ; $MediaFields = "Duration|Year" $RecordedTVPrefix = "System.RecordedTV." ; $RecordedTVFields = "ChannelNumber|EpisodeName|OriginalBroadcastDate|ProgramDescription|RecordingTime|StationName" $SearchPrefix = "System.Search." ; $SearchFields = "AutoSummary|HitCount|Rank|Store" #https://docs.microsoft.com/en-gb/windows/desktop/properties/search-bumper $SelectFields = $FieldTypes | ForEach-Object { (Get-Variable -Name "$($_)Fields").value -split "\|" } $IndexFields = $PropertyAliases.keys + $SelectFields | Sort-Object $IndexFields | ForEach-Object -Begin {$CodeFrag = "public struct IndexedItem`r`n{"} ` -Process {$CodeFrag += " public string $_;`r`n" } -end { Add-Type -TypeDefinition ($CodeFrag + "`r`n}") } #endregion Function Get-IndexedItem { <# .SYNOPSIS Gets files that have been indexed by Windows Desktop Search. .Description Searches the Windows-Index on the local computer, or on a remote file serving computer, looking for matching meta-data properties or performing free text searches against file content. .PARAMETER Filter Alias INCLUDE. A single string containing a WHERE condition, or containing multiple conditions linked with 'AND' or multiple strings, each holding a single condition, which will be joined together. The function tries to add file-name prefixes and single quotes around string values if they are omitted. If no =, >,< , Like or Contains is specified, the terms will be used in a CONTAINS free text search. Syntax Information for CONTAINS and FREETEXT can be found at http://msdn.microsoft.com/en-us/library/dd626247(v=office.11).aspx . .PARAMETER Where Specifies a field-name for a WHERE condition; used in conjunction with -Eq etc. An argument completer allows intellisense to suggest field names. .PARAMETER EQ Combined with -Where to specify "field is equal to ...". An argument completer allows intellisense to suggest possible values for this field. .PARAMETER NE Used with -Where to specify "field is not equal to ...". .PARAMETER GT Used with -Where to specify "field is greater than ...". .PARAMETER LT Used with -Where to specify "field is less than ...". .PARAMETER Like Used with -Where to specify a wild card match "field is like ...". .PARAMETER Contains Used with -Where to specify a free text search. .PARAMETER OrderBy Alias SORT. Either a single string containing one or more ORDER BY conditions, or multiple strings, each with a single condition which will be joined together. .PARAMETER Property By default all possible properties of indexed items are returned. If -Property is specified only the named properties will be returned. .PARAMETER Bare If -Bare is not specified, the command will convert the field-names to easier to read names. Specifying -Bare prevents this conversion and improves performance. .PARAMETER Path A single string containing a path which should be searched. This may be a UNC path to a share on a remote computer. If -Path is not specified, the whole index will be searched. .PARAMETER First Alias TOP. A single integer representing the number of items to be returned. .PARAMETER Value Alias GROUP A single string containing a field-name. If specified the search will return the unique values in this field, instead of objects for matching items. .PARAMETER Recurse If -Path is specified only a single folder is searched unless -Recurse is specified. If -Path is not specified the whole index is searched, and -Recurse is ignored. .PARAMETER List Instead of querying the index, produces a list of known field names, with short names and aliases which may be used instead. .PARAMETER NoFiles Normally if files are found, the command returns a file object with additional properties, which can be piped into commands which accept files. This switch prevents the file being fetched improving performance when the file object is not needed. .PARAMETER OutputVariable PowerShell will normally expand a table into its rows and pass return the rows via the pipeline. OutputVariable works like Common *variable Parameters, it takes a variable name and creates stores the database table in it. This variable is accessible from the calling scope. .EXAMPLE Get-IndexedItem -Filter "Contains(*,'Stingray')", "kind = 'picture'", "keywords='portfolio'" -path ~ -recurse Finds picture files anywhere on the current users profile, which have 'Portfolio' as a keyword tag, and 'stringray' in any indexed property. .EXAMPLE Get-IndexedItem Stingray, kind=picture, keyword=portfolio -recurse ~ | copy -destination e:\ Finds the same pictures as the previous example but uses Keyword as an alias for KeywordS, and leaves the ' marks round Portfolio and Contains() round stingray to be automatically inserted; then copies the found files to drive E:. .EXAMPLE Get-IndexedItem -filter stingray -path OneIndex16:// -recurse Finds OneNote items containing "Stingray". (note, nothing will be found without -recurse and the number after OneIndex is Office-version specific.) .EXAMPLE Get-IndexedItem -filter stingray -path ([system.environment]::GetFolderPath( [system.environment+specialFolder]::MyPictures )) -recurse Looks for pictures with stingray in any indexed property, limiting the scope of the search to the current user's 'My Pictures' folder and its subfolders. .EXAMPLE Get-IndexedItem -Filter "system.kind = 'recordedTV' " -order "System.RecordedTV.RecordingTime" -path "\\atom-engine\users" -recurse | format-list path,title,episodeName,programDescription Finds recorded TV files on a remote server named 'Atom-Engine' which are accessible via a share named 'users'. In this example, field-name prefixes are specified explicitly, instead of letting the function add them. Results are displayed as a list using a subset of the available fields specific to recorded TV. .EXAMPLE Get-IndexedItem -Value "kind" -path \\atom-engine\users -recurse Lists the kinds of files available on the on the 'users' share of a remote server named 'Atom-Engine'. .EXAMPLE Get-IndexedItem -Value "title" -filter "kind=recordedtv" -path \\atom-engine\users -recurse Lists the titles of RecordedTv files available on the on the 'users' share of a remote server named 'Atom-Engine'. .EXAMPLE Start (Get-IndexedItem -path "\\atom-engine\users" -recurse -Filter "title= 'Formula 1' " -order "System.RecordedTV.RecordingTime DESC" -top 1 ) Finds files entitled "Formula 1" on the 'users' share of a remote server named 'Atom-Engine'. Selects the most recent one by TV recording date, and opens it on the local computer. Note: start does not support piped input. .EXAMPLE Get-IndexedItem -Filter "System.Kind = 'Music' AND AlbumArtist like '%' " -NoFiles | Group-Object -NoElement -Property "AlbumArtist" | sort -Descending -property count Gets all music files with an Album Artist set, using a single filter string holding a where condition combining two terms which mix implicit and explicit field prefixes. The result is grouped by Artist and sorted to give popular artist first. .EXAMPLE Get-IndexedItem -Filter "Kind=music","DateModified>'2012-05-31'" -NoFiles | Select-Object -ExpandProperty name Gets Music files which have been modified since a given date and shows just their names. Note the date format is year-month-day; the date is actually a date time, so DataModified= will only match files saved at midnight. .EXAMPLE Get-IndexedItem "itemtype='.mp3'","AlbumArtist like '%'","RatingText <> '1 star'" -NoFiles -orderby encodingBitrate,size -path $null | ft -a AlbumArtist, Title, @{n="size"; e={($_.size/1MB).tostring("n2")+"MB" }},@{n="duration";e={$_.duration.totalseconds.tostring("n0")+"sec"}}, @{n="Byes/Sec";e={($_.size/128/$_.duration.totalSeconds).tostring("n0")+"Kb/s"}},@{n="Encoding";e={($_.EncodingBitrate/1000).tostring("n0")+"Kb/s"}}, @{n="Sample Rate";e={($_.sampleRate/1000).tostring("n1")+"KHz"}} Shows MP3 files with Artist and Track name, showing Size, duration, actual and encoding bits per second and sample rate. .EXAMPLE Get-IndexedItem -path c:\ -recurse -Filter cameramaker=Pentax* -Property focallength | group focallength -no | sort -property @{e={[double]$_.name}} Gets all the items which have a the Camera Maker set to "Pentax", anywhere on the C: drive but ONLY get their FocalLength property, and returns a sorted count of how many of each focal length there are. #> [CmdletBinding(DefaultParameterSetName='Filter')] [OutputType([IndexedItem],[system.io.fileinfo])] Param ( [parameter(ParameterSetName="Filter", Mandatory=$true,Position=0 )] [Alias("Include")][String[]]$Filter , [parameter(Position=1)] [String]$Path , [parameter(ParameterSetName="WhereEQ", Mandatory=$true,Position=0)] [parameter(ParameterSetName="WhereNE", Mandatory=$true,Position=0)] [parameter(ParameterSetName="WhereGT", Mandatory=$true,Position=0)] [parameter(ParameterSetName="WhereLT", Mandatory=$true,Position=0)] [parameter(ParameterSetName="WhereLike", Mandatory=$true,Position=0)] [parameter(ParameterSetName="WhereContains",Mandatory=$true,Position=0)] [String]$Where, [parameter(ParameterSetName="WhereEQ", Mandatory=$true)] [String]$EQ, [parameter(ParameterSetName="WhereNE", Mandatory=$true)] [String]$NE, [parameter(ParameterSetName="WhereGT", Mandatory=$true)] [String]$GT, [parameter(ParameterSetName="WhereLT", Mandatory=$true)] [String]$LT, [parameter(ParameterSetName="WhereLike", Mandatory=$true)] [String]$Like, [parameter(ParameterSetName="WhereContains",Mandatory=$true)] [String]$Contains, [parameter(ParameterSetName="Filter")] [parameter(ParameterSetName="WhereEQ")] [parameter(ParameterSetName="WhereNE")] [parameter(ParameterSetName="WhereGT")] [parameter(ParameterSetName="WhereLT")] [parameter(ParameterSetName="WhereLike")] [parameter(ParameterSetName="WhereContains")] [Alias("Sort")][String[]]$OrderBy = "ITEMURL", [parameter(ParameterSetName="Filter")] [parameter(ParameterSetName="WhereEQ")] [parameter(ParameterSetName="WhereNE")] [parameter(ParameterSetName="WhereGT")] [parameter(ParameterSetName="WhereLT")] [parameter(ParameterSetName="WhereLike")] [parameter(ParameterSetName="WhereContains")] [Alias("Top")][int]$First, [parameter(ParameterSetName="ValueList",Mandatory=$true)] [Alias("Group")][String]$Value, [parameter(ParameterSetName="Filter")] [parameter(ParameterSetName="WhereEQ")] [parameter(ParameterSetName="WhereNE")] [parameter(ParameterSetName="WhereGT")] [parameter(ParameterSetName="WhereLT")] [parameter(ParameterSetName="WhereLike")] [parameter(ParameterSetName="WhereContains")] [Alias("Select")][String[]]$Property, [Switch]$Recurse, [parameter(ParameterSetName="PropertyList",Mandatory=$true)] [Switch]$List, [parameter(ParameterSetName="Filter")] [parameter(ParameterSetName="WhereEQ")] [parameter(ParameterSetName="WhereNE")] [parameter(ParameterSetName="WhereGT")] [parameter(ParameterSetName="WhereLT")] [parameter(ParameterSetName="WhereLike")] [parameter(ParameterSetName="WhereContains")] [Switch]$NoFiles, [parameter(ParameterSetName="Filter")] [parameter(ParameterSetName="WhereEQ")] [parameter(ParameterSetName="WhereNE")] [parameter(ParameterSetName="WhereGT")] [parameter(ParameterSetName="WhereLT")] [parameter(ParameterSetName="WhereLike")] [parameter(ParameterSetName="WhereContains")] [Switch]$Bare, [String]$OutputVariable ) if ($List) { #Output a list of the fields and aliases we currently support. $( foreach ($type in $FieldTypes) { (Get-Variable -name "$($type)Fields").value -split "\|" | select-object @{n="FullName" ;e={(Get-Variable -Name "$($type)prefix").value+$_}}, @{n="ShortName";e={$_}} } ) + ($PropertyAliases.keys | Select-Object @{name="FullName" ;expression={$PropertyAliases[$_]}}, @{name="ShortName";expression={$_}} ) | Sort-Object -Property @{e={$_.FullName -split "\.\w+$"}},"FullName" return } #Make a giant SELECT clause from the field lists; replace "|" with ", " - field prefixes will be inserted later. #There is an extra comma to ensure the last field name is recognized and gets a prefix. This is tidied up later if ($EQ -and $Where) { $Filter += "$Where = $EQ" } if ($NE -and $Where) { $Filter += "$Where <> $NE" } if ($GT -and $Where) { $Filter += "$Where > $GT" } if ($LT -and $Where) { $Filter += "$Where < $LT" } if ($Like -and $Where) { $Filter += "$Where LIKE $Like" } if ($Contains -and $Where) { $Filter += "Contains( $Where , '$Contains' )" } if ($First) {$SQL = "SELECT TOP $First "} else {$SQL = "SELECT "} if ($Property) {$SQL += ($Property -join ", ") + ", "} else {$SQL += ($SelectFields -join ", ") + ", "} #IF a UNC name was specified as the path, build the FROM ... WHERE clause to include the computer name. if ($Path -match "\\\\([^\\]+)\\.") { $SQL += " FROM $($Matches[1]).SYSTEMINDEX WHERE " } else {$SQL += " FROM SYSTEMINDEX WHERE "} #If a WHERE condidtion was provided via -Filter, add it now if ($Filter) { #Convert * to % , unless preceded by open bracket $Filter = $Filter -replace "(?<!\(\s*)\*","%" #Insert quotes where needed any condition specified as "keywords=stingray" is turned into "Keywords = 'stingray' " $Filter = $Filter -replace "\s*(=|<|>|like)\s*([^\''\d][^\d\s\'']*)$" , ' $1 ''$2'' ' # Convert "= 'wildcard'" to "LIKE 'wildcard'" $Filter = $Filter -replace "\s*=\s*(?='.*%.*'\s*$)" ," LIKE " # was "\s*=\s*(?='.+%'\s*$)" ," LIKE " #If a no predicate was specified, use the term in a contains search over all fields. $Filter = ($Filter | ForEach-Object { if ($_ -match "'|=|<|>|like|contains|freetext|is") {$_} else {"Contains(*,'$_')"} }) #if $filter is an array of single conditions join them together with AND $SQL += $Filter -join " AND " } #If a path was given add SCOPE or DIRECTORY to WHERE depending on whether -recurse was specified. if ($Path) { if ($Path -notmatch "\w{4}:") {$Path = "file:" + (resolve-path -path $path).providerPath} # Path has to be in the form "file:C:/users" $Path = $Path -replace "\\","/" if ($SQL -notmatch "WHERE\s$") {$SQL += " AND " } #If the SQL statement doesn't end with "WHERE", add "AND" if ($Recurse) {$SQL += " SCOPE = '$path' " } #INDEX uses SCOPE <folder> for recursive search, else {$SQL += " DIRECTORY = '$path' " } # and DIRECTORY <folder> for non-recursive } if ($Value) { if ($SQL -notmatch "WHERE\s$") {$SQL += " AND " } #If the SQL statement doesn't end with "WHERE", add "AND" $SQL += " $Value is not null" $SQL = $SQL -replace "^SELECT.*?FROM","SELECT $Value, FROM" } #If the SQL statement Still ends with "WHERE" we'd return everything in the index. Bail out instead if ($SQL -match "WHERE\s*$") { Write-Warning -Message "You need to specify either a path , or a filter." ; return} #Add any order-by condition(s). Note there is an extra trailing comma to ensure field names are recognised when prefixes are inserted . if ($Value) {$SQL = "GROUP ON $Value, OVER ( $SQL )"} elseif ($OrderBy) {$SQL += " ORDER BY " + ($OrderBy -join " , " ) + ","} # For each entry in the PROPERTYALIASES Hash table look for the KEY part being used as a field name # and replace it with the associated value. The operation becomes # $SQL -replace "(?<=\s)CreationTime(?=\s*(=|\>|\<|,|Like))","System.DateCreated" # This translates to "Look for 'CreationTime' preceeded by a space and followed by ( optionally ) some spaces, and then # any of '=', '>' , '<', ',' or 'Like' (Looking for these prevents matching if the word is a search term, rather than a field name) # If you find it, replace it with "System.DateCreated" $PropertyAliases.Keys | ForEach-Object { $SQL= $SQL -replace "(?<=\s)$($_)(?=\s*(=|>|<|,|Is|Like))",$PropertyAliases.$_} # Now a similar process for all the field prefixes: this time the regular expression becomes for example, # $SQL -replace "(?<!\s)(?=(Dimensions|HorizontalSize|VerticalSize))","System.Image." # This translates to: "Look for a place which is preceeded by space and followed by 'Dimensions' or 'HorizontalSize' # just select the place (unlike aliases, don't select the fieldname here) and put the prefix at that point. foreach ($type in $FieldTypes) { $Fields = (Get-Variable -Name "$($type)Fields").value $Prefix = (Get-Variable -Name "$($type)Prefix").value $SQL = $SQL -replace "(?<=\s)(?=($Fields)\s*(=|>|<|,|Is|Like))" , $Prefix } # Some commas were put in just to ensure all the field names were found but need to be removed or the SQL won't run $SQL = $SQL -replace "\s*,\s*FROM\s+" , " FROM " $SQL = $SQL -replace "\s*,\s*OVER\s+" , " OVER " $SQL = $SQL -replace "\s*,\s*$" , "" #Finally we get to run the query: result comes back in a dataSet with 1 or more Datatables. Process each dataRow in the first (only) table Write-Debug -Message $SQL $adapter = New-Object -TypeName system.data.oledb.oleDBDataadapter -argumentlist $sql, "Provider=Search.CollatorDSO;Extended Properties=’Application=Windows’;" $ds = New-Object -TypeName system.data.dataset if ($adapter.Fill($ds)) { if ($OutputVariable) {Set-Variable -Scope 2 -Name $OutputVariable -Value $ds.Tables[0] } #Set the variable two scopes up. A script needs 1, in a module it needs 2. else { foreach ($row in $ds.Tables[0]) { #If the dataRow refers to a file output a file obj with extra properties, otherwise output a PSobject if ($Value) {$row | Select-Object -Property @{name=$Value; expression={$_.($ds.Tables[0].columns[0].columnname)}}} elseif ($Bare) {$row} else { if (($row."System.ItemUrl" -match "^file:") -and (-not $NoFiles)) { $obj = (Get-Item -force -LiteralPath (($row."System.ItemUrl" -replace "^file:","") -replace "\/","\")) if (-not $obj) {$obj = New-Object -TypeName psobject } } else { if ($row."System.ItemUrl") { $obj = New-Object -TypeName psobject -Property @{Path = $row."System.ItemUrl"} Add-Member -Force -InputObject $obj -Name "ToString" -MemberType "scriptmethod" -Value {$this.path} } else {$obj = New-Object -TypeName psobject } } if ($obj) { #Add all the the non-null dbColumns removing the prefix from the property name. foreach ($prop in (Get-Member -InputObject $row -MemberType property | Where-Object {$row."$($_.name)" -isnot [system.dbnull] })) { Add-member -ErrorAction "SilentlyContinue" -InputObject $obj -MemberType NoteProperty -Name (($prop.name -split "\." )[-1]) -Value $row."$($prop.name)" } #Add aliases foreach ($prop in ($PropertyAliases.Keys | Where-Object { ($row."$($propertyAliases.$_)" -isnot [System.DBNull] ) -and ($null -ne $row."$($propertyAliases.$_)")})) { Add-Member -ErrorAction "SilentlyContinue" -InputObject $obj -MemberType AliasProperty -Name $prop -Value ($propertyAliases.$prop -split "\." )[-1] } #Overwrite duration as a timespan not as 100ns ticks If ($obj.duration) { $obj.duration =([timespan]::FromMilliseconds($obj.Duration / 10000) )} $obj } } } } } } |